hypomnema 1.4.0 → 1.4.1

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/commands/graph.md CHANGED
@@ -12,9 +12,9 @@ You are running `/hypo:graph`. Generate a link dependency graph from wiki pages.
12
12
 
13
13
  ---
14
14
 
15
- ## Step 1 — Locate package root
15
+ ## Step 1 — Run script
16
16
 
17
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
17
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
18
18
 
19
19
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
20
20
 
@@ -23,7 +23,7 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
23
23
  ## Step 2 — Run the graph script
24
24
 
25
25
  ```bash
26
- node <package-root>/scripts/graph.mjs \
26
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/graph.mjs \
27
27
  [--hypo-dir="<path>"] \
28
28
  [--format=json|mermaid|dot] \
29
29
  [--min-edges=<n>]
@@ -27,18 +27,20 @@ Do **not** fetch the URL or read the file yet — the privacy guard in Step 2 mu
27
27
 
28
28
  ## Step 2 — Privacy guard (`.hypoignore`)
29
29
 
30
- Refuse to ingest secrets (`.env`, SSH keys, credentials) before they ever reach `sources/`. Locate the Hypomnema package root and run the guard for **both** the input path and the destination path:
30
+ The script paths below resolve via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run them as written. If one appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
31
+
32
+ Refuse to ingest secrets (`.env`, SSH keys, credentials) before they ever reach `sources/`. Run the guard for **both** the input path and the destination path:
31
33
 
32
34
  1. **If the source is a file path**, check it (use an absolute path):
33
35
 
34
36
  ```bash
35
- node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"] --check="<absolute-input-path>"
37
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/ingest.mjs [--hypo-dir="<path>"] --check="<absolute-input-path>"
36
38
  ```
37
39
 
38
40
  2. **Always** check the destination `sources/<slug>.<ext>`:
39
41
 
40
42
  ```bash
41
- node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"] --check="sources/<slug>.<ext>"
43
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/ingest.mjs [--hypo-dir="<path>"] --check="sources/<slug>.<ext>"
42
44
  ```
43
45
 
44
46
  If either command exits non-zero, **stop**: surface the `Refused: ...` message to the user and do not fetch, read, or save the source. The slug check matters because a user could rename a `.env` to an innocuous slug — the destination must still be blocked.
@@ -50,7 +52,7 @@ If either command exits non-zero, **stop**: surface the `Refused: ...` message t
50
52
  Run the ingest helper to surface existing orphaned sources:
51
53
 
52
54
  ```bash
53
- node <package-root>/scripts/ingest.mjs [--hypo-dir="<path>"]
55
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/ingest.mjs [--hypo-dir="<path>"]
54
56
  ```
55
57
 
56
58
  If there are orphaned sources already in `sources/`, ask: "There are N unprocessed sources — do you want to ingest one of those instead?"
package/commands/init.md CHANGED
@@ -43,10 +43,12 @@ Ask the following questions **one at a time**. Use the default if the user press
43
43
 
44
44
  ## Step 2a — From Remote (skip wizard) {#step-2a}
45
45
 
46
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
47
+
46
48
  If the user provided `--from-remote <url>`, run:
47
49
 
48
50
  ```bash
49
- node <package-root>/scripts/init.mjs \
51
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/init.mjs \
50
52
  --from-remote="<url>" \
51
53
  --hypo-dir="<hypo-dir>" \
52
54
  [--no-hooks] \
@@ -62,11 +64,11 @@ node <package-root>/scripts/init.mjs \
62
64
 
63
65
  ## Step 2 — Run the init script (new wiki)
64
66
 
65
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
67
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
66
68
  Then run:
67
69
 
