hypomnema 1.3.4 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/commands/feedback.md +4 -0
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CONTRIBUTING.md +56 -17
- package/hooks/hypo-auto-commit.mjs +6 -12
- package/hooks/hypo-session-record.mjs +5 -0
- package/hooks/hypo-session-start.mjs +7 -0
- package/hooks/hypo-shared.mjs +68 -1
- package/package.json +5 -2
- package/scripts/bump-version.mjs +20 -6
- package/scripts/check-readme-version.mjs +126 -0
- package/scripts/check-versions.mjs +171 -0
- package/scripts/crystallize.mjs +19 -32
- package/scripts/doctor.mjs +13 -4
- package/scripts/feedback.mjs +68 -1
- package/scripts/graph.mjs +5 -32
- package/scripts/init.mjs +2 -1
- package/scripts/lib/failure-type.mjs +33 -0
- package/scripts/lib/frontmatter.mjs +23 -2
- package/scripts/lib/schema-vocab.mjs +35 -0
- package/scripts/lib/template-schema-version.mjs +21 -0
- package/scripts/lib/wikilink.mjs +156 -0
- package/scripts/lint.mjs +86 -47
- package/scripts/rename.mjs +6 -30
- package/scripts/smoke-plugin.mjs +194 -0
- package/scripts/stats.mjs +22 -2
- package/scripts/upgrade.mjs +11 -3
- package/templates/SCHEMA.md +25 -1
- package/templates/hypo-config.md +1 -1
package/README.ko.md
CHANGED
|
@@ -23,7 +23,7 @@ _Claude에게 기록을 맡기세요 — 그리고 그 기록이 실제로 쌓
|
|
|
23
23
|
|
|
24
24
|
> **아래에서 자주 쓰이는 용어 간단 정리.** *프런트매터(frontmatter)* = 마크다운 파일 맨 위의 YAML 블록. *위키링크(wikilink)* = `[[페이지-슬러그]]` 형태의 교차 참조. *ADR* = "Architecture Decision Record" — 어떤 설계 결정을 *왜* 했는지 짧게 적은 마크다운 페이지. *projection*(투영) = 한 방향 자동 파생(`pages/feedback/*.md` → `MEMORY.md` / `<learned_behaviors>`). *훅(hook)* = Claude Code가 라이프사이클 이벤트에서 자동으로 실행하는 스크립트. *hot.md* / *session-state.md* = "방금 무엇을 했는지"와 "다음에 무엇을 할지"를 담는 프로젝트별 캐시 파일 — 멈춘 프로젝트를 한 번에 이어 받을 수 있게 합니다. 전체 용어 풀이는 [용어 사전](#용어-사전) 참조.
|
|
25
25
|
|
|
26
|
-
> **현재 자동화 범위와 다음 목표.** v1.
|
|
26
|
+
> **현재 자동화 범위와 다음 목표.** v1.4.0(현재 릴리스)는 트리거 모델을 솔직하게 정리합니다. 위키 작업(자료 정리·검색·세션 마무리)은 여전히 사용자가 `/hypo:*` 명령어를 직접 입력해 시작합니다. 다만 **v1.1.0**부터 위키가 한 세션에서 얼마나 활용됐는지를 측정하는 *관측성 지표(observability score)* 가 들어갔고, **v1.2.0**은 그 위에 사용자가 시키지 않아도 자동으로 동작하는 영역 4개를 추가했습니다:
|
|
27
27
|
> - **`feedback` 페이지를 단일 원천(source of truth)으로** — `pages/feedback/`에 한 번만 적으면, 위키가 `MEMORY.md`와 `~/.claude/CLAUDE.md`의 `<learned_behaviors>` 블록을 자동으로 갱신합니다.
|
|
28
28
|
> - **확장 파일 동봉 동기화** — 위키 안의 `~/hypomnema/extensions/{agents,commands,hooks,skills}/`에 둔 파일을 자동으로 `~/.claude/`에 반영합니다. `--codex` 옵션을 추가하면 `hooks`·`commands`는 `~/.codex/`에도 반영되지만, `agents`와 `skills`는 Claude 전용이라 의도적으로 건너뜁니다.
|
|
29
29
|
> - **프로젝트 자동 생성** — 작업 디렉터리를 git 저장소(`package.json`·`Cargo.toml` 등의 프로젝트 표식이 있는 곳)로 옮겼을 때 대응하는 위키 프로젝트가 없으면, 새로 만들지 물어봅니다.
|
|
@@ -250,7 +250,7 @@ v1.0에서는 `personal / shared / public` 3-mode를 만들었습니다. 현실
|
|
|
250
250
|
|
|
251
251
|
이와 별도로 `SessionStart` 훅은 npm 레지스트리와 Claude Code 플러그인 마켓플레이스를 백그라운드에서 확인합니다(세션 시작을 막지 않습니다). 새 버전이 게시되어 있으면 다음 세션 시작 시 "Update available!" 안내가 한 줄 표시됩니다. `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`을 지정하거나 `CI=true` 환경에서 실행하면 점검을 건너뜁니다.
|
|
252
252
|
|
|
253
|
-
위 네 가지 레인 외의 v1.3 세부 수정(세션 마무리 lint를 건드린 파일로 스코프해 무관한 debt로 `/compact`가 막히지 않게 한 변경, `feedback` scope 검증기가 cwd 유래 project id를 수용하게 한 수정, `--strict`가 에러로 승격하는 안정적 lint 경고 ID `W1`/`W2`/`W4`이며 `--fix`로 자동복구되는 `W3`는 경고로 유지)은 [`CHANGELOG.md`](CHANGELOG.md)를 참고하세요. **v1.3.1**은 수정 전용 패치입니다: 업데이트 notifier 배너가 이제 보이지 않던 stderr 출력 대신 top-level `systemMessage` 채널로 실제 사용자에게 도달하고, `/hypo:upgrade`가 플러그인·dual(수동+플러그인) 설치에서 core 훅을 중복 등록하지 않으며, 세션 마무리가 두 프로젝트가 같은 최신 날짜를 가질 때 완료된 close를 false-block 하지 않습니다. **v1.3.2**는 비-프로젝트(툴링·위키 전용) 세션을 무관한 프로젝트에 엮지 않고 닫는 1급 log-only close 경로와, 페이지 이름을 바꿀 때 해당하는 인바운드 위키링크를 갱신하는 `rename` 헬퍼를 추가합니다(모호하거나 append-only인 참조는 갱신하지 않고 보고합니다). 또한 세션 마무리 게이트(오늘 활동한 모든 프로젝트를 게이트, 세션별 마커와 `/compact`가 하나의 게이트를 공유, 도출 가능한 루트 `log.md` 항목 자동 도출)와 linter(`--json`이 파이프에서 잘리지 않음, 볼트 관습 위키링크를 오탐 대신 정상 해석)를 수정합니다. **v1.3.3**은 세션 마무리 게이트를 실수 발동에 대해 단단히 합니다: 모델이 실제 사용자 종료 신호 없이 세션을 닫을 수 없고, 종료 관련 텍스트를 읽는 것이 턴을 false-block하지 않으며, `--apply-session-close`가 사용자 종료 신호가 있는 close에서 payload 커밋과 마커 기록을 한 번에 끝내고, 일상적 트래커 bookkeeping이 무관한 프로젝트의 `/compact`를 cross-block하지 않습니다. 또한 `rename`을 디렉터리 서브트리 전체 이동으로 확장합니다(`/hypo:rename` 커맨드와 대상 경로가 이미 있을 때의 merge/renumber 충돌 리포트 포함). **v1.3.4**는 위키 위생 수정 전용 패치입니다: `init`이 손수 쓴 알려진 등가본 옆에 중복 stock 페이지를 떨구지 않고(`wiki-automation.md`나 `wiki-help.md`가 보이면 그 페이지를 유지하고 stock `hypo-*`를 건너뛰며 머지 경고를 크게 냅니다), 볼트 루트의 재생성 가능한 리포트 산출물(업그레이드 `MIGRATION-v*.md` 리포트, 예약된 `GRAPH_REPORT.md` 이름 포함)을 정상 커밋되게 두면서 카탈로그 스캔에서 제외합니다.
|
|
253
|
+
위 네 가지 레인 외의 v1.3 세부 수정(세션 마무리 lint를 건드린 파일로 스코프해 무관한 debt로 `/compact`가 막히지 않게 한 변경, `feedback` scope 검증기가 cwd 유래 project id를 수용하게 한 수정, `--strict`가 에러로 승격하는 안정적 lint 경고 ID `W1`/`W2`/`W4`이며 `--fix`로 자동복구되는 `W3`는 경고로 유지)은 [`CHANGELOG.md`](CHANGELOG.md)를 참고하세요. **v1.3.1**은 수정 전용 패치입니다: 업데이트 notifier 배너가 이제 보이지 않던 stderr 출력 대신 top-level `systemMessage` 채널로 실제 사용자에게 도달하고, `/hypo:upgrade`가 플러그인·dual(수동+플러그인) 설치에서 core 훅을 중복 등록하지 않으며, 세션 마무리가 두 프로젝트가 같은 최신 날짜를 가질 때 완료된 close를 false-block 하지 않습니다. **v1.3.2**는 비-프로젝트(툴링·위키 전용) 세션을 무관한 프로젝트에 엮지 않고 닫는 1급 log-only close 경로와, 페이지 이름을 바꿀 때 해당하는 인바운드 위키링크를 갱신하는 `rename` 헬퍼를 추가합니다(모호하거나 append-only인 참조는 갱신하지 않고 보고합니다). 또한 세션 마무리 게이트(오늘 활동한 모든 프로젝트를 게이트, 세션별 마커와 `/compact`가 하나의 게이트를 공유, 도출 가능한 루트 `log.md` 항목 자동 도출)와 linter(`--json`이 파이프에서 잘리지 않음, 볼트 관습 위키링크를 오탐 대신 정상 해석)를 수정합니다. **v1.3.3**은 세션 마무리 게이트를 실수 발동에 대해 단단히 합니다: 모델이 실제 사용자 종료 신호 없이 세션을 닫을 수 없고, 종료 관련 텍스트를 읽는 것이 턴을 false-block하지 않으며, `--apply-session-close`가 사용자 종료 신호가 있는 close에서 payload 커밋과 마커 기록을 한 번에 끝내고, 일상적 트래커 bookkeeping이 무관한 프로젝트의 `/compact`를 cross-block하지 않습니다. 또한 `rename`을 디렉터리 서브트리 전체 이동으로 확장합니다(`/hypo:rename` 커맨드와 대상 경로가 이미 있을 때의 merge/renumber 충돌 리포트 포함). **v1.3.4**는 위키 위생 수정 전용 패치입니다: `init`이 손수 쓴 알려진 등가본 옆에 중복 stock 페이지를 떨구지 않고(`wiki-automation.md`나 `wiki-help.md`가 보이면 그 페이지를 유지하고 stock `hypo-*`를 건너뛰며 머지 경고를 크게 냅니다), 볼트 루트의 재생성 가능한 리포트 산출물(업그레이드 `MIGRATION-v*.md` 리포트, 예약된 `GRAPH_REPORT.md` 이름 포함)을 정상 커밋되게 두면서 카탈로그 스캔에서 제외합니다. **v1.4.0**은 피드백 페이지에 선택적 `failure_type` 분류(여덟 값 중 하나, `hypomnema stats`가 유형별 개수로 노출)를 추가해 반복되는 실수 유형이 보이게 합니다. `SCHEMA`는 이 필드를 안 쓰는 페이지는 건드리지 않는 추가형 선택 필드로 2.1이 됩니다(이미 자유서술 `failure_type`을 손으로 적어 둔 페이지는 여덟 값 중 하나로 다시 분류해야 합니다). 또한 auto-commit이 머지 충돌을 만나면 절반만 머지된 볼트를 남기지 않고 깔끔히 abort하며, `lint`이 invalid YAML frontmatter(따옴표 없는 `: ` 값, 중복 top-level 키)를 조용히 통과시키지 않고, 중첩 매핑 키가 페이지 `type`을 가리지 못하게 합니다. 내부적으로는 릴리스 파이프라인이 Claude Code 플러그인 채널까지 게이트하고(버전 일치, 네트워크 없는 플러그인 smoke, README 버전 floor), 공유 `lib/wikilink.mjs` resolver가 `lint`·`rename`·`graph`·`crystallize`의 페이지 순회 로직을 통합합니다.
|
|
254
254
|
|
|
255
255
|
### 셋업 & 유지보수
|
|
256
256
|
|
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ _Make Claude take notes — and measure whether it actually does._
|
|
|
23
23
|
|
|
24
24
|
> **Quick decoder for terms used below.** *frontmatter* = the YAML block at the top of a markdown file; *wikilink* = a `[[page-slug]]` cross-reference; *ADR* = "Architecture Decision Record", a short markdown page that records *why* a design choice was made; *projection* = a one-way derive (`pages/feedback/*.md` → `MEMORY.md` / `<learned_behaviors>`); *hook* = a script that Claude Code runs automatically on lifecycle events; *hot.md* / *session-state.md* = the per-project cache files that hold "what just happened" and "what's next" so a paused project resumes in one read. Full glossary lives under [Term decoder](#term-decoder).
|
|
25
25
|
|
|
26
|
-
> **Current state vs. v2 vision.** v1.
|
|
26
|
+
> **Current state vs. v2 vision.** v1.4.0 (current release) is honest about its trigger model: most wiki behavior — ingest, query, session-close — still fires on **explicit `/hypo:*` commands**, but the auto-behavior surface is growing. The v2 thesis is *fully autonomous* — Claude reading, writing, and synthesizing the wiki without being asked. **v1.1.0** shipped the **observability score** that measures how often the wiki is actually used per session (ingest / query / session-close / citation rates). **v1.2.0** adds four load-bearing autonomous lanes on top:
|
|
27
27
|
> - **feedback-as-SoT with one-way projections** — `pages/feedback/` becomes the single source-of-truth (SoT) for behavior corrections; the wiki one-way derives `MEMORY.md` and the `<learned_behaviors>` block inside `~/.claude/CLAUDE.md`, so you edit one place and the projections refresh on their own.
|
|
28
28
|
> - **extensions companion sync** — anything you drop under `~/hypomnema/extensions/{agents,commands,hooks,skills}/` is mirrored into `~/.claude/` automatically; the optional `--codex` flag additionally mirrors `hooks` and `commands` into `~/.codex/` (agents/skills are Claude-only and skipped on the Codex target by design).
|
|
29
29
|
> - **auto-project creation on cwd match** — when you `cd` into a git repo with a project marker (`package.json`, `Cargo.toml`, etc.) and no matching wiki project exists, Hypomnema offers to scaffold one for you.
|
|
@@ -250,7 +250,7 @@ All hooks resolve the wiki root via `HYPO_DIR` env → `hypo-config.md` scan →
|
|
|
250
250
|
|
|
251
251
|
Additionally, the `SessionStart` hook performs a non-blocking background check against npm and the Claude Code plugin marketplace and prints an "Update available!" banner the next time a newer Hypomnema version has been published. Opt out with `HYPO_NO_UPDATE_CHECK=1`, `NO_UPDATE_NOTIFIER=1`, or by running under `CI=true`.
|
|
252
252
|
|
|
253
|
-
For fix-level v1.3 detail beyond the lanes above (session-close lint scoped to touched files so `/compact` is no longer blocked by unrelated debt, the `feedback` scope validator accepting cwd-derived project ids, and the stable lint warning IDs `W1`/`W2`/`W4` that `--strict` promotes to errors while `W3`, auto-repaired by `--fix`, stays a warning), see [`CHANGELOG.md`](CHANGELOG.md). **v1.3.1** is a fixes-only patch: update-notifier banners now actually reach the user via the top-level `systemMessage` channel instead of an invisible stderr write, `/hypo:upgrade` no longer double-registers core hooks for plugin or dual (manual + plugin) installs, and session-close no longer false-blocks a completed close when two projects share the latest date. **v1.3.2** adds a first-class log-only session-close path, so a non-project (tooling or wiki-only) session closes cleanly without being forced onto an unrelated project, plus a `rename` helper that rewrites the eligible inbound wikilinks when you rename a page (ambiguous or append-only references are reported, not rewritten). It also tightens the session-close gate (every project with activity today is gated, the per-session marker and `/compact` share one gate, and the derivable root `log.md` entry auto-derives) and the linter (`--json` no longer truncates on a pipe, and vault-convention wikilinks resolve instead of being false-flagged). **v1.3.3** hardens the session-close gate against accidental triggers: the model can no longer mark a session closed without a real user close signal, reading close-related text no longer false-blocks the turn, `--apply-session-close` commits its payload and writes the marker in one step on a close that carries a user close signal, and routine tracker bookkeeping no longer cross-blocks an unrelated project's `/compact`. It also extends `rename` to move a whole directory subtree (with a `/hypo:rename` command and a merge/renumber collision report when a destination already exists). **v1.3.4** is a fixes-only patch for wiki hygiene: `init` no longer drops a duplicate stock page next to a known hand-authored equivalent (when it sees `wiki-automation.md` or `wiki-help.md` it keeps that page and skips the stock `hypo-*` one, with a loud merge notice), and regenerable report output at the vault root (the upgrade `MIGRATION-v*.md` report, plus the reserved `GRAPH_REPORT.md` name) is excluded from the catalog scans while staying fully committable.
|
|
253
|
+
For fix-level v1.3 detail beyond the lanes above (session-close lint scoped to touched files so `/compact` is no longer blocked by unrelated debt, the `feedback` scope validator accepting cwd-derived project ids, and the stable lint warning IDs `W1`/`W2`/`W4` that `--strict` promotes to errors while `W3`, auto-repaired by `--fix`, stays a warning), see [`CHANGELOG.md`](CHANGELOG.md). **v1.3.1** is a fixes-only patch: update-notifier banners now actually reach the user via the top-level `systemMessage` channel instead of an invisible stderr write, `/hypo:upgrade` no longer double-registers core hooks for plugin or dual (manual + plugin) installs, and session-close no longer false-blocks a completed close when two projects share the latest date. **v1.3.2** adds a first-class log-only session-close path, so a non-project (tooling or wiki-only) session closes cleanly without being forced onto an unrelated project, plus a `rename` helper that rewrites the eligible inbound wikilinks when you rename a page (ambiguous or append-only references are reported, not rewritten). It also tightens the session-close gate (every project with activity today is gated, the per-session marker and `/compact` share one gate, and the derivable root `log.md` entry auto-derives) and the linter (`--json` no longer truncates on a pipe, and vault-convention wikilinks resolve instead of being false-flagged). **v1.3.3** hardens the session-close gate against accidental triggers: the model can no longer mark a session closed without a real user close signal, reading close-related text no longer false-blocks the turn, `--apply-session-close` commits its payload and writes the marker in one step on a close that carries a user close signal, and routine tracker bookkeeping no longer cross-blocks an unrelated project's `/compact`. It also extends `rename` to move a whole directory subtree (with a `/hypo:rename` command and a merge/renumber collision report when a destination already exists). **v1.3.4** is a fixes-only patch for wiki hygiene: `init` no longer drops a duplicate stock page next to a known hand-authored equivalent (when it sees `wiki-automation.md` or `wiki-help.md` it keeps that page and skips the stock `hypo-*` one, with a loud merge notice), and regenerable report output at the vault root (the upgrade `MIGRATION-v*.md` report, plus the reserved `GRAPH_REPORT.md` name) is excluded from the catalog scans while staying fully committable. **v1.4.0** adds an optional `failure_type` classification to feedback pages (one of eight values, surfaced as per-type counts by `hypomnema stats`) so recurring mistake types become visible — the `SCHEMA` moves to 2.1 as an additive, optional field that leaves any page not using it untouched (a page that already carried a hand-written free-text `failure_type` must be relabeled to one of the eight values). It also makes an auto-commit that hits a merge conflict abort cleanly instead of leaving a half-merged vault, stops `lint` from silently green-passing invalid YAML frontmatter (an unquoted `: ` value or a duplicate top-level key), and prevents a nested mapping key from shadowing the page `type`. Under the hood the release pipeline now also gates the Claude Code plugin channel (version consistency, a network-free plugin smoke check, and a README-version floor) and a shared `lib/wikilink.mjs` resolver consolidates the page-walking logic across `lint`, `rename`, `graph`, and `crystallize`.
|
|
254
254
|
|
|
255
255
|
### Setup & maintenance
|
|
256
256
|
|
package/commands/feedback.md
CHANGED
|
@@ -26,6 +26,7 @@ If the user did not provide them, ask. The classification fields are required so
|
|
|
26
26
|
6. **Targets**: "Where should this project?" → `project-memory` (MEMORY.md) and/or `claude-learned` (global CLAUDE.md). Default `project-memory`.
|
|
27
27
|
7. **Priority** (1–5, higher sorts first; default 3).
|
|
28
28
|
8. **Sensitivity**: `public` (default) or `sanitized` (redacted secrets/paths). `private` is not allowed — the wiki is git-pushed.
|
|
29
|
+
9. **Failure type** (optional): if this correction came from a real failure incident, classify it — `hallucination` | `false-completion` | `process-stall` | `over-caution` | `overreach` | `incompleteness` | `instruction-miss` | `convention-violation`. Omit it for a pure preference or a brand-new convention ("always do X"). When several fit, take the most specific (the list is in precedence order; see SCHEMA §3.1).
|
|
29
30
|
|
|
30
31
|
If **claude-learned** is among the targets, the page must be `scope: global` + `tier: L1`, and you must also collect:
|
|
31
32
|
- **Global summary**: a one-line summary for the CLAUDE.md learned-behaviors entry.
|
|
@@ -61,6 +62,7 @@ node <package-root>/scripts/feedback.mjs \
|
|
|
61
62
|
--memory-summary="<one-line MEMORY.md summary>" \
|
|
62
63
|
--reason="<why this rule exists>" \
|
|
63
64
|
[--global-summary="<one-line CLAUDE.md summary>" --promote-to-global] \
|
|
65
|
+
[--failure-type="<enum>"] \
|
|
64
66
|
[--source="session:<date>"] \
|
|
65
67
|
[--hypo-dir="<path>"] \
|
|
66
68
|
[--dry-run]
|
|
@@ -68,6 +70,8 @@ node <package-root>/scripts/feedback.mjs \
|
|
|
68
70
|
|
|
69
71
|
When `--targets` includes `claude-learned`, `--global-summary` and `--promote-to-global` are required (and `--scope=global --tier=L1`).
|
|
70
72
|
|
|
73
|
+
`--failure-type` is optional (one of the eight values above). On **append** to an existing topic it is set only if the page has none; if the page already carries a different `failure_type` the command errors (a page holds a single failure_type — use a separate topic for a different one). Without the flag, an append leaves the frontmatter untouched as before.
|
|
74
|
+
|
|
71
75
|
> **`scope: project:<project-id>` 주의.** `<project-id>`는 `feedback-sync`가 resolve한 project-id와 정확히 일치해야 한다 (default: cwd의 `/`,`.` → `-` 치환; `--project-id=<id>` 로 override). 일치하지 않으면 그 페이지는 해당 project의 MEMORY로 projection되지 **않는다** (silent skip — lint error 아님). v1.3.0부터 scope regex(`^(global|project:[A-Za-z0-9_-]+)$`)가 cwd-derived id 형식(`-Users-...`)을 그대로 허용하므로 lint 통과를 위해 `--project-id=<slug>`를 override할 필요는 없다. 단 cwd에 공백 등 `[A-Za-z0-9_-]` 밖 문자가 있으면 그 id는 여전히 거부되니 그때만 `--project-id=<id>`로 override한다.
|
|
72
76
|
|
|
73
77
|
On a real (non-dry-run) write, the script automatically runs `feedback-sync --write` to refresh MEMORY.md / CLAUDE.md. If that post-step reports drift it prints a one-line warning — the page is still saved; reconcile with `hypomnema feedback-sync --check`.
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -115,7 +115,7 @@ Hooks run automatically at Claude Code lifecycle events. They are deployed to `~
|
|
|
115
115
|
| `hypo-personal-check` | PreCompact validation: lint blockers, uncommitted changes, missing session-log entries → block compact |
|
|
116
116
|
| `hypo-auto-stage` | After Write/Edit on a wiki path, run `git add` (skips paths matching `.hypoignore`) |
|
|
117
117
|
| `hypo-hot-rebuild` | At session stop, regenerate root `hot.md` from recent activity; emit growth metrics + cache for next SessionStart |
|
|
118
|
-
| `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
|
|
118
|
+
| `hypo-session-record` | At session stop, append `{session_id, transcript_path, recorded_at, cwd, device}` to `.cache/sessions/index.jsonl` (primary source for the observability audit) |
|
|
119
119
|
| `hypo-auto-commit` | At session stop, filter changed paths through `.hypoignore`, commit non-ignored changes, `git pull --no-rebase` + `git push` (silent fail on missing remote) |
|
|
120
120
|
| `hypo-cwd-change` | When working directory changes, re-resolve the active project and inject its `hot.md` |
|
|
121
121
|
| `hypo-file-watch` | Notify on external wiki edits so the in-session view stays consistent |
|
|
@@ -335,7 +335,7 @@ journal/weekly/<YYYY-Www>.md ← committed report (heuristic v0, s
|
|
|
335
335
|
|
|
336
336
|
`session-audit.mjs` reads transcripts from two locations, in priority order:
|
|
337
337
|
|
|
338
|
-
1. **Primary:** `<hypo-root>/.cache/sessions/index.jsonl` — written by the Stop hook `hypo-session-record.mjs`. Each line: `{ session_id, transcript_path, recorded_at, cwd }`.
|
|
338
|
+
1. **Primary:** `<hypo-root>/.cache/sessions/index.jsonl` — written by the Stop hook `hypo-session-record.mjs`. Each line: `{ session_id, transcript_path, recorded_at, cwd, device }`. `device` (PRAC-17) is `os.hostname()` for multi-machine audit; the index lives under `.cache/` (gitignored in a normal vault) so it is a local-only per-session record.
|
|
339
339
|
2. **Fallback:** `~/.claude/projects/<encoded>/*.jsonl` — scanned when the index is missing or empty (legacy / freshly-installed wikis).
|
|
340
340
|
|
|
341
341
|
### Classification
|
|
@@ -366,6 +366,8 @@ The score is a **proxy, not ground truth**. The four-week baseline plan (capture
|
|
|
366
366
|
|
|
367
367
|
The observability pipeline reads but never republishes raw transcripts. Weekly reports only emit `session_id` plus aggregate counts — no transcript content, no URLs, no tool inputs. Transcripts themselves live under `~/.claude/projects/` or `.cache/sessions/` which `.hypoignore` already excludes from any sync.
|
|
368
368
|
|
|
369
|
+
**PRAC-17 audit fields (`device`, `session_id`).** `device` (`os.hostname()`) is recorded in two places with different sync boundaries, by design. In `.cache/sessions/index.jsonl` it is per-session-accurate but **local-only** (`.cache/` is gitignored in a normal vault). In the seeded session-log daily-shard frontmatter it is an **intentional synced field** — a creator-only stamp (the session/machine that first created the day's shard), carried across machines so the wiki itself records which machine opened each day. `session_id` (the Claude session UUID) is seeded into that same synced frontmatter, but only on the Stop-chain close path that passes `--session-id`; on manual/skill closes it is omitted. This deliberately puts a hostname (and, on the automated path, the session UUID) into git-tracked, synced content; it is acceptable for a single-user private vault where multi-machine identification is the goal. Operators who do not want a hostname synced can override `os.hostname()` at the OS level or treat the field as a pseudonymous label.
|
|
370
|
+
|
|
369
371
|
### Growth metrics (Lane B)
|
|
370
372
|
|
|
371
373
|
A separate, lightweight counter — distinct from the audit pipeline — runs at every Stop / SessionStart pair:
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -232,28 +232,56 @@ Hypomnema uses semver. Releases are automated via `release.yml` on `v*` tag push
|
|
|
232
232
|
|
|
233
233
|
### Cutting a release
|
|
234
234
|
|
|
235
|
+
The maintainer drives this checklist with a personal `/ship` command (it lives in
|
|
236
|
+
`~/.claude/commands/`, alongside `qa-before-ship` — it is maintainer tooling, not
|
|
237
|
+
a shipped plugin command, since an OSS user has no reason to release Hypomnema
|
|
238
|
+
itself). The sequence below is that same checklist, and is the authoritative
|
|
239
|
+
in-repo reference; follow it whether or not you have the `/ship` convenience
|
|
240
|
+
command.
|
|
241
|
+
|
|
235
242
|
Every Hypomnema release must carry a Korean summary alongside the English body
|
|
236
243
|
in **both** the CHANGELOG section AND the git tag annotation. The release
|
|
237
244
|
workflow enforces this with `scripts/check-bilingual.mjs`; lightweight tags or
|
|
238
|
-
a missing `### 한글 요약` section will block `npm publish`.
|
|
245
|
+
a missing `### 한글 요약` section will block `npm publish`. It also requires the
|
|
246
|
+
release version to appear in **both** `README.md` and `README.ko.md`
|
|
247
|
+
(`scripts/check-readme-version.mjs`) — the floor that stops the README reconcile
|
|
248
|
+
from being dropped.
|
|
239
249
|
|
|
240
250
|
```bash
|
|
241
251
|
# 1. Bump the version across package.json, plugin.json, marketplace.json,
|
|
242
252
|
# and templates/hypo-config.md. Takes a concrete semver (not patch/minor/major).
|
|
243
253
|
node scripts/bump-version.mjs <new-semver> # e.g. 1.2.2 or 1.3.0-rc.1
|
|
244
254
|
|
|
255
|
+
# 1b. bump-version does NOT touch package-lock.json — sync it so `npm ci` and the
|
|
256
|
+
# version-consistency gate stay green (the lock carries the version twice).
|
|
257
|
+
npm install --package-lock-only
|
|
258
|
+
|
|
245
259
|
# 2. Edit CHANGELOG.md — the new section MUST include a "### 한글 요약"
|
|
246
260
|
# sub-section with at least 10 Hangul characters of real summary text.
|
|
247
261
|
$EDITOR CHANGELOG.md
|
|
248
262
|
|
|
249
|
-
# 3.
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
263
|
+
# 3. Reconcile BOTH READMEs — add a v<version> sentence to the rolling version
|
|
264
|
+
# narrative in README.md AND README.ko.md, and update the first-viewport
|
|
265
|
+
# "current release" pointer. (Dropped 3x historically; check:readme is the floor.)
|
|
266
|
+
$EDITOR README.md README.ko.md
|
|
267
|
+
|
|
268
|
+
# 4. Verify locally before tagging (the full gate set CI + prepublishOnly run)
|
|
269
|
+
npm test # unit suite
|
|
270
|
+
npm run lint # vault linter
|
|
271
|
+
npm run check:versions # all version-carrying files (incl. package-lock) agree
|
|
272
|
+
npm run check:bilingual # CHANGELOG section has a 한글 요약
|
|
273
|
+
npm run check:readme # v<version> present in BOTH READMEs
|
|
274
|
+
npm run smoke:plugin # plugin manifest + hooks/commands/skills load-valid
|
|
275
|
+
npm run smoke-pack # packed tarball installs and resolves
|
|
276
|
+
npm run check:tracker-ids # no private-tracker pointers leaked into shipped files
|
|
277
|
+
|
|
278
|
+
# 5. Commit — include EVERY file the bump touched, the READMEs, and the lockfile.
|
|
279
|
+
git add package.json package-lock.json .claude-plugin/ templates/hypo-config.md \
|
|
280
|
+
CHANGELOG.md README.md README.ko.md
|
|
254
281
|
git commit -m "chore: release v<version>"
|
|
282
|
+
# Then open the release PR, pass review + green CI, and squash-merge to main.
|
|
255
283
|
|
|
256
|
-
#
|
|
284
|
+
# 6. Tag the merge commit with an ANNOTATED tag — never a lightweight tag.
|
|
257
285
|
# Annotation body shape: English summary, then "---" on its own line,
|
|
258
286
|
# then a Korean summary block.
|
|
259
287
|
git tag -a v<version> -m "$(cat <<'EOF'
|
|
@@ -269,22 +297,33 @@ Hypomnema v<version> — <한 줄 한글 요약>
|
|
|
269
297
|
EOF
|
|
270
298
|
)"
|
|
271
299
|
|
|
272
|
-
#
|
|
300
|
+
# 7. Rehearse the release locally (same checks CI runs against the tag)
|
|
273
301
|
node scripts/check-bilingual.mjs --tag v<version>
|
|
302
|
+
node scripts/check-versions.mjs --tag v<version>
|
|
303
|
+
npm publish --dry-run --access public # packs + prepublishOnly, NO registry PUT
|
|
274
304
|
|
|
275
|
-
#
|
|
276
|
-
|
|
305
|
+
# 8. Push the SPECIFIC tag alone — NOT --tags (that would push stale local tags
|
|
306
|
+
# and could trigger releases for versions you did not intend).
|
|
307
|
+
git push origin v<version>
|
|
277
308
|
```
|
|
278
309
|
|
|
279
310
|
The `release.yml` workflow then:
|
|
280
311
|
|
|
281
|
-
1. Verifies the tag matches
|
|
282
|
-
|
|
283
|
-
(`scripts/check-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
`
|
|
312
|
+
1. Verifies the tag matches the version in EVERY version-carrying file —
|
|
313
|
+
`package.json`, `package-lock.json`, `.claude-plugin/{plugin,marketplace}.json`,
|
|
314
|
+
and `templates/hypo-config.md` (`scripts/check-versions.mjs --tag`).
|
|
315
|
+
2. Smokes the plugin channel — manifest, `hooks/hooks.json` targets, and
|
|
316
|
+
command/skill component files (`scripts/smoke-plugin.mjs`).
|
|
317
|
+
3. Validates the CHANGELOG section AND the tag annotation are bilingual
|
|
318
|
+
(`scripts/check-bilingual.mjs`), and that the version is present in both
|
|
319
|
+
READMEs (`scripts/check-readme-version.mjs`).
|
|
320
|
+
4. Runs `npm test` and `npm run lint`.
|
|
321
|
+
5. Publishes to npm with `npm publish --access public --provenance` and creates
|
|
322
|
+
the GitHub Release from the tag body.
|
|
323
|
+
|
|
324
|
+
`NPM_TOKEN` must be set as a repository secret. After rotating it, run the
|
|
325
|
+
`release.yml` `workflow_dispatch` "publish credential pre-check" — it verifies the
|
|
326
|
+
token authenticates without publishing anything.
|
|
288
327
|
|
|
289
328
|
### Versioning rules
|
|
290
329
|
|
|
@@ -6,14 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { spawnSync } from 'child_process';
|
|
9
|
-
import { HYPO_DIR,
|
|
10
|
-
|
|
11
|
-
function git(...args) {
|
|
12
|
-
return spawnSync('git', ['-C', HYPO_DIR, ...args], { encoding: 'utf-8', timeout: 30000 });
|
|
13
|
-
}
|
|
9
|
+
import { HYPO_DIR, syncRemote, commitWikiChanges } from './hypo-shared.mjs';
|
|
14
10
|
|
|
15
11
|
function hasRemote() {
|
|
16
|
-
const r =
|
|
12
|
+
const r = spawnSync('git', ['-C', HYPO_DIR, 'remote'], { encoding: 'utf-8', timeout: 30000 });
|
|
17
13
|
return (r.stdout || '').trim().length > 0;
|
|
18
14
|
}
|
|
19
15
|
|
|
@@ -28,12 +24,10 @@ if (!result.committed) {
|
|
|
28
24
|
|
|
29
25
|
if (hasRemote()) {
|
|
30
26
|
// pull/push failures must not stop the session, but they can no longer be
|
|
31
|
-
// swallowed silently —
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const push = git('push');
|
|
36
|
-
if (push.status !== 0) appendSyncFailure(HYPO_DIR, 'push', push.stderr || push.stdout);
|
|
27
|
+
// swallowed silently — syncRemote records each to .cache/sync-state.json and,
|
|
28
|
+
// on a merge conflict, aborts the merge so the tree is never left half-merged
|
|
29
|
+
// (FEAT-17 hardening). session-start + doctor surface the result next session.
|
|
30
|
+
syncRemote(HYPO_DIR);
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
console.log(JSON.stringify({ continue: true, suppressOutput: true }));
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
14
14
|
import { dirname, join } from 'path';
|
|
15
|
+
import { hostname } from 'os';
|
|
15
16
|
import { HYPO_DIR } from './hypo-shared.mjs';
|
|
16
17
|
|
|
17
18
|
const INDEX_PATH = join(HYPO_DIR, '.cache', 'sessions', 'index.jsonl');
|
|
@@ -50,6 +51,10 @@ process.stdin.on('end', () => {
|
|
|
50
51
|
transcript_path: transcriptPath,
|
|
51
52
|
recorded_at: new Date().toISOString(),
|
|
52
53
|
cwd: payload.cwd || process.cwd(),
|
|
54
|
+
// PRAC-17: machine identity for multi-machine audit. index.jsonl lives
|
|
55
|
+
// under .cache/ (gitignored in a normal vault), so this is a LOCAL-only
|
|
56
|
+
// per-session record — accurate for every session, no sync/privacy cost.
|
|
57
|
+
device: hostname() || 'unknown',
|
|
53
58
|
};
|
|
54
59
|
appendFileSync(INDEX_PATH, JSON.stringify(entry) + '\n');
|
|
55
60
|
} catch (err) {
|
|
@@ -243,6 +243,13 @@ function syncStateNotice(pullOk) {
|
|
|
243
243
|
return '';
|
|
244
244
|
}
|
|
245
245
|
const last = entries[entries.length - 1];
|
|
246
|
+
if (last.op === 'conflict') {
|
|
247
|
+
return (
|
|
248
|
+
`[WIKI: remote diverged — auto-merge was aborted to protect your edits ` +
|
|
249
|
+
`(your local work is committed and safe; the other machine's version is on the remote). ` +
|
|
250
|
+
`Resolve manually: \`git -C ${HYPO_DIR} pull --no-rebase\`, fix conflicts, then push.]`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
246
253
|
return `[WIKI: last sync failed: ${last.op || '?'} — ${last.error || 'unknown'}]`;
|
|
247
254
|
}
|
|
248
255
|
const GLOBAL_HOT = join(HYPO_DIR, 'hot.md');
|
package/hooks/hypo-shared.mjs
CHANGED
|
@@ -846,7 +846,10 @@ function syncStatePath(hypoDir) {
|
|
|
846
846
|
* failure-log must not break the Stop hook that calls it.
|
|
847
847
|
*
|
|
848
848
|
* @param {string} hypoDir
|
|
849
|
-
* @param {'pull'|'push'} op
|
|
849
|
+
* @param {'pull'|'push'|'conflict'|'conflict-unresolved'} op 'conflict' = a
|
|
850
|
+
* merge conflict was detected and aborted (the tree was left clean at the
|
|
851
|
+
* local commit); 'conflict-unresolved' = the abort itself failed and the tree
|
|
852
|
+
* may still be half-merged (rare). See syncRemote.
|
|
850
853
|
* @param {string} error raw stderr/stdout; first non-empty line is kept
|
|
851
854
|
*/
|
|
852
855
|
export function appendSyncFailure(hypoDir, op, error) {
|
|
@@ -870,6 +873,70 @@ export function appendSyncFailure(hypoDir, op, error) {
|
|
|
870
873
|
}
|
|
871
874
|
}
|
|
872
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Pull + push the wiki against its remote, guaranteeing the working tree is
|
|
878
|
+
* never left half-merged. Called by the auto-commit Stop hook after a local
|
|
879
|
+
* commit succeeds.
|
|
880
|
+
*
|
|
881
|
+
* Failure policy (v1.4 "sync hardening", tracker FEAT-17):
|
|
882
|
+
* - clean fast-forward / conflict-free merge → push.
|
|
883
|
+
* - MERGE CONFLICT (`git pull --no-rebase` leaves unmerged paths): abort the
|
|
884
|
+
* merge so the tree returns to the just-committed local state ("ours"),
|
|
885
|
+
* record op='conflict', and do NOT push (a diverged branch cannot
|
|
886
|
+
* fast-forward, so the push would only add a noisy second failure). No data
|
|
887
|
+
* is lost: ours stays committed locally, "theirs" stays on the remote, and
|
|
888
|
+
* the divergence is surfaced by session-start + doctor until the user merges
|
|
889
|
+
* manually. Inline auto-resolution (preserving the losing version as a
|
|
890
|
+
* `.conflict-*` sibling) is deferred — see tracker PRAC-18.
|
|
891
|
+
* - non-conflict pull failure (network/auth: no unmerged paths) → record
|
|
892
|
+
* op='pull', then still attempt push (a transient pull blip should not block
|
|
893
|
+
* an otherwise-pushable commit); record op='push' if that also fails.
|
|
894
|
+
*
|
|
895
|
+
* Best-effort: never throws — a sync failure must not break the Stop hook.
|
|
896
|
+
*
|
|
897
|
+
* @param {string} hypoDir
|
|
898
|
+
* @returns {{pulled: boolean, pushed: boolean, conflict: boolean}}
|
|
899
|
+
*/
|
|
900
|
+
export function syncRemote(hypoDir) {
|
|
901
|
+
const git = (...args) =>
|
|
902
|
+
spawnSync('git', ['-C', hypoDir, ...args], { encoding: 'utf-8', timeout: 30000 });
|
|
903
|
+
const result = { pulled: false, pushed: false, conflict: false };
|
|
904
|
+
try {
|
|
905
|
+
const pull = git('pull', '--no-rebase', '-q');
|
|
906
|
+
if (pull.status === 0) {
|
|
907
|
+
result.pulled = true;
|
|
908
|
+
} else {
|
|
909
|
+
// A merge conflict leaves unmerged index entries; a network/auth failure
|
|
910
|
+
// leaves none. Only the former must be aborted to keep the tree clean.
|
|
911
|
+
const unmerged = git('ls-files', '-u');
|
|
912
|
+
const hasConflict = unmerged.status === 0 && (unmerged.stdout || '').trim().length > 0;
|
|
913
|
+
if (hasConflict) {
|
|
914
|
+
// Abort to return the tree to the just-committed local state. Verify the
|
|
915
|
+
// abort actually cleaned up: if it fails (filesystem/concurrent-mutation
|
|
916
|
+
// edge), the tree may still be half-merged, so record that distinctly
|
|
917
|
+
// ('conflict-unresolved') rather than masking it as a clean abort.
|
|
918
|
+
const abort = git('merge', '--abort');
|
|
919
|
+
const stillUnmerged = git('ls-files', '-u');
|
|
920
|
+
const aborted = abort.status === 0 && (stillUnmerged.stdout || '').trim().length === 0;
|
|
921
|
+
appendSyncFailure(
|
|
922
|
+
hypoDir,
|
|
923
|
+
aborted ? 'conflict' : 'conflict-unresolved',
|
|
924
|
+
pull.stderr || pull.stdout,
|
|
925
|
+
);
|
|
926
|
+
result.conflict = true;
|
|
927
|
+
return result; // do not push from a diverged branch
|
|
928
|
+
}
|
|
929
|
+
appendSyncFailure(hypoDir, 'pull', pull.stderr || pull.stdout);
|
|
930
|
+
}
|
|
931
|
+
const push = git('push');
|
|
932
|
+
if (push.status === 0) result.pushed = true;
|
|
933
|
+
else appendSyncFailure(hypoDir, 'push', push.stderr || push.stdout);
|
|
934
|
+
} catch {
|
|
935
|
+
// best-effort — never break the Stop hook
|
|
936
|
+
}
|
|
937
|
+
return result;
|
|
938
|
+
}
|
|
939
|
+
|
|
873
940
|
/**
|
|
874
941
|
* Stage + commit every non-.hypoignore change in the wiki. Does NOT pull/push —
|
|
875
942
|
* remote sync stays in the auto-commit Stop hook (commit is local + cheap; sync is
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypomnema",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "LLM-native personal wiki system for Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,12 +42,15 @@
|
|
|
42
42
|
"fix:verify": "node scripts/fix-status-verify.mjs",
|
|
43
43
|
"graph": "node scripts/graph.mjs",
|
|
44
44
|
"smoke-pack": "node scripts/smoke-pack.mjs",
|
|
45
|
+
"check:versions": "node scripts/check-versions.mjs",
|
|
46
|
+
"smoke:plugin": "node scripts/smoke-plugin.mjs",
|
|
45
47
|
"check:bilingual": "node scripts/check-bilingual.mjs --changelog",
|
|
48
|
+
"check:readme": "node scripts/check-readme-version.mjs",
|
|
46
49
|
"check:tracker-ids": "node scripts/check-tracker-ids.mjs --all",
|
|
47
50
|
"format": "prettier --write .",
|
|
48
51
|
"format:check": "prettier --check .",
|
|
49
52
|
"prepare": "node scripts/install-git-hooks.mjs",
|
|
50
|
-
"prepublishOnly": "npm test && npm run lint && npm run smoke-pack && npm run check:bilingual && npm run check:tracker-ids"
|
|
53
|
+
"prepublishOnly": "npm test && npm run lint && npm run check:versions && npm run smoke:plugin && npm run smoke-pack && npm run check:bilingual && npm run check:readme && npm run check:tracker-ids"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
53
56
|
"prettier": "^3.8.3"
|
package/scripts/bump-version.mjs
CHANGED
|
@@ -51,13 +51,27 @@ for (const { path, pattern } of targets) {
|
|
|
51
51
|
console.log(`✓ ${path}: ${current} → ${next}`);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
console.log(`\nThis bumps the 4 manifest files only. The full release checklist lives in`);
|
|
55
|
+
console.log(`docs/CONTRIBUTING.md "Cutting a release" — follow it, or these steps:`);
|
|
54
56
|
console.log(`\nNext steps:`);
|
|
55
|
-
console.log(` 1.
|
|
57
|
+
console.log(` 1. Sync package-lock (bump-version does NOT touch it; the lock carries the`);
|
|
58
|
+
console.log(` version twice and a stale lock fails check:versions + breaks npm ci):`);
|
|
59
|
+
console.log(` npm install --package-lock-only`);
|
|
60
|
+
console.log(` 2. Edit CHANGELOG.md — ensure the "## [${next}]" section has a`);
|
|
56
61
|
console.log(` "### 한글 요약" sub-section (release.yml check-bilingual gate).`);
|
|
57
|
-
console.log(`
|
|
58
|
-
console.log(`
|
|
59
|
-
console.log(`
|
|
62
|
+
console.log(` 3. Reconcile BOTH READMEs — add a v${next} sentence to README.md AND`);
|
|
63
|
+
console.log(` README.ko.md, and update the first-viewport "current release" pointer.`);
|
|
64
|
+
console.log(` (This step was dropped 3x; check:readme is now the floor gate.)`);
|
|
65
|
+
console.log(` 4. Local pre-checks — the full gate set CI + prepublishOnly run:`);
|
|
66
|
+
console.log(` npm test && npm run lint && npm run check:versions \\`);
|
|
67
|
+
console.log(` && npm run check:bilingual && npm run check:readme \\`);
|
|
68
|
+
console.log(` && npm run smoke:plugin && npm run smoke-pack && npm run check:tracker-ids`);
|
|
69
|
+
console.log(` 5. git add package.json package-lock.json .claude-plugin/ \\`);
|
|
70
|
+
console.log(` templates/hypo-config.md CHANGELOG.md README.md README.ko.md`);
|
|
71
|
+
console.log(` git commit -m "chore(release): v${next}"`);
|
|
72
|
+
console.log(` 6. Create an ANNOTATED tag (lightweight tags are rejected by CI):`);
|
|
60
73
|
console.log(` git tag -a v${next} -m "<English body>\\n\\n---\\n\\n<한글 요약>"`);
|
|
61
74
|
console.log(` See docs/CONTRIBUTING.md "Cutting a release" for the full template.`);
|
|
62
|
-
console.log(`
|
|
63
|
-
console.log(`
|
|
75
|
+
console.log(` 7. node scripts/check-bilingual.mjs --tag v${next} # local pre-check`);
|
|
76
|
+
console.log(` 8. git push origin <branch> # then push the tag ALONE to trigger release:`);
|
|
77
|
+
console.log(` git push origin v${next} # NOT --tags (avoid triggering stale tags)`);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-readme-version.mjs — assert the release version string is present in BOTH
|
|
3
|
+
// README.md and README.ko.md. This is the machine FLOOR for the README-reconcile
|
|
4
|
+
// step that was dropped three times (v1.2 / v1.3.0 / v1.3.1): a publish of vX.Y.Z
|
|
5
|
+
// must not go out unless vX.Y.Z is written into both READMEs' version narrative.
|
|
6
|
+
//
|
|
7
|
+
// SCOPE (honest): this catches the GROSS drop — shipping a version whose sentence
|
|
8
|
+
// was never added to the README at all. It does NOT verify the narrative is
|
|
9
|
+
// accurate or that the first-viewport "current release" pointer was updated; that
|
|
10
|
+
// stays a judgment step owned by the release checklist (docs/CONTRIBUTING.md
|
|
11
|
+
// "Cutting a release"). README/CHANGELOG carry
|
|
12
|
+
// prose version HISTORY (every past vX.Y.Z), so check-versions.mjs intentionally
|
|
13
|
+
// excludes them — this script is their complement, not a duplicate.
|
|
14
|
+
//
|
|
15
|
+
// Usage:
|
|
16
|
+
// node scripts/check-readme-version.mjs # version from package.json
|
|
17
|
+
// node scripts/check-readme-version.mjs --version 1.4.0 # explicit version
|
|
18
|
+
// node scripts/check-readme-version.mjs --root <dir> # point at a fixture (tests)
|
|
19
|
+
// node scripts/check-readme-version.mjs --json
|
|
20
|
+
//
|
|
21
|
+
// Exit 0 = both READMEs mention the version. Exit 1 = missing in one/both, or unreadable.
|
|
22
|
+
|
|
23
|
+
import { readFileSync } from 'fs';
|
|
24
|
+
import { join, dirname } from 'path';
|
|
25
|
+
import { fileURLToPath } from 'url';
|
|
26
|
+
|
|
27
|
+
function parseArgs(argv) {
|
|
28
|
+
const args = { root: null, version: null, json: false };
|
|
29
|
+
for (let i = 0; i < argv.length; i++) {
|
|
30
|
+
const a = argv[i];
|
|
31
|
+
if (a.startsWith('--root=')) args.root = a.slice(7);
|
|
32
|
+
else if (a === '--root') args.root = argv[++i];
|
|
33
|
+
else if (a.startsWith('--version=')) args.version = a.slice(10);
|
|
34
|
+
else if (a === '--version') args.version = argv[++i];
|
|
35
|
+
else if (a === '--json') args.json = true;
|
|
36
|
+
}
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
41
|
+
|
|
42
|
+
const README_FILES = ['README.md', 'README.ko.md'];
|
|
43
|
+
|
|
44
|
+
// Build a boundary-aware matcher so a version does not spuriously match inside a
|
|
45
|
+
// LONGER version token yet still matches at a sentence boundary. The checked
|
|
46
|
+
// version must be a standalone token, NOT a prefix of another version:
|
|
47
|
+
// - "1.3.40", "v11.3.4" → reject (digit continuation)
|
|
48
|
+
// - "1.3.4.5" → reject (dot-then-digit continuation)
|
|
49
|
+
// - "1.3.4-rc.1", "1.3.4+b" → reject (a stable check must NOT pass on a
|
|
50
|
+
// prerelease/build string — that defeats the floor)
|
|
51
|
+
// - "1.3.0-rc.1a", "1.3.0-rc.10", "1.3.0-rc.1.alpha" → a prerelease check must
|
|
52
|
+
// reject these continuation tokens too
|
|
53
|
+
// - "shipped v1.3.4." / "v1.3.4**" / "(v1.3.4)" → match (sentence/markup boundary)
|
|
54
|
+
// Leading: no digit/dot immediately before (an optional "v" is fine — it is
|
|
55
|
+
// neither). Trailing: not a semver-continuation char ([0-9A-Za-z+-]) and not a
|
|
56
|
+
// dot-then-alphanumeric (".5" / ".alpha" continuation); a sentence period
|
|
57
|
+
// (dot followed by space / punctuation / end) is allowed.
|
|
58
|
+
function versionPresent(text, version) {
|
|
59
|
+
const escaped = version.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
60
|
+
return new RegExp(`(?<![\\d.])${escaped}(?![\\dA-Za-z+-])(?!\\.[\\dA-Za-z])`).test(text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkReadmeVersions(root, version) {
|
|
64
|
+
const results = README_FILES.map((file) => {
|
|
65
|
+
const abs = join(root, file);
|
|
66
|
+
try {
|
|
67
|
+
const text = readFileSync(abs, 'utf-8');
|
|
68
|
+
return { file, present: versionPresent(text, version) };
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return { file, present: false, error: err?.message ?? String(err) };
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
const ok = results.every((r) => r.present);
|
|
74
|
+
return { ok, version, results };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
const args = parseArgs(process.argv.slice(2));
|
|
79
|
+
const root = args.root || REPO_ROOT;
|
|
80
|
+
|
|
81
|
+
let version = args.version;
|
|
82
|
+
if (!version) {
|
|
83
|
+
try {
|
|
84
|
+
version = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).version;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const msg = `cannot read package.json for version: ${err?.message ?? err}`;
|
|
87
|
+
if (args.json) console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
|
88
|
+
else console.error(`✗ ${msg}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (typeof version !== 'string' || !version) {
|
|
93
|
+
const msg = 'no version (—version empty and package.json has no "version" field)';
|
|
94
|
+
if (args.json) console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
|
|
95
|
+
else console.error(`✗ ${msg}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const report = checkReadmeVersions(root, version);
|
|
100
|
+
|
|
101
|
+
if (args.json) {
|
|
102
|
+
console.log(JSON.stringify(report, null, 2));
|
|
103
|
+
} else {
|
|
104
|
+
for (const r of report.results) {
|
|
105
|
+
const mark = r.error ? `ERROR: ${r.error}` : r.present ? 'found' : 'MISSING';
|
|
106
|
+
console.log(` ${r.file.padEnd(13)} ${mark}`);
|
|
107
|
+
}
|
|
108
|
+
if (report.ok) {
|
|
109
|
+
console.log(`\n✓ both READMEs mention version ${version}`);
|
|
110
|
+
} else {
|
|
111
|
+
const missing = report.results
|
|
112
|
+
.filter((r) => !r.present)
|
|
113
|
+
.map((r) => (r.error ? `${r.file} (unreadable)` : r.file))
|
|
114
|
+
.join(', ');
|
|
115
|
+
console.error(
|
|
116
|
+
`\n✗ version ${version} missing from: ${missing}\n` +
|
|
117
|
+
` Reconcile the version narrative in both READMEs before releasing ` +
|
|
118
|
+
`(the README reconcile step — see docs/CONTRIBUTING.md "Cutting a release").`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
process.exit(report.ok ? 0 : 1);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main();
|