polydeukes 0.5.0 → 0.6.1
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 +54 -74
- package/README.md +55 -86
- package/dist/baseline.d.ts +82 -0
- package/dist/baseline.js +166 -0
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +26 -6
- package/dist/claude-code-hook.d.ts +7 -5
- package/dist/claude-code-hook.js +69 -17
- package/dist/claude-code.d.ts +6 -0
- package/dist/claude-code.js +6 -0
- package/dist/covenant-check.d.ts +10 -17
- package/dist/covenant-check.js +44 -12
- package/dist/covenant-module.d.ts +2 -2
- package/dist/covenant-module.js +9 -1
- package/dist/docs/README.ko.md +60 -0
- package/dist/docs/README.md +64 -0
- package/dist/docs/catalog.json +464 -0
- package/dist/docs/concepts/judgment.ko.md +113 -0
- package/dist/docs/concepts/judgment.md +113 -0
- package/dist/docs/how-to/configure-project.ko.md +99 -0
- package/dist/docs/how-to/configure-project.md +95 -0
- package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
- package/dist/docs/how-to/connect-surfaces.md +118 -0
- package/dist/docs/how-to/write-disciplines.ko.md +124 -0
- package/dist/docs/how-to/write-disciplines.md +125 -0
- package/dist/docs/index.json +2046 -0
- package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
- package/dist/docs/reference/cli/covenant-check.md +98 -0
- package/dist/docs/reference/cli/docs.ko.md +97 -0
- package/dist/docs/reference/cli/docs.md +95 -0
- package/dist/docs/reference/cli/explain.ko.md +79 -0
- package/dist/docs/reference/cli/explain.md +84 -0
- package/dist/docs/reference/cli/init.ko.md +119 -0
- package/dist/docs/reference/cli/init.md +131 -0
- package/dist/docs/reference/configuration/index.ko.md +448 -0
- package/dist/docs/reference/configuration/index.md +474 -0
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
- package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
- package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
- package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
- package/dist/docs/reference/packages/core.ko.md +128 -0
- package/dist/docs/reference/{core.md → packages/core.md} +32 -13
- package/dist/docs/reference/packages/covenant.ko.md +115 -0
- package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
- package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
- package/dist/docs/reference/packages/polydeukes.md +139 -0
- package/dist/docs/troubleshooting.ko.md +142 -0
- package/dist/docs/troubleshooting.md +97 -122
- package/dist/docs/tutorials/first-judgment.ko.md +82 -0
- package/dist/docs/tutorials/first-judgment.md +81 -0
- package/dist/docs-catalog.d.ts +25 -0
- package/dist/docs-catalog.js +450 -0
- package/dist/docs-library.d.ts +23 -0
- package/dist/docs-library.js +347 -0
- package/dist/docs-markdown.d.ts +32 -0
- package/dist/docs-markdown.js +150 -0
- package/dist/docs-query.d.ts +11 -40
- package/dist/docs-query.js +28 -122
- package/dist/docs-types.d.ts +105 -0
- package/dist/docs-types.js +2 -0
- package/dist/explain.d.ts +3 -5
- package/dist/explain.js +48 -47
- package/dist/index.d.ts +2 -3
- package/dist/index.js +1 -2
- package/dist/init-claude-code.d.ts +5 -3
- package/dist/init-claude-code.js +226 -63
- package/dist/init-grok.d.ts +51 -0
- package/dist/init-grok.js +242 -0
- package/dist/load-config.d.ts +5 -1
- package/dist/load-config.js +2 -1
- package/dist/pre-state-reader.d.ts +22 -0
- package/dist/pre-state-reader.js +32 -0
- package/dist/scaffold-project.js +48 -8
- package/dist/schema/polydeukes.schema.json +38 -91
- package/package.json +7 -7
- package/dist/docs/configuration.md +0 -103
- package/dist/docs/installation.md +0 -212
- package/dist/docs/reference/configuration.md +0 -338
- package/dist/docs/reference/polydeukes.md +0 -287
package/dist/docs-query.js
CHANGED
|
@@ -1,138 +1,44 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* indistinguishable from a real one by the time it reaches a reader.
|
|
11
|
-
*
|
|
12
|
-
* Every failure throws so the bin can leave stdout at zero bytes and exit 2. Text written
|
|
13
|
-
* halfway is read as the document and quoted as the document — the same direction the
|
|
14
|
-
* judging surface fails in, for the same reason.
|
|
15
|
-
*/
|
|
16
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
18
|
-
/** The finite query domain — the topic list `pdks docs` prints with no argument. */
|
|
19
|
-
export const TOPICS = ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
20
|
-
/** The mapping, as data: which document answers a topic, and what to read next. */
|
|
21
|
-
const TOPIC_MAP = {
|
|
22
|
-
install: {
|
|
23
|
-
sections: [{ file: 'installation.md' }],
|
|
24
|
-
seeAlso: 'reference/polydeukes.md',
|
|
25
|
-
},
|
|
26
|
-
config: {
|
|
27
|
-
sections: [{ file: 'reference/configuration.md' }],
|
|
28
|
-
seeAlso: 'reference/core.md',
|
|
29
|
-
},
|
|
30
|
-
discipline: {
|
|
31
|
-
sections: [{ file: 'reference/configuration.md', heading: '## `disciplines`' }],
|
|
32
|
-
seeAlso: 'reference/covenant.md',
|
|
33
|
-
},
|
|
34
|
-
covenant: {
|
|
35
|
-
sections: [{ file: 'configuration.md', heading: '## What enforcement looks like' }],
|
|
36
|
-
seeAlso: 'reference/polydeukes.md',
|
|
37
|
-
},
|
|
38
|
-
witness: {
|
|
39
|
-
sections: [
|
|
40
|
-
{ file: 'reference/configuration.md', heading: '## `witness`' },
|
|
41
|
-
{ file: 'troubleshooting.md', heading: '## Opening a blocked call — the witness' },
|
|
42
|
-
],
|
|
43
|
-
seeAlso: 'reference/covenant.md',
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
/** A fenced block opens and closes on a line whose trimmed form starts with the marker. */
|
|
47
|
-
const FENCE = /^(?:`{3,}|~{3,})/;
|
|
48
|
-
/** An ATX heading, and its level in the capture. */
|
|
49
|
-
const HEADING = /^(#{1,6}) /;
|
|
50
|
-
function isTopic(value) {
|
|
51
|
-
return TOPICS.includes(value);
|
|
52
|
-
}
|
|
53
|
-
function headingLevel(line) {
|
|
54
|
-
return HEADING.exec(line)?.[1].length ?? 0;
|
|
1
|
+
import { runDocs } from './docs-library.js';
|
|
2
|
+
export { DOCS_TOPICS as TOPICS } from './docs-types.js';
|
|
3
|
+
/** Answer a legacy topic using the same catalog as search and show. */
|
|
4
|
+
export function queryDocs(spec) {
|
|
5
|
+
return runDocs({
|
|
6
|
+
docsRoot: spec.docsRoot,
|
|
7
|
+
args: spec.topic === undefined ? [] : [spec.topic],
|
|
8
|
+
version: '',
|
|
9
|
+
});
|
|
55
10
|
}
|
|
56
11
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* `heading` is matched by exact string equality. A document that renames its heading kills
|
|
61
|
-
* the query here rather than letting a normalizing matcher hand back a neighbouring section
|
|
62
|
-
* with full confidence.
|
|
63
|
-
*
|
|
64
|
-
* Both scans — for the start and for the boundary — run outside code fences. `#` lines
|
|
65
|
-
* inside a fence are content: the guides really carry them, and a fence-blind scanner cuts
|
|
66
|
-
* the answer at one of those lines while still looking like a success.
|
|
12
|
+
* Extract a section by its exact heading, retaining the original internal helper's contract.
|
|
13
|
+
* Topic retrieval uses stable IDs instead; this helper remains for existing internal callers.
|
|
67
14
|
*/
|
|
68
15
|
export function extractSection(markdown, heading) {
|
|
69
16
|
const lines = markdown.split('\n');
|
|
70
|
-
const level =
|
|
71
|
-
let
|
|
17
|
+
const level = /^(#{1,6}) /.exec(heading)?.[1].length ?? 0;
|
|
18
|
+
let fence;
|
|
72
19
|
let start = -1;
|
|
73
|
-
for (let
|
|
74
|
-
const line = lines[
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
openMarker = trimmed[0];
|
|
82
|
-
}
|
|
83
|
-
else if (trimmed[0] === openMarker) {
|
|
84
|
-
openMarker = undefined;
|
|
85
|
-
}
|
|
20
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
21
|
+
const line = lines[index];
|
|
22
|
+
const marker = /^(`{3,}|~{3,})/.exec(line.trim())?.[1];
|
|
23
|
+
if (marker) {
|
|
24
|
+
if (fence === undefined)
|
|
25
|
+
fence = { marker: marker[0], length: marker.length };
|
|
26
|
+
else if (fence.marker === marker[0] && marker.length >= fence.length)
|
|
27
|
+
fence = undefined;
|
|
86
28
|
continue;
|
|
87
29
|
}
|
|
88
|
-
if (
|
|
30
|
+
if (fence !== undefined)
|
|
89
31
|
continue;
|
|
90
|
-
}
|
|
91
32
|
if (start === -1) {
|
|
92
|
-
if (line === heading)
|
|
93
|
-
start =
|
|
94
|
-
}
|
|
33
|
+
if (line === heading)
|
|
34
|
+
start = index;
|
|
95
35
|
continue;
|
|
96
36
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
37
|
+
const nextLevel = /^(#{1,6}) /.exec(line)?.[1].length ?? 0;
|
|
38
|
+
if (nextLevel > 0 && nextLevel <= level)
|
|
39
|
+
return lines.slice(start, index).join('\n');
|
|
100
40
|
}
|
|
101
|
-
if (start === -1)
|
|
41
|
+
if (start === -1)
|
|
102
42
|
throw new Error(`heading not found: ${heading}`);
|
|
103
|
-
}
|
|
104
|
-
// A section that closes the document ends at end of file; the topic map points at one.
|
|
105
43
|
return lines.slice(start).join('\n');
|
|
106
44
|
}
|
|
107
|
-
function readSection(docsRoot, section) {
|
|
108
|
-
const path = join(docsRoot, section.file);
|
|
109
|
-
if (!existsSync(path)) {
|
|
110
|
-
// Named, never swallowed into empty text: a silently incomplete bundle would otherwise
|
|
111
|
-
// reach a reader as the document itself.
|
|
112
|
-
throw new Error(`bundled document missing: ${section.file}`);
|
|
113
|
-
}
|
|
114
|
-
const markdown = readFileSync(path, 'utf-8');
|
|
115
|
-
return section.heading === undefined ? markdown : extractSection(markdown, section.heading);
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Answer one documentation query.
|
|
119
|
-
*
|
|
120
|
-
* With no topic the result is the listing — how an AI discovers what it may ask at all.
|
|
121
|
-
* With one, it is the mapped section body followed by the bundled reference to read next.
|
|
122
|
-
*/
|
|
123
|
-
export function queryDocs(spec) {
|
|
124
|
-
if (spec.topic === undefined) {
|
|
125
|
-
return { text: `Polydeukes docs:\n${TOPICS.map((t) => ` pdks docs ${t}`).join('\n')}\n` };
|
|
126
|
-
}
|
|
127
|
-
if (!isTopic(spec.topic)) {
|
|
128
|
-
throw new Error(`unknown docs topic '${spec.topic}' — known topics: ${TOPICS.join(', ')}`);
|
|
129
|
-
}
|
|
130
|
-
const entry = TOPIC_MAP[spec.topic];
|
|
131
|
-
const body = entry.sections.map((section) => readSection(spec.docsRoot, section)).join('\n');
|
|
132
|
-
// Resolved against the bundle, not printed as the bare relative name. A reader given
|
|
133
|
-
// `reference/core.md` has to guess where the bundle lives before it can open anything, and
|
|
134
|
-
// this line is the only way most of the reference layer is reached at all. A path a file-read
|
|
135
|
-
// tool can take is the difference between a pointer and a dead end, and a dead end sends the
|
|
136
|
-
// reader back to the web search this command replaces.
|
|
137
|
-
return { text: `${body}\nSee also: ${join(spec.docsRoot, entry.seeAlso)}\n` };
|
|
138
|
-
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/** Languages included in every bundled document. */
|
|
2
|
+
export type DocsLanguage = 'en' | 'ko';
|
|
3
|
+
/** Backward-compatible topic names; their targets belong to the catalog. */
|
|
4
|
+
export declare const DOCS_TOPICS: readonly ['install', 'config', 'discipline', 'covenant', 'witness'];
|
|
5
|
+
/** Source-root-relative Markdown path and localized metadata used for retrieval and ranking. */
|
|
6
|
+
export type DocsTranslation = {
|
|
7
|
+
path: string;
|
|
8
|
+
title: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
terms?: string[];
|
|
11
|
+
};
|
|
12
|
+
/** Catalog entry identifying a document across languages and selecting it for bundling. */
|
|
13
|
+
export type DocsDocument = {
|
|
14
|
+
id: string;
|
|
15
|
+
category: string;
|
|
16
|
+
order: number;
|
|
17
|
+
bundled: boolean;
|
|
18
|
+
en: DocsTranslation;
|
|
19
|
+
ko?: DocsTranslation;
|
|
20
|
+
};
|
|
21
|
+
/** Source inventory shared by the documentation build and offline query commands. */
|
|
22
|
+
export type DocsCatalog = {
|
|
23
|
+
schemaVersion: number;
|
|
24
|
+
documents: DocsDocument[];
|
|
25
|
+
topics: Record<string, DocsTopic>;
|
|
26
|
+
redirects?: DocsRedirect[];
|
|
27
|
+
};
|
|
28
|
+
/** Ordered content references for a legacy topic, plus a document ID for further reading. */
|
|
29
|
+
export type DocsTopic = {
|
|
30
|
+
references: DocsReference[];
|
|
31
|
+
seeAlso: string;
|
|
32
|
+
};
|
|
33
|
+
/** Stable document ID, optionally narrowed to a section; omission selects the whole document. */
|
|
34
|
+
export type DocsReference = {
|
|
35
|
+
documentId: string;
|
|
36
|
+
sectionId?: string;
|
|
37
|
+
};
|
|
38
|
+
/** Move-notice path and canonical destination, both relative to the documentation root. */
|
|
39
|
+
export type DocsRedirect = {
|
|
40
|
+
path: string;
|
|
41
|
+
target: string;
|
|
42
|
+
};
|
|
43
|
+
/** Anchored Markdown slice with zero-based line bounds: inclusive start, exclusive end. */
|
|
44
|
+
export type DocsSection = {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
level: number;
|
|
48
|
+
startLine: number;
|
|
49
|
+
endLine: number;
|
|
50
|
+
text: string;
|
|
51
|
+
};
|
|
52
|
+
/** Bilingual metadata returned for a document included in a completed build. */
|
|
53
|
+
export type DocsBundleDocument = {
|
|
54
|
+
id: string;
|
|
55
|
+
bundled: boolean;
|
|
56
|
+
category: string;
|
|
57
|
+
order: number;
|
|
58
|
+
translations: Record<DocsLanguage, DocsTranslation>;
|
|
59
|
+
};
|
|
60
|
+
/** Indexed section location; the hash covers its entire translated document, not the section. */
|
|
61
|
+
export type DocsBundleSection = {
|
|
62
|
+
documentId: string;
|
|
63
|
+
language: DocsLanguage;
|
|
64
|
+
sectionId: string;
|
|
65
|
+
title: string;
|
|
66
|
+
level: number;
|
|
67
|
+
path: string;
|
|
68
|
+
hash: string;
|
|
69
|
+
};
|
|
70
|
+
/** Persisted bundle metadata and document hashes, reconstructed on load to detect inconsistency. */
|
|
71
|
+
export type DocsIndex = {
|
|
72
|
+
schemaVersion: 1;
|
|
73
|
+
documents: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
bundled: boolean;
|
|
76
|
+
category: string;
|
|
77
|
+
order: number;
|
|
78
|
+
translations: Record<DocsLanguage, DocsTranslation>;
|
|
79
|
+
hashes: Record<DocsLanguage, string>;
|
|
80
|
+
}>;
|
|
81
|
+
sections: DocsBundleSection[];
|
|
82
|
+
};
|
|
83
|
+
/** In-memory catalog and index with loaded Markdown keyed by stable document ID. */
|
|
84
|
+
export type LoadedDocsBundle = {
|
|
85
|
+
catalog: DocsCatalog;
|
|
86
|
+
index: DocsIndex;
|
|
87
|
+
documents: Map<string, LoadedDocsDocument>;
|
|
88
|
+
topics: Record<string, DocsTopic>;
|
|
89
|
+
};
|
|
90
|
+
/** Loaded bilingual Markdown with parsed sections and empty search-term lists where omitted. */
|
|
91
|
+
export type LoadedDocsDocument = {
|
|
92
|
+
id: string;
|
|
93
|
+
bundled: boolean;
|
|
94
|
+
category: string;
|
|
95
|
+
order: number;
|
|
96
|
+
translations: Record<DocsLanguage, {
|
|
97
|
+
path: string;
|
|
98
|
+
title: string;
|
|
99
|
+
summary: string;
|
|
100
|
+
terms: string[];
|
|
101
|
+
markdown: string;
|
|
102
|
+
hash: string;
|
|
103
|
+
sections: DocsSection[];
|
|
104
|
+
}>;
|
|
105
|
+
};
|
package/dist/explain.d.ts
CHANGED
|
@@ -16,11 +16,9 @@ export type ExplainSpec = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
18
18
|
*
|
|
19
|
-
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
23
|
-
* not the session's.
|
|
19
|
+
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
20
|
+
* exists here exactly as it does under a normal hook payload — the path is never read,
|
|
21
|
+
* because the injected transcript is the no-op one.
|
|
24
22
|
*/
|
|
25
23
|
export declare function explain(spec: ExplainSpec): Promise<{
|
|
26
24
|
text: string;
|
package/dist/explain.js
CHANGED
|
@@ -11,39 +11,45 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { resolveGitAdapterSettings } from '@polydeukes/adapter-git';
|
|
14
|
-
import { noopTranscript } from '@polydeukes/core';
|
|
14
|
+
import { AXIS_NAMES, deriveShape, noopTranscript, RELATION_NAMES } from '@polydeukes/core';
|
|
15
15
|
import { assembleSessionRegistrations } from './claude-code-hook.js';
|
|
16
16
|
import { assembleCommitRegistrations } from './covenant-check.js';
|
|
17
17
|
import { loadCovenantModule, resolveCovenantDist } from './covenant-module.js';
|
|
18
18
|
import { loadConfig } from './load-config.js';
|
|
19
19
|
/** The three meta-covenant labels: registrations that protect the judging chain itself. */
|
|
20
20
|
const META_LABELS = new Set(['self-mod', 'shell-mod', 'transcript-mod']);
|
|
21
|
-
/** Normalize an optional glob field to an array (absent = empty). */
|
|
22
|
-
function toGlobs(value) {
|
|
23
|
-
if (value === undefined)
|
|
24
|
-
return [];
|
|
25
|
-
return typeof value === 'string' ? [value] : value;
|
|
26
|
-
}
|
|
27
21
|
/**
|
|
28
|
-
* The
|
|
29
|
-
*
|
|
30
|
-
*
|
|
22
|
+
* The description of a declaration entry: its catalogue coordinate (the mechanism, the axes
|
|
23
|
+
* its sources derive, and the relations its entries decide), then what it routes on, how
|
|
24
|
+
* large its two regex lists are, how many sources it names and how many of those carry
|
|
25
|
+
* each non-file kind, whether it carries a valve, and whether the author left a `why`. An
|
|
26
|
+
* absent scope block admits every world.
|
|
27
|
+
*
|
|
28
|
+
* The axes are derived, never read off the declaration: `loadConfig` has already run the
|
|
29
|
+
* declaration through the validator, so the shape here is the one the catalogue admitted.
|
|
31
30
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
31
|
+
function declareDescription(entry, enforce) {
|
|
32
|
+
const declare = entry.declare;
|
|
33
|
+
const shape = deriveShape(declare);
|
|
34
|
+
const axes = AXIS_NAMES.filter((axis) => shape.axes.has(axis)).join(',');
|
|
35
|
+
const relations = RELATION_NAMES.filter((relation) => shape.relations.has(relation)).join(',');
|
|
36
|
+
const relate = declare.relate.map((relateEntry) => relateEntry.id).join(', ');
|
|
37
|
+
const scope = declare.scope === undefined ? 'scope every world' : `scope ${declare.scope.source}`;
|
|
38
|
+
const include = declare.scope?.include?.length ?? 0;
|
|
39
|
+
const exclude = declare.scope?.exclude?.length ?? 0;
|
|
40
|
+
const bindings = Object.values(declare.sources ?? {});
|
|
41
|
+
// A file binding is the unmarked kind, so only the two the surface has to supply are
|
|
42
|
+
// counted out; a kind nothing binds is left off rather than printed as a zero.
|
|
43
|
+
const kinds = ['sidecar', 'transcript']
|
|
44
|
+
.map((kind) => ({ kind, count: bindings.filter((binding) => kind in binding).length }))
|
|
45
|
+
.filter(({ count }) => count > 0)
|
|
46
|
+
.map(({ kind, count }) => `${kind} ${count}`);
|
|
47
|
+
const counted = kinds.length === 0 ? '' : ` (${kinds.join(', ')})`;
|
|
48
|
+
const sources = `sources ${bindings.length}${counted}`;
|
|
49
|
+
const valve = declare.witness === undefined ? '—' : '✓';
|
|
50
|
+
const why = entry.why === undefined ? '—' : '✓';
|
|
51
|
+
return (`${declare.mechanism} · ${axes} · ${relations} ${relate} · ${scope} · ` +
|
|
52
|
+
`include ${include} · exclude ${exclude} · ${sources} · valve ${valve} · why ${why}${enforce}`);
|
|
47
53
|
}
|
|
48
54
|
/** One rendered line: the kind column, the label column, then the description. */
|
|
49
55
|
function row(kind, label, width, description) {
|
|
@@ -59,8 +65,8 @@ function metaDescription(registration, surface) {
|
|
|
59
65
|
/** Render one surface: its header, its tallies, and one line per registration. */
|
|
60
66
|
function renderSurface(spec) {
|
|
61
67
|
const lines = [];
|
|
62
|
-
const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.
|
|
63
|
-
let
|
|
68
|
+
const width = Math.max(...spec.registrations.map((registration) => registration.label.length), ...spec.drafts.map((draft) => draft.id.length));
|
|
69
|
+
let declare = 0;
|
|
64
70
|
let skip = 0;
|
|
65
71
|
let meta = 0;
|
|
66
72
|
for (const registration of spec.registrations) {
|
|
@@ -75,38 +81,35 @@ function renderSurface(spec) {
|
|
|
75
81
|
lines.push(row('skip', registration.label, width, registration.skip.reason));
|
|
76
82
|
continue;
|
|
77
83
|
}
|
|
78
|
-
|
|
84
|
+
// Every non-meta body registration is one config entry's declaration; a label the config
|
|
85
|
+
// does not carry is an assembly the renderer was never told about.
|
|
79
86
|
const entry = spec.disciplines.find((candidate) => candidate.id === registration.label);
|
|
87
|
+
if (entry === undefined) {
|
|
88
|
+
throw new Error(`explain: registration '${registration.label}' matches no config entry`);
|
|
89
|
+
}
|
|
80
90
|
// The DECLARED level is rendered, never the effective one: an omission stays unmarked
|
|
81
91
|
// so the default and an author's explicit choice of it never read alike, and the
|
|
82
92
|
// surface header states what the omission resolves to.
|
|
83
|
-
const level = entry
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
: `${scopeOf(entry)} · why ${entry.why === undefined ? '—' : '✓'}${level}`;
|
|
87
|
-
lines.push(row('judge', registration.label, width, description));
|
|
88
|
-
}
|
|
89
|
-
for (const entry of spec.excluded) {
|
|
90
|
-
lines.push(row('excluded', entry.id, width, 'forbidCommand — no shell axis on this surface'));
|
|
93
|
+
const level = entry.enforce === undefined ? '' : ` · enforce: ${entry.enforce}`;
|
|
94
|
+
declare += 1;
|
|
95
|
+
lines.push(row('declare', registration.label, width, declareDescription(entry, level)));
|
|
91
96
|
}
|
|
92
97
|
for (const draft of spec.drafts) {
|
|
93
98
|
lines.push(row('draft', draft.id, width, 'unpromoted — no judgment'));
|
|
94
99
|
}
|
|
95
|
-
const tally = ` registrations ${meta +
|
|
96
|
-
`
|
|
100
|
+
const tally = ` registrations ${meta + declare + skip} · ` +
|
|
101
|
+
`declare ${declare} · skip ${skip} · meta ${meta} · draft ${spec.drafts.length}`;
|
|
97
102
|
return [spec.header, tally, ...lines].join('\n');
|
|
98
103
|
}
|
|
99
104
|
/**
|
|
100
105
|
* Read the config at `repoRoot`, assemble both surfaces, and render them.
|
|
101
106
|
*
|
|
102
|
-
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* compiler would report every context entry as a skip, which is the COMMIT surface's answer,
|
|
106
|
-
* not the session's.
|
|
107
|
+
* The session assembly is given a transcript path, so its `transcript-mod` registration
|
|
108
|
+
* exists here exactly as it does under a normal hook payload — the path is never read,
|
|
109
|
+
* because the injected transcript is the no-op one.
|
|
107
110
|
*/
|
|
108
111
|
export async function explain(spec) {
|
|
109
|
-
const { config, configPath } = loadConfig(spec.repoRoot);
|
|
112
|
+
const { config, configPath } = loadConfig({ rootDir: spec.repoRoot });
|
|
110
113
|
// Resolved and imported exactly as the two runners do, so what this renders is the table
|
|
111
114
|
// that would judge: a dist those runners would refuse cannot be rendered as if it worked.
|
|
112
115
|
// The load names the missing module and the recovery command.
|
|
@@ -125,14 +128,13 @@ export async function explain(spec) {
|
|
|
125
128
|
rootDir: spec.repoRoot,
|
|
126
129
|
covenant,
|
|
127
130
|
});
|
|
128
|
-
const gitSettings = resolveGitAdapterSettings(config.adapters?.git);
|
|
131
|
+
const gitSettings = resolveGitAdapterSettings({ namespace: config.adapters?.git });
|
|
129
132
|
const text = [
|
|
130
133
|
`pdks explain — ${configPath}`,
|
|
131
134
|
'',
|
|
132
135
|
renderSurface({
|
|
133
136
|
header: 'surface: session (claude-code hook) · disciplines: advise unless enforce: block · meta: block',
|
|
134
137
|
registrations: session,
|
|
135
|
-
excluded: [],
|
|
136
138
|
drafts,
|
|
137
139
|
disciplines,
|
|
138
140
|
selfModScope: 'common; includes the config file itself',
|
|
@@ -141,7 +143,6 @@ export async function explain(spec) {
|
|
|
141
143
|
renderSurface({
|
|
142
144
|
header: `surface: commit (git pre-commit) · enforce: ${gitSettings.enforce} · disciplines: advise unless enforce: block`,
|
|
143
145
|
registrations: commit,
|
|
144
|
-
excluded: disciplines.filter((entry) => entry.forbidCommand !== undefined),
|
|
145
146
|
drafts,
|
|
146
147
|
disciplines,
|
|
147
148
|
selfModScope: 'common ∪ adapters.git; deduped, includes the config file itself',
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,5 @@
|
|
|
18
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
19
19
|
*/
|
|
20
20
|
export type { ResolvedConfig } from '@polydeukes/core';
|
|
21
|
-
export { type
|
|
22
|
-
export { type
|
|
23
|
-
export { type LoadedConfig, loadConfig } from './load-config.js';
|
|
21
|
+
export { type CheckDomain, type CovenantCheckOutcome, type CovenantCheckSpec, runCovenantCheck, } from './covenant-check.ts';
|
|
22
|
+
export { type LoadConfigSpec, type LoadedConfig, loadConfig } from './load-config.ts';
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,5 @@
|
|
|
17
17
|
*
|
|
18
18
|
* See https://github.com/huskyhoochu/polydeukes
|
|
19
19
|
*/
|
|
20
|
-
export {
|
|
21
|
-
export { runCovenantCheck } from './covenant-check.js';
|
|
20
|
+
export { runCovenantCheck, } from './covenant-check.js';
|
|
22
21
|
export { loadConfig } from './load-config.js';
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* Nothing existing is overwritten. The settings file in particular is merged, never
|
|
16
16
|
* replaced: a consumer's other PreToolUse registrations and permissions are live
|
|
17
|
-
* configuration, and replacing them would disarm every other tool they wired.
|
|
17
|
+
* configuration, and replacing them would disarm every other tool they wired. A grok JSON
|
|
18
|
+
* whose command still names the grok mjs is rewritten to this hook's command so the two
|
|
19
|
+
* installers do not leave two spawn strings.
|
|
18
20
|
*/
|
|
19
|
-
import { type ScaffoldReport } from './scaffold-project.
|
|
21
|
+
import { type ScaffoldReport } from './scaffold-project.ts';
|
|
20
22
|
/**
|
|
21
23
|
* The generated classification skill — the procedure that turns a described problem into a
|
|
22
24
|
* registered entry. A classification procedure an agent never learns about is one that never
|
|
@@ -26,7 +28,7 @@ import { type ScaffoldReport } from './scaffold-project.js';
|
|
|
26
28
|
* lets an advised call through with exit 0, and the reason never reaches the model at call
|
|
27
29
|
* time — reading the telemetry log at task boundaries is the only way it arrives.
|
|
28
30
|
*/
|
|
29
|
-
export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the current families can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nAsk these questions in order; the first yes decides.\n\n| # | Question | Family | Entry key |\n| --- | --- | --- | --- |\n| 1 | Is the promise about content newly ADDED to a file (a pattern that must not appear in new lines)? | delta | `forbid` |\n| 2 | Is it about a whole path that must not be modified or deleted (creating it once stays allowed)? | path | `immutable` |\n| 3 | Is it about the shell command line itself, regardless of files? | command | `forbidCommand` |\n| 4 | Does it require that something else was already done earlier in the session (a tool call that must precede this one)? | context | `requirePrecedent` |\n| 5 | None of the above | \u2014 | `draft: true` (step 4b) |\n\nExisting occurrences are forgiven by the delta family \u2014 only new additions break the promise.\nThat is usually what you want: a discipline adopted today should not indict yesterday's code.\n\nTwo path-shaped promises take no `disciplines:` entry at all. A path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry\nkey. And a path that must never be CREATED is not expressible today: `immutable` allows\ncreation by design, so register that promise as a draft (step 4b).\n\n### 3. Check the observation boundary\n\nTwo kinds of promise cannot be judged here, whatever their shape:\n\n- **Destruction outside the repository** \u2014 judgment observes the project root only. Register\n nothing; use the agent's own permission deny policy for commands like `rm -rf ~`.\n- **Writes by child processes** \u2014 a test runner or script writing files is invisible to the\n session surface, which judges declared tool calls only. Say so to the user; the commit\n surface will still see the result as a staged diff.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n forbid: '\\.only\\('\n enforce: advise\n```\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow `in:` to the files where any\n match IS a break (`in:`/`except:` scope `forbid` and `requirePrecedent` only), or\n accept \"editing this file at all\" as the trigger.\n- **`^` silently disarms on the delta axis.** `forbid` scans whole file content as one\n string, so a line-start anchor matches the first line only \u2014 write `(^|\\n)` there.\n `forbidCommand` judges per line and the whole string, so `^` is safe on that axis.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`forbid` vs `forbidden`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the SHAPE of the promise inside `why`, so the promotion destination is already\nwritten down when a later engine can express it. Name the shape in these terms:\n\n| Shape | The promise reads like |\n| --- | --- |\n| pairing | every element of set A has a counterpart in set B (translation keys, i18n) |\n| companion | if X appears in a unit, Y must appear with it |\n| ordered | a sequence must keep its order (migration journals, version ladders) |\n| fingerprint | a derived artifact must match the hash/stamp of its source |\n| producer-owned | only a designated generator may write this artifact |\n| self-absolution | the party being judged must not write its own verdict field |\n| actor-scope | the same action is fine for one actor and a break for another |\n| phase-order | several precedents, in a fixed order |\n| turn-locality | the evidence must be in the same turn or time window |\n| stated-ground | the reason must be written down before the action |\n| controlled-vocabulary | only an enumerated set of words/values is allowed |\n| naming-convention | names must match a pattern per kind |\n| irreversible-marker | once present, a marker may never be removed |\n| delegation-scope | a delegated task may touch only its granted scope |\n| scope-valve | a defined exception valve, judged rather than ad hoc |\n| claim-verification | the claim must be re-run/measured, not trusted |\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'locale-files-move-together'\n why: 'pairing \u2014 en.json and ko.json must change in the same commit; one side alone is a break'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its family and\nsurfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run its family can\nactually reach:\n\n| Family | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| `forbid` / `immutable` | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidCommand` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `requirePrecedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 this family judges on the session surface only (the commit surface records it `skipped`) |\n\nThen undo the scratch break, repeat the same run, and confirm silence on the\nmust-NOT-match direction. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
|
|
31
|
+
export declare const GENERATED_SKILL = "---\nname: discipline-draft\ndescription: Turn a described discipline problem into a registered entry in polydeukes.config \u2014 a judged entry when the declaration grammar and observed evidence can express it, a draft entry otherwise. Use when the user describes a recurring problem they want promised away (\"I keep...\", \"stop X from happening\", \"we should never...\", \"how do I enforce Y\").\n---\n\n# discipline-draft \u2014 from a problem description to a registered discipline\n\nThis project is judged by Polydeukes. A discipline starts as prose and climbs a ladder \u2014\n`draft` (registered, read, never judged) \u2192 `advise` (judged, recorded, never stops a call) \u2192\n`block` (stops the call; the user's explicit choice, never the default). This skill walks a\nproblem description down to the right first rung and registers it.\n\n## Procedure\n\n### 1. Restate the problem as a promise\n\nRewrite the description as one sentence of the form \"X must not happen\" or \"when A happens,\nB must also happen\". If the sentence needs \"unless\" more than once, split it into two\npromises and classify each separately.\n\n### 2. Classify the shape\n\nChoose from the current catalogue, then check whether the intended surface can supply the\nrequired evidence. A mechanism name constrains the declaration; it does not implement the\npromise by itself. The extracted axes and body relations must be subsets of the admitted\nsets below. Scope filtering is separate from the extracted axes.\n\n| Mechanism | Admitted axes | Body relations | Evidence or structural condition |\n| --- | --- | --- | --- |\n| `pairing` | `world` | `equal` | Compare supplied files or channels; extract keys when values may differ. |\n| `companion` | `change`, `world` | `implies` | Compare presence by key; a multi-file promise needs the observed change set. |\n| `monotonic-order` | `change`, `world` | `ordered` | Extract a sequence with an explicit comparison field; order is not presence. |\n| `fingerprint-sync` | `world` | `equal` | Compare supplied stamps; no generator or compiler runs during judgment. |\n| `producer-owned` | `actor` | `empty`, `nonEmpty` | Requires host-provided actor evidence, not an artifact's self-reported producer. |\n| `self-absolution-ban` | `change` | `unchanged`, `empty` | Extract protected fields or path changes; choose creation/deletion supply explicitly. |\n| `actor-scope` | `actor` | `empty`, `nonEmpty` | Requires a proven actor; a missing actor is not proof of the main session. |\n| `precedent` | `history`, `world` | `nonEmpty` | Requires an observed earlier call in a transcript or supplied channel. |\n| `phase-order` | `history` | `ordered` | Compare observed call ordinals; missing phases need a separate presence promise. |\n| `turn-locality` | `history` | `nonEmpty` | Requires observed turns and time or ordinal boundaries. |\n| `stated-ground` | `history` | `nonEmpty` | Can require recorded text, not establish whether its reasoning is sound. |\n| `controlled-vocabulary` | `change`, `world` | `subset` | Extract values and an explicit allowed set. |\n| `naming` | `change` | `empty`, `nonEmpty` | Scope must read `target.path`; match the intended name pattern. |\n| `added-only` | `change` | `empty` | Compare pre/post extractions and judge only newly added matches. |\n| `one-way-marker` | `change` | `subset` | Existing markers must remain in the extracted post-change set. |\n| `delegated-scope` | \u2014 | \u2014 | Reserved for a definition-time evaluator; not accepted in current declarations. |\n| `scoped-valve` | `change`, `actor`, `world`, `history` | `empty`, `nonEmpty`, `equal`, `subset`, `implies`, `ordered`, `unchanged` | Requires a `witness` block expressing the exception condition. |\n| `forbidden-command` | `change` | `empty` | Scope must read `command`; a text pattern is not shell semantic analysis. |\n\nThese four requests illustrate the classification boundary:\n\n| Request | Classification | Proof |\n| --- | --- | --- |\n| The English and Korean locale files must carry identical keys. | `pairing`, with two supplied files. | An unmatched key breaks; translated values may differ. |\n| Every status must belong to an allowed list. | `controlled-vocabulary`, with a supplied allowed set. | An unknown status breaks; an allowed status passes. |\n| A successful package lookup must precede a manifest edit. | `precedent`, with observed session history. | Failed or absent lookups break; an unavailable transcript is a supply case. |\n| A fresh benchmark must execute during judgment to prove a performance claim. | `draft`: the engine does not execute benchmarks. | Comparing an existing report would be a different promise. |\n\nRun `pdks docs show write-disciplines` for the key-pairing walkthrough and\n`pdks docs show configuration --section disciplines` for the declaration grammar.\nUse `--lang ko` for Korean; these commands read the installed version offline.\n\nAn `added-only` declaration forgives existing occurrences \u2014 only what the edit adds breaks\nthe promise. That is usually what you want: a discipline adopted today should not indict\nyesterday's code.\n\nOne path-shaped promise takes no `disciplines:` entry at all: a path nobody may touch\nbelongs in the top-level `protectedPaths:` list \u2014 its own config block, never an entry key.\n\n### 3. Check the observation boundary\n\nDo not confuse an expressible relation with available evidence:\n\n- **Files outside the repository** \u2014 file-change protection observes the project root.\n Use the host's permission policy for comprehensive protection outside it. A command-text\n pattern may recognize a particular string, but does not observe all resulting writes.\n- **Writes by child processes** \u2014 arbitrary writes inside a test runner or script are not\n individually observed by the session surface. A commit comparison can observe the resulting\n files when they enter its selected diff; it does not recover the originating tool history.\n- **Missing history or actor channels** \u2014 choose the declaration's supply policy explicitly.\n Commit observations have no session transcript; `supply: pass` records a skip, not success.\n- **Fresh execution or semantic proof** \u2014 the engine compares supplied evidence. It does not\n run a new benchmark or prove that a written explanation is true. Preserve that unmet promise\n as a draft rather than silently replacing it with a weaker text check.\n\n### 4a. Expressible now \u2014 register a judged entry\n\nAdd the entry to the `disciplines:` array in `polydeukes.config.yaml`. Advise is the default\nlanding \u2014 a break is recorded as `advised` and the call goes on \u2014 and the `enforce: advise`\nline below only spells that default out. NEVER write `enforce: block` from this skill:\npromotion to block is the user's own choice, made after the advise measurements have been\nread.\n\nThe examples below are whole documents, so `languages:` \u2014 the schema's one required block \u2014\nappears alongside the entry; in a config that already has one, copy the entry only.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-focused-tests'\n why: 'a committed .only silently shrinks the suite to one test'\n declare:\n mechanism: 'added-only'\n scope: { source: 'target.path', include: ['^src/'] }\n supply: { pre: 'empty', post: 'empty' }\n extract:\n before:\n - { op: 'source', of: 'pre' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n after:\n - { op: 'source', of: 'post' }\n - { op: 'lines' }\n - { op: 'keyByPattern', re: '(\\.only\\()' }\n added:\n - { op: 'onlyIn', of: 'after', notIn: 'before' }\n relate:\n - id: 'nothing-added'\n relation: { op: 'empty', of: 'added' }\n message: 'adds {key}: {value}'\n enforce: advise\n```\n\nA command-line ban reads the fixed source `command` and scopes on it \u2014 the scope is part of\nthe mechanism's shape, so a `forbidden-command` entry without it is refused at load time:\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'no-force-push'\n why: 'a force push rewrites history nobody reviewed'\n declare:\n mechanism: 'forbidden-command'\n scope: { source: 'command' }\n extract:\n hits:\n - { op: 'source', of: 'command' }\n - { op: 'lines' }\n - { op: 'matches', re: 'git push\\b.*--force(?![\\w-])' }\n relate:\n - { id: 'no-force', relation: { op: 'empty', of: 'hits' }, message: '{value}' }\n enforce: advise\n```\n\nThe following examples implement the first three classification cases. Both locale files and\nthe allowed-status file must exist and contain valid JSON. File bindings use the proposed\ncontents for a file changed by the current observation, not a second stale disk read.\n\n```yaml\nlanguages:\n json:\n productionGlob: 'locales/**/*.json'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'locale-key-parity'\n why: 'the ko and en locales must carry the same keys'\n declare:\n mechanism: 'pairing'\n scope: { source: 'target.path', include: ['^locales/(ko|en)[.]json$'] }\n sources:\n ko: { file: 'locales/ko.json' }\n en: { file: 'locales/en.json' }\n supply: { ko: 'error', en: 'error' }\n extract:\n koKeys: [{ op: 'source', of: 'ko' }, { op: 'json' }, { op: 'flattenKeys' }]\n enKeys: [{ op: 'source', of: 'en' }, { op: 'json' }, { op: 'flattenKeys' }]\n relate:\n - id: 'parity'\n relation: { op: 'equal', of: ['koKeys', 'enKeys'] }\n messageBySide:\n left: '{key} is in ko only'\n right: '{key} is in en only'\n enforce: advise\n```\n\n```yaml\nlanguages:\n json:\n productionGlob: '*.json'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'status-vocabulary'\n why: 'statuses.json may contain only values listed in allowed-statuses.json'\n declare:\n mechanism: 'controlled-vocabulary'\n scope: { source: 'target.path', include: ['^statuses[.]json$'] }\n sources: { allowed: { file: 'allowed-statuses.json' } }\n supply: { post: 'error', allowed: 'error' }\n extract:\n selected: [{ op: 'source', of: 'post' }, { op: 'json' }, { op: 'items' }]\n permitted: [{ op: 'source', of: 'allowed' }, { op: 'json' }, { op: 'items' }]\n relate:\n - id: 'allowed-status'\n relation: { op: 'subset', of: 'selected', in: 'permitted' }\n message: 'unknown status: {value}'\n enforce: advise\n```\n\nHere both status files are JSON arrays of strings. This declaration scopes on statuses.json;\nediting only the allowed list does not trigger it. Broaden the observation deliberately if\nchanges to that list must recheck all dependent files.\n\n```yaml\nlanguages:\n typescript:\n productionGlob: 'src/**'\n testCmd: 'pnpm test'\ndisciplines:\n - id: 'manifest-needs-npm-view'\n why: 'a successful package lookup must precede a manifest edit'\n declare:\n mechanism: 'precedent'\n scope: { source: 'target.path', include: ['^(packages/[^/]+/)?package[.]json$'] }\n sources: { session: { transcript: true } }\n supply: { session: 'pass' }\n extract:\n npmView:\n - { op: 'source', of: 'session' }\n - { op: 'toolUses', names: ['Bash'] }\n - { op: 'filter', when: [{ field: 'succeeded', eq: true }] }\n - { op: 'select', path: 'args.command' }\n - { op: 'matches', re: '^npm view ' }\n relate:\n - id: 'npm-view'\n relation: { op: 'nonEmpty', of: 'npmView' }\n message: 'no successful npm view precedes this edit'\n enforce: advise\n```\n\nThe precedent example proves only that an observed successful Bash call starts with npm view;\nit does not prove that the lookup concerns the dependency being edited. The commit surface has\nno transcript and therefore skips this example by its explicit supply policy.\n\n**Write the regex yourself \u2014 the user states the promise, you author the pattern.** The\npattern is the part users find hardest, so never hand the prose back and ask for one. Three\nauthoring traps, each measured on a live config:\n\n- **A pattern answers a syntactic question only.** \"Is this string a forbidden word\" is\n syntax; \"is this a new dependency version\" is meaning, and a regex leaks both ways on a\n semantic question. When the question is semantic, narrow the declaration's own `scope`\n block to the files where any match IS a break, or accept \"editing this file at all\" as\n the trigger.\n- **`^` means what the preceding step left.** After a `lines` step a declaration's\n pattern sees one line at a time, so `^` anchors to that line; over an unsplit source it\n anchors to the whole text and matches the first line only. A ban over the command line\n puts `lines` before its `matches` for exactly that reason.\n- **Author both directions.** Before registering, write down one string the pattern must\n match and one nearby string it must not (`only(` vs `only_helper(`, a flag vs its\n substring). A pattern checked in only the breaking direction over-fires in review-proof\n ways.\n\n### 4b. Not expressible yet \u2014 register a draft\n\nA draft is prose with a handle: `id`, `why`, and the literal marker `draft: true` \u2014 no other\nkeys. It produces no judgment and no telemetry; `pdks explain` lists it as unpromoted.\nRecord the intended promise and the exact missing capability inside `why`. Do not classify\npairing, vocabulary, or history promises as drafts merely because they are absent from a short\nexample list. Check the catalogue, extraction steps, and observation channel first. A reserved\n`delegated-scope` declaration cannot be registered as a judged entry.\n\n```yaml\nlanguages:\n placeholder:\n productionGlob: 'src/**'\n testCmd: 'echo \"set a verification command for {scope}\"'\ndisciplines:\n - id: 'benchmark-supports-performance-claim'\n why: 'a performance claim needs a fresh benchmark run during judgment; the engine cannot execute it'\n draft: true\n```\n\n### 5. Prove it fires, then close\n\nRun `pdks explain` and confirm the new entry is listed (a judged entry with its mechanism\nand surfaces; a draft as unpromoted).\n\nFor a judged entry, registration is not the finish \u2014 a pattern that never fires protects\nnothing while looking installed. Fire it once for real, with the proof run the declaration's\nown mechanism can actually reach:\n\n| Mechanism | Break it once | The entry's id shows up in |\n| --- | --- | --- |\n| a file-reading one (`added-only`, `naming`, \u2026) | one scratch edit matching the must-match direction | `pdks covenant check --worktree` output \u2014 the exit stays 0 at advise, the id is the proof |\n| `forbidden-command` | run one harmless command matching the pattern | the telemetry log tail \u2014 at advise the call proceeds and its row records the id |\n| `precedent` | one in-scope edit made without the required precedent | the telemetry log tail \u2014 a declaration reading the session judges on the session surface only (the commit surface has none, so its `supply` policy records it `skipped`) |\n\nThen undo the scratch break, repeat the same observation, and confirm a passing row for the\nmust-NOT-match case. Silence alone may mean a scope miss, unchanged files, or unavailable evidence;\ncheck `pdks explain` and telemetry for `config-fault`, `no-observation`, or `supply-pass`. Close by telling the user which rung the entry landed on and\nthat `enforce: block` is theirs to add later if the advise record earns it.\n\n## Updating this skill without losing local edits\n\nAn upgrade does not overwrite an existing skill; rerunning `pdks init claude-code` reports it\nskipped. Generate a fresh copy in a disposable project using the installed package, compare it\nwith this file, and merge the changes you want. Keep a backup of local additions. Do not delete\nthe existing skill to force regeneration in the working project.\n\n## Reading the advise record\n\nAn `advised` row means a promise was broken and the call went through anyway. Rows land in\nthe telemetry log at the path configured by `telemetry.logPath` (default\n`.polydeukes/roi.log`). The hook's stderr note is not shown to you, so consult the log at\ntask boundaries: before committing, or after a batch of edits, read the tail and act on any\n`advised` row \u2014 fix the break, or tell the user why it should stand. An advisory nobody\nreads measures nothing.\n";
|
|
30
32
|
/** `initClaudeCode` input — the target tree and the preflight seam. */
|
|
31
33
|
export type InitClaudeCodeSpec = {
|
|
32
34
|
/** Project root to install into — every write below is relative to it. */
|