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.
Files changed (79) hide show
  1. package/README.ko.md +54 -74
  2. package/README.md +55 -86
  3. package/dist/baseline.d.ts +82 -0
  4. package/dist/baseline.js +166 -0
  5. package/dist/bin.d.ts +1 -1
  6. package/dist/bin.js +26 -6
  7. package/dist/claude-code-hook.d.ts +7 -5
  8. package/dist/claude-code-hook.js +69 -17
  9. package/dist/claude-code.d.ts +6 -0
  10. package/dist/claude-code.js +6 -0
  11. package/dist/covenant-check.d.ts +10 -17
  12. package/dist/covenant-check.js +44 -12
  13. package/dist/covenant-module.d.ts +2 -2
  14. package/dist/covenant-module.js +9 -1
  15. package/dist/docs/README.ko.md +60 -0
  16. package/dist/docs/README.md +64 -0
  17. package/dist/docs/catalog.json +464 -0
  18. package/dist/docs/concepts/judgment.ko.md +113 -0
  19. package/dist/docs/concepts/judgment.md +113 -0
  20. package/dist/docs/how-to/configure-project.ko.md +99 -0
  21. package/dist/docs/how-to/configure-project.md +95 -0
  22. package/dist/docs/how-to/connect-surfaces.ko.md +115 -0
  23. package/dist/docs/how-to/connect-surfaces.md +118 -0
  24. package/dist/docs/how-to/write-disciplines.ko.md +124 -0
  25. package/dist/docs/how-to/write-disciplines.md +125 -0
  26. package/dist/docs/index.json +2046 -0
  27. package/dist/docs/reference/cli/covenant-check.ko.md +101 -0
  28. package/dist/docs/reference/cli/covenant-check.md +98 -0
  29. package/dist/docs/reference/cli/docs.ko.md +97 -0
  30. package/dist/docs/reference/cli/docs.md +95 -0
  31. package/dist/docs/reference/cli/explain.ko.md +79 -0
  32. package/dist/docs/reference/cli/explain.md +84 -0
  33. package/dist/docs/reference/cli/init.ko.md +119 -0
  34. package/dist/docs/reference/cli/init.md +131 -0
  35. package/dist/docs/reference/configuration/index.ko.md +448 -0
  36. package/dist/docs/reference/configuration/index.md +474 -0
  37. package/dist/docs/reference/packages/adapter-claude-code.ko.md +83 -0
  38. package/dist/docs/reference/{adapter-claude-code.md → packages/adapter-claude-code.md} +16 -10
  39. package/dist/docs/reference/packages/adapter-git.ko.md +101 -0
  40. package/dist/docs/reference/{adapter-git.md → packages/adapter-git.md} +25 -15
  41. package/dist/docs/reference/packages/core.ko.md +128 -0
  42. package/dist/docs/reference/{core.md → packages/core.md} +32 -13
  43. package/dist/docs/reference/packages/covenant.ko.md +115 -0
  44. package/dist/docs/reference/{covenant.md → packages/covenant.md} +41 -26
  45. package/dist/docs/reference/packages/polydeukes.ko.md +134 -0
  46. package/dist/docs/reference/packages/polydeukes.md +139 -0
  47. package/dist/docs/troubleshooting.ko.md +142 -0
  48. package/dist/docs/troubleshooting.md +97 -122
  49. package/dist/docs/tutorials/first-judgment.ko.md +82 -0
  50. package/dist/docs/tutorials/first-judgment.md +81 -0
  51. package/dist/docs-catalog.d.ts +25 -0
  52. package/dist/docs-catalog.js +450 -0
  53. package/dist/docs-library.d.ts +23 -0
  54. package/dist/docs-library.js +347 -0
  55. package/dist/docs-markdown.d.ts +32 -0
  56. package/dist/docs-markdown.js +150 -0
  57. package/dist/docs-query.d.ts +11 -40
  58. package/dist/docs-query.js +28 -122
  59. package/dist/docs-types.d.ts +105 -0
  60. package/dist/docs-types.js +2 -0
  61. package/dist/explain.d.ts +3 -5
  62. package/dist/explain.js +48 -47
  63. package/dist/index.d.ts +2 -3
  64. package/dist/index.js +1 -2
  65. package/dist/init-claude-code.d.ts +5 -3
  66. package/dist/init-claude-code.js +226 -63
  67. package/dist/init-grok.d.ts +51 -0
  68. package/dist/init-grok.js +242 -0
  69. package/dist/load-config.d.ts +5 -1
  70. package/dist/load-config.js +2 -1
  71. package/dist/pre-state-reader.d.ts +22 -0
  72. package/dist/pre-state-reader.js +32 -0
  73. package/dist/scaffold-project.js +48 -8
  74. package/dist/schema/polydeukes.schema.json +38 -91
  75. package/package.json +7 -7
  76. package/dist/docs/configuration.md +0 -103
  77. package/dist/docs/installation.md +0 -212
  78. package/dist/docs/reference/configuration.md +0 -338
  79. package/dist/docs/reference/polydeukes.md +0 -287
