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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.ko.md +39 -14
- package/README.md +39 -14
- package/commands/capture.md +8 -6
- package/commands/crystallize.md +39 -20
- package/docs/ARCHITECTURE.md +49 -14
- package/docs/CONTRIBUTING.md +31 -29
- package/hooks/base-store.mjs +265 -0
- package/hooks/hooks.json +18 -1
- package/hooks/hypo-auto-commit.mjs +92 -15
- package/hooks/hypo-auto-minimal-crystallize.mjs +63 -20
- package/hooks/hypo-auto-stage.mjs +41 -1
- package/hooks/hypo-close-guard.mjs +246 -0
- package/hooks/hypo-cwd-change.mjs +31 -2
- package/hooks/hypo-file-watch.mjs +21 -2
- package/hooks/hypo-first-prompt.mjs +19 -3
- package/hooks/hypo-hot-rebuild.mjs +43 -5
- package/hooks/hypo-lookup.mjs +86 -29
- package/hooks/hypo-personal-check.mjs +24 -3
- package/hooks/hypo-session-record.mjs +2 -3
- package/hooks/hypo-session-start.mjs +199 -20
- package/hooks/hypo-shared.mjs +2080 -176
- package/hooks/proposal-store.mjs +513 -0
- package/hooks/version-check.mjs +45 -0
- package/package.json +42 -14
- package/scripts/capture.mjs +556 -37
- package/scripts/crystallize.mjs +751 -110
- package/scripts/doctor.mjs +787 -26
- package/scripts/feedback-sync.mjs +515 -44
- package/scripts/graph.mjs +35 -13
- package/scripts/init.mjs +287 -41
- package/scripts/lib/extensions.mjs +656 -1
- package/scripts/lib/git-hooks-dir.mjs +229 -0
- package/scripts/lib/hypo-ignore.mjs +54 -6
- package/scripts/lib/hypo-root.mjs +56 -6
- package/scripts/lib/page-usage.mjs +15 -2
- package/scripts/lib/pkg-json.mjs +40 -0
- package/scripts/lib/plugin-detect.mjs +96 -6
- package/scripts/lib/project-create.mjs +5 -1
- package/scripts/lib/rename-marker.mjs +39 -0
- package/scripts/lib/wd-match.mjs +23 -5
- package/scripts/lib/wikilink.mjs +32 -6
- package/scripts/lint.mjs +103 -5
- package/scripts/proposal.mjs +1032 -0
- package/scripts/query.mjs +25 -4
- package/scripts/rename.mjs +223 -18
- package/scripts/resume.mjs +34 -12
- package/scripts/stats.mjs +41 -9
- package/scripts/uninstall.mjs +141 -6
- package/scripts/upgrade.mjs +197 -15
- package/skills/crystallize/SKILL.md +44 -7
- package/skills/debate/SKILL.md +88 -0
- package/skills/debate/references/orchestration-patterns.md +83 -0
- package/templates/.hyposcanignore +10 -0
- package/templates/SCHEMA.md +12 -0
- package/templates/gitignore +9 -0
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +6 -0
- package/scripts/.gitkeep +0 -0
- package/scripts/check-bilingual.mjs +0 -153
- package/scripts/check-readme-version.mjs +0 -126
- package/scripts/check-tracker-ids.mjs +0 -426
- package/scripts/check-versions.mjs +0 -171
- package/scripts/install-git-hooks.mjs +0 -293
- package/scripts/lib/changelog-classify.mjs +0 -216
- package/scripts/lib/check-bilingual.mjs +0 -244
- package/scripts/lib/check-tracker-ids.mjs +0 -217
- package/scripts/lib/pre-commit-format.mjs +0 -251
- 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/
|
package/templates/SCHEMA.md
CHANGED
|
@@ -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;
|
package/templates/gitignore
CHANGED
|
@@ -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
|
package/templates/hypo-config.md
CHANGED
package/templates/hypo-guide.md
CHANGED
|
@@ -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();
|