polydeukes 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,8 @@
6
6
  증거를 고르고, 추출 과정과 관계를 적은 다음 위반과 정상 사례를 각각 실행합니다.
7
7
  관측 결과를 보고 차단이 필요하다고 판단하기 전까지는 기본 강제 수준인 `advise`를 유지합니다.
8
8
 
9
+ 관계와 추출 연산의 전체 문법은 [선언 언어 참조](../reference/declaration-language/index.ko.md)에서 확인할 수 있습니다.
10
+
9
11
  <a id="locale-key-pairing"></a>
10
12
  ## 번역 키 짝 맞춤
11
13
 
@@ -90,13 +92,55 @@ git restore -- locales/en.json locales/ko.json
90
92
  소스 파일이 존재한다는 이유만으로 선언이 실행되지는 않습니다. 관측된 변경 중 하나
91
93
  이상이 해당 선언의 적용 범위와 일치해야 합니다.
92
94
 
95
+ <a id="locale-key-pairing-many"></a>
96
+ ### 번역 파일이 셋 이상일 때
97
+
98
+ `equal`은 추출 결과 두 개를 비교합니다. 파일이 셋 이상이면 모든 파일의 키를 모은 합집합을
99
+ 만들고, 파일마다 `subset` 하나로 그 합집합을 모두 가지고 있는지 확인합니다. `onlyIn`은
100
+ 합집합에 아직 없는 키만 더하므로, 여러 파일에 있는 키도 그 키가 빠진 파일마다 증인 1건으로
101
+ 나옵니다. 이 방법은 `flattenKeys`가 각 항목의 키와 값에 같은 점 경로를 싣는다는 점에 기댑니다.
102
+ `onlyIn`은 키를 비교하고 `subset`은 값을 비교하기 때문입니다. 줄 번호를 키로 삼는 `lines`처럼
103
+ 키와 값이 다른 추출에는 이 방법을 쓸 수 없습니다.
104
+
105
+ `ko`·`en`·`fr` 세 파일이라면 위 예제의 `locale-key-parity` 항목을 다음으로 바꿉니다. `supply:
106
+ 'error'`는 없는 파일을 거부하므로, 위 절차를 실행하기 전에 같은 키를 가진 `locales/fr.json`도
107
+ 만듭니다(`printf '{"home":"Accueil"}\n' > locales/fr.json`).
108
+
109
+ ```yaml
110
+ - id: 'locale-key-parity'
111
+ why: 'every locale file must carry the same keys'
112
+ declare:
113
+ mechanism: 'pairing'
114
+ sources:
115
+ ko: { file: 'locales/ko.json' }
116
+ en: { file: 'locales/en.json' }
117
+ fr: { file: 'locales/fr.json' }
118
+ supply: { ko: 'error', en: 'error', fr: 'error' }
119
+ scope: { source: 'target.path', include: ['^locales/(ko|en|fr)\.json$'] }
120
+ extract:
121
+ ko: [{ op: 'source', of: 'ko' }, { op: 'json' }, { op: 'flattenKeys' }]
122
+ en: [{ op: 'source', of: 'en' }, { op: 'json' }, { op: 'flattenKeys' }]
123
+ fr: [{ op: 'source', of: 'fr' }, { op: 'json' }, { op: 'flattenKeys' }]
124
+ enNew: [{ op: 'onlyIn', of: 'en', notIn: 'ko' }]
125
+ koEn: [{ op: 'union', of: ['ko', 'enNew'] }]
126
+ frNew: [{ op: 'onlyIn', of: 'fr', notIn: 'koEn' }]
127
+ all: [{ op: 'union', of: ['koEn', 'frNew'] }]
128
+ relate:
129
+ - { id: 'ko-full', relation: { op: 'subset', of: 'all', in: 'ko' }, message: '{value} is missing from ko' }
130
+ - { id: 'en-full', relation: { op: 'subset', of: 'all', in: 'en' }, message: '{value} is missing from en' }
131
+ - { id: 'fr-full', relation: { op: 'subset', of: 'all', in: 'fr' }, message: '{value} is missing from fr' }
132
+ ```
133
+
134
+ 기준이 되는 파일은 없습니다. `ko`에만 있는 키는 `en-full`과 `fr-full`을 위반하고, `en`과
135
+ `fr`에만 있는 키는 `ko-full` 하나만 위반합니다. 파일이 하나 늘 때마다 추출 세 개(그 파일의 키,
136
+ `onlyIn`, 다음 `union`)와 관계 항목 하나가 늘고, 모든 관계 항목의 `of`는 그 마지막 `union`으로
137
+ 옮깁니다.
138
+
93
139
  <a id="which-list"></a>