package/README.ko.md CHANGED
@@ -1,74 +1,54 @@
1
- # polydeukes
2
-
3
- **한국어** · [English](./README.md)
4
-
5
- > 스코프 없는 umbrella 패키지입니다. `pdks` CLI 진입점과 config 디스커버리 로더가 살고,
6
- > 프레임워크의 조각을 저장소가 실제로 돌리는 표면에 맞게 조립하는 유일한 자리입니다.
7
-
8
- **알파(alpha) 단계입니다.** 이 패키지는 스코프 없는 `polydeukes` 이름을 예약하고, 스코프
9
- 모듈(`@polydeukes/*`)을 조립할 수 있는 유일한 패키지로 그 위에 섭니다. 다른 모든 의존은 코어를
10
- 통해서만 단방향으로 흐릅니다.
11
-
12
- ## 여기 담긴 것
13
-
14
- - **`loadConfig(rootDir)`.** config 디스커버리입니다. 주어진 루트 바로 아래에서 정확히 하나의
15
- 데이터 config(`polydeukes.config` 파일의 yaml·yml·json 형태)를 찾아, 안전 스키마로 해석하고
16
- (설정 데이터는 결코 실행되지 않습니다) 코어의 `defineConfig()`에 검증을 맡깁니다. 모든 실패
17
- 분기는 예외를 던집니다(throw). 조용한 기본값은 금지이고, 발견된 파일은 자기 자신을 보호
18
- 표면에 편입시킵니다.
19
- - **`pdks covenant check`.** `pdks` bin의 첫 실물 서브커맨드입니다(`polydeukes`는 별칭).
20
- pre-commit 판정 러너로, 스테이징 영역의 변경을 `@polydeukes/adapter-git`이 수집해
21
- 약속(covenant) 입력 IR로 번역하고, 세션 훅이 부르는 바로 그 in-process 판정기로 보냅니다.
22
- 판정기는 하나, 표면은 여럿입니다. `--worktree`와 `--range <base>..<head>`는 같은 판정을
23
- 작업 트리나 ref 범위에 진단 호출로 돌리며, 증인 프롬프트는 뜨지 않습니다. 맥락족 규율(discipline, `requirePrecedent`)도 다른 규율과
24
- 똑같이 조립되지만, 읽을 세션이 없으므로 스킵 등록이 됩니다. 스테이징 변경과 매치하면 자기
25
- id와 변경을 담은 `skipped` 이벤트를 남기고 커밋은 진행됩니다. 여기서 판정하면 걸리는
26
- 커밋마다 정당하게 통과할 길 없이 막히고, 아예 걸러내면 게이트가 물러섰다는 사실이 가려집니다.
27
- 세션 표면이 읽을 transcript를 갖지 못했을 때와 같은 처분입니다. 빈 스테이징은 명시적
28
- 통과이고, config가 없거나 불량이면 닫힌 실패(exit 2)입니다.
29
- - **`pdks init claude-code`.** 세션 표면 설치기입니다. 아무것도 쓰기 전에 대상 프로젝트에서
30
- `polydeukes`가 해소되는지부터 증명하고, 그다음 모든 배포 경로가 공유하는 것(데이터 config와
31
- `.polydeukes/` 무시 줄)과 이 경로가 지는 것(위임자 훅, 그 파일이 이미 담고 있던 것에 병합되는
32
- `.claude/settings.json` 등록, 그리고 AI 파트너를 `pdks docs`로 보내는 범위 한정 규율 파일)을
33
- 만듭니다. 기존 산출물은 덮어쓰지 않습니다. 이미 있는 것은 보고하고 그대로 두므로 재실행은
34
- 아무 일도 하지 않고, 사용자가 고친 내용이 살아남습니다. 생성된 config 해소 경로를 보호
35
- 목록에 담고 증인(witness) 블록을 함께 갖습니다. 밸브가 없으면 첫 차단이 곧 프로젝트 정지인데,
36
- 방금 등록한 훅 자신이 보호 대상이기 때문입니다. config 철자 둘이 공존하는 트리, 파싱되지 않는
37
- settings 파일, 해소되지 않는 패키지는 전부 선행 조건 실패입니다. 반쯤 배선된 트리를 남기는
38
- 대신 파일을 하나도 만들지 않습니다.
39
- - **`pdks docs [topic]`.** 오프라인 문서 열람기입니다. 영어 가이드와 레퍼런스 층이 빌드 시점에
40
- `dist/docs`로 복사되므로, 소비자의 AI 파트너는 검색 엔진이 색인한 아무 판본이 아니라 판정을
41
- 수행하는 코드와 함께 실린 문서를 읽습니다. 인자 없이 부르면 토픽 다섯을 열거하는데, 에이전트가
42
- 무엇을 물을 있는지 아는 경로가 그 목록뿐입니다. 토픽을 주면 그 절을 원문 그대로 돌려주고
43
- 이어서 읽을 레퍼런스를 한 줄 덧붙입니다. 질의 정의역은 그 다섯 이름뿐입니다. 모르는 토픽, 없는
44
- 동봉 문서, 문서가 더는 담지 않는 표제는 각각 무엇이 없었는지 stderr에 이름을 적고 exit 2로
45
- 끝나며 stdout은 0바이트로 둡니다. 절반만 쓰인 답은 에이전트가 그것을 문서로 읽고 그대로
46
- 인용하므로, 그런 상태를 만드는 경로를 두지 않습니다.
47
- - **커밋 표면 증인(witness) 밸브.** `block` 수위(기본값)에서 스테이징 영역의 변경이 실제로
48
- 약속을 깨면 러너가 `/dev/tty`에서 증인 토큰 전문을 1회 묻습니다(부분 문자열은 거부합니다).
49
- 프롬프트는 깨진 등록과 걸린 항목, 그리고 이 한 번의 답이 커밋 전체를 덮는다는 사실을 적습니다.
50
- 깨끗한 커밋은 프롬프트를 보지 않습니다. TTY가 없으면, 즉 CI나 에이전트가 띄운 `git commit`
51
- 이면 프롬프트도 통과할 길도 없습니다. 밸브는 터미널 앞의 인간에게만 닿고, 어떤 상태도 남기지
52
- 않으며, 증언으로 통과한 판정은 모두 `witnessed`로 기록됩니다. 조용한 우회는 없습니다.
53
- - **시행 수위.** git 어댑터의 네임스페이스 설정 `adapters.git.enforce: block | advise`가 커밋
54
- 표면 판정의 처분을 고릅니다. `advise`에서는 밸브 자체가 조립되지 않습니다. 판정은 `advised`
55
- 이벤트로 기록되고, stderr에 권고 한 줄이 남으며, 커밋은 진행됩니다. 차단하는 대신 측정하는
56
- 백스톱입니다. 완화되는 것은 판정뿐이라, 판정 자체가 불가능한 실행(설정 없음·무효, 판정 본체
57
- 해석 불가)은 어느 수위에서든 exit 2로 닫힙니다.
58
-
59
- ## 전체 지도
60
-
61
- | 모듈 | 역할 |
62
- |---|---|
63
- | `@polydeukes/core` | 약속(covenant) 프로토콜, config 스키마, ROI 텔레메트리, 대화 기록 이음새 |
64
- | `@polydeukes/covenant` | 디스패처, 판정 본체, Bash 분석, 규율(discipline) 라이브러리 |
65
- | `@polydeukes/adapter-claude-code` | 세션 표면. PreToolUse 페이로드를 약속(covenant) 입력 IR로 번역 |
66
- | `@polydeukes/adapter-git` | 커밋 표면. `staged diff`를 약속(covenant) 입력 IR로 번역 |
67
- | `@polydeukes/ledger` · `@polydeukes/memory` · `@polydeukes/verify` | 청사진 단계 |
68
-
69
- 아키텍처 청사진과 설계 근거는
70
- [프로젝트 저장소](https://github.com/huskyhoochu/polydeukes)에 있습니다.
71
-
72
- ## 라이선스
73
-
74
- MIT
1
+ # `polydeukes`
2
+
3
+ [English](./README.md) · **한국어**
4
+
5
+ Polydeukes는 우산 패키지입니다. 패키지 하나에 `pdks` 실행 파일, 설정 로더,
6
+ 커밋 표면 실행기, 세션 표면 실행기 서브패스, 동봉 스키마가 모두 포함돼 있습니다.
7
+
8
+ <a id="overview"></a>
9
+ ## 개요
10
+
11
+ 공개 계약 심볼과 진입점은 다음과 같습니다.
12
+
13
+ - `loadConfig`
14
+ - `runCovenantCheck`
15
+ - `ResolvedConfig`
16
+ - `polydeukes/claude-code` `runClaudeCodeHook`
17
+ - `polydeukes/schema.json`
18
+ - `pdks covenant check`
19
+ - `pdks init claude-code`
20
+ - `pdks init grok`
21
+ - `pdks explain`
22
+ - `pdks docs [topic]`
23
+
24
+ <a id="public-symbols"></a>
25
+ ## 공개 심볼
26
+
27
+ ```ts
28
+ import { loadConfig, runCovenantCheck } from 'polydeukes';
29
+ import { runClaudeCodeHook } from 'polydeukes/claude-code';
30
+ ```
31
+
32
+ ```ts
33
+ function loadConfig(spec: { rootDir: string }): {
34
+ config: import('@polydeukes/core').ResolvedConfig;
35
+ configPath: string;
36
+ };
37
+
38
+ function runCovenantCheck(spec: {
39
+ repoRoot: string;
40
+ telemetryPath?: string;
41
+ covenantDist?: string;
42
+ ttyPrompt?: (prompt: string) => string | null;
43
+ domain?: unknown;
44
+ }): Promise<{ exitCode: 0 | 2 }>;
45
+ ```
46
+
47
+ <a id="see-also"></a>
48
+ ## 같이 보기
49
+
50
+ - [`polydeukes` 패키지 레퍼런스](../../docs/reference/packages/polydeukes.ko.md)
51
+ - [`설정 레퍼런스`](../../docs/reference/configuration/index.ko.md)
52
+ - [`pdks covenant check`](../../docs/reference/cli/covenant-check.ko.md)
53
+ - [`pdks init`](../../docs/reference/cli/init.ko.md)
54
+ - [`pdks explain`](../../docs/reference/cli/explain.ko.md)
package/README.md CHANGED
@@ -1,86 +1,55 @@
1
- # polydeukes
2
-
3
- **English** · [한국어](https://github.com/huskyhoochu/polydeukes/blob/main/packages/polydeukes/README.ko.md)
4
-
5
- > The unscoped umbrella: the `pdks` CLI entry point and the config discovery loader — the one
6
- > place where the framework's pieces are assembled for the surfaces a repository actually runs.
7
-
8
- **Alpha.** This package reserves the unscoped `polydeukes` name and sits above the scoped
9
- `@polydeukes/*` modules as the only one allowed to assemble them — every other dependency stays
10
- one-way, through the core alone.
11
-
12
- ## What lives here
13
-
14
- - **`loadConfig(rootDir)`** — config discovery. Exactly one root data config (a
15
- `polydeukes.config` file in yaml, yml, or json form) directly under the given root, parsed with
16
- a safe schema (config data is never executable) and validated by the core's `defineConfig()`.
17
- Every failure branch throws — silent defaults are forbidden — and the discovered file attaches
18
- itself to its own protection surface.
19
- - **`pdks covenant check`** — the first real subcommand of the `pdks` bin (`polydeukes` is an
20
- alias). A pre-commit judgment runner: staged changes are collected by `@polydeukes/adapter-git`,
21
- translated into the covenant input IR, and dispatched through the very in-process judges the
22
- session hook calls — one judge, every surface. `--worktree` and `--range <base>..<head>` run the same
23
- judgment over the working tree or a ref range as a diagnostic call, with no witness prompt.
24
- Context-family disciplines (`requirePrecedent`) assemble
25
- here like any other, but with no session to read they become skip registrations: when one
26
- matches a staged change it records a `skipped` event carrying its id and that change, and the
27
- commit proceeds. Judging them would block every matching commit with no legitimate pass path;
28
- filtering them out would hide that a gate stood down. It is the same disposition the session
29
- surface uses when it has no transcript. An empty staging area is an explicit pass; a missing or
30
- invalid config fails closed.
31
- - **`pdks explain`** — the assembly reader. It assembles both surfaces' registration sets through
32
- the same functions the two runners call and prints them without judging: every registration
33
- with its kind (`meta` / `judge` / `skip` / `excluded`), the routing scope of each entry, whether
34
- it carries a `why`, and for each skip the compiler's reason — the one that otherwise reaches
35
- stderr only when the cause is a config fault. No judge thunk is called and no telemetry row is
36
- written. A config that cannot be loaded fails at exit `2` with stdout at zero bytes.
37
- - **`pdks init claude-code`** — the session-surface installer. It proves `polydeukes` resolves
38
- from the target project before writing anything, then creates what every distribution path
39
- shares (the data config and its `.polydeukes/` ignore line) and what this path owns (a delegator
40
- hook, its `.claude/settings.json` registration merged into whatever that file already carries,
41
- and a scoped discipline file pointing an AI partner at `pdks docs`). Nothing existing is
42
- overwritten: an artifact already there is reported and left alone, so a re-run is a no-op and a
43
- consumer's edits survive. The generated config carries the resolution paths on its protection
44
- list and a witness block — without the valve the first block would freeze the project, since the
45
- hook it just registered is itself protected. Two coexisting config spellings, an unparseable
46
- settings file, and a package that cannot be resolved are all precondition failures: each leaves
47
- zero files rather than a half-wired tree.
48
- - **`pdks docs [topic]`** — the offline documentation reader. The English guides and the reference
49
- layer are copied into `dist/docs` at build time, so a consumer's AI partner reads the
50
- documentation that shipped with the code doing the judging instead of whichever release a search
51
- engine indexed. With no argument it lists the five topics — that listing is how an agent
52
- discovers what it may ask at all; with one it returns that topic's section verbatim plus the
53
- reference to read next. The query domain is those five names and nothing else: an unknown topic,
54
- a bundled document that is missing, and a heading a document no longer carries each name what
55
- was missing on stderr and exit 2, leaving stdout at zero bytes. A partially written answer would
56
- be read as the document and quoted onward, so no path produces one.
57
- - **The commit-surface witness valve** — at the `block` level (the default), when a staged change
58
- actually breaks a covenant, the runner prompts once on `/dev/tty` for the full witness token (a
59
- substring is refused), naming the broken registration, the matched entry, and the commit-wide
60
- reach of the one answer. A clean commit never prompts. No TTY — CI, an agent-spawned
61
- `git commit` — means no prompt and no way through: the valve is reachable only by a human at a
62
- terminal, and nothing is ever persisted. Every witnessed pass is measured as `witnessed`, never
63
- silent.
64
- - **The enforcement level** — the git adapter's namespace setting
65
- `adapters.git.enforce: block | advise` selects what a commit-surface verdict does. Under
66
- `advise` the valve is structurally absent: a verdict is recorded as an `advised` event, one
67
- advisory line lands on stderr, and the commit proceeds — a backstop that measures instead of
68
- blocking. Only the verdict is relaxed: a run that cannot judge (missing or invalid config, an
69
- unresolvable judge body) fails closed at exit 2 at either level.
70
-
71
- ## The wider map
72
-
73
- | Module | Role |
74
- |---|---|
75
- | `@polydeukes/core` | Covenant protocol, config schema, ROI telemetry, transcript seam |
76
- | `@polydeukes/covenant` | Dispatcher, judge bodies, Bash analysis, discipline library |
77
- | `@polydeukes/adapter-claude-code` | Session surface — PreToolUse payloads → covenant input IR |
78
- | `@polydeukes/adapter-git` | Commit surface — staged diffs → covenant input IR |
79
- | `@polydeukes/ledger` · `@polydeukes/memory` · `@polydeukes/verify` | Blueprint stage |
80
-
81
- See the [project repository](https://github.com/huskyhoochu/polydeukes) for the architecture
82
- blueprint and design rationale.
83
-
84
- ## License
85
-
86
- MIT
1
+ # `polydeukes`
2
+
3
+ **English** · [한국어](./README.ko.md)
4
+
5
+ Polydeukes is the umbrella package. Install this package; it ships the `pdks` bin, the config
6
+ loader, the commit-surface runner, the session-surface runner subpath, and the bundled schema
7
+ artifact.
8
+
9
+ <a id="overview"></a>
10
+ ## Overview
11
+
12
+ Public contract symbols and entry points:
13
+
14
+ - `loadConfig`
15
+ - `runCovenantCheck`
16
+ - `ResolvedConfig`
17
+ - `polydeukes/claude-code` `runClaudeCodeHook`
18
+ - `polydeukes/schema.json`
19
+ - `pdks covenant check`
20
+ - `pdks init claude-code`
21
+ - `pdks init grok`
22
+ - `pdks explain`
23
+ - `pdks docs [topic]`
24
+
25
+ <a id="public-symbols"></a>
26
+ ## Public symbols
27
+
28
+ ```ts
29
+ import { loadConfig, runCovenantCheck } from 'polydeukes';
30
+ import { runClaudeCodeHook } from 'polydeukes/claude-code';
31
+ ```
32
+
33
+ ```ts
34
+ function loadConfig(spec: { rootDir: string }): {
35
+ config: import('@polydeukes/core').ResolvedConfig;
36
+ configPath: string;
37
+ };
38
+
39
+ function runCovenantCheck(spec: {
40
+ repoRoot: string;
41
+ telemetryPath?: string;
42
+ covenantDist?: string;
43
+ ttyPrompt?: (prompt: string) => string | null;
44
+ domain?: unknown;
45
+ }): Promise<{ exitCode: 0 | 2 }>;
46
+ ```
47
+
48
+ <a id="see-also"></a>
49
+ ## See also
50
+
51
+ - [`polydeukes` package reference](../../docs/reference/packages/polydeukes.md)
52
+ - [`Configuration reference`](../../docs/reference/configuration/index.md)
53
+ - [`pdks covenant check`](../../docs/reference/cli/covenant-check.md)
54
+ - [`pdks init`](../../docs/reference/cli/init.md)
55
+ - [`pdks explain`](../../docs/reference/cli/explain.md)
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Post-hoc state comparison — the baseline comparator.
3
+ *
4
+ * The session surface judges declared calls, so a write nobody declared leaves no row at
5
+ * all. This module answers the same question from the other end: summarize the protected
6
+ * entries' on-disk state, and after the fact name the entries whose state moved with no
7
+ * judgment row explaining it. It records rather than blocks — every failure shape here
8
+ * resolves to a value, never a throw.
9
+ *
10
+ * Split the way the telemetry module is: {@link findUnattributed} is pure and
11
+ * {@link snapshotBaseline} / {@link writeBaseline} / {@link readBaseline} are the I/O
12
+ * points.
13
+ */
14
+ import type { TelemetryRecord } from '@polydeukes/core';
15
+ /** One hash per protected entry, keyed by the entry exactly as configured. */
16
+ export type BaselineSnapshot = Record<string, string>;
17
+ /**
18
+ * The baseline file's contents: the entry hashes plus the instant the attribution window
19
+ * opens at. Named for the file rather than the concept — `Baseline` already belongs to the
20
+ * delta family, where it means a file's prior CONTENT, not a protection snapshot.
21
+ *
22
+ * The two fields travel together because they are read together — pairing one comparison's
23
+ * hashes with another's cut would silently mis-attribute, undetectably.
24
+ */
25
+ export type StoredBaseline = {
26
+ entries: BaselineSnapshot;
27
+ cutAt?: string;
28
+ };
29
+ /**
30
+ * Summarize each entry's on-disk state as one sha256.
31
+ *
32
+ * `entries` are repo-relative paths of either shape — a file or a directory. Every entry
33
+ * keeps a key whatever its state, so the diff always sees it; a missing entry simply hashes
34
+ * to the empty set.
35
+ */
36
+ export declare function snapshotBaseline(spec: {
37
+ rootDir: string;
38
+ entries: string[];
39
+ }): BaselineSnapshot;
40
+ /**
41
+ * The entries whose hash changed and that no window row explains (pure).
42
+ *
43
+ * Attribution is asked per entry, never per window: "the window holds an attributing row"
44
+ * and "THIS entry's change has one" are different claims, and answering the first is the
45
+ * fail-open shape this repository already shipped once.
46
+ *
47
+ * The window is cut by `cutAt` — rows at or after that instant, which are the ones written
48
+ * since the previous comparison. Cutting by time rather than by a record count keeps the
49
+ * bound meaningful when the log is trimmed or rotated; a positional index would point past
50
+ * the end and empty the window for every later call. An absent cut admits every row: over-
51
+ * attributing costs one missed alarm in a session that is already anomalous, while under-
52
+ * attributing floods an ordinary one and teaches the reader to ignore the alarm entirely.
53
+ *
54
+ * Only entries the previous baseline already watched are compared. An entry the config just
55
+ * added has no prior hash, and reporting it would make editing one's own policy look
56
+ * identical to a tamper; an entry the config dropped is simply no longer watched.
57
+ */
58
+ export declare function findUnattributed(spec: {
59
+ previous: BaselineSnapshot;
60
+ current: BaselineSnapshot;
61
+ records: TelemetryRecord[];
62
+ cutAt?: string;
63
+ }): string[];
64
+ /**
65
+ * Write the baseline file.
66
+ *
67
+ * `cutAt` is the instant this comparison ran, so the next one attributes from rows written
68
+ * at or after it. Written at call END, so the rows explaining what this call itself changed
69
+ * fall before the cut — the snapshot beside them already absorbed those changes.
70
+ */
71
+ export declare function writeBaseline(path: string, snapshot: BaselineSnapshot, cutAt?: string): void;
72
+ /**
73
+ * Read the baseline — entries and window cut in ONE parse — or `null`.
74
+ *
75
+ * One parse rather than two: reading the hashes and the cut separately lets a concurrent
76
+ * write land between them, pairing one comparison's state with another's window bound. That
77
+ * mismatch changes which entries are attributed and leaves no trace anywhere.
78
+ *
79
+ * Absence and corruption are the same signal — re-establish and record — so neither throws.
80
+ * Observation is fail-open: the worst outcome is a missing datum, never a blocked call.
81
+ */
82
+ export declare function readBaseline(path: string): StoredBaseline | null;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Post-hoc state comparison — the baseline comparator.
3
+ *
4
+ * The session surface judges declared calls, so a write nobody declared leaves no row at
5
+ * all. This module answers the same question from the other end: summarize the protected
6
+ * entries' on-disk state, and after the fact name the entries whose state moved with no
7
+ * judgment row explaining it. It records rather than blocks — every failure shape here
8
+ * resolves to a value, never a throw.
9
+ *
10
+ * Split the way the telemetry module is: {@link findUnattributed} is pure and
11
+ * {@link snapshotBaseline} / {@link writeBaseline} / {@link readBaseline} are the I/O
12
+ * points.
13
+ */
14
+ import { createHash } from 'node:crypto';
15
+ import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ /**
18
+ * The two verdict words that explain a change — the ones that mean a mutation of a
19
+ * protected entry was judged and let through anyway.
20
+ *
21
+ * `witnessed` is a break a human opened in person; `advised` is one an advise-level surface
22
+ * recorded without stopping. Both name a write the session knew about, so the change that
23
+ * follows is accounted for.
24
+ *
25
+ * The other four do not attribute, each for its own reason. `passed` means the call was
26
+ * judged and did NOT break the covenant — on a protected entry that is a mention, not a
27
+ * mutation, so it explains no later change; admitting it is what let a read-only call
28
+ * absolve a tamper. `blocked` stops the call, so what follows is not its doing, and
29
+ * admitting it would make provoking a block a licence for every later write to that entry
30
+ * (the residue a blocked call really left is present when that same call compares, reported
31
+ * there once, and folded into its own re-establishment). `skipped` is a recorded absence of
32
+ * judgment, which explains nothing. `unattributed` must never silence its own successor.
33
+ */
34
+ const ATTRIBUTING_EVENTS = ['witnessed', 'advised'];
35
+ /**
36
+ * Fold one file's path and content into `hash`.
37
+ *
38
+ * The path participates, so a rename with byte-identical content still moves the entry's
39
+ * hash. An unreadable file contributes a fixed absence marker rather than aborting the
40
+ * walk — absence is a state, and a file that vanished mid-walk is exactly the state the
41
+ * comparison exists to see. The marker leads with an escaped NUL so no readable file's
42
+ * contents can collide with it; written as an escape rather than a literal byte, which
43
+ * would make this source a binary file to git and hide it from every diff.
44
+ */
45
+ function foldFile(hash, absolutePath, key) {
46
+ hash.update(key);
47
+ try {
48
+ hash.update(readFileSync(absolutePath));
49
+ }
50
+ catch {
51
+ hash.update('\0absent');
52
+ }
53
+ }
54
+ /**
55
+ * Fold everything reachable under `absolutePath` into `hash`, in sorted order.
56
+ *
57
+ * Sorted so an unchanged tree hashes identically twice — directory iteration order is not
58
+ * a guarantee, and a nondeterministic term would read as a change on every call. A path
59
+ * that is a file, or that does not exist at all, is not a traversal failure: six of the
60
+ * live config's thirteen entries name a file, and a vanished entry hashes to the empty set.
61
+ */
62
+ function foldEntry(hash, rootDir, relPath) {
63
+ let children;
64
+ try {
65
+ children = readdirSync(join(rootDir, relPath), { withFileTypes: true });
66
+ }
67
+ catch {
68
+ foldFile(hash, join(rootDir, relPath), relPath);
69
+ return;
70
+ }
71
+ for (const child of [...children].sort((a, b) => a.name.localeCompare(b.name))) {
72
+ foldEntry(hash, rootDir, `${relPath}/${child.name}`);
73
+ }
74
+ }
75
+ /**
76
+ * Summarize each entry's on-disk state as one sha256.
77
+ *
78
+ * `entries` are repo-relative paths of either shape — a file or a directory. Every entry
79
+ * keeps a key whatever its state, so the diff always sees it; a missing entry simply hashes
80
+ * to the empty set.
81
+ */
82
+ export function snapshotBaseline(spec) {
83
+ const snapshot = {};
84
+ for (const entry of spec.entries) {
85
+ const hash = createHash('sha256');
86
+ foldEntry(hash, spec.rootDir, entry);
87
+ snapshot[entry] = hash.digest('hex');
88
+ }
89
+ return snapshot;
90
+ }
91
+ /**
92
+ * The entries whose hash changed and that no window row explains (pure).
93
+ *
94
+ * Attribution is asked per entry, never per window: "the window holds an attributing row"
95
+ * and "THIS entry's change has one" are different claims, and answering the first is the
96
+ * fail-open shape this repository already shipped once.
97
+ *
98
+ * The window is cut by `cutAt` — rows at or after that instant, which are the ones written
99
+ * since the previous comparison. Cutting by time rather than by a record count keeps the
100
+ * bound meaningful when the log is trimmed or rotated; a positional index would point past
101
+ * the end and empty the window for every later call. An absent cut admits every row: over-
102
+ * attributing costs one missed alarm in a session that is already anomalous, while under-
103
+ * attributing floods an ordinary one and teaches the reader to ignore the alarm entirely.
104
+ *
105
+ * Only entries the previous baseline already watched are compared. An entry the config just
106
+ * added has no prior hash, and reporting it would make editing one's own policy look
107
+ * identical to a tamper; an entry the config dropped is simply no longer watched.
108
+ */
109
+ export function findUnattributed(spec) {
110
+ const cutAt = spec.cutAt;
111
+ const attributed = new Set(spec.records
112
+ .filter((record) => cutAt === undefined || record.timestamp >= cutAt)
113
+ .filter((record) => ATTRIBUTING_EVENTS.includes(record.event))
114
+ .map((record) => record.subject));
115
+ return Object.keys(spec.current).filter((entry) => entry in spec.previous &&
116
+ spec.current[entry] !== spec.previous[entry] &&
117
+ !attributed.has(entry));
118
+ }
119
+ /**
120
+ * Write the baseline file.
121
+ *
122
+ * `cutAt` is the instant this comparison ran, so the next one attributes from rows written
123
+ * at or after it. Written at call END, so the rows explaining what this call itself changed
124
+ * fall before the cut — the snapshot beside them already absorbed those changes.
125
+ */
126
+ export function writeBaseline(path, snapshot, cutAt) {
127
+ writeFileSync(path, JSON.stringify({ entries: snapshot, cutAt }));
128
+ }
129
+ /**
130
+ * Read the baseline — entries and window cut in ONE parse — or `null`.
131
+ *
132
+ * One parse rather than two: reading the hashes and the cut separately lets a concurrent
133
+ * write land between them, pairing one comparison's state with another's window bound. That
134
+ * mismatch changes which entries are attributed and leaves no trace anywhere.
135
+ *
136
+ * Absence and corruption are the same signal — re-establish and record — so neither throws.
137
+ * Observation is fail-open: the worst outcome is a missing datum, never a blocked call.
138
+ */
139
+ export function readBaseline(path) {
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(readFileSync(path, 'utf-8'));
143
+ }
144
+ catch {
145
+ return null;
146
+ }
147
+ // Validate what the comparison will use: valid JSON of the wrong shape parses fine and
148
+ // then crashes the diff, which the fail-open wiring swallows — detection would go dark
149
+ // with no re-establishment and no row.
150
+ if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
151
+ return null;
152
+ }
153
+ const { entries, cutAt } = parsed;
154
+ if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
155
+ return null;
156
+ }
157
+ if (Object.values(entries).some((value) => typeof value !== 'string')) {
158
+ return null;
159
+ }
160
+ // An unparseable cut degrades to absent, which admits the whole window — the same safe
161
+ // direction {@link findUnattributed} takes, rather than a bound nobody can trust.
162
+ return {
163
+ entries: entries,
164
+ cutAt: typeof cutAt === 'string' ? cutAt : undefined,
165
+ };
166
+ }
package/dist/bin.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `pdks` / `polydeukes` — the umbrella bin.
4
4
  *
