hypomnema 1.6.2 → 1.7.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.
Files changed (70) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ko.md +39 -14
  4. package/README.md +39 -14
  5. package/commands/capture.md +8 -6
  6. package/commands/crystallize.md +39 -20
  7. package/docs/ARCHITECTURE.md +49 -14
  8. package/docs/CONTRIBUTING.md +31 -29
  9. package/hooks/base-store.mjs +265 -0
  10. package/hooks/hooks.json +18 -1
  11. package/hooks/hypo-auto-commit.mjs +92 -15
  12. package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
  13. package/hooks/hypo-auto-stage.mjs +41 -1
  14. package/hooks/hypo-close-guard.mjs +246 -0
  15. package/hooks/hypo-cwd-change.mjs +31 -2
  16. package/hooks/hypo-file-watch.mjs +21 -2
  17. package/hooks/hypo-first-prompt.mjs +19 -3
  18. package/hooks/hypo-hot-rebuild.mjs +43 -5
  19. package/hooks/hypo-lookup.mjs +86 -29
  20. package/hooks/hypo-personal-check.mjs +24 -3
  21. package/hooks/hypo-session-record.mjs +2 -3
  22. package/hooks/hypo-session-start.mjs +199 -20
  23. package/hooks/hypo-shared.mjs +2080 -176
  24. package/hooks/proposal-store.mjs +513 -0
  25. package/hooks/version-check.mjs +45 -0
  26. package/package.json +42 -14
  27. package/scripts/capture.mjs +556 -37
  28. package/scripts/crystallize.mjs +751 -110
  29. package/scripts/doctor.mjs +787 -26
  30. package/scripts/feedback-sync.mjs +515 -44
  31. package/scripts/graph.mjs +35 -13
  32. package/scripts/init.mjs +287 -41
  33. package/scripts/lib/extensions.mjs +656 -1
  34. package/scripts/lib/git-hooks-dir.mjs +229 -0
  35. package/scripts/lib/hypo-ignore.mjs +54 -6
  36. package/scripts/lib/hypo-root.mjs +56 -6
  37. package/scripts/lib/page-usage.mjs +15 -2
  38. package/scripts/lib/pkg-json.mjs +40 -0
  39. package/scripts/lib/plugin-detect.mjs +96 -6
  40. package/scripts/lib/project-create.mjs +5 -1
  41. package/scripts/lib/rename-marker.mjs +39 -0
  42. package/scripts/lib/wd-match.mjs +23 -5
  43. package/scripts/lib/wikilink.mjs +32 -6
  44. package/scripts/lint.mjs +103 -5
  45. package/scripts/proposal.mjs +1032 -0
  46. package/scripts/query.mjs +25 -4
  47. package/scripts/rename.mjs +223 -18
  48. package/scripts/resume.mjs +34 -12
  49. package/scripts/stats.mjs +41 -9
  50. package/scripts/uninstall.mjs +141 -6
  51. package/scripts/upgrade.mjs +197 -15
  52. package/skills/crystallize/SKILL.md +44 -7
  53. package/skills/debate/SKILL.md +88 -0
  54. package/skills/debate/references/orchestration-patterns.md +83 -0
  55. package/templates/.hyposcanignore +10 -0
  56. package/templates/SCHEMA.md +12 -0
  57. package/templates/gitignore +9 -0
  58. package/templates/hypo-config.md +1 -1
  59. package/templates/hypo-guide.md +6 -0
  60. package/scripts/.gitkeep +0 -0
  61. package/scripts/check-bilingual.mjs +0 -153
  62. package/scripts/check-readme-version.mjs +0 -126
  63. package/scripts/check-tracker-ids.mjs +0 -426
  64. package/scripts/check-versions.mjs +0 -171
  65. package/scripts/install-git-hooks.mjs +0 -293
  66. package/scripts/lib/changelog-classify.mjs +0 -216
  67. package/scripts/lib/check-bilingual.mjs +0 -244
  68. package/scripts/lib/check-tracker-ids.mjs +0 -217
  69. package/scripts/lib/pre-commit-format.mjs +0 -251
  70. package/scripts/pre-commit-format.mjs +0 -198