94
140
  ## 어느 목록에 적는가
95
141
 
96
- 규율 목록은 셋이고, 항목이 어느 목록에 속하는지는 소스 축이 정합니다. 기전이 정하지도 않고
97
- 관계가 정하지도 않습니다. 같은 `companion` 기전이라도 `file` 소스 위에 서면 `disciplines`에
98
- 있고 `changes` 위에 서면 `changeSetDisciplines`에 있습니다. 선언의 소스를 읽으면 목록이
99
- 따라 나옵니다.
142
+ 선언이 읽는 소스에 따라 규율 목록을 선택합니다. 예를 들어 `companion` 선언이 `file`
143
+ 소스만 읽으면 `disciplines`에, `changes`를 읽으면 `changeSetDisciplines`에 적습니다.
100
144
 
101
145
  | 선언이 읽는 것 | 목록 | 예 |
102
146
  |---|---|---|
@@ -112,37 +156,30 @@ git restore -- locales/en.json locales/ko.json
112
156
  됩니다. 규칙 자체와 오류 모양은
113
157
  [설정 참조](../reference/configuration/index.ko.md#placement-rule)에 있습니다.
114
158
 
115
- 표면이 공급해야 것을 본체가 읽지 않아도 실제로는 표면에 묶이는 항목이 있습니다. 대화
116
- 기록을 읽는 밸브(`witness`)가 달린 항목은 밸브 자신의 `extract`가 대화 기록을 묶으므로 세션
117
- 항목이 됩니다.
159
+ 목록을 고를 때는 `witness` 블록의 소스도 포함합니다. 블록의 `extract`가 대화 기록을
160
+ 읽으면 항목 전체를 `sessionDisciplines`에 적습니다.
118
161
 
119
162
  <a id="posture"></a>
120
163
  ## 무인 실시간 표면의 기본 자세
121
164
 
122
- 무인 실시간 표면은 터미널 앞에 사람이 없는 어댑터 훅이나 SDK 호출자입니다. 사람이 지켜보는
123
- 자리에는 적용되지 않는 규칙 둘이 이곳에 적용됩니다.
124
-
125
- **루프가 회차 안에서 스스로 고치지 못하는 항목을 `enforce: block`으로 올립니다.** 기준은
126
- "되돌릴 없는가"가 아닙니다. 실시간 차단의 비용은 단위입니다. 모델이 stderr의 사유를
127
- 읽고 다시 시도하므로 위반이 회차 안에서 고쳐집니다. `advise`로 두면 같은 위반이 뒤의
128
- 검사, 테스트 실행이나 CI나 리뷰어에게까지 가서 회차 하나를 쓰며 최대 45분이 듭니다.
129
- "무인이니 전부 차단"이라는 뜻은 아닙니다. 차단은 회피를 낳고 회피는 텔레메트리 행을 남기지
130
- 않으므로, 루프가 손댈 수 없는 항목은 위반이라도 기록되는 `advise`에 두는 편이 낫습니다. 이
131
- 기준은 설정 저자의 것입니다.
132
-
133
- **밸브가 없으므로 사유가 값으로 돌아옵니다.** 실시간 무인 표면에는 증인 밸브가 없습니다.
134
- TTY도 사람의 턴도 없고, SDK는 증인 인자를 받지 않으며 세션을 지어내지도 않습니다. 그 자리를
135
- 대신하는 것이 사유를 데이터로 돌려주는 일입니다. `checkCovenant`는
136
- `{ verdict: 'blocked', reason }`을 돌려주며 `reason`은 판정기 자신의 stderr이고,
137
- `{ verdict: 'upheld', advisories }`는 종료 코드 0인 실행의 권고 줄을 싣습니다. 소비자는 그
138
- 텍스트를 사람이 나중에 읽는 자리, 이슈나 로그에 적고 멈춥니다. 권고 텍스트를 모델에게
139
- 보일지도 소비자가 정합니다. 무인 루프에는 stderr 한 줄을 읽을 사람이 없으므로, 호출자가
140
- 전달해야 권고가 소비됩니다. 판정 결과의 모양은
141
- [`@polydeukes/sdk-ts` 참조](../reference/packages/sdk-ts.ko.md)에 있습니다.
142
-
143
- SDK 자신의 기본값은 실행 전체에 대한 `enforce: 'block'`이며, 이것은 표면의 강제 수준이지
144
- 항목의 것이 아닙니다. 보호 경로와 `enforce: block` 항목이 호출을 멈추고, 나머지 위반은
145
- `advised`로 기록됩니다. 두 어댑터도 같은 방식으로 판정기를 스폰합니다.
165
+ 사람이 지켜보지 않는 어댑터 훅이나 SDK 호출자는 차단과 권고를 받았을 어떻게 처리할지
166
+ 정해야 합니다.
167
+
168
+ **권고만으로는 루프가 위반을 수정하지 않을 `enforce: block`을 사용합니다.** 호출자는
169
+ 모델에게 사유와 재시도 방법을 전달해야 합니다. 루프가 진단에 따라 조치할 없는 항목은
170
+ `advise`로 두고, 기록된 위반을 사람이 검토할 있게 합니다. 해당 루프의 실제 관측 결과를
171
+ 바탕으로 강제 수준을 선택하세요.
172
+
173
+ **진단을 호출자에게 돌려줍니다.** `checkCovenant`는 판정기의 stderr를 담은
174
+ `{ verdict: 'blocked', reason }` 또는 종료 코드 0인 실행의 권고를 담은
175
+ `{ verdict: 'upheld', advisories }`를 반환합니다. SDK는 별도의 증인 인자를 받지 않습니다.
176
+ 호출자는 진단을 모델에게 전달할지, 이슈나 로그에 기록할지, 재시도하거나 중단할지 정합니다.
177
+ 권고도 호출자가 전달해야 모델이 읽을 있습니다.
178
+ 반환 타입은 [SDK 참조](../reference/packages/sdk-ts.ko.md#verdicts)에 있습니다.
179
+
180
+ SDK는 실행 전체에 기본값 `enforce: 'block'`을 적용합니다. 수준에서는 보호 경로와
181
+ `enforce: block` 항목이 호출을 멈출 있고, 나머지 규율 위반은 `advised`로 기록됩니다.
182
+ 에이전트 어댑터도 같은 설정을 사용합니다.
146
183
 
147
184
  <a id="when-to-draft"></a>
148
185
  ## 선언 대신 초안으로 남길 때
@@ -6,6 +6,8 @@ A discipline describes a practice you want checked. Choose the observed files or
6
6
  write an extraction and relation, then exercise both a violation and a valid case. Leave enforcement
7
7
  at `advise` until you decide that the observed results justify blocking.
8
8
 
9
+ For the complete syntax of relations and extraction steps, see the [Declaration language reference](../reference/declaration-language/index.md).
10
+
9
11
  <a id="locale-key-pairing"></a>
10
12
  ## Locale key pairing
11
13
 
@@ -88,14 +90,56 @@ before it appears in the diff. This example commits a baseline to exercise modif
88
90
  cleanup predictable. A declaration does not run
89
91
  merely because its source exists: at least one observed change must match its scope.
90
92
 
93
+ <a id="locale-key-pairing-many"></a>
94
+ ### Three or more locale files
95
+
96
+ `equal` compares two extractions. For three or more files, build the union of every file's keys
97
+ and require each file to contain it: one `subset` per file. `onlyIn` adds only the keys the union
98
+ does not hold yet, so a key present in several files is still one witness per file that lacks it.
99
+ The recipe relies on `flattenKeys` giving each item its dot path as both key and value: `onlyIn`
100
+ compares keys and `subset` compares values. An extraction whose keys differ from its values, such
101
+ as `lines` (keyed by line number), cannot use it.
102
+
103
+ For `ko`, `en`, and `fr`, replace the `locale-key-parity` entry above with this one, and create
104
+ `locales/fr.json` with the same keys (`printf '{"home":"Accueil"}\n' > locales/fr.json`) before
105
+ running the walkthrough, since `supply: 'error'` refuses a missing file:
106
+
107
+ ```yaml
108
+ - id: 'locale-key-parity'
109
+ why: 'every locale file must carry the same keys'
110
+ declare:
111
+ mechanism: 'pairing'
112
+ sources:
113
+ ko: { file: 'locales/ko.json' }
114
+ en: { file: 'locales/en.json' }
115
+ fr: { file: 'locales/fr.json' }
116
+ supply: { ko: 'error', en: 'error', fr: 'error' }
117
+ scope: { source: 'target.path', include: ['^locales/(ko|en|fr)\.json$'] }
118
+ extract:
119
+ ko: [{ op: 'source', of: 'ko' }, { op: 'json' }, { op: 'flattenKeys' }]
120
+ en: [{ op: 'source', of: 'en' }, { op: 'json' }, { op: 'flattenKeys' }]
121
+ fr: [{ op: 'source', of: 'fr' }, { op: 'json' }, { op: 'flattenKeys' }]
122
+ enNew: [{ op: 'onlyIn', of: 'en', notIn: 'ko' }]
123
+ koEn: [{ op: 'union', of: ['ko', 'enNew'] }]
124
+ frNew: [{ op: 'onlyIn', of: 'fr', notIn: 'koEn' }]
125
+ all: [{ op: 'union', of: ['koEn', 'frNew'] }]
126
+ relate:
127
+ - { id: 'ko-full', relation: { op: 'subset', of: 'all', in: 'ko' }, message: '{value} is missing from ko' }
128
+ - { id: 'en-full', relation: { op: 'subset', of: 'all', in: 'en' }, message: '{value} is missing from en' }
129
+ - { id: 'fr-full', relation: { op: 'subset', of: 'all', in: 'fr' }, message: '{value} is missing from fr' }
130
+ ```
131
+
132
+ No file is the reference: a key only in `ko` breaks `en-full` and `fr-full`, and a key in `en`
133
+ and `fr` but not `ko` breaks `ko-full` alone. Each further file adds three extractions (its
134
+ keys, its `onlyIn`, the next `union`) and one relate entry, and every relate entry's `of` moves
135
+ to that last `union`.
136
+
91
137
  <a id="which-list"></a>
92
138
  ## Which list does it go in
93
139
 
94
- There are three discipline lists, and the source axis decides which one an entry belongs to.
95
- The mechanism does not decide it and the relation does not decide it: the same `companion`
96
- mechanism sits in `disciplines` when it stands over `file` sources, and in
97
- `changeSetDisciplines` when it stands over `changes`. Read the declaration's sources and the
98
- list follows.
140
+ Choose a discipline list by the sources the declaration reads. For example, a `companion`
141
+ declaration reading only `file` sources belongs in `disciplines`; one reading `changes`
142
+ belongs in `changeSetDisciplines`.
99
143
 
100
144
  | The declaration reads | List | Examples |
101
145
  |---|---|---|
@@ -111,37 +155,30 @@ names the entry, the channels it reads, and the list it belongs in, so the fix i
111
155
  body unchanged. The rule itself and the error shapes are in [the configuration
112
156
  reference](../reference/configuration/index.md#placement-rule).
113
157
 
114
- An entry can also read nothing a surface has to supply and still be surface-bound in practice:
115
- a valve (`witness`) that reads the transcript makes its entry a session entry, because the
116
- valve's own `extract` binds the transcript.
158
+ Include the `witness` block's sources when choosing a list. If its `extract` reads the
159
+ transcript, the whole entry belongs in `sessionDisciplines`.
117
160
 
118
161
  <a id="posture"></a>
119
162
  ## Posture on an unattended real-time surface
120
163
 
121
- An unattended real-time surface is an adapter hook or an SDK caller with no human at the
122
- terminal. Two rules apply there that do not apply where a person is watching.
123
-
124
- **Promote an entry to `enforce: block` when the loop cannot fix it inside the turn.** The
125
- criterion is not "is this irreversible". A real-time block costs seconds: the model reads the
126
- reason on stderr and retries, so the violation is corrected within the turn. Left at `advise`,
127
- the same violation travels to a later check — a test run, CI, a reviewer — and costs a whole
128
- turn, up to 45 minutes. This is not "block everything because nobody is watching": blocking
129
- produces avoidance, and avoidance leaves no telemetry row, so an entry the loop cannot act on
130
- belongs at `advise` where its break is at least recorded. The criterion is the config author's.
131
-
132
- **The reason comes back as a value, because there is no valve.** A real-time unattended
133
- surface has no witness valve: there is no TTY and no human turn, and the SDK takes no witness
134
- argument and invents no session. What stands in its place is the reason travelling as data.
135
- `checkCovenant` returns `{ verdict: 'blocked', reason }` where `reason` is the judge's own
136
- stderr, and `{ verdict: 'upheld', advisories }` carries the advisory lines of an exit-0 run.
137
- The consumer writes that text where a person reads it later an issue, a log — and stops.
138
- Whether the model sees the advisory text is the consumer's decision too: an unattended loop has
139
- no reader for a stderr line, so advise is only consumed if the caller passes it on. The
140
- [`@polydeukes/sdk-ts` reference](../reference/packages/sdk-ts.md) has the verdict shapes.
141
-
142
- The SDK's own default is `enforce: 'block'` for the run, which is the surface's level, not an
143
- entry's: protected paths and `enforce: block` entries stop the call, and every other break is
144
- recorded as `advised`. Both adapters spawn the judge the same way.
164
+ An unattended adapter hook or SDK caller needs an explicit response to blocked and advised
165
+ judgments.
166
+
167
+ **Use `enforce: block` when an advisory alone will not lead the loop to correct a violation.**
168
+ The caller must give the model the reason and a way to retry. Keep an entry at `advise` if the
169
+ loop cannot act on its diagnostic, and arrange for someone to review the recorded violations.
170
+ Choose the level based on observations from that loop.
171
+
172
+ **Return diagnostics to the caller.** `checkCovenant` returns
173
+ `{ verdict: 'blocked', reason }` with the judge's stderr, or `{ verdict: 'upheld', advisories }`
174
+ with the advisory output of an exit-0 run. The SDK accepts no separate witness argument.
175
+ The caller decides whether to send diagnostics to the model, record them in an issue or log,
176
+ and retry or stop. An advisory reaches the model only if the caller forwards it.
177
+ See the [SDK reference](../reference/packages/sdk-ts.md#verdicts) for the return types.
178
+
179
+ The SDK defaults to `enforce: 'block'` for the whole run. At that level, protected paths and
180
+ entries with `enforce: block` can stop the call; other discipline violations remain `advised`.
181
+ The three agent adapters use the same setting.
145
182
 
146
183
  <a id="when-to-draft"></a>
147
184
  ## When to draft instead of declaring