Skip to content

Commit 9081819

Browse files
committed
test(yaml): block-scalar content-floor depth gate — close the indent-side verification asymmetry
The block-scalar carrier (§2a⁗) shares the consuming-carry CORE with #block-sequence, but its content FLOOR is a per-construct body — so the block-sequence deepest-sibling probe cannot catch a floor regression. This sweeps a compact-nested `|N`/`|` block scalar d=1..12 (explicit |2/|3 + auto) and asserts, bidirectionally vs the eemeli CST oracle, that the floor line is block content and the floor-1 line is a comment — catching both an under-floor (scalar ends too early) and an over-floor (deeper lines swallowed as block string, the monogram#14 saturation) regression. Validated: against the pre-#14 flat-floor grammar (0ff0c64) the probe flags 22 floor-1 mismatches at d>=2. Wired as yaml/blockscalar-depth. With angle-depth (delimiter+lookahead) this completes the depth- verification coverage across all three depth-bug-bearing classes (indent-carry, angle/\g<>); suite 39/39.
1 parent a9c4bbd commit 9081819

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

test/check.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const GATES: Gate[] = [
5151
{ group: 'yaml', name: 'flow-sites', args: ['test/flow-sites.ts'] },
5252
{ group: 'yaml', name: 'compact-nest-sites', args: ['test/compact-nest-sites.ts'] },
5353
{ group: 'yaml', name: 'deepest-sibling', args: ['test/yaml-deepest-sibling-probe.ts'] },
54+
{ group: 'yaml', name: 'blockscalar-depth', args: ['test/yaml-blockscalar-depth-probe.ts'] },
5455
{ group: 'generative', name: 'scope≡role', args: ['test/generative.ts'] },
5556
{ group: 'generative', name: 'gap-ledger-selftest', args: ['test/gap-ledger-selftest.ts'] },
5657
{ group: 'generative', name: 'gap-ledger-check', args: ['test/gap-ledger.ts', '--check'] },
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// yaml-blockscalar-depth-probe.ts — the depth gate for the BLOCK-SCALAR carrier (§2a⁗), the indent
2+
// construct whose body-mode (the content FLOOR) is per-construct and therefore NOT covered by the
3+
// block-sequence deepest-sibling probe even though both share the consuming-carry core.
4+
//
5+
// A `|N`/`>N`/`|` block scalar whose mapping sits in a compact block sequence (`- - … k: |N`) has a
6+
// content floor; a line AT the floor is block-scalar content, a line one column SHALLOWER ends the
7+
// scalar (a comment / sibling). The §2a⁗ carry must place that floor correctly at ARBITRARY compact
8+
// depth (the monogram#12 #14 class: a flat floor saturated at depth ≥ 2). This sweeps d=1..12 and
9+
// checks BOTH directions against the eemeli CST oracle: the floor line must be block content (an
10+
// UNDER-floor regression paints it a comment), the floor-1 line must be a comment (an OVER-floor
11+
// regression paints it block string). Covers explicit `|N` (N=2,3) and the auto-detect `|`.
12+
//
13+
// Run (bare node): node test/yaml-blockscalar-depth-probe.ts · Exit 0 iff zero mismatch.
14+
import { readFileSync } from 'node:fs';
15+
import { createRequire } from 'node:module';
16+
import vsctm from 'vscode-textmate';
17+
import onig from 'vscode-oniguruma';
18+
import YAML from 'yaml';
19+
20+
const { INITIAL, Registry } = vsctm;
21+
const { loadWASM, OnigScanner, OnigString } = onig;
22+
const require = createRequire(import.meta.url);
23+
const wasm = readFileSync(require.resolve('vscode-oniguruma/release/onig.wasm'));
24+
await loadWASM(wasm.buffer.slice(wasm.byteOffset, wasm.byteOffset + wasm.byteLength));
25+
26+
const raw = JSON.parse(readFileSync('./yaml.tmLanguage.json', 'utf8'));
27+
const registry = new Registry({
28+
onigLib: Promise.resolve({
29+
createOnigScanner: (ps: string[]) => new OnigScanner(ps),
30+
createOnigString: (s: string) => new OnigString(s),
31+
}),
32+
loadGrammar: async (scope: string) => (scope === raw.scopeName ? raw : null),
33+
});
34+
const grammar = await registry.loadGrammar(raw.scopeName);
35+
36+
function scopeAt(src: string, lineIdx: number, col: number): string | null {
37+
const lines = src.split('\n');
38+
let rs = INITIAL;
39+
let res: string | null = null;
40+
for (let i = 0; i < lines.length; i++) {
41+
const r = grammar!.tokenizeLine(lines[i], rs);
42+
if (i === lineIdx) for (const t of r.tokens) if (col >= t.startIndex && col < t.endIndex) res = t.scopes[t.scopes.length - 1];
43+
rs = r.ruleStack;
44+
}
45+
return res;
46+
}
47+
48+
// eemeli CST oracle: the kind of token covering the byte offset of (lineIdx, col).
49+
function oracleKind(src: string, lineIdx: number, col: number): 'blockscalar' | 'comment' | '?' {
50+
const lines = src.split('\n');
51+
let off = 0;
52+
for (let i = 0; i < lineIdx; i++) off += lines[i].length + 1;
53+
off += col;
54+
let kind: 'blockscalar' | 'comment' | '?' = '?';
55+
const walk = (t: any) => {
56+
if (!t || typeof t !== 'object') return;
57+
if (t.offset != null && typeof t.source === 'string' && off >= t.offset && off < t.offset + t.source.length) {
58+
if (t.type === 'comment') kind = 'comment';
59+
else if (t.type === 'block-scalar') kind = 'blockscalar';
60+
}
61+
for (const k in t) if (t[k] && typeof t[k] === 'object') walk(t[k]);
62+
};
63+
for (const tok of new YAML.Parser().parse(src)) walk(tok);
64+
return kind;
65+
}
66+
67+
const bad: string[] = [];
68+
let checked = 0;
69+
70+
// At each depth, the floor line (indent = floor) must be block content; the floor-1 line a comment.
71+
function check(label: string, src: string, floor: number) {
72+
// floor line is line 1 for explicit (header, floor-line, below) — caller passes the line layout via
73+
// marker text; we locate the two marker lines by their leading-space width.
74+
const lines = src.split('\n');
75+
const floorLine = lines.findIndex((l) => l.startsWith(' '.repeat(floor) + '#atfloor'));
76+
const belowLine = lines.findIndex((l) => l.startsWith(' '.repeat(floor - 1) + '#below'));
77+
if (floorLine < 0 || belowLine < 0) { bad.push(`${label}: witness malformed`); return; }
78+
checked += 2;
79+
const gF = scopeAt(src, floorLine, floor), oF = oracleKind(src, floorLine, floor);
80+
const gB = scopeAt(src, belowLine, floor - 1), oB = oracleKind(src, belowLine, floor - 1);
81+
// floor line: oracle blockscalar, grammar string.unquoted.block (UNDER-floor if grammar says comment)
82+
if (!(oF === 'blockscalar' && /string\.unquoted\.block/.test(gF ?? ''))) bad.push(`${label} FLOOR@${floor}: grammar «${gF}» oracle «${oF}» (want block content)`);
83+
// floor-1 line: oracle comment, grammar comment (OVER-floor if grammar says block string)
84+
if (!(oB === 'comment' && /comment/.test(gB ?? ''))) bad.push(`${label} FLOOR-1@${floor - 1}: grammar «${gB}» oracle «${oB}» (want comment)`);
85+
}
86+
87+
for (let d = 1; d <= 12; d++) {
88+
const dash = '- '.repeat(d);
89+
const prefix = dash.length; // 2*d
90+
// EXPLICIT |N (N = 2, 3): floor = prefix + N.
91+
for (const N of [2, 3]) {
92+
const floor = prefix + N;
93+
const src = `${dash}k: |${N}\n` + ' '.repeat(floor) + '#atfloor\n' + ' '.repeat(floor - 1) + '#below\n';
94+
if (YAML.parseDocument(src).errors.length === 0) check(`d${d} |${N}`, src, floor);
95+
}
96+
// AUTO-detect |: the floor is the first body line's indent; place it at prefix+2.
97+
{
98+
const floor = prefix + 2;
99+
const src = `${dash}k: |\n` + ' '.repeat(floor) + '#atfloor\n' + ' '.repeat(floor - 1) + '#below\n';
100+
if (YAML.parseDocument(src).errors.length === 0) check(`d${d} |auto`, src, floor);
101+
}
102+
}
103+
104+
console.log(`checked ${checked} (floor, floor-1) assertions at d=1..12 (explicit |2/|3 + auto |) vs the eemeli CST oracle`);
105+
for (const b of bad) console.log(` ✗ ${b}`);
106+
console.log(bad.length === 0 ? '\n✓ block-scalar content floor correct at every depth (no saturation)' : `\n✗ ${bad.length} mismatch(es)`);
107+
process.exit(bad.length === 0 ? 0 : 1);

0 commit comments

Comments
 (0)