|
16 | 16 | import { normScope } from './scope-roles.ts'; |
17 | 17 | import type { CstNode, CstChild } from '../src/gen-parser.ts'; |
18 | 18 | import type { CstGrammar, TokenPattern } from '../src/types.ts'; |
19 | | -import type { GenOptions } from './grammar-gen.ts'; |
| 19 | +import { commentSpec, type GenOptions, type GenInput } from './grammar-gen.ts'; |
20 | 20 |
|
21 | 21 | // The generation knobs BOTH consumers use, so the gap ledger sees the SAME derived corpus (hence the |
22 | 22 | // SAME divergence set) as generative.ts's check. `seed` is a no-op (the generator is a pure function |
@@ -173,9 +173,59 @@ export function collectViolations(args: { |
173 | 173 | return out; |
174 | 174 | } |
175 | 175 |
|
| 176 | +// ── COMMENT-WITNESS check (the last coverage hole: comment scopes) ─────────────────────────────── |
| 177 | +// A comment is a `skip:true` token the parser DROPS — it never becomes a CST leaf, so the leaf walk above |
| 178 | +// (and `checkedTokens`) can NEVER reach a comment's highlighter scope (0% coverage). The generator closes |
| 179 | +// that by INJECTING a comment at a safe position and recording its span as a WITNESS in `GenInput.tokens` |
| 180 | +// (grammar-gen.ts §8). THIS is the consumer: for each recorded comment witness, the flat highlighter must |
| 181 | +// paint that span with the COMMENT bucket — graded with the SAME `scopeBucket` partition the rest of the |
| 182 | +// check uses. The judge compares against the GENERATOR'S witness, not a parser leaf (there is none) — the |
| 183 | +// crux the prompt names. Leniency mirrors `collectViolations`: a comment is CONSISTENT if the highlighter |
| 184 | +// paints ANY part of its span `comment` (so the `<!--`/`-->` punctuation sub-scope is fine, only the body |
| 185 | +// needs `comment`); a span with NO `comment` anywhere (painted entirely string / text / etc.) is the gap. |
| 186 | + |
| 187 | +// The comment-token NAMES of a grammar (so a witness in `tokens` is identifiable as a comment) — the SAME |
| 188 | +// generic discovery the generator uses, memoised per grammar object so repeated calls are cheap. |
| 189 | +const commentNamesCache = new WeakMap<object, Set<string>>(); |
| 190 | +export function commentTokenNames(grammar: CstGrammar): Set<string> { |
| 191 | + let s = commentNamesCache.get(grammar); |
| 192 | + if (!s) { s = commentSpec(grammar)?.names ?? new Set<string>(); commentNamesCache.set(grammar, s); } |
| 193 | + return s; |
| 194 | +} |
| 195 | + |
| 196 | +// The comment WITNESSES of one generated input: the `tokens` entries whose name is a comment token. ONLY |
| 197 | +// a comment-INJECTED input (its strategy carries the `comment:` marker) has authoritative, span-verified |
| 198 | +// witnesses — there `tokens` is exactly the comment(s) the generator spliced at a known offset. A BASE |
| 199 | +// input may ALSO carry `materialize`-recorded comment tokens (the grammar can emit a native `<!-- -->`), |
| 200 | +// but those spans are unreliable for markup fragments (a degenerate `> <!--x-->` mis-spans, an empty |
| 201 | +// `<!---->` is all-punctuation) and were never meant as a ground-truth witness — so they are NOT graded. |
| 202 | +export function commentWitnesses(grammar: CstGrammar, input: GenInput): GenInput['tokens'] { |
| 203 | + if (!input.strategy.includes('comment:')) return []; |
| 204 | + const names = commentTokenNames(grammar); |
| 205 | + return names.size ? input.tokens.filter((t) => names.has(t.name) && t.end > t.start) : []; |
| 206 | +} |
| 207 | + |
| 208 | +// Grade each comment witness: a divergence iff the highlighter painted NO part of the witness span with the |
| 209 | +// `comment` bucket. Returns the divergences as `Violation`s (kind `#comment uncolored`), filed like the |
| 210 | +// others so they flow into the same report / gate / gap-ledger plumbing. |
| 211 | +export function collectCommentViolations(args: { grammar: CstGrammar; input: string; strategy: string; witnesses: GenInput['tokens']; toks: TmTok[] }): Violation[] { |
| 212 | + const out: Violation[] = []; |
| 213 | + for (const w of args.witnesses) { |
| 214 | + const got = spanBuckets(args.toks, args.input, w.start, w.end); |
| 215 | + if (got.has('comment')) continue; // painted as a comment somewhere → consistent |
| 216 | + const gotBucket = [...got][0] ?? 'none'; |
| 217 | + out.push({ input: args.input, strategy: args.strategy, pos: w.start, text: w.text, tokenType: w.name, expected: 'comment', got: gotBucket, gotScope: innerOf(scopeAt(args.toks, w.start)), kind: '#comment uncolored' }); |
| 218 | + } |
| 219 | + return out; |
| 220 | +} |
| 221 | + |
176 | 222 | // What GATES vs what is a report-only DISCOVERY (generative.ts's exact predicate): |
177 | 223 | // • an ANCHORED-MARKER misfire (#23) ALWAYS gates. |
178 | 224 | // • a STRUCTURAL-LITERAL→content divergence (#24) gates on the STRUCTURED strategies, but is |
179 | 225 | // report-only on FUZZ inputs (a standing flat-TM frontier limit). The gap ledger lists the |
180 | 226 | // DISCOVERED ones (the !isGated set) — the floor-blind divergences a corpus metric is blind to. |
181 | | -export const isGated = (v: { kind: string; strategy: string }): boolean => v.kind.startsWith('#23') || !v.strategy.startsWith('fuzz'); |
| 227 | +// • a COMMENT-uncolored divergence ALWAYS gates: a comment (parser-dropped, highlighter-scoped |
| 228 | +// `comment.*` by construction) painted as a NON-comment is unambiguous — there is no legitimate |
| 229 | +// "frontier limit" where an injected comment is not a comment (its position is chosen safe + it |
| 230 | +// round-trips). On today's correct grammars this finds 0; it CATCHES a future scope regression. |
| 231 | +export const isGated = (v: { kind: string; strategy: string }): boolean => v.kind.startsWith('#23') || v.kind.startsWith('#comment') || !v.strategy.startsWith('fuzz'); |
0 commit comments