From a38ecfbdf5f5c8be15db7e55fa712fa72a7459fa Mon Sep 17 00:00:00 2001 From: jun0 Date: Wed, 26 Aug 2026 09:54:45 +0900 Subject: [PATCH] [rustjava-worklog-json-proposals-convention] docs: adopt worklog .md+.json convention with proposals --- .github/workflows/rust.yml | 7 ++ AGENTS.md | 24 ++++++ REPORT.md | 13 ++++ STATE.md | 3 + ...-26-worklog-json-proposals-convention.json | 40 ++++++++++ ...08-26-worklog-json-proposals-convention.md | 30 +++++++ scripts/check-worklog-json.py | 78 +++++++++++++++++++ 7 files changed, 195 insertions(+) create mode 100644 docs/worklog/2026-08-26-worklog-json-proposals-convention.json create mode 100644 docs/worklog/2026-08-26-worklog-json-proposals-convention.md create mode 100755 scripts/check-worklog-json.py diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7d49d20f..c6d7371f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -54,3 +54,10 @@ jobs: # test_utils requires tokio rt-multi-thread, which does not compile on wasm - run: cargo clippy --workspace --exclude test_utils --target wasm32-unknown-unknown -- -D warnings - run: cargo test --all + + # docs-only lock (AGENTS.md "Round Worklog") — one runner, not the 6-cell matrix + worklog_json: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - run: python3 scripts/check-worklog-json.py diff --git a/AGENTS.md b/AGENTS.md index 7610e0f4..ec149106 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,3 +26,27 @@ - `classfile/` - Class file parser - `java_class_proto/` - Java class prototypes - `test_utils/` - Shared test utilities + +## Round Worklog `docs/worklog/` — human `.md` + machine `.json`, always a pair +When a round leaves follow-up proposals or their disposition, drop **two files with the same +basename** in `docs/worklog/`: `YYYY-MM-DD-.md` (the human axis) and +`YYYY-MM-DD-.json` (the machine axis). Without the `.json`, the proposal is +**structurally unreachable** by the cockpit "후속 작업 추천" panel — its scanner reads `.json` only. + +**Do not invent a schema** — these key names are shared with otterpebble/dodu/qts. The consumer +(`/api/proposals`, `scanRepoSimple`) reads exactly these: + +| key | type | what the consumer does with it | +|---|---|---| +| `date` | `"YYYY-MM-DD"` | sort axis (falls back to the filename's first 10 chars — set it anyway) | +| `proposals[]` | array of objects | one element = one card. `ref` is derived as `#p<0-based index>` | +| `proposals[].title` `plainSummary` `userBenefit` `why` `tradeoff` `effort` `target` | string | card body — fill **all 7**; an empty string renders as an empty field | +| `adoptedProposals[]` · `declinedProposals[]` | string (`ref`) array | removes that `ref` from the open recommendations (disposition record) | + +Any other key (`schema`, `taskId`, `summary`, `changes`, `verification`, `issues`, …) is free — +the consumer does not read them, so they are for humans and the next round. + +**No retroactive conversion.** The convention applies to new rounds only; the lock asks only +"if a `.json` exists, is it well-formed and does it have its `.md` sibling" — it never demands a +`.json` for an existing `.md`. Lock: `scripts/check-worklog-json.py`, run by the `worklog_json` +CI job (`cargo test` does not cover docs). diff --git a/REPORT.md b/REPORT.md index 5d96c2af..993d491c 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,5 +1,18 @@ # REPORT +## [2026-08-26] 회차 워크로그 `.json` + `proposals` 규약 이식 (rustjava-worklog-json-proposals-convention) +- 무엇을: `AGENTS.md` 에 「Round Worklog `docs/worklog/`」 절(소비처가 읽는 키 표 · 소급 없음), + `scripts/check-worklog-json.py` 잠금 6축, `rust.yml` 에 `worklog_json` job 1개(ubuntu 단일 러너), + `docs/worklog/` 개시 + 이 회차 `.md`/`.json` 한 쌍. **Rust 코드 변경 0.** +- 왜: cockpit 「후속 작업 추천」 커버리지 6 repo 중 채워진 것이 2개뿐이고 RustJava 는 + `docs/worklog` **디렉터리 자체가 없어**(2026-08-26 재실측 `archiveErr: pathspec … did not match`) + 구조적으로 0건이었다. qts 회차가 만든 규약을 **복제**했다 — 새 스키마·새 기구 0. +- 사용자 영향: 착지 후 RustJava 의 후속 제안이 cockpit 화면에 처음 뜬다(이 회차 2건). + 회차마다 워크로그 2파일 작성 부담이 는다. +- 후속 추천: ★**규약을 심었다 ≠ 카드가 계속 는다.** 이 repo 회차 기록 정본은 `REPORT.md` 라 + 워크로그 작성이 DoD 에 없다 — 의무화 여부는 미결(짝 `.json` 의 `proposals[0]`). + 잠금이 `cargo test` 밖 CI job 이라 로컬 DoD 3명령으로는 안 돈다(`proposals[1]`). + ## [2026-08-25] beta clippy `double_must_use` red 해소 (rustjava-ci-beta-clippy-double-must-use-red) - 무엇을: `Cargo.lock` 의 `async-trait` 0.1.89→**0.1.92**, `#[async_recursion]` **7지점**에 국소 `#[allow(clippy::double_must_use)]`, `rust.yml` matrix 에 `fail-fast: false` 1줄. **기능 변경 0.** diff --git a/STATE.md b/STATE.md index 7968e2d5..0893c274 100644 --- a/STATE.md +++ b/STATE.md @@ -1,6 +1,9 @@ # STATE ## 진행중 +- [rustjava-worklog-json-proposals-convention] 회차 워크로그 `docs/worklog/` `.md`+`.json` 한 쌍 규약 + 이식(qts 2026-08-23 규약 복제 · 스키마 발명 0) — `AGENTS.md` 절 + `scripts/check-worklog-json.py` + 잠금 + `rust.yml` `worklog_json` job. **PR 대기 — 게이트③ 미착지.** - [rustjava-ci-beta-clippy-double-must-use-red] beta clippy `double_must_use` 13건 red 해소 — `async-trait` 0.1.89→0.1.92 + `async_recursion` 7지점 국소 `allow` + matrix `fail-fast: false`. **PR 대기 — 게이트③ 미착지.** diff --git a/docs/worklog/2026-08-26-worklog-json-proposals-convention.json b/docs/worklog/2026-08-26-worklog-json-proposals-convention.json new file mode 100644 index 00000000..b1fbbd4d --- /dev/null +++ b/docs/worklog/2026-08-26-worklog-json-proposals-convention.json @@ -0,0 +1,40 @@ +{ + "schema": 1, + "date": "2026-08-26", + "title": "회차 워크로그 `.json` + `proposals` 규약 이식 — RustJava 의 cockpit 「후속 작업 추천」 구조적 ENOENT 해소", + "services": ["RustJava"], + "taskId": "rustjava-worklog-json-proposals-convention", + "summary": "cockpit `/api/proposals` 의 `derived.coverage` 에서 RustJava 만 `docs/worklog` 자체가 ENOENT 였다(2026-08-26 재실측: `archiveErr: pathspec 'docs/worklog' did not match any files`). qts 회차(2026-08-23)가 만든 규약을 스키마 발명 0 으로 복제해 AGENTS.md 에 「Round Worklog」 절을 세우고, `docs/worklog/` 를 이 회차 자신의 `.md`+`.json` 한 쌍으로 개시했다. 잠금은 qts 의 6축을 그대로 옮기되 pytest 가 없는 repo 라 `scripts/check-worklog-json.py` + CI 단일 job 으로 구현했다. 과거 회차 소급 0.", + "changes": [ + "`AGENTS.md` — 「Round Worklog `docs/worklog/`」 절 신설. 소비처가 읽는 키 표(`date` · `proposals[]` 7키 · `adoptedProposals`/`declinedProposals`) + 그 밖의 키는 자유 + 소급 없음 명시. ★`CLAUDE.md` 가 `@AGENTS.md` 로 참조하므로 이 자리가 매 세션 상시 로드된다.", + "`scripts/check-worklog-json.py` 신설 — qts `tests/guardrail/test_worklog_json_schema.py` 와 같은 6축(파싱·짝 `.md` 실재·`date`↔파일명 일치·`proposals[]` 7키 전수·처분 ref 형식·AGENTS.md 문구 잔존).", + "`.github/workflows/rust.yml` — `worklog_json` job 1개 추가(ubuntu 단일 러너). 6셀 rust 매트릭스에 붙이지 않았다 — 문서 검사를 6번 돌릴 이유가 없다.", + "`docs/worklog/2026-08-26-worklog-json-proposals-convention.{md,json}` — 규약의 자기증명 1쌍 겸 `docs/worklog/` 개시." + ], + "deploy": { "sha": "", "urls": [] }, + "verification": "기준선 실측(2026-08-26, 로컬 cockpit `GET :4477/api/proposals`): RustJava = `{json:0, md:0, proposals:0, archiveErr: \"fatal: pathspec 'docs/worklog' did not match any files\"}` ⇒ 08-23 실측(`ENOENT`)이 지금도 참이고 처방은 «폴더 신설»이 맞다(wie 의 `json:1 · proposals:0` 과 다른 처방). ★착지 전 수치는 바뀌지 않는다 — `worklogDirOf` 가 `git archive origin/main` 으로 읽으므로 머지 전 `0` 이 정상이다. 착지 후 기대값 = `json:1 · md:1 · proposals:2`(캐시 60초). 잠금 자체는 돌연변이 대조쌍으로 양방향 확인했다(제품 호출부 개악 → red, 원복 → green).", + "issues": [], + "proposals": [ + { + "title": "RustJava 회차에 워크로그 작성을 «의무»로 할지 결정 — 규약만으로는 카드가 늘지 않는다", + "plainSummary": "이번에 만든 건 «워크로그를 쓸 때 형식을 맞추는» 규약이다. 아무도 쓰지 않으면 화면의 추천 수는 0 그대로다.", + "userBenefit": "RustJava 회차가 남긴 후속 제안이 실제로 화면에 모인다 — 지금은 «쓰기로 한 회차»만 모이고 그 빈도가 정해져 있지 않다.", + "why": "이 repo 의 회차 기록 정본은 `REPORT.md` 상단 append(`CLAUDE.md` §Definition of Done)이고, 워크로그는 오늘 처음 생겼다. ⇒ DoD 가 요구하는 것은 `REPORT.md` 이지 `docs/worklog/` 가 아니므로, 규약은 서 있어도 «흘러 들어오는 양»은 0 에 가까울 수 있다. otterpebble 은 1,163건으로 의무화 쪽을 골랐고 qts 는 12건으로 그러지 않았다 — RustJava 가 어느 쪽인지는 아직 아무도 정하지 않았다.", + "tradeoff": "⑴DoD 에 넣으면 매 회차 파일 2개가 늘어 정직한 부담이 는다. ⑵`REPORT.md` 를 파싱해 자동 생성하면 그것이 이 리니지가 반복해 기각한 «파서 제2원본»이다. ⑶현상 유지면 규약은 서 있고 카드는 0 인 상태가 계속된다. 셋 다 공짜가 아니다.", + "effort": "S — ⑴은 `CLAUDE.md` DoD 1줄 + AGENTS.md 포인터. ⑵는 별건 설계.", + "target": "CLAUDE.md · AGENTS.md" + }, + { + "title": "잠금이 `cargo test` 밖에 있다 — 로컬 DoD 3명령으로는 안 돈다", + "plainSummary": "형식 검사는 CI 에서만 돈다. 로컬에서 평소 쓰는 검사 명령에는 안 걸려서, 틀린 파일을 올려야 비로소 빨개진다.", + "userBenefit": "워크로그 형식이 틀렸을 때 push 전에 알게 된다 — 지금은 PR 을 열어야 안다.", + "why": "`CLAUDE.md` DoD 는 `cargo fmt --check`·`cargo clippy`·`cargo test` 3개다. 워크로그 잠금을 그 안에 넣으려면 JSON 파서 의존성(`serde_json`)을 dev-dependency 로 들여야 하는데, 문서 검사 하나 때문에 워크스페이스 의존성을 늘리고 6셀 매트릭스의 빌드 시간을 태우는 값이 검사 값보다 크다고 보고 CI job 으로 갈랐다. 그 판단이 옳은지는 재고 여지가 있다.", + "tradeoff": "⑴`serde_json` dev-dep + Rust 테스트로 옮기면 `cargo test` 한 줄에 들어오지만 의존성·빌드시간이 는다. ⑵DoD 문안에 `python3 scripts/check-worklog-json.py` 를 4번째 명령으로 추가하면 공짜지만 «사람이 기억해야 하는 줄»이 하나 는다. ⑶현상 유지면 CI 가 유일한 그물이다(실제로 막기는 한다 — 머지 전에 red 다).", + "effort": "XS — ⑵면 1줄. ⑴이면 S.", + "target": "CLAUDE.md · scripts/check-worklog-json.py" + } + ], + "resolvedIssues": [], + "adoptedProposals": ["2026-08-23-worklog-json-with-proposals-convention#p2"], + "declinedProposals": [] +} diff --git a/docs/worklog/2026-08-26-worklog-json-proposals-convention.md b/docs/worklog/2026-08-26-worklog-json-proposals-convention.md new file mode 100644 index 00000000..9a99c342 --- /dev/null +++ b/docs/worklog/2026-08-26-worklog-json-proposals-convention.md @@ -0,0 +1,30 @@ +# 2026-08-26 · 회차 워크로그 `.json` + `proposals` 규약 이식 (RustJava) + +`taskId`: `rustjava-worklog-json-proposals-convention` · +채택 근거: `2026-08-23-worklog-json-with-proposals-convention#p2`(qts 회차 제안 · 운영자 cockpit 채택) + +## 무엇을 · 왜 +cockpit 「후속 작업 추천」 커버리지 6 repo 중 채워진 것이 2개(otterpebble·dodu)뿐이었고, +RustJava 는 그중에서도 `docs/worklog` **디렉터리 자체가 없어** 구조적으로 0건이었다. +qts 회차(2026-08-23)가 세운 규약을 **스키마 발명 0** 으로 복제해 이 repo 에 심었다. + +## 한 일 +- `AGENTS.md` 에 「Round Worklog `docs/worklog/`」 절 — `.md`(사람 축) + `.json`(기계 축) 한 쌍, + 소비처가 실제로 읽는 키 표, 그 밖의 키는 자유, **과거 소급 없음**. +- `scripts/check-worklog-json.py` — qts 의 guardrail 6축을 그대로 옮긴 잠금. +- `.github/workflows/rust.yml` 에 `worklog_json` job 1개(ubuntu 단일 러너). +- 이 파일과 짝 `.json` — 규약의 자기증명 겸 `docs/worklog/` 개시. + +## 사용자 영향 +착지 후 cockpit 「후속 작업 추천」에 RustJava 카드가 처음으로 뜬다(이 회차 제안 2건). +그 전까지 수치는 바뀌지 않는다 — 소비처가 `origin/main` 을 `git archive` 로 읽기 때문이다. + +## 한계 (정직한 대가) +- **규약을 심었다 ≠ 제안이 계속 뜬다.** 다음 회차들이 워크로그를 쓰지 않으면 수치는 + 이 회차 2건에서 멈춘다. 「의무화할 것인가」는 아래 제안 #0 이 진다. +- 회차마다 파일 2개를 더 쓰는 부담이 는다. +- 잠금이 `cargo test` 밖(CI job)에 있어 로컬 DoD 3명령으로는 돌지 않는다 — 제안 #1. + +## 후속 제안 +기계 축은 짝 `.json` 의 `proposals[]` 에 있다. 요약: ⑴RustJava 회차에 워크로그 의무화 여부 결정 +⑵잠금을 `cargo test` 안으로 들일지(=`serde_json` dev-dep) 아니면 DoD 문안에 1줄 추가할지 결정. diff --git a/scripts/check-worklog-json.py b/scripts/check-worklog-json.py new file mode 100755 index 00000000..6fcf0549 --- /dev/null +++ b/scripts/check-worklog-json.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Lock for the `docs/worklog/` machine-readable `.json` convention (AGENTS.md). + +Why this lock exists: a malformed `.json` silently produces zero cards in the cockpit +"후속 작업 추천" panel — no error, no warning, just nothing. This turns that silence red. +It checks only the keys the consumer (`/api/proposals`, `scanRepoSimple`) actually reads; +every other key is free. + +It never demands a `.json` for an existing `.md` — the convention is not retroactive. + +qts locks the same six axes in `tests/guardrail/test_worklog_json_schema.py`; this repo has no +pytest (and `cargo test` would need a JSON dependency to read docs), so it is a plain script. +""" + +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +WORKLOG = ROOT / "docs" / "worklog" +AGENTS = ROOT / "AGENTS.md" + +# keys scanRepoSimple reads out of a proposals[] element — all strings +PROPOSAL_KEYS = ("title", "plainSummary", "userBenefit", "why", "tradeoff", "effort", "target") +DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}") + +fail = [] + + +def check(cond, msg): + if not cond: + fail.append(msg) + + +for p in sorted(WORKLOG.glob("*.json")): + try: + obj = json.loads(p.read_text(encoding="utf-8")) + except json.JSONDecodeError as e: + fail.append(f"{p.name}: unparseable — the whole file is ignored by the consumer ({e})") + continue + if not isinstance(obj, dict): + fail.append(f"{p.name}: top level is not an object") + continue + + check(p.with_suffix(".md").exists(), f"{p.name}: no `.md` sibling — the `.json` is an addition, not a replacement") + check(DATE_RE.match(p.stem), f"{p.name}: filename does not start with YYYY-MM-DD") + check(obj.get("date") == p.stem[:10], f"{p.name}: date({obj.get('date')}) != filename date({p.stem[:10]}) — sorting would lie") + + proposals = obj.get("proposals", []) + if not isinstance(proposals, list): + fail.append(f"{p.name}: proposals is not an array") + proposals = [] + for i, prop in enumerate(proposals): + if not isinstance(prop, dict): + fail.append(f"{p.name}#p{i}: element is not an object (consumer skips it)") + continue + for k in PROPOSAL_KEYS: + v = prop.get(k) + check(isinstance(v, str) and v.strip(), f"{p.name}#p{i}: '{k}' missing — that field renders empty on the card") + + for key in ("adoptedProposals", "declinedProposals"): + refs = obj.get(key, []) + if not isinstance(refs, list): + fail.append(f"{p.name}: {key} is not an array") + continue + for r in refs: + check(isinstance(r, str) and "#p" in r, f"{p.name}: {key} element {r!r} — must be `#p` for the disposition to apply") + +agents = AGENTS.read_text(encoding="utf-8") +check("## Round Worklog" in agents, "AGENTS.md: the Round Worklog convention section is gone") +for k in ("adoptedProposals", "declinedProposals", "proposals[]"): + check(k in agents, f"AGENTS.md: the consumed key '{k}' is no longer documented") + +for m in fail: + print(f"WORKLOG-JSON: {m}", file=sys.stderr) +print(f"checked {len(list(WORKLOG.glob('*.json')))} worklog .json file(s), {len(fail)} problem(s)") +sys.exit(1 if fail else 0)