@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,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Criterion Baseline Module (#4309).
|
|
4
|
+
*
|
|
5
|
+
* A closed annotation grammar written next to a quantitative criterion:
|
|
6
|
+
*
|
|
7
|
+
* [HEAD=<value> EXPECT<op><value> [@<sha>] [GUARD MUTATION: <ref>] [UNPINNED]]
|
|
8
|
+
* value ∈ <integer> | absent | present | error:<exit-code>
|
|
9
|
+
* op ∈ == != >= > <= <
|
|
10
|
+
*
|
|
11
|
+
* The notation records what the criterion measures at HEAD, so a criterion that
|
|
12
|
+
* already holds before the change (and therefore proves nothing) is caught
|
|
13
|
+
* statically: rule B1. A free-form `# baseline at HEAD:` comment is the legacy
|
|
14
|
+
* form this notation replaces: rule B2 (warning).
|
|
15
|
+
*
|
|
16
|
+
* Pure: no fs, no child_process, no console. Nothing is executed (replay is a
|
|
17
|
+
* separate concern). Every parse failure becomes a status, never an exit code
|
|
18
|
+
* change (fail open). Every regex is bounded: the closing-bracket search is a
|
|
19
|
+
* slice of at most 500 characters that stops at a line break.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.MAX_BASELINE_STATUSES = exports.BASELINE_STATUS = exports.BASELINE_RULE = void 0;
|
|
23
|
+
exports.parseBaselineValue = parseBaselineValue;
|
|
24
|
+
exports.renderBaselineValue = renderBaselineValue;
|
|
25
|
+
exports.compareBaseline = compareBaseline;
|
|
26
|
+
exports.decodeNotationEntities = decodeNotationEntities;
|
|
27
|
+
exports.parseBaselineNotation = parseBaselineNotation;
|
|
28
|
+
exports.parseCriteriaAllowMarkers = parseCriteriaAllowMarkers;
|
|
29
|
+
exports.scanBaselineNotation = scanBaselineNotation;
|
|
30
|
+
const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
|
|
31
|
+
exports.BASELINE_RULE = Object.freeze({
|
|
32
|
+
NON_DISCRIMINATING: 'B1',
|
|
33
|
+
LEGACY_COMMENT: 'B2',
|
|
34
|
+
});
|
|
35
|
+
exports.BASELINE_STATUS = Object.freeze({
|
|
36
|
+
MALFORMED: 'malformed',
|
|
37
|
+
BAD_VALUE: 'bad_value',
|
|
38
|
+
ORDERING_NEEDS_INTEGER: 'ordering_needs_integer',
|
|
39
|
+
BAD_REV: 'bad_rev',
|
|
40
|
+
PIN_CONFLICT: 'pin_conflict',
|
|
41
|
+
UNRECOGNIZED_TRAILING: 'unrecognized_trailing',
|
|
42
|
+
UNTERMINATED: 'unterminated',
|
|
43
|
+
OUTSIDE_CRITERIA_ZONE: 'outside_criteria_zone',
|
|
44
|
+
SCANNER_ERROR: 'scanner_error',
|
|
45
|
+
TRUNCATED: 'truncated',
|
|
46
|
+
});
|
|
47
|
+
/** Most statuses a scan records; the rest collapse into one `truncated` entry. */
|
|
48
|
+
exports.MAX_BASELINE_STATUSES = 50;
|
|
49
|
+
const CLOSE_SEARCH_WINDOW = 500;
|
|
50
|
+
const TEXT_CAP = 200;
|
|
51
|
+
const ZONE_TAGS = ['acceptance_criteria', 'automated', 'verify'];
|
|
52
|
+
const INTEGER_RE = /^-?\d{1,15}$/;
|
|
53
|
+
const ERROR_RE = /^error:(\d{1,3})$/;
|
|
54
|
+
const REV_RE = /^[0-9a-f]{7,40}$/;
|
|
55
|
+
const HEAD_EXPECT_RE = /^\s*HEAD\s*=\s*(\S+)\s+EXPECT\s*(==|!=|>=|<=|>|<)\s*(\S+?)(?=\s|$)/;
|
|
56
|
+
const OPENER_RE = /\[[ \t]*HEAD[ \t]*=/g;
|
|
57
|
+
const LEGACY_RE = /#[ \t]*baseline[ \t]+at[ \t]+HEAD[ \t]*:/gi;
|
|
58
|
+
const ALLOW_RE = /<!--\s*plan-criteria-allow:\s*(R[1-6]|B[12])\s+-\s+([^>\n]{0,500}?\S)\s*-->/g;
|
|
59
|
+
/** Parse one value token; null when it is outside the value grammar. */
|
|
60
|
+
function parseBaselineValue(token) {
|
|
61
|
+
if (typeof token !== 'string')
|
|
62
|
+
return null;
|
|
63
|
+
if (INTEGER_RE.test(token))
|
|
64
|
+
return { kind: 'integer', value: Number(token) };
|
|
65
|
+
if (token === 'absent')
|
|
66
|
+
return { kind: 'absent' };
|
|
67
|
+
if (token === 'present')
|
|
68
|
+
return { kind: 'present' };
|
|
69
|
+
const err = ERROR_RE.exec(token);
|
|
70
|
+
if (err) {
|
|
71
|
+
const code = Number(err[1]);
|
|
72
|
+
if (code >= 1 && code <= 255)
|
|
73
|
+
return { kind: 'error', code };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
/** Inverse of `parseBaselineValue`. */
|
|
78
|
+
function renderBaselineValue(v) {
|
|
79
|
+
switch (v.kind) {
|
|
80
|
+
case 'integer': return String(v.value);
|
|
81
|
+
case 'error': return `error:${v.code}`;
|
|
82
|
+
case 'absent': return 'absent';
|
|
83
|
+
default: return 'present';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function valuesEqual(a, b) {
|
|
87
|
+
if (a.kind !== b.kind)
|
|
88
|
+
return false;
|
|
89
|
+
if (a.kind === 'integer' && b.kind === 'integer')
|
|
90
|
+
return a.value === b.value;
|
|
91
|
+
if (a.kind === 'error' && b.kind === 'error')
|
|
92
|
+
return a.code === b.code;
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Does `head op expect` already hold? null when the operator needs an integer
|
|
97
|
+
* EXPECT and got none. A non-integer HEAD never satisfies an ordering operator.
|
|
98
|
+
*/
|
|
99
|
+
function compareBaseline(head, op, expect) {
|
|
100
|
+
if (op === '==')
|
|
101
|
+
return valuesEqual(head, expect);
|
|
102
|
+
if (op === '!=')
|
|
103
|
+
return !valuesEqual(head, expect);
|
|
104
|
+
if (expect.kind !== 'integer')
|
|
105
|
+
return null;
|
|
106
|
+
if (head.kind !== 'integer')
|
|
107
|
+
return false;
|
|
108
|
+
switch (op) {
|
|
109
|
+
case '>=': return head.value >= expect.value;
|
|
110
|
+
case '>': return head.value > expect.value;
|
|
111
|
+
case '<=': return head.value <= expect.value;
|
|
112
|
+
default: return head.value < expect.value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function decodeNotationEntities(s) {
|
|
116
|
+
return s.replace(/>/g, '>').replace(/</g, '<').replace(/&/g, '&');
|
|
117
|
+
}
|
|
118
|
+
/** Parse the text between `[` and `]`. Fatal problems yield a null notation. */
|
|
119
|
+
function parseBaselineNotation(inner) {
|
|
120
|
+
const statuses = [];
|
|
121
|
+
const fatal = (code, detail) => ({
|
|
122
|
+
notation: null,
|
|
123
|
+
statuses: [{ code, detail }],
|
|
124
|
+
});
|
|
125
|
+
const text = decodeNotationEntities(String(inner));
|
|
126
|
+
const m = HEAD_EXPECT_RE.exec(text);
|
|
127
|
+
if (!m)
|
|
128
|
+
return fatal(exports.BASELINE_STATUS.MALFORMED, 'expected `HEAD=<value> EXPECT<op><value>` with op one of == != >= > <= <');
|
|
129
|
+
const op = m[2];
|
|
130
|
+
const headToken = m[1];
|
|
131
|
+
const expectToken = m[3];
|
|
132
|
+
if (expectToken.startsWith('=')) {
|
|
133
|
+
return fatal(exports.BASELINE_STATUS.MALFORMED, `unknown operator near \`${op}${expectToken}\``);
|
|
134
|
+
}
|
|
135
|
+
const head = parseBaselineValue(headToken);
|
|
136
|
+
const expect = parseBaselineValue(expectToken);
|
|
137
|
+
if (head === null || expect === null) {
|
|
138
|
+
const bad = head === null ? headToken : expectToken;
|
|
139
|
+
return fatal(exports.BASELINE_STATUS.BAD_VALUE, `\`${bad.slice(0, 60)}\` is not an integer, absent, present or error:<1-255>`);
|
|
140
|
+
}
|
|
141
|
+
if (op !== '==' && op !== '!=' && expect.kind !== 'integer') {
|
|
142
|
+
return fatal(exports.BASELINE_STATUS.ORDERING_NEEDS_INTEGER, `operator \`${op}\` needs an integer EXPECT`);
|
|
143
|
+
}
|
|
144
|
+
let tail = text.slice(m[0].length).trim();
|
|
145
|
+
let rev = null;
|
|
146
|
+
let guard = false;
|
|
147
|
+
let mutation = null;
|
|
148
|
+
let unpinned = false;
|
|
149
|
+
const revMatch = /^@(\S*)/.exec(tail);
|
|
150
|
+
if (revMatch) {
|
|
151
|
+
tail = tail.slice(revMatch[0].length).trim();
|
|
152
|
+
if (REV_RE.test(revMatch[1])) {
|
|
153
|
+
rev = revMatch[1];
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
statuses.push({ code: exports.BASELINE_STATUS.BAD_REV, detail: `\`@${revMatch[1].slice(0, 60)}\` is not a 7-40 character lowercase hex sha` });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const guardMatch = /^GUARD(?=\s|$)/.exec(tail);
|
|
160
|
+
if (guardMatch) {
|
|
161
|
+
guard = true;
|
|
162
|
+
tail = tail.slice(guardMatch[0].length).trim();
|
|
163
|
+
const mutMatch = /^MUTATION:([\s\S]*)$/.exec(tail);
|
|
164
|
+
if (mutMatch) {
|
|
165
|
+
let ref = mutMatch[1].trim();
|
|
166
|
+
const trailingUnpinned = /(?:^|\s)UNPINNED$/.exec(ref);
|
|
167
|
+
if (trailingUnpinned) {
|
|
168
|
+
unpinned = true;
|
|
169
|
+
ref = ref.slice(0, ref.length - trailingUnpinned[0].length).trim();
|
|
170
|
+
}
|
|
171
|
+
const misplacedPin = /(?:^|\s)(@\S+)$/.exec(ref);
|
|
172
|
+
if (misplacedPin) {
|
|
173
|
+
statuses.push({
|
|
174
|
+
code: exports.BASELINE_STATUS.UNRECOGNIZED_TRAILING,
|
|
175
|
+
detail: `\`${misplacedPin[1].slice(0, 60)}\` must precede GUARD (\`@<rev> GUARD MUTATION: <ref>\`)`,
|
|
176
|
+
});
|
|
177
|
+
ref = ref.slice(0, ref.length - misplacedPin[0].length).trim();
|
|
178
|
+
}
|
|
179
|
+
mutation = ref.length > 0 ? ref : null;
|
|
180
|
+
tail = '';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const unpinnedMatch = /^UNPINNED(?=\s|$)/.exec(tail);
|
|
184
|
+
if (unpinnedMatch) {
|
|
185
|
+
unpinned = true;
|
|
186
|
+
tail = tail.slice(unpinnedMatch[0].length).trim();
|
|
187
|
+
}
|
|
188
|
+
if (tail.length > 0) {
|
|
189
|
+
statuses.push({ code: exports.BASELINE_STATUS.UNRECOGNIZED_TRAILING, detail: `unrecognized text after the notation: \`${tail.slice(0, 60)}\`` });
|
|
190
|
+
}
|
|
191
|
+
if (rev !== null && unpinned) {
|
|
192
|
+
statuses.push({ code: exports.BASELINE_STATUS.PIN_CONFLICT, detail: '`@<sha>` and `UNPINNED` are mutually exclusive' });
|
|
193
|
+
}
|
|
194
|
+
return { notation: { head, op, expect, rev, guard, mutation, unpinned }, statuses };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Rule ids carried by `<!-- plan-criteria-allow: <id> - <reason> -->` markers.
|
|
198
|
+
* The reason must be non-empty. Shared with the #4024 R-rule scanner.
|
|
199
|
+
*/
|
|
200
|
+
function parseCriteriaAllowMarkers(content) {
|
|
201
|
+
const allow = new Set();
|
|
202
|
+
if (typeof content !== 'string')
|
|
203
|
+
return allow;
|
|
204
|
+
const re = new RegExp(ALLOW_RE.source, 'g');
|
|
205
|
+
let m;
|
|
206
|
+
while ((m = re.exec(content)) !== null)
|
|
207
|
+
allow.add(m[1]);
|
|
208
|
+
return allow;
|
|
209
|
+
}
|
|
210
|
+
function emptyResult() {
|
|
211
|
+
return { findings: [], statuses: [], notationCount: 0, notations: [], errors: [], warnings: [] };
|
|
212
|
+
}
|
|
213
|
+
function cap(s) {
|
|
214
|
+
return s.length > TEXT_CAP ? `${s.slice(0, TEXT_CAP)}...` : s;
|
|
215
|
+
}
|
|
216
|
+
function describeB1(n) {
|
|
217
|
+
const cmp = `${renderBaselineValue(n.head)} ${n.op} ${renderBaselineValue(n.expect)}`;
|
|
218
|
+
const base = `criterion already holds at HEAD (${cmp}), so it measures nothing about this change; `
|
|
219
|
+
+ 'tighten the criterion so it fails at HEAD, or record `GUARD MUTATION: <ref>`';
|
|
220
|
+
return n.guard && n.mutation === null
|
|
221
|
+
? `${base} (GUARD is present but its MUTATION reference is missing or empty)`
|
|
222
|
+
: base;
|
|
223
|
+
}
|
|
224
|
+
/** Scan a plan for baseline notations (B1) and legacy baseline comments (B2). */
|
|
225
|
+
function scanBaselineNotation(content, deps = {}) {
|
|
226
|
+
if (typeof content !== 'string' || content.length === 0)
|
|
227
|
+
return emptyResult();
|
|
228
|
+
try {
|
|
229
|
+
const parse = deps.parseNotation ?? parseBaselineNotation;
|
|
230
|
+
const result = emptyResult();
|
|
231
|
+
const allow = parseCriteriaAllowMarkers(content);
|
|
232
|
+
const spans = [];
|
|
233
|
+
for (const tag of ZONE_TAGS)
|
|
234
|
+
spans.push(...(0, markdown_sectionizer_cjs_1.extractTaggedBlockSpans)(content, tag));
|
|
235
|
+
const inZone = (idx) => spans.some((s) => idx >= s.start && idx < s.end);
|
|
236
|
+
let cursorIdx = 0;
|
|
237
|
+
let cursorLine = 1;
|
|
238
|
+
const lineAt = (idx) => {
|
|
239
|
+
if (idx < cursorIdx) {
|
|
240
|
+
cursorIdx = 0;
|
|
241
|
+
cursorLine = 1;
|
|
242
|
+
}
|
|
243
|
+
for (let i = cursorIdx; i < idx; i += 1) {
|
|
244
|
+
const c = content.charCodeAt(i);
|
|
245
|
+
// A bare CR ends a line; the CR of a CRLF pair is counted by its LF.
|
|
246
|
+
if (c === 10 || (c === 13 && content.charCodeAt(i + 1) !== 10))
|
|
247
|
+
cursorLine += 1;
|
|
248
|
+
}
|
|
249
|
+
cursorIdx = idx;
|
|
250
|
+
return cursorLine;
|
|
251
|
+
};
|
|
252
|
+
let droppedStatuses = 0;
|
|
253
|
+
let firstDroppedLine = 0;
|
|
254
|
+
let firstDroppedText = '';
|
|
255
|
+
const addStatus = (code, line, text, detail) => {
|
|
256
|
+
if (result.statuses.length >= exports.MAX_BASELINE_STATUSES) {
|
|
257
|
+
if (droppedStatuses === 0) {
|
|
258
|
+
firstDroppedLine = line;
|
|
259
|
+
firstDroppedText = text;
|
|
260
|
+
}
|
|
261
|
+
droppedStatuses += 1;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
result.statuses.push({ code, line, text: cap(text), detail });
|
|
265
|
+
};
|
|
266
|
+
const addFinding = (rule, line, text, message) => {
|
|
267
|
+
if (allow.has(rule))
|
|
268
|
+
return;
|
|
269
|
+
const severity = rule === 'B1' ? 'error' : 'warning';
|
|
270
|
+
result.findings.push({ rule, severity, line, text: cap(text), message });
|
|
271
|
+
const rendered = `[plan-criteria ${rule}] line ${line}: ${message}`;
|
|
272
|
+
(severity === 'error' ? result.errors : result.warnings).push(rendered);
|
|
273
|
+
};
|
|
274
|
+
const opener = new RegExp(OPENER_RE.source, 'g');
|
|
275
|
+
let om;
|
|
276
|
+
while ((om = opener.exec(content)) !== null) {
|
|
277
|
+
const idx = om.index;
|
|
278
|
+
const line = lineAt(idx);
|
|
279
|
+
const windowEnd = Math.min(content.length, idx + 1 + CLOSE_SEARCH_WINDOW);
|
|
280
|
+
let close = -1;
|
|
281
|
+
for (let i = idx + 1; i < windowEnd; i += 1) {
|
|
282
|
+
const c = content.charCodeAt(i);
|
|
283
|
+
if (c === 93) {
|
|
284
|
+
close = i;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
if (c === 10 || c === 13)
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (close === -1) {
|
|
291
|
+
addStatus(exports.BASELINE_STATUS.UNTERMINATED, line, content.slice(idx, Math.min(windowEnd, idx + TEXT_CAP)), 'no closing `]` on the same line');
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
result.notationCount += 1;
|
|
295
|
+
const text = content.slice(idx, close + 1);
|
|
296
|
+
if (!inZone(idx)) {
|
|
297
|
+
addStatus(exports.BASELINE_STATUS.OUTSIDE_CRITERIA_ZONE, line, text, 'notation is not inside <acceptance_criteria>, <automated> or <verify>; not judged');
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
let parsed;
|
|
301
|
+
try {
|
|
302
|
+
parsed = parse(content.slice(idx + 1, close));
|
|
303
|
+
}
|
|
304
|
+
catch (e) {
|
|
305
|
+
addStatus(exports.BASELINE_STATUS.SCANNER_ERROR, line, text, e instanceof Error ? e.message : String(e));
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
for (const s of parsed.statuses)
|
|
309
|
+
addStatus(s.code, line, text, s.detail);
|
|
310
|
+
const n = parsed.notation;
|
|
311
|
+
if (n === null)
|
|
312
|
+
continue;
|
|
313
|
+
result.notations.push({ line, index: idx, text: cap(text), notation: n });
|
|
314
|
+
const holds = compareBaseline(n.head, n.op, n.expect);
|
|
315
|
+
if (holds === true && !(n.guard && n.mutation !== null)) {
|
|
316
|
+
addFinding(exports.BASELINE_RULE.NON_DISCRIMINATING, line, text, describeB1(n));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const legacy = new RegExp(LEGACY_RE.source, 'gi');
|
|
320
|
+
let lm;
|
|
321
|
+
while ((lm = legacy.exec(content)) !== null) {
|
|
322
|
+
if (!inZone(lm.index))
|
|
323
|
+
continue;
|
|
324
|
+
addFinding(exports.BASELINE_RULE.LEGACY_COMMENT, lineAt(lm.index), lm[0], 'free-form `# baseline at HEAD:` comment is not checked; write `[HEAD=<value> EXPECT<op><value>]` instead');
|
|
325
|
+
}
|
|
326
|
+
if (droppedStatuses > 0) {
|
|
327
|
+
result.statuses.push({
|
|
328
|
+
code: exports.BASELINE_STATUS.TRUNCATED,
|
|
329
|
+
line: firstDroppedLine,
|
|
330
|
+
text: cap(firstDroppedText),
|
|
331
|
+
detail: `${droppedStatuses} further status${droppedStatuses === 1 ? '' : 'es'} omitted after the first ${exports.MAX_BASELINE_STATUSES}`,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
catch (e) {
|
|
337
|
+
const failed = emptyResult();
|
|
338
|
+
failed.statuses.push({
|
|
339
|
+
code: exports.BASELINE_STATUS.SCANNER_ERROR,
|
|
340
|
+
line: 0,
|
|
341
|
+
text: '',
|
|
342
|
+
detail: e instanceof Error ? e.message : String(e),
|
|
343
|
+
});
|
|
344
|
+
return failed;
|
|
345
|
+
}
|
|
346
|
+
}
|