refactor: 문서 개선 중
This commit is contained in:
@@ -124,7 +124,7 @@ function strip(src) {
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
function positiveChecks(text, lines, docMode, rulesMode) {
|
||||
function positiveChecks(text, lines, docMode, rulesMode, recordKind = '') {
|
||||
const out = [];
|
||||
const sentences = text.split(/(?<=[.?!])\s+|\n{2,}/).map(x => x.trim()).filter(Boolean);
|
||||
|
||||
@@ -217,14 +217,21 @@ function positiveChecks(text, lines, docMode, rulesMode) {
|
||||
// 바꿔 둔 자리) 판단할 수 없으니 건너뛴다. 「이름 : 값」 줄도 문장이 아니라 건너뛴다.
|
||||
const DANGLING = /(때|고|며|면|를|을|는|은|이|가|에서|으로|에|와|과|도|서|아|어|지|니|라서|라|의)$/;
|
||||
let inFront = lines[0] === '---';
|
||||
let currentSection = '';
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (inFront) { if (i > 0 && line === '---') inFront = false; continue; }
|
||||
const heading = /^##\s+(.+?)\s*$/.exec(line.trim());
|
||||
if (heading) currentSection = heading[1];
|
||||
const t = line.trimEnd();
|
||||
if (!t.trim() || t !== line) continue; // 빈 줄 · 끝에 공백(인라인 코드 자리)
|
||||
if (/^\s*(#|-|\*|\d+\.|:::|<!--|\||>)/.test(t) || t.includes(' : ')) continue;
|
||||
const next = (lines[i + 1] ?? '').trim();
|
||||
if (next !== '') continue; // 문단이 이어진다
|
||||
// REFERENCE의 `적용 조건`은 Studio schema상 독립 문장 본문이 아니라 조건 값 목록이다.
|
||||
// `~할 때`, 명사구 같은 fragment를 문장으로 늘리면 오히려 렌더링이 장황해진다.
|
||||
// 이 예외는 해당 kind/section에만 한정하고, 다른 본문의 dangling 문장은 계속 막는다.
|
||||
if (recordKind === 'REFERENCE' && currentSection === '적용 조건') continue;
|
||||
if (DANGLING.test(t)) {
|
||||
out.push({ id: 'dangling-sentence', sev: ERR, line: i + 1,
|
||||
msg: `문장이 끝나지 않은 채 문단이 끝납니다("…${t.slice(-24)}"). 지우다 남은 조각이거나 마침표가 빠졌습니다.` });
|
||||
@@ -289,6 +296,8 @@ const files = args.filter(a => !a.startsWith('--'));
|
||||
let errTotal = 0;
|
||||
for (const file of files) {
|
||||
const raw = readFileSync(file, 'utf8');
|
||||
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
||||
const recordKind = front?.[1].match(/^kind:\s*(\S+)/m)?.[1] ?? '';
|
||||
const text = strip(raw);
|
||||
const lines = text.split('\n');
|
||||
const hits = [];
|
||||
@@ -302,7 +311,7 @@ for (const file of files) {
|
||||
}
|
||||
}
|
||||
});
|
||||
for (const p of positiveChecks(text, lines, docMode, rulesMode)) hits.push({ line: null, ...p, match: null });
|
||||
for (const p of positiveChecks(text, lines, docMode, rulesMode, recordKind)) hits.push({ line: null, ...p, match: null });
|
||||
|
||||
const errs = hits.filter(h => h.sev === ERR);
|
||||
const warns = hits.filter(h => h.sev === WARN);
|
||||
|
||||
@@ -70,15 +70,44 @@ Agent(subagent_type="record-writer", prompt=<references/subagent-prompts.md 의
|
||||
그것을 되풀이하지 않아도 되는 것이 이 방식의 값이다 — 프롬프트는 **이번 런의 입력 경로와
|
||||
글감**만 준다.
|
||||
|
||||
**단계가 아닌 에이전트가 넷 더 있다.** 기록 한 편을 놓고 역할을 가른 것이라 아무 단계에도
|
||||
붙지 않는다. 부를지는 사람이 정하고, 원장의 단계 칸에는 안 들어간다.
|
||||
**단계 밖의 보조 에이전트도 역할을 고정한다.** 선택적으로 부르는 리뷰와 v3 런에서 절차상
|
||||
필요한 command/fact review를 섞지 않는다. `qualityReviews`에 들어가는 역할은 단계 칸과 별도로
|
||||
검증기가 확인한다.
|
||||
|
||||
| 에이전트 | 언제 | 안 하는 일 |
|
||||
|---|---|---|
|
||||
| `source-auditor` | S3 앞. 원본의 주장을 `관측`·`추론`·`미검증`으로 가른 표를 만든다 | 기록을 안 쓴다 |
|
||||
| `fact-reviewer` | 쓴 뒤. 결과 문장을 원문과 한 글자씩 역대조한다 | 파일을 안 고친다 |
|
||||
| `reader-reviewer` | 쓴 뒤. 제목·요약·목차만 보고 30초 안에 읽히는지 본다 | 본문을 안 연다 |
|
||||
| `setup-runner` | Setup 을 쓴 뒤. 손으로 끝까지 칠 수 있는지 읽는다 | 실제로 치지는 않는다 |
|
||||
| `command-pedagogy-planner` | S3 뒤 initial analysis에 finding이 있을 때 | Markdown을 안 고친다 |
|
||||
| `command-pedagogy-editor` | planner가 만든 plan을 command span에 반영할 때 | command block 밖의 산문을 안 고친다 |
|
||||
| `command-pedagogy-reviewer` | S6 뒤 final analysis에 shell/CLI block이 있을 때 | 파일을 안 고친다 |
|
||||
| `fact-reviewer` | command/prose/voice 수정이 모두 끝난 S6 뒤 | 파일을 안 고친다 |
|
||||
| `reader-reviewer` | 필요할 때 제목·요약·목차만 보고 30초 안에 읽히는지 본다 | 본문을 안 연다 |
|
||||
| `setup-runner` | Setup을 쓴 뒤 추가 운영성 검토가 필요할 때 | 실제로 치지는 않는다 |
|
||||
|
||||
## Command pedagogy lane — S3 직후 초벌, S6 뒤 최종 리뷰
|
||||
|
||||
S3이 기록을 만들면 먼저 결정론적 분석을 남긴다.
|
||||
|
||||
```bash
|
||||
python3 scripts/check-command-pedagogy.py <기록.md> \
|
||||
-o runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
```
|
||||
|
||||
- **shell/CLI block이 없으면** command planner/editor/reviewer를 모두 `SKIPPED`로 적고 사유를 남긴다.
|
||||
- **finding이 없으면 planner/editor**는 `SKIPPED`다. 단, shell/CLI가 있으므로 최종 reviewer는 S6 뒤에 돈다.
|
||||
- finding이 있으면 `command-pedagogy-planner`가 frozen plan을 만들고 `command-pedagogy-editor`가
|
||||
`CommandPatchSet`만 만든다. editor가 기록 전체를 직접 고치지 않는다.
|
||||
- patch 적용은 `scripts/apply-command-pedagogy-patch.py`만 쓴다. 이 도구는 initial analysis의
|
||||
hash와 span을 대조해 **원래 command block 밖의 산문을 건드리지 못하게 한다.**
|
||||
- S5와 S6은 명령어를 보호 구간으로 취급한다. 둘이 끝난 뒤 같은 deterministic analyzer를 다시
|
||||
돌려 `finalAnalysis`를 남긴다. initial에 command block이 있었는데 final에서 전부 사라지거나
|
||||
`majorFindings > 0`이면 끝난 것이 아니다.
|
||||
- final에 shell/CLI가 하나라도 있으면 `command-pedagogy-reviewer`가 독립적으로 `PASS`해야 한다.
|
||||
`FAIL`과 `UNCERTAIN`은 막는다.
|
||||
- 마지막으로 `fact-reviewer`가 **모든 command/prose/voice 수정 이후** 기술 사실을 역대조해 `PASS`해야 한다.
|
||||
|
||||
이 영수증은 `run.json`의 `qualityReviews.commandPedagogy`와
|
||||
`qualityReviews.technicalEvidence`에 남긴다. command/fact review 자체는 schemaVersion 3 계약이다. schemaVersion 4부터는 analysis/plan/patch/review JSON의 repo-relative path+sha256과 최종 기록 `sourceSha256`까지 남긴다. v3 이전/이전 형식 원장에 새 영수증을 소급해 만들지 않는다. `CommandPlan`/`CommandPatchSet`은 `scripts/validate-command-pedagogy-artifact.py`로 검증한다.
|
||||
|
||||
## 순서가 고정된 곳
|
||||
|
||||
@@ -95,8 +124,9 @@ Agent(subagent_type="record-writer", prompt=<references/subagent-prompts.md 의
|
||||
S6 이 채울 자리(선택·비교·어긋남)가 보인다. 순서를 뒤집으면 S5 가 S6 이 넣은 목소리를
|
||||
「과한 대구」로 다시 깎는다.
|
||||
|
||||
**S6 → S7.** Studio 에는 문장 손질이 끝난 것만 넣는다. 저장한 뒤에 고치면 Studio 쪽 `version`
|
||||
이 올라가고 검증 산출물이 무효가 된다.
|
||||
**S6 → final command review → fact review → S7.** Studio에는 문장 손질뿐 아니라 command-pedagogy와
|
||||
technical-evidence 검토까지 끝난 것만 넣는다. 저장한 뒤에 고치면 Studio 쪽 `version`이 올라가고
|
||||
검증 산출물이 무효가 된다.
|
||||
|
||||
## S5·S6 뒤에는 S3 관문을 다시 돌린다
|
||||
|
||||
@@ -112,6 +142,12 @@ node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로
|
||||
|
||||
원장에서는 이것이 S5·S6 의 관문이지 S3 의 재실행이 아니다.
|
||||
|
||||
`check_evidence.mjs --repo` 의 exit 의미는 구분한다. `0`은 대조 완료, `1`은 실제 불일치,
|
||||
`2`는 검사 대상 자체가 성립하지 않음, `3`은 원본 저장소가 현재 머신에 없어 repo 대조를
|
||||
수행하지 못한 상태다. 새 publication run의 관문에서는 `3`을 PASS로 기록하면 안 된다. 반면
|
||||
저장소 전체를 dogfooding하는 `verify-pipeline.py`는 기존 문서의 내용 불일치와 실행 환경의
|
||||
부재를 섞지 않기 위해 `3`을 `UNVERIFIABLE` WARN으로 따로 센다.
|
||||
|
||||
## 건너뛰어도 되는 단계
|
||||
|
||||
이미 있는 것을 다시 만들지 않는다. 건너뛴 단계도 **원장에 `SKIPPED` 와 사유를 적는다.**
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Command Pedagogy Editor — Canonical Contract
|
||||
|
||||
이 계약은 provider와 무관한 editor의 SSOT다. `.claude/agents/command-pedagogy-editor.md`는 이 파일을 가리키기만 한다.
|
||||
|
||||
먼저 `.agents/skills/writing-practitioner-guides/SKILL.md`와 `references/command-pedagogy.md`를 읽는다. validated `CommandPlan`이 있을 때만 실행한다.
|
||||
|
||||
## 책임
|
||||
- 기록 파일을 직접 다시 쓰지 않는다.
|
||||
- frozen plan이 지목한 command span만 `CommandPatchSet`으로 제안한다.
|
||||
- command block 밖의 산문, technical claim/evidence, target host/session, file meaning, security boundary를 바꾸지 않는다.
|
||||
- validation과 destructive cleanup을 같은 chain에 두지 않는다.
|
||||
- diagnostic 문맥에서는 필요한 raw output을 먼저 관찰할 수 있게 한다.
|
||||
- automation/reference에서는 정당한 compact syntax를 유지할 수 있다.
|
||||
- 변경할 필요가 없으면 empty `patches`를 허용한다.
|
||||
|
||||
## 출력 계약
|
||||
`.agents/skills/running-tech-log-pipeline/schemas/command-patch-set.schema.json`을 만족하는 JSON만 산출한다. 적용은 `scripts/apply-command-pedagogy-patch.py`만 사용하며 stale source/block hash를 우회하지 않는다.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Command Pedagogy Planner — Canonical Contract
|
||||
|
||||
이 계약은 provider와 무관한 planner의 SSOT다. `.claude/agents/command-pedagogy-planner.md`는 이 파일을 가리키기만 한다.
|
||||
|
||||
먼저 `.agents/skills/writing-practitioner-guides/SKILL.md`와 `references/command-pedagogy.md`를 읽는다. deterministic initial analysis에 finding이 있을 때만 실행한다.
|
||||
|
||||
## 책임
|
||||
- Markdown을 직접 고치지 않는다.
|
||||
- analysis가 지목한 command block마다 기술적 intent, 실행 host/cwd, prerequisite, 단계, 예상 결과, cleanup을 `CommandPlan`으로 명시한다.
|
||||
- tutorial/operator/diagnostic에서는 관찰 가능한 한 단계/한 책임을 우선한다.
|
||||
- automation/reference에서는 compact shell이 목적에 맞으면 유지할 수 있다. `sed`, `printf`, pipeline, command substitution 자체를 금지하지 않는다.
|
||||
- SSOT/evidence에 없는 alias, 파일, prerequisite, 성공 결과를 만들지 않는다.
|
||||
- target host/session, file meaning, security boundary, technical claim을 바꾸지 않는다.
|
||||
|
||||
## 출력 계약
|
||||
`.agents/skills/running-tech-log-pipeline/schemas/command-plan.schema.json`을 만족하는 JSON만 산출한다. `source_sha256`과 각 group의 `source_sha256`은 initial analysis의 frozen hash와 같아야 한다. 모든 finding 대상 block을 빠짐없이 command group으로 다룬다.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Command Pedagogy Reviewer — Canonical Contract
|
||||
|
||||
이 계약은 provider와 무관한 독립 reviewer의 SSOT다. `.claude/agents/command-pedagogy-reviewer.md`는 이 파일을 가리키기만 한다.
|
||||
|
||||
먼저 `.agents/skills/writing-practitioner-guides/SKILL.md`와 `references/command-pedagogy.md`를 읽는다. command/prose/voice repair가 모두 끝난 최종 publication text, initial/final deterministic analysis, CommandPlan, SSOT/evidence를 함께 본다. 파일은 수정하지 않는다.
|
||||
|
||||
## PASS 조건
|
||||
- 실행 context와 prerequisite가 필요한 위치에서 분명하다.
|
||||
- setup/transfer/validation/observation/cleanup이 불필요한 opaque one-liner로 뭉치지 않는다.
|
||||
- destructive cleanup이 validation evidence를 같은 chain에서 지우지 않는다.
|
||||
- troubleshooting의 첫 경로에서 필요한 stderr/raw output을 숨기지 않는다.
|
||||
- 남아 있는 `sed`, `printf`, substitution, redirect, pipeline은 문서 mode에서 합리적이다.
|
||||
- command repair가 technical operation, target, security boundary, evidence를 바꾸지 않았다.
|
||||
- 결과나 prerequisite를 발명하지 않았다.
|
||||
|
||||
## 판정
|
||||
`PASS | FAIL | UNCERTAIN`. `FAIL`과 `UNCERTAIN`은 acceptance를 막는다. 결과 artifact에는 `scope=command-pedagogy`, `reviewer=command-pedagogy-reviewer`, `verdict`, `source_sha256`, `findings`, `notes`를 남겨 최종 publication hash에 결속한다.
|
||||
@@ -0,0 +1,29 @@
|
||||
schema_version: "1.0"
|
||||
default_mode: operator
|
||||
blanket_ban: false
|
||||
|
||||
modes:
|
||||
tutorial:
|
||||
prefer: [one-concern-per-step, explicit-execution-context, explain-before-run, expected-result, separate-verification, separate-cleanup]
|
||||
operator:
|
||||
prefer: [copyable, intent-revealing, observable, bounded-side-effects]
|
||||
diagnostic:
|
||||
allow: [pipelines, grep, jq, awk]
|
||||
require: [preserve-useful-errors, explain-filtering]
|
||||
automation:
|
||||
allow: [sed, awk, jq, printf, pipelines, command-substitution, compound-commands]
|
||||
require: [stated-input, stated-output, reason-for-automation, verification]
|
||||
reference:
|
||||
allow: [compact-command-forms]
|
||||
require: [intent-remains-visible]
|
||||
|
||||
major_findings:
|
||||
- validation_cleanup_same_chain
|
||||
- destructive_side_effects_without_observable_checkpoint
|
||||
- critical_diagnostic_output_hidden
|
||||
|
||||
principles:
|
||||
- deterministic_analyzer_reports_findings_only
|
||||
- command_repair_before_prose_repair
|
||||
- final_technical_evidence_review_after_all_repairs
|
||||
- command_free_sections_skip_command_model_calls
|
||||
@@ -121,6 +121,24 @@ node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs
|
||||
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
|
||||
```
|
||||
|
||||
`--repo`는 선택 옵션이 아니다. S3와 S3 관문을 다시 실행하는 S5·S6 모두 live source
|
||||
reconciliation을 시도해야 한다. 현재 기계에 `sourceRepository.path`가 없어서 명령이 exit 3을
|
||||
돌려주면 그 결과를 exit 0으로 바꾸거나 `--repo`를 빼지 않는다. schemaVersion 5 원장에는
|
||||
다음처럼 **대조 불가 자체를 영수증으로 남긴다.**
|
||||
|
||||
```text
|
||||
semanticId: evidence-repo
|
||||
status: UNVERIFIABLE
|
||||
exit: 3
|
||||
reason: source repository unavailable on current machine
|
||||
acceptedByProjectReview: true
|
||||
```
|
||||
|
||||
`verify-pipeline-run.py`는 v5부터 S3·S5·S6 각각에 `semanticId=evidence-repo` 관문이 정확히
|
||||
하나 있는지, 실제 명령에 `check_evidence.mjs <프로젝트> --repo`가 들어 있는지까지 확인한다.
|
||||
`UNVERIFIABLE`은 실패를 숨긴 PASS가 아니다. 실제 exit 3, 대조 불가 이유, 프로젝트 리뷰의
|
||||
명시적 수용이 모두 있을 때만 완료를 막지 않는 별도 상태다.
|
||||
|
||||
**노드가 `PROMOTE` 이고 `CONFIRMED` 인지 먼저 본다.** 아니면 쓰지 않는다.
|
||||
|
||||
**인용한 줄은 SSOT 에서 찾아 대조한다.** 앞선 기록에서 옮겨 적은 것은 확인한 것이 아니다 —
|
||||
@@ -129,6 +147,47 @@ node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로
|
||||
그림이 필요해 보이면 **`final/assets/` 에 이미 있는지부터 본다.** 계약의 `ssot-assets` 가 이
|
||||
글감에 배정한 그림이 있으면 그 파일을 그대로 가리킨다. 없을 때만 S4 로 넘긴다.
|
||||
|
||||
### S3-C — command-pedagogy 보조 흐름
|
||||
|
||||
S3가 쓴 기록은 command의 **기술적 정답**과 **사람이 따라갈 수 있는 표현**을 분리해서 본다.
|
||||
먼저 deterministic `initialAnalysis`를 만든다.
|
||||
|
||||
```bash
|
||||
python3 scripts/check-command-pedagogy.py <기록.md> \
|
||||
-o runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
```
|
||||
|
||||
`initialAnalysis.shellBlocks == 0`이면 planner/editor/reviewer는 모두 `SKIPPED`다. shell block은
|
||||
있지만 finding이 0이면 planner/editor만 `SKIPPED`하고 reviewer는 S6 뒤 최종본을 본다.
|
||||
|
||||
finding이 하나라도 있으면 역할을 나눈다.
|
||||
|
||||
1. `command-pedagogy-planner`가 분석이 지목한 block만 대상으로 `CommandPlan`을 만든다.
|
||||
2. `command-pedagogy-editor`는 기록을 직접 다시 쓰지 않고 `CommandPatchSet`만 만든다.
|
||||
3. 아래 적용기로 patch를 적용한다.
|
||||
|
||||
```bash
|
||||
python3 scripts/apply-command-pedagogy-patch.py \
|
||||
<기록.md> <command-initial.json> <command-patch-set.json> \
|
||||
-o <수정본.md>
|
||||
```
|
||||
|
||||
적용기는 initial source hash, block id, block hash를 대조한다. 따라서 editor가 **command block 밖의 산문**을
|
||||
직접 바꾸는 경로가 없다. stale hash나 unknown block이면 적용하지 않고 기록을 다시 분석한다.
|
||||
|
||||
S5·S6이 끝난 뒤 최종 기록에 같은 분석을 다시 실행해 `finalAnalysis`를 남긴다. initial에 shell block이
|
||||
있었는데 final이 0이면 command를 없애서 문제를 숨긴 것으로 보고 실패한다. `finalAnalysis.majorFindings`
|
||||
(`majorFindings`)가 1 이상이어도 실패한다. minor finding은 전역 금지 규칙이 아니므로 reviewer가 문맥을 본다.
|
||||
|
||||
최종 shell/CLI block이 있으면 `command-pedagogy-reviewer`가 독립적으로 `PASS`해야 한다. 그 다음
|
||||
`fact-reviewer`가 command/prose/voice 변경이 모두 끝난 결과를 SSOT/evidence와 역대조해 `PASS`해야 한다.
|
||||
두 reviewer는 파일을 고치지 않는다.
|
||||
|
||||
원장 위치는 `qualityReviews.commandPedagogy.{initialAnalysis,finalAnalysis,planner,editor,reviewer}`와
|
||||
`qualityReviews.technicalEvidence`다. command/fact review 기본 계약은 schemaVersion 3부터 적용한다.
|
||||
|
||||
schemaVersion 4부터는 command lane의 first-class artifact도 강제한다. `initialAnalysis`/`finalAnalysis`와 DONE 상태의 planner/editor/reviewer는 repository 내부 JSON artifact의 `path`+`sha256` 영수증을 남긴다. `CommandPlan`과 `CommandPatchSet`은 각각 `schemas/command-plan.schema.json`, `schemas/command-patch-set.schema.json` 계약을 따르고 `scripts/validate-command-pedagogy-artifact.py`로 frozen analysis와 대조한다. command reviewer와 `technicalEvidence`의 `sourceSha256`은 모든 repair가 끝난 최종 기록 파일의 SHA256과 같아야 한다. v3에는 이 새 영수증을 소급 요구하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## S4 — 기록 → 그림
|
||||
|
||||
@@ -156,6 +156,92 @@ error 0 까지 고쳐라.
|
||||
돌려줄 것: 위 JSON 형식. skillEcho 포함.
|
||||
```
|
||||
|
||||
### S3-C1 — initial command analysis
|
||||
|
||||
S3가 끝나면 항상 deterministic **initial command analysis**를 만든다.
|
||||
|
||||
```bash
|
||||
python3 scripts/check-command-pedagogy.py <기록.md> --mode <문서 mode> \
|
||||
-o runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
```
|
||||
|
||||
shell/CLI block이 0이면 아래 세 command agent를 부르지 않고 원장의 planner/editor/reviewer를
|
||||
`SKIPPED`로 적는다. block은 있지만 finding이 0이면 planner/editor만 `SKIPPED`다.
|
||||
|
||||
### S3-C2 — CommandPlan (finding이 있을 때만)
|
||||
|
||||
```
|
||||
Agent(subagent_type="command-pedagogy-planner", prompt=<아래 command planner 프롬프트>)
|
||||
```
|
||||
|
||||
```text
|
||||
너는 command-pedagogy planner다.
|
||||
|
||||
먼저 .agents/skills/writing-practitioner-guides/SKILL.md 를 끝까지 읽고
|
||||
references/command-pedagogy.md 도 읽어라.
|
||||
|
||||
기록: <기록.md>
|
||||
initial command analysis: runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
근거: <이 기록의 source가 가리키는 SSOT 절>
|
||||
|
||||
분석이 지목한 block만 계획해라. Markdown을 수정하지 마라.
|
||||
SSOT/evidence에 없는 alias, prerequisite, 성공 결과를 만들지 마라.
|
||||
compact syntax 자체를 금지하지 말고 문서 모드와 관찰 가능성으로 판단해라.
|
||||
|
||||
돌려줄 것: CommandPlan JSON + skillEcho.
|
||||
skillEcho는 writing-practitioner-guides/SKILL.md에서 원문 한 줄을 그대로 옮긴 것이다.
|
||||
```
|
||||
|
||||
CommandPlan을 `runs/<프로젝트>/<runId>/stage/S3/command-plan.json`에 저장한 뒤 frozen analysis와 검증한다.
|
||||
|
||||
```bash
|
||||
python3 scripts/validate-command-pedagogy-artifact.py plan \
|
||||
runs/<프로젝트>/<runId>/stage/S3/command-plan.json \
|
||||
--analysis runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
```
|
||||
|
||||
### S3-C3 — bounded command editor (finding이 있을 때만)
|
||||
|
||||
```
|
||||
Agent(subagent_type="command-pedagogy-editor", prompt=<아래 command editor 프롬프트>)
|
||||
```
|
||||
|
||||
```text
|
||||
너는 command-pedagogy editor다.
|
||||
|
||||
먼저 .agents/skills/writing-practitioner-guides/SKILL.md 를 끝까지 읽고
|
||||
references/command-pedagogy.md 도 읽어라.
|
||||
|
||||
기록: <기록.md>
|
||||
initial command analysis: <command-initial.json>
|
||||
CommandPlan: <command-plan.json>
|
||||
근거: <이 기록의 source가 가리키는 SSOT 절>
|
||||
|
||||
기록 파일을 직접 수정하지 마라. frozen plan의 block_id에 대한 CommandPatchSet JSON만 만들어라.
|
||||
command block 밖의 산문, 기술 주장, target host/session, file meaning, security boundary를 바꾸지 마라.
|
||||
|
||||
돌려줄 것: CommandPatchSet JSON + skillEcho.
|
||||
```
|
||||
|
||||
CommandPatchSet을 `runs/<프로젝트>/<runId>/stage/S3/command-patch-set.json`에 저장하고 검증한다.
|
||||
|
||||
```bash
|
||||
python3 scripts/validate-command-pedagogy-artifact.py patch \
|
||||
runs/<프로젝트>/<runId>/stage/S3/command-patch-set.json \
|
||||
--analysis runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
```
|
||||
|
||||
editor 결과는 에이전트가 직접 적용하지 않는다. host orchestrator가 bounded applier를 쓴다.
|
||||
|
||||
```bash
|
||||
python3 scripts/apply-command-pedagogy-patch.py \
|
||||
<기록.md> <command-initial.json> <command-patch-set.json> \
|
||||
-o /tmp/command-repaired.md
|
||||
mv /tmp/command-repaired.md <기록.md>
|
||||
```
|
||||
|
||||
그 다음 S3 관문을 다시 실행하고 S4로 간다.
|
||||
|
||||
---
|
||||
|
||||
## S4 — 기록 → 그림
|
||||
@@ -270,6 +356,102 @@ check_voice.mjs 는 목소리가 모자란지 재지 않는다. 지어낸 목소
|
||||
돌려줄 것: 위 JSON 형식. skillEcho 포함. 흔적이 없었으면 status DONE 에 notes 로 적어라.
|
||||
```
|
||||
|
||||
### R1 — final command analysis + independent command review
|
||||
|
||||
S6가 끝나면 **final command analysis**를 다시 만든다.
|
||||
|
||||
```bash
|
||||
python3 scripts/check-command-pedagogy.py <기록.md> --mode <initial과 같은 문서 mode> \
|
||||
-o runs/<프로젝트>/<runId>/review/command-final.json
|
||||
```
|
||||
|
||||
initial analysis에 shell block이 있었는데 final이 0이면 멈춘다. final `majorFindings`가 남아도
|
||||
멈춘다. final shell/CLI block이 하나라도 있으면 다음 reviewer를 부른다.
|
||||
|
||||
```
|
||||
Agent(subagent_type="command-pedagogy-reviewer", prompt=<아래 command reviewer 프롬프트>)
|
||||
```
|
||||
|
||||
```text
|
||||
너는 final command-pedagogy reviewer다. 파일을 고치지 마라.
|
||||
|
||||
먼저 .agents/skills/writing-practitioner-guides/SKILL.md 를 끝까지 읽고
|
||||
references/command-pedagogy.md 도 읽어라.
|
||||
|
||||
최종 기록: <기록.md>
|
||||
initial command analysis: <command-initial.json>
|
||||
final command analysis: <command-final.json>
|
||||
CommandPlan: <있으면 command-plan.json, 없으면 null>
|
||||
근거: <이 기록의 source가 가리키는 SSOT/evidence>
|
||||
|
||||
최종 command가 기술적으로 같은 일을 하면서 사람이 실행·관찰·진단할 수 있는지 독립적으로 판정해라.
|
||||
직접 수정하지 마라. PASS / FAIL / UNCERTAIN 중 하나만 낸다.
|
||||
FAIL과 UNCERTAIN은 acceptance를 막는다.
|
||||
|
||||
돌려줄 것: reviewer, verdict, findings, notes + skillEcho.
|
||||
```
|
||||
|
||||
### R2 — final technical-evidence review
|
||||
|
||||
command/prose/voice 수정이 모두 끝난 뒤 마지막으로 fact reviewer를 부른다. command가 없는 글도
|
||||
이 검토는 생략하지 않는다.
|
||||
|
||||
```
|
||||
Agent(subagent_type="fact-reviewer", prompt=<아래 fact reviewer 프롬프트>)
|
||||
```
|
||||
|
||||
```text
|
||||
너는 최종 technical-evidence reviewer다. 파일을 고치지 마라.
|
||||
|
||||
최종 기록: <기록.md>
|
||||
SSOT: docs/<프로젝트>/final/document.md
|
||||
추가 evidence: <tech-log-tree node가 가리키는 증거와 원본 가이드 경로>
|
||||
|
||||
모든 command repair, prose rewrite, voice edit가 끝난 이 최종본을 원문과 역대조해라.
|
||||
수치·단위·경로 결합·미검증→확정 전환·유무 오기·비밀 노출을 본다.
|
||||
|
||||
돌려줄 것은 fact-reviewer 계약의 VERDICT: PASS | FAIL 형식이다.
|
||||
PASS만 S7로 갈 수 있다.
|
||||
```
|
||||
|
||||
두 결과를 각각 `qualityReviews.commandPedagogy.reviewer`와
|
||||
`qualityReviews.technicalEvidence`에 적은 뒤 S7로 간다.
|
||||
|
||||
|
||||
### R3 — schemaVersion 4 artifact / hash receipt
|
||||
|
||||
v4 원장은 “호출했다”는 문자열만으로 command lane을 통과시키지 않는다. 아래 JSON artifact를
|
||||
repository 안의 run 디렉터리에 보존하고 각각의 **repo-relative path + sha256**을 `run.json`에 적는다.
|
||||
|
||||
- `command-initial.json` → `qualityReviews.commandPedagogy.initialAnalysis.artifact`
|
||||
- `command-plan.json` → planner가 DONE일 때 `planner.artifact`
|
||||
- `command-patch-set.json` → editor가 DONE일 때 `editor.artifact`
|
||||
- `command-final.json` → `finalAnalysis.artifact`
|
||||
- `command-review.json` → reviewer가 DONE일 때 `reviewer.artifact`
|
||||
|
||||
영수증 형식은 다음이다.
|
||||
|
||||
```json
|
||||
{"path":"runs/<프로젝트>/<runId>/review/command-final.json","sha256":"<64 hex>"}
|
||||
```
|
||||
|
||||
파일을 저장한 뒤 `sha256sum <artifact>`로 실제 digest를 구한다. planner/editor를 조건상 SKIPPED한
|
||||
경우 해당 `artifact`는 `null`이다. command reviewer를 실행했다면 reviewer 결과 JSON에는
|
||||
`source_sha256`을 넣고, `run.json`의 reviewer `sourceSha256`에도 **최종 기록 파일의 SHA256**을
|
||||
같이 적는다.
|
||||
|
||||
```bash
|
||||
sha256sum <기록.md>
|
||||
sha256sum runs/<프로젝트>/<runId>/stage/S3/command-initial.json
|
||||
sha256sum runs/<프로젝트>/<runId>/review/command-final.json
|
||||
```
|
||||
|
||||
`fact-reviewer`도 같은 최종 기록을 검토했다는 증거로
|
||||
`qualityReviews.technicalEvidence.sourceSha256`에 동일한 최종 기록 SHA256을 적는다. 그 뒤에 기록
|
||||
파일을 한 글자라도 바꾸면 reviewer/fact-review를 다시 실행해야 한다.
|
||||
|
||||
schemaVersion 3 원장은 이 artifact/hash 필드가 생기기 전 계약이므로 소급해서 채우지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## S7 — Studio 저장
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "command-patch-set.schema.json",
|
||||
"title": "CommandPatchSet",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"source_sha256",
|
||||
"patches"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"const": "1.0"
|
||||
},
|
||||
"source_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"patches": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"block_id",
|
||||
"expected_sha256",
|
||||
"replacement_markdown"
|
||||
],
|
||||
"properties": {
|
||||
"block_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"expected_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"replacement_markdown": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "command-plan.schema.json",
|
||||
"title": "CommandPlan",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"schema_version",
|
||||
"section_id",
|
||||
"source_sha256",
|
||||
"mode",
|
||||
"command_groups"
|
||||
],
|
||||
"properties": {
|
||||
"schema_version": {
|
||||
"const": "1.0"
|
||||
},
|
||||
"section_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"source_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"mode": {
|
||||
"enum": [
|
||||
"tutorial",
|
||||
"operator",
|
||||
"diagnostic",
|
||||
"automation",
|
||||
"reference"
|
||||
]
|
||||
},
|
||||
"command_groups": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"id",
|
||||
"block_id",
|
||||
"source_sha256",
|
||||
"goal",
|
||||
"execution_context",
|
||||
"prerequisites",
|
||||
"steps",
|
||||
"cleanup"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"block_id": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"source_sha256": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]{64}$"
|
||||
},
|
||||
"goal": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"execution_context": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"host",
|
||||
"cwd"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerequisites": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"command",
|
||||
"reason",
|
||||
"expected_result"
|
||||
],
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"expected_result": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cleanup": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"command",
|
||||
"reason"
|
||||
],
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"schemaVersion": 5,
|
||||
"runId": "<YYYY-MM-DD-HHMM>",
|
||||
"project": "<프로젝트>",
|
||||
"record": "<docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md — 이 런이 만드는 기록>",
|
||||
@@ -104,5 +104,68 @@
|
||||
"gates": [],
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
],
|
||||
"qualityReviews": {
|
||||
"commandPedagogy": {
|
||||
"initialAnalysis": {
|
||||
"cmd": "",
|
||||
"exit": null,
|
||||
"shellBlocks": null,
|
||||
"findings": null,
|
||||
"majorFindings": null,
|
||||
"artifact": null
|
||||
},
|
||||
"finalAnalysis": {
|
||||
"cmd": "",
|
||||
"exit": null,
|
||||
"shellBlocks": null,
|
||||
"findings": null,
|
||||
"majorFindings": null,
|
||||
"artifact": null
|
||||
},
|
||||
"planner": {
|
||||
"runBy": "command-pedagogy-planner",
|
||||
"skill": "writing-practitioner-guides",
|
||||
"status": "PENDING",
|
||||
"skipReason": "",
|
||||
"skillEcho": "",
|
||||
"skillRevision": null,
|
||||
"notes": "",
|
||||
"artifact": null
|
||||
},
|
||||
"editor": {
|
||||
"runBy": "command-pedagogy-editor",
|
||||
"skill": "writing-practitioner-guides",
|
||||
"status": "PENDING",
|
||||
"skipReason": "",
|
||||
"skillEcho": "",
|
||||
"skillRevision": null,
|
||||
"notes": "",
|
||||
"artifact": null
|
||||
},
|
||||
"reviewer": {
|
||||
"runBy": "command-pedagogy-reviewer",
|
||||
"skill": "writing-practitioner-guides",
|
||||
"status": "PENDING",
|
||||
"skipReason": "",
|
||||
"skillEcho": "",
|
||||
"skillRevision": null,
|
||||
"verdict": null,
|
||||
"notes": "",
|
||||
"artifact": null,
|
||||
"sourceSha256": null
|
||||
}
|
||||
},
|
||||
"technicalEvidence": {
|
||||
"runBy": "fact-reviewer",
|
||||
"status": "PENDING",
|
||||
"skipReason": "",
|
||||
"verdict": null,
|
||||
"notes": "",
|
||||
"sourceSha256": null,
|
||||
"liveSourceReconciliation": null,
|
||||
"liveSourceReason": "",
|
||||
"acceptedByProjectReview": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Command Pedagogy Policy
|
||||
|
||||
이 문서는 `writing-practitioner-guides`의 명령 작성 원칙을 command-pedagogy 보조 흐름에서
|
||||
같은 기준으로 쓰기 위한 계약이다. **명령을 짧게 만드는 것보다 사람이 실행하면서 원인과 결과를
|
||||
볼 수 있게 만드는 것이 우선**이다.
|
||||
|
||||
## 모드
|
||||
|
||||
| mode | 기본 판단 |
|
||||
|---|---|
|
||||
| `tutorial` | 명시적 단계, 목적, 전제조건, 예상 결과가 우선 |
|
||||
| `operator` | 한 단계에서 무엇을 바꾸고 무엇을 확인하는지 드러나야 함 |
|
||||
| `diagnostic` | 필터링·stderr 억제 전에 원래 출력을 먼저 보여 줌 |
|
||||
| `automation` | compact shell 허용. 왜 자동화하는지 이유가 있어야 함 |
|
||||
| `reference` | 숙련자가 복사할 compact form 허용. 의미가 숨겨지면 안 됨 |
|
||||
|
||||
기본 모드는 `operator`다. 기록이 학습용 절차라면 `tutorial` 또는 `diagnostic` 쪽 판단을
|
||||
우선한다.
|
||||
|
||||
문서 전체의 성격과 특정 command block의 성격이 다르면 block 바로 앞에서 mode를 좁힐 수 있다.
|
||||
|
||||
````markdown
|
||||
<!-- command-mode: operator -->
|
||||
```bash
|
||||
kubectl get pods
|
||||
```
|
||||
````
|
||||
|
||||
기존 문서를 corpus 검증할 때는 문서 전체를 함부로 operator로 간주하지 않고 `reference`를
|
||||
기본으로 읽은 뒤 이 marker를 우선한다. 실행 명령이 아니라 CLI 이름을 사용해 흐름만 설명하는
|
||||
`text` fence도 명시적으로 `reference` 또는 `automation`으로 분류할 수 있다. marker가
|
||||
없는데 command처럼 보이는 `text` fence는 자동 수정하지 않고 reviewer가 분류할 신호로만 남긴다.
|
||||
|
||||
## 검토 규칙
|
||||
|
||||
- 같은 관리 호스트를 반복한다면 raw IP를 계속 쓰기보다 안정적인 SSH alias를 먼저 설명한다.
|
||||
- transfer, login, validation, observation, cleanup을 한 줄로 합쳐 인과를 숨기지 않는다.
|
||||
- validation과 destructive cleanup은 분리한다. 검증 실패를 보기 전에 증거를 지우면 안 된다.
|
||||
- 사람이 읽으며 작성해야 하는 작은 설정 파일은 생성용 `printf`보다 파일 내용을 먼저 보여 준다.
|
||||
- command substitution을 첫 학습 경로로 쓸 때는 그 값이 어디서 오는지 먼저 드러낸다.
|
||||
- troubleshooting의 첫 경로에서 stderr나 raw output을 숨기지 않는다.
|
||||
- multi-stage pipeline의 중간 출력이 개념 이해에 필요하면 나누거나 각 단계를 먼저 설명한다.
|
||||
- 명령 하나에 의미 단위 하나를 두는 편이 관찰 가능성을 높인다면 keystroke 절약보다 그쪽을 택한다.
|
||||
|
||||
## 전역 금지 규칙이 아니다
|
||||
|
||||
다음 문법 자체를 금지하지 않는다.
|
||||
|
||||
- `sed`
|
||||
- `printf`
|
||||
- pipeline
|
||||
- command substitution
|
||||
- redirect
|
||||
- remote shell
|
||||
|
||||
자동화나 reference가 목적이면 그대로 둘 수 있다. 핵심 질문은 **“이 표현이 기술적으로 맞는가?”만이
|
||||
아니라 “이 문서의 독자가 이 표현으로 시스템을 읽고 실패를 진단할 수 있는가?”**다.
|
||||
|
||||
## 보존 경계
|
||||
|
||||
command repair는 기술 의미를 단순화하는 작업이 아니다.
|
||||
|
||||
- 대상 host/session을 바꾸지 않는다.
|
||||
- 파일의 의미와 보안 경계를 바꾸지 않는다.
|
||||
- SSOT/evidence에 없는 prerequisite, alias, 파일, 성공 결과를 발명하지 않는다.
|
||||
- command block 밖의 기존 산문은 editor가 직접 수정하지 않는다.
|
||||
- editor는 `CommandPatchSet`만 만들고 `scripts/apply-command-pedagogy-patch.py`가 원래 command
|
||||
span에 patch를 적용한다.
|
||||
@@ -9,6 +9,12 @@
|
||||
// 2. frontmatter 의 source 앵커가 SSOT 를 가리키는가
|
||||
// 3. 기록의 title 이 계약(tech-log-tree.json)의 title 과 같은가
|
||||
//
|
||||
// exit code:
|
||||
// 0 = 확인한 범위에서 일치
|
||||
// 1 = 실제 불일치
|
||||
// 2 = 검사 대상/인자가 성립하지 않음
|
||||
// 3 = --repo 원본이 이 머신에 없어 저장소 대조를 수행하지 못함
|
||||
//
|
||||
// 검사기가 못 보던 자리다. `verify-tech-log-tree.py` 는 slug 와 칸의 존재만 보고,
|
||||
// 인용한 코드가 실재하는지도 제목이 계약과 같은지도 보지 않는다.
|
||||
import { readFileSync, readdirSync, statSync, existsSync } from "node:fs";
|
||||
@@ -82,6 +88,9 @@ function inSsot(line) {
|
||||
}
|
||||
|
||||
const findings = [];
|
||||
// 저장소가 이 머신에 마운트되지 않은 상태는 evidence mismatch가 아니다.
|
||||
// 확인 자체를 못 한 것이므로 별도 상태로 보존하고 exit 3으로 돌려준다.
|
||||
const unverifiable = [];
|
||||
const studio = join(base, "tech-log-studio");
|
||||
for (const topicDir of readdirSync(studio)) {
|
||||
const tp = join(studio, topicDir);
|
||||
@@ -140,7 +149,10 @@ if (withRepo) {
|
||||
for (const repo of repos) {
|
||||
const name = repo.name || project;
|
||||
if (!repo.path) { findings.push(["tech-log-tree.json", "sourceRepository.path 가 없다", name]); continue; }
|
||||
if (!existsSync(repo.path)) { findings.push(["tech-log-tree.json", "저장소 경로가 없다", `${name} — ${repo.path}`]); continue; }
|
||||
if (!existsSync(repo.path)) {
|
||||
unverifiable.push(["tech-log-tree.json", "저장소 경로가 이 기계에 없다", `${name} — ${repo.path}`]);
|
||||
continue;
|
||||
}
|
||||
// 갈래가 여럿이면 revisions 로 적는다. 둘 다 없으면 verify-tech-log-tree.py 가 warn 을 낸다
|
||||
const revs = repo.revision ? { revision: repo.revision } : (repo.revisions || {});
|
||||
// 체크아웃이 없는 저장소는 반입한 쪽의 매니페스트가 리비전을 고정한다. 그럴 때는
|
||||
@@ -166,12 +178,31 @@ for (const [f, rule, detail] of findings) {
|
||||
if (!grouped.has(rule)) grouped.set(rule, []);
|
||||
grouped.get(rule).push(`${f} — ${detail}`);
|
||||
}
|
||||
const unverifiableGrouped = new Map();
|
||||
for (const [f, rule, detail] of unverifiable) {
|
||||
if (!unverifiableGrouped.has(rule)) unverifiableGrouped.set(rule, []);
|
||||
unverifiableGrouped.get(rule).push(`${f} — ${detail}`);
|
||||
}
|
||||
|
||||
console.log(`\n[${project}] 증빙 대조${withRepo ? " (저장소 포함)" : ""}`);
|
||||
if (!findings.length) { console.log(" 문제 없음"); process.exit(0); }
|
||||
for (const [rule, items] of [...grouped].sort((a, b) => b[1].length - a[1].length)) {
|
||||
console.log(` ✗ ${String(items.length).padStart(4)} ${rule}`);
|
||||
for (const it of items.slice(0, 3)) console.log(` · ${it}`);
|
||||
if (items.length > 3) console.log(` … 외 ${items.length - 3}건`);
|
||||
}
|
||||
console.log(`\n합계 ${findings.length}건`);
|
||||
process.exit(1);
|
||||
for (const [rule, items] of [...unverifiableGrouped].sort((a, b) => b[1].length - a[1].length)) {
|
||||
console.log(` ? ${String(items.length).padStart(4)} ${rule}`);
|
||||
for (const it of items.slice(0, 3)) console.log(` · ${it}`);
|
||||
if (items.length > 3) console.log(` … 외 ${items.length - 3}건`);
|
||||
}
|
||||
|
||||
if (!findings.length && !unverifiable.length) {
|
||||
console.log(" 문제 없음");
|
||||
process.exit(0);
|
||||
}
|
||||
if (findings.length) {
|
||||
console.log(`\n불일치 ${findings.length}건 · 대조 불가 ${unverifiable.length}건`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`\n불일치 0건 · 대조 불가 ${unverifiable.length}건`);
|
||||
process.exit(3);
|
||||
|
||||
Reference in New Issue
Block a user