Repository navigation
Expand file tree
/
Copy pathconfig-schema.mjs
More file actions
91 lines (87 loc) · 8.01 KB
/
Copy pathconfig-schema.mjs
File metadata and controls
91 lines (87 loc) · 8.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
// Single source of truth for every .coalledger.json key (SKILL-REPO-PATTERN
// Layer 3). Flat key list like CoalMine/CoalWash. verify.mjs validates the
// factory template against it; every runtime read goes through clampedRead so
// an out-of-range or wrong-typed value silently degrades to the factory
// default, never misbehaves.
//
// Spec fields:
// key canonical .coalledger.json key
// type 'bool' | 'int' | 'number' | 'enum' | 'strArr'
// min/max bounds for 'int'/'number' (inclusive)
// values allowed values for 'enum' (compared case-insensitively)
// def factory default — the clamp target for any invalid value
// help one-line description
//
// Standard-system keys (language / updateMode / updateCheckDays) keep
// CoalMine's schema shapes byte-for-byte (values + bounds + help) — one flock,
// one color. disabledCanaries stays free-form (membership is not validated:
// pinning the name list here would break forward-compat every time a canary
// ships in a later phase).
export const CONFIG_SCHEMA = [
{ key: 'coalledgerMode', type: 'enum', values: ['auto', 'manual', 'off'], def: 'auto', help: 'Master switch: auto = session-start conductor offers the doc canaries; manual = conductor silent, you invoke canaries yourself; off = fully silent' },
{ key: 'language', type: 'enum', values: ['auto', 'th', 'en', 'ja', 'zh', 'es'], def: 'auto', help: 'Language override for prompts and nudges (auto, th, en, ja, zh, es)' },
{ key: 'disabledCanaries', type: 'strArr', def: [], help: "Canary names to disable (e.g. [\"doc-structure\"]); 'conductor' or 'all' silences the conductor entirely" },
{ key: 'severityFloor', type: 'enum', values: ['low', 'medium', 'high', 'critical'], def: 'low', help: "Report findings at or above this severity (severity is assigned by context in the report step, never mechanically). Fully settable per project, same as quickVsFull below (this key is read by the agent from the project file directly, so the global-vs-project consent clamp in config-load.mjs has no path to it): a cloned repo setting a high project floor is honoured as written and can narrow what a run reports. Unlike quickVsFull's spend, which is backstopped unconditionally at run-time (a separate per-run consent gates it regardless of this file), a narrowed report is inspectable only ON REQUEST via /coalledger:stats -- not visible by default, and stats shows the floor's value, never the count of findings it withheld" },
{ key: 'quickVsFull', type: 'enum', values: ['quick', 'full'], def: 'quick', help: 'Default scan tier for mixed canaries: quick = mechanical layer only (~free, report-only) · full = adds the semantic layer (paid; always a separate consent). Fully settable per project: this key is read by the agent from the project file directly, so the global-vs-project consent clamp in config-load.mjs has no path to it and cannot second-guess it — a project setting full is honoured as written, and the separate per-run consent (not this clamp) is what gates the actual spend' },
{ key: 'docLeak', type: 'bool', def: true, help: 'Enable the #7 doc-leak canary (prose-level public/private boundary; default on — a private-only project turns it off)' },
{ key: 'publicMode', type: 'bool', def: false, help: 'Treat this project’s docs as PUBLIC-facing (raises leak/grounding stakes in severity context)' },
{ key: 'docsDriftNudge', type: 'bool', def: true, help: 'Docs memory-drift advisory: when the agent finishes responding, if documentation files were edited but MEMORY.md has not been updated this session, show ONE quiet line to the user, in the transcript and the interactive UI (the model does not read it) (default on; set false to silence). The DOCS mirror of CoalMine memoryDriftNudge (CODE)' },
{ key: 'emDash', type: 'enum', values: ['unspaced', 'spaced', 'off'], def: 'off', help: "doc-quality's em-dash typography rule (CWK-073). unspaced = the house form word—word, so a SPACED em-dash is the finding; spaced = the inverse; off (factory default) = the rule does not run at all -- a house choice, never forced on another user's repo" },
{ key: 'updateMode', type: 'enum', values: ['ask', 'auto', 'remind', 'off'], def: 'ask', help: 'Self-update behavior at session start (ask, auto, remind, off; default: ask)' },
{ key: 'updateCheckDays', type: 'int', min: 1, max: 365, def: 14, help: 'Days between self-update checks/reminders (default: 14)' },
{ key: 'scanEverything', type: 'bool', def: false, help: 'CWK-057, the owner\'s antivirus-scope law, one flock one colour with CoalMine\'s key of the same name/polarity/default: when true, report findings down to severityFloor "low" for this run regardless of the configured floor (default: off). Positive polarity — true = MORE disclosure, never a double negative. Bounded, not "scan everything": CoalLedger has no automatic scan-scope cut to bypass at all (every canary is agent-invoked, never auto-batch-scanned the way CoalMine\'s touch/stop pipeline is) — this key reaches ONLY severityFloor. It does NOT re-enable a canary disabled via disabledCanaries/docLeak/coalledgerMode (those are ON/OFF consent switches, not scan scope), and does NOT force quickVsFull to "full" (that would escalate PAID spend via a scope key, which hooks-safety.md §9 forbids regardless of polarity). REACH, stated where a reader meets the key (CWK-057 INSPECT MED-1, CWK-064 INSPECT MED-1): the agent now reads it as a global+project merge (project wins when set), never the project file alone — but this is still an instruction the AGENT follows from SKILL.md prose, not a gate the code enforces, so the consent clamp in config-load.mjs still has no path to it, the same way severityFloor is' },
];
// Validate an already-parsed JSON value against a spec.
// Returns an error message fragment ("must be ...") or null when valid.
export function validateValue(spec, v) {
switch (spec.type) {
case 'bool':
return typeof v === 'boolean' ? null : 'must be a boolean';
case 'int':
if (typeof v !== 'number' || !Number.isFinite(v)) return 'must be a finite number';
if (!Number.isInteger(v)) return 'must be an integer';
if (spec.min != null && v < spec.min) return `must be >= ${spec.min}`;
if (spec.max != null && v > spec.max) return `must be <= ${spec.max}`;
return null;
case 'number':
if (typeof v !== 'number' || !Number.isFinite(v)) return 'must be a finite number';
if (spec.min != null && v < spec.min) return `must be >= ${spec.min}`;
if (spec.max != null && v > spec.max) return `must be <= ${spec.max}`;
return null;
case 'enum':
return typeof v === 'string' && spec.values.includes(v.toLowerCase())
? null
: `must be one of: ${spec.values.join(', ')}`;
case 'strArr':
return Array.isArray(v) && v.every((x) => typeof x === 'string')
? null
: 'must be an array of strings';
default:
return `has an unknown spec type '${spec.type}'`;
}
}
// Validate a full parsed config object (unknown keys are reported, never thrown).
export function validateConfig(cfg) {
const errors = [];
if (!cfg || typeof cfg !== 'object' || Array.isArray(cfg)) return ['config must be a JSON object'];
const byKey = new Map(CONFIG_SCHEMA.map((s) => [s.key, s]));
for (const [key, v] of Object.entries(cfg)) {
const spec = byKey.get(key);
if (!spec) { errors.push(`'${key}' not in schema`); continue; }
const err = validateValue(spec, v);
if (err) errors.push(`'${key}' ${err}`);
}
return errors;
}
// Clamped read: return the config value for `key` if valid, else the factory
// default (enums normalized to lowercase). An unknown key returns undefined —
// that is a programming error, surfaced loud in tests, silent at runtime.
export function clampedRead(cfg, key) {
const spec = CONFIG_SCHEMA.find((s) => s.key === key);
if (!spec) return undefined;
const v = cfg ? cfg[key] : undefined;
if (v === undefined || validateValue(spec, v) !== null) return spec.def;
if (spec.type === 'enum') return v.toLowerCase();
if (spec.type === 'strArr') return v.map((x) => x.toLowerCase());
return v;
}