@@ -0,0 +1,88 @@
1
+ ---
2
+ description: Structured debate to re-verify a wiki knowledge claim before you trust it, or to harden a hard-to-reverse decision into a durable wiki record. Runs a fixed order (interrogate, then verify against code and docs, then synthesize). Use when hypo:verify flags a page as overdue and you need to actually re-check its claim, when a decision is hard to reverse (architecture, refactor, external API integration) and should leave an ADR, or when the user asks to "debate", "stress-test", or "verify this claim". Skip it for reversible or already-obvious calls.
3
+ ---
4
+
5
+ You are running `/hypo:debate`. It formalizes an informal practice (a two-worker adversarial
6
+ review) into a reproducible fixed order, so the same rigor applies every time.
7
+
8
+ The premise: separate the mind that **generates and interrogates** from the mind that
9
+ **verifies**. Interrogation without verification lets a plausible wrong answer survive;
10
+ verification without interrogation confirms a wrong premise with precision. The order is
11
+ fixed for that reason.
12
+
13
+ The debate always ends in a durable wiki artifact: a refreshed page, a correction, or an
14
+ ADR. A debate that leaves nothing on disk was just a conversation. This is the producer
15
+ reviewer pattern applied to a single claim; for the orchestration vocabulary it belongs to,
16
+ see [references/orchestration-patterns.md](references/orchestration-patterns.md).
17
+
18
+ ## Two uses
19
+
20
+ 1. **Re-verify a wiki claim (primary).** `/hypo:verify` surfaces pages whose `verify_by_date`
21
+ is overdue (or that carry no `verify_by` question at all). This skill is the deep re-check
22
+ behind that prompt: does the page's `verify_by` question still hold against the current
23
+ code and docs? The outcome updates the page exactly as `/hypo:verify` Step 4 does (see
24
+ below), with the debate supplying the evidence.
25
+ 2. **Harden a hard-to-reverse decision.** Architecture choices, refactor direction, external
26
+ API or dependency integration, where the first-pass answer is often wrong (concurrency,
27
+ migrations, security boundaries, distributed state). The decision must land as an ADR or a
28
+ wiki page; if it would not, it is not big enough for a debate.
29
+
30
+ ## When not to use
31
+
32
+ - A change whose diff you can state in one sentence. Just do it.
33
+ - A reversible call, an already-obvious choice, or a one-off lookup. The debate's fixed cost
34
+ exceeds its value.
35
+
36
+ ## Fixed three phases
37
+
38
+ ### Phase 1. Interrogate (do not answer yet)
39
+
40
+ Attack the claim or decision. Build a list of questions, not conclusions.
41
+
42
+ - Surface hidden assumptions: "this rests on X being true. What if X is false?"
43
+ - Stand up at least two alternatives (three including the incumbent). Best and worst case of
44
+ each.
45
+ - List failure modes: how does this decision bite six months from now?
46
+ - Output: a list of **falsifiable claims**. Each must be resolvable to true / false by code,
47
+ docs, or a command ("p95 under 50ms", not "it will be fast"). For a wiki claim, restate the
48
+ page's `verify_by` question in a form you can actually check against the current code or
49
+ docs.
50
+
51
+ ### Phase 2. Verify (against code, docs, and the wiki)
52
+
53
+ Check each Phase 1 claim, one at a time. Do not invent new claims here.
54
+
55
+ - Read the code, find the docs, run the command. Judge each claim true / false / unknown.
56
+ - Unknown is unknown. A zero-result grep is not proof of absence.
57
+ - For a wiki claim, check it against the page's own cited sources and the code or docs it
58
+ describes, not against memory.
59
+ - For genuine independence, hand this phase to a reviewer who did not run the interrogation
60
+ (a fresh-context subagent, or a second person). Self-review of your own interrogation
61
+ breeds confirmation bias.
62
+ - Output: a **per-claim verdict table** (claim, verdict, evidence line or command).
63
+
64
+ ### Phase 3. Synthesize
65
+
66
+ Reconcile Phases 1 and 2 into one outcome, and write it down.
67
+
68
+ - Build the conclusion only from claims that survived verification. For each rejected
69
+ alternative, leave one line on why it fell.
70
+ - Name the residual risk (remaining unknowns, fail-open points) alongside the outcome. Do not
71
+ hide it.
72
+ - Land the durable artifact:
73
+ - Wiki claim: answer the page's `verify_by` question **yes / no / partially**. On yes,
74
+ refresh `last_reviewed` to today and push `verify_by_date` forward. On no or partially,
75
+ edit the page to correct it, then refresh both fields. This is exactly `/hypo:verify`
76
+ Step 4; the debate is what earns the answer.
77
+ - Decision: record it as an ADR (or a wiki page) holding the **decision, rationale, and
78
+ residual risk**. That record, not the discussion, is the deliverable.
79
+
80
+ ## Execution notes
81
+
82
+ - Run the three phases inline in order, or split only Phase 2 out to an independent reviewer.
83
+ That independence contributes the most to decision quality (the verifier's fresh context).
84
+ - Keep the boundary between interrogation (1) and verification (2). If a new alternative
85
+ occurs during verification, note it for Phase 1 and finish verifying the current claims
86
+ before opening a new round. Mixing the two blurs both.
87
+ - Do not accept a reviewer's verdict uncritically. The final call at synthesis stays with the
88
+ person running the debate.
@@ -0,0 +1,83 @@
1
+ # Multi-agent orchestration patterns
2
+
3
+ A shared vocabulary for designing multi-agent work. `/hypo:debate` is one instance of it
4
+ (the producer-reviewer pattern). When you orchestrate several agents around your wiki work,
5
+ name the pattern first, then build.
6
+
7
+ The one selection rule: **does the next stage need a barrier?** If not, pipeline. If it must
8
+ see everything at once, fan-out / fan-in. If several angles look at the same target, expert
9
+ pool. If one agent must grade another's output, producer-reviewer. If one place must hold the
10
+ decision, supervisor. If delegation spawns more delegation, hierarchical.
11
+
12
+ ## 1. Pipeline
13
+
14
+ Each item flows through the stages independently. No barrier between stages, so item A can be
15
+ in stage 3 while item B is still in stage 1. With enough parallel capacity, wall-clock trends
16
+ toward the slowest single-item chain rather than the sum of per-stage worsts (setting aside
17
+ pipeline fill and drain). With limited capacity, items queue and it degrades toward the
18
+ barrier case.
19
+
20
+ - Use when: every item runs the same chain and items do not wait on each other (per-file
21
+ transforms, a find then fix then verify done per item).
22
+ - Mistake: adding a barrier because you think a stage needs all prior results. That is
23
+ fan-out / fan-in, not a pipeline. Do any flatten / map / filter inside a stage.
24
+
25
+ ## 2. Fan-out / fan-in
26
+
27
+ Spread work in parallel (fan-out), then gather all of it at a barrier (fan-in) to merge,
28
+ dedup, or aggregate. Justified only when stage N needs every result of stage N-1 together.
29
+
30
+ - Use when: dedup across the full set before an expensive downstream step, early-exit when
31
+ the total is zero, or a result that must be compared against all the others.
32
+ - Mistake: forgetting the barrier's wait cost. Fast workers idle waiting on slow ones. Use it
33
+ only when you genuinely need all results.
34
+
35
+ ## 3. Expert pool
36
+
37
+ Several agents look at the same target through **different lenses** at once. Each holds a
38
+ different expertise (correctness, security, performance, reproducibility) or a different
39
+ viewpoint, blind to what the others see.
40
+
41
+ - Use when: one search angle cannot find everything, or a defect can break in more than one
42
+ way. The goal is **perspective diversity**, not redundant voting.
43
+ - Mistake: giving N agents the same prompt and calling it an expert pool. That is just
44
+ redundancy. The lenses must actually differ.
45
+
46
+ ## 4. Producer-reviewer
47
+
48
+ A producer makes something and an **independent** reviewer verifies it adversarially. Use it
49
+ when the maker should not grade their own work. Prompt the reviewer to find why it is wrong
50
+ first. `/hypo:debate` is this pattern applied to one claim.
51
+
52
+ - Use when: catching defects in a change, a design, or an artifact from a fresh context.
53
+ - Mistake: handing the reviewer the producer's rationale. The reviewer should see the target,
54
+ not the conclusion, to stay independent. Do not accept the review uncritically; the final
55
+ call stays with the orchestrator.
56
+
57
+ ## 5. Supervisor
58
+
59
+ One orchestrator delegates to workers and integrates the results. The **decision and the
60
+ human gate stay with the orchestrator**; only the heavy execution goes down.
61
+
62
+ - Use when: keeping the main context for decisions while isolating large raw material or
63
+ diffs in subagents. When you delegate, hand over everything the main context knows: the
64
+ goal, the read / write scope, the constraints already confirmed, the verify command, the
65
+ return schema.
66
+ - Mistake: treating a worker's output as evidence. It is a candidate. Trust it blindly and
67
+ you get a phantom backlog.
68
+
69
+ ## 6. Hierarchical delegation
70
+
71
+ A tree of delegation: a higher worker re-delegates to lower ones. Splits a large problem into
72
+ sub-problems handled by depth.
73
+
74
+ - Use when: a breadth or depth one orchestrator cannot hold (a large migration, a broad
75
+ audit).
76
+ - Mistake: allowing unbounded nesting depth. Keep the hierarchy to a controlled one or two
77
+ levels, and do not let a subagent open its own sub-delegation without a reason.
78
+
79
+ ## When not to reach for multi-agent at all
80
+
81
+ If the main context already holds the material and the task finishes faster than the delegation
82
+ overhead, do it inline. Before picking a pattern, ask whether the work is worth splitting. Most
83
+ single lookups are not.
@@ -0,0 +1,10 @@
1
+ # .hyposcanignore — patterns excluded from scan (lint/graph/stats/query/verify/doctor) only
2
+ # One glob per line. Lines starting with # are comments.
3
+ # Matched files are skipped by catalog scans but are still committed and still
4
+ # readable by hooks. For secrets and anything that must never be committed or
5
+ # surfaced by a hook, use .hypoignore instead — this file does NOT block commits.
6
+ # Syntax follows .gitignore glob rules.
7
+
8
+ # Draft / scratch (uncomment to hide from scans without blocking their commit)
9
+ # drafts/
10
+ # scratch/
@@ -91,11 +91,23 @@ Optional fields (add as needed):
91
91
  confidence: high | medium | low | speculative
92
92
  evidence_strength: direct | inferred | hearsay
93
93
  scope: always | project | session
94
+ visibility_scope: shared | machine:<device>
94
95
  source: <slug or URL>
95
96
  verify_by: <question to re-check at next review>
96
97
  verify_by_date: YYYY-MM-DD
97
98
  ```
98
99
 
100
+ `visibility_scope` is a different axis from `scope`: `scope` is memory lifetime,
101
+ `visibility_scope` is where a page may surface. Omitting it means `shared` (the
102
+ implicit default of every pre-existing page), so lookup/query/injection stay
103
+ unchanged. `machine:<device>` hides the page from every machine except the one
104
+ whose `currentDevice()` output equals `<device>`. Copy that exact string from the
105
+ newest `device` value in `.cache/sessions/index.jsonl`, or from a recent
106
+ session-log shard's `device:` frontmatter (both are `currentDevice()` outputs); a
107
+ hand-typed value that does not match will hide the page on its own machine too.
108
+ An empty owner (`machine:`) hides the page everywhere. `agent:<id>` is a reserved
109
+ forward-compat value that is not filtered yet.
110
+
99
111
  ### 3.1. `feedback` type — projection fields
100
112
 
101
113
  Feedback pages are the single source of truth for behavior corrections;
@@ -3,3 +3,12 @@
3
3
 
4
4
  # Runtime cache: sync-state.json contains hostname() and timestamps
5
5
  .cache/
6
+
7
+ # Append-lock files: transient <target>.lock guards around session-log / log.md
8
+ # writes (crystallize apply, deriveRootLogEntries). Normally deleted immediately;
9
+ # a crashed holder can leave a stale one, which must never be committed.
10
+ *.lock
11
+ # The lock is published by linking a staged sibling into place. `*.lock` does not
12
+ # match the staging name, and a crash between the link and its cleanup leaves one
13
+ # behind — where `git add -A` would happily pick it up.
14
+ *.lock.*.tmp
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: Hypomnema Config
3
3
  type: config
4
- version: "1.6.2"
4
+ version: "1.7.1"
5
5
  created: YYYY-MM-DD
6
6
  ---
7
7
 
@@ -2,6 +2,7 @@
2
2
  title: Wiki Operations Guide
3
3
  type: reference
4
4
  updated: YYYY-MM-DD
5
+ version: 1
5
6
  tags: [wiki, guide, operations]
6
7
  ---
7
8
 
@@ -108,6 +109,11 @@ Ask: *"이 작업이 마무리되었나요? 세션을 정리(crystallize)할까
108
109
  committing the files, run `/hypo:crystallize` (`--mark-session-closed --session-id=<id> --transcript-path=<path>`).
109
110
  Both writers gate the marker on the SAME `precompactGateStatus` as `/compact`,
110
111
  so the marker only lands when step 6 would print **"Compact-ready"**.
112
+ `--session-id` is not optional on the apply path. Before it writes anything, the
113
+ apply reads that session's transcript for evidence the **user** asked to close, and
114
+ refuses the whole close (exit 1, nothing on disk, nothing committed) when the id is
115
+ missing, resolves to no transcript, or the user never asked. A refusal means the
116
+ close should not happen: ask, then re-run.
111
117
 
112
118
  Skip session close for: single bug fix, single-file edit, Q&A only.
113
119
 
package/scripts/.gitkeep DELETED
File without changes
@@ -1,153 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * check-bilingual.mjs — CLI gate for the bilingual release-doc rule.
4
- *
5
- * Modes:
6
- * --changelog [version] Validate CHANGELOG.md section for given version.
7
- * Defaults to package.json's "version" field.
8
- * Wired into npm `prepublishOnly` so publishes fail
9
- * when the Korean summary is missing.
10
- *
11
- * --tag <ref> Validate annotated tag body for given ref.
12
- * Wired into .github/workflows/release.yml so a
13
- * lightweight tag or a missing Korean section
14
- * blocks the npm publish step.
15
- *
16
- * Exits 0 on pass, 1 on fail with a stderr diagnostic.
17
- */
18
-
19
- import { readFileSync } from 'fs';
20
- import { spawnSync } from 'child_process';
21
- import { fileURLToPath } from 'url';
22
- import { dirname, join } from 'path';
23
- import {
24
- validateChangelog,
25
- validateTagBody,
26
- listChangelogVersions,
27
- meetsKoreanCutoff,
28
- } from './lib/check-bilingual.mjs';
29
-
30
- const __dirname = dirname(fileURLToPath(import.meta.url));
31
- const REPO_ROOT = join(__dirname, '..');
32
-
33
- const RULE_REF =
34
- 'Rule source: CLAUDE.md learned_behaviors (release-doc-bilingual, 2026-05-24). ' +
35
- 'OSS Hypomnema ships must carry English body + Korean summary in both CHANGELOG section and git tag annotation.';
36
-
37
- function fail(msg) {
38
- process.stderr.write(`[check-bilingual] FAIL: ${msg}\n${RULE_REF}\n`);
39
- process.exit(1);
40
- }
41
-
42
- function ok(msg) {
43
- process.stdout.write(`[check-bilingual] OK: ${msg}\n`);
44
- process.exit(0);
45
- }
46
-
47
- function usage(exitCode) {
48
- process.stdout.write(
49
- `Usage:\n` +
50
- ` node scripts/check-bilingual.mjs --changelog [version]\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` +
56
- ` node scripts/check-bilingual.mjs --tag <ref>\n` +
57
- ` Validate annotated tag body (lightweight tags are rejected).\n`,
58
- );
59
- process.exit(exitCode);
60
- }
61
-
62
- const args = process.argv.slice(2);
63
- const mode = args[0];
64
- const wantAll = args.includes('--all');
65
-
66
- if (mode === '--help' || mode === '-h') usage(0);
67
- if (!mode) usage(1);
68
-
69
- if (mode === '--changelog') {
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;
102
- if (!version) {
103
- try {
104
- const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf-8'));
105
- version = pkg.version;
106
- } catch (err) {
107
- fail(`cannot read package.json: ${err.message}`);
108
- }
109
- }
110
- if (!version) fail('no version (arg empty, package.json has no "version" field)');
111
-
112
- const result = validateChangelog(content, version);
113
- if (!result.ok) fail(result.reason);
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
- }
119
- } else if (mode === '--tag') {
120
- const ref = args[1];
121
- if (!ref) fail('--tag requires a ref argument (e.g. v1.2.1)');
122
-
123
- // Reject lightweight tags. `git rev-parse <ref>^{tag}` succeeds ONLY for
124
- // annotated tags. For lightweight tags the ^{tag} peel fails because there
125
- // is no tag object — the ref points straight at a commit.
126
- const tagObj = spawnSync('git', ['rev-parse', '--verify', '--quiet', `${ref}^{tag}`], {
127
- encoding: 'utf-8',
128
- cwd: REPO_ROOT,
129
- });
130
- if (tagObj.status !== 0) {
131
- const exists = spawnSync('git', ['rev-parse', '--verify', '--quiet', ref], {
132
- encoding: 'utf-8',
133
- cwd: REPO_ROOT,
134
- });
135
- if (exists.status !== 0) fail(`tag ${ref} not found`);
136
- fail(
137
- `tag ${ref} is a lightweight tag, not annotated. ` +
138
- `Re-create with: git tag -a ${ref} -m "<English body>\n\n---\n\n<Korean summary>"`,
139
- );
140
- }
141
-
142
- const tagBody = spawnSync('git', ['tag', '-l', `--format=%(contents)`, ref], {
143
- encoding: 'utf-8',
144
- cwd: REPO_ROOT,
145
- });
146
- if (tagBody.status !== 0) fail(`failed to read tag ${ref}: ${tagBody.stderr}`);
147
-
148
- const result = validateTagBody(tagBody.stdout || '');
149
- if (!result.ok) fail(`tag ${ref} — ${result.reason}`);
150
- ok(`tag ${ref} annotation — ${result.hangulCount} Hangul chars after last "---" separator.`);
151
- } else {
152
- fail(`unknown mode: ${mode}. Use --changelog or --tag (see --help).`);
153
- }
@@ -1,126 +0,0 @@
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();