68
70
  ```bash
69
- node <package-root>/scripts/init.mjs \
71
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/init.mjs \
70
72
  --hypo-dir="<hypo-dir>" \
71
73
  [--no-hooks] \
72
74
  [--codex] \
package/commands/lint.md CHANGED
@@ -14,9 +14,9 @@ You are running `/hypo:lint`. Validate all wiki pages for frontmatter correctnes
14
14
 
15
15
  ---
16
16
 
17
- ## Step 1 — Locate package root
17
+ ## Step 1 — Run script
18
18
 
19
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
19
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
20
20
 
21
21
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag and the script resolves the Hypomnema root automatically via `HYPO_DIR` → `hypo-config.md` scan → `~/hypomnema`.
22
22
 
@@ -25,7 +25,7 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
25
25
  ## Step 2 — Run lint
26
26
 
27
27
  ```bash
28
- node <package-root>/scripts/lint.mjs [--hypo-dir="<path>"] [--json] [--fix]
28
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/lint.mjs [--hypo-dir="<path>"] [--json] [--fix]
29
29
  ```
30
30
 
31
31
  Options:
package/commands/query.md CHANGED
@@ -18,14 +18,14 @@ Ask the user what they want to know if it was not provided in the command invoca
18
18
 
19
19
  ---
20
20
 
21
- ## Step 2 — Locate package root and search
21
+ ## Step 2 — Search
22
22
 
23
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
23
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
24
24
 
25
25
  Run full-text search:
26
26
 
27
27
  ```bash
28
- node <package-root>/scripts/query.mjs \
28
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/query.mjs \
29
29
  --q="<query terms>" \
30
30
  [--hypo-dir="<path>"] \
31
31
  [--limit=10]
@@ -14,9 +14,9 @@ You are running `/hypo:rename`. Move a page or directory and content-aware rewri
14
14
 
15
15
  ---
16
16
 
17
- ## Step 1 — Locate package root
17
+ ## Step 1 — Run script
18
18
 
19
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
19
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
20
20
 
21
21
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
22
22
 
@@ -27,7 +27,7 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
27
27
  Always run the dry-run (no `--apply`) before writing anything:
28
28
 
29
29
  ```bash
30
- node <package-root>/scripts/rename.mjs \
30
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/rename.mjs \
31
31
  [--hypo-dir="<path>"] \
32
32
  --from=<slug|rel|dir> \
33
33
  --to=<slug|rel|dir> \
@@ -61,7 +61,7 @@ If the result is `ok: false`:
61
61
  Once the user confirms the dry-run looks right, re-run with `--apply`:
62
62
 
63
63
  ```bash
64
- node <package-root>/scripts/rename.mjs \
64
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/rename.mjs \
65
65
  [--hypo-dir="<path>"] \
66
66
  --from=<slug|rel|dir> \
67
67
  --to=<slug|rel|dir> \
@@ -14,12 +14,14 @@ You are running `/hypo:resume`. Load the session state for an active project and
14
14
 
15
15
  ## Step 1 — Resolve project
16
16
 
17
- If the user named a project in the command invocation, use that. Otherwise, locate the Hypomnema package root and run:
17
+ If the user named a project in the command invocation, use that. Otherwise, run:
18
18
 
19
19
  ```bash
20
- node <package-root>/scripts/resume.mjs [--hypo-dir="<path>"] [--project=<name>]
20
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/resume.mjs [--hypo-dir="<path>"] [--project=<name>]
21
21
  ```
22
22
 
23
+ The plugin harness expands `${CLAUDE_PLUGIN_ROOT}` to this package's absolute path before you see it, so run the command as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
24
+
23
25
  When `--project` is omitted, the script prefers the project whose `working_dir` contains the current directory (cwd-first); if nothing under the current directory matches, it falls back to the most recently active project from `hot.md`.
24
26
 
25
27
  ---
package/commands/stats.md CHANGED
@@ -13,9 +13,9 @@ You are running `/hypo:stats`. Display a summary of wiki health and activity.
13
13
 
14
14
  ---
15
15
 
16
- ## Step 1 — Locate package root
16
+ ## Step 1 — Run script
17
17
 
18
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
18
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
19
19
 
20
20
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
21
21
 
