Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/rust.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
24 changes: 24 additions & 0 deletions AGENTS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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-<slug>.md` (the human axis) and
`YYYY-MM-DD-<slug>.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 `<basename>#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).
13 changes: 13 additions & 0 deletions REPORT.md
Original file line numberDiff line numberDiff line change
@@ -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.**
Expand Down
3 changes: 3 additions & 0 deletions STATE.md
Original file line numberDiff line numberDiff line change
@@ -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 대기 — 게이트③ 미착지.**
Expand Down
40 changes: 40 additions & 0 deletions docs/worklog/2026-08-26-worklog-json-proposals-convention.json
Original file line numberDiff line numberDiff line change
@@ -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": []
}
30 changes: 30 additions & 0 deletions docs/worklog/2026-08-26-worklog-json-proposals-convention.md
Original file line numberDiff line numberDiff line change
@@ -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줄 추가할지 결정.
78 changes: 78 additions & 0 deletions scripts/check-worklog-json.py
Original file line numberDiff line numberDiff line change
@@ -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 `<basename>#p<index>` 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)