polydeukes 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,34 +1,36 @@
1
1
  /**
2
- * `scaffoldProject` — the project-side scaffold layer (DIST-02 §3-i).
2
+ * `scaffoldProject` — the project-side scaffold layer.
3
3
  *
4
4
  * The half of an installation every distribution path shares: the data config the judges
5
5
  * read, and the telemetry ignore line. What differs between paths is REGISTRATION — how the
6
6
  * agent is told to spawn a judge at all — and that lives one layer up (`initClaudeCode` for
7
- * the `init` path, a manifest for the plugin one). The split is what lets a second path
8
- * reuse this function unchanged instead of scaffolding a config a second time, so nothing
9
- * that registers anything belongs here.
7
+ * the `init` path). The split is what lets a second path reuse this function unchanged
8
+ * instead of scaffolding a config a second time, so nothing that registers anything belongs
9
+ * here.
10
10
  *
11
- * Nothing existing is ever overwritten (§5-d invariant 1): an artifact that is already there
12
- * is reported and left alone. The config existence check reads all three discovery
13
- * candidates rather than the canonical name alone — writing `polydeukes.config.yaml` next to
14
- * a project's `.yml` makes {@link loadConfig} throw on ambiguity, and the fail-closed session
15
- * surface then blocks every call, so the installer itself would be what stopped the project.
16
- * Existence is FILE PRESENCE, never parse success: reading a broken config as "absent" would
17
- * destroy the very file the consumer was midway through fixing, and fixing it is their job.
11
+ * Nothing existing is ever overwritten: an artifact that is already there is reported and
12
+ * left alone. The config existence check reads all three discovery candidates rather than
13
+ * the canonical name alone — writing `polydeukes.config.yaml` next to a project's `.yml`
14
+ * makes {@link loadConfig} throw on ambiguity, and the fail-closed session surface then
15
+ * blocks every call, so the installer itself would be what stopped the project. Existence is
16
+ * FILE PRESENCE, never parse success: reading a broken config as "absent" would destroy the
17
+ * very file the consumer was midway through fixing, and fixing it is their job.
18
18
  */
19
19
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import { CONFIG_FILENAMES } from './load-config.js';
22
- /** `.gitignore` name and the telemetry directory entry it must carry (§3-a). */
22
+ /** `.gitignore` name and the telemetry directory entry it must carry. */
23
23
  const GITIGNORE = '.gitignore';
24
24
  const TELEMETRY_IGNORE_LINE = '.polydeukes/';
25
25
  const GITIGNORE_ENTRY = `# Polydeukes telemetry — local observation data, never committed.\n${TELEMETRY_IGNORE_LINE}\n`;
26
26
  /**
27
- * The generated config: the §3-d minimum protection set and the §3-e witness block, both
28
- * mandatory. Emitted as a literal template rather than serialized from an object because
29
- * the comments ARE the artifact — a consumer's first contact with the protection surface is
30
- * reading why each entry is on it. No `$schema` line: the consumer-side spelling of that
31
- * path is unmeasured (§2-c), and a wrong one loses editor validation silently.
27
+ * The generated config: the minimum protection set and the witness block, both mandatory.
28
+ * Emitted as a literal template rather than serialized from an object because the comments
29
+ * ARE the artifact — a consumer's first contact with the protection surface is reading why
30
+ * each entry is on it.
31
+ *
32
+ * {@link schemaDirective} prepends the `yaml-language-server` line when the schema is where
33
+ * that line would name it.
32
34
  */