5
- * A thin argv shim: four subcommands, each matched by direct comparison against a finite
5
+ * A thin argv shim: each subcommand is matched by direct comparison against a finite
6
6
  * table. Anything else prints usage and exits 2 — an unknown argument must never pass
7
7
  * silently (fail-closed, the same posture as an unjudgeable payload).
8
8
  *
package/dist/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `pdks` / `polydeukes` — the umbrella bin.
4
4
  *
5
- * A thin argv shim: four subcommands, each matched by direct comparison against a finite
5
+ * A thin argv shim: each subcommand is matched by direct comparison against a finite
6
6
  * table. Anything else prints usage and exits 2 — an unknown argument must never pass
7
7
  * silently (fail-closed, the same posture as an unjudgeable payload).
8
8
  *
@@ -11,7 +11,7 @@
11
11
  * CI or by an agent-spawned shell — no controlling terminal), the seam stays absent and
12
12
  * the witness valve is structurally unreachable, so only a human at a terminal can arm it.
13
13
  */
14
- import { closeSync, openSync, readSync, writeSync } from 'node:fs';
14
+ import { closeSync, openSync, readFileSync, readSync, writeSync } from 'node:fs';
15
15
  import { dirname, join } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
17
  /**
@@ -88,15 +88,35 @@ if (args.length === 2 && args[0] === 'init' && args[1] === 'claude-code') {
88
88
  process.exit(2);
89
89
  }
90
90
  }
91
- if (args[0] === 'docs' && args.length <= 2) {
91
+ if (args.length === 2 && args[0] === 'init' && args[1] === 'grok') {
92
+ try {
93
+ const { initGrok } = await import('./init-grok.js');
94
+ const { created, skipped } = initGrok({ projectRoot: process.cwd() });
95
+ for (const path of created) {
96
+ process.stdout.write(`created ${path}\n`);
97
+ }
98
+ for (const path of skipped) {
99
+ process.stdout.write(`skipped ${path} (already present)\n`);
100
+ }
101
+ process.exit(0);
102
+ }
103
+ catch (error) {
104
+ process.stderr.write(`pdks init grok failed: ${error instanceof Error ? error.message : String(error)}\n`);
105
+ process.exit(2);
106
+ }
107
+ }
108
+ if (args[0] === 'docs') {
92
109
  try {
93
110
  // Imported inside the try for the same reason `init` is: the query core and the
94
111
  // markdown behind it have no business on `covenant check`'s load path.
95
- const { queryDocs } = await import('./docs-query.js');
112
+ const { runDocs } = await import('./docs-library.js');
96
113
  // The bundle ships beside this file, so the docs root comes from the module's own
97
114
  // location — never from the working directory, which is whatever shell invoked us.
98
115
  const docsRoot = join(dirname(fileURLToPath(import.meta.url)), 'docs');
99
- const { text } = queryDocs({ docsRoot, topic: args[1] });
116
+ const manifest = JSON.parse(readFileSync(join(docsRoot, '../../package.json'), 'utf8'));
117
+ if (typeof manifest.version !== 'string')
118
+ throw new Error('missing package version');
119
+ const { text } = runDocs({ docsRoot, args: args.slice(1), version: manifest.version });
100
120
  await emitAndExit(text);
101
121
  }
102
122
  catch (error) {
@@ -149,7 +169,7 @@ function parseCheckDomain(flags) {
149
169
  }
150
170
  const domain = args[0] === 'covenant' && args[1] === 'check' ? parseCheckDomain(args.slice(2)) : null;
151
171
  if (domain === null) {
152
- process.stderr.write('usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain | pdks init claude-code | pdks docs [topic]\n');
172
+ process.stderr.write('usage: pdks covenant check [--worktree | --range <base>..<head>] | pdks explain | pdks init claude-code | pdks init grok | pdks docs [topic | search <query> | show <document-id>]\n');
153
173
  process.exit(2);
154
174
  }
155
175
  try {
@@ -25,8 +25,12 @@
25
25
  */
