@pareto4/pp-core 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/NOTICE +23 -0
- package/README.md +10 -1
- package/bin/install.js +34 -1
- package/gemini-extension.json +1 -1
- package/package.json +8 -3
- package/pareto-skills/pp-project-setup/SKILL.md +32 -440
- package/pp-core/bin/lib/capability-state.cjs +13 -7
- package/pp-core/bin/lib/config.cjs +7 -22
- package/pp-core/bin/lib/init.cjs +4 -4
- package/pp-core/bin/lib/installer-migration-report.cjs +1 -1
- package/pp-core/bin/lib/loop-resolver.cjs +6 -0
- package/pp-core/bin/lib/profile-output.cjs +4 -2
- package/pp-core/bin/lib/runtime-name-policy.cjs +0 -1
- package/pp-core/bin/lib/surface.cjs +2 -2
- package/pp-core/bin/shared/config-schema.manifest.json +4 -0
- package/pp-core/references/workstream-flag.md +13 -0
- package/pp-core/workflows/discovery-sprint.md +409 -16
- package/pp-core/workflows/discovery-to-build/steps/architecture-deliverables.md +250 -0
- package/pp-core/workflows/discovery-to-build.md +218 -48
- package/pp-core/workflows/discuss-phase/modes/chain.md +5 -0
- package/pp-core/workflows/discuss-phase.md +4 -0
- package/pp-core/workflows/execute-phase.md +4 -0
- package/pp-core/workflows/new-milestone.md +4 -0
- package/pp-core/workflows/plan-phase.md +4 -0
- package/pp-core/workflows/progress.md +4 -0
- package/pp-core/workflows/quick.md +4 -0
- package/pp-core/workflows/resume-project.md +4 -0
- package/pp-core/workflows/ship.md +4 -0
- package/pp-core/workflows/transition.md +4 -0
- package/pp-core/workflows/validate-phase.md +4 -0
- package/scripts/changeset/lint.cjs +20 -5
- package/scripts/lint-docs-required.cjs +10 -3
- package/scripts/release-channel.cjs +58 -0
- package/scripts/release-tarball-smoke.cjs +4 -0
- package/scripts/verify-opengsd-sweep.cjs +12 -0
- package/scripts/verify-rename-guard.cjs +333 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* release-channel.cjs
|
|
6
|
+
*
|
|
7
|
+
* Pure decision logic mapping a git release tag to an npm dist-tag publish
|
|
8
|
+
* channel. Given a tag like `v1.9.0-rc.1` returns `'next'`; given a clean
|
|
9
|
+
* `v1.8.1` returns `'latest'`. Fails closed (returns `null` / CLI exits 1) on
|
|
10
|
+
* any unrecognized or malformed input instead of defaulting to `'latest'` —
|
|
11
|
+
* the production-facing channel (ASVS V5 tampering concern: `CI_COMMIT_TAG`
|
|
12
|
+
* is attacker-influenceable if a bad tag is pushed). Matching is exhaustive
|
|
13
|
+
* and full-string anchored: anything that isn't an exact `X.Y.Z` release or
|
|
14
|
+
* an exact `X.Y.Z-(rc|beta|alpha).N` prerelease returns null — no partial or
|
|
15
|
+
* heuristic match ever resolves to `'latest'`.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node scripts/release-channel.cjs "$CI_COMMIT_TAG" # prints channel, exit 0
|
|
19
|
+
* node scripts/release-channel.cjs # reads CI_COMMIT_TAG env
|
|
20
|
+
* argv (process.argv[2]) is preferred over the CI_COMMIT_TAG env var so the
|
|
21
|
+
* module is deterministic under its own tests even when CI_COMMIT_TAG is
|
|
22
|
+
* set in the ambient environment (as it always is on a real GitLab tag
|
|
23
|
+
* pipeline).
|
|
24
|
+
* (unrecognized/empty tag) -> stderr message, exit 1, no stdout
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
28
|
+
|
|
29
|
+
// Exact release version: no prerelease suffix, no build metadata, no trailing
|
|
30
|
+
// garbage.
|
|
31
|
+
const RELEASE_RE = /^\d+\.\d+\.\d+$/;
|
|
32
|
+
|
|
33
|
+
// Exact prerelease version: matches upstream gsd-core's own prerelease convention: -rc.N / -beta.N / -alpha.N — gsd-allow-legacy-name
|
|
34
|
+
const PRERELEASE_RE = /^\d+\.\d+\.\d+-(?:rc|beta|alpha)\.\d+$/;
|
|
35
|
+
|
|
36
|
+
function channelForTag(tag) {
|
|
37
|
+
if (typeof tag !== 'string' || tag.length === 0) return null;
|
|
38
|
+
const version = tag.replace(/^v/, '');
|
|
39
|
+
if (RELEASE_RE.test(version)) return 'latest';
|
|
40
|
+
if (PRERELEASE_RE.test(version)) return 'next';
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { channelForTag, PRERELEASE_RE };
|
|
45
|
+
|
|
46
|
+
function cliMain() {
|
|
47
|
+
const tag = process.argv[2] || process.env.CI_COMMIT_TAG || '';
|
|
48
|
+
const channel = channelForTag(tag);
|
|
49
|
+
if (!channel) {
|
|
50
|
+
throw new ExitError(1, `release-channel: unrecognized tag "${tag}"`);
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(channel + '\n');
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (require.main === module) {
|
|
57
|
+
runMain(cliMain);
|
|
58
|
+
}
|
|
@@ -756,6 +756,10 @@ module.exports = {
|
|
|
756
756
|
locateLegacyGsdProgress,
|
|
757
757
|
driveInstaller,
|
|
758
758
|
findInstallerBin,
|
|
759
|
+
// 20-02 Task 1: expose the installed pp-tools bin resolver so the
|
|
760
|
+
// REL-02 discuss->plan->execute smoke test can resolve it without
|
|
761
|
+
// re-implementing bin resolution (RESEARCH.md Don't-Hand-Roll).
|
|
762
|
+
findGsdToolsBin, // gsd-allow-legacy-name
|
|
759
763
|
};
|
|
760
764
|
|
|
761
765
|
if (require.main === module) {
|
|
@@ -41,6 +41,16 @@ const ROOT = path.resolve(__dirname, '..');
|
|
|
41
41
|
/** Matches any recognised OpenGSD spelling in a line of text. */
|
|
42
42
|
const OPENGSD_RE = /opengsd|open-gsd/i;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* MIT-attribution carve-out (line-level): the README's License & Attribution
|
|
46
|
+
* section must name the upstream project — the same D-07 class as
|
|
47
|
+
* LICENSE/NOTICE, but scoped to attribution lines only so the rest of the
|
|
48
|
+
* README stays on the scanned surface.
|
|
49
|
+
*/
|
|
50
|
+
const ATTRIBUTION_LINE_RE =
|
|
51
|
+
/hard fork of \[gsd-core\]\(https:\/\/github\.com\/open-gsd\/gsd-core\)/;
|
|
52
|
+
const README_FILE = path.join(__dirname, '..', 'README.md');
|
|
53
|
+
|
|
44
54
|
/**
|
|
45
55
|
* Broader brand pattern for npm-facing manifest metadata (package.json
|
|
46
56
|
* `description` / `author`). Catches the upstream product name ("GSD Core")
|
|
@@ -185,6 +195,8 @@ function scanFiles(files) {
|
|
|
185
195
|
const lines = content.split('\n');
|
|
186
196
|
for (let i = 0; i < lines.length; i++) {
|
|
187
197
|
if (OPENGSD_RE.test(lines[i])) {
|
|
198
|
+
// README attribution line: MIT-required upstream credit (D-07 class).
|
|
199
|
+
if (abs === README_FILE && ATTRIBUTION_LINE_RE.test(lines[i])) continue;
|
|
188
200
|
hits.push({
|
|
189
201
|
file: path.relative(ROOT, abs),
|
|
190
202
|
line: i + 1,
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* verify-rename-guard.cjs — REBR-04 rename-integrity ratchet guard.
|
|
6
|
+
*
|
|
7
|
+
* The gsd → pp rename is functionally finished (Phase 15/16/17); this guard
|
|
8
|
+
* is a CI ratchet that stops it from regressing — it is explicitly NOT a
|
|
9
|
+
* cosmetic sweep of the ~1,186 files that still legitimately contain `gsd`
|
|
10
|
+
* (runtime contracts, historical corpora, migration provenance, etc.).
|
|
11
|
+
*
|
|
12
|
+
* Design (D-01..D-11, .planning/phases/18-rename-integrity-guard/18-CONTEXT.md):
|
|
13
|
+
* D-01 Baseline ratchet: a committed baseline captures existing gsd hits;
|
|
14
|
+
* CI fails only on a NEW (novel) hit, never on the existing debt.
|
|
15
|
+
* D-02 Line-fingerprint precision: baseline keys on (file, normalized line
|
|
16
|
+
* content), not (file, line NUMBER) — see Pitfall 1 below.
|
|
17
|
+
* D-03 Case-insensitive substring match: RENAME_RE = /gsd/i, deliberately
|
|
18
|
+
* broad; carve-outs and keep-tokens do the narrowing.
|
|
19
|
+
* D-04 Growth-only enforcement: shrinking/removing a baselined gsd
|
|
20
|
+
* reference NEVER fails the guard — it is a harmless upper bound.
|
|
21
|
+
* D-07 Whole tracked tree: scans every git-tracked text file (skip
|
|
22
|
+
* binaries), not a curated subset.
|
|
23
|
+
* D-09 Single-source allowlist authority: tests/fixtures/gsd-keep-tokens.cjs
|
|
24
|
+
* (isFrozen/isAttribution for whole-file carve-outs, isKeepTokenLine
|
|
25
|
+
* for per-line exemptions). Never fork a parallel token list here.
|
|
26
|
+
* D-10 Whole-file/dir carve-outs (installer-migrations, .changeset/archived,
|
|
27
|
+
* the fixture itself) live in gsd-keep-tokens.cjs's FROZEN_PATHS.
|
|
28
|
+
*
|
|
29
|
+
* This plan (18-03) does NOT generate/commit the real-tree baseline — that is
|
|
30
|
+
* plan 04 (wave 2), once the tree reflects REBR-05's functional fixes. A
|
|
31
|
+
* missing baseline file here is treated as an EMPTY baseline, which lets the
|
|
32
|
+
* subprocess test wrapper fully control the baseline via env-var redirection.
|
|
33
|
+
*
|
|
34
|
+
* Exit codes:
|
|
35
|
+
* 0 — no novel gsd reference found (or --update-baseline succeeded)
|
|
36
|
+
* 1 — one or more novel gsd references found outside the baseline
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* node scripts/verify-rename-guard.cjs
|
|
40
|
+
* node scripts/verify-rename-guard.cjs --update-baseline
|
|
41
|
+
*
|
|
42
|
+
* Env overrides (test-only, mirrors GSD_LINT_LEGACY_REPO_ROOT in
|
|
43
|
+
* scripts/lint-legacy-dir-name.cjs):
|
|
44
|
+
* RENAME_GUARD_REPO_ROOT — redirect the whole-tree scan to a temp git repo
|
|
45
|
+
* RENAME_GUARD_BASELINE_PATH — redirect the baseline file to a temp path
|
|
46
|
+
*
|
|
47
|
+
* Exports:
|
|
48
|
+
* runScan(opts?) → { byFile, hitList, baseline, novel, fileCount, repoRoot, baselinePath }
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const { execFileSync } = require('node:child_process');
|
|
52
|
+
const fs = require('node:fs');
|
|
53
|
+
const path = require('node:path');
|
|
54
|
+
|
|
55
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
56
|
+
const keepTokens = require('../tests/fixtures/gsd-keep-tokens.cjs');
|
|
57
|
+
|
|
58
|
+
// D-03: deliberately broad case-insensitive substring match — carve-outs and
|
|
59
|
+
// keep-tokens (not a narrower pattern) do the exemption work.
|
|
60
|
+
const RENAME_RE = /gsd/i;
|
|
61
|
+
|
|
62
|
+
// RENAME_GUARD_REPO_ROOT lets tests redirect the guard to a temporary git repo
|
|
63
|
+
// without touching the real working tree (mirrors GSD_LINT_LEGACY_REPO_ROOT).
|
|
64
|
+
const REPO_ROOT = process.env.RENAME_GUARD_REPO_ROOT
|
|
65
|
+
? path.resolve(process.env.RENAME_GUARD_REPO_ROOT)
|
|
66
|
+
: path.resolve(__dirname, '..');
|
|
67
|
+
|
|
68
|
+
// RENAME_GUARD_BASELINE_PATH lets tests redirect the baseline file to a
|
|
69
|
+
// temporary path. The real default lives under tests/ but is NOT committed
|
|
70
|
+
// by this plan (18-03) — plan 04 generates and commits it against the final
|
|
71
|
+
// post-REBR-05 tree. A missing file is treated as an empty baseline.
|
|
72
|
+
const DEFAULT_BASELINE_PATH = path.join(__dirname, '..', 'tests', 'rename-guard-baseline.json');
|
|
73
|
+
const BASELINE_PATH = process.env.RENAME_GUARD_BASELINE_PATH
|
|
74
|
+
? path.resolve(process.env.RENAME_GUARD_BASELINE_PATH)
|
|
75
|
+
: DEFAULT_BASELINE_PATH;
|
|
76
|
+
|
|
77
|
+
// A NUL byte between the relative path and the normalized line text — cannot
|
|
78
|
+
// occur in either half of a text-file scan, so it is a safe composite-key
|
|
79
|
+
// separator for the Set-diff below.
|
|
80
|
+
const KEY_SEP = '\u0000';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* isBinary(fullPath) — NUL-byte probe on the first 512 bytes.
|
|
84
|
+
* Copied verbatim from scripts/lint-legacy-dir-name.cjs's isBinary().
|
|
85
|
+
*
|
|
86
|
+
* @param {string} fullPath absolute path to probe
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isBinary(fullPath) {
|
|
90
|
+
let fd;
|
|
91
|
+
try {
|
|
92
|
+
fd = fs.openSync(fullPath, 'r');
|
|
93
|
+
const buf = Buffer.allocUnsafe(512);
|
|
94
|
+
const bytesRead = fs.readSync(fd, buf, 0, 512, 0);
|
|
95
|
+
return buf.subarray(0, bytesRead).includes(0);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
} finally {
|
|
99
|
+
if (fd != null) {
|
|
100
|
+
try { fs.closeSync(fd); } catch { /* best-effort */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* normalizeLine(line) — content-key normalization (D-02, Pitfall 4).
|
|
107
|
+
*
|
|
108
|
+
* `String.prototype.trimEnd()` strips trailing whitespace AND trailing line
|
|
109
|
+
* terminators (including a lone `\r` left behind when a CRLF file is split
|
|
110
|
+
* on `\n`), so a single trimEnd() call both trims trailing whitespace and
|
|
111
|
+
* neutralizes the CRLF/LF cross-platform baseline-desync pitfall documented
|
|
112
|
+
* in tests/workflow-size-budget.test.cjs's #683 regression.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} line
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
function normalizeLine(line) {
|
|
118
|
+
return line.trimEnd();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* collectHits(repoRoot) — enumerate the whole tracked tree and collect every
|
|
123
|
+
* RENAME_RE hit not exempted by a whole-file carve-out (isFrozen/isAttribution)
|
|
124
|
+
* or a per-line keep-token (isKeepTokenLine).
|
|
125
|
+
*
|
|
126
|
+
* @param {string} repoRoot absolute path to scan (git ls-files cwd)
|
|
127
|
+
* @returns {{
|
|
128
|
+
* byFile: Object<string, Set<string>>,
|
|
129
|
+
* hitList: Array<{file: string, line: number, text: string}>,
|
|
130
|
+
* fileCount: number,
|
|
131
|
+
* }}
|
|
132
|
+
*/
|
|
133
|
+
function collectHits(repoRoot) {
|
|
134
|
+
let trackedFiles;
|
|
135
|
+
try {
|
|
136
|
+
trackedFiles = execFileSync('git', ['ls-files'], { cwd: repoRoot, encoding: 'utf8' })
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((f) => f.trim())
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw new ExitError(1, 'ERROR verify-rename-guard: git ls-files failed: ' + err.message);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** @type {Object<string, Set<string>>} */
|
|
145
|
+
const byFile = {};
|
|
146
|
+
/** @type {Array<{file: string, line: number, text: string}>} */
|
|
147
|
+
const hitList = [];
|
|
148
|
+
|
|
149
|
+
for (const relPath of trackedFiles) {
|
|
150
|
+
const fullPath = path.join(repoRoot, relPath);
|
|
151
|
+
|
|
152
|
+
// D-09/D-10 whole-file/dir carve-out layer — single-source authority.
|
|
153
|
+
if (keepTokens.isFrozen(fullPath) || keepTokens.isAttribution(fullPath)) continue;
|
|
154
|
+
|
|
155
|
+
if (isBinary(fullPath)) continue;
|
|
156
|
+
|
|
157
|
+
let content;
|
|
158
|
+
try {
|
|
159
|
+
content = fs.readFileSync(fullPath, 'utf8');
|
|
160
|
+
} catch {
|
|
161
|
+
// Unreadable file (permissions, race with deletion, etc.) — skip silently.
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const lines = content.split('\n');
|
|
166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
167
|
+
const raw = lines[i];
|
|
168
|
+
if (!RENAME_RE.test(raw)) continue;
|
|
169
|
+
|
|
170
|
+
// D-09 per-line exemption — single-source authority.
|
|
171
|
+
if (keepTokens.isKeepTokenLine(raw)) continue;
|
|
172
|
+
|
|
173
|
+
const normalized = normalizeLine(raw);
|
|
174
|
+
if (!byFile[relPath]) byFile[relPath] = new Set();
|
|
175
|
+
// Pitfall 3 (accepted limitation): two identical-content gsd lines in the
|
|
176
|
+
// same file collapse to one Set entry — occurrence count is not tracked.
|
|
177
|
+
// This is an intentional D-02 trade-off, not a bug — do not "fix" it into
|
|
178
|
+
// a fragile per-occurrence counter.
|
|
179
|
+
byFile[relPath].add(normalized);
|
|
180
|
+
hitList.push({ file: relPath, line: i + 1, text: normalized });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { byFile, hitList, fileCount: trackedFiles.length };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* loadBaseline(baselinePath) — read the committed baseline JSON.
|
|
189
|
+
* A missing or unreadable/invalid file is treated as an EMPTY baseline (this
|
|
190
|
+
* plan does not commit a real-tree baseline; plan 04 does).
|
|
191
|
+
*
|
|
192
|
+
* @param {string} baselinePath
|
|
193
|
+
* @returns {Object<string, string[]>}
|
|
194
|
+
*/
|
|
195
|
+
function loadBaseline(baselinePath) {
|
|
196
|
+
try {
|
|
197
|
+
const raw = fs.readFileSync(baselinePath, 'utf8');
|
|
198
|
+
return JSON.parse(raw);
|
|
199
|
+
} catch {
|
|
200
|
+
return {};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* serializeBaseline(byFile) — stable sorted serializer for --update-baseline.
|
|
206
|
+
* Mirrors scripts/update-size-baseline.cjs's serializeBaseline() shape:
|
|
207
|
+
* sorted file keys, sorted per-file line arrays, 2-space indent, trailing \n.
|
|
208
|
+
*
|
|
209
|
+
* @param {Object<string, Set<string>|string[]>} byFile
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
function serializeBaseline(byFile) {
|
|
213
|
+
const sorted = {};
|
|
214
|
+
for (const key of Object.keys(byFile).sort()) {
|
|
215
|
+
sorted[key] = [...byFile[key]].sort();
|
|
216
|
+
}
|
|
217
|
+
return JSON.stringify(sorted, null, 2) + '\n';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* computeNovel(byFile, baseline) — D-04 growth-only Set-diff.
|
|
222
|
+
*
|
|
223
|
+
* Returns ONLY current-minus-known (novel) composite keys — NEVER
|
|
224
|
+
* known-minus-current (stale/shrink), because shrinking or removing a
|
|
225
|
+
* baselined gsd reference is always safe (D-04). This deliberately does NOT
|
|
226
|
+
* reuse scripts/lib/allowlist-ratchet.cjs's assertWithinAllowlist() unmodified,
|
|
227
|
+
* since that helper also fails on stale entries (see Pitfall 2 in RESEARCH.md).
|
|
228
|
+
*
|
|
229
|
+
* @param {Object<string, Set<string>>} byFile current scan result
|
|
230
|
+
* @param {Object<string, string[]>} baseline loaded baseline
|
|
231
|
+
* @returns {string[]} sorted array of "relPath normalizedLine" keys
|
|
232
|
+
*/
|
|
233
|
+
function computeNovel(byFile, baseline) {
|
|
234
|
+
const currentKeys = new Set();
|
|
235
|
+
for (const [file, lines] of Object.entries(byFile)) {
|
|
236
|
+
for (const line of lines) currentKeys.add(file + KEY_SEP + line);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const knownKeys = new Set();
|
|
240
|
+
for (const [file, lines] of Object.entries(baseline)) {
|
|
241
|
+
for (const line of lines) knownKeys.add(file + KEY_SEP + line);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return [...currentKeys].filter((k) => !knownKeys.has(k)).sort();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* runScan(opts?) — pure scan entry point (no process.exit, no stdout/stderr
|
|
249
|
+
* writes). Consumed by the CLI wrapper (cliMain) and directly by tests.
|
|
250
|
+
*
|
|
251
|
+
* @param {object} [opts]
|
|
252
|
+
* @param {string} [opts.repoRoot] defaults to the module-level REPO_ROOT
|
|
253
|
+
* @param {string} [opts.baselinePath] defaults to the module-level BASELINE_PATH
|
|
254
|
+
* @returns {{
|
|
255
|
+
* byFile: Object<string, Set<string>>,
|
|
256
|
+
* hitList: Array<{file: string, line: number, text: string}>,
|
|
257
|
+
* baseline: Object<string, string[]>,
|
|
258
|
+
* novel: string[],
|
|
259
|
+
* fileCount: number,
|
|
260
|
+
* repoRoot: string,
|
|
261
|
+
* baselinePath: string,
|
|
262
|
+
* }}
|
|
263
|
+
*/
|
|
264
|
+
function runScan(opts) {
|
|
265
|
+
const repoRoot = (opts && opts.repoRoot) || REPO_ROOT;
|
|
266
|
+
const baselinePath = (opts && opts.baselinePath) || BASELINE_PATH;
|
|
267
|
+
|
|
268
|
+
const { byFile, hitList, fileCount } = collectHits(repoRoot);
|
|
269
|
+
const baseline = loadBaseline(baselinePath);
|
|
270
|
+
const novel = computeNovel(byFile, baseline);
|
|
271
|
+
|
|
272
|
+
return { byFile, hitList, baseline, novel, fileCount, repoRoot, baselinePath };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* splitKey(key) — inverse of the KEY_SEP composite key used by computeNovel().
|
|
277
|
+
* @param {string} key
|
|
278
|
+
* @returns {{file: string, text: string}}
|
|
279
|
+
*/
|
|
280
|
+
function splitKey(key) {
|
|
281
|
+
const idx = key.indexOf(KEY_SEP);
|
|
282
|
+
return { file: key.slice(0, idx), text: key.slice(idx + 1) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── CLI entry ────────────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
function cliMain() {
|
|
288
|
+
if (process.argv.includes('--update-baseline')) {
|
|
289
|
+
const { byFile, fileCount } = collectHits(REPO_ROOT);
|
|
290
|
+
fs.writeFileSync(BASELINE_PATH, serializeBaseline(byFile));
|
|
291
|
+
process.stdout.write(
|
|
292
|
+
'ok verify-rename-guard: baseline updated at ' +
|
|
293
|
+
path.relative(REPO_ROOT, BASELINE_PATH) +
|
|
294
|
+
' (' + fileCount + ' tracked file(s) scanned, ' +
|
|
295
|
+
Object.keys(byFile).length + ' file(s) with gsd reference(s))\n'
|
|
296
|
+
);
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const { novel, hitList, fileCount } = runScan();
|
|
301
|
+
|
|
302
|
+
if (novel.length === 0) {
|
|
303
|
+
process.stdout.write(
|
|
304
|
+
'ok verify-rename-guard: ' + fileCount + ' tracked file(s) checked, 0 novel gsd reference(s)\n'
|
|
305
|
+
);
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
'\nERROR verify-rename-guard: ' + novel.length +
|
|
311
|
+
' novel gsd reference(s) found (not in the committed baseline)\n\n'
|
|
312
|
+
);
|
|
313
|
+
for (const key of novel) {
|
|
314
|
+
const { file, text } = splitKey(key);
|
|
315
|
+
const hit = hitList.find((h) => h.file === file && h.text === text);
|
|
316
|
+
const lineNo = hit ? hit.line : '?';
|
|
317
|
+
process.stderr.write(' ' + file + ':' + lineNo + ' — ' + JSON.stringify(text) + '\n');
|
|
318
|
+
}
|
|
319
|
+
process.stderr.write('\n');
|
|
320
|
+
process.stderr.write(
|
|
321
|
+
'Fix at the source, add the `gsd-allow-legacy-name` marker on the line if the\n' +
|
|
322
|
+
'use is intentional, or run `node scripts/verify-rename-guard.cjs --update-baseline`\n' +
|
|
323
|
+
'if the reference is legitimately grandfathered.\n\n'
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
throw new ExitError(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
module.exports = { runScan, RENAME_RE, serializeBaseline, collectHits, loadBaseline, computeNovel };
|
|
330
|
+
|
|
331
|
+
if (require.main === module) {
|
|
332
|
+
runMain(cliMain);
|
|
333
|
+
}
|