polydeukes 0.6.1 → 0.7.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 +14 -28
- package/README.md +14 -28
- package/dist/baseline.d.ts +37 -1
- package/dist/baseline.js +68 -1
- package/dist/bin.d.ts +3 -4
- package/dist/bin.js +73 -90
- package/dist/covenant/bash-line.d.ts +130 -0
- package/dist/covenant/bash-line.js +566 -0
- package/dist/covenant/declaration-engine.d.ts +108 -0
- package/dist/covenant/declaration-engine.js +415 -0
- package/dist/covenant/discipline.d.ts +103 -0
- package/dist/covenant/discipline.js +667 -0
- package/dist/covenant/dispatch.d.ts +153 -0
- package/dist/covenant/dispatch.js +237 -0
- package/dist/covenant/extract-steps.d.ts +122 -0
- package/dist/covenant/extract-steps.js +0 -0
- package/dist/covenant/mention.d.ts +117 -0
- package/dist/covenant/mention.js +209 -0
- package/dist/covenant/module.d.ts +25 -0
- package/dist/covenant/module.js +23 -0
- package/dist/covenant/mutation-rules.d.ts +40 -0
- package/dist/covenant/mutation-rules.js +146 -0
- package/dist/covenant/relations.d.ts +46 -0
- package/dist/covenant/relations.js +68 -0
- package/dist/covenant/run-covenant.d.ts +95 -0
- package/dist/covenant/run-covenant.js +164 -0
- package/dist/covenant/self-mod.d.ts +55 -0
- package/dist/covenant/self-mod.js +84 -0
- package/dist/covenant/shell-evidence.d.ts +38 -0
- package/dist/covenant/shell-evidence.js +247 -0
- package/dist/covenant/shell-mod.d.ts +84 -0
- package/dist/covenant/shell-mod.js +213 -0
- package/dist/covenant/supply.d.ts +54 -0
- package/dist/covenant/supply.js +50 -0
- package/dist/covenant/transcript-mod.d.ts +60 -0
- package/dist/covenant/transcript-mod.js +254 -0
- package/dist/covenant/ttl-witness.d.ts +47 -0
- package/dist/covenant/ttl-witness.js +80 -0
- package/dist/covenant-check.d.ts +72 -39
- package/dist/covenant-check.js +260 -121
- package/dist/diff-ir.d.ts +28 -0
- package/dist/diff-ir.js +310 -0
- package/dist/docs/README.ko.md +8 -8
- package/dist/docs/README.md +8 -8
- package/dist/docs/catalog.json +44 -28
- package/dist/docs/concepts/judgment.ko.md +2 -2
- package/dist/docs/concepts/judgment.md +2 -2
- package/dist/docs/how-to/configure-project.ko.md +16 -16
- package/dist/docs/how-to/configure-project.md +15 -12
- package/dist/docs/how-to/connect-surfaces.ko.md +39 -34
- package/dist/docs/how-to/connect-surfaces.md +36 -33
- package/dist/docs/how-to/write-disciplines.ko.md +61 -6
- package/dist/docs/how-to/write-disciplines.md +62 -6
- package/dist/docs/index.json +607 -373
- package/dist/docs/reference/cli/covenant-check.ko.md +112 -65
- package/dist/docs/reference/cli/covenant-check.md +114 -59
- package/dist/docs/reference/cli/docs.ko.md +2 -2
- package/dist/docs/reference/cli/docs.md +2 -2
- package/dist/docs/reference/cli/explain.ko.md +14 -9
- package/dist/docs/reference/cli/explain.md +15 -10
- package/dist/docs/reference/cli/init.ko.md +52 -52
- package/dist/docs/reference/cli/init.md +43 -55
- package/dist/docs/reference/configuration/index.ko.md +171 -66
- package/dist/docs/reference/configuration/index.md +178 -71
- package/dist/docs/reference/packages/adapter-claude-code.ko.md +38 -11
- package/dist/docs/reference/packages/adapter-claude-code.md +38 -14
- package/dist/docs/reference/packages/adapter-grok.ko.md +77 -0
- package/dist/docs/reference/packages/adapter-grok.md +79 -0
- package/dist/docs/reference/packages/core.ko.md +49 -3
- package/dist/docs/reference/packages/core.md +54 -5
- package/dist/docs/reference/packages/polydeukes.ko.md +138 -77
- package/dist/docs/reference/packages/polydeukes.md +146 -81
- package/dist/docs/reference/packages/sdk-ts.ko.md +161 -0
- package/dist/docs/reference/packages/sdk-ts.md +164 -0
- package/dist/docs/troubleshooting.ko.md +17 -15
- package/dist/docs/troubleshooting.md +18 -14
- package/dist/docs/tutorials/first-judgment.ko.md +2 -2
- package/dist/docs/tutorials/first-judgment.md +2 -2
- package/dist/explain.d.ts +1 -1
- package/dist/explain.js +37 -22
- package/dist/load-config.d.ts +1 -1
- package/dist/load-config.js +1 -1
- package/dist/pre-state-reader.d.ts +1 -1
- package/dist/pre-state-reader.js +1 -1
- package/dist/scaffold-project.d.ts +2 -2
- package/dist/scaffold-project.js +9 -3
- package/dist/schema/polydeukes.schema.json +12 -0
- package/dist/worktree-reader.d.ts +19 -0
- package/dist/worktree-reader.js +30 -0
- package/package.json +3 -17
- package/dist/claude-code-hook.d.ts +0 -79
- package/dist/claude-code-hook.js +0 -372
- package/dist/claude-code.d.ts +0 -6
- package/dist/claude-code.js +0 -6
- package/dist/covenant-module.d.ts +0 -25
- package/dist/covenant-module.js +0 -42
- package/dist/docs/reference/packages/adapter-git.ko.md +0 -101
- package/dist/docs/reference/packages/adapter-git.md +0 -108
- package/dist/docs/reference/packages/covenant.ko.md +0 -115
- package/dist/docs/reference/packages/covenant.md +0 -123
- package/dist/index.d.ts +0 -22
- package/dist/index.js +0 -21
- package/dist/init-claude-code.d.ts +0 -52
- package/dist/init-claude-code.js +0 -586
- package/dist/init-grok.d.ts +0 -51
- package/dist/init-grok.js +0 -242
package/README.ko.md
CHANGED
|
@@ -2,47 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) · **한국어**
|
|
4
4
|
|
|
5
|
-
Polydeukes는 우산 패키지입니다. 이 패키지 하나에 `pdks` 실행 파일,
|
|
6
|
-
|
|
5
|
+
Polydeukes는 우산 패키지입니다. 이 패키지 하나에 `pdks` 실행 파일, 판정기,
|
|
6
|
+
두 표면의 조립 루트, 세션 표면 실행기 서브패스, 동봉 스키마가 모두 포함돼 있습니다.
|
|
7
7
|
|
|
8
8
|
<a id="overview"></a>
|
|
9
9
|
## 개요
|
|
10
10
|
|
|
11
|
-
공개 계약
|
|
11
|
+
공개 계약 진입점은 다음과 같습니다.
|
|
12
12
|
|
|
13
|
-
- `
|
|
14
|
-
- `runCovenantCheck`
|
|
15
|
-
- `ResolvedConfig`
|
|
16
|
-
- `polydeukes/claude-code` → `runClaudeCodeHook`
|
|
13
|
+
- `pdks` / `polydeukes` — 실행 파일
|
|
17
14
|
- `polydeukes/schema.json`
|
|
15
|
+
|
|
16
|
+
CLI 명령은 다음과 같습니다.
|
|
17
|
+
|
|
18
18
|
- `pdks covenant check`
|
|
19
|
-
- `pdks init
|
|
20
|
-
- `pdks init grok
|
|
19
|
+
- `pdks init`
|
|
20
|
+
- `pdks-grok init` (`@polydeukes/adapter-grok`가 제공)
|
|
21
21
|
- `pdks explain`
|
|
22
22
|
- `pdks docs [topic]`
|
|
23
23
|
|
|
24
24
|
<a id="public-symbols"></a>
|
|
25
25
|
## 공개 심볼
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
```
|
|
27
|
+
없습니다. 이 패키지는 TypeScript 진입점을 공개하지 않습니다. `import 'polydeukes'`는
|
|
28
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`로 실패하고, 사용자가 닿는 것은 `pdks` 실행 파일과 동봉된
|
|
29
|
+
스키마입니다. 표면은 판정기에 표준 입력으로 입력을 넘기고 종료 코드를 읽습니다. 에이전트
|
|
30
|
+
어댑터의 훅이 하는 일이 바로 그것입니다. 이 패키지를 peer 의존으로 선언하고 import 없이
|
|
31
|
+
실행 파일을 스폰합니다.
|
|
46
32
|
|
|
47
33
|
<a id="see-also"></a>
|
|
48
34
|
## 같이 보기
|
package/README.md
CHANGED
|
@@ -2,48 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
**English** · [한국어](./README.ko.md)
|
|
4
4
|
|
|
5
|
-
Polydeukes is the umbrella package. Install this package; it ships the `pdks` bin, the
|
|
6
|
-
|
|
5
|
+
Polydeukes is the umbrella package. Install this package; it ships the `pdks` bin, the judge,
|
|
6
|
+
both surfaces' composition roots, the session-surface runner subpath, and the bundled schema
|
|
7
7
|
artifact.
|
|
8
8
|
|
|
9
9
|
<a id="overview"></a>
|
|
10
10
|
## Overview
|
|
11
11
|
|
|
12
|
-
Public contract
|
|
12
|
+
Public contract entry points:
|
|
13
13
|
|
|
14
|
-
- `
|
|
15
|
-
- `runCovenantCheck`
|
|
16
|
-
- `ResolvedConfig`
|
|
17
|
-
- `polydeukes/claude-code` → `runClaudeCodeHook`
|
|
14
|
+
- `pdks` / `polydeukes` — the bin
|
|
18
15
|
- `polydeukes/schema.json`
|
|
16
|
+
|
|
17
|
+
CLI commands:
|
|
18
|
+
|
|
19
19
|
- `pdks covenant check`
|
|
20
|
-
- `pdks init
|
|
21
|
-
- `pdks init grok`
|
|
20
|
+
- `pdks init`
|
|
21
|
+
- `pdks-grok init` (ships with `@polydeukes/adapter-grok`)
|
|
22
22
|
- `pdks explain`
|
|
23
23
|
- `pdks docs [topic]`
|
|
24
24
|
|
|
25
25
|
<a id="public-symbols"></a>
|
|
26
26
|
## Public symbols
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
```
|
|
28
|
+
None. This package publishes no TypeScript entry point: `import 'polydeukes'` fails with
|
|
29
|
+
`ERR_PACKAGE_PATH_NOT_EXPORTED`, and what a consumer reaches is the `pdks` bin and the
|
|
30
|
+
bundled schema. A surface hands the judge its input on stdin and reads the exit code, which
|
|
31
|
+
is what an agent adapter's hook does — it takes this package as a peer dependency and
|
|
32
|
+
spawns the bin rather than importing it.
|
|
47
33
|
|
|
48
34
|
<a id="see-also"></a>
|
|
49
35
|
## See also
|
package/dist/baseline.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* {@link snapshotBaseline} / {@link writeBaseline} / {@link readBaseline} are the I/O
|
|
12
12
|
* points.
|
|
13
13
|
*/
|
|
14
|
-
import type
|
|
14
|
+
import { type TelemetryRecord } from '@polydeukes/core';
|
|
15
15
|
/** One hash per protected entry, keyed by the entry exactly as configured. */
|
|
16
16
|
export type BaselineSnapshot = Record<string, string>;
|
|
17
17
|
/**
|
|
@@ -80,3 +80,39 @@ export declare function writeBaseline(path: string, snapshot: BaselineSnapshot,
|
|
|
80
80
|
* Observation is fail-open: the worst outcome is a missing datum, never a blocked call.
|
|
81
81
|
*/
|
|
82
82
|
export declare function readBaseline(path: string): StoredBaseline | null;
|
|
83
|
+
/** The label every post-hoc state comparison row carries. */
|
|
84
|
+
export declare const BASELINE_LABEL = "baseline";
|
|
85
|
+
/**
|
|
86
|
+
* Compare the protected entries' on-disk state against the stored baseline and record what
|
|
87
|
+
* moved with no judgment explaining it.
|
|
88
|
+
*
|
|
89
|
+
* Runs at hook call START, before this call's own judgment rows land, so the window it reads
|
|
90
|
+
* is the one the previous comparison left open. Returns the record count as of right now —
|
|
91
|
+
* where the NEXT window opens, which {@link updateBaseline} persists at call end.
|
|
92
|
+
*
|
|
93
|
+
* The comparison records, it never blocks: no row it writes and no failure it hits changes
|
|
94
|
+
* a verdict or an exit code, which is why every caller keeps it outside the judgment path.
|
|
95
|
+
*/
|
|
96
|
+
export declare function compareBaseline(spec: {
|
|
97
|
+
repoRoot: string;
|
|
98
|
+
telemetryPath: string;
|
|
99
|
+
entries: string[];
|
|
100
|
+
}): void;
|
|
101
|
+
/**
|
|
102
|
+
* Re-establish the baseline at hook call END.
|
|
103
|
+
*
|
|
104
|
+
* At call end rather than right after the comparison: refreshing at comparison time would
|
|
105
|
+
* miss whatever this call's own judged writes changed, leaving detection permanently one
|
|
106
|
+
* call behind.
|
|
107
|
+
*
|
|
108
|
+
* The cut is stamped HERE, beside the snapshot, not at the comparison that opened the call.
|
|
109
|
+
* Both describe the same instant — everything this call did is already folded into the
|
|
110
|
+
* hashes — so the rows explaining it belong before the cut. Stamping the earlier instant
|
|
111
|
+
* instead would re-admit this call's own judgment rows into the next window, where they
|
|
112
|
+
* would attribute a change they had nothing to do with: a call that merely MENTIONED a
|
|
113
|
+
* protected entry would then absolve any tamper that followed it.
|
|
114
|
+
*/
|
|
115
|
+
export declare function updateBaseline(spec: {
|
|
116
|
+
repoRoot: string;
|
|
117
|
+
entries: string[];
|
|
118
|
+
}): void;
|
package/dist/baseline.js
CHANGED
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* points.
|
|
13
13
|
*/
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
|
+
import { appendRecordFailOpen, readRecords, } from '@polydeukes/core';
|
|
17
18
|
/**
|
|
18
19
|
* The two verdict words that explain a change — the ones that mean a mutation of a
|
|
19
20
|
* protected entry was judged and let through anyway.
|
|
@@ -164,3 +165,69 @@ export function readBaseline(path) {
|
|
|
164
165
|
cutAt: typeof cutAt === 'string' ? cutAt : undefined,
|
|
165
166
|
};
|
|
166
167
|
}
|
|
168
|
+
/** The label every post-hoc state comparison row carries. */
|
|
169
|
+
export const BASELINE_LABEL = 'baseline';
|
|
170
|
+
/**
|
|
171
|
+
* Compare the protected entries' on-disk state against the stored baseline and record what
|
|
172
|
+
* moved with no judgment explaining it.
|
|
173
|
+
*
|
|
174
|
+
* Runs at hook call START, before this call's own judgment rows land, so the window it reads
|
|
175
|
+
* is the one the previous comparison left open. Returns the record count as of right now —
|
|
176
|
+
* where the NEXT window opens, which {@link updateBaseline} persists at call end.
|
|
177
|
+
*
|
|
178
|
+
* The comparison records, it never blocks: no row it writes and no failure it hits changes
|
|
179
|
+
* a verdict or an exit code, which is why every caller keeps it outside the judgment path.
|
|
180
|
+
*/
|
|
181
|
+
export function compareBaseline(spec) {
|
|
182
|
+
const baselinePath = join(spec.repoRoot, '.polydeukes', 'baseline.json');
|
|
183
|
+
// Read before any row of this comparison lands, so the rows this call is about to write
|
|
184
|
+
// cannot fall inside the window they would then explain away.
|
|
185
|
+
const { records } = readRecords(spec.telemetryPath);
|
|
186
|
+
const stored = readBaseline(baselinePath);
|
|
187
|
+
if (stored === null) {
|
|
188
|
+
// Absence and corruption are the same signal. The baseline file is NOT on the protection
|
|
189
|
+
// list — protecting it would need a comparison of its own — so its disappearance has to
|
|
190
|
+
// stay legible in the log instead.
|
|
191
|
+
appendRecordFailOpen(spec.telemetryPath, {
|
|
192
|
+
event: 'unattributed',
|
|
193
|
+
label: BASELINE_LABEL,
|
|
194
|
+
subject: baselinePath,
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const changed = findUnattributed({
|
|
199
|
+
previous: stored.entries,
|
|
200
|
+
current: snapshotBaseline({ rootDir: spec.repoRoot, entries: spec.entries }),
|
|
201
|
+
records,
|
|
202
|
+
// The cut travels with the hashes it belongs to, from the one read above. Rows older
|
|
203
|
+
// than it were already spent explaining the state that snapshot recorded.
|
|
204
|
+
cutAt: stored.cutAt,
|
|
205
|
+
});
|
|
206
|
+
// One row per changed entry — an aggregate row could not say WHICH gate definition moved.
|
|
207
|
+
for (const entry of changed) {
|
|
208
|
+
appendRecordFailOpen(spec.telemetryPath, {
|
|
209
|
+
event: 'unattributed',
|
|
210
|
+
label: BASELINE_LABEL,
|
|
211
|
+
subject: entry,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Re-establish the baseline at hook call END.
|
|
217
|
+
*
|
|
218
|
+
* At call end rather than right after the comparison: refreshing at comparison time would
|
|
219
|
+
* miss whatever this call's own judged writes changed, leaving detection permanently one
|
|
220
|
+
* call behind.
|
|
221
|
+
*
|
|
222
|
+
* The cut is stamped HERE, beside the snapshot, not at the comparison that opened the call.
|
|
223
|
+
* Both describe the same instant — everything this call did is already folded into the
|
|
224
|
+
* hashes — so the rows explaining it belong before the cut. Stamping the earlier instant
|
|
225
|
+
* instead would re-admit this call's own judgment rows into the next window, where they
|
|
226
|
+
* would attribute a change they had nothing to do with: a call that merely MENTIONED a
|
|
227
|
+
* protected entry would then absolve any tamper that followed it.
|
|
228
|
+
*/
|
|
229
|
+
export function updateBaseline(spec) {
|
|
230
|
+
const dotDir = join(spec.repoRoot, '.polydeukes');
|
|
231
|
+
mkdirSync(dotDir, { recursive: true });
|
|
232
|
+
writeBaseline(join(dotDir, 'baseline.json'), snapshotBaseline({ rootDir: spec.repoRoot, entries: spec.entries }), new Date().toISOString());
|
|
233
|
+
}
|
package/dist/bin.d.ts
CHANGED
|
@@ -6,9 +6,8 @@
|
|
|
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
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the witness valve is structurally unreachable, so only a human at a terminal can arm it.
|
|
9
|
+
* `covenant check` reads its observation from stdin and nothing else: the IR JSON by
|
|
10
|
+
* default, a unified diff under `--diff`. No other file descriptor is opened, so the
|
|
11
|
+
* process never asks a human anything.
|
|
13
12
|
*/
|
|
14
13
|
export {};
|
package/dist/bin.js
CHANGED
|
@@ -6,50 +6,41 @@
|
|
|
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
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* the witness valve is structurally unreachable, so only a human at a terminal can arm it.
|
|
9
|
+
* `covenant check` reads its observation from stdin and nothing else: the IR JSON by
|
|
10
|
+
* default, a unified diff under `--diff`. No other file descriptor is opened, so the
|
|
11
|
+
* process never asks a human anything.
|
|
13
12
|
*/
|
|
14
|
-
import {
|
|
13
|
+
import { readFileSync, readSync } from 'node:fs';
|
|
15
14
|
import { dirname, join } from 'node:path';
|
|
16
15
|
import { fileURLToPath } from 'node:url';
|
|
17
16
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* Read stdin to EOF. `readFileSync(0)` returns only what the first read delivers, so a
|
|
18
|
+
* diff larger than the pipe buffer would arrive truncated and translate to a partial
|
|
19
|
+
* observation; this loops until a read answers zero bytes.
|
|
21
20
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
catch {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
return (prompt) => {
|
|
21
|
+
function readStdin() {
|
|
22
|
+
const chunks = [];
|
|
23
|
+
const buffer = Buffer.alloc(65536);
|
|
24
|
+
for (;;) {
|
|
25
|
+
let bytes;
|
|
31
26
|
try {
|
|
32
|
-
|
|
33
|
-
const buffer = Buffer.alloc(4096);
|
|
34
|
-
const bytes = readSync(fd, buffer, 0, buffer.length, null);
|
|
35
|
-
return buffer
|
|
36
|
-
.subarray(0, Math.max(bytes, 0))
|
|
37
|
-
.toString('utf-8')
|
|
38
|
-
.replace(/\r?\n$/, '');
|
|
27
|
+
bytes = readSync(0, buffer, 0, buffer.length, null);
|
|
39
28
|
}
|
|
40
|
-
catch {
|
|
41
|
-
|
|
29
|
+
catch (error) {
|
|
30
|
+
const { code } = error;
|
|
31
|
+
// A pipe with no writer left answers EOF this way on some platforms; EAGAIN is a
|
|
32
|
+
// non-blocking fd with nothing ready yet, which is not the end of the input.
|
|
33
|
+
if (code === 'EOF')
|
|
34
|
+
break;
|
|
35
|
+
if (code === 'EAGAIN')
|
|
36
|
+
continue;
|
|
37
|
+
throw error;
|
|
42
38
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// An EBADF thrown from this finally would override the `return null` above and
|
|
49
|
-
// escape the seam.
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
};
|
|
39
|
+
if (bytes === 0)
|
|
40
|
+
break;
|
|
41
|
+
chunks.push(Buffer.from(buffer.subarray(0, bytes)));
|
|
42
|
+
}
|
|
43
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
53
44
|
}
|
|
54
45
|
/**
|
|
55
46
|
* Write `text` to stdout and end the process — exit 0 once the write drains, exit 2 when
|
|
@@ -65,14 +56,14 @@ async function emitAndExit(text) {
|
|
|
65
56
|
process.exit(0);
|
|
66
57
|
}
|
|
67
58
|
const args = process.argv.slice(2);
|
|
68
|
-
if (args.length ===
|
|
59
|
+
if (args.length === 1 && args[0] === 'init') {
|
|
69
60
|
try {
|
|
70
|
-
// Imported inside the try, not above it: ESM imports are eager, so the
|
|
61
|
+
// Imported inside the try, not above it: ESM imports are eager, so the scaffold stays
|
|
71
62
|
// off `covenant check`'s load path, which a pre-commit hook spawns on every commit. A
|
|
72
63
|
// rejected import outside the try would reach node's unhandled-rejection exit 1, the
|
|
73
64
|
// exact crash this bin refuses to make.
|
|
74
|
-
const {
|
|
75
|
-
const { created, skipped } =
|
|
65
|
+
const { scaffoldProject } = await import('./scaffold-project.js');
|
|
66
|
+
const { created, skipped } = scaffoldProject(process.cwd());
|
|
76
67
|
for (const path of created) {
|
|
77
68
|
process.stdout.write(`created ${path}\n`);
|
|
78
69
|
}
|
|
@@ -84,24 +75,7 @@ if (args.length === 2 && args[0] === 'init' && args[1] === 'claude-code') {
|
|
|
84
75
|
catch (error) {
|
|
85
76
|
// A precondition failure leaves zero files; the message names what the user has to do
|
|
86
77
|
// before running this again.
|
|
87
|
-
process.stderr.write(`pdks init
|
|
88
|
-
process.exit(2);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
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`);
|
|
78
|
+
process.stderr.write(`pdks init failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
105
79
|
process.exit(2);
|
|
106
80
|
}
|
|
107
81
|
}
|
|
@@ -142,48 +116,57 @@ if (args.length === 1 && args[0] === 'explain') {
|
|
|
142
116
|
}
|
|
143
117
|
}
|
|
144
118
|
/**
|
|
145
|
-
* Read the `covenant check` flags
|
|
146
|
-
*
|
|
147
|
-
*
|
|
119
|
+
* Read the `covenant check` flags, or null for any argv outside the finite table: `--diff`
|
|
120
|
+
* at most once, `--enforce` at most once with `advise` or `block`, in either order, and
|
|
121
|
+
* nothing else.
|
|
148
122
|
*/
|
|
149
|
-
function
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (base === '' || head === '')
|
|
123
|
+
function parseCheckFlags(flags) {
|
|
124
|
+
let diffMode = false;
|
|
125
|
+
let enforce;
|
|
126
|
+
for (let i = 0; i < flags.length; i += 1) {
|
|
127
|
+
const flag = flags[i];
|
|
128
|
+
if (flag === '--diff' && !diffMode) {
|
|
129
|
+
diffMode = true;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (flag === '--enforce' && enforce === undefined) {
|
|
133
|
+
const level = flags[i + 1];
|
|
134
|
+
if (level !== 'advise' && level !== 'block')
|
|
135
|
+
return null;
|
|
136
|
+
enforce = level;
|
|
137
|
+
i += 1;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
167
140
|
return null;
|
|
168
|
-
|
|
141
|
+
}
|
|
142
|
+
return { diffMode, enforce };
|
|
169
143
|
}
|
|
170
|
-
const
|
|
171
|
-
if (
|
|
172
|
-
process.stderr.write('usage: pdks covenant check [--
|
|
144
|
+
const check = args[0] === 'covenant' && args[1] === 'check' ? parseCheckFlags(args.slice(2)) : null;
|
|
145
|
+
if (check === null) {
|
|
146
|
+
process.stderr.write('usage: pdks covenant check [--diff] [--enforce advise|block] | pdks explain | pdks init | pdks docs [topic | search <query> | show <document-id>]\n');
|
|
173
147
|
process.exit(2);
|
|
174
148
|
}
|
|
149
|
+
const { diffMode, enforce } = check;
|
|
175
150
|
try {
|
|
176
151
|
// Loaded here rather than at the top of the file. This runner statically pulls in the
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
152
|
+
// core and the judge, so a top-level import made every subcommand wait on both
|
|
153
|
+
// resolving — and `docs` is the one that has to answer in a tree where they do not,
|
|
154
|
+
// since a package installed but never built is exactly the state `pdks docs install`
|
|
155
|
+
// is asked about. The catch below already answers for whatever this import cannot do,
|
|
156
|
+
// at the same exit 2 it answers everything else with.
|
|
182
157
|
const { runCovenantCheck } = await import('./covenant-check.js');
|
|
158
|
+
const { covenantInputFromUnifiedDiff } = await import('./diff-ir.js');
|
|
159
|
+
const text = readStdin();
|
|
183
160
|
const { exitCode } = await runCovenantCheck({
|
|
184
161
|
repoRoot: process.cwd(),
|
|
185
|
-
|
|
186
|
-
|
|
162
|
+
// A thunk, not a value: the runner settles the telemetry path before calling it, so a
|
|
163
|
+
// translation or parse failure lands as the same one blocked row every other
|
|
164
|
+
// fail-closed branch leaves.
|
|
165
|
+
input: () => (diffMode ? covenantInputFromUnifiedDiff({ text }) : JSON.parse(text)),
|
|
166
|
+
// The input mode is the surface: a diff is a finished change set, anything else is one
|
|
167
|
+
// call the caller observed.
|
|
168
|
+
surface: diffMode ? 'changeSet' : 'session',
|
|
169
|
+
...(enforce !== undefined && { enforce }),
|
|
187
170
|
});
|
|
188
171
|
process.exit(exitCode);
|
|
189
172
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash command-line tokenizer + mutation-target extraction core.
|
|
3
|
+
*
|
|
4
|
+
* Pure functions only — zero I/O, spawn, or logging. A hand-rolled single-pass character
|
|
5
|
+
* scanner recognizes quote state (`'`, `"`, `$'…'`, `\`), control operators
|
|
6
|
+
* (`;` `&&` `||` `|` `&`), redirect operators (`>` `>>` `>|` `<` `2>` `&>`, attached `>f`),
|
|
7
|
+
* and marks tokens opaque when their static value is unknowable (command substitution,
|
|
8
|
+
* parameter expansion, globs).
|
|
9
|
+
*
|
|
10
|
+
* Fail-closed: no input ever throws. A construct the scanner cannot finish reading yields a
|
|
11
|
+
* partial result — the commands it did read, plus one `unread` span per failure; in
|
|
12
|
+
* {@link extractMutations} each span becomes one indeterminate entry. Block/allow
|
|
13
|
+
* decisions, read-only allowlists, and detection rules live in the judges that consume this.
|
|
14
|
+
*/
|
|
15
|
+
/** A single word token with a static-opacity flag (`opaque` = value not knowable). */
|
|
16
|
+
export type WordToken = {
|
|
17
|
+
text: string;
|
|
18
|
+
opaque: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** A redirect operator paired with its target word (`>` `>>` `<` `2>` `&>`). */
|
|
21
|
+
export type RedirectToken = {
|
|
22
|
+
operator: string;
|
|
23
|
+
target: WordToken;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* One heredoc body a command declared, with its delimiter's quoting. `literal` means the
|
|
27
|
+
* delimiter was quoted, so the body is written verbatim; an unquoted delimiter expands its
|
|
28
|
+
* body, which only a consumer can decide what to do about.
|
|
29
|
+
*/
|
|
30
|
+
export type HeredocBody = {
|
|
31
|
+
body: string;
|
|
32
|
+
literal: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* One simple command: its word tokens, any redirect operators, and the heredoc bodies it
|
|
36
|
+
* declared (in declaration order — absent when it declared none).
|
|
37
|
+
*/
|
|
38
|
+
export type SimpleCommand = {
|
|
39
|
+
words: WordToken[];
|
|
40
|
+
redirects: RedirectToken[];
|
|
41
|
+
heredocs?: HeredocBody[];
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A span of the line the scanner could not read, with the reason it stopped. `reason` is a
|
|
45
|
+
* telemetry pass-through value: no consumer branches on it.
|
|
46
|
+
*/
|
|
47
|
+
export type UnreadSpan = {
|
|
48
|
+
text: string;
|
|
49
|
+
reason: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* A half-open span `[start, end)` of the line bash hands to a command as stdin data instead
|
|
53
|
+
* of executing: a heredoc body, or a herestring's target word.
|
|
54
|
+
*/
|
|
55
|
+
export type DataSpan = {
|
|
56
|
+
start: number;
|
|
57
|
+
end: number;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The tokenizer's result: the commands it read, plus one span per failure it hit. A failure
|
|
61
|
+
* does not discard the line — the read commands reach precise judgment and only the spans
|
|
62
|
+
* fall to a consumer's conservative treatment. An empty `unread` is the "fully read" signal.
|
|
63
|
+
*
|
|
64
|
+
* `data` lists the stdin-data spans in source order, non-overlapping: heredoc bodies and
|
|
65
|
+
* herestring words bash hands over without expanding them. A body under an unquoted
|
|
66
|
+
* delimiter that carries `$` or a backtick, and an opaque herestring word, are text bash
|
|
67
|
+
* runs first and are not data. A span the scanner could not finish reading is never among
|
|
68
|
+
* them either, so unread bytes stay on the command line.
|
|
69
|
+
*/
|
|
70
|
+
export type TokenizeResult = {
|
|
71
|
+
commands: SimpleCommand[];
|
|
72
|
+
unread: UnreadSpan[];
|
|
73
|
+
data: DataSpan[];
|
|
74
|
+
};
|
|
75
|
+
/** A detected mutation target (path) with the name of the rule that found it. */
|
|
76
|
+
export type MutationTarget = {
|
|
77
|
+
path: string;
|
|
78
|
+
rule: string;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* A detection rule seam: a pure function over a single simple command that returns the
|
|
82
|
+
* mutation targets it detects.
|
|
83
|
+
*/
|
|
84
|
+
export type MutationRule = {
|
|
85
|
+
name: string;
|
|
86
|
+
detect(command: SimpleCommand): MutationTarget[];
|
|
87
|
+
};
|
|
88
|
+
/** A structure that cannot be decided deterministically, with the reason why. */
|
|
89
|
+
export type Indeterminate = {
|
|
90
|
+
reason: string;
|
|
91
|
+
};
|
|
92
|
+
/** The extraction result — detected mutations and undecidable structures, kept separate. */
|
|
93
|
+
export type MutationAnalysis = {
|
|
94
|
+
mutations: MutationTarget[];
|
|
95
|
+
indeterminate: Indeterminate[];
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* True when `commandName` (a command word's basename) re-parses its string arguments in a
|
|
99
|
+
* nested shell — the reinterpretation boundary the tokenizer refuses to parse into. A judge
|
|
100
|
+
* can consult this to refuse to treat such a command as provably read-only.
|
|
101
|
+
*/
|
|
102
|
+
export declare function isNestedShellCommand(commandName: string): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The word that names the command, skipping any leading assignments — undefined when the
|
|
105
|
+
* command is nothing but assignments.
|
|
106
|
+
*
|
|
107
|
+
* Read at the nested-shell boundary only. There an unskipped assignment hides `bash` behind
|
|
108
|
+
* `FOO=1` and the line passes with confidence; for the read-only allowlist and precedent
|
|
109
|
+
* evidence the same miss is the conservative direction, so those keep reading `words[0]`.
|
|
110
|
+
*/
|
|
111
|
+
export declare function commandNameWord(command: SimpleCommand): WordToken | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* Tokenize one shell line into simple commands. Never throws, and never discards what it
|
|
114
|
+
* read: a construct it cannot finish reading is recorded as an `unread` span and the scan
|
|
115
|
+
* carries on.
|
|
116
|
+
*/
|
|
117
|
+
export declare function tokenizeCommandLine(line: string): TokenizeResult;
|
|
118
|
+
/**
|
|
119
|
+
* The executed text of a command line: the line with every `data` span deleted and nothing
|
|
120
|
+
* put in its place, so the words bash runs keep their spelling and line structure. Bytes
|
|
121
|
+
* the scanner could not finish reading are not data spans and stay as written.
|
|
122
|
+
*/
|
|
123
|
+
export declare function executedText(line: string): string;
|
|
124
|
+
/**
|
|
125
|
+
* Extract mutation targets from a shell line via injected rules. A simple command
|
|
126
|
+
* contributes an indeterminate entry when it is a nested-shell call OR contains any opaque
|
|
127
|
+
* word (in which case its rules are still applied, but an undecidable structure is present);
|
|
128
|
+
* each unread span yields one more. Never throws.
|
|
129
|
+
*/
|
|
130
|
+
export declare function extractMutations(line: string, rules: MutationRule[]): MutationAnalysis;
|