@@ -24,7 +24,7 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
24
24
  ## Step 2 — Run the stats script
25
25
 
26
26
  ```bash
27
- node <package-root>/scripts/stats.mjs [--hypo-dir="<path>"] [--json]
27
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/stats.mjs [--hypo-dir="<path>"] [--json]
28
28
  ```
29
29
 
30
30
  ---
@@ -12,9 +12,9 @@ You are running `/hypo:upgrade`. Check if the installed Hypomnema wiki is out of
12
12
 
13
13
  ---
14
14
 
15
- ## Step 1 — Locate package root
15
+ ## Step 1 — Run script
16
16
 
17
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
17
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
18
18
 
19
19
  If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. Otherwise omit the flag.
20
20
 
@@ -23,7 +23,7 @@ If the user specified a Hypomnema directory, pass it as `--hypo-dir="<path>"`. O
23
23
  ## Step 2 — Run upgrade check
24
24
 
25
25
  ```bash
26
- node <package-root>/scripts/upgrade.mjs [--hypo-dir="<path>"]
26
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/upgrade.mjs [--hypo-dir="<path>"]
27
27
  ```
28
28
 
29
29
  Show the output verbatim.
@@ -54,7 +54,7 @@ If there is anything to update, ask the user:
54
54
 
55
55
  - If **yes** → run with `--apply`:
56
56
  ```bash
57
- node <package-root>/scripts/upgrade.mjs [--hypo-dir="<path>"] --apply
57
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/upgrade.mjs [--hypo-dir="<path>"] --apply
58
58
  ```
59
59
  - If **no** → tell the user they can apply later by running `/hypo:upgrade` again.
60
60
 
@@ -11,12 +11,12 @@ You are running `/hypo:verify`. Audit wiki pages for overdue or missing `verify_
11
11
 
12
12
  ---
13
13
 
14
- ## Step 1 — Locate package root and run
14
+ ## Step 1 — Run
15
15
 
16
- Locate the Hypomnema package root (the directory containing this file's parent `commands/`).
16
+ The script path below resolves via `${CLAUDE_PLUGIN_ROOT}`, which the plugin harness expands to this package's absolute path before you see it, so run it as written. If it appears unexpanded (a literal `${CLAUDE_PLUGIN_ROOT}`), read the package root from the `hypo@hypomnema` installPath in `~/.claude/plugins/installed_plugins.json` rather than guessing from the cache layout.
17
17
 
18
18
  ```bash
