hypomnema 1.4.0 → 1.4.2
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 +151 -153
- package/README.md +121 -123
- package/commands/audit.md +4 -4
- package/commands/crystallize.md +22 -7
- package/commands/doctor.md +3 -3
- package/commands/feedback.md +5 -3
- package/commands/graph.md +3 -3
- package/commands/ingest.md +6 -4
- package/commands/init.md +5 -3
- package/commands/lint.md +3 -3
- package/commands/query.md +3 -3
- package/commands/rename.md +4 -4
- package/commands/resume.md +4 -2
- package/commands/stats.md +3 -3
- package/commands/upgrade.md +4 -4
- package/commands/verify.md +3 -3
- package/docs/CONTRIBUTING.md +107 -16
- package/hooks/hypo-auto-minimal-crystallize.mjs +23 -11
- package/hooks/hypo-shared.mjs +96 -14
- package/package.json +6 -1
- package/scripts/check-bilingual.mjs +49 -11
- package/scripts/check-tracker-ids.mjs +60 -1
- package/scripts/crystallize.mjs +275 -39
- package/scripts/lib/changelog-classify.mjs +216 -0
- package/scripts/lib/check-bilingual.mjs +125 -22
- package/scripts/lib/check-tracker-ids.mjs +19 -0
- package/scripts/lib/project-create.mjs +23 -5
- package/scripts/lib/schema-vocab.mjs +105 -1
- package/scripts/lint.mjs +9 -1
- package/scripts/weekly-report.mjs +9 -3
- package/skills/crystallize/SKILL.md +30 -2
- package/templates/SCHEMA.md +11 -3
- package/templates/hypo-config.md +1 -1
- package/templates/hypo-guide.md +4 -1
- package/scripts/bump-version.mjs +0 -77
- package/scripts/smoke-pack.mjs +0 -261
- package/scripts/smoke-plugin.mjs +0 -194
package/hooks/hypo-shared.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readFileSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
existsSync,
|
|
13
|
+
statSync,
|
|
13
14
|
appendFileSync,
|
|
14
15
|
mkdirSync,
|
|
15
16
|
rmSync,
|
|
@@ -607,7 +608,15 @@ function closeCandidateSlugs(hypoDir, dates) {
|
|
|
607
608
|
}
|
|
608
609
|
for (const d of dates) {
|
|
609
610
|
const re = new RegExp('^## \\[' + escapeRegExp(d) + '\\] session \\| (\\S+)', 'gm');
|
|
610
|
-
for (const m of content.matchAll(re))
|
|
611
|
+
for (const m of content.matchAll(re)) {
|
|
612
|
+
// B-1: only real projects are close candidates. A malformed log heading
|
|
613
|
+
// (`## [d] session | hypomnema:`) or a stale slug yields a ghost token
|
|
614
|
+
// that no longer maps to a `projects/<slug>/` directory — gating on disk
|
|
615
|
+
// keeps it out of the dangling-close set. Directory (not bare-exists)
|
|
616
|
+
// mirrors the apply-path project check (crystallize.mjs:193).
|
|
617
|
+
const dir = join(projectsDir, m[1]);
|
|
618
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) slugs.add(m[1]);
|
|
619
|
+
}
|
|
611
620
|
}
|
|
612
621
|
}
|
|
613
622
|
return slugs;
|
|
@@ -675,7 +684,29 @@ function hasTodayCloseActivity(hypoDir, project, dates) {
|
|
|
675
684
|
* dates: string[], fallback: boolean, primary: string|null,
|
|
676
685
|
* project: string|null, stale: string[], missing: string[]}}
|
|
677
686
|
*/
|
|
678
|
-
export function sessionCloseGlobalStatus(hypoDir) {
|
|
687
|
+
export function sessionCloseGlobalStatus(hypoDir, opts = {}) {
|
|
688
|
+
// projectOverride (check-only): the caller (`crystallize --check-session-close
|
|
689
|
+
// --project=<slug>`) wants THIS project's close status, not the recency pick —
|
|
690
|
+
// bypass discovery and report the single project, preserving the global return
|
|
691
|
+
// shape. NEVER threaded from a marker-writing path (--mark / apply auto-marker /
|
|
692
|
+
// PreCompact): those stay global so the marker == compact-ready invariant holds
|
|
693
|
+
// (ADR 0047, codex design review). A green status here is a project-scoped
|
|
694
|
+
// diagnostic, not the global compact-readiness verdict.
|
|
695
|
+
if (opts.projectOverride) {
|
|
696
|
+
const s = sessionCloseFileStatus(hypoDir, { projectOverride: opts.projectOverride });
|
|
697
|
+
return {
|
|
698
|
+
ok: s.ok,
|
|
699
|
+
projects: s.project
|
|
700
|
+
? [{ project: s.project, ok: s.ok, stale: s.stale, missing: s.missing }]
|
|
701
|
+
: [],
|
|
702
|
+
dates: freshDates(),
|
|
703
|
+
fallback: false,
|
|
704
|
+
primary: s.project,
|
|
705
|
+
project: s.project,
|
|
706
|
+
stale: s.stale,
|
|
707
|
+
missing: s.missing,
|
|
708
|
+
};
|
|
709
|
+
}
|
|
679
710
|
const dates = freshDates();
|
|
680
711
|
const recency = resolveActiveProject(hypoDir); // no cwd — close never picks by cwd
|
|
681
712
|
const todayActive = [...closeCandidateSlugs(hypoDir, dates)].filter((p) =>
|
|
@@ -748,6 +779,21 @@ function deriveLogTitle(tail) {
|
|
|
748
779
|
return t.replace(/^\s*[—:-]\s*/, '').trim(); // drop a leading separator
|
|
749
780
|
}
|
|
750
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Build the canonical root log.md entry for ONE session-log heading. Single
|
|
784
|
+
* source of truth for the derived-entry format, shared by the global Stop-hook
|
|
785
|
+
* derive (deriveRootLogEntries) and apply's direct per-close write (B-1), so the
|
|
786
|
+
* two paths never drift on the `→ [[projects/<slug>/hot]]` pointer or spacing.
|
|
787
|
+
* `headingTail` is the text AFTER `## [date]` in the authored session-log heading.
|
|
788
|
+
* @returns {{ heading: string, block: string }} heading = the `## [date] session
|
|
789
|
+
* | <slug>` line used for exact-line dedup; block = the full two-line entry.
|
|
790
|
+
*/
|
|
791
|
+
export function rootLogEntry(slug, date, headingTail) {
|
|
792
|
+
const title = deriveLogTitle(headingTail);
|
|
793
|
+
const heading = `## [${date}] session | ${slug}` + (title ? ` — ${title}` : '');
|
|
794
|
+
return { heading, block: `${heading}\n→ [[projects/${slug}/hot]]` };
|
|
795
|
+
}
|
|
796
|
+
|
|
751
797
|
/**
|
|
752
798
|
* Append any missing root log.md `## [date] session | <slug>` entries derived from
|
|
753
799
|
* each today-active project's session-log heading(s). Idempotent: dedups on the
|
|
@@ -811,11 +857,10 @@ export function deriveRootLogEntries(hypoDir) {
|
|
|
811
857
|
const headingRe = new RegExp('^#{1,6} \\[' + escapeRegExp(date) + '\\]\\s*(.*)$', 'gm');
|
|
812
858
|
let m;
|
|
813
859
|
while ((m = headingRe.exec(slog)) !== null) {
|
|
814
|
-
const
|
|
815
|
-
const heading = `## [${date}] session | ${slug}` + (title ? ` — ${title}` : '');
|
|
860
|
+
const { heading, block } = rootLogEntry(slug, date, m[1]);
|
|
816
861
|
if (seenHeadings.has(heading)) continue; // exact-line dedup (log.md + queued)
|
|
817
862
|
seenHeadings.add(heading);
|
|
818
|
-
additions.push(
|
|
863
|
+
additions.push(block);
|
|
819
864
|
}
|
|
820
865
|
}
|
|
821
866
|
}
|
|
@@ -1560,23 +1605,40 @@ export function closeFileTargets(hypoDir) {
|
|
|
1560
1605
|
* evidence file for EVERY freshDate (not just dates[0]) so the lint scope matches
|
|
1561
1606
|
* what sessionCloseFileStatus actually checks across a local/UTC date boundary.
|
|
1562
1607
|
*/
|
|
1608
|
+
// The lint-scope target set for ONE project's close: the shared root files
|
|
1609
|
+
// (hot.md / log.md) plus that project's mandatory close files — session-state,
|
|
1610
|
+
// project hot, and each fresh date's session-log evidence file. Used both by
|
|
1611
|
+
// `--check-session-close --project=<slug>` (a project-scoped diagnostic — see
|
|
1612
|
+
// precompactGateStatus opts.projectOverride) and as the per-project building
|
|
1613
|
+
// block of closeFileTargetsGlobal, so the two scopes stay identical per project.
|
|
1614
|
+
export function closeFileTargetsForProject(hypoDir, slug) {
|
|
1615
|
+
const dates = freshDates();
|
|
1616
|
+
const out = new Set(['hot.md', 'log.md']);
|
|
1617
|
+
out.add(`projects/${slug}/session-state.md`);
|
|
1618
|
+
out.add(`projects/${slug}/hot.md`);
|
|
1619
|
+
// Scope to the file each date's freshness is PROVEN by (daily shard or, via
|
|
1620
|
+
// fallback, the legacy monthly), so a corrupt evidence file can't pass the
|
|
1621
|
+
// gate while its lint error is demoted to an out-of-scope notice.
|
|
1622
|
+
for (const d of dates) out.add(sessionLogScopePath(hypoDir, slug, d));
|
|
1623
|
+
return out;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1563
1626
|
export function closeFileTargetsGlobal(hypoDir) {
|
|
1564
1627
|
const dates = freshDates();
|
|
1565
1628
|
const out = new Set(['hot.md', 'log.md']);
|
|
1566
1629
|
let active = [...closeCandidateSlugs(hypoDir, dates)].filter((p) =>
|
|
1567
1630
|
hasTodayCloseActivity(hypoDir, p, dates),
|
|
1568
1631
|
);
|
|
1632
|
+
// No project closed today → fall back to the recency project (mirrors
|
|
1633
|
+
// sessionCloseGlobalStatus's own fallback at the top of this file), so the lint
|
|
1634
|
+
// scope never narrows below the close-status scope. Dropping this (root-only
|
|
1635
|
+
// when active=[]) would re-open the very gap closeFileTargetsForProject closes.
|
|
1569
1636
|
if (active.length === 0) {
|
|
1570
1637
|
const recency = resolveActiveProject(hypoDir);
|
|
1571
1638
|
if (recency) active = [recency];
|
|
1572
1639
|
}
|
|
1573
1640
|
for (const p of active) {
|
|
1574
|
-
out.add(
|
|
1575
|
-
out.add(`projects/${p}/hot.md`);
|
|
1576
|
-
// Scope to the file each date's freshness is PROVEN by (daily shard or, via
|
|
1577
|
-
// fallback, the legacy monthly), so a corrupt evidence file can't pass the
|
|
1578
|
-
// gate while its lint error is demoted to an out-of-scope notice.
|
|
1579
|
-
for (const d of dates) out.add(sessionLogScopePath(hypoDir, p, d));
|
|
1641
|
+
for (const f of closeFileTargetsForProject(hypoDir, p)) out.add(f);
|
|
1580
1642
|
}
|
|
1581
1643
|
return out;
|
|
1582
1644
|
}
|
|
@@ -1629,8 +1691,18 @@ export function partitionLintScope(findings, scope) {
|
|
|
1629
1691
|
* opts.transcriptPath to widen it to the session's edited files exactly as the
|
|
1630
1692
|
* hook does.
|
|
1631
1693
|
*
|
|
1694
|
+
* opts.projectOverride (CHECK-ONLY) narrows BOTH the close status and the lint
|
|
1695
|
+
* scope to a single project, for `--check-session-close --project=<slug>`. A
|
|
1696
|
+
* green result is then a project-scoped diagnostic, NOT the global compact-ready
|
|
1697
|
+
* verdict (ADR 0046) — the caller must surface the scope. It is NEVER passed from
|
|
1698
|
+
* a marker-writing path (--mark / apply auto-marker / PreCompact); those stay
|
|
1699
|
+
* global so a marker can't attest compact-ready while PreCompact re-checks red
|
|
1700
|
+
* (the marker == compact-ready invariant, ADR 0047 / codex design review). When a
|
|
1701
|
+
* log-only marker governs the session, log-only mode wins and projectOverride is
|
|
1702
|
+
* ignored.
|
|
1703
|
+
*
|
|
1632
1704
|
* @param {string} hypoDir
|
|
1633
|
-
* @param {{lintScope?: Iterable<string>, transcriptPath?: string|null, claudeHome?: string}} [opts]
|
|
1705
|
+
* @param {{lintScope?: Iterable<string>, transcriptPath?: string|null, claudeHome?: string, projectOverride?: string|null}} [opts]
|
|
1634
1706
|
* @returns {{ok: boolean, close: object, blockers: {type:string,reason:string}[], notices: {type:string,reason:string}[], driftTargets: string[], skipped: {lint:boolean, feedback:boolean}}}
|
|
1635
1707
|
*/
|
|
1636
1708
|
export function precompactGateStatus(hypoDir, opts = {}) {
|
|
@@ -1693,7 +1765,9 @@ export function precompactGateStatus(hypoDir, opts = {}) {
|
|
|
1693
1765
|
});
|
|
1694
1766
|
}
|
|
1695
1767
|
} else {
|
|
1696
|
-
close
|
|
1768
|
+
// projectOverride narrows the close status to one project (check-only); a
|
|
1769
|
+
// marker-writing caller never sets it, so the marker path stays global.
|
|
1770
|
+
close = sessionCloseGlobalStatus(hypoDir, { projectOverride: opts.projectOverride });
|
|
1697
1771
|
if (!close.ok) {
|
|
1698
1772
|
blockers.push({
|
|
1699
1773
|
type: 'close',
|
|
@@ -1744,8 +1818,16 @@ export function precompactGateStatus(hypoDir, opts = {}) {
|
|
|
1744
1818
|
// mandatory files in and re-introduce the cross-project attribution. The
|
|
1745
1819
|
// session's own transcript-touched files are still added below (a log-only
|
|
1746
1820
|
// session is accountable for the wiki files it actually edited).
|
|
1821
|
+
// Lint scope: explicit opts.lintScope wins; else log-only uses the shared
|
|
1822
|
+
// root files only; else projectOverride narrows to that one project's close
|
|
1823
|
+
// files (matching the narrowed close status above); else the global set.
|
|
1747
1824
|
const scope = new Set(
|
|
1748
|
-
opts.lintScope ||
|
|
1825
|
+
opts.lintScope ||
|
|
1826
|
+
(logOnly
|
|
1827
|
+
? ['hot.md', 'log.md']
|
|
1828
|
+
: opts.projectOverride
|
|
1829
|
+
? closeFileTargetsForProject(hypoDir, opts.projectOverride)
|
|
1830
|
+
: closeFileTargetsGlobal(hypoDir)),
|
|
1749
1831
|
);
|
|
1750
1832
|
if (opts.transcriptPath && existsSync(opts.transcriptPath)) {
|
|
1751
1833
|
for (const f of extractTouchedWikiFiles(opts.transcriptPath, hypoDir)) scope.add(f);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hypomnema",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|