first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

268
scripts/cases.mjs Normal file
View File

@@ -0,0 +1,268 @@
/**
* Adversarial case corpus for Phase 4D parser conformance.
*
* Each case is raw bytes as an author could actually produce them. Nothing here
* is normalized on the way in: the point is what the two parsers do with the
* awkward form, so the awkward form is what is stored.
*/
const SKILL = [
'---',
'id: sample-skill',
'name: Sample Skill',
'description: A sample skill.',
'pages:',
' - candidates',
'status: active',
'actions:',
' - navigate_to_candidates',
'---',
'',
'# Sample Skill',
'',
'## Purpose',
'',
'- Read the pipeline.',
'',
'## Capabilities',
'',
'- Summarize candidates.',
'',
].join('\n');
const AGENT = [
'---',
'id: sample-agent',
'name: Sample Agent',
'description: A sample agent.',
'icon: users',
'status: published',
'version: 2',
'reasoning: balanced',
'trigger: Use on candidates.',
'pages:',
' - candidates',
'skills:',
' - candidate-search',
'starters:',
' - label: Who is waiting?',
' prompt: Who is waiting on a decision?',
'permissions:',
' owner: demo@krow.app',
' access: all',
'---',
'',
'# Sample Agent',
'',
'## Instructions',
'',
'Answer about candidates.',
'',
'## Purpose',
'',
'- Report the pipeline.',
'',
].join('\n');
/** A skill body, with the frontmatter lines replaced wholesale. */
const skillWith = (fmLines) => ['---', ...fmLines, '---', '', '# Sample Skill', '', '## Purpose', '', '- Read the pipeline.', ''].join('\n');
const agentWith = (fmLines) => ['---', ...fmLines, '---', '', '# Sample Agent', '', '## Instructions', '', 'Answer.', ''].join('\n');
const BASE_SKILL_FM = [
'id: sample-skill',
'name: Sample Skill',
'description: A sample skill.',
'pages:',
' - candidates',
];
const BASE_AGENT_FM = [
'id: sample-agent',
'name: Sample Agent',
'description: A sample agent.',
'pages:',
' - candidates',
];
/** BASE_SKILL_FM with one line appended. */
const skillPlus = (...extra) => skillWith([...BASE_SKILL_FM, ...extra]);
const agentPlus = (...extra) => agentWith([...BASE_AGENT_FM, ...extra]);
export const CASES = [
/* ── Baselines ────────────────────────────────────────────────────────── */
{ name: 'baseline-skill', kind: 'skill', raw: SKILL },
{ name: 'baseline-agent', kind: 'agent', raw: AGENT },
/* ── Byte-level shape ─────────────────────────────────────────────────── */
{ name: 'utf8-bom', kind: 'skill', raw: '' + SKILL },
{ name: 'utf8-bom-agent', kind: 'agent', raw: '' + AGENT },
{ name: 'crlf', kind: 'skill', raw: SKILL.replace(/\n/g, '\r\n') },
{ name: 'crlf-agent', kind: 'agent', raw: AGENT.replace(/\n/g, '\r\n') },
{ name: 'cr-only', kind: 'skill', raw: SKILL.replace(/\n/g, '\r') },
{ name: 'bom-crlf-blankline', kind: 'skill', raw: '\r\n' + SKILL.replace(/\n/g, '\r\n') },
{ name: 'leading-blank-line', kind: 'skill', raw: '\n' + SKILL },
{ name: 'multiple-leading-blank-lines', kind: 'skill', raw: '\n\n\n' + SKILL },
{ name: 'leading-spaces-then-blank-lines', kind: 'skill', raw: ' \n \n' + SKILL },
{ name: 'no-trailing-newline', kind: 'skill', raw: SKILL.replace(/\n+$/, '') },
{ name: 'many-trailing-newlines', kind: 'skill', raw: SKILL + '\n\n\n' },
{ name: 'trailing-spaces-on-values', kind: 'skill', raw: skillWith(BASE_SKILL_FM.map((l) => l + ' ')) },
{ name: 'trailing-ws-after-open-fence', kind: 'skill', raw: SKILL.replace(/^---/, '--- ') },
{ name: 'trailing-tab-after-close-fence', kind: 'skill', raw: SKILL.replace(/\n---\n/, '\n---\t\n') },
{ name: 'frontmatter-only-no-body', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---' },
{ name: 'frontmatter-only-trailing-newline', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---\n' },
/* ── Scalars ──────────────────────────────────────────────────────────── */
{ name: 'double-quoted-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Sample Skill"', 'description: "A sample."', 'pages:', ' - candidates']) },
{ name: 'single-quoted-scalar', kind: 'skill', raw: skillWith(["id: sample-skill", "name: 'Sample Skill'", "description: 'A sample.'", 'pages:', ' - candidates']) },
{ name: 'colon-in-quoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Sample: Skill"', 'description: "Note: read this."', 'pages:', ' - candidates']) },
{ name: 'colon-in-unquoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: Note: read this.', 'pages:', ' - candidates']) },
{ name: 'hash-in-quoted-string', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "Shift #1"', 'description: "Tag #ops"', 'pages:', ' - candidates']) },
{ name: 'hash-unquoted-trailing-comment', kind: 'skill', raw: skillPlus('category: ops # a trailing comment') },
{ name: 'hash-unquoted-midword', kind: 'skill', raw: skillPlus('category: ops#1') },
{ name: 'doubled-quote-escape', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: "She said ""go"""', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'full-line-comment', kind: 'skill', raw: skillWith(['# a comment line', ...BASE_SKILL_FM]) },
{ name: 'empty-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description:', 'pages:', ' - candidates']) },
{ name: 'tilde-scalar', kind: 'skill', raw: skillPlus('category: ~') },
{ name: 'null-scalar', kind: 'skill', raw: skillPlus('category: null') },
{ name: 'boolean-scalar', kind: 'agent', raw: agentPlus('webSearch: true') },
{ name: 'integer-scalar', kind: 'agent', raw: agentPlus('version: 3') },
{ name: 'float-scalar', kind: 'agent', raw: agentPlus('version: 1.5') },
{ name: 'negative-integer', kind: 'agent', raw: agentPlus('version: -2') },
/* ── Collections ──────────────────────────────────────────────────────── */
{ name: 'empty-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', 'actions:']) },
{ name: 'inline-flow-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages: [candidates]']) },
{ name: 'inline-flow-map', kind: 'agent', raw: agentPlus('permissions: {owner: a, access: all}') },
{ name: 'sequence-of-mappings', kind: 'agent', raw: AGENT },
{ name: 'nested-mapping', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' owner: demo@krow.app', ' access: specific', ' people:', ' - user: a@b.c', ' role: editor']) },
{ name: 'dash-alone-nested-block', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' -', ' label: Hello', ' prompt: Hello there']) },
{ name: 'tab-indented-sequence', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', '\t- candidates']) },
{ name: 'four-space-indent', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'ragged-indent', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', ' - positions']) },
/* ── Multiline / unsupported YAML ─────────────────────────────────────── */
{ name: 'block-scalar-literal', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: |', ' Line one.', ' Line two.', 'pages:', ' - candidates']) },
{ name: 'block-scalar-folded', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: >', ' Line one.', 'pages:', ' - candidates']) },
{ name: 'anchor-and-alias', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: &n Sample Skill', 'description: *n', 'pages:', ' - candidates']) },
{ name: 'multi-document', kind: 'skill', raw: skillWith([...BASE_SKILL_FM, '---', 'id: second']) },
/* ── Keys ─────────────────────────────────────────────────────────────── */
{ name: 'duplicate-key', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: First Name', 'name: Second Name', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'duplicate-key-array', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', 'pages:', ' - positions']) },
{ name: 'unsupported-frontmatter-field', kind: 'skill', raw: skillPlus('unknownField: whatever') },
{ name: 'unsupported-field-agent', kind: 'agent', raw: agentPlus('nonsense: 1') },
{ name: 'key-with-space', kind: 'skill', raw: skillWith(['id: sample-skill', 'my key: value', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'uppercase-key', kind: 'skill', raw: skillPlus('Category: Ops') },
/* ── Malformed ────────────────────────────────────────────────────────── */
{ name: 'malformed-yaml-bare-line', kind: 'skill', raw: skillWith(['id: sample-skill', 'this is not a pair', 'name: Sample Skill', 'pages:', ' - candidates']) },
{ name: 'malformed-open-fence-two-dashes', kind: 'skill', raw: SKILL.replace(/^---/, '--') },
{ name: 'malformed-open-fence-four-dashes', kind: 'skill', raw: SKILL.replace(/^---/, '----') },
{ name: 'malformed-open-fence-indented', kind: 'skill', raw: ' ' + SKILL },
{ name: 'malformed-open-fence-text-after', kind: 'skill', raw: SKILL.replace(/^---/, '---yaml') },
{ name: 'malformed-close-fence-two-dashes', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n--\n\n# Body\n' },
{ name: 'malformed-close-fence-missing', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n\n# Body\n' },
{ name: 'malformed-close-fence-four-dashes', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n----\n\n# Body\n' },
{ name: 'empty-fence-pair', kind: 'skill', raw: '---\n---\n\n# Body\n' },
{ name: 'empty-fence-with-blank', kind: 'skill', raw: '---\n\n---\n\n# Body\n' },
{ name: 'missing-frontmatter', kind: 'skill', raw: '# Sample Skill\n\n## Purpose\n\n- Read the pipeline.\n' },
{ name: 'empty-markdown', kind: 'skill', raw: '' },
{ name: 'whitespace-only-markdown', kind: 'skill', raw: ' \n\n\t\n' },
{ name: 'frontmatter-is-a-sequence', kind: 'skill', raw: '---\n- one\n- two\n---\n\n# Body\n' },
/* ── Field-level validity ─────────────────────────────────────────────── */
{ name: 'invalid-definition-id-uppercase', kind: 'skill', raw: skillWith(['id: Sample_Skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-definition-id-leading-dash', kind: 'skill', raw: skillWith(['id: -sample', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-definition-id-underscore', kind: 'skill', raw: skillWith(['id: sample_skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-id-derives-from-name', kind: 'skill', raw: skillWith(['name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-name', kind: 'skill', raw: skillWith(['id: sample-skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-pages', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.']) },
{ name: 'unknown-page', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - nowhere']) },
{ name: 'page-alias-uppercase', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - Candidates']) },
{ name: 'page-alias-underscore', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - talent_pool']) },
{ name: 'invalid-status-skill', kind: 'skill', raw: skillPlus('status: bogus') },
{ name: 'inactive-status-skill', kind: 'skill', raw: skillPlus('status: inactive') },
{ name: 'invalid-status-agent', kind: 'agent', raw: agentPlus('status: bogus') },
{ name: 'invalid-reasoning-agent', kind: 'agent', raw: agentPlus('reasoning: turbo') },
{ name: 'invalid-icon-agent', kind: 'agent', raw: agentPlus('icon: rocket') },
{ name: 'invalid-version-agent', kind: 'agent', raw: agentPlus('version: zero') },
{ name: 'invalid-field-type-pages-scalar', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages: candidates']) },
{ name: 'invalid-field-type-pages-scalar-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages: candidates']) },
{ name: 'invalid-field-type-name-list', kind: 'skill', raw: skillWith(['id: sample-skill', 'name:', ' - a', ' - b', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'invalid-permissions-access', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' access: nobody']) },
{ name: 'self-subagent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'subagents:', ' - sample-agent']) },
/* ── Size ─────────────────────────────────────────────────────────────── */
{ name: 'oversized-markdown', kind: 'skill', raw: skillPlus() + '\n' + 'x'.repeat(65600) },
{ name: 'at-size-bound', kind: 'skill', raw: (() => { const b = skillPlus(); return b + 'y'.repeat(65536 - b.length); })() },
/* ── Gaps closed after the first oracle run ───────────────────────────── */
{ name: 'missing-name-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'missing-pages-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.']) },
{ name: 'unknown-page-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - nowhere']) },
{ name: 'oversized-agent', kind: 'agent', raw: agentPlus() + '\n' + 'x'.repeat(65600) },
{ name: 'visibility-field-personal', kind: 'skill', raw: skillPlus('visibility: personal') },
{ name: 'visibility-field-invalid', kind: 'skill', raw: skillPlus('visibility: nobody') },
{ name: 'blank-page-entry-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates', ' - ""']) },
{ name: 'duplicate-page-entry-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates', ' - candidates']) },
{ name: 'id-with-trailing-space', kind: 'skill', raw: skillWith(['id: sample-skill ', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-quoted', kind: 'skill', raw: skillWith(['id: "sample-skill"', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'starters-plain-strings-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' - Who is waiting?', ' - Where are the gaps?']) },
{ name: 'starter-missing-label-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'starters:', ' - prompt: Only a prompt']) },
{ name: 'knowledge-note-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A note', ' body: The body of the note.']) },
{ name: 'knowledge-bad-kind-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A note', ' kind: rumour', ' body: x']) },
{ name: 'knowledge-link-without-url-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'knowledge:', ' - label: A link', ' kind: link']) },
{ name: 'skills-scalar-coerced-agent', kind: 'agent', raw: agentPlus('skills: candidate-search') },
{ name: 'skills-blank-entry-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'skills:', ' - candidate-search', ' - ""']) },
{ name: 'web-search-snake-case-agent', kind: 'agent', raw: agentPlus('web_search: true') },
{ name: 'web-search-string-true-agent', kind: 'agent', raw: agentPlus('webSearch: "true"') },
{ name: 'version-quoted-integer-agent', kind: 'agent', raw: agentPlus('version: "3"') },
{ name: 'version-zero-agent', kind: 'agent', raw: agentPlus('version: 0') },
{ name: 'version-empty-agent', kind: 'agent', raw: agentPlus('version:') },
{ name: 'crlf-inside-frontmatter-only', kind: 'skill', raw: (() => { const i = SKILL.indexOf('\n---\n', 3); return SKILL.slice(0, i).replace(/\n/g, '\r\n') + SKILL.slice(i); })() },
{ name: 'body-whitespace-only-after-fence', kind: 'skill', raw: '---\n' + BASE_SKILL_FM.join('\n') + '\n---\n \n\t\n' },
{ name: 'description-whitespace-only', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: " "', 'pages:', ' - candidates']) },
{ name: 'permissions-null-agent', kind: 'agent', raw: agentPlus('permissions:') },
{ name: 'permissions-people-bad-role-agent', kind: 'agent', raw: agentWith([...BASE_AGENT_FM, 'permissions:', ' access: specific', ' people:', ' - user: a@b.c', ' role: overlord']) },
/* ── Gaps closed after the second oracle run ──────────────────────────────
*
* Two rules had no case that could tell a correct implementation from a
* plausible wrong one, so both were being asserted by accident:
*
* The id fallback chain. Every earlier case either declares `id:` or
* declares a `name:` to slug, so the THIRD link — the default path, which
* is the literal `custom` on both sides — was never reached by a case that
* is otherwise valid. `id-omitted-unnamed` is that case: the frontend
* accepts it as the skill `custom`, and a backend deriving no id would
* refuse it on save.
*
* Text coercion. `name`, `description`, `category`, `trigger` and the
* entries of `pages` are text columns in migration 000005, and a definition
* may write any of them as a list, a number or a boolean. The frontend
* keeps the raw value on the record and stringifies it at each use; the
* backend has to choose the string at the boundary. These cases pin which
* string it chooses.
*/
{ name: 'id-omitted-unnamed', kind: 'skill', raw: skillWith(['description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-named', kind: 'skill', raw: skillWith(['name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-unnamed-agent', kind: 'agent', raw: agentWith(['description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-named-agent', kind: 'agent', raw: agentWith(['name: Sample Agent', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'id-omitted-name-unsluggable', kind: 'skill', raw: skillWith(['name: "!!!"', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'name-numeric', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: 42', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'name-boolean', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: true', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'description-list', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description:', ' - one', ' - two', 'pages:', ' - candidates']) },
{ name: 'description-numeric', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: 7', 'pages:', ' - candidates']) },
{ name: 'pages-numeric-entry', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - candidates', ' - 5']) },
{ name: 'pages-mapping-entry', kind: 'skill', raw: skillWith(['id: sample-skill', 'name: Sample Skill', 'description: A sample.', 'pages:', ' - page: candidates']) },
{ name: 'trigger-list-agent', kind: 'agent', raw: agentPlus('trigger:', ' - one', ' - two') },
{ name: 'name-list-agent', kind: 'agent', raw: agentWith(['id: sample-agent', 'name:', ' - a', ' - b', 'description: A sample.', 'pages:', ' - candidates']) },
{ name: 'category-numeric', kind: 'skill', raw: skillPlus('category: 3') },
/* The other backend-only bound. agent_definitions.version is a PostgreSQL
`integer`; the frontend accepts any whole number of 1 or more, so a version
above 2^31-1 is one the editor takes and the database cannot store. It had
no case at all, which left the rule asserted by nobody. */
{ name: 'version-above-int32-agent', kind: 'agent', raw: agentPlus('version: 3000000000') },
{ name: 'version-at-int32-agent', kind: 'agent', raw: agentPlus('version: 2147483647') },
];