33
35
  const GENERATED_CONFIG = `# Polydeukes protection policy — generated by \`pdks init claude-code\`.
34
36
  #
@@ -53,15 +55,12 @@ languages:
53
55
  # layer that can watch it happen.
54
56
  #
55
57
  # A minimum. Add entries as you find you want them.
56
- # node_modules, .claude/node_modules — the resolution path. The generated hook loads the
57
- # judge by package NAME, so every directory Node's ancestor walk can answer from decides
58
- # which code judges this session. A stub planted on that walk replaces the judge outright
59
- # and every call then passes with no telemetry row at all.
58
+ #
59
+ # This list is what blocks. Every \`disciplines:\` entry below lands at advise — a break is
60
+ # recorded and the call goes on — unless the entry itself says \`enforce: block\`.
60
61
  protectedPaths:
61
62
  - '.claude/hooks'
62
63
  - '.claude/settings.json'
63
- - 'node_modules'
64
- - '.claude/node_modules'
65
64
 
66
65
  # The time-boxed witness — the human valve on a blocked verdict. A human types this token so
67
66
  # it stands alone on a message's FIRST line, the window holds for ttlMinutes, then blocking
@@ -75,15 +74,55 @@ protectedPaths:
75
74
  witness:
76
75
  token: 'pdks witness'
77
76
  ttlMinutes: 10
77
+
78
+ # The disciplines you judge by, and the three rungs one climbs — shown as three entries so
79
+ # each rung is a line you can copy. Uncomment to start; ids must stay distinct.
80
+ #
81
+ # disciplines:
82
+ # # A draft: prose only, no predicate. Registered and read, never judged.
83
+ # - id: 'no-todo-in-shipped-code-draft'
84
+ # why: 'a TODO nobody owns is a decision deferred out of sight'
85
+ # draft: true
86
+ #
87
+ # # Promoted to a judgment. Advise is the default — recorded as \`advised\`, never stops
88
+ # # the call — so this line is optional; it is written here to show the rung.
89
+ # - id: 'no-todo-in-shipped-code'
90
+ # why: 'a TODO nobody owns is a decision deferred out of sight'
91
+ # forbid: 'TODO'
92
+ # enforce: advise
93
+ #
94
+ # # The promotion — block is your choice, never the default.
95
+ # - id: 'no-todo-in-shipped-code-blocking'
96
+ # why: 'a TODO nobody owns is a decision deferred out of sight'
97
+ # forbid: 'TODO'
98
+ # enforce: block
78
99
  `;
100
+ /** The schema's path from a config sitting in `projectRoot`, as the directive spells it. */
101
+ const SCHEMA_REL = 'node_modules/polydeukes/dist/schema/polydeukes.schema.json';
102
+ /**
103
+ * The `yaml-language-server` line for a config written into `projectRoot`, or nothing.
104
+ *
105
+ * An editor resolves a relative `$schema` against the config file's own directory, and this
106
+ * command writes the config where it was invoked — so in a monorepo sub-package, whose install
107
+ * hoisted to the workspace root, {@link SCHEMA_REL} names a path that is not there. The check
108
+ * is one look at that exact path: present means the line an editor would follow leads to the
109
+ * schema, absent means it leads nowhere, and a line leading nowhere is worse than none. An
110
+ * unresolvable `$schema` produces no editor error and no validation, and a line the tool wrote
111
+ * is not one its user thinks to audit.
112
+ */
113
+ function schemaDirective(projectRoot) {
114
+ return existsSync(join(projectRoot, SCHEMA_REL))
115
+ ? `# yaml-language-server: $schema=${SCHEMA_REL}\n`
116
+ : '';
117
+ }
79
118
  /**
80
- * Create the project-side artifacts of a Polydeukes installation in `projectRoot` (§3-i),
81
- * skipping whatever is already there.
119
+ * Create the project-side artifacts of a Polydeukes installation in `projectRoot`, skipping
120
+ * whatever is already there.
82
121
  *
83
- * Throws when two or more config spellings coexist (§3-a third disposition) — that tree is
84
- * already stopped, since {@link loadConfig} refuses an ambiguous discovery, and adding
85
- * artifacts to it would wire a judge whose every call fails closed. The throw lands before
86
- * any write, so a human deleting one config is all it takes to reopen the path.
122
+ * Throws when two or more config spellings coexist — that tree is already stopped, since
123
+ * {@link loadConfig} refuses an ambiguous discovery, and adding artifacts to it would wire a
124
+ * judge whose every call fails closed. The throw lands before any write, so a human deleting
125
+ * one config is all it takes to reopen the path.
87
126
  */
