@opengsd/gsd-core 1.20.1 → 1.21.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/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +234 -126
- package/commands/gsd/autonomous.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +450 -753
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +80 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +13 -11
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +11 -19
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +107 -31
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +91 -41
- package/gsd-core/bin/lib/state.cjs +446 -61
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +97 -17
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +3 -2
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +8 -5
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +97 -17
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates docs/CLI-SURFACE-MANIFEST.json from the CLI surface registry
|
|
6
|
+
* (`src/cli-surface.cts`, compiled to `gsd-core/bin/lib/cli-surface.cjs`) and the
|
|
7
|
+
* command aliases (`gsd-core/bin/lib/command-aliases.cjs`). ADR-4909 D2, #6087.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/gen-cli-surface-manifest.cjs # print the manifest to stdout
|
|
11
|
+
* node scripts/gen-cli-surface-manifest.cjs --write # rewrite the manifest file
|
|
12
|
+
* node scripts/gen-cli-surface-manifest.cjs --check # exit 1 if the manifest is missing/stale
|
|
13
|
+
* node scripts/gen-cli-surface-manifest.cjs --check --json # machine-readable report
|
|
14
|
+
* node scripts/gen-cli-surface-manifest.cjs --manifest-path <p> # test-only: target another file
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
23
|
+
const MANIFEST_REL = 'docs/CLI-SURFACE-MANIFEST.json';
|
|
24
|
+
const MANIFEST_PATH = path.join(ROOT, MANIFEST_REL);
|
|
25
|
+
const SURFACE_REL = 'gsd-core/bin/lib/cli-surface.cjs';
|
|
26
|
+
const SURFACE_PATH = path.join(ROOT, SURFACE_REL);
|
|
27
|
+
const ALIASES_PATH = path.join(ROOT, 'gsd-core/bin/lib/command-aliases.cjs');
|
|
28
|
+
|
|
29
|
+
const USAGE =
|
|
30
|
+
'Usage: node scripts/gen-cli-surface-manifest.cjs [--write | --check] [--json] [--manifest-path <path>]';
|
|
31
|
+
|
|
32
|
+
// Each compiled module the manifest reads, beside the source it is built from. The registry
|
|
33
|
+
// is read through its compiled form, so a stale compile would write or check a stale manifest.
|
|
34
|
+
const COMPILED_PAIRS = [
|
|
35
|
+
{ source: path.join(ROOT, 'src/cli-surface.cts'), compiled: SURFACE_PATH },
|
|
36
|
+
{
|
|
37
|
+
source: path.join(ROOT, 'src/command-arg-projection.cts'),
|
|
38
|
+
compiled: path.join(ROOT, 'gsd-core/bin/lib/command-arg-projection.cjs'),
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether the compiled file is exactly what the build would emit for the source: true or false,
|
|
44
|
+
* or null when typescript or the build options cannot be loaded (the caller falls back to mtime).
|
|
45
|
+
*/
|
|
46
|
+
function compiledMatchesSource(source, compiled, loadTypescript) {
|
|
47
|
+
try {
|
|
48
|
+
const ts = loadTypescript();
|
|
49
|
+
const config = ts.readConfigFile(path.join(ROOT, 'tsconfig.build.json'), ts.sys.readFile);
|
|
50
|
+
if (config.error !== undefined) return null;
|
|
51
|
+
// parseJsonConfigFileContent resolves `extends`; 18003 (no input files) is about the include globs, not the options.
|
|
52
|
+
const { options, errors } = ts.parseJsonConfigFileContent(config.config, ts.sys, ROOT);
|
|
53
|
+
if (errors.some((e) => e.code !== 18003)) return null;
|
|
54
|
+
const emitted = ts.transpileModule(fs.readFileSync(source, 'utf8'), { compilerOptions: options, fileName: source });
|
|
55
|
+
return emitted.outputText === fs.readFileSync(compiled, 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Why the compiled registry cannot be trusted, or null when it is fresh: a compiled file that
|
|
63
|
+
* is missing, or older than the source beside it, means `npm run build:lib` has not run since
|
|
64
|
+
* the source changed. A source that is not on disk (an installed package) is not checked.
|
|
65
|
+
* Pairs are compared one to one: tsc leaves an unchanged module's output alone, so a source that
|
|
66
|
+
* is newer by mtime only (a checkout, a touch) is judged by content: the source is transpiled
|
|
67
|
+
* with the repo's own typescript and the build options of tsconfig.build.json, and equal to the
|
|
68
|
+
* compiled text means fresh. When typescript cannot be loaded the mtime comparison decides.
|
|
69
|
+
*
|
|
70
|
+
* @param {Array<{source: string, compiled: string}>} [pairs]
|
|
71
|
+
* @param {() => object} [loadTypescript]
|
|
72
|
+
* @returns {string | null}
|
|
73
|
+
*/
|
|
74
|
+
function staleCompileProblem(pairs = COMPILED_PAIRS, loadTypescript = () => require('typescript')) {
|
|
75
|
+
for (const { source, compiled } of pairs) {
|
|
76
|
+
const rel = (p) => path.relative(ROOT, p) || p;
|
|
77
|
+
if (!fs.existsSync(compiled)) {
|
|
78
|
+
return `compiled artifact not found at ${rel(compiled)}. Run \`npm run build:lib\` first.`;
|
|
79
|
+
}
|
|
80
|
+
if (!fs.existsSync(source)) continue;
|
|
81
|
+
if (fs.statSync(source).mtimeMs > fs.statSync(compiled).mtimeMs) {
|
|
82
|
+
if (compiledMatchesSource(source, compiled, loadTypescript) === true) continue;
|
|
83
|
+
return (
|
|
84
|
+
`${rel(compiled)} is older than ${rel(source)}. Run \`npm run build:lib\` first ` +
|
|
85
|
+
'(if it emits nothing, delete tsconfig.build.tsbuildinfo and run it again).'
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function loadSurface() {
|
|
93
|
+
const problem = staleCompileProblem();
|
|
94
|
+
if (problem !== null) throw new ExitError(2, `gen-cli-surface-manifest: ${problem}`);
|
|
95
|
+
return require(SURFACE_PATH);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** canonical -> sorted, unique aliases, across every `*_COMMAND_ALIASES` array. */
|
|
99
|
+
function aliasMap() {
|
|
100
|
+
const mod = require(ALIASES_PATH);
|
|
101
|
+
const collected = new Map();
|
|
102
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
103
|
+
if (!key.endsWith('_COMMAND_ALIASES') || !Array.isArray(value)) continue;
|
|
104
|
+
for (const entry of value) {
|
|
105
|
+
if (!entry || typeof entry.canonical !== 'string') continue;
|
|
106
|
+
const set = collected.get(entry.canonical) ?? new Set();
|
|
107
|
+
for (const alias of entry.aliases ?? []) set.add(alias);
|
|
108
|
+
collected.set(entry.canonical, set);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return new Map([...collected].map(([canonical, set]) => [canonical, [...set].sort()]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sorted = (list) => [...(list ?? [])].sort();
|
|
115
|
+
|
|
116
|
+
function flagKinds(spec) {
|
|
117
|
+
const repeatable = new Set(spec.repeatableFlags ?? []);
|
|
118
|
+
const bare = new Set(spec.bareEmptyFlags ?? []);
|
|
119
|
+
const kinds = {};
|
|
120
|
+
for (const f of spec.valueFlags ?? []) {
|
|
121
|
+
kinds[f] = repeatable.has(f) ? 'repeatable' : bare.has(f) ? 'value-or-bare' : 'value';
|
|
122
|
+
}
|
|
123
|
+
for (const f of spec.booleanFlags ?? []) kinds[f] = 'boolean';
|
|
124
|
+
for (const f of spec.optionalValueFlags ?? []) kinds[f] = 'optional-value';
|
|
125
|
+
for (const f of spec.listFlags ?? []) kinds[f] = 'list';
|
|
126
|
+
for (const f of spec.multiwordFlags ?? []) kinds[f] = 'multiword';
|
|
127
|
+
return sortedObject(kinds);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function sortedObject(obj) {
|
|
131
|
+
const out = {};
|
|
132
|
+
for (const k of Object.keys(obj).sort()) out[k] = obj[k];
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildVerbEntry(verb, spec, aliases) {
|
|
137
|
+
const entry = {
|
|
138
|
+
verb,
|
|
139
|
+
aliases: sorted(aliases.get(verb)),
|
|
140
|
+
positionals: spec.positionals,
|
|
141
|
+
flags: flagKinds(spec),
|
|
142
|
+
required: sorted(spec.requiredFlags),
|
|
143
|
+
workstreamScoped: !!spec.workstreamScoped,
|
|
144
|
+
};
|
|
145
|
+
if (spec.freePositionals) entry.freePositionals = true;
|
|
146
|
+
if (spec.rejectShortFlags) entry.rejectShortFlags = true;
|
|
147
|
+
if ((spec.eqFlags ?? []).length > 0) entry.eq = sorted(spec.eqFlags);
|
|
148
|
+
if ((spec.nonEmptyFlags ?? []).length > 0) entry.nonEmpty = sorted(spec.nonEmptyFlags);
|
|
149
|
+
if ((spec.emptyValueFlags ?? []).length > 0) entry.emptyValue = sorted(spec.emptyValueFlags);
|
|
150
|
+
if ((spec.emptyEqFlags ?? []).length > 0) entry.emptyEq = sorted(spec.emptyEqFlags);
|
|
151
|
+
if (spec.passthroughAfter) entry.passthroughAfter = spec.passthroughAfter;
|
|
152
|
+
if (spec.thirdParty) entry.thirdParty = true;
|
|
153
|
+
return entry;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildManifest() {
|
|
157
|
+
const { CLI_SURFACE, GLOBAL_FLAGS, UNDECLARED_VERBS } = loadSurface();
|
|
158
|
+
const aliases = aliasMap();
|
|
159
|
+
const flags = {};
|
|
160
|
+
for (const f of GLOBAL_FLAGS.valueFlags ?? []) flags[f] = 'value';
|
|
161
|
+
for (const f of GLOBAL_FLAGS.booleanFlags ?? []) flags[f] = 'boolean';
|
|
162
|
+
const verbs = Object.keys(CLI_SURFACE)
|
|
163
|
+
.sort()
|
|
164
|
+
.map((verb) => buildVerbEntry(verb, CLI_SURFACE[verb], aliases));
|
|
165
|
+
return {
|
|
166
|
+
schema: 1,
|
|
167
|
+
global: {
|
|
168
|
+
flags: sortedObject(flags),
|
|
169
|
+
eq: sorted(GLOBAL_FLAGS.eqFlags),
|
|
170
|
+
help: [...(GLOBAL_FLAGS.helpTokens ?? [])],
|
|
171
|
+
rejected: [...(GLOBAL_FLAGS.rejectedTokens ?? [])],
|
|
172
|
+
},
|
|
173
|
+
verbs,
|
|
174
|
+
undeclared: sorted(UNDECLARED_VERBS),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function serialize(manifest) {
|
|
179
|
+
return JSON.stringify(manifest, null, 2) + '\n';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function diffVerbs(expected, actual) {
|
|
183
|
+
const exp = new Map(expected.verbs.map((v) => [v.verb, JSON.stringify(v)]));
|
|
184
|
+
const act = new Map((actual.verbs ?? []).map((v) => [v.verb, JSON.stringify(v)]));
|
|
185
|
+
const added = [...exp.keys()].filter((k) => !act.has(k));
|
|
186
|
+
const removed = [...act.keys()].filter((k) => !exp.has(k));
|
|
187
|
+
const changed = [...exp.keys()].filter((k) => act.has(k) && act.get(k) !== exp.get(k));
|
|
188
|
+
return { added, removed, changed };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* @param {{manifestPath?: string}} [opts]
|
|
193
|
+
* @returns {{ok: boolean, problems: Array<{kind: 'missing'|'stale'|'invalid-json', subject: string, message: string}>}}
|
|
194
|
+
*/
|
|
195
|
+
function checkReport({ manifestPath = MANIFEST_PATH } = {}) {
|
|
196
|
+
const problems = [];
|
|
197
|
+
if (!fs.existsSync(manifestPath)) {
|
|
198
|
+
problems.push({
|
|
199
|
+
kind: 'missing',
|
|
200
|
+
subject: manifestPath,
|
|
201
|
+
message: `${manifestPath} not found. Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
202
|
+
});
|
|
203
|
+
return { ok: false, problems };
|
|
204
|
+
}
|
|
205
|
+
const text = fs.readFileSync(manifestPath, 'utf8');
|
|
206
|
+
let actual;
|
|
207
|
+
try {
|
|
208
|
+
actual = JSON.parse(text);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
problems.push({
|
|
211
|
+
kind: 'invalid-json',
|
|
212
|
+
subject: manifestPath,
|
|
213
|
+
message: `${manifestPath} is not valid JSON (${err.message}). Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
214
|
+
});
|
|
215
|
+
return { ok: false, problems };
|
|
216
|
+
}
|
|
217
|
+
const expected = buildManifest();
|
|
218
|
+
if (serialize(expected) !== text) {
|
|
219
|
+
const { added, removed, changed } = diffVerbs(expected, actual);
|
|
220
|
+
const parts = [];
|
|
221
|
+
if (added.length) parts.push(`missing verbs: ${added.join(', ')}`);
|
|
222
|
+
if (removed.length) parts.push(`removed verbs: ${removed.join(', ')}`);
|
|
223
|
+
if (changed.length) parts.push(`changed verbs: ${changed.join(', ')}`);
|
|
224
|
+
if (!parts.length) parts.push('global/undeclared sections or formatting differ');
|
|
225
|
+
const subject = [...added, ...removed, ...changed][0] ?? 'global';
|
|
226
|
+
problems.push({
|
|
227
|
+
kind: 'stale',
|
|
228
|
+
subject,
|
|
229
|
+
message: `${manifestPath} is stale (${parts.join('; ')}). Run: node scripts/gen-cli-surface-manifest.cjs --write`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return { ok: problems.length === 0, problems };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseArgs(argv) {
|
|
236
|
+
const opts = { write: false, check: false, json: false, manifestPath: MANIFEST_PATH };
|
|
237
|
+
for (let i = 0; i < argv.length; i++) {
|
|
238
|
+
const arg = argv[i];
|
|
239
|
+
if (arg === '--write') opts.write = true;
|
|
240
|
+
else if (arg === '--check') opts.check = true;
|
|
241
|
+
else if (arg === '--json') opts.json = true;
|
|
242
|
+
else if (arg === '--manifest-path') {
|
|
243
|
+
const value = argv[i + 1];
|
|
244
|
+
if (value === undefined) throw new ExitError(2, `--manifest-path requires a path argument.\n${USAGE}`);
|
|
245
|
+
opts.manifestPath = path.resolve(value);
|
|
246
|
+
i++;
|
|
247
|
+
} else {
|
|
248
|
+
throw new ExitError(2, `unknown argument: ${arg}\n${USAGE}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (opts.write && opts.check) throw new ExitError(2, `--write and --check are mutually exclusive.\n${USAGE}`);
|
|
252
|
+
return opts;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function main() {
|
|
256
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
257
|
+
|
|
258
|
+
if (opts.write) {
|
|
259
|
+
const manifest = buildManifest();
|
|
260
|
+
fs.mkdirSync(path.dirname(opts.manifestPath), { recursive: true });
|
|
261
|
+
const tmp = `${opts.manifestPath}.${process.pid}.tmp`;
|
|
262
|
+
fs.writeFileSync(tmp, serialize(manifest), 'utf8');
|
|
263
|
+
fs.renameSync(tmp, opts.manifestPath);
|
|
264
|
+
process.stdout.write(`Wrote ${opts.manifestPath} - ${manifest.verbs.length} verb(s).\n`);
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (opts.check) {
|
|
269
|
+
const report = checkReport({ manifestPath: opts.manifestPath });
|
|
270
|
+
if (opts.json) {
|
|
271
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
272
|
+
} else if (report.ok) {
|
|
273
|
+
process.stdout.write(`${opts.manifestPath} is up to date.\n`);
|
|
274
|
+
} else {
|
|
275
|
+
for (const p of report.problems) process.stderr.write(`${p.kind}: ${p.subject}: ${p.message}\n`);
|
|
276
|
+
}
|
|
277
|
+
if (!report.ok) throw new ExitError(1);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
process.stdout.write(serialize(buildManifest()));
|
|
282
|
+
return 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (require.main === module) runMain(main);
|
|
286
|
+
|
|
287
|
+
module.exports = { buildManifest, checkReport, serialize, staleCompileProblem, aliasMap, MANIFEST_PATH };
|
|
@@ -139,11 +139,62 @@ const MACOS_GENERATED_HEADER =
|
|
|
139
139
|
'// Windows-oriented tier in platform-conformance-tier.generated.cjs — see\n' +
|
|
140
140
|
'// docs/adr/4593-macos-conformance-tier-architecture.md for the full rationale.\n';
|
|
141
141
|
|
|
142
|
+
// #6002 child_process call-site shapes. First arguments that are node itself:
|
|
143
|
+
// `process.execPath` or the literal 'node' / "node".
|
|
144
|
+
const NODE_FIRST_ARG = "(?:process\\.execPath\\s*[,)]|(['\"])node\\1\\s*[,)])";
|
|
145
|
+
const NOT_NODE_FIRST_ARG = '(?!' + NODE_FIRST_ARG + ')';
|
|
146
|
+
// One source of truth for the call names every shape below matches.
|
|
147
|
+
const CHILD_SYNC_NAMES = 'spawnSync|execSync|execFileSync';
|
|
148
|
+
const CHILD_ASYNC_NAMES = 'spawn|execFile|exec';
|
|
149
|
+
const CHILD_CALL_NAMES = CHILD_SYNC_NAMES + '|' + CHILD_ASYNC_NAMES;
|
|
150
|
+
const CHILD_NON_NODE_CALL = new RegExp(
|
|
151
|
+
'(?:\\b(?:' + CHILD_SYNC_NAMES + ')|(?<![.\\w$])(?:' + CHILD_ASYNC_NAMES + '))\\(\\s*' + NOT_NODE_FIRST_ARG,
|
|
152
|
+
);
|
|
153
|
+
// Member form `<ident>.spawn(...)` (e.g. `cp.spawn('bash')`, `child_process.execFile('git')`).
|
|
154
|
+
// Heuristic: any `<ident>.(spawn|spawnSync|execFile|execFileSync|exec|execSync)(` whose first
|
|
155
|
+
// argument is not node counts as a non-node call, EXCEPT a regex `.exec(` — the receiver is a
|
|
156
|
+
// regex literal (preceded by `/`) or an identifier ending in re/Re/RE/regex/Regex/REGEX.
|
|
157
|
+
const CHILD_MEMBER_CALL = new RegExp(
|
|
158
|
+
'([\\w$]+)\\.(?:' + CHILD_CALL_NAMES + ')\\(\\s*' + NOT_NODE_FIRST_ARG,
|
|
159
|
+
'g',
|
|
160
|
+
);
|
|
161
|
+
const REGEX_RECEIVER_NAME = /(?:re|Re|RE|regex|Regex|REGEX)$/;
|
|
162
|
+
function hasMemberNonNodeCall(content) {
|
|
163
|
+
CHILD_MEMBER_CALL.lastIndex = 0;
|
|
164
|
+
let m;
|
|
165
|
+
while ((m = CHILD_MEMBER_CALL.exec(content)) !== null) {
|
|
166
|
+
if (content[m.index - 1] === '/') continue; // regex-literal receiver: /x/g.exec(
|
|
167
|
+
if (/\.exec\(/.test(m[0]) && REGEX_RECEIVER_NAME.test(m[1])) continue;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// `promisify(cp.execFile)` hides the call sites from the call-shape scan: an unidentifiable,
|
|
173
|
+
// counted site.
|
|
174
|
+
const CHILD_PROMISIFIED = /\bpromisify\(\s*(?:[\w$]+\.)?(?:execFile|exec|spawn)\b/;
|
|
175
|
+
const CHILD_NODE_ONLY_CALL = new RegExp(
|
|
176
|
+
'\\b(?:' + CHILD_CALL_NAMES + ')\\(\\s*' + NODE_FIRST_ARG + '|(?<![.\\w$])fork\\(',
|
|
177
|
+
);
|
|
178
|
+
// Code constructs only: a bare `lock` word matches prose, so lock-ness is a lockfile/lock-helper
|
|
179
|
+
// identifier, a `.lock` path suffix, a rename/unlink call, EBUSY/EPERM, the installer or --config-dir.
|
|
180
|
+
const FS_LOCK_OR_INSTALL =
|
|
181
|
+
/\b(?:lockfile|acquireLock|releaseLock|withLock|lockSync|proper-lockfile)\b|\.lock\b|\b(?:rename|unlink)(?:Sync)?\(|\bE(?:BUSY|PERM)\b|install\.js|--config-dir/;
|
|
182
|
+
const CHILD_SHELL_TRUE = /\bshell\s*:\s*true\b/;
|
|
183
|
+
// Pre-#6002 raw-child-process test, kept for source-file reachability
|
|
184
|
+
// (scripts/ci-test-scope.cjs), whose behavior must not change.
|
|
185
|
+
function preIssue6002RawChildProcess(content) {
|
|
186
|
+
if (/require\((['"])(?:node:)?child_process\1\)/.test(content)) return true;
|
|
187
|
+
if (!content.includes('child_process')) return false;
|
|
188
|
+
return /\bspawnSync\(|\bexecSync\(|\bexecFileSync\(/.test(content);
|
|
189
|
+
}
|
|
190
|
+
|
|
142
191
|
/**
|
|
143
192
|
* Detection categories (#4591 design doc). Each entry's `test` receives the
|
|
144
193
|
* raw file content string and returns true when that category's signal is
|
|
145
194
|
* present. Order matches the design doc's enumeration; `signals` in
|
|
146
|
-
* `classifyContent`'s return value preserves this order.
|
|
195
|
+
* `classifyContent`'s return value preserves this order. Optional fields
|
|
196
|
+
* (the pre-#6002 view used by `classifyUnstrippedContent`): `raw: <fn>`
|
|
197
|
+
* replaces `test` there; `rawSkip: true` omits the category there.
|
|
147
198
|
*/
|
|
148
199
|
const CATEGORIES = [
|
|
149
200
|
{
|
|
@@ -172,16 +223,43 @@ const CATEGORIES = [
|
|
|
172
223
|
},
|
|
173
224
|
{
|
|
174
225
|
name: 'raw-child-process',
|
|
175
|
-
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
226
|
+
raw: preIssue6002RawChildProcess, // pre-#6002 test used by classifyUnstrippedContent
|
|
227
|
+
// #6002: matches a call that can spawn a platform-dependent binary, not
|
|
228
|
+
// the mere presence of the module. Requires the child_process token AND a
|
|
229
|
+
// sync or async spawn call (spawnSync/execSync/execFileSync/spawn/execFile/
|
|
230
|
+
// exec) whose first argument is NOT node itself (`process.execPath` or the
|
|
231
|
+
// literal 'node' / "node"): a child that only re-runs node behaves
|
|
232
|
+
// identically on every OS and is no real-OS signal. `fork(` always runs
|
|
233
|
+
// node, so it is node-only by construction and never a signal on its own.
|
|
234
|
+
// Any other first argument (another literal, an identifier, a template
|
|
235
|
+
// literal) matches (over-inclusion, the safe direction), as does
|
|
236
|
+
// `shell: true` anywhere in a child_process-using file. A file that
|
|
237
|
+
// requires child_process but whose call sites cannot be identified at all
|
|
238
|
+
// (aliased or destructured-and-renamed usage) also counts. A same-named
|
|
239
|
+
// local identifier (e.g. `spawnResult`) never forms the substring
|
|
240
|
+
// `spawnSync(`; bare `.exec(` member calls (e.g. `re.exec(`) are not
|
|
241
|
+
// spawn call sites.
|
|
179
242
|
test: (content) => {
|
|
180
|
-
if (/require\((['"])(?:node:)?child_process\1\)/.test(content)) return true;
|
|
181
243
|
if (!content.includes('child_process')) return false;
|
|
182
|
-
|
|
244
|
+
if (CHILD_SHELL_TRUE.test(content)) return true;
|
|
245
|
+
if (CHILD_NON_NODE_CALL.test(content)) return true;
|
|
246
|
+
if (hasMemberNonNodeCall(content)) return true;
|
|
247
|
+
if (CHILD_PROMISIFIED.test(content)) return true;
|
|
248
|
+
return !CHILD_NODE_ONLY_CALL.test(content);
|
|
183
249
|
},
|
|
184
250
|
},
|
|
251
|
+
{
|
|
252
|
+
name: 'fs-lock-or-install-child',
|
|
253
|
+
rawSkip: true, // did not exist pre-#6002; classifyUnstrippedContent omits it
|
|
254
|
+
// #6002 decision: a node-only child still counts when the file also
|
|
255
|
+
// exercises Windows-sensitive filesystem behavior (locking, rename/unlink
|
|
256
|
+
// semantics, EBUSY/EPERM, the installer, --config-dir), because the child
|
|
257
|
+
// is then driving that behavior in a real process.
|
|
258
|
+
test: (content) =>
|
|
259
|
+
content.includes('child_process') &&
|
|
260
|
+
CHILD_NODE_ONLY_CALL.test(content) &&
|
|
261
|
+
FS_LOCK_OR_INSTALL.test(content),
|
|
262
|
+
},
|
|
185
263
|
{
|
|
186
264
|
name: 'shell-interpreter-spawn',
|
|
187
265
|
// Added after an adversarial review (#4641) caught a REAL false negative
|
|
@@ -259,15 +337,8 @@ const NOISY_FOR_SOURCE_REACHABILITY = new Set(['symlink-keyword']);
|
|
|
259
337
|
* expressible as content; reach for this map only when it structurally is
|
|
260
338
|
* not.
|
|
261
339
|
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* (src/external-descriptor-trust.cts:41-49), which calls the AMBIENT
|
|
265
|
-
* `path` module directly — `path.resolve(root, target)` and `path.sep` —
|
|
266
|
-
* with no platform/path injection seam. Its win32 semantics (drive
|
|
267
|
-
* letters, UNC paths, `\` separator) are therefore only reachable by
|
|
268
|
-
* actually running on Windows; the win32 branch is unreachable on Linux.
|
|
269
|
-
* This is a security-relevant write-confinement gate, so a silent gap
|
|
270
|
-
* here is a security regression, not a coverage nit (#4641).
|
|
340
|
+
* Entries and their reasons live in the Map below (the single source); each
|
|
341
|
+
* reason is a required value, so none can be added without one.
|
|
271
342
|
*/
|
|
272
343
|
const ALWAYS_REAL_OS = new Map([
|
|
273
344
|
[
|
|
@@ -276,6 +347,13 @@ const ALWAYS_REAL_OS = new Map([
|
|
|
276
347
|
'path module (path.resolve/path.sep) with no platform injection; its win32 branch (drive ' +
|
|
277
348
|
'letters, UNC paths, \\ separator) is unreachable on Linux. Security-relevant write-confinement gate.',
|
|
278
349
|
],
|
|
350
|
+
[
|
|
351
|
+
'tests/state-todos-render.test.cjs',
|
|
352
|
+
'Exercises renderPendingTodosMarkdown (src/init.cts:2646-2650), which uses the ambient path module ' +
|
|
353
|
+
'(path.isAbsolute/path.relative) with no platform injection to build repo-relative todo links ' +
|
|
354
|
+
'(#4384/#4421); its win32 branch (drive letters, \\ separator) is unreachable on Linux. The test ' +
|
|
355
|
+
"file's only content signal was a node-only spawnSync, which #6002 deliberately ignores.",
|
|
356
|
+
],
|
|
279
357
|
]);
|
|
280
358
|
|
|
281
359
|
/**
|
|
@@ -301,18 +379,157 @@ const MACOS_CATEGORIES = [
|
|
|
301
379
|
{ name: 'case-sensitivity', test: (content) => /case.?insensitiv|case.?sensitiv/i.test(content) },
|
|
302
380
|
{ name: 'chmod-mode-bit', test: (content) => /chmodSync|chmod\(/.test(content) || /0o[0-7]{3,4}\b/.test(content) },
|
|
303
381
|
{ name: 'symlink-keyword', test: (content) => /\bsymlink/i.test(content) },
|
|
382
|
+
// #6002: realpath calls normalize the macOS /var -> /private/var tmpdir symlink.
|
|
383
|
+
{ name: 'realpath', test: (content) => /\brealpath(Sync)?\b/.test(content) },
|
|
304
384
|
];
|
|
305
385
|
|
|
386
|
+
/** Keywords after which a `/` begins a regex literal rather than a division. */
|
|
387
|
+
const REGEX_AFTER_KEYWORD = new Set([
|
|
388
|
+
'return', 'typeof', 'case', 'in', 'of', 'delete', 'void', 'throw', 'new', 'else', 'do', 'instanceof', 'yield', 'await',
|
|
389
|
+
]);
|
|
390
|
+
const REGEX_AFTER_CHAR = '(,=:[!&|?{};+-*%<>~^';
|
|
391
|
+
|
|
392
|
+
function regexAllowedAfter(out) {
|
|
393
|
+
let j = out.length - 1;
|
|
394
|
+
while (j >= 0 && /\s/.test(out[j])) j--;
|
|
395
|
+
if (j < 0) return true;
|
|
396
|
+
if (REGEX_AFTER_CHAR.includes(out[j])) return true;
|
|
397
|
+
if (/[A-Za-z0-9_$]/.test(out[j])) {
|
|
398
|
+
let k = j;
|
|
399
|
+
while (k >= 0 && /[A-Za-z0-9_$]/.test(out[k])) k--;
|
|
400
|
+
return REGEX_AFTER_KEYWORD.has(out.slice(k + 1, j + 1));
|
|
401
|
+
}
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* #6002 — remove `//` line comments and block comments from source text so
|
|
407
|
+
* the CATEGORIES / MACOS_CATEGORIES tests match platform BEHAVIOR in code, not
|
|
408
|
+
* words in prose. A single-pass scanner tracks whether it is inside a '…', "…"
|
|
409
|
+
* or `…` string literal (honoring backslash escapes), so a `//` inside a
|
|
410
|
+
* string (e.g. a URL) does not start a comment. A `/` is read as the start of
|
|
411
|
+
* a regex literal (scanned to its closing unescaped `/`, honoring `[...]`
|
|
412
|
+
* classes and escapes) when the previous significant character is one of
|
|
413
|
+
* `( , = : [ ! & | ? { } ; + - * % < > ~ ^`, the start of input, or a keyword
|
|
414
|
+
* such as return/typeof; the regex body is kept verbatim so quotes or `//`
|
|
415
|
+
* inside it cannot corrupt string or comment tracking. An unterminated regex
|
|
416
|
+
* (newline first) falls back to a plain `/`. '…' and "…" strings end at a
|
|
417
|
+
* newline so an unbalanced quote cannot swallow the rest of the file. A line
|
|
418
|
+
* comment is dropped up to (not including) its newline; a block comment
|
|
419
|
+
* becomes one space (its newlines are kept) so adjacent tokens never fuse.
|
|
420
|
+
*
|
|
421
|
+
* @param {string} content
|
|
422
|
+
* @returns {string}
|
|
423
|
+
*/
|
|
424
|
+
function stripComments(content) {
|
|
425
|
+
const src = typeof content === 'string' ? content : '';
|
|
426
|
+
const n = src.length;
|
|
427
|
+
let out = '';
|
|
428
|
+
let i = 0;
|
|
429
|
+
|
|
430
|
+
// Scans code from `i`. When `inExpr` (inside a template `${ … }`), returns at the
|
|
431
|
+
// matching unnested `}` (not consumed); otherwise runs to end of input.
|
|
432
|
+
function scanCode(inExpr) {
|
|
433
|
+
let depth = 0;
|
|
434
|
+
while (i < n) {
|
|
435
|
+
const ch = src[i];
|
|
436
|
+
const next = src[i + 1];
|
|
437
|
+
if (inExpr && ch === '}' && depth === 0) return;
|
|
438
|
+
if (inExpr && ch === '{') depth++;
|
|
439
|
+
else if (inExpr && ch === '}') depth--;
|
|
440
|
+
if (ch === '/' && next === '/') {
|
|
441
|
+
while (i < n && src[i] !== '\n') i++;
|
|
442
|
+
} else if (ch === '/' && next === '*') {
|
|
443
|
+
i += 2;
|
|
444
|
+
let newlines = '';
|
|
445
|
+
while (i < n && !(src[i] === '*' && src[i + 1] === '/')) {
|
|
446
|
+
if (src[i] === '\n') newlines += '\n';
|
|
447
|
+
i++;
|
|
448
|
+
}
|
|
449
|
+
i += 2;
|
|
450
|
+
out += ' ' + newlines;
|
|
451
|
+
} else if (ch === '/' && regexAllowedAfter(out)) {
|
|
452
|
+
let j = i + 1;
|
|
453
|
+
let inClass = false;
|
|
454
|
+
let closed = false;
|
|
455
|
+
while (j < n && src[j] !== '\n') {
|
|
456
|
+
const c = src[j];
|
|
457
|
+
if (c === '\\') {
|
|
458
|
+
j += 2;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (c === '[') inClass = true;
|
|
462
|
+
else if (c === ']') inClass = false;
|
|
463
|
+
else if (c === '/' && !inClass) {
|
|
464
|
+
closed = true;
|
|
465
|
+
break;
|
|
466
|
+
}
|
|
467
|
+
j++;
|
|
468
|
+
}
|
|
469
|
+
if (closed) {
|
|
470
|
+
out += src.slice(i, j + 1);
|
|
471
|
+
i = j + 1;
|
|
472
|
+
} else {
|
|
473
|
+
out += ch;
|
|
474
|
+
i++;
|
|
475
|
+
}
|
|
476
|
+
} else if (ch === '`') {
|
|
477
|
+
out += ch;
|
|
478
|
+
i++;
|
|
479
|
+
while (i < n) {
|
|
480
|
+
const c = src[i];
|
|
481
|
+
if (c === '\\' && i + 1 < n) {
|
|
482
|
+
out += c + src[i + 1];
|
|
483
|
+
i += 2;
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (c === '$' && src[i + 1] === '{') {
|
|
487
|
+
out += '${';
|
|
488
|
+
i += 2;
|
|
489
|
+
scanCode(true);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
out += c;
|
|
493
|
+
i++;
|
|
494
|
+
if (c === '`') break;
|
|
495
|
+
}
|
|
496
|
+
} else if (ch === "'" || ch === '"') {
|
|
497
|
+
out += ch;
|
|
498
|
+
i++;
|
|
499
|
+
while (i < n) {
|
|
500
|
+
const c = src[i];
|
|
501
|
+
if (c === '\\' && i + 1 < n) {
|
|
502
|
+
out += c + src[i + 1];
|
|
503
|
+
i += 2;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (c === '\n') break;
|
|
507
|
+
out += c;
|
|
508
|
+
i++;
|
|
509
|
+
if (c === ch) break;
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
out += ch;
|
|
513
|
+
i++;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
scanCode(false);
|
|
519
|
+
return out;
|
|
520
|
+
}
|
|
521
|
+
|
|
306
522
|
/**
|
|
307
523
|
* Pure classifier: given a test file's raw string content, returns which
|
|
308
524
|
* platform-conformance categories matched and whether the file needs real-OS
|
|
309
|
-
* coverage (true iff at least one category matched).
|
|
525
|
+
* coverage (true iff at least one category matched). Comments are stripped
|
|
526
|
+
* first (#6002).
|
|
310
527
|
*
|
|
311
528
|
* @param {string} content
|
|
312
529
|
* @returns {{needsRealOs: boolean, signals: string[]}}
|
|
313
530
|
*/
|
|
314
531
|
function classifyContent(content) {
|
|
315
|
-
const text =
|
|
532
|
+
const text = stripComments(content);
|
|
316
533
|
const signals = [];
|
|
317
534
|
for (const category of CATEGORIES) {
|
|
318
535
|
if (category.test(text)) signals.push(category.name);
|
|
@@ -320,6 +537,26 @@ function classifyContent(content) {
|
|
|
320
537
|
return { needsRealOs: signals.length > 0, signals };
|
|
321
538
|
}
|
|
322
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Pre-#6002 classification: no comment stripping and the legacy
|
|
542
|
+
* raw-child-process test (any child_process require counts); the
|
|
543
|
+
* fs-lock-or-install-child category does not exist here. Used by
|
|
544
|
+
* scripts/ci-test-scope.cjs for src/ reachability, whose behavior #6002 must
|
|
545
|
+
* not change.
|
|
546
|
+
*
|
|
547
|
+
* @param {string} content
|
|
548
|
+
* @returns {{needsRealOs: boolean, signals: string[]}}
|
|
549
|
+
*/
|
|
550
|
+
function classifyUnstrippedContent(content) {
|
|
551
|
+
const text = typeof content === 'string' ? content : '';
|
|
552
|
+
const signals = [];
|
|
553
|
+
for (const category of CATEGORIES) {
|
|
554
|
+
if (category.rawSkip) continue;
|
|
555
|
+
if ((category.raw || category.test)(text)) signals.push(category.name);
|
|
556
|
+
}
|
|
557
|
+
return { needsRealOs: signals.length > 0, signals };
|
|
558
|
+
}
|
|
559
|
+
|
|
323
560
|
/**
|
|
324
561
|
* Pure classifier, macOS-specific signal set (#4593). Same shape as
|
|
325
562
|
* classifyContent, against MACOS_CATEGORIES instead of CATEGORIES.
|
|
@@ -328,7 +565,7 @@ function classifyContent(content) {
|
|
|
328
565
|
* @returns {{needsRealOs: boolean, signals: string[]}}
|
|
329
566
|
*/
|
|
330
567
|
function classifyMacosContent(content) {
|
|
331
|
-
const text =
|
|
568
|
+
const text = stripComments(content); // #6002
|
|
332
569
|
const signals = [];
|
|
333
570
|
for (const category of MACOS_CATEGORIES) {
|
|
334
571
|
if (category.test(text)) signals.push(category.name);
|
|
@@ -728,6 +965,8 @@ if (require.main === module) {
|
|
|
728
965
|
|
|
729
966
|
module.exports = {
|
|
730
967
|
classifyContent,
|
|
968
|
+
classifyUnstrippedContent,
|
|
969
|
+
stripComments,
|
|
731
970
|
CATEGORIES,
|
|
732
971
|
NOISY_FOR_SOURCE_REACHABILITY,
|
|
733
972
|
ALWAYS_REAL_OS,
|