4.3: the directive floor counts directive-only and carried types in the parsed document, and run parameters name the gate
CI / gate (push) Successful in 25s
CI / publish (push) Has been skipped

This commit is contained in:
2026-09-15 21:44:49 +02:00
parent ef71d55c13
commit 5fe2e6ec42
2 changed files with 20 additions and 8 deletions
+17 -5
View File
@@ -2,6 +2,7 @@ import fc from 'fast-check'
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import test from 'node:test' import test from 'node:test'
import type { AdfDocument } from './adf/document.ts'
import type { Arbitrary, DepthIdentifier } from 'fast-check' import type { Arbitrary, DepthIdentifier } from 'fast-check'
import type { AttributeVocabulary } from './adf/attribute-vocabulary.ts' import type { AttributeVocabulary } from './adf/attribute-vocabulary.ts'
import type { JsonValue } from './json-value.ts' import type { JsonValue } from './json-value.ts'
@@ -18,6 +19,7 @@ import { markAttributes } from './adf/mark-attributes.ts'
import { markSpelling } from './markdown/mark-spellings.ts' import { markSpelling } from './markdown/mark-spellings.ts'
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts' import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
import { marksAttribute } from './markdown/block-directive-marks.ts' import { marksAttribute } from './markdown/block-directive-marks.ts'
import { nodeContent, nodeMarks } from './adf/document.ts'
import { serializeCanonicalJson } from './canonical-json.ts' import { serializeCanonicalJson } from './canonical-json.ts'
import { spellAttributes, spellJsonAttribute, spellLeafDirective, spellStringAttribute, spellVocabulary } from './markdown/directive-syntax.ts' import { spellAttributes, spellJsonAttribute, spellLeafDirective, spellStringAttribute, spellVocabulary } from './markdown/directive-syntax.ts'
import { textDirectiveName } from './markdown/text-directive.ts' import { textDirectiveName } from './markdown/text-directive.ts'
@@ -32,10 +34,11 @@ type InlineMarkdown = { destination: Arbitrary<string>; inlines: Arbitrary<strin
type LeafMarkdown = { fencedCode: Arbitrary<string>; leafBlock: Arbitrary<string> } type LeafMarkdown = { fencedCode: Arbitrary<string>; leafBlock: Arbitrary<string> }
const directiveShapedFloor = 350 const commonMarkTypes = new Set(['blockquote', 'bulletList', 'codeBlock', 'hardBreak', 'heading', 'listItem', 'orderedList', 'paragraph', 'rule', 'text'])
const directiveSpelling = /(?<!\\):[a-z][A-Za-z0-9]*[[{]|^[\t >*+.)0-9-]*::+[a-z]/m const directiveShapedFloor = 330
const fixpointFloor = 600 const fixpointFloor = 600
const gateRuns = 1000 const gateRuns = 1000
const markdownMarkTypes = new Set(Object.keys(markAttributes).filter((type) => markSpelling(type)?.kind !== 'directive'))
const vocabularies = [...Object.values(blockDirectives).map((directive) => directive.attributes), ...Object.values(inlineDirectives).map((directive) => directive.attributes), ...Object.values(markAttributes)] const vocabularies = [...Object.values(blockDirectives).map((directive) => directive.attributes), ...Object.values(inlineDirectives).map((directive) => directive.attributes), ...Object.values(markAttributes)]
const attributeKeys = [ const attributeKeys = [
@@ -392,6 +395,15 @@ const document = fc
.tuple(markdown, fc.oneof({ arbitrary: fc.constant('\n'), weight: 8 }, { arbitrary: fc.constantFrom('\r\n', '\r'), weight: 1 }), fc.constantFrom('', '', '\n', ' \n')) .tuple(markdown, fc.oneof({ arbitrary: fc.constant('\n'), weight: 8 }, { arbitrary: fc.constantFrom('\r\n', '\r'), weight: 1 }), fc.constantFrom('', '', '\n', ' \n'))
.map(([held, ending, trailing]) => `${held}${trailing}`.replaceAll('\n', ending)) .map(([held, ending, trailing]) => `${held}${trailing}`.replaceAll('\n', ending))
function holdsDirectiveShape(document: AdfDocument): boolean {
const pending = [...nodeContent(document)]
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
if (!commonMarkTypes.has(node.type) || nodeMarks(node).some((mark) => !markdownMarkTypes.has(mark.type))) return true
pending.push(...nodeContent(node))
}
return false
}
test('generated markdown refuses, or what it parses to refuses to emit, or its spelling reads back and spells itself', { timeout: propertyTimeout }, () => { test('generated markdown refuses, or what it parses to refuses to emit, or its spelling reads back and spells itself', { timeout: propertyTimeout }, () => {
const parameters = propertyRuns(gateRuns) const parameters = propertyRuns(gateRuns)
let directiveShaped = 0 let directiveShaped = 0
@@ -403,7 +415,7 @@ test('generated markdown refuses, or what it parses to refuses to emit, or its s
const emitted = adfToMarkdown(parsed.value) const emitted = adfToMarkdown(parsed.value)
if (!emitted.ok) return if (!emitted.ok) return
fixpoints += 1 fixpoints += 1
if (directiveSpelling.test(emitted.value)) directiveShaped += 1 if (holdsDirectiveShape(parsed.value)) directiveShaped += 1
const read = markdownToAdf(emitted.value) const read = markdownToAdf(emitted.value)
assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`) assert.ok(read.ok, read.ok ? '' : `${read.error.code}: ${read.error.message} — reading ${JSON.stringify(emitted.value)}`)
assert.deepEqual(toEditorNormal(read.value), toEditorNormal(parsed.value), `reading ${JSON.stringify(emitted.value)}`) assert.deepEqual(toEditorNormal(read.value), toEditorNormal(parsed.value), `reading ${JSON.stringify(emitted.value)}`)
@@ -413,8 +425,8 @@ test('generated markdown refuses, or what it parses to refuses to emit, or its s
}), }),
parameters, parameters,
) )
if (parameters.seed === undefined) return if (!parameters.gate) return
assert.ok(fixpoints >= fixpointFloor, `${fixpoints} of ${gateRuns} runs reached the fixpoint, under the floor of ${fixpointFloor}`) assert.ok(fixpoints >= fixpointFloor, `${fixpoints} of ${gateRuns} runs reached the fixpoint, under the floor of ${fixpointFloor}`)
assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint spelled a directive, under the floor of ${directiveShapedFloor}`) assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark only a directive or the carry spells, under the floor of ${directiveShapedFloor}`)
}) })
+3 -3
View File
@@ -158,9 +158,9 @@ const positions = fc.letrec<Positions>((tie) => {
export const adfDocument = fc.array(positions.block, { depthIdentifier, maxLength: 4, minLength: 1 }).map((content): AdfDocument => toEditorNormal({ content, type: 'doc', version: 1 })) export const adfDocument = fc.array(positions.block, { depthIdentifier, maxLength: 4, minLength: 1 }).map((content): AdfDocument => toEditorNormal({ content, type: 'doc', version: 1 }))
export function propertyRuns(gateRuns: number): { numRuns: number; seed?: number } { export function propertyRuns(gateRuns: number): { gate: boolean; numRuns: number; seed?: number } {
const deepRuns = env[deepRunsVariable] const deepRuns = env[deepRunsVariable]
if (deepRuns === undefined) return { numRuns: gateRuns, seed: gateSeed } if (deepRuns === undefined) return { gate: true, numRuns: gateRuns, seed: gateSeed }
assert.ok(/^[1-9]\d*$/.test(deepRuns), `${deepRunsVariable} is a run count in digits, such as ${deepRunsVariable}=10000: found ${JSON.stringify(deepRuns)}`) assert.ok(/^[1-9]\d*$/.test(deepRuns), `${deepRunsVariable} is a run count in digits, such as ${deepRunsVariable}=10000: found ${JSON.stringify(deepRuns)}`)
return { numRuns: Number(deepRuns) } return { gate: false, numRuns: Number(deepRuns) }
} }