@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
|
@@ -31,7 +31,7 @@ const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
|
|
|
31
31
|
const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
|
|
32
32
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
33
33
|
const planningWorkspaceMod = require("./planning-workspace.cjs");
|
|
34
|
-
const { planningDir } = planningWorkspaceMod;
|
|
34
|
+
const { planningDir, planningRoot } = planningWorkspaceMod;
|
|
35
35
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
36
36
|
const phaseLocatorMod = require("./phase-locator.cjs");
|
|
37
37
|
const { phaseDirectoryOptions, findPhaseInternal, resolvePhaseDirectoryLookup, matchPhaseDirsForLookup } = phaseLocatorMod;
|
|
@@ -40,12 +40,24 @@ const roadmapModule = require("./roadmap.cjs");
|
|
|
40
40
|
const { getRoadmapPhaseWithFallback } = roadmapModule;
|
|
41
41
|
/**
|
|
42
42
|
* Resolve a caller-supplied path (absolute, or relative to `projectDir`) and require the result to
|
|
43
|
-
* stay inside `projectDir`
|
|
44
|
-
*
|
|
43
|
+
* stay inside `projectDir` or inside the realpath of the project's OWN `.planning` (#5474: it may be
|
|
44
|
+
* a symlink to an out-of-repo store, so its phases realpath outside `projectDir`; ADR-5057 "Two
|
|
45
|
+
* containment roots"). Both decisions go through `tryWithinRoot` (realpath containment, ADR-4650);
|
|
46
|
+
* a symlink inside the store that leaves it, `..` traversal and a name-prefix sibling of the store
|
|
47
|
+
* still fail both. Returns the contained path the predicate produced, or a `GateUsageFailure`
|
|
48
|
+
* (`usage`, `path escapes its allowed directory: <arg>`).
|
|
49
|
+
*
|
|
50
|
+
* Trust assumption: the `.planning` link is trusted like the rest of the project tree. Whatever it
|
|
51
|
+
* resolves to becomes a containment root, so a `.planning` linked to a broad directory (a home
|
|
52
|
+
* directory, `/etc`) makes every path under that directory contained. This is accepted by design
|
|
53
|
+
* (#5474 scope): the link is part of the checkout the user chose to run gates on, and the gates
|
|
54
|
+
* read paths, they do not grant access the user's own shell lacks. Only the project's own
|
|
55
|
+
* `.planning` is honoured; any other symlink target is still refused.
|
|
45
56
|
*/
|
|
46
57
|
function resolveContainedPath(inputPath, projectDir) {
|
|
47
58
|
const candidate = node_path_1.default.isAbsolute(inputPath) ? inputPath : node_path_1.default.join(projectDir, inputPath);
|
|
48
|
-
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot)
|
|
59
|
+
const contained = (0, security_cjs_1.tryWithinRoot)(candidate, projectDir, security_cjs_1.PathAcceptance.AbsoluteInsideRoot) ??
|
|
60
|
+
(0, security_cjs_1.tryWithinRoot)(candidate, planningRoot(projectDir), security_cjs_1.PathAcceptance.AbsoluteInsideRoot);
|
|
49
61
|
if (contained === null) {
|
|
50
62
|
return (0, gate_verdict_cjs_1.gateUsageFailure)(gate_verdict_cjs_1.GATE_FAILURE_CODE.USAGE, `path escapes its allowed directory: ${inputPath}`);
|
|
51
63
|
}
|
|
@@ -14,6 +14,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
|
|
|
14
14
|
const node_path_1 = __importDefault(require("node:path"));
|
|
15
15
|
const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
|
|
16
16
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
18
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
19
|
+
const { readConfigFileBounded } = configFileReadMod;
|
|
17
20
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
18
21
|
const capabilityStateMod = require("./capability-state.cjs");
|
|
19
22
|
const { isCapabilityActive } = capabilityStateMod;
|
|
@@ -175,6 +178,20 @@ function safeReadJson(filePath) {
|
|
|
175
178
|
return null;
|
|
176
179
|
}
|
|
177
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Safely read and parse `<planningDir>/config.json` as a bounded regular file (#5759). Returns
|
|
183
|
+
* null on a missing, non-regular (FIFO/device), oversized, unreadable or malformed file — never
|
|
184
|
+
* blocks. Not `safeReadJson`: that reader is for graph artifacts and opens blocking.
|
|
185
|
+
*/
|
|
186
|
+
function safeReadConfigJson(planningDir) {
|
|
187
|
+
try {
|
|
188
|
+
const raw = readConfigFileBounded(node_path_1.default.join(planningDir, 'config.json'));
|
|
189
|
+
return raw === null ? null : JSON.parse(raw);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
178
195
|
/**
|
|
179
196
|
* Build a bidirectional adjacency map from graph nodes and edges.
|
|
180
197
|
* Each node ID maps to an array of { target, edge } entries.
|
|
@@ -390,7 +407,7 @@ const SNAPSHOT_FILENAME = '.last-build-snapshot.json';
|
|
|
390
407
|
* default `<planningDir>/graphs/graph.json` when unset/blank/non-string.
|
|
391
408
|
*/
|
|
392
409
|
function resolveGraphLocation(cwd, planningDir) {
|
|
393
|
-
const config =
|
|
410
|
+
const config = safeReadConfigJson(planningDir);
|
|
394
411
|
const graphify = (config && config['graphify']);
|
|
395
412
|
const configuredValue = graphify && graphify['graph_path'];
|
|
396
413
|
if (typeof configuredValue === 'string' && configuredValue.trim().length > 0) {
|
|
@@ -564,7 +581,7 @@ function graphifyBuild(cwd) {
|
|
|
564
581
|
const graphsDir = node_path_1.default.join(planningDir, 'graphs');
|
|
565
582
|
node_fs_1.default.mkdirSync(graphsDir, { recursive: true });
|
|
566
583
|
// Read build timeout from config -- default 300s per D-02
|
|
567
|
-
const config =
|
|
584
|
+
const config = safeReadConfigJson(planningDir) || {};
|
|
568
585
|
const graphifyConfig = config.graphify;
|
|
569
586
|
const timeoutSec = (graphifyConfig && graphifyConfig.build_timeout) || 300;
|
|
570
587
|
return {
|
|
@@ -324,7 +324,11 @@ function runRepairAction(cwd, action, paths, args = {}) {
|
|
|
324
324
|
case REMEDY_ACTION.ADD_NYQUIST_KEY:
|
|
325
325
|
case REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY: {
|
|
326
326
|
const key = action === REMEDY_ACTION.ADD_NYQUIST_KEY ? 'nyquist_validation' : 'ai_integration_phase';
|
|
327
|
-
|
|
327
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws (the repair fails, never blocks).
|
|
328
|
+
const configRaw = configLoaderMod.readConfigFileBounded(configPath);
|
|
329
|
+
if (configRaw === null) {
|
|
330
|
+
throw Object.assign(new Error(`ENOENT: no such file or directory, open '${configPath}'`), { code: 'ENOENT' });
|
|
331
|
+
}
|
|
328
332
|
const configParsed = JSON.parse(configRaw);
|
|
329
333
|
if (!configParsed['workflow'])
|
|
330
334
|
configParsed['workflow'] = {};
|
|
@@ -48,7 +48,7 @@ const secrets_cjs_1 = require("./secrets.cjs");
|
|
|
48
48
|
const scanPhasePlans = require("./plan-scan.cjs");
|
|
49
49
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- plan-document.cjs is an export= CommonJS module
|
|
50
50
|
const planDocument = require("./plan-document.cjs");
|
|
51
|
-
const
|
|
51
|
+
const state_transition_cjs_1 = require("./state-transition.cjs");
|
|
52
52
|
const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
|
|
53
53
|
const host_runtime_detection_cjs_1 = require("./host-runtime-detection.cjs");
|
|
54
54
|
const security_cjs_1 = require("./security.cjs");
|
|
@@ -540,7 +540,10 @@ function detectHasPriorPhases(cwd, phaseInfo) {
|
|
|
540
540
|
*/
|
|
541
541
|
function readConfigJsonBoolean(cwd, keyPath) {
|
|
542
542
|
try {
|
|
543
|
-
|
|
543
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws and degrades to false below.
|
|
544
|
+
const raw = configLoader.readConfigFileBounded(node_path_1.default.join(planningDir(cwd), 'config.json'));
|
|
545
|
+
if (raw === null)
|
|
546
|
+
return false;
|
|
544
547
|
let cursor = JSON.parse(raw);
|
|
545
548
|
for (const segment of keyPath) {
|
|
546
549
|
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
@@ -576,7 +579,10 @@ function readNonEmptyFileOrNull(filePath) {
|
|
|
576
579
|
*/
|
|
577
580
|
function readConfigJsonValue(cwd, keyPath) {
|
|
578
581
|
try {
|
|
579
|
-
|
|
582
|
+
// Bounded read (#5759): a FIFO/device/oversized config throws and degrades to undefined below.
|
|
583
|
+
const raw = configLoader.readConfigFileBounded(node_path_1.default.join(planningDir(cwd), 'config.json'));
|
|
584
|
+
if (raw === null)
|
|
585
|
+
return undefined;
|
|
580
586
|
let cursor = JSON.parse(raw);
|
|
581
587
|
for (const segment of keyPath) {
|
|
582
588
|
if (cursor === null || typeof cursor !== 'object' || Array.isArray(cursor))
|
|
@@ -1058,8 +1064,7 @@ function cmdInitExecutePhase(cwd, phase, raw, options = {}) {
|
|
|
1058
1064
|
const stateWarnings = [];
|
|
1059
1065
|
if (phaseInfo?.['directory'] && node_fs_1.default.existsSync(node_path_1.default.join(cwd, phaseInfo['directory']))) {
|
|
1060
1066
|
const diskPlans = listPhasePlanFiles(node_path_1.default.join(cwd, phaseInfo['directory'])).length;
|
|
1061
|
-
const
|
|
1062
|
-
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1067
|
+
const totalPlansInPhase = (0, state_transition_cjs_1.readRecordedPlanTotal)(stateContent);
|
|
1063
1068
|
if (totalPlansInPhase !== null && diskPlans !== totalPlansInPhase) {
|
|
1064
1069
|
stateWarnings.push(`Plan count mismatch: STATE.md says ${totalPlansInPhase}, disk has ${diskPlans}`);
|
|
1065
1070
|
}
|
|
@@ -1286,8 +1291,7 @@ function cmdInitPlanPhase(cwd, phase, raw, options = {}) {
|
|
|
1286
1291
|
if (stateContent !== null) {
|
|
1287
1292
|
const stateWarnings = [];
|
|
1288
1293
|
result['state_validation_ran'] = true;
|
|
1289
|
-
const
|
|
1290
|
-
const totalPlansInPhase = totalPlansRaw ? parseInt(totalPlansRaw, 10) : null;
|
|
1294
|
+
const totalPlansInPhase = (0, state_transition_cjs_1.readRecordedPlanTotal)(stateContent);
|
|
1291
1295
|
if (totalPlansInPhase !== null &&
|
|
1292
1296
|
phaseInfo &&
|
|
1293
1297
|
totalPlansInPhase !==
|
|
@@ -2635,9 +2639,7 @@ function cmdInitManager(cwd, raw) {
|
|
|
2635
2639
|
const phaseNum = capturesBracketId ? match[2] : match[1];
|
|
2636
2640
|
if (bracketId)
|
|
2637
2641
|
bracketIdsByPhaseNumber.set(phaseNum, bracketId);
|
|
2638
|
-
const phaseName = (capturesBracketId ? match[3] : match[2])
|
|
2639
|
-
.replace(/\(INSERTED\)/i, '')
|
|
2640
|
-
.trim();
|
|
2642
|
+
const phaseName = (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(capturesBracketId ? match[3] : match[2]);
|
|
2641
2643
|
let displayId;
|
|
2642
2644
|
if (bracketId) {
|
|
2643
2645
|
try {
|
|
@@ -3443,7 +3445,7 @@ function cmdInitProgress(cwd, raw, options = {}) {
|
|
|
3443
3445
|
let hm;
|
|
3444
3446
|
while ((hm = headingPattern.exec(roadmapContent)) !== null) {
|
|
3445
3447
|
roadmapPhaseNums.add(hm[hNumGroup]);
|
|
3446
|
-
roadmapPhaseNames.set(hm[hNumGroup],
|
|
3448
|
+
roadmapPhaseNames.set(hm[hNumGroup], (0, phase_entry_grammar_cjs_1.stripInsertedMarker)(hm[hNameGroup]));
|
|
3447
3449
|
}
|
|
3448
3450
|
// #4906 Phase 5 (#4984): NOT migrated onto phaseHeadingPrefixSrcFor — same
|
|
3449
3451
|
// out-of-scope reasoning as roadmapPhaseMatchesExactPrefix/detectPhaseMvpMode
|
|
@@ -36,9 +36,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
37
37
|
const node_path_1 = __importDefault(require("node:path"));
|
|
38
38
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
40
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
41
|
+
const { unboundedConfigFileReason, printablePath, readConfigFileBounded } = configFileReadMod;
|
|
39
42
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
|
|
40
43
|
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
41
|
-
const { installFs } = installFsAdapter;
|
|
44
|
+
const { installFs, isRealInstallFs } = installFsAdapter;
|
|
42
45
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- model-resolver.cjs is an export= CommonJS module
|
|
43
46
|
const modelResolver = require("./model-resolver.cjs");
|
|
44
47
|
const { EFFORT_SET: GSD_EFFORT_SET } = modelResolver;
|
|
@@ -54,19 +57,36 @@ const { mergeEffortTierDefaults } = modelCatalog;
|
|
|
54
57
|
function _readGsdConfigFile(absPath, label) {
|
|
55
58
|
if (!installFs().existsSync(absPath))
|
|
56
59
|
return null;
|
|
60
|
+
// #5759: refuse a FIFO/device/socket or an oversized file BEFORE the adapter's blocking read
|
|
61
|
+
// (shared guard; a path that is not on the real fs is left to the adapter).
|
|
57
62
|
let raw;
|
|
58
63
|
try {
|
|
59
|
-
|
|
64
|
+
if (isRealInstallFs()) {
|
|
65
|
+
// The real fs: the descriptor-bound reader (non-blocking open, fstat, cap).
|
|
66
|
+
const text = readConfigFileBounded(absPath);
|
|
67
|
+
if (text === null)
|
|
68
|
+
return null;
|
|
69
|
+
raw = text;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// An injected adapter cannot give a descriptor: stat the real path first (best effort).
|
|
73
|
+
const unbounded = unboundedConfigFileReason(absPath);
|
|
74
|
+
if (unbounded !== null) {
|
|
75
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${unbounded}\n`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
raw = installFs().readFileSync(absPath, 'utf-8');
|
|
79
|
+
}
|
|
60
80
|
}
|
|
61
81
|
catch (err) {
|
|
62
|
-
process.stderr.write(`gsd: warning — could not read ${label} (${absPath}): ${err.message}\n`);
|
|
82
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
63
83
|
return null;
|
|
64
84
|
}
|
|
65
85
|
try {
|
|
66
86
|
return JSON.parse(raw);
|
|
67
87
|
}
|
|
68
88
|
catch (err) {
|
|
69
|
-
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${absPath}): ${err.message}\n`);
|
|
89
|
+
process.stderr.write(`gsd: warning — invalid JSON in ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
70
90
|
return null;
|
|
71
91
|
}
|
|
72
92
|
}
|
|
@@ -122,7 +122,7 @@ function pruneEmptyCorpusParents(start, stop) {
|
|
|
122
122
|
}
|
|
123
123
|
function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix) {
|
|
124
124
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(configDir), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
125
|
-
throw new Error(
|
|
125
|
+
throw new Error(runtimeArtifactInstallPlan.syncRuntimeSurfaceCorpusSymlinkRefusalMessage(destination, configDir));
|
|
126
126
|
}
|
|
127
127
|
assertCorpusTreeHasNoSymlinks(destination);
|
|
128
128
|
// Remove only paths the previous manifest proves GSD owned and which the
|
|
@@ -160,15 +160,7 @@ function syncRuntimeSurfaceCorpus(source, destination, configDir, manifestPrefix
|
|
|
160
160
|
* The compatibility marker does not replace that installed-corpus authority.
|
|
161
161
|
*/
|
|
162
162
|
function provisionRuntimeSurfaceCorpus(layout, configDir, scope) {
|
|
163
|
-
const required =
|
|
164
|
-
if ((0, install_scope_cjs_1.isGlobalScope)(scope)) {
|
|
165
|
-
for (const kind of layout.kinds) {
|
|
166
|
-
if (kind.kind === 'commands' || kind.kind === 'skills')
|
|
167
|
-
required.add('commands');
|
|
168
|
-
if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
|
|
169
|
-
required.add('agents');
|
|
170
|
-
}
|
|
171
|
-
}
|
|
163
|
+
const required = runtimeArtifactInstallPlan.requiredRuntimeSurfaceSources(layout, scope);
|
|
172
164
|
if (required.size === 0)
|
|
173
165
|
return;
|
|
174
166
|
const corpusRoot = node_path_1.default.join(configDir, 'gsd-core');
|
|
@@ -557,7 +549,7 @@ function _resolveDevPreferencesSkillTarget(targetDir, runtime, scope = 'global')
|
|
|
557
549
|
// legacy dev-preferences migration lands in the SAME tree the installer
|
|
558
550
|
// and surface-apply use. Runtimes with no `home` override are unaffected.
|
|
559
551
|
hasHomeOverride = skillsKindEntry.home != null;
|
|
560
|
-
installRoot =
|
|
552
|
+
installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, skillsKindEntry);
|
|
561
553
|
skillDir = node_path_1.default.join(runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath), stemName);
|
|
562
554
|
}
|
|
563
555
|
else {
|
|
@@ -666,7 +658,7 @@ function _copyStaged(stagedDir, destDir, kind, configDir, runtime) {
|
|
|
666
658
|
// that case this defense-in-depth check must confine against the resolved
|
|
667
659
|
// alternate root instead, matching the upstream gate's own root selection in
|
|
668
660
|
// createRuntimeArtifactInstallPlan.
|
|
669
|
-
const installRoot = (
|
|
661
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(configDir, kind);
|
|
670
662
|
// Strict-subpath + NUL containment via the canonical gate (shared with the
|
|
671
663
|
// layout-driven install plan); throws if destDir escapes the install root.
|
|
672
664
|
// destDir here is an absolute path; path.resolve(installRoot, absoluteDest) returns it unchanged, so the gate's strict-subpath check still correctly confines it to installRoot.
|
|
@@ -1300,7 +1292,7 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
1300
1292
|
// previously lived inline in bin/install.js.
|
|
1301
1293
|
const behaviors = hostBehaviorsFor(runtime);
|
|
1302
1294
|
const projectDir = scope === 'global' ? process.cwd() : configDir;
|
|
1303
|
-
if (behaviors
|
|
1295
|
+
if (runtimeArtifactInstallPlan.shouldInstallCombinedFamily(behaviors)) {
|
|
1304
1296
|
// #2329: combined-family runtimes (OpenCode/Kilo) bypass
|
|
1305
1297
|
// _runLegacyInstallMigrations below entirely (early return), so their
|
|
1306
1298
|
// legacy-directory cleanup needs its own pre-materialization hook here.
|
|
@@ -1378,12 +1370,12 @@ function installRuntimeArtifacts(runtime, configDir, scope, resolvedProfile, res
|
|
|
1378
1370
|
// A fake adapter can change what those probes observe for paths that
|
|
1379
1371
|
// were never real to begin with; it cannot make this `if` pass for a
|
|
1380
1372
|
// path the real filesystem would refuse.
|
|
1381
|
-
const installRoot = (
|
|
1373
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(configDir, kind);
|
|
1382
1374
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
1383
1375
|
// Threat model from #1704 / ADR-1239 Phase B preserved: path-traversal and
|
|
1384
1376
|
// resolved-target-equals-root still refuse regardless of opt-in.
|
|
1385
1377
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1386
|
-
throw new Error(
|
|
1378
|
+
throw new Error(runtimeArtifactInstallPlan.installRuntimeArtifactsSymlinkRefusalMessage(dest, installRoot));
|
|
1387
1379
|
}
|
|
1388
1380
|
// #2875 defect fix (--minimal regression closed): a restricted profile
|
|
1389
1381
|
// (e.g. --minimal) can legitimately stage ZERO agents — no skill in
|
|
@@ -1577,6 +1569,7 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1577
1569
|
const skillsKindEntry = layout.kinds.find((k) => k.kind === 'skills');
|
|
1578
1570
|
if (!skillsKindEntry)
|
|
1579
1571
|
return 0;
|
|
1572
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, skillsKindEntry);
|
|
1580
1573
|
// #3712: combined-family runtimes take installRuntimeArtifacts' early return
|
|
1581
1574
|
// BEFORE its guard runs, and this writer honors `skillsKindEntry.home` below and
|
|
1582
1575
|
// then prunes that destination. opencode/kilo declare no `home` today, so there
|
|
@@ -1603,13 +1596,12 @@ function installOpencodeFamilySkills(runtime, targetDir, rawCommandsDir, pathPre
|
|
|
1603
1596
|
// OpenCode/Kilo writer lands in the SAME tree the installer and surface-apply use.
|
|
1604
1597
|
// Runtimes with no `home` override (opencode, kilo today) are unaffected. Must stay
|
|
1605
1598
|
// in lockstep with the sibling writers — the destination-parity test enforces it.
|
|
1606
|
-
const installRoot = skillsKindEntry.home ?? targetDir;
|
|
1607
1599
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, skillsKindEntry.destSubpath);
|
|
1608
1600
|
// Symlink-escape guard: reject if any path component between installRoot and
|
|
1609
1601
|
// dest is a symlink that would redirect writes outside the install root.
|
|
1610
1602
|
// #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
|
|
1611
1603
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1612
|
-
throw new Error(
|
|
1604
|
+
throw new Error(runtimeArtifactInstallPlan.installOpencodeFamilySkillsSymlinkRefusalMessage(dest, installRoot));
|
|
1613
1605
|
}
|
|
1614
1606
|
installFs().mkdirSync(dest, { recursive: true });
|
|
1615
1607
|
// Preserve user-owned GSD-prefixed skill dirs across the gsd-* prune.
|
|
@@ -1798,14 +1790,14 @@ function installAgentsKindStandalone(runtime, targetDir, scope, resolvedProfile,
|
|
|
1798
1790
|
const stagedAgentFiles = installFs().existsSync(stagedDir)
|
|
1799
1791
|
? installFs().readdirSync(stagedDir).filter((f) => f.endsWith('.md'))
|
|
1800
1792
|
: [];
|
|
1801
|
-
const installRoot = (
|
|
1793
|
+
const installRoot = runtimeArtifactInstallPlan.resolveRuntimeArtifactInstallRoot(targetDir, agentsKindEntry);
|
|
1802
1794
|
const dest = runtimeArtifactInstallPlan.assertDestWithinConfigHome(installRoot, agentsKindEntry.destSubpath);
|
|
1803
1795
|
// Symlink-escape guard — same gate _copyStaged/installOpencodeFamilySkills apply
|
|
1804
1796
|
// to their own writes (#2393 GSD_ALLOW_SYMLINKED_DEST opt-in preserved). Runs
|
|
1805
1797
|
// even when nothing will be written this call — the stale-agent prune below
|
|
1806
1798
|
// (`_removeGsdEntries`) still touches `dest` whenever it already exists.
|
|
1807
1799
|
if (hasExistingSymlinkBetween(node_path_1.default.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
|
|
1808
|
-
throw new Error(
|
|
1800
|
+
throw new Error(runtimeArtifactInstallPlan.installAgentsKindStandaloneSymlinkRefusalMessage(dest, installRoot));
|
|
1809
1801
|
}
|
|
1810
1802
|
// #2875 defect fix, corrected: the ORIGINAL fix returned `null` (no-op)
|
|
1811
1803
|
// whenever a restricted profile (e.g. --minimal) staged ZERO agents,
|
|
@@ -259,4 +259,8 @@ function mkInstallTempDir(prefix) {
|
|
|
259
259
|
installFs().mkdirSync(dir, { recursive: true });
|
|
260
260
|
return dir;
|
|
261
261
|
}
|
|
262
|
-
|
|
262
|
+
/** True when no fake adapter is active, i.e. installFs() is the real `node:fs` (#5759). */
|
|
263
|
+
function isRealInstallFs() {
|
|
264
|
+
return current === REAL_ADAPTER;
|
|
265
|
+
}
|
|
266
|
+
module.exports = { installFs, withInstallFs, mkInstallTempDir, isRealInstallFs };
|
|
@@ -35,9 +35,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
35
35
|
const node_path_1 = __importDefault(require("node:path"));
|
|
36
36
|
const node_os_1 = __importDefault(require("node:os"));
|
|
37
37
|
const runtime_homes_cjs_1 = require("./runtime-homes.cjs");
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
|
|
39
|
+
const configFileReadMod = require("./config-file-read.cjs");
|
|
40
|
+
const { unboundedConfigFileReason, printablePath, readConfigFileBounded } = configFileReadMod;
|
|
41
|
+
/**
|
|
42
|
+
* Text of an install-time config file, or null (with a printable-path warning) when it is not a
|
|
43
|
+
* regular file within the cap or cannot be read. The real fs uses the descriptor-bound reader;
|
|
44
|
+
* an injected adapter gets the stat guard before its own read (#5759).
|
|
45
|
+
*/
|
|
46
|
+
function readInstallConfigText(absPath, label) {
|
|
47
|
+
try {
|
|
48
|
+
if (isRealInstallFs())
|
|
49
|
+
return readConfigFileBounded(absPath);
|
|
50
|
+
const unbounded = unboundedConfigFileReason(absPath);
|
|
51
|
+
if (unbounded !== null) {
|
|
52
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${unbounded}\n`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return installFs().readFileSync(absPath, 'utf-8');
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
process.stderr.write(`gsd: warning — could not read ${label} (${printablePath(absPath)}): ${printablePath(err.message)}\n`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
38
62
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-fs-adapter.cjs is an export= CommonJS module
|
|
39
63
|
const installFsAdapter = require("./install-fs-adapter.cjs");
|
|
40
|
-
const { installFs } = installFsAdapter;
|
|
64
|
+
const { installFs, isRealInstallFs } = installFsAdapter;
|
|
41
65
|
// eslint-disable-next-line @typescript-eslint/no-require-imports -- install-effort-resolver.cjs is an export= CommonJS module
|
|
42
66
|
const installEffortResolver = require("./install-effort-resolver.cjs");
|
|
43
67
|
const { _readGsdConfigFile, _findAncestorGsdConfigPath } = installEffortResolver;
|
|
@@ -62,7 +86,9 @@ function readGsdGlobalModelOverrides(options = {}) {
|
|
|
62
86
|
const defaultsPath = node_path_1.default.join(home, '.gsd', 'defaults.json');
|
|
63
87
|
if (!installFs().existsSync(defaultsPath))
|
|
64
88
|
return null;
|
|
65
|
-
const raw =
|
|
89
|
+
const raw = readInstallConfigText(defaultsPath, 'global defaults'); // #5759: never block on a planted FIFO
|
|
90
|
+
if (raw === null)
|
|
91
|
+
return null;
|
|
66
92
|
const parsed = JSON.parse(raw);
|
|
67
93
|
const overrides = parsed.model_overrides;
|
|
68
94
|
if (!overrides || typeof overrides !== 'object')
|
|
@@ -99,7 +125,11 @@ function readGsdEffectiveModelOverrides(targetDir = null, options = {}) {
|
|
|
99
125
|
const candidate = _findAncestorGsdConfigPath(targetDir);
|
|
100
126
|
if (candidate) {
|
|
101
127
|
try {
|
|
102
|
-
|
|
128
|
+
// #5759: refuse a FIFO/device/oversized project config before the adapter's blocking read.
|
|
129
|
+
const text = readInstallConfigText(candidate, 'project config');
|
|
130
|
+
if (text === null)
|
|
131
|
+
throw new Error('unusable project config');
|
|
132
|
+
const parsed = JSON.parse(text);
|
|
103
133
|
if (parsed && typeof parsed === 'object' && parsed.model_overrides && typeof parsed.model_overrides === 'object') {
|
|
104
134
|
projectOverrides = parsed.model_overrides;
|
|
105
135
|
}
|
package/gsd-core/bin/lib/io.cjs
CHANGED
|
@@ -364,6 +364,10 @@ const ERROR_REASON = Object.freeze({
|
|
|
364
364
|
COMMIT_DOCS_GUARD_NOT_A_REPO: 'commit_docs_guard_not_a_repo',
|
|
365
365
|
COMMIT_DOCS_GUARD_FOREIGN_HOOK: 'commit_docs_guard_foreign_hook',
|
|
366
366
|
COMMIT_DOCS_GUARD_HOOKS_PATH_SET: 'commit_docs_guard_hooks_path_set',
|
|
367
|
+
// commit verb (#6043, ADR-4908 §3): it commits in the worktree the process
|
|
368
|
+
// runs in, or refuses with one of these. Wire values are the typed `reason`.
|
|
369
|
+
COMMIT_NOT_GIT_REPO: 'not_git_repo',
|
|
370
|
+
COMMIT_OUTSIDE_WORKTREE: 'outside_worktree',
|
|
367
371
|
// security-scan
|
|
368
372
|
SECURITY_SCAN_FAILED: 'security_scan_failed',
|
|
369
373
|
// --pick (#3365 / #3358, ADR-3473 §8.4): an absent field or non-JSON
|
|
@@ -499,6 +503,8 @@ const REASON_TO_OUTCOME = Object.freeze({
|
|
|
499
503
|
[ERROR_REASON.COMMIT_DOCS_GUARD_NOT_A_REPO]: 'UNAVAILABLE',
|
|
500
504
|
[ERROR_REASON.COMMIT_DOCS_GUARD_FOREIGN_HOOK]: 'UNAVAILABLE',
|
|
501
505
|
[ERROR_REASON.COMMIT_DOCS_GUARD_HOOKS_PATH_SET]: 'UNAVAILABLE',
|
|
506
|
+
[ERROR_REASON.COMMIT_NOT_GIT_REPO]: 'UNAVAILABLE',
|
|
507
|
+
[ERROR_REASON.COMMIT_OUTSIDE_WORKTREE]: 'FAIL',
|
|
502
508
|
// Its own docstring: "an absent field or non-JSON command output is a
|
|
503
509
|
// failure, never a demotion to an empty answer" — the field/output was
|
|
504
510
|
// supposed to be there and was not; a prerequisite of the query failed.
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.XML_DECISION_TAG_NAMES = void 0;
|
|
15
|
+
exports.matchFenceDelimiter = matchFenceDelimiter;
|
|
15
16
|
exports.stripFencedCode = stripFencedCode;
|
|
16
17
|
exports.stripInlineCode = stripInlineCode;
|
|
17
18
|
exports.scanInlineCodeSpans = scanInlineCodeSpans;
|
|
@@ -24,6 +25,7 @@ exports.iterateBullets = iterateBullets;
|
|
|
24
25
|
exports.updateBullet = updateBullet;
|
|
25
26
|
exports.updateHeading = updateHeading;
|
|
26
27
|
exports.extractTaggedBlocks = extractTaggedBlocks;
|
|
28
|
+
exports.extractTaggedBlockSpans = extractTaggedBlockSpans;
|
|
27
29
|
exports.stripTaggedBlocks = stripTaggedBlocks;
|
|
28
30
|
exports.editOutsideTaggedBlocks = editOutsideTaggedBlocks;
|
|
29
31
|
exports.maskFences = maskFences;
|
|
@@ -35,6 +37,30 @@ exports.editHeadingSlice = editHeadingSlice;
|
|
|
35
37
|
exports.deleteSection = deleteSection;
|
|
36
38
|
exports.extractXmlTagBodies = extractXmlTagBodies;
|
|
37
39
|
const pattern_cjs_1 = require("./pattern.cjs");
|
|
40
|
+
const FENCE_DELIMITER_RE = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
41
|
+
/**
|
|
42
|
+
* The single CommonMark fence-delimiter grammar: ≤3 spaces of indent, a run of
|
|
43
|
+
* ≥3 backticks or tildes, then trailing text. A trailing `\r` is stripped first
|
|
44
|
+
* (CRLF safety). Returns `null` when the line is not a delimiter line at all.
|
|
45
|
+
* `stripFencedCode`, `scanFencedBlocks`, `tokenizeHeadings` and the roadmap
|
|
46
|
+
* parser's comment/fence blanking all classify delimiter lines through this one
|
|
47
|
+
* matcher.
|
|
48
|
+
*/
|
|
49
|
+
function matchFenceDelimiter(line) {
|
|
50
|
+
const m = FENCE_DELIMITER_RE.exec(line.replace(/\r$/, ''));
|
|
51
|
+
if (m === null)
|
|
52
|
+
return null;
|
|
53
|
+
const char = m[2][0];
|
|
54
|
+
const len = m[2].length;
|
|
55
|
+
const trailing = m[3];
|
|
56
|
+
return {
|
|
57
|
+
char,
|
|
58
|
+
len,
|
|
59
|
+
trailing,
|
|
60
|
+
canOpen: !(char === '`' && trailing.includes('`')),
|
|
61
|
+
closes: (opener) => char === opener.char && len >= opener.len && /^\s*$/.test(trailing),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
38
64
|
// ─── stripFencedCode ──────────────────────────────────────────────────────────
|
|
39
65
|
/**
|
|
40
66
|
* CommonMark-correct fenced-code-block stripper.
|
|
@@ -62,27 +88,20 @@ function stripFencedCode(content) {
|
|
|
62
88
|
const lines = content.split('\n');
|
|
63
89
|
const kept = [];
|
|
64
90
|
let openFence = null;
|
|
65
|
-
// Matches: optional indent (≤3 spaces per CommonMark), fence run, optional info string
|
|
66
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
67
91
|
for (const rawLine of lines) {
|
|
68
|
-
|
|
69
|
-
const line = rawLine.replace(/\r$/, '');
|
|
70
|
-
const m = delimRe.exec(line);
|
|
92
|
+
const m = matchFenceDelimiter(rawLine);
|
|
71
93
|
if (m) {
|
|
72
|
-
const char = m[2][0];
|
|
73
|
-
const len = m[2].length;
|
|
74
|
-
const trailing = m[3];
|
|
75
94
|
if (openFence === null) {
|
|
76
95
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
77
96
|
// If it does, this line is NOT a valid fence opener (treat as ordinary content).
|
|
78
|
-
if (
|
|
97
|
+
if (!m.canOpen) {
|
|
79
98
|
kept.push(rawLine);
|
|
80
99
|
continue;
|
|
81
100
|
}
|
|
82
101
|
// Opening delimiter — record fence state, drop this line
|
|
83
|
-
openFence = { char, len };
|
|
102
|
+
openFence = { char: m.char, len: m.len };
|
|
84
103
|
}
|
|
85
|
-
else if (
|
|
104
|
+
else if (m.closes(openFence)) {
|
|
86
105
|
// Closing delimiter (same char, sufficient length, no trailing content) — close and drop
|
|
87
106
|
openFence = null;
|
|
88
107
|
}
|
|
@@ -213,9 +232,8 @@ function stripInlineCodeLine(line) {
|
|
|
213
232
|
* different-info-string fence, a fence nested/indented inside another fence,
|
|
214
233
|
* and a `~~~` fence are all classified exactly as `stripFencedCode` would.
|
|
215
234
|
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
* machine, pending a T-tier consolidation.
|
|
235
|
+
* Delimiter classification goes through `matchFenceDelimiter`, the one shared
|
|
236
|
+
* grammar (also used by `stripFencedCode` and `tokenizeHeadings`).
|
|
219
237
|
*
|
|
220
238
|
* Exported so `context-predicates.cts` can consume this seam directly for its
|
|
221
239
|
* line-preserving fenced-line skip detection, instead of carrying a third
|
|
@@ -223,24 +241,19 @@ function stripInlineCodeLine(line) {
|
|
|
223
241
|
* comment).
|
|
224
242
|
*/
|
|
225
243
|
function scanFencedBlocks(lines) {
|
|
226
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
227
244
|
const blocks = [];
|
|
228
245
|
let open = null;
|
|
229
246
|
for (let i = 0; i < lines.length; i++) {
|
|
230
|
-
const
|
|
231
|
-
const m = delimRe.exec(line);
|
|
247
|
+
const m = matchFenceDelimiter(lines[i]);
|
|
232
248
|
if (!m)
|
|
233
249
|
continue;
|
|
234
|
-
const char = m[2][0];
|
|
235
|
-
const len = m[2].length;
|
|
236
|
-
const trailing = m[3];
|
|
237
250
|
if (open === null) {
|
|
238
251
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
239
|
-
if (
|
|
252
|
+
if (!m.canOpen)
|
|
240
253
|
continue; // not a valid opener — ordinary content
|
|
241
|
-
open = { char, len, infoString: trailing.trim(), openLineIdx: i };
|
|
254
|
+
open = { char: m.char, len: m.len, infoString: m.trailing.trim(), openLineIdx: i };
|
|
242
255
|
}
|
|
243
|
-
else if (
|
|
256
|
+
else if (m.closes(open)) {
|
|
244
257
|
blocks.push({
|
|
245
258
|
char: open.char,
|
|
246
259
|
len: open.len,
|
|
@@ -319,30 +332,26 @@ function tokenizeHeadings(content) {
|
|
|
319
332
|
const tokens = [];
|
|
320
333
|
// We re-run the fence state machine to know which lines are "kept", so we
|
|
321
334
|
// can map line index in original to whether it survived.
|
|
322
|
-
const delimRe = /^( {0,3})(`{3,}|~{3,})(.*)$/;
|
|
323
335
|
let openFence = null;
|
|
324
336
|
// Accumulate character offset as we iterate lines
|
|
325
337
|
let charOffset = 0;
|
|
326
338
|
for (let i = 0; i < originalLines.length; i++) {
|
|
327
339
|
const rawLine = originalLines[i];
|
|
328
340
|
const line = rawLine.replace(/\r$/, '');
|
|
329
|
-
const dm =
|
|
341
|
+
const dm = matchFenceDelimiter(rawLine);
|
|
330
342
|
if (dm) {
|
|
331
|
-
const char = dm[2][0];
|
|
332
|
-
const len = dm[2].length;
|
|
333
|
-
const trailing = dm[3];
|
|
334
343
|
if (openFence === null) {
|
|
335
344
|
// CommonMark §4.5: backtick fence info string must not contain a backtick.
|
|
336
|
-
if (
|
|
345
|
+
if (!dm.canOpen) {
|
|
337
346
|
// Not a valid fence opener — check for heading on this line (will fall through)
|
|
338
347
|
}
|
|
339
348
|
else {
|
|
340
|
-
openFence = { char, len };
|
|
349
|
+
openFence = { char: dm.char, len: dm.len };
|
|
341
350
|
charOffset += rawLine.length + 1;
|
|
342
351
|
continue;
|
|
343
352
|
}
|
|
344
353
|
}
|
|
345
|
-
else if (
|
|
354
|
+
else if (dm.closes(openFence)) {
|
|
346
355
|
openFence = null;
|
|
347
356
|
charOffset += rawLine.length + 1;
|
|
348
357
|
continue;
|
|
@@ -818,6 +827,28 @@ function extractTaggedBlocks(content, tagName, allowAttributes = false) {
|
|
|
818
827
|
}
|
|
819
828
|
return results;
|
|
820
829
|
}
|
|
830
|
+
/**
|
|
831
|
+
* Offset spans of the bodies `extractTaggedBlocks` returns (#4309): same
|
|
832
|
+
* pattern, same blocks, but each body is reported as `{ start, end }` into
|
|
833
|
+
* `content` so a caller can test whether an arbitrary offset lies inside a
|
|
834
|
+
* block (and dedupe nested blocks) without re-harvesting the text.
|
|
835
|
+
* `content.slice(span.start, span.end)` equals the matching extracted body.
|
|
836
|
+
*/
|
|
837
|
+
function extractTaggedBlockSpans(content, tagName, allowAttributes = false) {
|
|
838
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
839
|
+
return [];
|
|
840
|
+
if (typeof tagName !== 'string' || tagName.length === 0)
|
|
841
|
+
return [];
|
|
842
|
+
const closeLength = `</${tagName}>`.length;
|
|
843
|
+
const pattern = taggedBlockPattern(tagName, 'g', allowAttributes);
|
|
844
|
+
const spans = [];
|
|
845
|
+
let match;
|
|
846
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
847
|
+
const end = match.index + match[0].length - closeLength;
|
|
848
|
+
spans.push({ start: end - match[1].length, end });
|
|
849
|
+
}
|
|
850
|
+
return spans;
|
|
851
|
+
}
|
|
821
852
|
/**
|
|
822
853
|
* Build the single, ReDoS-safe `<tag>…</tag>` block regex shared by
|
|
823
854
|
* `extractTaggedBlocks` (extract bodies) and `stripTaggedBlocks` (remove blocks).
|