diff --git a/CHANGELOG.md b/CHANGELOG.md index 66ebb5f2..8911aebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,14 @@ modified: 2026-09-13 ## [Unreleased] +### Added +- **`npm run docs:check`가 현행 문서의 버전 표지를 `package.json` 버전과 대조한다** — `docs/harness-overview.template.html`· + 생성본의 hero 배지·최신 🆕 배너·footer, `docs/what-changes-latest-version.html`의 footer, `docs/index.html` what-changes + 목록의 첫 항목. 지금까지는 이 표면들에 가드가 없어 overview 배지가 두 세대, 시뮬레이션 footer가 17릴리스 밀린 채 발행됐다. + `docs/` 최상위 HTML은 결정론적으로 분류된다(스냅샷 `-<버전>.html` / "기준" 라벨 문서 / 현행 / 무버전 — + `scripts/docs-version-drift.mjs` 머리 주석) — 현행으로 분류됐는데 등록도 명시 제외도 없는 새 문서는 검사가 실패한다. + `docs/harness-workflow-simulation.html`은 본문 현행화가 먼저라 사유와 함께 명시 제외했다(`docs/followups.md` 10번). + ### Fixed - **`init --stack X`로 강제한 스택이 저장되지 않아 `doctor`가 `stack` 관리 절을 stale로 잘못 경고하던 결함.** `doctor`의 관리 절 stale 검사·`migrate`의 관리 절 백업 diff·플래그 없는 `init`이 모두 감지 스택으로 다시 렌더해, diff --git a/MAINTAINING.md b/MAINTAINING.md index 41d3be8f..99846783 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -191,15 +191,16 @@ grep -rn 'origin/main' commands/ skills/ templates/ - **`docs/harness-overview.template.html`의 버전 표기 3곳을 손으로 갱신한 뒤 재생성합니다** — hero 배지(`vX.Y.Z`), 최신 그룹 배너 산문(`🆕 …` 블록), footer. `docs/harness-overview.html`은 생성물이지만 **자동 갱신되는 것은 커맨드·파일 인벤토리뿐** - 이고 이 셋은 템플릿에 하드코딩돼 있습니다. 그래서 `docs:check`가 green이어도 낡습니다 — - **`docs:check`를 세대 확인 근거로 쓰지 마세요.** 확인은 `grep -n 'v\?0\.[0-9.]*' docs/harness-overview.template.html`로 합니다. - 생성물을 직접 고치면 다음 `docs:generate`가 되돌립니다. 템플릿을 고치고 재생성하세요. + 이고 이 셋은 템플릿에 하드코딩돼 있습니다. `docs:check`는 이 셋의 **버전 번호**가 `package.json`과 + 같은지만 대조합니다(`scripts/docs-version-drift.mjs`) — 배너 **산문**이 이번 릴리스를 제대로 설명하는지는 + 사람이 봐야 합니다. 생성물을 직접 고치면 다음 `docs:generate`가 되돌립니다. 템플릿을 고치고 재생성하세요. - **`docs/index.html`의 what-changes 목록에 새 버전을 등재합니다** — 등재하지 않으면 방금 쓴 - 릴리스 노트가 문서 허브에서 도달 불가입니다. 이 목록에는 가드가 없어 빠뜨려도 아무것도 빨개지지 않습니다. + 릴리스 노트가 문서 허브에서 도달 불가입니다. `docs:check`가 목록의 첫 항목이 현행 버전인지 대조합니다. - **왜 이 두 줄이 절차에 있나:** 0.22.0과 0.23.0이 **연속으로** overview 템플릿을 놓쳐 배지가 두 세대(v0.21.0) 밀린 채 발행됐습니다. 가드 없는 표면은 절차에 적히지 않으면 반드시 밀립니다. - 결합 강도 순서를 기억하세요: `what-changes-*`(3방향 강제) > `harness-overview`(생성+pin, **산문은 무방비**) - > `prerequisites.md`(doctor 양방향) > `index.html`·simulation·guide류(**가드 0**). + 결합 강도 순서를 기억하세요: `what-changes-*`(3방향 강제) > `harness-overview`·`index.html`(생성+pin·버전 표지 대조, + **산문은 무방비**) > `prerequisites.md`(doctor 양방향) > simulation·guide류(**가드 0** — "X 기준" 라벨 문서와 + 명시 제외 문서는 표지 대조 대상이 아닙니다. 분류 규칙은 `scripts/docs-version-drift.mjs` 머리 주석). 6. `CHANGELOG.md`의 `## [Unreleased]`를 새 버전 헤딩(`## [X.Y.Z] - YYYY-MM-DD`)으로 이동 7. main에서 4~6단계의 결과를 **한 커밋**으로 만들어 push합니다. 기능 변경은 PR로 들어오지만, 릴리스 준비 커밋 자체는 그 PR들이 이미 병합된 main 위에 얹는 범프·문서 커밋입니다. ```bash diff --git a/docs/ao-worker-rules.md b/docs/ao-worker-rules.md index b1ec8607..4905030a 100644 --- a/docs/ao-worker-rules.md +++ b/docs/ao-worker-rules.md @@ -31,7 +31,7 @@ PR 브랜치에서 절대 건드리지 않는다. AO 는 워커마다 별도 워 런타임 의존성 0개, lockfile 없음. `npm install` 을 실행하지 마라. lockfile 을 만들지 마라. `package.json` 에 의존성을 추가하지 마라. - 테스트는 바로 돌린다: `npm test` (unit + e2e, 이어서 perf 를 `--test-concurrency=1` 로). 좁히려면 `npm run test:unit` / `npm run test:e2e`. -- CI 는 `npm test` 다음에 `npm run docs:check`(생성 문서 `docs/harness-overview.html` 바이트 대조)도 돈다. 로컬 검증도 둘 다 +- CI 는 `npm test` 다음에 `npm run docs:check`(생성 문서 `docs/harness-overview.html` 바이트 대조 + 현행 문서 버전 표지 대조)도 돈다. 로컬 검증도 둘 다 돌린다. docs:check 실패는 annotation 이 없으니 로컬에서 재현하고 `npm run docs:generate` 로 다시 만든다. - Node `>=24` 필수. CI 매트릭스도 `24` 단일 항목이며, 이유는 `test.yml` 주석에 있다 — "LTS 커버리지" 명목으로 18/20/22 를 되살리지 마라. diff --git a/docs/followups.md b/docs/followups.md index 717fa44c..272a8e90 100644 --- a/docs/followups.md +++ b/docs/followups.md @@ -7,7 +7,18 @@ ## 우선순위 -**남은 것: 없음.** +**남은 것: 10번 하나.** + +10. **`docs/harness-workflow-simulation.html` 본문 현행화 → 버전 드리프트 검사 등록** (2026-09-27, task `docs-version-drift-check`) + - 문제: hero 배지·🆕 배너·footer가 v0.40.0에 머물러 있다(현행과 16릴리스 차이). 그러나 본문 시나리오가 + 0.40.0 기준이라 표지만 고치면 틀린 문서에 현행 도장을 찍는다 — 적어도 0.42.1(handoff 단독 커밋 중단)· + 0.44.0(머지 후 종결은 커밋 하나)이 워크스루 단계에 걸린다. 0.40.1–0.44.2 CHANGELOG 전체를 대조해야 한다. + - 현재 상태: `scripts/docs-version-drift.mjs`의 `excludedCurrentDocuments`에 사유와 함께 올라 있다 — + 조용히 빠진 것이 아니라 **명시 제외**다. 분류가 `current`인 한 제외 항목은 유지되고, "기준" 라벨을 달아 + `baseline`이 되면 검사가 낡은 제외 항목이라며 실패한다. + - 완료 조건: 본문을 현행화하고 표지 셋을 package.json 버전에 맞춘 뒤, 항목을 `excludedCurrentDocuments`에서 + `currentVersionDocuments`로 옮긴다(표지는 `overviewMarkers`와 같은 hero·🆕·footer, footer 정규식만 확인). + `npm run docs:check` green. (4번은 2026-09-12에 **B(src 상수 + 문서 블록 + pin)로 결정**해 task `framing-prompts-in-src`로 올려 여기서 지웠다. testcritic은 `--rubric` 선택자로 3 루브릭 모두 src에 둔다.) (6번은 2026-09-12에 처리했다. 다만 **전제가 반쯤 뒤집혔다** — 실측 결과 이 머신(데스크톱 앱 세션)에서는 diff --git a/docs/harness-overview.html b/docs/harness-overview.html index 89b2be13..2cb15ba0 100644 --- a/docs/harness-overview.html +++ b/docs/harness-overview.html @@ -1658,6 +1658,11 @@