19
- node <package-root>/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
19
+ node ${CLAUDE_PLUGIN_ROOT}/scripts/verify.mjs [--hypo-dir="<path>"] [--file=<path>]
20
20
  ```
21
21
 
22
22
  Options:
@@ -222,10 +222,83 @@ Open the PR against `main`. Include:
222
222
  - **What changed** — short summary
223
223
  - **Why** — the user-visible motivation
224
224
  - **Manual verification steps** — anything the test suite cannot cover
225
+ - **Changelog**: one English line plus one Korean line if the change is user-visible (see CHANGELOG conventions below)
225
226
  - **Migration notes** — if upgrading existing installs needs special handling
226
227
 
227
228
  ---
228
229
 
230
+ ## CHANGELOG conventions
231
+
232
+ `CHANGELOG.md` is the source of truth for release history. It follows a fixed section model, and contributors feed it through the `## Changelog` block in the PR template rather than editing `CHANGELOG.md` directly. The release collector and the maintainer assemble the final entries at release time.
233
+
234
+ ### PR title vs. merge commit
235
+
236
+ - **PR title**: Conventional Commits plus a scope, e.g. `feat(feedback): add failure_type enum`. The type drives the CHANGELOG section (see the classification table below).
237
+ - **Merge commit**: the squash-merge subject carries the PR number (`#123`). That is where `#N` comes from, not the PR title. The two conventions stay separate.
238
+ - Internal tracker ids (`FEAT-`, `IMPR-`, `ISSUE-`, `PRAC-`) may appear in your local notes and in source or workflow comments (an internal reference, like an ADR anchor), but never on the published changelog and release surface: not in the CHANGELOG body, not in the PR `## Changelog` block, not in a tag annotation, not in a GitHub Release. The only identifier that ships in those is the PR number `#N`. `check-tracker-ids --tag` gates the tag body; the migration keeps the CHANGELOG body clean.
239
+
240
+ ### The `## Changelog` block
241
+
242
+ If a change is user-visible, fill the `## Changelog` block in the PR body with one English line and one Korean line:
243
+
244
+ ```
245
+ ## Changelog
246
+
247
+ - EN: Feedback pages accept an optional `failure_type` so recurring mistakes are visible.
248
+ - KO: 피드백 페이지에 선택적 `failure_type`를 달아 반복되는 실수를 집계로 볼 수 있습니다.
249
+ ```
250
+
251
+ - Reference the PR by number only (`#123`). No internal tracker ids on this surface.
252
+ - No em dashes; use a colon, comma, or parentheses.
253
+ - Internal-only changes (a refactor with no user-visible effect, test-only, CI plumbing) write `None` and skip the lines.
254
+
255
+ ### Section model
256
+
257
+ Each version block in `CHANGELOG.md` is ordered, top to bottom:
258
+
259
+ 1. Optional `> [!IMPORTANT]` migration or breaking callout.
260
+ 2. `### New Features`, `### Bug Fixes`, `### Chores`: present only when that version has a user-relevant entry of that kind. An empty section is omitted. Keep each entry to one compact line; split or trim anything that runs to multiple sentences. Describe only what a user sees: purely internal work (release tooling, contributor docs, a refactor with no user-visible effect) gets no prose entry and lives in the `### Changelog` index alone.
261
+ 3. `### Changelog`: the PR-link index plus contributors. Always present. Language-neutral. It lists every merged PR, internal ones included.
262
+
263
+ At and after the v1.2.0 cutoff, each non-empty gated section (`New Features`, `Bug Fixes`, `Chores`) splits its content into `#### English` then `#### 한국어`, in that order:
264
+
265
+ ```
266
+ ### New Features
267
+ #### English
268
+ - Feedback pages accept an optional `failure_type`. (#141)
269
+ #### 한국어
270
+ - 피드백 페이지가 선택적 `failure_type`를 받습니다. (#141)
271
+ ```
272
+
273
+ Versions below the v1.2.0 cutoff (1.0.0, 1.0.1, 1.1.0) are English-only: the English body sits directly under the section header with no `####` sub-blocks. Korean is not back-filled for those releases.
274
+
275
+ The `### Changelog` index is language-neutral, one merged PR per line:
276
+
277
+ ```
278
+ ### Changelog
279
+ - #141 feedback failure_type classification
280
+ - #140 invalid-YAML lint guard
281
+ Contributors: @handle
282
+ ```
283
+
284
+ Each line is `- #N <short title>`, no em dash. The `Contributors:` line lists that version's PR authors, de-duplicated; the release collector fills the handles from the GitHub API.
285
+
286
+ A `### Known Issues` or `### Notes` block, when a version has one, is a trailing note (a caveat, not a change). It is not one of the gated sections, so it carries no `####` sub-blocks even at or after the cutoff; bilingual text there is recommended, not enforced.
287
+
288
+ ### Classification
289
+
290
+ The section is decided by change kind, with the Conventional Commit type as the default and content as the override:
291
+
292
+ | Input | Section |
293
+ |---|---|
294
+ | `feat:` / `FEAT-` | New Features |
295
+ | `fix:` / `ISSUE-` | Bug Fixes |
296
+ | `chore:` / `refactor:` / `docs:` / `ci:` / `perf:` / `IMPR-` / `PRAC-` | Chores |
297
+
298
+ Chores is defined by kind, not by visibility: improvements (`IMPR`), refactors, and release or internal cleanup all land in Chores even when user-visible. The external changelog highlights only New Features and Bug Fixes; everything else is a Chore.
299
+
300
+ ---
301
+
229
302
  ## Release process
