feat: 가상화 문서들 추가

This commit is contained in:
DongHyeonka
2026-09-10 08:54:05 +09:00
parent e9f6a93327
commit 43e1aadef0
695 changed files with 153404 additions and 12754 deletions
@@ -11,7 +11,8 @@ Produce a highly detailed, source-traceable engineering analysis. This stage dis
## Required sequence
1. **First read `<분석 대상 저장소>/analysis-queue.yaml`.** Before inspecting any project contents, apply `references/queue-contract.md`: reconcile newly discovered project directories, preserve queue order, and determine the single active project.
0. **Fix the two roots before anything else.** `<분석 대상 저장소>` is the repository being analyzed — an absolute path outside this repository, given by the caller. `docs/<프로젝트>/` is inside *this* repository. Never write into the analyzed repository, and never read analysis state from it.
1. **Read `<분석 대상 저장소>/analysis-queue.yaml` if it exists.** It exists only when several repositories are queued. Apply `references/queue-contract.md`: reconcile newly discovered project directories, preserve queue order, and determine the single active project. **If there is no queue, analyze the one repository the caller named and skip to step 3.** A missing queue is not a blocker — it means nothing is queued.
2. If an `IN_PROGRESS` project exists, analyze only that project. If none exists, activate the first `PENDING` project in queue order. Never preempt an active project because a new project appeared.
3. For the selected `<분석 대상 저장소>`, check the nearest `AGENTS.md` or equivalent repository instructions.
4. Record Git revision and `git status` when Git is available. Never modify or reset user source as part of analysis.
@@ -2,7 +2,7 @@
## Primary evidence first
Store command output, logs, generated query plans, benchmark results, browser observations, and other primary material under `docs/<프로젝트>/evidence/raw/` before creating presentation assets.
Store command output, logs, generated query plans, benchmark results, browser observations, and other primary material under `docs/<프로젝트>/final/evidence/raw/` before creating presentation assets.
## Terminal evidence
@@ -15,7 +15,7 @@ For a command used as evidence, retain:
- raw stdout/stderr;
- source revision when relevant.
Render terminal UI with `./tools/terminal-evidence/render_terminal.py`.
Render terminal UI with `scripts/terminal-evidence/render_terminal.py`.
The visual asset is explanatory. The raw evidence is the provenance.
@@ -2,7 +2,7 @@
## 분석 기준 revision
- repository: `/shared/codebase/<project>`
- repository: `<분석 대상 저장소의 절대 경로>`
- revision: `<git revision or non-git snapshot note>`
## Build and module map
@@ -1,7 +1,7 @@
{
"schemaVersion": 2,
"project": "<project>",
"codebasePath": "/shared/codebase/<project>",
"codebasePath": "<분석 대상 저장소의 절대 경로>",
"gitRevision": null,
"analysisStatus": "NOT_STARTED",
"analysisCycle": 1,
@@ -82,7 +82,28 @@ They do not hold candidates, and in a folded project they are gone.
with the fields its kind requires. There is no second tree to keep in step.
10. Run `references/decomposition-checklist.md`.
11. Record `candidateScope`, the source document hash, and the project revision.
12. `python3 scripts/verify-tech-log-tree.py <project>` — errors must be 0.
12. `python3 scripts/build-tech-log-tree.py <project>` first, then
`python3 scripts/verify-tech-log-tree.py <project>` — errors must be 0. Build fills
`ssotSha256`; verify errors when it is absent, so verifying before building always fails.
## Three fields the verifier requires and this procedure does not otherwise name
Write them by hand. `verify-tech-log-tree.py` counts each as an error when missing.
| Field | What goes in it |
|---|---|
| `candidateScope` | the range candidates were found in — above |
| `sourceRepository` | `path` of the analyzed repository, its `revision`, and how that was established. Leave `revision` `null` rather than inventing one; when the work is split across branches, pair names and commits under `revisions` |
| `ssotSha256` | filled by `build-tech-log-tree.py`. Its job is to catch a tree whose SSOT changed after the candidates were chosen |
```json
"sourceRepository": {
"path": "/absolute/path/to/analyzed-repo",
"revision": null,
"revisions": {"AP1 develop-pattern1": "64175266"},
"verified": "how the revision was established, or why it is null"
}
```
Use `.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md` as the
output contract.
@@ -0,0 +1,140 @@
---
name: publishing-tech-log-to-studio
description: Use when a finished Tech Log record .md must be put into Tech Log Studio through the browser with Playwright MCP — creating or opening the working copy, uploading assets, filling the per-kind fields, and saving. Save only; this skill never publishes.
---
# Studio 반입 — 저장까지만
## 이 스킬의 경계
기록 `.md` 하나를 Studio 편집 화면에 넣고 **저장**한다. 거기서 끝난다.
**게시하지 않는다.** 게시는 공개 사이트에 올리는 일이고, 되돌리려면 `unpublish` 를 해야 하며
그 사이에 누구나 본다. 더 중요한 것은 **한 번이라도 게시한 문서는 게시를 취소해도 지워지지
않는다는 것이다** — 삭제가 409 로 거절되고 「공개된 기록은 삭제할 수 없습니다」가 뜬다. 그래서
시험 삼아 게시하지 않는다. 게시는 사람이 미리보기를 읽고 판단한다.
편집 화면 오른쪽 `aside` 에 버튼이 `저장`·`게시` 둘뿐이다. **`게시` 를 누르면 저장·검증·
미리보기·게시가 한 번에 돈다.** 이 스킬은 `저장` 만 누른다.
## 들어가기 전 조건
| 조건 | 확인 |
|---|---|
| 기록 `.md` 가 파서를 통과했다 | `studio-body.py` 로 바꾼 파일에 `check_body.mjs`, error 0 |
| 문장 검사를 지났다 | `check_prose.mjs` error 0 |
| 인용이 SSOT 에 실재한다 | `check_evidence.mjs <프로젝트> --repo` |
| 브라우저가 이미 로그인돼 있다 | 아래 「인증」 |
**검사를 안 지난 초안을 넣지 않는다.** 저장은 빈 칸도 받아 주기 때문에(Studio 는 한 번에 다
쓰지 않아도 저장되게 만들어져 있다) 넣는 것 자체는 성공한다. 그래서 파서·문장 검사를 여기서
대신 잡아 주지 않는다.
## 인증
Studio 는 조회에도 권한을 요구한다. 읽는 것이 게시 전 초안이기 때문이다.
**이 저장소에 자격증명을 두지 않는다.** 이미 로그인된 브라우저 세션을 쓴다. 편집 화면을 열었을
때 상태 `aside` 가 25초 안에 안 뜨면 **인증이 안 된 것으로 보고 멈춘다.** 로그인 화면을
자동으로 통과하려 들지 않는다 — 사용자에게 로그인해 달라고 말하고 기다린다.
```
aside[class*="studio-document-status"] ← 이것이 안 보이면 인증 실패
```
## 주소
| 무엇 | 주소 |
|---|---|
| Studio | `https://hyeonworks.com/studio` |
| 편집 화면 | `https://hyeonworks.com/studio/documents/<id>/edit` |
| 새 문서 | Studio 에서 `새 문서` → 종류 선택 → `작업본 만들기` |
기록 frontmatter 의 `id``studio:` 가 이미 차 있으면 **새로 만들지 않는다.** 그 주소로 바로
간다. 비어 있으면 새로 만들고, 받은 uuid 와 편집 주소를 기록 frontmatter 에 적는다.
## 절차
### 1. 기록을 읽고 무엇을 넣을지 정한다
`kind` 로 칸 목록이 정해진다. 어떤 `##` 제목이 Studio 의 어느 칸인지는
[references/studio-form-map.md](references/studio-form-map.md).
**frontmatter 는 메타데이터고, 본문의 `##` 가 Studio 의 칸이며, 제목 아래 첫 문단이 `요약`
이다.** 계약에 없는 `##` 는 화면에 자리가 없어 통째로 사라진다.
### 2. 그림이 있으면 Asset 을 먼저 올린다
frontmatter `assets:``file:` 이 올릴 파일이고 `key:` 는 저장소 쪽 이름이다. 올리면 서버가
`<이름>-<해시8>` 형태의 키를 준다.
**본문을 넣기 전에 올린다.** 순서를 뒤집으면 미리보기가 본문 전체를 막는다 —
`1:1 supported local evidence key not found: <asset-key>`. 다른 경로로 올렸으면 편집 화면을
한 번 새로 고쳐야 목록을 다시 받는다. 본문 문법 문제로 오해하기 쉽다.
본문은 서버가 준 키로 바꿔서 넣는다.
```bash
python3 scripts/studio-body.py <기록.md> --key <저장소 key>=<서버가 준 key> --body-only
```
저장소의 `.md` 는 마크다운 이미지로 두고 고치지 않는다. `:::evidence` 는 Studio 렌더러의
구문이라 저장소에 쓰면 편집기에서 그림이 안 보인다.
### 3. 칸을 채운다
되풀이되는 칸(`영향`·`선택지`·`사실`처럼 여러 줄인 것)은 **줄 수를 먼저 맞추고** 값을 넣는다.
줄이 모자란 채로 채우면 뒤엣것이 조용히 버려진다. 셀렉터와 배치 실행 방법은
[references/playwright-recipes.md](references/playwright-recipes.md).
**본문이 없는 세 종류(Reference·Question·Decision)의 칸은 평문으로 렌더링된다.** 백틱과
파이프가 글자 그대로 보이고, 줄바꿈은 `<br>` 로만 살아난다.
### 4. 저장한다
```
aside[class*="studio-document-status"] 안의 `저장` 버튼
→ 같은 aside 의 글자가 `저장됨` 으로 바뀔 때까지 기다린다 (최대 30초)
```
`저장됨` 을 못 보면 실패다. 그 `aside` 의 글자를 그대로 읽어서 보고한다. **버튼을 다시 누르지
않는다** — 검증 오류로 막힌 것을 연타로 뚫으려다 `게시` 를 누르게 된다.
버전은 같은 `aside` 의 첫 `dd` 에 있다. 저장 전후로 읽어 두면 실제로 올라갔는지 보인다.
### 5. 미리보기로 읽는다
`즉시 미리보기` 탭은 저장한 값이 아니라 **화면에 입력한 값**을 렌더링한다. 그래서 게시 없이도
공개 화면과 같은 블록 렌더러로 본문을 볼 수 있다.
읽을 항목은 `../writing-tech-log-records/references/studio-draft-review.md` 의 목록을 쓴다.
고칠 것이 있으면 `편집` 탭으로 돌아가 고치고 다시 저장한다.
### 6. 기록에 되적는다
새로 만들었으면 frontmatter 의 `id``studio:` 를 채우고 색인을 다시 만든다.
```bash
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
```
`build-tech-log-tree.py` 는 frontmatter 의 `id` 가 있으면 `publication``게시됨` 으로
적는다. **이 칸은 「Studio 에 있다」는 뜻이지 「공개돼 있다」가 아니다.** 공개 여부는 기록의
`status``public:` 이 말한다.
## 시험용 초안을 남기지 않는다
확인하려고 만든 작업본은 지운다. 다만 **Decision 은 계약에 삭제 경로가 없다** — 확인용으로
만들지 않는 편이 낫다. 관계로 참조된 문서도 지워지지 않는다(`DOCUMENT_IN_USE`). 참조하는 쪽의
관계를 먼저 끊는다.
## 실패했을 때
| 증상 | 원인 |
|---|---|
| `aside` 가 안 뜬다 | 인증. 로그인 화면을 자동으로 넘기지 않는다 |
| 미리보기가 본문 전체를 막는다 | Asset 을 올리기 전에 본문을 넣었다. 화면을 새로 고친다 |
| 칸을 못 찾는다 (`label` 매치 0) | 그 종류에 없는 칸이다. `studio-form-map.md` 를 다시 본다 |
| 값이 잘렸다 | 되풀이 칸의 줄 수를 안 맞추고 채웠다 |
| `저장됨` 이 안 뜬다 | 검증이 막은 것이다. `aside` 글자를 읽어 보고한다 |
@@ -0,0 +1,230 @@
# Playwright MCP 로 편집 화면을 다루는 법
여기 적은 셀렉터는 지어낸 것이 아니라 이 저장소가 실제로 돌린 것이다. 원문은
`.playwright-mcp/_p1.mjs`·`_p2.mjs`·`_p3.mjs` 에 남아 있다.
## 두 가지 방식
| 방식 | 언제 |
|---|---|
| MCP 도구를 하나씩 (`browser_navigate` · `browser_snapshot` · `browser_fill_form` · `browser_click`) | 기록 한 건. 화면을 보면서 한다 |
| `browser_run_code_unsafe` 로 한 번에 | 여러 건을 같은 모양으로 채운다 |
**처음 넣는 기록은 하나씩 한다.** 배치는 이미 한 번 성공한 모양을 반복할 때만 쓴다. 화면을
안 보고 배치를 돌리면 못 찾은 칸이 조용히 버려진다.
## 새로 만들 때 — `/studio/documents/new`
종류를 라디오로 고르고 `작업본 만들기` 를 누른다. 라디오의 이름은 화면에 보이는 그대로다.
| 종류 | 라디오 이름 | 화면이 적어 놓은 칸 |
|---|---|---|
| Case | `Case` | 문제 · 결론 · 환경 · 재현 · 본문 |
| Reference | `Reference` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| Concept | **`개념`** | 기준 버전 · 본문 |
| Question | `Question` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| Decision | `Decision` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
**Concept 만 라디오 이름이 한글(`개념`)이다.** 나머지 넷은 영어다.
```js
await page.goto('https://hyeonworks.com/studio/documents/new');
await page.getByRole('radio', { name: /^Reference/ }).check();
await page.getByRole('button', { name: '작업본 만들기' }).click();
// 주소가 /studio/documents/<uuid>/edit 로 바뀐다. 그 uuid 를 기록 frontmatter 에 적는다
```
화면에 이렇게 적혀 있다 — 「이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다.」
**만들었으면 그 자리에서 칸을 채우고 저장한다.** 만들어 두고 나중에 돌아오지 않는다.
## 화면의 기준점
```js
const RAIL = 'aside[class*="studio-document-status"]';
```
`aside` 가 상태 레일이다. 여기에 버전(`dd` 첫 번째)과 저장 상태(`저장됨`)와 버튼
(`저장`·`게시`)이 있다.
**이것이 25초 안에 안 뜨면 인증이 안 된 것이다.** 로그인 화면을 자동으로 넘기려 들지 않는다.
```js
await page.goto('https://hyeonworks.com/studio/documents/' + id + '/edit');
try { await page.waitForSelector(RAIL, { timeout: 25000 }); }
catch { return { error: 'AUTH? ' + page.url() }; }
```
## 버전 읽기
```js
const version = await page.locator(RAIL + ' dd').first().innerText();
```
저장 전후로 읽는다. 안 올라갔으면 저장이 안 된 것이다.
## 칸 채우기
칸은 `label` 안의 `span` 이 이름을 들고 있다.
```js
const loc = page.locator('xpath=//label[./span[normalize-space(.)="' + label + '"]]')
.locator('textarea, input').first();
if (await loc.count() !== 1) { /* 그 종류에 없는 칸이다 — 채우지 말고 기록한다 */ }
if (await loc.inputValue() === value) { /* 이미 같다 — 건드리지 않는다 */ }
await loc.fill(value);
```
**매치가 1이 아니면 채우지 않고 못 찾았다고 적는다.** 비슷한 이름의 다른 칸에 넣는 것보다
안 넣는 쪽이 낫다.
**이미 같은 값이면 건드리지 않는다.** 안 그러면 바뀐 것이 없는데 버전만 올라간다.
## 되풀이되는 칸은 줄 수를 먼저 맞춘다
`영향`·`선택지`·`사실`처럼 여러 줄인 칸은 `fieldset``legend` 가 이름이고 줄이
`.studio-ordered-item` 이다.
```js
const fs = page.locator('xpath=//fieldset[./legend[normalize-space(.)="' + legend + '"]]').first();
let cur = await fs.locator('.studio-ordered-item').count();
while (cur > want) { // 줄이 남으면 뒤에서 지운다
await fs.locator('.studio-ordered-item').last().getByRole('button', { name: '삭제' }).click();
await page.waitForTimeout(60); cur--;
}
while (cur < want) { // 모자라면 더한다
await fs.getByRole('button', { name: legend + ' 추가' }).click();
await page.waitForTimeout(60); cur++;
}
```
**값을 넣기 전에 한다.** 줄이 모자란 채로 채우면 뒤엣것이 조용히 버려진다.
**행 클래스가 칸마다 다르다.** `관계``.studio-relation-item` 이고 나머지가
`.studio-ordered-item` 이다. 하나로 세면 관계는 늘 0으로 읽힌다.
**더한 뒤에는 카운트가 아니라 라벨 목록으로 검산한다.** 실제로 「판단 기준 추가」를 7번
눌렀는데 행이 9개 생겼다. 카운트만 믿으면 못 잡는다.
```js
// 채우기 전에 그 fieldset 안의 라벨을 전부 읽어 몇 줄인지 다시 센다
const labels = await fs.locator('label span').allInnerTexts();
```
## 칸 DOM 이 두 가지다
위 xpath 는 절반만 통한다. 실제 편집 화면에는 모양이 둘이다.
```html
<label class="studio-field"><span>문제</span><textarea></textarea></label> <!-- A -->
<div class="studio-field"><label for="studio-field-summary">요약</label>
<textarea id="studio-field-summary"></textarea></div> <!-- B -->
```
`요약` · `본문 Markdown` · `관계 N 이유` 가 B형이다. A형만 찾으면 이 칸들이 조용히 안 채워진다.
**둘 다 본다.**
```js
async function field(page, name) {
const a = page.locator(`xpath=//label[./span[normalize-space(.)="${name}"]]`)
.locator('textarea, input').first();
if (await a.count()) return a;
const id = await page.locator(`xpath=//label[normalize-space(.)="${name}"]`)
.first().getAttribute('for');
return id ? page.locator('#' + id) : null;
}
```
**본문 칸의 이름은 `본문` 이 아니라 `본문 Markdown` 이다.**
## Asset 을 올릴 때
`Asset 업로드` 는 모달을 띄우고 **`filechooser` 이벤트를 내지 않는다.** 기다리면 타임아웃이다.
모달 안의 `input[type=file]` 에 직접 넣는다.
```js
await page.getByRole('button', { name: 'Asset 업로드' }).click();
await page.locator('input[type=file]').setInputFiles(svgPath);
// 대체 텍스트 칸에 기록 본문의 alt 를 그대로 넣는다
```
**올린 직후 목록이 갱신되지 않는다.** 화면의 `Asset 검색` 을 다시 눌러야 새 키가 나온다.
새로 올리면 서버가 새 키를 준다(`<이름>-<해시8>`) — 옛 키는 목록에 남는다. 다른 문서가
참조할 수 있으니 지우지 않는다.
## 값을 읽을 때는 `getByLabel` 을 쓰지 않는다
칸이 `<label><span>이름</span><textarea></label>` 모양이라 `getByLabel(...).inputValue()`
30초 타임아웃으로 실패한다. `fill` 은 되는데 읽기만 안 된다. 읽기는 `evaluate` 로 한다.
```js
const got = await page.evaluate((name) => {
const el = [...document.querySelectorAll('label')]
.find(l => l.querySelector('span')?.textContent.trim() === name);
return el?.querySelector('textarea, input')?.value ?? null;
}, '판단 기준');
```
**넣은 값을 다시 읽어 원문과 대조한다.** 넣었다는 것과 들어갔다는 것은 다르다.
## 저장
```js
await page.locator(RAIL).getByRole('button', { name: '저장' }).first().click();
await page.waitForFunction(() => {
const el = document.querySelector('aside[class*="studio-document-status"]');
return el && /저장됨/.test(el.innerText);
}, null, { timeout: 30000 });
```
**버튼을 이름으로 고른다.** `aside button` 의 첫 번째로 고르면 화면이 바뀌었을 때 `게시`
누르게 된다.
`저장됨` 을 못 보면 실패다. 다시 누르지 말고 레일의 글자를 읽어서 보고한다.
```js
const why = (await page.locator(RAIL).innerText()).replace(/\s+/g, ' ').slice(0, 140);
```
## 바뀐 것이 없으면 저장하지 않는다
```js
if (filled.length === 0 && rowsChanged.length === 0) { /* CLEAN — 저장 버튼을 누르지 않는다 */ }
```
누를 때마다 `version` 이 올라가고, 버전이 올라가면 앞서 만든 검증·미리보기 산출물이 무효가
된다(`validatedVersion == document.version` 이 깨진다).
## 미리보기
**`즉시 미리보기` 는 탭이 아니다.** 편집 화면 오른쪽에 나란히 놓인 `region` 이라
`getByRole('tab', ...)` 은 0개를 찾는다. 화면에 `[role="tab"]` 자체가 없다.
```js
await page.getByRole('region', { name: /즉시 미리보기/ }).scrollIntoViewIfNeeded();
```
저장한 값이 아니라 화면에 입력한 값을 렌더링한다. **Asset 을 방금 올렸으면 편집 화면을 한 번
새로 고친다** — 안 그러면 편집 화면이 들고 있는 옛 asset 목록에서 키를 못 찾아 본문 전체가
막힌다.
```text
초안을 미리 볼 수 없습니다
1:1 supported local evidence key not found: <asset-key>
```
## 배치로 돌릴 때 돌려받을 것
한 건마다 이만큼은 남긴다. 무엇이 채워졌고 무엇을 못 찾았는지 없이 「성공」만 돌려받으면
못 찾은 칸이 묻힌다.
```js
{ name, version, saved: 'OK v13' | 'CLEAN' | 'FAIL <레일 글자>',
filled: ['문제','결론'], skipped: 2, miss: ['적용 조건'], rows: ['영향 2→3'] }
```
## 절대로 하지 않는 것
- `게시` 버튼을 누르지 않는다
- 로그인 화면에 자격증명을 입력하지 않는다
- `저장됨` 이 안 뜬다고 연타하지 않는다
- 시험 삼아 문서를 만들지 않는다 — 한 번 게시한 문서는 취소해도 삭제가 409 로 거절된다
@@ -0,0 +1,103 @@
# 기록 `.md` 의 어디가 Studio 의 어느 칸인가
## 세 자리
| 기록 `.md` | Studio |
|---|---|
| frontmatter | 메타데이터. 화면 칸이 아니다 |
| 제목 바로 아래 첫 문단 | **`요약` 칸** |
| `## <이름>` | **같은 이름의 칸** |
`## 요약` 이라는 절을 만들지 않는다 — Studio 에 그런 칸이 없어 통째로 사라진다. `## 출처`
칸이 아니다. 원본 경로는 frontmatter 의 `source` 에 있다.
## 종류마다의 칸
| 종류 | 기록 `.md``##` 이름 | 본문 |
|---|---|---|
| **Case** | `관계` · `문제` · `결론` · `검증 환경` · `재현 조건` · `본문` | 있음 |
| **Concept** | `관계` · `본문` | 있음 |
| **Reference** | `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시` | 없음 |
| **Question** | `관계` · `사실` · `가정` · `미지수` · `제약` · `선택지` · `다음 검증` | 없음 |
| **Decision** | **`근거`** · `결정문` · `판단 이유` · `영향` | 없음 |
**Decision 만 관계 절 이름이 `근거` 다.** 그리고 근거가 1개 이상 없으면 게시가 거절된다.
## 화면의 라벨은 기록의 절 이름과 다르다
**이것이 이 문서에서 가장 자주 틀리는 자리다.** 위 표는 기록 `.md` 가 쓰는 이름이고,
편집 화면의 라벨은 다른 말을 쓴다. 그리고 `/studio/documents/new` 의 종류 카드에 적힌 요약
(`목적 · 규칙 · 적용 조건 · 예외 · 예시`)은 **카드 문구이지 편집 화면의 라벨이 아니다.**
Reference 에서 실제로 확인한 대응이다.
| 기록의 `##` | 편집 화면의 라벨 |
|---|---|
| `목적` | **`이 기준을 쓰는 이유`** |
| `규칙` | **`판단 기준`** — 제목과 본문 두 칸이 한 줄이다 |
| `적용 조건` | **`적용할 때`** |
| `예외` | **`예외와 주의`** |
| `예시` | `예시` |
| `관계` | `관계` |
종류 이름도 자리마다 다르다. 상태 레일은 Reference 를 **`적용 기준`** 이라고 부르고,
`새 문서` 화면의 라디오는 `Reference` 다.
**화면을 먼저 스냅샷으로 읽고 그 라벨을 쓴다.** 이 표를 외워서 넣지 않는다 — 화면이 바뀌면
표가 먼저 낡는다.
## 기록에 없는데 화면에 있는 칸
| 칸 | 무엇 |
|---|---|
| `축` | 주제 안의 변이(SPA · Mediator · BFF · Forward-Auth). **주제를 고른 뒤에 나타난다.** 안 고르면 주제 공통 기록이 된다 |
| `마지막 검증일` | 기록의 `verifiedOn` 이다. 없으면 비워 둔다 |
**근거가 없으면 비워 둔다.** `verifiedOn` 이 없는 채로 저장하면 미리보기에 「마지막 검증」 절이
값 없이 뜬다. 그것은 날짜를 지어내는 것보다 낫다 — 게시 전에 사람이 채울지 정한다.
## frontmatter 에서 화면으로 가는 값
| frontmatter | 어디로 |
|---|---|
| `id` | 편집 주소 `/studio/documents/<id>/edit` |
| `kind` | 새 문서를 만들 때 고르는 종류 |
| `slug` · `title` | 화면 위쪽의 슬러그·제목 칸 |
| `topic` · `topicName` · `project` | 주제·프로젝트 선택 |
| `basisVersion` (Concept) | 기준 버전 칸 |
| `questionStatus` (Question) · `decisionStatus` (Decision) | 상태 선택 |
| `assets[].file` | 올릴 Asset 파일 |
| `assets[].key` | 본문 `:::evidence key` 의 저장소 쪽 이름. 올리면 서버 키로 바뀐다 |
**계약이 화면에 주는 상태와 도메인이 들고 있는 상태가 다르다.** `QuestionStatus` 는 화면에서
`OPEN`·`RESOLVED` 둘인데 도메인은 `OPEN`·`INVESTIGATING`·`PAUSED`·`RESOLVED` 넷이고,
Decision 의 도메인 `ACCEPTED` 가 화면에서는 `ADOPTED` 로 보인다. 화면에서 고른 값이 도메인
상태를 덮어쓰지는 않는다.
## 본문이 없는 세 종류
`Reference`·`Question`·`Decision` 의 칸은 **평문으로 렌더링된다.**
- 백틱과 파이프가 글자 그대로 보인다. 코드·표를 넣지 않는다
- 줄바꿈은 `<br>` 로만 살아난다
- 나열은 쉼표로 잇지 말고 `이름 : 값` 으로 줄을 나눈다
코드·표·그림이 필요하면 짝이 되는 Case 나 Concept 에 담고 `관계` 로 가리킨다.
## 본문을 넣기 전에
저장소의 `.md` 는 그림을 마크다운 이미지로 싣는다. Studio 는 `:::evidence` 를 쓴다. 바꾸는 것은
스크립트가 한다.
```bash
python3 scripts/studio-body.py <기록.md> --body-only # 저장소 key 그대로
python3 scripts/studio-body.py <기록.md> --key a=a-1a2b3c4d --body-only # 서버가 준 key 로
```
저장소 파일 자체는 고치지 않는다.
## 되돌아오는 값
저장이 끝나면 Studio 가 `id` 를 준다. 새로 만든 기록이면 frontmatter 의 `id``studio:`
채운다. 그 두 칸이 차면 색인이 `publication``게시됨` 으로 적는데, **그것은 「Studio 에
있다」는 뜻이고 공개됐다는 뜻이 아니다.** 공개 여부는 `status``public:` 이 말한다.
@@ -0,0 +1,160 @@
---
name: running-tech-log-pipeline
description: Use when a codebase must go all the way to a saved Tech Log Studio draft — running the seven stages (SSOT, tree, record, diagram, prose, voice, Studio save) as separate subagents, one skill per stage, with a run ledger that records which skill each stage actually used and which gate it passed.
---
# Tech Log 파이프라인 실행
## 이 스킬이 하는 일
스킬 일곱 개를 **한 줄기로 잇는다.** 각 스킬은 자기 단계만 알고 앞뒤를 모른다. 이 스킬이
단계 사이의 인계물과 순서와 관문을 정하고, 단계마다 **서브에이전트를 하나씩 띄운다.**
한 실행이 남기는 것은 산출물과 **런 원장**(`runs/<프로젝트>/<runId>/run.json`) 둘이다.
원장은 단계마다 어떤 스킬을 실제로 읽었고 어떤 관문을 어떤 종료 코드로 지났는지를 적는다.
`python3 scripts/verify-pipeline-run.py <원장>` 이 그 원장을 검사한다.
**원장이 이 스킬의 존재 이유다.** 스킬을 안 읽고 쓴 글과 읽고 쓴 글은 결과물만 봐서는
구분되지 않는다. 원장은 그것을 기계가 구분할 수 있게 만든다.
## 왜 단계마다 서브에이전트인가
한 세션이 일곱 단계를 이어서 하면 앞 단계의 문맥이 뒤 단계로 새어 든다. 그러면 세 가지가
망가진다.
- **스킬을 안 읽고도 그럴듯하게 쓴다.** 3단계에서 본문 규칙을 읽었으니 5단계에서
`rewriting-technical-prose-naturally` 를 열지 않아도 문장이 나온다. 스킬이 적용됐는지
확인할 방법이 사라진다.
- **관문이 형식이 된다.** 자기가 쓴 글을 자기가 검사하면 실패를 「이건 예외」로 넘긴다.
- **어디서 어긋났는지 못 찾는다.** 결과가 이상할 때 일곱 단계 중 어디가 원인인지 가릴 수 없다.
서브에이전트는 **자기 단계의 입력 파일과 자기 스킬만** 받는다. 다른 단계가 무엇을 했는지
모른다. 그래서 스킬을 안 읽으면 못 쓰고, 못 쓰면 원장에 남는다.
## 일곱 단계
| # | 단계 | 스킬 | 산출물 |
|---|---|---|---|
| S1 | 코드베이스 → SSOT | `analyzing-codebase-for-tech-log` | `docs/<프로젝트>/final/document.md` |
| S2 | SSOT → 분해 계약 | `deriving-tech-log-root-tree` | `docs/<프로젝트>/tech-log-studio/tech-log-tree.json` |
| S3 | 글감 → 기록 | `writing-tech-log-records` | `.../<주제>/<종류>/<기록>.md` |
| S4 | 기록 → 그림 | `technical-visualizer` | `final/assets/<이름>/` · `final/.techviz/<이름>/` |
| S5 | AI 티 제거 | `rewriting-technical-prose-naturally` | 같은 기록 파일 (제자리 수정) |
| S6 | 일한 사람의 목소리 | `writing-as-the-person-who-did-it` | 같은 기록 파일 (제자리 수정) |
| S7 | Studio 저장 | `publishing-tech-log-to-studio` | Studio 작업본 + `studio:` URL. **게시하지 않는다** |
단계마다의 입력·관문·원장 칸은 [references/stage-contracts.md](references/stage-contracts.md).
서브에이전트에 그대로 넣는 프롬프트는 [references/subagent-prompts.md](references/subagent-prompts.md).
## 순서가 고정된 곳
세 자리는 바꾸면 결과가 틀어진다.
**S3 → S4.** 그림은 기록을 쓴 다음에 만든다. 무엇을 그릴지는 기록 본문이 정하고
(`writing-tech-log-records/references/choosing-a-diagram.md` 의 세 관문), 그림의 근거는 그
기록의 `source` 앵커가 가리키는 SSOT 절이다. **기록 본문이 그림의 소재를 고르고, SSOT 절이
그림의 사실을 댄다.** 기록 `.md``techviz prepare` 에 넣지 않는다 — 기록은 SSOT 의 인용이라
줄 번호가 근거가 되지 못한다.
**S4 → S5 → S6.** 문장 손질은 본문이 다 찬 뒤에 한다. 그림을 붙이면 본문에 그림을 읽는 문단이
한둘 늘고, 그 문단도 같은 손질을 받아야 한다. S5 가 먼저다 — 번역투와 반복 문형을 걷어낸 뒤라야
S6 이 채울 자리(선택·비교·어긋남)가 보인다. 순서를 뒤집으면 S5 가 S6 이 넣은 목소리를
「과한 대구」로 다시 깎는다.
**S6 → S7.** Studio 에는 문장 손질이 끝난 것만 넣는다. 저장한 뒤에 고치면 Studio 쪽 `version`
이 올라가고 검증 산출물이 무효가 된다.
## S5·S6 뒤에는 S3 관문을 다시 돌린다
문장을 고치면 본문 문법과 인용이 함께 움직인다. `check_prose` 가 error 0 이어도 `check_body`
가 깨지거나, 고쳐 쓴 코드블록 한 줄이 SSOT 와 달라져 `check_evidence` 가 걸린다.
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
원장에서는 이것이 S5·S6 의 관문이지 S3 의 재실행이 아니다.
## 건너뛰어도 되는 단계
이미 있는 것을 다시 만들지 않는다. 건너뛴 단계도 **원장에 `SKIPPED` 와 사유를 적는다.**
적지 않고 빠뜨린 것과 판단해서 건너뛴 것을 구분해야 한다.
| 단계 | 건너뛰는 조건 |
|---|---|
| S1 | `final/document.md` 가 있고 그 안에서 이 글감의 근거를 찾을 수 있다 |
| S2 | `tech-log-tree.json` 에 이 글감이 `PROMOTE` · `CONFIRMED` 로 이미 있다 |
| S4 | 그림이 필요 없다(세 관문에 걸린다) 또는 `ssot-assets` 가 배정한 그림이 이미 있다 |
| S7 | 사용자가 Studio 반입을 요청하지 않았다 |
**S3·S5·S6 은 건너뛰지 않는다.** 기록을 쓰는 단계와 문장을 고치는 두 단계는 이 파이프라인의
산출물 자체다.
## 실행 절차
### 0. 런을 연다
```bash
python3 scripts/verify-pipeline-run.py --init runs/<프로젝트>/<runId>/run.json \
--project <프로젝트> --record <기록 경로>
```
`runId``YYYY-MM-DD-HHMM` 이다. 원장의 틀은
[templates/run.json](templates/run.json) 이고, `--init` 이 그 틀을 채워 놓는다.
### 1. 단계마다 서브에이전트를 띄운다
프롬프트는 [references/subagent-prompts.md](references/subagent-prompts.md) 의 것을 쓴다.
프롬프트에 **반드시** 들어가야 하는 넷이 있다.
1. **스킬 이름과 「먼저 그 SKILL.md 를 끝까지 읽어라」** — 요약을 주지 않는다. 요약을 주면
에이전트가 스킬을 안 연다.
2. **자기 단계의 입력 파일 경로만.** 앞 단계가 무엇을 했는지 설명하지 않는다.
3. **관문 명령 원문과 「error 0 까지 고쳐라」.**
4. **스킬 영수증** — 그 SKILL.md 에서 자기 단계에 해당하는 규칙 한 줄을 **원문 그대로**
인용해 돌려보내게 한다. 이것이 스킬을 열었다는 기계 검증 가능한 증거다
(`verify-pipeline-run.py` 가 그 문자열이 실제 SKILL.md 안에 있는지 대조한다).
한 단계가 끝나면 그 결과를 원장에 적고 다음 단계를 띄운다. **단계를 병렬로 띄우지 않는다**
S1~S7 은 앞 단계의 산출물이 뒤 단계의 입력이다. 병렬이 되는 것은 같은 단계 안에서
**서로 다른 기록 여러 건**을 처리할 때뿐이다.
### 2. 원장을 검사한다
```bash
python3 scripts/verify-pipeline-run.py runs/<프로젝트>/<runId>/run.json
```
error 0 이어야 런이 끝난 것이다. 이 검사기가 보는 것은 결과물의 품질이 아니라 **절차의
준수**다 — 단계가 빠졌는지, 스킬 영수증이 그 스킬의 실제 문장인지, 관문이 돌았고 종료 코드가
0 이었는지, 적어 낸 산출물이 디스크에 있는지.
### 3. 프로젝트 검사기를 돌린다
```bash
python3 scripts/verify-tech-log-tree.py <프로젝트>
python3 scripts/verify-project-layout.py <프로젝트>
python3 scripts/audit-records.py <프로젝트>
python3 scripts/check-figure-text.py <프로젝트>
```
## 이 스킬이 하지 않는 것
- **판단을 대신하지 않는다.** 어떤 후보를 `PROMOTE` 할지, 그림이 필요한지, 어떤 종류인지는
각 단계의 스킬이 정한다. 이 스킬은 그 스킬이 실제로 불렸는지만 본다.
- **관문을 완화하지 않는다.** 관문이 error 를 내면 그 단계는 끝나지 않은 것이다. 원장에
`FAILED` 로 적고 멈춘다. 다음 단계로 넘기지 않는다.
- **게시하지 않는다.** S7 은 저장까지다. 게시는 사람이 공개 화면을 보고 판단한다.
## 자주 어긋나는 자리
| 증상 | 원인 | 원장에 남는 모습 |
|---|---|---|
| 문장이 여전히 AI 같다 | S5 를 같은 세션이 겸했다 | `skillEcho` 가 비었거나 SKILL.md 에 없는 문장 |
| 그림 안에 문장이 있다 | S4 가 `check-figure-text.py` 를 안 돌렸다 | 관문 목록에 그 명령이 없다 |
| SSOT 에 없는 인용이 있다 | S3 이 앞 기록에서 코드를 옮겨 적었다 | `check_evidence` 종료 코드 ≠ 0 |
| 기록이 색인에 없다 | S2 를 건너뛰고 S3 을 했다 | S2 가 `SKIPPED` 인데 사유가 없다 |
| Studio 에서 그림이 안 보인다 | S7 이 Asset 을 올리기 전에 본문을 넣었다 | S7 관문에 미리보기 확인이 없다 |
@@ -0,0 +1,254 @@
# 단계 계약
단계마다 넷을 정한다 — **입력 · 스킬 · 관문 · 산출물.** 원장에 적히는 것도 이 넷이다.
관문은 종료 코드가 0 이어야 지난 것이다. 0 이 아니면 그 단계는 `FAILED` 이고 다음 단계로
넘어가지 않는다.
---
## S1 — 코드베이스 → SSOT
| | |
|---|---|
| 스킬 | `analyzing-codebase-for-tech-log` |
| 입력 | 분석 대상 저장소 경로(사용자가 준다) · 그 저장소의 `AGENTS.md`(있으면) |
| 산출물 | `docs/<프로젝트>/final/document.md` |
| 관문 | `python3 scripts/verify-project-layout.py <프로젝트>` |
**분석 대상 저장소는 이 저장소 밖이다.** `docs/<프로젝트>` 는 이 저장소 기준이고, 분석 대상은
사용자가 준 절대 경로다. 두 저장소를 섞지 않는다 — 대상 저장소를 고치지 않는다.
**`analysis-queue.yaml` 은 여러 프로젝트를 줄 세울 때만 쓴다.** 사용자가 저장소 하나를
지목했으면 큐 없이 그 하나를 분석한다. 큐가 있으면 큐가 정한 활성 프로젝트를 따른다.
작업 재료(`analysis/` · `notes/` · `checkpoints/` · `state.json` · `source-index.md`)는 분석
중에만 있고, 끝나면 `final/document.md` 로 합치고 지운다.
```bash
python3 scripts/fold-analysis-into-final.py <프로젝트>
```
**S2 가 무엇을 기대하는지 알고 쓴다.** S2 는 `final/document.md` 를 절 단위로 훑어 후보를
찾는다. 절 제목이 무엇을 다루는지 말하지 않으면 후보가 안 잡힌다. 접어 넣은 문서라면
제1부(통합 분석)가 후보 범위이고 제2·3부는 근거다.
산출물이 이미 있고 이 글감의 근거가 그 안에 있으면 `SKIPPED` 로 적고 사유를 남긴다.
### 이미 접어 넣은 프로젝트를 다시 볼 때 — 대조 모드
`final/document.md` 가 이미 있는 프로젝트에 S1 을 다시 돌리는 것은 **분석이 아니라 대조**다.
스킬의 절차(작업 재료를 만들고 → 분석하고 → 접어 넣는다)를 그대로 밟으면 두 곳이 깨진다.
- `docs/<프로젝트>/``state.json`·`analysis/` 를 만들면 배치 검사기가 error 로 센다.
완료된 프로젝트의 폴더는 `final/``tech-log-studio/` 뿐이다.
- `final/document.md` 를 고치면 `ssotSha256` 이 어긋나 분해 계약이 통째로 무효가 된다.
그래서 대조 모드는 이렇게 돈다.
| | 분석 모드 | 대조 모드 |
|---|---|---|
| 작업 재료 | `docs/<프로젝트>/analysis/` | `runs/<프로젝트>/<runId>/stage/S1/` |
| SSOT | 만들거나 접어 넣는다 | **고치지 않는다.** 보강 후보만 적는다 |
| 끝 조건 | fold 하고 재료를 지운다 | 어긋난 것·빠진 것을 목록으로 남긴다 |
SSOT 가 코드와 **어긋나는** 것을 찾으면 그것은 보강 후보가 아니다. 크게 적고 사람에게
올린다 — 이미 그 SSOT 를 근거로 쓴 기록이 있기 때문이다.
---
## S2 — SSOT → 분해 계약
| | |
|---|---|
| 스킬 | `deriving-tech-log-root-tree` |
| 입력 | `docs/<프로젝트>/final/document.md` **하나** |
| 산출물 | `docs/<프로젝트>/tech-log-studio/tech-log-tree.json` |
| 관문 | `python3 scripts/build-tech-log-tree.py <프로젝트>``python3 scripts/verify-tech-log-tree.py <프로젝트>` (error 0) |
`analysis/**` 를 후보를 찾으려고 열지 않는다. 분석에만 있는 자료를 발견하면 `final/document.md`
를 먼저 보강한다.
**검사기나 계약이 요구하는데 스킬의 절차가 안 적는 칸이 있다.** 손으로 채운다.
| 칸 | 무엇 |
|---|---|
| `candidateScope` | 후보를 찾은 범위. 적지 않으면 모듈 분석 절 제목이 전부 글감이 된다 |
| `sourceRepository` | 분석한 저장소의 경로·리비전·그렇게 판단한 근거. 모르면 `null`, 지어내지 않는다 |
| `ssotSha256` | `build-tech-log-tree.py` 가 채운다. 그래서 build 를 먼저 돌리고 verify 를 돌린다 |
| `ssot-assets` · `ssot-evidence` | SSOT 가 이미 그린 그림과 이미 돌린 측정을 글감에 배정한다. 계약 문서에만 있고 절차에는 이 단계가 없다 |
| `candidateScope.excludedAnchorPattern` | 「범위 밖 글감」 검사가 이 칸으로 판정한다. 없으면 그 검사가 통째로 꺼진다 |
`assetLedger` 는 계약이 요구하지만 스크립트 어느 것도 읽지 않는다. 사람이 보는 칸이다.
**`source` 앵커의 형식이 어디에도 적혀 있지 않다.** keycloak 은 「h2 슬러그 + `-ap1`」 같은
합성 앵커를 쓰고, 제목 슬러그를 쓴 프로젝트도 있다. 검사기는 SSOT 경로를 포함하는지만 보고
실재하는 heading 으로 풀지 않는다. **한 프로젝트 안에서는 한 형식으로 통일한다** — S4 가
`--heading` 값을 이 앵커에서 옮기기 때문이다.
**검사기는 「후보 ↔ 글감」만 본다. 「SSOT ↔ 후보」는 안 본다.** 그래서 SSOT 에 있는 재료를
후보 대장에 올리지도 않고 지나쳐도 error 가 0 이다. 범위의 절을 끝까지 읽는 것은 사람의 일이다.
제외가 0 건인 분해는 선별하지 않은 분해다. 후보마다 처분(`PROMOTE`·`MERGE_INTO`·
`KEEP_IN_SSOT`·`NEEDS_EVIDENCE`·`NEEDS_DECISION`)을 적고, 사람이 다시 읽은 것만
`dispositionReview: CONFIRMED` 로 둔다.
---
## S3 — 글감 → 기록
| | |
|---|---|
| 스킬 | `writing-tech-log-records` |
| 입력 | `tech-log-tree.json` 의 노드 하나 · 그 노드의 `source` 앵커가 가리키는 SSOT 절 |
| 산출물 | `docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md` |
| 관문 | 아래 셋 |
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
**노드가 `PROMOTE` 이고 `CONFIRMED` 인지 먼저 본다.** 아니면 쓰지 않는다.
**인용한 줄은 SSOT 에서 찾아 대조한다.** 앞선 기록에서 옮겨 적은 것은 확인한 것이 아니다 —
그렇게 게시된 기록에 SSOT 와 다른 redirect URI 가 네 곳 있었다.
그림이 필요해 보이면 **`final/assets/` 에 이미 있는지부터 본다.** 계약의 `ssot-assets` 가 이
글감에 배정한 그림이 있으면 그 파일을 그대로 가리킨다. 없을 때만 S4 로 넘긴다.
---
## S4 — 기록 → 그림
| | |
|---|---|
| 스킬 | `technical-visualizer` |
| 입력 | S3 이 쓴 기록 `.md`(무엇을 그릴지) · 그 기록의 `source` 앵커가 가리키는 SSOT 절(그림의 사실) |
| 산출물 | `final/.techviz/<이름>/{context.json,prompt.md,spec.json}` · `final/assets/<이름>/` |
| 관문 | `techviz lint` · `check-figure-text.py` · `check-figure-overlap.py` · `preview-figure.py` 로 눈 확인 |
**입력이 둘이라는 것이 이 단계의 전부다.**
- **무엇을 그릴지는 기록 본문이 정한다.** 세 관문(자리가 Case·Concept 인가 · 표가 아닌가 ·
옆 문단이 이미 말하지 않았는가)을 지나야 그린다. 그리고 **그림이 주장하는 것을 기록 본문이
말해야 한다.** 본문이 안 적은 단계를 그림만 넣으면 설명 없는 주장이 남는다. 순서는
본문을 먼저 보강하고 그다음 그림을 붙인다.
- **그림의 사실은 SSOT 절이 댄다.** 기록은 SSOT 의 인용이라 줄 번호가 근거가 되지 못한다.
`techviz prepare` 에는 `final/document.md` 를 넣고, 절은 기록의 `source` 앵커로 지목한다.
```bash
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "<기록의 source 앵커가 가리키는 절 제목>" \
-o docs/<프로젝트>/final/.techviz/<이름>/context.json
```
`--line` 은 쓰지 않는다. context 는 관리 블록을 접은 좌표를 쓰므로 파일 줄 번호와 어긋난다.
**그림 안에는 이름만 넣는다.** 문장은 `<desc>` 와 옆 문단에 둔다. lint 는 이것을 못 잡는다 —
`check-figure-text.py` 가 잡는다.
**lint 는 좌표를 안 본다.** 관계가 이어져 있는지만 본다. 그래서 구역 둘이 겹쳐 그려지거나
라벨이 상자에 먹혀도 통과한다. `check-figure-overlap.py` 가 그것을 본다.
```bash
python3 scripts/check-figure-overlap.py <프로젝트>
python3 scripts/check-figure-overlap.py --file 그림.svg
```
**그래도 마지막에는 눈으로 본다.** 검사기가 보는 것은 배경 사각형의 좌표라, 글자가 상자
밖으로 조금 나가거나 화살표가 라벨을 지나는 것은 못 잡는다.
기록에 되적는다 — frontmatter `assets:``key``file`(=`final/assets/<이름>/<이름>.svg`)
을 적고, 본문에는 마크다운 이미지로 넣는다. `:::evidence` 는 저장소에 쓰지 않는다. 사본을
`tech-log-studio/` 쪽에 두지 않는다 — 사본에는 `.techviz/<이름>/` 이 없어 다시 못 만든다.
---
## S5 — AI 티 제거
| | |
|---|---|
| 스킬 | `rewriting-technical-prose-naturally` |
| 입력 | S3·S4 를 지난 기록 `.md` (제자리 수정) |
| 산출물 | 같은 파일 |
| 관문 | `check_prose.mjs` error 0 · `style_profile.mjs` · S3 관문 재실행 |
```bash
S=.agents/skills/rewriting-technical-prose-naturally/scripts
node $S/check_prose.mjs --warn <기록.md> # error 0 까지 고친다
node $S/style_profile.mjs <기록.md>
```
문서 전체를 다시 쓴 것이 아니면 `--doc` 을 빼고 부른다.
**`density.mjs` 는 본문이 있는 종류에만 건다.** 그 기준은 글 한 편(낱말 1277+ · 코드블록 1+ ·
수치 13+)을 잰 값이고, Reference·Question·Decision 은 칸이 평문이라 코드블록을 넣는 것 자체가
규칙 위반이다. 본문 없는 종류에 걸면 구조적으로 통과할 수 없는 관문이 되고, 통과시키려면
없는 측정값을 지어내야 한다.
**문체 수치를 맞추려고 문장을 넣지 않는다.** 검사기는 표면 패턴만 보고 뜻은 못 본다.
**보호 구간을 건드리지 않는다** — 수치·날짜·버전·단위·코드·명령어·URL·직접 인용·공식 명칭은
원문과 한 글자도 달라지면 안 된다. 그래서 문장을 고친 뒤 `check_evidence.mjs` 를 다시 돌린다.
---
## S6 — 일한 사람의 목소리
| | |
|---|---|
| 스킬 | `writing-as-the-person-who-did-it` |
| 입력 | S5 를 지난 기록 `.md` · **그리고 그 기록의 상류 자료** (SSOT · 커밋 메시지 · 주석 · `확인하지 못한 것` 칸) |
| 산출물 | 같은 파일 |
| 관문 | `check_voice.mjs` · `check_prose.mjs` 재실행 · S3 관문 재실행 |
```bash
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <기록.md>
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
```
**S5 다음이다.** 번역투와 반복 문형을 걷어낸 뒤라야 채울 자리가 보인다. 그리고 이 스킬이
넣은 문장을 `check_prose` 가 다시 본다 — 그래서 재실행이 관문이다.
**자료에 흔적이 없으면 이 단계는 여기서 끝난다.** 없는 사람을 만들지 않는다. 「처음에는」·
「고민 끝에」·「놀랍게도」를 자료 없이 쓰면 지어낸 것이다. 원장에는 `DONE` 에 「흔적 없음」을
적는다 — `SKIPPED` 가 아니다. 찾아봤다는 것이 이 단계의 일이다.
**`check_voice.mjs` 는 목소리가 모자란지 재지 않는다.** 지어낸 목소리를 잡는다. 조용하다고
목소리가 생긴 것은 아니다.
---
## S7 — Studio 저장
| | |
|---|---|
| 스킬 | `publishing-tech-log-to-studio` |
| 입력 | S6 을 지난 기록 `.md` · frontmatter `assets:` 가 가리키는 SVG |
| 산출물 | Studio 작업본. 기록 frontmatter 의 `id`·`studio:` |
| 관문 | 상태 레일이 `저장됨` · `python3 scripts/build-tech-log-tree.py``verify-tech-log-tree.py` |
**저장까지다. 게시하지 않는다.**
Asset 을 본문보다 먼저 올린다. 순서를 뒤집으면 미리보기가 본문 전체를 막는다.
바뀐 것이 없으면 저장 버튼을 누르지 않는다 — 누를 때마다 `version` 이 올라가고 앞서 만든
검증·미리보기 산출물이 무효가 된다.
---
## 관문 요약
| 단계 | 명령 |
|---|---|
| S1 | `verify-project-layout.py <프로젝트>` |
| S2 | `build-tech-log-tree.py``verify-tech-log-tree.py` (error 0) |
| S3 | `studio-body.py``check_body.mjs` · `check_prose.mjs` · `check_evidence.mjs --repo` |
| S4 | `techviz lint` · `check-figure-text.py` · `check-figure-overlap.py` · `preview-figure.py` (눈 확인) |
| S5 | `check_prose.mjs` (error 0) · `style_profile.mjs` · S3 관문 |
| S6 | `check_voice.mjs` · `check_prose.mjs` · S3 관문 |
| S7 | `저장됨` 확인 · `build-tech-log-tree.py``verify-tech-log-tree.py` |
@@ -0,0 +1,275 @@
# 서브에이전트 프롬프트
단계마다 에이전트를 하나 띄운다. 아래를 그대로 쓰고 `<...>` 만 바꾼다.
## 모든 프롬프트에 들어가는 넷
1. **스킬 이름과 「SKILL.md 를 끝까지 먼저 읽어라」.** 요약을 주지 않는다. 요약을 주면
스킬을 안 연다.
2. **자기 단계의 입력 경로만.** 앞 단계가 무엇을 했는지 설명하지 않는다.
3. **관문 명령 원문.** 「검사해라」가 아니라 붙여 넣을 수 있는 명령을 준다.
4. **스킬 영수증** — SKILL.md 에서 한 줄을 **원문 그대로** 인용해 돌려보내게 한다.
`verify-pipeline-run.py` 가 그 문자열이 실제 파일 안에 있는지 대조한다.
## 돌려받는 형식 (모든 단계 공통)
```json
{
"stage": "S3",
"skill": "writing-tech-log-records",
"skillEcho": "<SKILL.md 에서 그대로 옮긴 한 줄>",
"status": "DONE",
"outputs": ["docs/keycloak/tech-log-studio/.../case-x.md"],
"gates": [{"cmd": "node ... check_body.mjs /tmp/studio-body.md", "exit": 0}],
"notes": "<판단한 것과 못 한 것>"
}
```
`skillEcho` 를 지어내지 말라고 프롬프트에 적는다. 파일에 없는 문장이면 검사기가 잡는다.
## 파일을 쓰라고 할 때는 방법을 함께 준다
서브에이전트의 `Write` 는 「보고는 파일이 아니라 글로 돌려라」는 기본 정책에 막힐 수 있다.
S1 처럼 산출물이 `.md` 인 단계는 그래서 한 줄을 더한다.
```
파일을 쓸 때 Write 툴이 막히면 Bash heredoc (`cat > 경로 <<'EOF'`) 을 써라.
```
이것을 안 적으면 에이전트가 산출물을 만들지 못하고 본문에 통째로 붙여 돌려준다.
---
## S1 — 코드베이스 → SSOT
```
너는 Tech Log 파이프라인의 1단계를 맡는다.
먼저 .agents/skills/analyzing-codebase-for-tech-log/SKILL.md 를 끝까지 읽어라.
references/ 아래 문서도 그 스킬이 읽으라는 것을 읽어라. 요약본은 주지 않는다.
대상 저장소: <절대 경로>
쓸 곳: docs/<프로젝트>/
analysis-queue.yaml 이 없으면 이 저장소 하나만 분석한다. 큐가 없다는 이유로 멈추지 마라.
대상 저장소를 고치지 마라. 읽기만 한다.
끝나면 분석 재료를 SSOT 로 합쳐라:
python3 scripts/fold-analysis-into-final.py <프로젝트>
합친 뒤 analysis/ · notes/ · checkpoints/ · state.json · source-index.md 를 지운다.
관문:
python3 scripts/verify-project-layout.py <프로젝트>
error 0 이 될 때까지 고쳐라.
돌려줄 것 (JSON):
stage, skill, skillEcho, status, outputs, gates, notes
skillEcho 는 방금 읽은 SKILL.md 에서 네 작업에 해당하는 규칙 한 줄을 원문 그대로 옮긴 것이다.
지어내지 마라 — 파일에 그 문자열이 있는지 기계가 대조한다.
```
---
## S2 — SSOT → 분해 계약
```
너는 Tech Log 파이프라인의 2단계를 맡는다.
먼저 .agents/skills/deriving-tech-log-root-tree/SKILL.md 를 끝까지 읽어라.
references/candidate-disposition.md 와 references/decomposition-checklist.md 도 읽어라.
출력 계약은 .agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md 다.
입력: docs/<프로젝트>/final/document.md — 이것 하나다.
analysis/** 를 후보를 찾으려고 열지 마라.
출력: docs/<프로젝트>/tech-log-studio/tech-log-tree.json
검사기가 error 로 요구하는데 스킬 본문이 안 적는 칸 셋을 손으로 채워라:
candidateScope — 후보를 찾은 범위
sourceRepository — 분석한 저장소의 경로·리비전·판단 근거. 모르면 null 로 두고 지어내지 마라
ssotSha256 — build 가 채운다. 그래서 build 를 먼저 돌린다
제외가 0 건인 분해는 선별하지 않은 분해다. 후보마다 처분을 적고, 다시 읽은 것만
dispositionReview: CONFIRMED 로 둬라.
관문:
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
error 0 까지 고쳐라.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S3 — 글감 → 기록
```
너는 Tech Log 파이프라인의 3단계를 맡는다.
먼저 .agents/skills/writing-tech-log-records/SKILL.md 를 끝까지 읽어라.
그 스킬이 가리키는 references/ 중 네 종류에 해당하는 것을 읽어라 —
record-kinds.md · writing-each-kind.md · body-syntax.md · code-tables-diagrams.md ·
explaining.md · ai-tells.md · choosing-a-diagram.md.
글감: docs/<프로젝트>/tech-log-studio/tech-log-tree.json 의 <주제> / <종류> / "<제목>"
그 노드가 PROMOTE 이고 dispositionReview 가 CONFIRMED 인지 먼저 확인해라. 아니면 쓰지 마라.
근거: 그 노드의 source 앵커가 가리키는 docs/<프로젝트>/final/document.md 의 절.
인용하는 줄은 SSOT 에서 찾아 대조해라. 기억이나 다른 기록에서 옮겨 적지 마라.
쓸 곳: docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<파일>.md
그림이 필요해 보이면 docs/<프로젝트>/final/assets/ 에 이미 있는지부터 봐라.
없으면 이 단계에서 만들지 말고 notes 에 "그림 필요: <무엇을>" 이라고 적어라. 4단계가 만든다.
관문:
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
error 0 까지 고쳐라.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S4 — 기록 → 그림
```
너는 Tech Log 파이프라인의 4단계를 맡는다.
먼저 두 개를 읽어라:
.agents/skills/technical-visualizer/SKILL.md — 끝까지
.agents/skills/writing-tech-log-records/references/choosing-a-diagram.md
기록: <기록.md>
이 기록을 읽고 무엇을 그릴지 정해라. 세 관문을 지나야 그린다 —
자리가 Case·Concept 인가 / 표로 될 것이 아닌가 / 옆 문단이 이미 말하지 않았는가.
그리고 그림이 주장하는 것을 이 기록 본문이 말하고 있어야 한다. 본문이 안 적은 단계를
그림만으로 넣지 마라.
그림의 근거는 기록이 아니라 기록의 source 앵커가 가리키는 SSOT 절이다:
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "<그 절 제목>" \
-o docs/<프로젝트>/final/.techviz/<이름>/context.json
--line 은 쓰지 마라.
그림 안의 <text> 는 전부 이름이어야 한다. 문장은 <desc> 와 옆 문단에 둬라.
관문:
./scripts/techviz lint docs/<프로젝트>/final/.techviz/<이름>/spec.json \
--context docs/<프로젝트>/final/.techviz/<이름>/context.json
python3 scripts/check-figure-text.py <프로젝트>
python3 scripts/check-figure-overlap.py <프로젝트>
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs
마지막 것은 PNG 로 떠서 라벨이 상자를 덮지 않는지 눈으로 봐라. lint 는 그것을 못 잡는다.
기록에 되적어라 — frontmatter assets: 에 key 와 file, 본문에는 마크다운 이미지.
:::evidence 를 저장소 .md 에 쓰지 마라.
돌려줄 것: 위 JSON 형식. skillEcho 포함. 그리지 않기로 했으면 status 를 SKIPPED 로 하고
어느 관문에 걸렸는지 적어라.
```
---
## S5 — AI 티 제거
```
너는 Tech Log 파이프라인의 5단계를 맡는다.
먼저 .agents/skills/rewriting-technical-prose-naturally/SKILL.md 를 끝까지 읽어라.
그 스킬이 "첫 rewrite 전에 읽으라"고 지정한 references 세 개도 읽어라 —
document-skeleton.md · article-shape.md · korean-tech-blog-register.md.
고칠 파일: <기록.md> (제자리에서 고친다)
너의 일은 문체다. 사실을 만들지 마라. 분류가 틀렸거나 근거가 모자란 것은 네 일이 아니다 —
발견하면 고치지 말고 notes 에 적어라.
보호 구간을 건드리지 마라: 수치 · 날짜 · 버전 · 단위 · 코드 · 명령어 · URL · 직접 인용 ·
공식 명칭. 한 글자도 달라지면 안 된다.
관문:
S=.agents/skills/rewriting-technical-prose-naturally/scripts
node $S/check_prose.mjs --warn <기록.md> # error 0 까지
node $S/style_profile.mjs <기록.md>
그리고 문장을 고쳤으니 본문 문법과 인용을 다시 본다:
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
수치를 맞추려고 문장을 넣지 마라. 검사기는 표면 패턴만 본다.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S6 — 일한 사람의 목소리
```
너는 Tech Log 파이프라인의 6단계를 맡는다.
먼저 .agents/skills/writing-as-the-person-who-did-it/SKILL.md 를 끝까지 읽어라.
references/voice-moves.md 도 읽어라. 그 스킬이 "이것보다 먼저 본다"고 한
../rewriting-technical-prose-naturally/references/article-shape.md 를 먼저 읽어라.
고칠 파일: <기록.md> (제자리에서 고친다)
상류 자료: docs/<프로젝트>/final/document.md · <대상 저장소의 커밋 메시지·주석·README>
찾을 것은 자료에 남아 있는 사람의 흔적이다 — 무엇을 골랐고 무엇과 견주었나,
확인하지 못한 것이 무엇이고 그것이 어느 주장에 걸리나, 처음 생각과 어긋난 자리가 있나.
없는 사람을 만들지 마라. 「처음에는」·「고민 끝에」·「놀랍게도」를 자료 없이 쓰면 지어낸 것이다.
넣은 문장마다 그것이 어느 파일 어느 줄에서 왔는지 댈 수 있어야 한다.
자료에 흔적이 없으면 아무것도 넣지 말고 그렇게 보고해라. 그것도 이 단계를 한 것이다.
관문:
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <기록.md>
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
check_voice.mjs 는 목소리가 모자란지 재지 않는다. 지어낸 목소리를 잡는다.
돌려줄 것: 위 JSON 형식. skillEcho 포함. 흔적이 없었으면 status DONE 에 notes 로 적어라.
```
---
## S7 — Studio 저장
```
너는 Tech Log 파이프라인의 7단계를 맡는다.
먼저 .agents/skills/publishing-tech-log-to-studio/SKILL.md 를 끝까지 읽어라.
references/studio-form-map.md 와 references/playwright-recipes.md 도 읽어라.
넣을 기록: <기록.md>
도구: Playwright MCP (mcp__playwright__browser_*)
저장까지만 한다. 게시 버튼을 누르지 마라. 한 번 게시한 문서는 취소해도 삭제가 409 로 거절된다.
상태 레일 aside[class*="studio-document-status"] 가 25초 안에 안 뜨면 인증이 안 된 것이다.
로그인 화면에 자격증명을 입력하지 말고 거기서 멈추고 사용자에게 알려라.
frontmatter 의 id 와 studio: 가 이미 있으면 그 주소로 가라. 새로 만들지 마라.
그림이 있으면 Asset 을 본문보다 먼저 올려라. 순서를 뒤집으면 미리보기가 본문을 막는다.
관문:
상태 레일의 글자가 저장됨 으로 바뀌는 것을 확인 (최대 30초)
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
저장됨 이 안 뜨면 버튼을 다시 누르지 말고 레일의 글자를 그대로 보고해라.
돌려줄 것: 위 JSON 형식. skillEcho 포함. gates 에 저장 전후 version 을 적어라.
```
@@ -0,0 +1,101 @@
{
"schemaVersion": 1,
"runId": "<YYYY-MM-DD-HHMM>",
"project": "<프로젝트>",
"record": "<docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md — 이 런이 만드는 기록>",
"startedAt": "<ISO-8601>",
"finishedAt": null,
"stages": [
{
"id": "S1",
"name": "코드베이스 → SSOT",
"skill": "analyzing-codebase-for-tech-log",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S2",
"name": "SSOT → 분해 계약",
"skill": "deriving-tech-log-root-tree",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S3",
"name": "글감 → 기록",
"skill": "writing-tech-log-records",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S4",
"name": "기록 → 그림",
"skill": "technical-visualizer",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S5",
"name": "AI 티 제거",
"skill": "rewriting-technical-prose-naturally",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S6",
"name": "일한 사람의 목소리",
"skill": "writing-as-the-person-who-did-it",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S7",
"name": "Studio 저장",
"skill": "publishing-tech-log-to-studio",
"runBy": "subagent",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
}
]
}
+46 -1
View File
@@ -192,6 +192,16 @@ Load supporting guidance only as needed:
`techviz build ... --document docs/<프로젝트>/final/document.md` 가 그 블록을 갱신한다.
### Tech Log 파이프라인에서 불릴 때
`running-tech-log-pipeline` 의 4단계가 이 스킬이다. 입력이 둘이라는 것만 다르다.
- **무엇을 그릴지는 방금 쓴 기록 본문이 정한다.** 세 관문은
`../writing-tech-log-records/references/choosing-a-diagram.md` 에 있고, 그림이 주장하는
것을 기록 본문이 말하고 있어야 한다.
- **그림의 사실은 SSOT 절이 댄다.** 기록은 SSOT 의 인용이라 줄 번호가 근거가 되지 못한다.
`prepare` 에는 `final/document.md` 를 넣고 절은 기록의 `source` 앵커로 지목한다.
### Tech Log 기록으로 옮길 때
런의 `document.md` 는 마크다운 이미지로 그림을 싣지만, Studio 기록은 다르다. SVG 를 Studio 에 Asset 으로
@@ -203,10 +213,45 @@ Load supporting guidance only as needed:
```
`references/code-tables-diagrams.md`(writing-tech-log-records)의 규칙이 함께 적용된다. 특히
**`<text>` 는 이름만 담고 문장은 `<desc>` 와 옆 문단에 둔다.** 이 저장소의 기존 손그림 SVG 는 이 규칙을
**`<text>` 는 이름만 담고 문장은 `<desc>` 와 옆 문단에 둔다.** `render` 뒤에 반드시 돌린다 —
`spec.json``label`·`details`·edge `label` 이 그대로 `<text>` 가 되므로 스펙을 쓸 때부터 이름으로 쓴다.
```bash
python3 scripts/check-figure-text.py <프로젝트> # <text> 가 전부 이름인가
python3 scripts/check-figure-overlap.py <프로젝트> # 상자와 라벨이 서로를 덮지 않는가
```
### 컴파일한 뒤 반드시 눈으로 본다
**lint 는 라벨이 상자를 덮는 것을 못 잡는다.** 엣지가 노드를 지나가는 것(`edge-through-node`)은
보지만 라벨은 앵커 점만 보고 폭을 재지 않는다. 그래서 `PASS` 인 그림에도 라벨이 상자에 먹히거나
경계선 위에 얹히는 일이 생긴다. SVG 를 PNG 로 떠서 본다.
```bash
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs
python3 scripts/preview-figure.py --file 그림.svg
```
지금까지 확인한 것:
| 증상 | 원인 | 대응 |
|---|---|---|
| 엣지 라벨이 옆 상자에 먹힌다 | 라벨이 길다 | 라벨을 짧은 이름으로. 자세한 것은 노드 `details` 로 |
| 라벨이 group 점선 위에 얹힌다 | `two-zone-pipeline` 은 지역 **안쪽** 엣지 라벨을 캔버스 top 에 고정한다 | 지역 안 엣지를 없애거나 `component-flow` + `groups` 로 바꾼다 |
| 원기둥이 제목·항목을 덮는다 | `shape: cylinder``details` 가 많다 | `details` 를 줄이거나 `shape: box` | 이 저장소의 기존 손그림 SVG 는 이 규칙을
어기고 문단과 각주 번호·커밋 해시를 캔버스 안에 넣어 두었다. 다시 만들 때 그 문장들은 본문으로 내린다.
### 그림을 만들기 전에
`rewriting-technical-prose-naturally``## Figures` 를 먼저 읽는다. 옆 문단이 이미 말한 것을 상자와
화살표로 다시 그린 그림은 만들지 않는다. 그림이 담아야 할 것은 순서·구조·측정값·실제 산출물이다.
**표로 되는 것을 그림으로 그리지 않는다.** `comparison` 프로필은 연결성 검사에서 빠지기 때문에
항목을 나란히 늘어놓기만 해도 lint 를 통과한다. 그것이 표다 — 표는 값을 비교하고 그림은
포함·순서·경계처럼 자리로만 보이는 것을 맡는다. 스펙을 쓰기 전에 묻는다.
> **관계선을 다 지워도 뜻이 남는가.** 남으면 표다. 마크다운 표로 쓴다.
`verify-project-layout.py` 의 「표로 되는 그림」이 관계선 없이 항목마다 같은 수의 `details`
늘어놓은 spec 을 센다. `comparison` 이 맞는 자리는 비교 자체가 자리로 드러나는 때다 — 겹치는
범위, 갈라지는 경계처럼.
@@ -46,16 +46,21 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
남겨 두고 나중에 걷어내는 순서가 아니다. **문체를 손보기 전에 문장을 고른다** — 설명이 끝난
뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 먼저 뺀다(`ai-tells.md` 첫 절). 문체 규칙의
정본은 `ai-tells.md` 다.
**그림이 필요한지, 필요하면 무엇을 그릴지는 `references/choosing-a-diagram.md`.** 자리(Case·
Concept 만) · 표가 아닌지 · 옆 문단이 이미 말하지 않았는지 세 관문을 지나야 그린다.
순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 **`final/assets/` 에 그
그림이 이미 있는지부터 본다.** SSOT 를 만들 때 그려 둔 것이 있고 계약의 `ssot-assets` 가 이
글감에 배정해 두었으면 그것을 쓴다 — `final/assets/tech-log-studio/` 로 복사하고 기록의
`assets` 가 그 사본을 가리킨다. **없을 때만** `technical-visualizer` 로 새로 만든다. 손으로
글감에 배정해 두었으면 기록의 `assets`**그 파일을 그대로** 가리킨다. 사본을 따로 만들지
않는다 — 사본에는 `.techviz/<이름>/` 이 없어 다시 만들 수 없다. **없을 때만**
`technical-visualizer` 로 새로 만든다. 손으로
SVG 를 그리지 않고, 모든 글에 그림을 만들지도 않는다.
인용할 측정도 같다. `final/evidence/` 에 있는 원문을 가리키고 같은 것을 다시 돌리지 않는다.
**그림과 측정을 그 자리에서 만들어 내기 전에 SSOT 가 이미 가진 것을 먼저 찾는다** — keycloak
에서는 그러지 않아 정본까지 갖춘 그림 13 장이 남고 이름이 다른 그림 5 장이 새로 만들어졌다.
4. **검사** — 셋 다 돌린다. 파서와 문장과 증빙은 각각 다른 것을 본다.
- `scripts/check_body.mjs` Case 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다.
- `scripts/check_body.mjs` — 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다.
저장소의 그림은 마크다운 이미지이므로 `python3 scripts/studio-body.py <기록> -o /tmp/x.md`
로 바꾼 파일에 돌린다. 저장소 파일에 그대로 돌리면 `unsafe image URL` 로 실패한다.
- `rewriting-technical-prose-naturally/scripts/check_prose.mjs` — 문장 규범. **error 0 이 될 때까지 고친다.**
칸 하나나 한 절만 고쳤으면 `--doc` 없이 부른다. 이어서 `style_profile.mjs` 로 문체 수치를 본다.
- `scripts/check_evidence.mjs <프로젝트> --repo`**인용한 것이 실재하는지.** 본문 코드블록의
@@ -77,7 +82,7 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
|---|---|---|
| `deriving-tech-log-root-tree` | 후보에 처분을 매기고 `PROMOTE` 를 글감으로 올린다 | 글을 쓰지 않는다 |
| `writing-tech-log-records` | 종류를 고르고 칸과 본문을 쓴다. `explaining.md`·`ai-tells.md` 를 처음부터 적용한다 | — |
| `technical-visualizer` | 문장으로 따라가기 어려운 관계를 그림으로 만든다 | 모든 글에 그림을 붙이지 않는다 |
| `technical-visualizer` | 문장으로 따라가기 어려운 관계를 그림으로 만든다 | 무엇을 그릴지 정하지 않는다 — `choosing-a-diagram.md` 가 정한다 |
| `rewriting-technical-prose-naturally` | 사실과 구조가 이미 맞는 초안의 번역투·반복 문형·과한 대구를 고친다 | 분류가 틀렸거나 근거가 모자란 것은 못 고친다 |
| `writing-as-the-person-who-did-it` | 자료에 남아 있는 선택·비교·어긋남·확인하지 못한 범위를 제자리에 놓는다 | 자료에 없는 「처음에는」·「고민 끝에」를 만들지 않는다 |
@@ -0,0 +1,82 @@
# 기록에 어떤 그림이 필요한지 정하는 기준
`code-tables-diagrams.md` 는 그림을 **어떻게** 그리는지를 말한다. 이 문서는 그 앞 단계 —
**이 기록에 그림이 필요한가, 필요하다면 무엇을 그리는가** 를 정한다.
## 세 관문
순서대로 통과해야 그림을 만든다. 하나라도 걸리면 그리지 않는다.
### 1. 자리가 있는가
`assets` 는 본문이 있는 두 종류만 갖는다 — **Case 와 Concept**. Reference·Question·Decision 의
칸은 평문으로 렌더링돼 그림이 들어갈 자리가 없다.
파생 기록에 그림이 필요해 보이면 그 그림은 **짝이 되는 Case 나 Concept 의 것**이다. 거기 담고
`관계`로 가리킨다. 담을 Case 나 Concept 이 없으면 그 그림은 아직 집이 없다 — 계약의
`assetLedger.unassigned` 에 그렇게 적고, 새 글감을 세울지는 따로 판단한다.
### 2. 표가 아닌가
> **관계선을 다 지워도 뜻이 남는가.** 남으면 표다.
표는 값을 비교하고, 그림은 **포함·순서·경계**처럼 자리로만 보이는 것을 맡는다. 항목을 같은
속성으로 늘어놓은 것은 마크다운 표로 쓴다. `verify-project-layout.py` 의 「표로 되는 그림」이
관계선 없이 항목마다 같은 수의 `details` 를 늘어놓은 spec 을 센다.
### 3. 옆 문단이 이미 말하지 않았는가
> **이 그림이 없으면 독자가 무엇을 못 보나.** 한 문장으로 답할 수 없으면 그리지 않는다.
기록에 이미 그 비교표가 있으면 그림은 중복이다. 실제로 그렇게 만든 그림 셋을 지웠다 —
`keyset-vs-offset` 을 넣은 기록에는 훑은 행·buffers·exec 까지 있는 플랜 비교표가 이미 있었다.
## 종류마다 무엇을 그리나
세 관문을 통과했을 때, 그 기록이 요구하는 그림은 종류마다 다르다.
| 종류 | 그림이 답하는 물음 | 흔한 profile |
|---|---|---|
| **Case** | 이 요청 한 번이 어떤 순서로 무엇을 지나갔나 | `sequence` · `component-flow` |
| **Case** (경계가 논지일 때) | 무엇이 어느 경계 안에 있고 무엇이 밖에 있나 | `two-zone-pipeline` |
| **Concept** | 남의 것이 어떤 순서·구조로 동작하나 | `sequence` · `component-flow` · `ports-adapters` |
Case 는 **내가 돌려서 본 것**이라 대개 순서가 논지다. Concept 은 **남의 것이 어떻게 동작하는지**라
구조나 변환 사슬이 논지다. 어느 쪽이든 「무엇이 무엇으로 바뀌는가」를 못 적으면 아직 그릴 것이
없다는 뜻이다.
**한 절에 그림 하나.** 같은 절에 구조 그림과 흐름 그림을 둘 다 넣으면 독자가 어느 쪽을 먼저
읽어야 하는지 알 수 없다. 둘 다 필요하면 절을 나눈다.
## 어디를 근거로 삼나 — 기록의 `source` 가 앵커다
`techviz prepare``final/document.md` 를 받는다. 기록은 `tech-log-studio/` 에 있지만 **그림의
근거는 기록이 아니라 기록이 가리키는 SSOT 절**이다. 기록의 `source` 앵커를 그대로 쓴다.
```bash
# 기록의 source: final/document.md#선택의-이유와-지킨-경계-ap1 이면
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "AP1: OAuth와 JWT 계약을 가장 가까이서 관찰한다" \
-o docs/<프로젝트>/final/.techviz/<id>/context.json
```
`--line` 은 쓰지 않는다. context 는 관리 블록을 접은 좌표를 쓰므로 파일의 줄 번호와 어긋난다.
`--heading` 이나 `--marker` 로 절을 지목한다.
**그림이 주장하는 것을 기록 본문이 말해야 한다.** SSOT 에 근거가 있어도 기록이 그 단계를 적지
않았으면 그림만 넣지 않는다 — 설명 없는 주장이 남는다. 순서는 하나다.
> 본문을 먼저 보강한다 → 그다음 그림을 붙인다.
`ap1-browser-bearer-flow` 가 그랬다. 마지막 단계인 `/api/me` 응답 4필드를 기록이 말하지 않아
붙이지 못하고 있다가, SSOT §474 를 근거로 본문에 한 줄을 더한 뒤에 붙였다.
## 만든 뒤
`code-tables-diagrams.md` 의 규범과 아래 둘을 함께 돌린다. lint 는 라벨이 상자를 덮는 것을
못 잡는다.
```bash
python3 scripts/check-figure-text.py <프로젝트> # <text> 가 전부 이름인가
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs # PNG 로 떠서 눈으로 본다
```
@@ -160,6 +160,28 @@ SVG는 `image/svg+xml`로 올라가고 다른 이미지와 같게 다뤄진다.
`READY`가 아닌 Asset은 게시 시 거절된다.
### 저장소의 `.md` 에는 `:::evidence` 를 쓰지 않는다
`:::evidence`는 Studio 렌더러의 구문이다. 저장소의 `.md`를 그 형태로 쓰면 편집기에서 그림이
보이지 않고 구문이 글자로 남는다. **저장소는 읽는 형태로 쓴다.**
```markdown
![브라우저 SPA, Keycloak, Resource Server 사이에서 …](../../../final/assets/ap1-browser-bearer-flow/ap1-browser-bearer-flow.svg)
```
`alt`는 그림의 `alt`를 그대로 쓰고, 경로는 frontmatter `assets`의 `file`과 같아야 한다.
Studio 로 보낼 때만 `:::evidence` 로 바꾼다. 손으로 고치지 않는다.
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/x.md # check_body 는 이 파일에 돌린다
python3 scripts/studio-body.py <기록.md> --body-only # Studio 에 붙여넣을 본문
python3 scripts/studio-body.py <기록.md> --key a=a-1a2b3c4d # 서버가 준 키로
```
Studio 파서는 상대 경로 이미지를 `unsafe image URL`로 거절하므로 `check_body.mjs`는 **바꾼
파일**에 돌린다. 저장소 파일에 그대로 돌리면 그림 자리에서 실패한다.
## 일반 이미지
Asset이 아닌 그림은 Markdown으로 쓴다.
@@ -62,7 +62,7 @@ topicName: OAuth/OIDC 인증 경계
```yaml
assets:
- key: eager-lazy-query-sequence # 본문의 :::evidence key 와 같은 값
file: ../../../final/assets/tech-log-studio/eager-lazy-query-sequence.svg
file: ../../../final/assets/diagrams/eager-lazy-query-sequence/eager-lazy-query-sequence.svg
evidence:
- ../../../final/evidence/explain/highlights-child-plan-A.txt
```
@@ -90,6 +90,10 @@
- [ ] 그림이 있는 문단에 글을 섞지 않았다
- [ ] `alt`가 무엇이 보이는지 말한다
- [ ] 그림 안 `<text>`가 전부 이름이다. 문장도 숫자도 없다 (`<title>`·`<desc>`는 예외)
- [ ] 눈으로만 보지 않고 `python3 scripts/check-figure-text.py <프로젝트>` 를 돌렸다
- [ ] 관계선을 다 지워도 뜻이 남는 그림이 아니다 (남으면 표다 — 마크다운 표로 쓴다)
- [ ] `python3 scripts/preview-figure.py`로 PNG 를 떠서 눈으로 봤다 (lint 는 라벨이 상자를 덮는 것을 못 잡는다)
- [ ] 본문의 그림이 마크다운 이미지다 (`:::evidence` 는 Studio 로 보낼 때 `studio-body.py` 가 만든다)
- [ ] 코드에 자격증명·토큰·내부 호스트가 없다. 지운 자리가 보인다
- [ ] 제목 id와 표 id가 겹치지 않는다
@@ -46,6 +46,31 @@ analysis material in one file. Only the first is candidate material.
`excluded` names the parts that are evidence rather than candidates. A node may cite an
anchor from an excluded part in `source`; it may not exist because of one.
`excludedAnchorPattern` is optional and is the only field the verifier can act on. Without it
the rule above is a sentence nobody enforces — the check that a node did not come *only* from
outside the scope is switched off entirely.
```json
"candidateScope": {
"document": "final/document.md",
"sections": ["§3", "§4"],
"excluded": ["제2부 — 모듈 분석 전문"],
"excludedAnchorPattern": "#a[0-9]+$"
}
```
It is a regular expression matched against each `source` anchor. A node whose anchors *all*
match it is an error: it was promoted from evidence, not from the candidate scope.
## Anchors resolve to real sections
`source` and `sourceRefs` anchors are checked against the SSOT's own headings when the project
writes them as heading slugs (`#검토한-선택지와-막힌-지점-ap1` = the h2 slug plus a
discriminator). **Keep one anchor style per project.** A project that numbers its anchors
(`#§1.1`, `#10-2`, `#a18`) gets a warning instead — the verifier cannot tell whether the
section it names exists, and the diagram stage cannot translate the anchor into a
`techviz prepare --heading` value without a person reading it.
## Topics
Each Topic has `topic` (its key), `title`, **`readerQuestion`**, and `kinds` with the five
@@ -165,6 +165,8 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
## 본문이 있는 두 종류의 공통 규칙
- **그림이 필요한지와 무엇을 그릴지는 `choosing-a-diagram.md` 가 정한다** — 종류마다 그림이
답해야 할 물음이 다르다. Case 는 순서, Concept 은 구조나 변환 사슬이 대개 논지다
- 그림은 `technical-visualizer` 로 만든다. 손으로 SVG 를 그리지 않는다
- 그림 안에는 이름만 넣는다. 문장은 `<desc>` 와 옆 문단에 둔다
- 그림과 증거는 frontmatter 의 `assets` · `evidence` 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다