88
127
  export function scaffoldProject(projectRoot) {
89
128
  const report = { created: [], skipped: [] };
@@ -96,7 +135,7 @@ export function scaffoldProject(projectRoot) {
96
135
  report.skipped.push(found[0]);
97
136
  }
98
137
  else {
99
- writeFileSync(join(projectRoot, CONFIG_FILENAMES[0]), GENERATED_CONFIG);
138
+ writeFileSync(join(projectRoot, CONFIG_FILENAMES[0]), schemaDirective(projectRoot) + GENERATED_CONFIG);
100
139
  report.created.push(CONFIG_FILENAMES[0]);
101
140
  }
102
141
  // The entry is judged as a whole line. A substring test would treat a commented-out line
@@ -0,0 +1,229 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Polydeukes config",
4
+ "description": "Schema v2 (config as data) — equivalent to the runtime defineConfig() validator; the equivalence is enforced by a contract test.",
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "required": ["languages"],
8
+ "properties": {
9
+ "$schema": {
10
+ "description": "IDE schema reference — accepted and ignored by the runtime validator.",
11
+ "type": "string"
12
+ },
13
+ "languages": {
14
+ "description": "Language axis, first-class. Keys are user values ('typescript', 'python', ...).",
15
+ "type": "object",
16
+ "minProperties": 1,
17
+ "additionalProperties": { "$ref": "#/$defs/languageProfile" }
18
+ },
19
+ "protectedPaths": {
20
+ "description": "Raw protected path patterns — normalization happens at resolve time.",
21
+ "type": "array",
22
+ "items": { "type": "string", "minLength": 1 }
23
+ },
24
+ "adapters": {
25
+ "description": "Adapter namespaces — keys are adapter names, values are each adapter's own settings object (contents not validated by the core schema).",
26
+ "type": "object",
27
+ "additionalProperties": { "type": "object" }
28
+ },
29
+ "telemetry": { "$ref": "#/$defs/telemetry" },
30
+ "disciplines": {
31
+ "description": "User-declared disciplines — validated data the covenant package compiles. Fully identical entries are copy-paste duplicates (by-id uniqueness is enforced by the runtime validator).",
32
+ "type": "array",
33
+ "uniqueItems": true,
34
+ "items": { "$ref": "#/$defs/discipline" }
35
+ },
36
+ "witness": { "$ref": "#/$defs/witness" }
37
+ },
38
+ "$defs": {
39
+ "languageProfile": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "required": ["productionGlob", "testCmd"],
43
+ "properties": {
44
+ "productionGlob": {
45
+ "description": "What counts as production source for this language.",
46
+ "anyOf": [
47
+ { "type": "string", "minLength": 1 },
48
+ {
49
+ "type": "array",
50
+ "minItems": 1,
51
+ "items": { "type": "string", "minLength": 1 }
52
+ }
53
+ ]
54
+ },
55
+ "testCmd": {
56
+ "description": "Shell command template that verifies the given scope — every literal {scope} token is substituted at resolve time.",
57
+ "type": "string",
58
+ "minLength": 1
59
+ }
60
+ }
61
+ },
62
+ "telemetry": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "logPath": {
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"
71
+ }
72
+ }
73
+ },
74
+ "witness": {
75
+ "description": "TTL witness settings — the agreed token and validity window (minutes) consumed by the covenant valve assembly (CONFIG-05).",
76
+ "type": "object",
77
+ "additionalProperties": false,
78
+ "required": ["token", "ttlMinutes"],
79
+ "properties": {
80
+ "token": {
81
+ "description": "The agreed phrase a human types alone on a message's first line — quoting it mid-sentence is a mention, not an invocation (COVENANT-15). Must be non-empty after trimming; the value itself is free.",
82
+ "type": "string",
83
+ "minLength": 1,
84
+ "pattern": "\\S"
85
+ },
86
+ "ttlMinutes": {
87
+ "description": "Validity window in minutes from the user message's timestamp. Must be a finite number > 0.",
88
+ "type": "number",
89
+ "exclusiveMinimum": 0
90
+ }
91
+ }
92
+ },
93
+ "disciplineId": {
94
+ "description": "Unique handle — telemetry label and verdict reason prefix. The three meta-covenant labels are reserved.",
95
+ "type": "string",
96
+ "minLength": 1,
97
+ "not": { "enum": ["self-mod", "shell-mod", "transcript-mod"] }
98
+ },
99
+ "disciplineWhy": {
100
+ "description": "Prose rationale — never judged, and carried into the break message.",
101
+ "type": "string"
102
+ },
103
+ "enforceLevel": {
104
+ "description": "The entry's own level. 'advise' records a break without stopping it; 'block' pins the entry at block. Composes with the surface's level, the lenient side winning; absent means advise; 'block' is the promotion rung.",
105
+ "enum": ["block", "advise"]
106
+ },
107
+ "globOrGlobs": {
108
+ "anyOf": [
109
+ { "type": "string", "minLength": 1 },
110
+ {
111
+ "type": "array",
112
+ "minItems": 1,
113
+ "items": { "type": "string", "minLength": 1 }
114
+ }
115
+ ]
116
+ },
117
+ "requirePrecedent": {
118
+ "description": "Context family — the session evidence one edit requires beforehand. Exactly one evidence key. The core owns and fully validates 'command'; every other key is adapter vocabulary whose value passes through verbatim (unconstrained here, judged by that adapter).",
119
+ "type": "object",
120
+ "minProperties": 1,
121
+ "maxProperties": 1,
122
+ "properties": {
123
+ "command": {
124
+ "description": "Core evidence vocabulary — regex over the shell command strings observed in the session.",
125
+ "type": "string",
126
+ "minLength": 1,
127
+ "format": "regex"
128
+ }
129
+ }
130
+ },
131
+ "discipline": {
132
+ "description": "One discipline entry — exactly one predicate family (forbid | immutable | forbidCommand | requirePrecedent), or a draft (id + why + draft: true, no predicate); in/except scope the forbid and requirePrecedent families, and when scopes requirePrecedent only.",
133
+ "oneOf": [
134
+ {
135
+ "type": "object",
136
+ "additionalProperties": false,
137
+ "required": ["id", "why", "draft"],
138
+ "properties": {
139
+ "id": { "$ref": "#/$defs/disciplineId" },
140
+ "why": {
141
+ "description": "A draft's only body is its prose, so why is required and non-empty here.",
142
+ "type": "string",
143
+ "minLength": 1
144
+ },
145
+ "draft": {
146
+ "description": "Marks an unpromoted entry — declared as prose, never judged, never recorded. Only the literal true exists; false would be dead data synonymous with absence.",
147
+ "const": true
148
+ }
149
+ }
150
+ },
151
+ {
152
+ "type": "object",
153
+ "additionalProperties": false,
154
+ "required": ["id", "forbid"],
155
+ "properties": {
156
+ "id": { "$ref": "#/$defs/disciplineId" },
157
+ "why": { "$ref": "#/$defs/disciplineWhy" },
158
+ "enforce": { "$ref": "#/$defs/enforceLevel" },
159
+ "in": { "$ref": "#/$defs/globOrGlobs" },
160
+ "except": { "$ref": "#/$defs/globOrGlobs" },
161
+ "forbid": {
162
+ "description": "Delta family — string shorthand is equivalent to { added }.",
163
+ "anyOf": [
164
+ { "type": "string", "minLength": 1, "format": "regex" },
165
+ {
166
+ "type": "object",
167
+ "additionalProperties": false,
168
+ "required": ["added"],
169
+ "properties": {
170
+ "added": { "type": "string", "minLength": 1, "format": "regex" }
171
+ }
172
+ }
173
+ ]
174
+ }
175
+ }
176
+ },
177
+ {
178
+ "type": "object",
179
+ "additionalProperties": false,
180
+ "required": ["id", "immutable"],
181
+ "properties": {
182
+ "id": { "$ref": "#/$defs/disciplineId" },
183
+ "why": { "$ref": "#/$defs/disciplineWhy" },
184
+ "enforce": { "$ref": "#/$defs/enforceLevel" },
185
+ "immutable": {
186
+ "description": "Path family — its own glob is the scope.",
187
+ "$ref": "#/$defs/globOrGlobs"
188
+ }
189
+ }
190
+ },
191
+ {
192
+ "type": "object",
193
+ "additionalProperties": false,
194
+ "required": ["id", "forbidCommand"],
195
+ "properties": {
196
+ "id": { "$ref": "#/$defs/disciplineId" },
197
+ "why": { "$ref": "#/$defs/disciplineWhy" },
198
+ "enforce": { "$ref": "#/$defs/enforceLevel" },
199
+ "forbidCommand": {
200
+ "description": "Command family — regex over shell command strings.",
201
+ "type": "string",
202
+ "minLength": 1,
203
+ "format": "regex"
204
+ }
205
+ }
206
+ },
207
+ {
208
+ "type": "object",
209
+ "additionalProperties": false,
210
+ "required": ["id", "requirePrecedent"],
211
+ "properties": {
212
+ "id": { "$ref": "#/$defs/disciplineId" },
213
+ "why": { "$ref": "#/$defs/disciplineWhy" },
214
+ "enforce": { "$ref": "#/$defs/enforceLevel" },
215
+ "in": { "$ref": "#/$defs/globOrGlobs" },
216
+ "except": { "$ref": "#/$defs/globOrGlobs" },
217
+ "when": {
218
+ "description": "Context-family trigger — added-direction delta regex. Absent means every in-scope change triggers.",
219
+ "type": "string",
220
+ "minLength": 1,
221
+ "format": "regex"
222
+ },
223
+ "requirePrecedent": { "$ref": "#/$defs/requirePrecedent" }
224
+ }
225
+ }
226
+ ]
227
+ }
228
+ }
229
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polydeukes",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "A development discipline framework for building alongside an AI coding partner — deterministic covenants, a verifiable work ledger, local memory, and adversarial verification. Alpha.",
5
5
  "keywords": [
6
6
  "harness",
@@ -39,7 +39,8 @@
39
39
  "./claude-code": {
40
40
  "types": "./dist/claude-code-hook.d.ts",
41
41
  "import": "./dist/claude-code-hook.js"
42
- }
42
+ },
43
+ "./schema.json": "./dist/schema/polydeukes.schema.json"
43
44
  },
44
45
  "files": [
45
46
  "dist",
@@ -53,10 +54,10 @@
53
54
  },
54
55
  "dependencies": {
55
56
  "yaml": "2.9.0",
56
- "@polydeukes/adapter-claude-code": "^0.3.0",
57
- "@polydeukes/covenant": "^0.3.0",
58
- "@polydeukes/core": "^0.3.0",
59
- "@polydeukes/adapter-git": "^0.3.0"
57
+ "@polydeukes/adapter-claude-code": "^0.5.0",
58
+ "@polydeukes/adapter-git": "^0.5.0",
59
+ "@polydeukes/covenant": "^0.5.0",
60
+ "@polydeukes/core": "^0.5.0"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/node": "^24.0.0",
@@ -64,7 +65,7 @@
64
65
  "vitest": "^4.1.0"
65
66
  },
66
67
  "scripts": {
67
- "build": "tsc -p tsconfig.build.json && node scripts/copy-docs.mjs",
68
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-docs.mjs && node scripts/copy-schema.mjs",
68
69
  "typecheck": "tsc --noEmit",
69
70
  "test": "vitest run"
70
71
  }