26
26
  import { type CanonicalTranscript } from '@polydeukes/core';
27
27
  import { type CovenantRegistration } from '@polydeukes/covenant';
28
- import { type CovenantModule } from './covenant-module.js';
29
- import { loadConfig } from './load-config.js';
28
+ import { type CovenantModule } from './covenant-module.ts';
29
+ import { loadConfig } from './load-config.ts';
30
+ /** {@link runClaudeCodeHook} result — the exit code the hook process leaves with. */
31
+ export type ClaudeCodeHookOutcome = {
32
+ exitCode: 0 | 2;
33
+ };
30
34
  /** `runClaudeCodeHook` input — the `CovenantCheckSpec` shape, session side. */
31
35
  export type ClaudeCodeHookSpec = {
32
36
  /** Repository root — config discovery and discipline glob scoping both anchor here. */
@@ -72,6 +76,4 @@ export declare function assembleSessionRegistrations(spec: SessionAssemblySpec):
72
76
  * left and its rows land ahead of this call's judgment; the re-establishment runs last, so
73
77
  * this call's own judged writes are folded in rather than alarmed on next time.
74
78
  */
75
- export declare function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<{
76
- exitCode: 0 | 2;
77
- }>;
79
+ export declare function runClaudeCodeHook(spec: ClaudeCodeHookSpec): Promise<ClaudeCodeHookOutcome>;