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/README.ko.md
CHANGED
|
@@ -1,74 +1,54 @@
|
|
|
1
|
-
# polydeukes
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
-
|
|
54
|
-
|
|
55
|
-
이벤트로 기록되고, 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** · [한국어](
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
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;
|
package/dist/baseline.js
ADDED
|
@@ -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:
|
|
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:
|
|
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] === '
|
|
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 {
|
|
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
|
|
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.
|
|
29
|
-
import { loadConfig } from './load-config.
|
|
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>;
|