@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.
Files changed (135) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +234 -126
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/gsd-core/bin/gsd-tools.cjs +450 -753
  11. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  12. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  13. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  14. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  15. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  16. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  17. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  18. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  19. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  20. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  21. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  22. package/gsd-core/bin/lib/commands.cjs +80 -2
  23. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  24. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  25. package/gsd-core/bin/lib/config.cjs +139 -101
  26. package/gsd-core/bin/lib/configuration.cjs +41 -2
  27. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  28. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  29. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  30. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  31. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  32. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  33. package/gsd-core/bin/lib/graphify.cjs +19 -2
  34. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  35. package/gsd-core/bin/lib/init.cjs +13 -11
  36. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  37. package/gsd-core/bin/lib/install-engine.cjs +11 -19
  38. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  39. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  40. package/gsd-core/bin/lib/io.cjs +6 -0
  41. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  42. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  43. package/gsd-core/bin/lib/milestone.cjs +8 -6
  44. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  45. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  46. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  47. package/gsd-core/bin/lib/phase.cjs +107 -31
  48. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  49. package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
  50. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  51. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  52. package/gsd-core/bin/lib/project-context.cjs +439 -0
  53. package/gsd-core/bin/lib/project-root.cjs +9 -230
  54. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  55. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  56. package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
  57. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  58. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  59. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
  60. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  61. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  62. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  63. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  64. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  65. package/gsd-core/bin/lib/state-transition.cjs +91 -41
  66. package/gsd-core/bin/lib/state.cjs +446 -61
  67. package/gsd-core/bin/lib/surface.cjs +4 -7
  68. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  69. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  70. package/gsd-core/bin/lib/verify.cjs +49 -6
  71. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  72. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  73. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  74. package/gsd-core/references/checkpoints.md +1 -1
  75. package/gsd-core/references/planner-guidance.md +11 -0
  76. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  77. package/gsd-core/references/planner-reversibility.md +2 -0
  78. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  79. package/gsd-core/workflows/autonomous.md +4 -4
  80. package/gsd-core/workflows/code-review-fix.md +5 -7
  81. package/gsd-core/workflows/execute-phase.md +21 -7
  82. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  83. package/gsd-core/workflows/plan-phase.md +18 -19
  84. package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
  85. package/hooks/dist/gsd-config-reload.js +2 -2
  86. package/hooks/dist/gsd-context-monitor.js +2 -1
  87. package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
  88. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  89. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-workflow-guard.js +2 -1
  92. package/hooks/dist/lib/config-read.js +49 -0
  93. package/hooks/dist/lib/git-cmd.js +3 -1
  94. package/hooks/dist/lib/git-head-tree.js +474 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +97 -17
  96. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  97. package/hooks/gsd-agent-isolation-guard.js +3 -2
  98. package/hooks/gsd-config-reload.js +2 -2
  99. package/hooks/gsd-context-monitor.js +2 -1
  100. package/hooks/gsd-cursor-subagent-start.js +8 -5
  101. package/hooks/gsd-read-injection-scanner.js +2 -2
  102. package/hooks/gsd-secret-read-guard.js +141 -6
  103. package/hooks/gsd-statusline.js +3 -2
  104. package/hooks/gsd-workflow-guard.js +2 -1
  105. package/hooks/lib/config-read.js +49 -0
  106. package/hooks/lib/git-cmd.js +3 -1
  107. package/hooks/lib/git-head-tree.js +474 -0
  108. package/hooks/lib/isolation-sentinel.js +97 -17
  109. package/hooks/lib/secret-read-exemptions.js +161 -0
  110. package/package.json +7 -2
  111. package/pi/package-agents/gsd-executor.md +1 -1
  112. package/pi/package-agents/gsd-plan-checker.md +1 -1
  113. package/pi/package-agents/gsd-planner.md +3 -3
  114. package/pi/package-agents/gsd-verifier.md +2 -2
  115. package/pi/package-agents/manifest.json +4 -4
  116. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  117. package/scripts/changeset/cli.cjs +2 -2
  118. package/scripts/changeset/serialize.cjs +12 -2
  119. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  120. package/scripts/ci-fix-next-rerun.cjs +149 -0
  121. package/scripts/ci-next-health.cjs +52 -9
  122. package/scripts/ci-test-scope.cjs +77 -71
  123. package/scripts/command-contract-helpers.cjs +56 -7
  124. package/scripts/docs-guard-registry.cjs +3 -0
  125. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  126. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  127. package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
  128. package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
  129. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  130. package/scripts/lint-removed-but-needed.cjs +562 -50
  131. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  132. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  133. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  134. package/skills/gsd-autonomous/SKILL.md +1 -1
  135. 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(/&gt;/g, '>').replace(/&lt;/g, '<').replace(/&amp;/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
+ }