병합 마커 포맷

Module 프로젝트 소스 파일 + + scripts/docs-version-drift.mjs + Module + 현행 문서의 버전 표지를 package.json과 대조 (docs:check) + scripts/generate-harness-overview.mjs Module @@ -2368,6 +2373,11 @@

병합 마커 포맷

Test 자동 회귀 검증 + + tests/docs-version-drift.test.mjs + Test + 자동 회귀 검증 + tests/doctor.test.mjs Test diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-artifact.md b/docs/hslee/docs-version-drift-check/docs-version-drift-check-artifact.md new file mode 100644 index 00000000..9e9029ec --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-artifact.md @@ -0,0 +1,33 @@ +# docs-version-drift-check — Artifact + +*최종 결과물과 학습 내용을 기록한다.* + +## 결과 + + +## Reviews +*Codex 등 리뷰 실행 시 결과(요약·발견·조치)를 날짜와 함께 남긴다. 남기지 않은 리뷰는 "안 한 것"으로 간주.* +*기계 판독용 마커를 함께 남긴다: ``* + +### 2026-09-27T13:51:44.900Z — codex (harness-team review) + +- engine: codex · scope: diff · tip: f607e7e09e043e1feffeffea0db1135d1e20b0d4 · exit 0 · 1446 B + +```text +전하, **P2 발견 3건입니다.** `origin/main` 대비 diff를 읽기 전용으로 검토했고, `docs:check`의 기존 CI 연결 위치는 적절하며 현재 실행도 통과했습니다. + +- **P2** [scripts/docs-version-drift.mjs:46](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-8/scripts/docs-version-drift.mjs:46) — ``처럼 속성이 하나 추가되면 버전 표지를 인식하지 못해 새 현행 문서가 `unversioned`로 빠집니다. +- **P2** [scripts/docs-version-drift.mjs:50](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-8/scripts/docs-version-drift.mjs:50) — footer의 임의 버전도 표지로 취급해 `Requires React 19.0.0`인 무버전 문서를 `current`로 오분류합니다. +- **P2** [scripts/docs-version-drift.mjs:62](/Users/hsonpro/.ao/data/worktrees/harness-aijient-team-plugin/harness-aijient-team-plugin-8/scripts/docs-version-drift.mjs:62) — 첫 정규식 매치만 검사하므로 주석의 현행 hero 표지나 앞선 빠른 링크가 실제 낡은 표지를 가려도 통과합니다. + +**판정: should-fix.** 위 세 경우는 메모리 내 입력으로 재현했습니다. `npm run docs:check`와 쓰기가 필요 없는 신규 테스트 5개는 통과했으며, 전체 테스트는 실행하지 않았습니다. 파일은 변경하지 않았습니다. +``` + + + +판별·조치 (작성 세션): +- P2-1 속성이 더 붙은 hero 태그 미인식 → **진짜 결함**(현행 문서가 unversioned로 조용히 빠짐 = 미탐). 태그 정규식을 속성 순서·추가 속성에 무관하게 고쳤고 분류 테스트에 케이스 추가. +- P2-2 footer 속 타 제품 버전을 current로 오분류 → **오탐이지만 의도로 유지.** 실패가 시끄러운 방향이라 사람이 등록·제외·"기준" 라벨을 고르게 된다. 표지 형식이 문서마다 달라(`v0.44.2`·`Plugin 0.23.0`·`plugin · 0.20.0`) 좁히면 미탐이 생긴다. 머리 주석에 명시. +- P2-3 첫 매치만 검사 → **부분 수용.** HTML 주석이 표지를 가리는 경우는 분류·판정 전에 주석을 지워 고쳤다(테스트 추가). 첫 매치 규칙 자체는 유지 — 🆕 배너는 옛 배너가 뒤에 오는 게 정상이고, index 목록도 첫 항목이 최신이라는 게 계약이다. + +## Learnings diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-context.md b/docs/hslee/docs-version-drift-check/docs-version-drift-check-context.md new file mode 100644 index 00000000..d506e459 --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-context.md @@ -0,0 +1,27 @@ +# docs-version-drift-check — Context Card + + +## Now +- Goal: +- Current atomic step: +- Stop / human-decision condition: + +## Constraints and settled decisions +- + +## JIT retrieval map +- Identifiers / symbols: +- Narrow globs: +- Read next: +- Verification command: + +## Failure capsules (max 3 unresolved) +### F-001 +- Signal: +- Tried: +- Compact finding / current hypothesis: +- Next discriminator: +- Source (safe path or command): + +## Resume checklist +- diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-handoff.md b/docs/hslee/docs-version-drift-check/docs-version-drift-check-handoff.md new file mode 100644 index 00000000..806e0925 --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-handoff.md @@ -0,0 +1,28 @@ +# docs-version-drift-check — Handoff + +(세션 종료 시 post-commit hook이 자동 갱신합니다) + +## 2026-09-27T13:48:56.677Z — 5221318 feat(docs): docs:check가 현행 문서의 버전 표지를 package.json과 대조한다 +CHANGELOG.md | 8 ++ + MAINTAINING.md | 13 +-- + docs/ao-worker-rules.md | 2 +- + docs/followups.md | 13 ++- + docs/harness-overview.html | 10 +++ + .../docs-version-drift-check-artifact.md | 13 +++ + .../docs-version-drift-check-context.md | 27 ++++++ + .../docs-version-drift-check-handoff.md | 3 + + .../docs-version-drift-check-meta.json | 11 +++ + .../docs-version-drift-check-plan.md | 20 +++++ + .../docs-version-drift-check-spec.md | 42 ++++++++++ + scripts/docs-version-drift.mjs | 98 ++++++++++++++++++++++ + scripts/generate-harness-overview.mjs | 10 +++ + tests/docs-version-drift.test.mjs | 87 +++++++++++++++++++ + 14 files changed, 349 insertions(+), 8 deletions(-) + +## 2026-09-27T13:52:45.351Z — 9d29e26 fix(docs): 버전 표지 검사가 속성 붙은 태그를 인식하고 HTML 주석을 무시한다 +.../docs-version-drift-check-artifact.md | 20 ++++++++++++++++++++ + .../docs-version-drift-check-meta.json | 12 +++++++++++- + .../docs-version-drift-check-plan.md | 2 +- + scripts/docs-version-drift.mjs | 8 ++++++-- + tests/docs-version-drift.test.mjs | 6 ++++++ + 5 files changed, 44 insertions(+), 4 deletions(-) diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-meta.json b/docs/hslee/docs-version-drift-check/docs-version-drift-check-meta.json new file mode 100644 index 00000000..68bf3bb8 --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-meta.json @@ -0,0 +1,21 @@ +{ + "user": "hslee", + "task": "docs-version-drift-check", + "created": "2026-09-27", + "firstActivatedAt": "2026-09-27T13:43:25.278Z", + "status": "open", + "closedAt": null, + "forcedAt": null, + "forcedIssues": null, + "reviews": [ + { + "kind": "codex", + "engine": "codex", + "scope": "diff", + "tip": "f607e7e09e043e1feffeffea0db1135d1e20b0d4", + "at": "2026-09-27T13:51:44.900Z", + "exitCode": 0, + "outputBytes": 1446 + } + ] +} diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-plan.md b/docs/hslee/docs-version-drift-check/docs-version-drift-check-plan.md new file mode 100644 index 00000000..37bfcc42 --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-plan.md @@ -0,0 +1,20 @@ +# docs-version-drift-check — Plan + +## 목표 +현행 문서의 버전 표지 드리프트를 `docs:check`가 결정론적으로 잡는다. + +## 단계 +- [x] 대상 범위 확정 — 분류 규칙, 시뮬레이션은 명시 제외(오케스트레이터 결정 A) +- [x] 실패 테스트 작성 (`tests/docs-version-drift.test.mjs`) +- [x] 검사 구현 + `docs:check` 합류, overview 인벤토리 재생성 +- [x] MAINTAINING·ao-worker-rules·followups(10번)·CHANGELOG [Unreleased] 갱신 +- [x] `npm test`·`npm run docs:check` green +- [x] 커밋 + `harness-team review`(codex) + artifact 기록 +- [ ] push·PR·CI 확인 +- [ ] 머지 후 main에서 종결 (워커 몫 아님) + +## Ontology 변경 로그 +- 2026-09-27 현행 문서 / 기준 문서 / 표지 정의 신설 + +## 참고 +- docs/followups.md 10번 (시뮬레이션 본문 현행화) diff --git a/docs/hslee/docs-version-drift-check/docs-version-drift-check-spec.md b/docs/hslee/docs-version-drift-check/docs-version-drift-check-spec.md new file mode 100644 index 00000000..a6efe4a3 --- /dev/null +++ b/docs/hslee/docs-version-drift-check/docs-version-drift-check-spec.md @@ -0,0 +1,42 @@ +# docs-version-drift-check — Spec + +## 목적 / 요구사항 +- 문제: docs의 HTML 문서 중 "현행"을 표방하는 문서의 버전 표지(hero 배지·🆕 배너·footer 등)가 `package.json` 버전과 + 어긋나도 아무 가드가 없다. `docs:check`는 overview 생성본 바이트 대조뿐이라 green이어도 표지가 낡는다. + 실례: overview 배지 두 세대 지연(0.22.0·0.23.0), 시뮬레이션 footer v0.23.0 17릴리스 방치(09-19), 시뮬레이션 현재 v0.40.0. +- 영향: 문서 독자(소비자·유지보수자), 릴리스 절차 5단계. +- 기대 결과: 현행 문서의 버전 표지가 어긋나면 `npm run docs:check`가 실패한다. 새 문서가 현행인지 기준 문서인지 결정론적으로 갈린다. +- 제약: 새 의존성 금지, 기존 `docs:check`에 합류, 문서에 미래 버전 번호를 박지 않는다, 버전 범프 안 함. + +## 설계 / 접근 +- `scripts/docs-version-drift.mjs`: 분류기 + 등록 문서별 표지 정규식 + 명시 제외 목록. `generate-harness-overview.mjs --check`가 호출. +- 분류(docs/ 최상위 *.html, 첫 일치): snapshot(`-.html`) → baseline(버전 담은 hero 태그/footer 중 하나라도 "기준") → + current(버전 담은 hero 태그/footer, "기준" 없음) → unversioned. current는 등록 또는 사유 있는 명시 제외가 필수. + 제외 항목이 current가 아니게 되면 낡은 제외로 실패. +- 등록: overview 템플릿·생성본(hero·최신 🆕 상한·footer), what-changes-latest-version(footer — title·dd는 기존 테스트 몫), + index.html(what-changes 목록 첫 항목). +- 명시 제외: `harness-workflow-simulation.html` — 본문이 0.40.0 기준이라 표지만 고치면 거짓 도장. 오케스트레이터 결정 A(2026-09-27): + 본문 현행화는 후속 task, `docs/followups.md` 10번에 기록. +- 표지 옆 산문은 검사하지 않는다(사람이 쓴 `왜`). + +## Ontology +- **현행 문서**: 분류 규칙상 current — 표지가 항상 현행 package.json 버전이어야 하는 문서. +- **기준 문서**: 표지에 "X 기준"을 달아 기준 버전을 스스로 밝힌 문서(guide류·diagrams·schematics). 검사 대상 아님. +- **표지(marker)**: 버전 번호를 캡처하는 문서별 정규식. 못 찾으면 실패(가드가 조용히 꺼지지 않게). +- 게이트 근거: 목표·제약·성공 기준·영향 파일 모두 위에 명시, 범위는 오케스트레이터가 확정. + +## Ambiguity 자가진단 +- [x] **Goal 명확도** (40%) — 목표가 한 문장으로 구체화되었는가? +- [x] **Constraint 명확도** (30%) — 기술/시간/범위 제약이 명시되었는가? +- [x] **Success 기준** (30%) — 완료를 어떻게 측정하는가? +- [x] **Context 명확도** (brownfield 한정) — 영향 받는 기존 코드/파일을 식별했는가? +- [x] **Ambiguity ≤ 0.2** — 위 항목 가중합 ≥ 0.8 + +## Done evidence +```json +{ "version": 1, "review": "required" } +``` + +## 참고 +- `tests/docs-version-drift.test.mjs`, `scripts/docs-version-drift.mjs`, `tests/what-changes-latest-version.test.mjs`(선례) +- MAINTAINING.md 릴리스 절차 5단계 diff --git a/scripts/docs-version-drift.mjs b/scripts/docs-version-drift.mjs new file mode 100644 index 00000000..1d5c7f0e --- /dev/null +++ b/scripts/docs-version-drift.mjs @@ -0,0 +1,102 @@ +import { readFile, readdir } from 'node:fs/promises'; +import { join } from 'node:path'; + +// 현행 package.json 버전을 표방하는 docs/*.html의 버전 표지가 실제 버전과 어긋나는지 검사한다. +// `npm run docs:check`가 호출한다. 표지 옆의 산문(🆕 배너 본문 등)은 사람이 쓰며 여기서 검사하지 않는다. +// +// 분류 규칙 (docs/ 최상위 *.html, 위에서부터 첫 일치): +// 1. snapshot — 파일명이 `-<버전>.html`로 끝난다. 그 시점의 기록이라 검사하지 않는다. +// 2. baseline — 버전을 담은 hero 태그(``)나 footer 중 하나라도 "기준"을 단다. +// 기준 버전을 스스로 밝힌 문서라 현행을 표방하지 않는다 — 현행화하려면 "기준"을 떼고 등록한다. +// 3. current — 버전을 담은 hero 태그나 footer가 있고 "기준"이 없다. documents에 표지를 등록하거나, +// exclusions에 사유와 함께 올려야 한다. 어느 쪽에도 없으면 검사가 실패한다. +// 4. unversioned — 그 밖. 속 버전은 분류에 쓰지 않는다. +// HTML 주석은 분류·표지 판정 전에 지운다. footer 속 다른 제품의 버전(예: 의존성 버전)도 3으로 분류되는데, +// 이 오탐은 의도다 — 실패가 시끄러운 쪽이라 등록·제외·"기준" 라벨 중 하나를 사람이 고르게 된다. + +const versionPattern = /\d+\.\d+\.\d+/; +const snapshotPattern = /-\d+\.\d+(?:\.\d+)?\.html$/; +const stripComments = (html) => html.replace(/<!--[\s\S]*?-->/g, ''); + +export const overviewMarkers = [ + { name: 'hero badge', pattern: /<span class="tag tag-purple">v(\d+\.\d+\.\d+)<\/span>/ }, + // 묶음 배너(`🆕 0.37.0–0.39.1`)는 상한으로 판정한다. + { name: 'latest banner', pattern: /🆕 (?:\d+\.\d+\.\d+–)?(\d+\.\d+\.\d+)/ }, + { name: 'footer', pattern: /<footer>[^<]*?\bv(\d+\.\d+\.\d+)/ }, +]; + +export const currentVersionDocuments = [ + { path: 'docs/harness-overview.template.html', markers: overviewMarkers }, + { path: 'docs/harness-overview.html', markers: overviewMarkers }, + // title·요약 dd는 tests/what-changes-latest-version.test.mjs가 강제한다 — 여기서는 그 테스트가 안 보는 footer만. + { + path: 'docs/what-changes-latest-version.html', + markers: [{ name: 'footer', pattern: /<footer>Harness Aijient Team Plugin (\d+\.\d+\.\d+)/ }], + }, + { + path: 'docs/index.html', + markers: [{ name: 'newest what-changes entry', pattern: /href="what-changes-(\d+\.\d+\.\d+)\.html"/ }], + }, +]; + +export const excludedCurrentDocuments = [ + { + path: 'docs/harness-workflow-simulation.html', + reason: '본문 시나리오가 0.40.0 기준이라 표지만 고칠 수 없다 — 본문 현행화 후 documents로 옮긴다 (docs/followups.md)', + }, +]; + +function versionSurfaces(html) { + const tags = [...html.matchAll(/<span\b[^>]*\bclass="tag\b[^"]*"[^>]*>([^<]*)<\/span>/g)].map((match) => match[1]); + const footers = [...html.matchAll(/<footer[^>]*>([\s\S]*?)<\/footer>|<div class="footer">([\s\S]*?)<\/div>/g)] + .map((match) => match[1] ?? match[2]); + return [...tags, ...footers].filter((text) => versionPattern.test(text)); +} + +export function classifyDocument(path, html) { + if (snapshotPattern.test(path)) return 'snapshot'; + const surfaces = versionSurfaces(stripComments(html)); + if (surfaces.some((text) => text.includes('기준'))) return 'baseline'; + return surfaces.length > 0 ? 'current' : 'unversioned'; +} + +export function findMarkerDrift(html, markers, version) { + html = stripComments(html); + return markers.flatMap(({ name, pattern }) => { + const found = html.match(pattern)?.[1] ?? null; + return found === version ? [] : [{ marker: name, found }]; + }); +} + +export async function checkDocsVersionDrift( + root, + { documents = currentVersionDocuments, exclusions = excludedCurrentDocuments } = {}, +) { + const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')); + const registered = new Set(documents.map((document) => document.path)); + const excluded = new Set(exclusions.map((exclusion) => exclusion.path)); + const problems = []; + + for (const { path, markers } of documents) { + const html = await readFile(join(root, path), 'utf8'); + for (const drift of findMarkerDrift(html, markers, version)) { + problems.push({ + path, + message: `${drift.marker}이(가) ${drift.found ?? '없음'} — package.json은 ${version}`, + }); + } + } + + const names = (await readdir(join(root, 'docs'))).filter((name) => name.endsWith('.html')).sort(); + for (const name of names) { + const path = `docs/${name}`; + if (registered.has(path)) continue; + const kind = classifyDocument(path, await readFile(join(root, path), 'utf8')); + if (kind === 'current' && !excluded.has(path)) { + problems.push({ path, message: '현행 버전을 표방하지만 검사 대상에 없다 — documents 또는 exclusions에 등록하라' }); + } else if (kind !== 'current' && excluded.has(path)) { + problems.push({ path, message: `exclusions에 있지만 분류가 ${kind}다 — 낡은 제외 항목을 지워라` }); + } + } + return problems; +} diff --git a/scripts/generate-harness-overview.mjs b/scripts/generate-harness-overview.mjs index 233c0d8e..1372e1a9 100644 --- a/scripts/generate-harness-overview.mjs +++ b/scripts/generate-harness-overview.mjs @@ -4,6 +4,8 @@ import { basename, dirname, join, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; +import { checkDocsVersionDrift } from './docs-version-drift.mjs'; + const repositoryRoot = dirname(dirname(fileURLToPath(import.meta.url))); const templatePath = 'docs/harness-overview.template.html'; const outputPath = 'docs/harness-overview.html'; @@ -135,6 +137,7 @@ function fileRole(path, commandDescriptions) { if (path.endsWith('.mmd')) return 'harness overview 아키텍처 다이어그램 원본'; if (path === templatePath) return 'harness overview HTML 템플릿'; if (path === 'scripts/generate-harness-overview.mjs') return '다이어그램과 인벤토리를 한 번에 생성'; + if (path === 'scripts/docs-version-drift.mjs') return '현행 문서의 버전 표지를 package.json과 대조 (docs:check)'; return '프로젝트 소스 파일'; } @@ -212,6 +215,13 @@ async function main() { process.exitCode = 1; return; } + const drift = await checkDocsVersionDrift(repositoryRoot); + if (drift.length > 0) { + for (const { path, message } of drift) console.error(`${path}: ${message}`); + console.error('현행 문서의 버전 표지가 package.json과 다릅니다. scripts/docs-version-drift.mjs 머리 주석을 보세요.'); + process.exitCode = 1; + return; + } console.log('harness overview 생성 상태가 최신입니다.'); return; } diff --git a/tests/docs-version-drift.test.mjs b/tests/docs-version-drift.test.mjs new file mode 100644 index 00000000..2f39ab42 --- /dev/null +++ b/tests/docs-version-drift.test.mjs @@ -0,0 +1,93 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import test from 'node:test'; + +import { + checkDocsVersionDrift, + classifyDocument, + findMarkerDrift, + overviewMarkers, +} from '../scripts/docs-version-drift.mjs'; + +const root = dirname(dirname(fileURLToPath(import.meta.url))); + +function overviewFixture({ hero = '1.2.3', banner = '1.2.3', footer = '1.2.3' } = {}) { + return [ + `<div class="hero-tags"><span class="tag tag-purple">v${hero}</span></div>`, + `<strong>🆕 ${banner} — 무엇:</strong>`, + '<strong>🆕 0.9.0 — 옛 배너:</strong>', + `<footer>harness-aijient-team-plugin v${footer} (AGENTS.md SSOT)</footer>`, + ].join('\n'); +} + +test('current-version markers pass when every marker equals the package version', () => { + assert.deepEqual(findMarkerDrift(overviewFixture(), overviewMarkers, '1.2.3'), []); +}); + +test('a stale footer is reported with the version it still shows', () => { + assert.deepEqual( + findMarkerDrift(overviewFixture({ footer: '1.1.0' }), overviewMarkers, '1.2.3'), + [{ marker: 'footer', found: '1.1.0' }], + ); +}); + +test('a removed marker fails instead of silently disabling the guard', () => { + const html = overviewFixture().replace(/<footer>.*<\/footer>/, ''); + assert.deepEqual(findMarkerDrift(html, overviewMarkers, '1.2.3'), [{ marker: 'footer', found: null }]); +}); + +test('a current-looking marker inside an HTML comment does not mask a stale one', () => { + const html = `<!-- <span class="tag tag-purple">v1.2.3</span> -->\n${overviewFixture({ hero: '1.1.0' })}`; + assert.deepEqual(findMarkerDrift(html, overviewMarkers, '1.2.3'), [{ marker: 'hero badge', found: '1.1.0' }]); +}); + +test('a grouped banner is judged by its upper bound', () => { + assert.deepEqual(findMarkerDrift(overviewFixture({ banner: '1.0.0–1.2.3' }), overviewMarkers, '1.2.3'), []); + assert.deepEqual( + findMarkerDrift(overviewFixture({ banner: '1.0.0–1.2.2' }), overviewMarkers, '1.2.3'), + [{ marker: 'latest banner', found: '1.2.2' }], + ); +}); + +test('classification separates snapshots, baseline-labelled, current and unversioned documents', () => { + const bareHero = '<span class="tag tag-purple">v1.2.3</span>'; + assert.equal(classifyDocument('docs/x-1.2.3.html', bareHero), 'snapshot'); + assert.equal(classifyDocument('docs/x.html', bareHero), 'current'); + assert.equal(classifyDocument('docs/x.html', '<footer>plugin v1.2.3 · 소스에서 생성됨</footer>'), 'current'); + assert.equal(classifyDocument('docs/x.html', '<span aria-label="version" class="tag tag-purple">v1.2.3</span>'), 'current'); + // 한 표면이라도 "기준"을 달면 문서 전체가 기준 버전을 선언한 것이다 — 다른 표면의 맨 버전은 그 기준의 반복이다. + assert.equal( + classifyDocument('docs/x.html', '<span class="tag tag-purple">1.0.0 기준</span><footer>plugin · 1.0.0 · 가이드</footer>'), + 'baseline', + ); + assert.equal(classifyDocument('docs/x.html', '<div class="footer"><span>V1.0.0 기준</span></div>'), 'baseline'); + assert.equal(classifyDocument('docs/x.html', '<title>Harness 1.0.0 — 가이드
더 읽을 것
'), 'unversioned'); +}); + +test('an unregistered current document and a stale exclusion both fail the check', async () => { + const fixture = await mkdtemp(join(tmpdir(), 'docs-version-drift-')); + try { + await mkdir(join(fixture, 'docs')); + await writeFile(join(fixture, 'package.json'), JSON.stringify({ version: '1.2.3' })); + await writeFile(join(fixture, 'docs/registered.html'), overviewFixture()); + await writeFile(join(fixture, 'docs/new-page.html'), 'v1.2.3'); + await writeFile(join(fixture, 'docs/refreshed.html'), '
가이드
'); + await writeFile(join(fixture, 'docs/new-page-1.0.0.html'), 'v1.0.0'); + + const problems = await checkDocsVersionDrift(fixture, { + documents: [{ path: 'docs/registered.html', markers: overviewMarkers }], + exclusions: [{ path: 'docs/refreshed.html', reason: '후속 task' }], + }); + + assert.deepEqual(problems.map((problem) => problem.path).sort(), ['docs/new-page.html', 'docs/refreshed.html']); + } finally { + await rm(fixture, { recursive: true, force: true }); + } +}); + +test('the repository has no current-version drift in its docs', async () => { + assert.deepEqual(await checkDocsVersionDrift(root), []); +});