230
303
 
231
304
  Hypomnema uses semver. Releases are automated via `release.yml` on `v*` tag push.
@@ -239,14 +312,16 @@ itself). The sequence below is that same checklist, and is the authoritative
239
312
  in-repo reference; follow it whether or not you have the `/ship` convenience
240
313
  command.
241
314
 
242
- Every Hypomnema release must carry a Korean summary alongside the English body
315
+ Every Hypomnema release must carry Korean alongside the English body
243
316
  in **both** the CHANGELOG section AND the git tag annotation. The release
244
- workflow enforces this with `scripts/check-bilingual.mjs`; lightweight tags or
245
- a missing `### 한글 요약` section will block `npm publish`. It also requires the
317
+ workflow enforces this with `scripts/check-bilingual.mjs`; a lightweight tag, or
318
+ a gated CHANGELOG section missing its `#### 한국어` sub-block, will block `npm publish`. It also requires the
246
319
  release version to appear in **both** `README.md` and `README.ko.md`
247
320
  (`scripts/check-readme-version.mjs`) — the floor that stops the README reconcile
248
321
  from being dropped.
249
322
 
323
+ Prerequisite: the changelog collector in step 2 reads merged-PR data through the GitHub CLI, so have `gh` installed and authenticated (`gh auth status`) before you start. It is a maintainer-only release script and is not shipped to npm.
324
+
250
325
  ```bash
251
326
  # 1. Bump the version across package.json, plugin.json, marketplace.json,
252
327
  # and templates/hypo-config.md. Takes a concrete semver (not patch/minor/major).
@@ -256,8 +331,14 @@ node scripts/bump-version.mjs <new-semver> # e.g. 1.2.2 or 1.3.0-rc.1
256
331
  # version-consistency gate stay green (the lock carries the version twice).
257
332
  npm install --package-lock-only
258
333
 
259
- # 2. Edit CHANGELOG.md — the new section MUST include a "### 한글 요약"
260
- # sub-section with at least 10 Hangul characters of real summary text.
334
+ # 2. Draft the new CHANGELOG.md section from the merged PRs' `## Changelog`
335
+ # blocks (see CHANGELOG conventions above). The collector prints a draft for
336
+ # the range since the last tag; paste it, then finalize the wording by hand,
337
+ # dropping any entry that is not user-relevant. --strict fails if any PR lacks
338
+ # a usable block.
339
+ node scripts/collect-changelog.mjs --strict # maintainer-only; not shipped to npm
340
+ # Each gated section (New Features / Bug Fixes / Chores) MUST carry both a
341
+ # "#### English" and a "#### 한국어" sub-block; check:bilingual enforces it.
261
342
  $EDITOR CHANGELOG.md
262
343
 
263
344
  # 3. Reconcile BOTH READMEs — add a v<version> sentence to the rolling version
@@ -269,7 +350,7 @@ $EDITOR README.md README.ko.md
269
350
  npm test # unit suite
270
351
  npm run lint # vault linter
271
352
  npm run check:versions # all version-carrying files (incl. package-lock) agree
272
- npm run check:bilingual # CHANGELOG section has a 한글 요약
353
+ npm run check:bilingual # each gated CHANGELOG section has #### English + #### 한국어
273
354
  npm run check:readme # v<version> present in BOTH READMEs
274
355
  npm run smoke:plugin # plugin manifest + hooks/commands/skills load-valid
275
356
  npm run smoke-pack # packed tarball installs and resolves
@@ -281,17 +362,19 @@ git add package.json package-lock.json .claude-plugin/ templates/hypo-config.md
281
362
  git commit -m "chore: release v<version>"
282
363
  # Then open the release PR, pass review + green CI, and squash-merge to main.
283
364
 
284
- # 6. Tag the merge commit with an ANNOTATED tag never a lightweight tag.
365
+ # 6. Tag the merge commit with an ANNOTATED tag, never a lightweight tag.
285
366
  # Annotation body shape: English summary, then "---" on its own line,
286
- # then a Korean summary block.
367
+ # then a Korean summary block. The GitHub Release republishes this body
368
+ # verbatim, so keep it on the same public surface as the CHANGELOG: PR
369
+ # numbers (#N) only, no internal tracker ids (check-tracker-ids --tag gates it).
287
370
  git tag -a v<version> -m "$(cat <<'EOF'
288
- Hypomnema v<version> <one-line English summary>
371
+ Hypomnema v<version>: <one-line English summary>
289
372
 
290
- <a few lines of English body what shipped, links, etc.>
373
+ <a few lines of English body: what shipped, links, etc.>
291
374
 
292
375
  ---
293
376
 
294
- Hypomnema v<version> <한 줄 한글 요약>
377
+ Hypomnema v<version>: <한 줄 한글 요약>
295
378
 
296
379
  <몇 줄의 한글 요약 본문.>
297
380
  EOF
@@ -300,6 +383,7 @@ EOF
300
383
  # 7. Rehearse the release locally (same checks CI runs against the tag)
301
384
  node scripts/check-bilingual.mjs --tag v<version>
302
385
  node scripts/check-versions.mjs --tag v<version>
386
+ node scripts/check-tracker-ids.mjs --tag v<version> # no tracker ids leak into the tag body
303
387
  npm publish --dry-run --access public # packs + prepublishOnly, NO registry PUT
304
388
 
305
389
  # 8. Push the SPECIFIC tag alone — NOT --tags (that would push stale local tags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hypomnema",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "LLM-native personal wiki system for Claude Code",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,6 +9,11 @@
9
9
  },
10
10
  "files": [
11
11
  "scripts/",
12
+ "!scripts/collect-changelog.mjs",
13
+ "!scripts/lib/collect-changelog.mjs",
14
+ "!scripts/bump-version.mjs",
15
+ "!scripts/smoke-pack.mjs",
16
+ "!scripts/smoke-plugin.mjs",
12
17
  "commands/",
13
18
  "hooks/",
14
19
  "skills/",
@@ -20,7 +20,12 @@ import { readFileSync } from 'fs';
20
20
  import { spawnSync } from 'child_process';
21
21
  import { fileURLToPath } from 'url';
22
22
  import { dirname, join } from 'path';
23
- import { validateChangelog, validateTagBody } from './lib/check-bilingual.mjs';
23
+ import {
24
+ validateChangelog,
25
+ validateTagBody,
26
+ listChangelogVersions,
27
+ meetsKoreanCutoff,
28
+ } from './lib/check-bilingual.mjs';
24
29
 
25
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
31
  const REPO_ROOT = join(__dirname, '..');
@@ -43,7 +48,11 @@ function usage(exitCode) {
43
48
  process.stdout.write(
44
49
  `Usage:\n` +
45
50
  ` node scripts/check-bilingual.mjs --changelog [version]\n` +
46
- ` Validate CHANGELOG.md "## [<version>]" section. Default version: package.json.\n` +
51
+ ` Validate one CHANGELOG.md "## [<version>]" section (section model).\n` +
52
+ ` Default version: package.json.\n` +
53
+ ` node scripts/check-bilingual.mjs --changelog --all\n` +
54
+ ` Validate EVERY documented version (Korean enforced at >= 1.2.0,\n` +
55
+ ` English-only versions below the cutoff pass).\n` +
47
56
  ` node scripts/check-bilingual.mjs --tag <ref>\n` +
48
57
  ` Validate annotated tag body (lightweight tags are rejected).\n`,
49
58
  );
@@ -52,12 +61,44 @@ function usage(exitCode) {
52
61
 
53
62
  const args = process.argv.slice(2);
54
63
  const mode = args[0];
64
+ const wantAll = args.includes('--all');
55
65
 
56
66
  if (mode === '--help' || mode === '-h') usage(0);
57
67
  if (!mode) usage(1);
58
68
 
59
69
  if (mode === '--changelog') {
60
- let version = args[1];
70
+ let content;
71
+ try {
72
+ content = readFileSync(join(REPO_ROOT, 'CHANGELOG.md'), 'utf-8');
73
+ } catch (err) {
74
+ fail(`cannot read CHANGELOG.md: ${err.message}`);
75
+ }
76
+
77
+ if (wantAll) {
78
+ // Validate every documented version. Korean is enforced only at/after the
79
+ // cutoff; pre-cutoff versions pass on English presence (format.md §9). This
80
+ // is the migration gate — it must not green-pass a half-migrated file.
81
+ const versions = listChangelogVersions(content);
82
+ if (versions.length === 0) fail('no "## [<version>]" sections found in CHANGELOG.md');
83
+ const failures = [];
84
+ let enforced = 0;
85
+ for (const v of versions) {
86
+ const r = validateChangelog(content, v);
87
+ if (meetsKoreanCutoff(v)) enforced++;
88
+ if (!r.ok) failures.push(` [${v}] ${r.reason}`);
89
+ }
90
+ if (failures.length) {
91
+ fail(`${failures.length}/${versions.length} version(s) failed:\n${failures.join('\n')}`);
92
+ }
93
+ ok(
94
+ `CHANGELOG.md --all: ${versions.length} versions conform ` +
95
+ `(${enforced} Korean-enforced at >= 1.2.0, ${versions.length - enforced} English-only below cutoff).`,
96
+ );
97
+ }
98
+
99
+ // single-version path (kept: prepublishOnly and release.yml call --changelog
100
+ // with no version, defaulting to package.json's).
101
+ let version = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
61
102
  if (!version) {
62
103
  try {
63
104
  const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf-8'));
@@ -68,16 +109,13 @@ if (mode === '--changelog') {
68
109
  }
69
110
  if (!version) fail('no version (arg empty, package.json has no "version" field)');
70
111
 
71
- let content;
72
- try {
73
- content = readFileSync(join(REPO_ROOT, 'CHANGELOG.md'), 'utf-8');
74
- } catch (err) {
75
- fail(`cannot read CHANGELOG.md: ${err.message}`);
76
- }
77
-
78
112
  const result = validateChangelog(content, version);
79
113
  if (!result.ok) fail(result.reason);
80
- ok(`CHANGELOG.md [${version}] — ${result.hangulCount} Hangul chars in "### 한글 요약".`);
114
+ if (result.koreanExempt) {
115
+ ok(`CHANGELOG.md [${version}] — pre-cutoff version, English-only (Korean exempt).`);
116
+ } else {
117
+ ok(`CHANGELOG.md [${version}] — ${result.hangulCount} Hangul chars across "#### 한국어" sub-blocks.`);
118
+ }
81
119
  } else if (mode === '--tag') {
82
120
  const ref = args[1];
83
121
  if (!ref) fail('--tag requires a ref argument (e.g. v1.2.1)');
@@ -51,6 +51,7 @@ import {
51
51
  messageHasGitTemplate,
52
52
  BLOCKED_PATTERNS,
53
53
  USER_FACING_PATTERNS,
54
+ TAG_BODY_PATTERNS,
54
55
  } from './lib/check-tracker-ids.mjs';
55
56
  import {
56
57
  parseNameStatus,
@@ -297,6 +298,52 @@ function runCommitMsg(file, json) {
297
298
  process.exit(report(violations, json));
298
299
  }
299
300
 
301
+ // Scan an annotated tag's body for ALL wiki tracker prefixes (TAG_BODY_PATTERNS:
302
+ // ISSUE-/fix #/FEAT-/IMPR-/PRAC-). The tag body is the PUBLIC release surface —
303
+ // `gh release create --notes-from-tag` republishes it verbatim — and it carries
304
+ // no code, so unlike the file gate it must reject every prefix (changelog-pr-
305
+ // guide §5 / T4). Wired into release.yml before `gh release create`. `--tag -`
306
+ // reads the body from stdin (piping / test), `--tag <ref>` reads it from git.
307
+ function runTag(ref, json) {
308
+ let body;
309
+ if (ref === '-') {
310
+ try {
311
+ body = readFileSync(0, 'utf-8');
312
+ } catch (err) {
313
+ process.stderr.write(`[check-tracker-ids] --tag -: cannot read stdin: ${err.message}\n`);
314
+ process.exit(2);
315
+ }
316
+ } else {
317
+ // Require an annotated tag: `<ref>^{tag}` peels only for an annotated tag
318
+ // object. A lightweight tag has no body to leak, but a release uses
319
+ // --notes-from-tag on an annotated tag, so a lightweight one here is a
320
+ // release misconfiguration — fail loudly rather than silently pass.
321
+ const tagObj = git(['rev-parse', '--verify', '--quiet', `${ref}^{tag}`]);
322
+ if (tagObj.status !== 0) {
323
+ const exists = git(['rev-parse', '--verify', '--quiet', ref]);
324
+ if (exists.status !== 0) {
325
+ process.stderr.write(`[check-tracker-ids] --tag: tag ${ref} not found\n`);
326
+ } else {
327
+ process.stderr.write(
328
+ `[check-tracker-ids] --tag: ${ref} is a lightweight tag (no annotation body to scan)\n`,
329
+ );
330
+ }
331
+ process.exit(2);
332
+ }
333
+ const tagBody = git(['tag', '-l', '--format=%(contents)', ref]);
334
+ if (tagBody.status !== 0) {
335
+ process.stderr.write(
336
+ `[check-tracker-ids] --tag: failed to read tag ${ref}: ${tagBody.stderr}\n`,
337
+ );
338
+ process.exit(2);
339
+ }
340
+ body = tagBody.stdout || '';
341
+ }
342
+ const file = ref === '-' ? '<stdin>' : `tag:${ref}`;
343
+ const violations = scanText(body, TAG_BODY_PATTERNS).map((h) => ({ file, ...h }));
344
+ process.exit(report(violations, json));
345
+ }
346
+
300
347
  // ── arg parsing ────────────────────────────────────────────────────────────
301
348
 
302
349
  function usage(code) {
@@ -304,7 +351,10 @@ function usage(code) {
304
351
  'Usage:\n' +
305
352
  ' node scripts/check-tracker-ids.mjs [--all] [--json]\n' +
306
353
  ' node scripts/check-tracker-ids.mjs --staged [--json]\n' +
307
- ' node scripts/check-tracker-ids.mjs --commit-msg <file> [--json]\n',
354
+ ' node scripts/check-tracker-ids.mjs --commit-msg <file> [--json]\n' +
355
+ ' node scripts/check-tracker-ids.mjs --tag <ref|-> [--json]\n' +
356
+ ' Scan an annotated tag body (or stdin via "-") for ALL tracker\n' +
357
+ ' prefixes; the public release surface must be tracker-ID-0.\n',
308
358
  );
309
359
  process.exit(code);
310
360
  }
@@ -321,6 +371,15 @@ if (argv.includes('--commit-msg')) {
321
371
  usage(2);
322
372
  }
323
373
  runCommitMsg(file, json);
374
+ } else if (argv.includes('--tag')) {
375
+ const i = argv.indexOf('--tag');
376
+ const ref = argv[i + 1];
377
+ // `-` (stdin) is a valid ref token here; only a missing/flag value is an error.
378
+ if (!ref || (ref.startsWith('--') && ref !== '-')) {
379
+ process.stderr.write('[check-tracker-ids] --tag requires a ref argument (or "-" for stdin)\n');
380
+ usage(2);
381
+ }
382
+ runTag(ref, json);
324
383
  } else if (argv.includes('--staged')) {
325
384
  runStaged(json);
326
385
  } else {