From e7db610849e73190dbfce8eea6eb60929174894f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Wed, 13 May 2026 21:40:57 -0600 Subject: [PATCH 001/130] Add README family section template helpers --- packages/ast-template/src/index.ts | 88 ++++++++++++++++++- .../test/session.integration.test.ts | 50 +++++++++++ 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/packages/ast-template/src/index.ts b/packages/ast-template/src/index.ts index ff06f4f..75c32b1 100644 --- a/packages/ast-template/src/index.ts +++ b/packages/ast-template/src/index.ts @@ -11,7 +11,8 @@ import { DEFAULT_TEMPLATE_TOKEN_CONFIG, applyTemplateTreeExecutionToDirectory, planTemplateTreeExecutionFromDirectories, - reportTemplateDirectoryRunner + reportTemplateDirectoryRunner, + resolveTemplateTokens } from '@structuredmerge/ast-merge'; import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; @@ -19,6 +20,91 @@ import { mergeToml } from '../../toml-merge/src/index'; export const packageName = '@structuredmerge/ast-template'; +export const README_FAMILY_LANGUAGE_ORDER = ['go', 'ruby', 'rust', 'typescript'] as const; + +const README_FAMILY_LANGUAGE_LABELS: Readonly> = { + go: 'Go', + ruby: 'Ruby', + rust: 'Rust', + typescript: 'TypeScript' +}; + +function readmeFamilyLabel(language: string): string { + return README_FAMILY_LANGUAGE_LABELS[language] ?? language; +} + +function stringField(value: unknown): string { + return typeof value === 'string' ? value : ''; +} + +function recordField(value: unknown): Readonly> { + return value && typeof value === 'object' && !Array.isArray(value) + ? (value as Readonly>) + : {}; +} + +export function readmeFamilyLanguageAliases( + selfLanguage: string, + languageOrder: readonly string[] = README_FAMILY_LANGUAGE_ORDER +): Readonly> { + const aliases: Record = { + SELF_LANG: readmeFamilyLabel(selfLanguage), + SELF_LANG_ID: selfLanguage + }; + languageOrder + .filter((language) => language !== selfLanguage) + .forEach((language, index) => { + const prefix = `IMP_LANG${index + 1}`; + aliases[prefix] = readmeFamilyLabel(language); + aliases[`${prefix}_ID`] = language; + }); + + return aliases; +} + +export function readmeFamilyTokenValues( + family: Readonly> +): Readonly> { + const self = recordField(family.self); + const selfId = stringField(self.id); + const implementations = new Map>>(); + const rawImplementations = Array.isArray(family.implementations) ? family.implementations : []; + for (const rawImplementation of rawImplementations) { + const implementation = recordField(rawImplementation); + implementations.set(stringField(implementation.id), implementation); + } + + const tokens: Record = { + ...readmeFamilyLanguageAliases(selfId), + FAMILY_SECTION_HEADING: stringField(family.section_heading), + FAMILY_DESCRIPTION: stringField(family.description), + SELF_PACKAGE_ROOT: stringField(self.package_root), + SELF_PACKAGE_MANAGER: stringField(self.package_manager) + }; + + README_FAMILY_LANGUAGE_ORDER.filter((language) => language !== selfId).forEach((language, index) => { + const implementation = implementations.get(language) ?? {}; + const prefix = `IMP_LANG${index + 1}`; + tokens[`${prefix}_PACKAGE_ROOT`] = stringField(implementation.package_root); + tokens[`${prefix}_PACKAGE_MANAGER`] = stringField(implementation.package_manager); + }); + + return tokens; +} + +export function renderReadmeFamilySection( + templatePartial: string, + family: Readonly>, + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): string { + const replacements: Record = {}; + for (const [key, value] of Object.entries(readmeFamilyTokenValues(family))) { + replacements[`SM|${key}`] = value; + } + + return resolveTemplateTokens(templatePartial, replacements, config); +} + export type DirectorySessionMode = 'plan' | 'apply' | 'reapply'; export interface TemplateDirectorySessionReport { diff --git a/packages/ast-template/test/session.integration.test.ts b/packages/ast-template/test/session.integration.test.ts index a6ac630..39dc7c2 100644 --- a/packages/ast-template/test/session.integration.test.ts +++ b/packages/ast-template/test/session.integration.test.ts @@ -74,6 +74,9 @@ import { reportDefaultAdapterCapabilitiesFromDirectories, importSessionInvocationEnvelope, reportTemplateDirectorySessionStatus, + readmeFamilyLanguageAliases, + readmeFamilyTokenValues, + renderReadmeFamilySection, reapplyTemplateDirectorySessionToDirectory, sessionCommandEnvelope, sessionCommandPayloadEnvelope, @@ -96,6 +99,53 @@ interface SessionFixtureSection { expected: unknown; } +interface ReadmeFamilySectionTemplateContractFixture { + canonical_language_order: string[]; + alias_derivation_cases: Array<{ + self: string; + expected_aliases: Record; + expected_alternative_ids: string[]; + }>; + metadata_case: { + family: Record; + expected_token_values: Record; + }; + template_partial: string; + expected_rendered_partial: string; +} + +describe('README family section template contract fixture', () => { + it('conforms to the shared fixture', () => { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-738-readme-family-section-template-contract', + 'readme-family-section-template-contract.json' + ); + const fixture = JSON.parse(readFileSync(fixturePath, 'utf8')) as ReadmeFamilySectionTemplateContractFixture; + + for (const testCase of fixture.alias_derivation_cases) { + const aliases = readmeFamilyLanguageAliases(testCase.self, fixture.canonical_language_order); + for (const [key, expected] of Object.entries(testCase.expected_aliases)) { + expect(aliases[key]).toBe(expected); + } + expect([aliases.IMP_LANG1_ID, aliases.IMP_LANG2_ID, aliases.IMP_LANG3_ID]).toEqual( + testCase.expected_alternative_ids + ); + } + + const tokenValues = readmeFamilyTokenValues(fixture.metadata_case.family); + for (const [key, expected] of Object.entries(fixture.metadata_case.expected_token_values)) { + expect(tokenValues[key]).toBe(expected); + } + expect(renderReadmeFamilySection(fixture.template_partial, fixture.metadata_case.family)).toBe( + fixture.expected_rendered_partial + ); + }); +}); + describe('template directory session report fixture', () => { it('conforms to the shared fixture', () => { const fixturePath = path.resolve( From 12b0db91eb568c1b4b333c0ee79a4056be2d033c Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 00:00:14 -0600 Subject: [PATCH 002/130] Apply README family sections from ast-template --- packages/ast-template/src/index.ts | 90 +++++++++++++++++++ .../test/session.integration.test.ts | 19 ++++ 2 files changed, 109 insertions(+) diff --git a/packages/ast-template/src/index.ts b/packages/ast-template/src/index.ts index 75c32b1..d1459ef 100644 --- a/packages/ast-template/src/index.ts +++ b/packages/ast-template/src/index.ts @@ -105,6 +105,96 @@ export function renderReadmeFamilySection( return resolveTemplateTokens(templatePartial, replacements, config); } +export function applyReadmeFamilySection( + templatePartial: string, + packageMetadata: Readonly>, + family: Readonly>, + destinationContent: string | null, + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): { content: string; changed: boolean } { + const renderedSection = renderReadmeFamilySection(templatePartial, family, config).replace(/\n+$/u, ''); + const baseContent = + destinationContent ?? + `# ${stringField(packageMetadata.name)}\n\n${stringField(packageMetadata.summary)}\n`; + const content = replaceOrInsertMarkdownHeadingSection( + baseContent, + stringField(family.section_heading), + 2, + renderedSection + ); + + return { content, changed: content !== baseContent }; +} + +function replaceOrInsertMarkdownHeadingSection( + content: string, + headingText: string, + headingLevel: number, + replacement: string +): string { + const normalized = content.replace(/\n+$/u, ''); + if (normalized === '') { + return `${replacement}\n`; + } + + const lines = normalized.split('\n'); + const headingPrefix = `${'#'.repeat(headingLevel)} `; + const headingIndex = lines.findIndex((line) => line.trim() === `${headingPrefix}${headingText}`); + if (headingIndex >= 0) { + let end = lines.length; + for (let index = headingIndex + 1; index < lines.length; index += 1) { + const level = markdownHeadingLevel(lines[index]); + if (level > 0 && level <= headingLevel) { + end = index; + break; + } + } + const outputLines = [ + ...lines.slice(0, headingIndex), + ...replacement.split('\n'), + ...(end < lines.length && lines[end] !== '' ? [''] : []), + ...lines.slice(end) + ]; + return `${outputLines.join('\n')}\n`; + } + + const insertAt = readmeFamilySectionInsertIndex(lines); + const outputLines = [ + ...lines.slice(0, insertAt), + ...(insertAt > 0 && lines[insertAt - 1] !== '' ? [''] : []), + ...replacement.split('\n'), + ...(insertAt < lines.length && lines[insertAt] !== '' ? [''] : []), + ...lines.slice(insertAt) + ]; + return `${outputLines.join('\n')}\n`; +} + +function markdownHeadingLevel(line: string): number { + const trimmed = line.trim(); + const match = /^(#{1,6})\s/u.exec(trimmed); + return match ? match[1].length : 0; +} + +function readmeFamilySectionInsertIndex(lines: readonly string[]): number { + let index = 0; + if (markdownHeadingLevel(lines[0] ?? '') === 1) { + index = 1; + while (index < lines.length && lines[index] === '') { + index += 1; + } + } + while (index < lines.length) { + if (lines[index] === '') { + while (index < lines.length && lines[index] === '') { + index += 1; + } + return index; + } + index += 1; + } + return lines.length; +} + export type DirectorySessionMode = 'plan' | 'apply' | 'reapply'; export interface TemplateDirectorySessionReport { diff --git a/packages/ast-template/test/session.integration.test.ts b/packages/ast-template/test/session.integration.test.ts index 39dc7c2..0cf2e23 100644 --- a/packages/ast-template/test/session.integration.test.ts +++ b/packages/ast-template/test/session.integration.test.ts @@ -74,6 +74,7 @@ import { reportDefaultAdapterCapabilitiesFromDirectories, importSessionInvocationEnvelope, reportTemplateDirectorySessionStatus, + applyReadmeFamilySection, readmeFamilyLanguageAliases, readmeFamilyTokenValues, renderReadmeFamilySection, @@ -107,11 +108,18 @@ interface ReadmeFamilySectionTemplateContractFixture { expected_alternative_ids: string[]; }>; metadata_case: { + package: Record; family: Record; expected_token_values: Record; }; template_partial: string; expected_rendered_partial: string; + readme_application_cases: Array<{ + label: string; + destination_content: string | null; + expected_content: string; + changed: boolean; + }>; } describe('README family section template contract fixture', () => { @@ -143,6 +151,17 @@ describe('README family section template contract fixture', () => { expect(renderReadmeFamilySection(fixture.template_partial, fixture.metadata_case.family)).toBe( fixture.expected_rendered_partial ); + + for (const testCase of fixture.readme_application_cases) { + const actual = applyReadmeFamilySection( + fixture.template_partial, + fixture.metadata_case.package, + fixture.metadata_case.family, + testCase.destination_content + ); + expect(actual.content).toBe(testCase.expected_content); + expect(actual.changed).toBe(testCase.changed); + } }); }); From 2f20eb0daff1f0412201ad23b8ad797d7a939282 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 00:39:43 -0600 Subject: [PATCH 003/130] Sync README family sections across packages --- packages/ast-template/src/index.ts | 76 +++++++++++++++++++ .../test/session.integration.test.ts | 41 +++++++++- 2 files changed, 116 insertions(+), 1 deletion(-) diff --git a/packages/ast-template/src/index.ts b/packages/ast-template/src/index.ts index d1459ef..da40e9c 100644 --- a/packages/ast-template/src/index.ts +++ b/packages/ast-template/src/index.ts @@ -7,6 +7,8 @@ import type { TemplateTokenConfig, TemplateTreeRunResult } from '@structuredmerge/ast-merge'; +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; import { DEFAULT_TEMPLATE_TOKEN_CONFIG, applyTemplateTreeExecutionToDirectory, @@ -126,6 +128,80 @@ export function applyReadmeFamilySection( return { content, changed: content !== baseContent }; } +export interface ReadmeFamilyPackage { + id: string; + readme_path: string; + package: Readonly>; + family: Readonly>; +} + +export interface ReadmeFamilyPackageReportEntry { + id: string; + readme_path: string; + changed: boolean; + created: boolean; +} + +export interface ReadmeFamilyPackageReport { + package_count: number; + changed_count: number; + created_count: number; + entries: ReadmeFamilyPackageReportEntry[]; +} + +export function applyReadmeFamilySectionsToPackageDirectories( + root: string, + templatePartial: string, + packages: readonly ReadmeFamilyPackage[], + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): ReadmeFamilyPackageReport { + const report: ReadmeFamilyPackageReport = { + package_count: packages.length, + changed_count: 0, + created_count: 0, + entries: [] + }; + + for (const packageEntry of packages) { + const readmePath = path.join(root, ...packageEntry.readme_path.split('/')); + let destinationContent: string | null = null; + let created = false; + try { + destinationContent = readFileSync(readmePath, 'utf8'); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + created = true; + } else { + throw error; + } + } + + const application = applyReadmeFamilySection( + templatePartial, + packageEntry.package, + packageEntry.family, + destinationContent, + config + ); + if (application.changed) { + mkdirSync(path.dirname(readmePath), { recursive: true }); + writeFileSync(readmePath, application.content); + report.changed_count += 1; + if (created) { + report.created_count += 1; + } + } + report.entries.push({ + id: packageEntry.id, + readme_path: packageEntry.readme_path, + changed: application.changed, + created: created && application.changed + }); + } + + return report; +} + function replaceOrInsertMarkdownHeadingSection( content: string, headingText: string, diff --git a/packages/ast-template/test/session.integration.test.ts b/packages/ast-template/test/session.integration.test.ts index 0cf2e23..cce8725 100644 --- a/packages/ast-template/test/session.integration.test.ts +++ b/packages/ast-template/test/session.integration.test.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { mkdirSync, readFileSync, rmSync } from 'node:fs'; +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; import type { @@ -75,6 +75,7 @@ import { importSessionInvocationEnvelope, reportTemplateDirectorySessionStatus, applyReadmeFamilySection, + applyReadmeFamilySectionsToPackageDirectories, readmeFamilyLanguageAliases, readmeFamilyTokenValues, renderReadmeFamilySection, @@ -120,6 +121,17 @@ interface ReadmeFamilySectionTemplateContractFixture { expected_content: string; changed: boolean; }>; + package_directory_case: { + packages: Array<{ + id: string; + readme_path: string; + package: Record; + family: Record; + initial_content: string | null; + expected_content: string; + }>; + expected_report: unknown; + }; } describe('README family section template contract fixture', () => { @@ -162,6 +174,33 @@ describe('README family section template contract fixture', () => { expect(actual.content).toBe(testCase.expected_content); expect(actual.changed).toBe(testCase.changed); } + + const tempRoot = path.resolve( + process.cwd(), + 'packages', + 'ast-template', + 'tmp', + 'readme-family-packages' + ); + rmSync(tempRoot, { recursive: true, force: true }); + for (const packageCase of fixture.package_directory_case.packages) { + if (packageCase.initial_content !== null) { + const readmePath = path.join(tempRoot, ...packageCase.readme_path.split('/')); + mkdirSync(path.dirname(readmePath), { recursive: true }); + writeFileSync(readmePath, packageCase.initial_content); + } + } + const report = applyReadmeFamilySectionsToPackageDirectories( + tempRoot, + fixture.template_partial, + fixture.package_directory_case.packages + ); + expect(report).toEqual(fixture.package_directory_case.expected_report); + for (const packageCase of fixture.package_directory_case.packages) { + const readmePath = path.join(tempRoot, ...packageCase.readme_path.split('/')); + expect(readFileSync(readmePath, 'utf8')).toBe(packageCase.expected_content); + } + rmSync(tempRoot, { recursive: true, force: true }); }); }); From 52abe27ba7aace5618343830a24bd63a2b5d63c9 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 02:52:09 -0600 Subject: [PATCH 004/130] Add TypeScript README style generation --- packages/kettle-nodule/src/index.ts | 359 ++++++++++++++++++ .../kettle-nodule/test/thin-slice.test.ts | 141 ++++++- 2 files changed, 499 insertions(+), 1 deletion(-) diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index c57e5c4..e44fc24 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -83,6 +83,53 @@ export interface ProjectReport { readonly diagnostics: readonly Diagnostic[]; } +export interface ReadmeStyleReport { + readonly readmePath: string; + readonly changed: boolean; + readonly style: string; + readonly preservedSections: readonly string[]; + readonly renderedSections: readonly string[]; + readonly omittedSections: readonly string[]; + readonly missingIntegrations: readonly string[]; + readonly disabledIntegrations: readonly string[]; + readonly unresolvedLogoSlugs: readonly string[]; + readonly licenseFilesChanged: boolean; + readonly copyrightAuthors: readonly string[]; + readonly finalContent: string; +} + +interface KettleConfig { + readonly readme?: ReadmeConfig; +} + +interface ReadmeConfig { + readonly style?: string; + readonly project_emoji?: string; + readonly logo_row?: { + readonly enabled?: boolean; + readonly max_count?: number; + readonly logos?: readonly ReadmeLogo[]; + }; + readonly badges?: { + readonly disabled?: readonly string[]; + }; + readonly preserve_sections?: readonly string[]; + readonly section_aliases?: Record; + readonly conditional_sections?: { + readonly floss_funding?: string; + }; + readonly license?: { + readonly spdx?: readonly string[]; + }; +} + +interface ReadmeLogo { + readonly type?: string; + readonly slug?: string; + readonly alt?: string; + readonly href?: string; +} + interface PackageJson { readonly name?: string; readonly description?: string; @@ -228,6 +275,23 @@ export function applyProject(projectRoot: string): ProjectReport { return report; } +export function planReadmeStyle(projectRoot: string): ReadmeStyleReport { + const facts = discoverFacts(projectRoot); + const config = readKettleConfig(projectRoot).readme ?? {}; + const readmePath = path.join(projectRoot, 'README.md'); + const original = existsSync(readmePath) ? readFileSync(readmePath, 'utf8') : ''; + const hasSecurity = existsSync(path.join(projectRoot, 'SECURITY.md')); + return renderReadmeStyle(original, facts, config, hasSecurity); +} + +export function applyReadmeStyle(projectRoot: string): ReadmeStyleReport { + const report = planReadmeStyle(projectRoot); + if (!report.changed) return report; + + writeFileSync(path.join(projectRoot, report.readmePath), report.finalContent); + return report; +} + function executeRecipe(input: { readonly projectRoot: string; readonly recipe: PackagingRecipe; @@ -553,6 +617,301 @@ function repositoryUrl(repository: PackageJson['repository']): string | undefine return repository?.url; } +function readKettleConfig(projectRoot: string): KettleConfig { + const configPath = path.join(projectRoot, 'kettle.yml'); + if (!existsSync(configPath)) return {}; + return parseKettleConfig(readFileSync(configPath, 'utf8')); +} + +function parseKettleConfig(source: string): KettleConfig { + const readme: { + style?: string; + project_emoji?: string; + logo_row?: { enabled?: boolean; max_count?: number; logos: ReadmeLogo[] }; + badges?: { disabled: string[] }; + preserve_sections?: string[]; + section_aliases?: Record; + conditional_sections?: { floss_funding?: string }; + license?: { spdx: string[] }; + } = {}; + let section = ''; + let list = ''; + let currentLogo: Record | undefined; + for (const rawLine of source.split('\n')) { + const line = rawLine.trimEnd(); + const trimmed = line.trim(); + if (trimmed.length === 0 || trimmed === 'readme:') continue; + if (trimmed.endsWith(':') && !trimmed.startsWith('- ')) { + const key = trimmed.slice(0, -1); + if (['logo_row', 'badges', 'section_aliases', 'conditional_sections', 'license'].includes(key)) { + section = key; + list = ''; + } else if (section === 'logo_row' && key === 'logos') { + list = 'logos'; + readme.logo_row ??= { logos: [] }; + } else if (section === 'badges' && key === 'disabled') { + list = 'disabled'; + readme.badges ??= { disabled: [] }; + } else if (section === 'license' && key === 'spdx') { + list = 'spdx'; + readme.license ??= { spdx: [] }; + } else { + section = key; + } + continue; + } + if (trimmed.startsWith('- ')) { + const value = unquote(trimmed.slice(2)); + if (section === 'logo_row' && list === 'logos') { + currentLogo = {}; + readme.logo_row ??= { logos: [] }; + readme.logo_row.logos.push(currentLogo); + const [key, rest] = splitYamlPair(value); + if (key) currentLogo[key] = rest; + } else if (section === 'badges' && list === 'disabled') { + readme.badges ??= { disabled: [] }; + readme.badges.disabled.push(value); + } else if (section === 'license' && list === 'spdx') { + readme.license ??= { spdx: [] }; + readme.license.spdx.push(value); + } else if (section === 'preserve_sections') { + readme.preserve_sections ??= []; + readme.preserve_sections.push(value); + } + continue; + } + const [key, value] = splitYamlPair(trimmed); + if (!key) continue; + if (section === '') { + if (key === 'style') readme.style = value; + if (key === 'project_emoji') readme.project_emoji = value; + if (key === 'preserve_sections') section = 'preserve_sections'; + } else if (section === 'logo_row') { + readme.logo_row ??= { logos: [] }; + if (currentLogo && list === 'logos') { + currentLogo[key] = value; + } else if (key === 'enabled') { + readme.logo_row.enabled = value === 'true'; + } else if (key === 'max_count') { + readme.logo_row.max_count = Number.parseInt(value, 10); + } + } else if (section === 'section_aliases') { + readme.section_aliases ??= {}; + readme.section_aliases[key] = value; + } else if (section === 'conditional_sections') { + readme.conditional_sections ??= {}; + if (key === 'floss_funding') readme.conditional_sections.floss_funding = value; + } + } + return { readme }; +} + +function splitYamlPair(value: string): readonly [string, string] { + const index = value.indexOf(':'); + if (index < 0) return ['', '']; + return [value.slice(0, index).trim(), unquote(value.slice(index + 1).trim())]; +} + +function unquote(value: string): string { + return value.replace(/^["']|["']$/g, ''); +} + +function renderReadmeStyle( + destination: string, + facts: PackageFacts, + rawConfig: ReadmeConfig, + hasSecurity: boolean +): ReadmeStyleReport { + const config = defaultReadmeConfig(rawConfig); + const preserved = preservedReadmeSections(destination, config); + const license = readmeLicense(config, facts); + const [logoRow, unresolvedLogoSlugs] = readmeLogoRow(config); + const [badgeCloud, missingIntegrations, disabledIntegrations] = readmeBadgeCloud( + config, + facts, + license, + hasSecurity + ); + const renderedSections = [ + ...(logoRow ? ['Logos'] : []), + 'Project Name', + 'Badges', + 'Synopsis', + 'Info you can shake a stick at', + 'Installation', + 'Configuration', + 'Basic Usage', + 'Versioning', + 'License', + 'A request for help' + ]; + const omittedSections = ['Hostile RubyGems Takeover', 'Secure Installation']; + const includeFunding = shouldIncludeFunding(config, license); + if (includeFunding) renderedSections.push('FLOSS Funding'); + else omittedSections.push('FLOSS Funding'); + if (hasSecurity) renderedSections.push('Security'); + else omittedSections.push('Security'); + renderedSections.push('Contributing'); + + const sections = [ + ...(logoRow ? [logoRow] : []), + `# ${config.project_emoji} ${facts.package.name}`, + ...(badgeCloud ? [badgeCloud] : []), + `## 🌻 Synopsis\n\n${preserved.synopsis ?? ''}`, + `## 💡 Info you can shake a stick at\n\nCompatible with ${facts.npm.packageManager ?? 'the configured npm runtime'}.\n\n
\nCompatibility, federated DVCS, and enterprise support\n\nAdditional compatibility and support details are generated from package metadata and shared fixtures.\n\n
`, + `## ✨ Installation\n\n\`\`\`console\n${packageManagerCommand(facts.npm.packageManager)} add ${facts.package.name}\n\`\`\``, + `## ⚙️ Configuration\n\n${preserved.configuration ?? ''}`, + `## 🔧 Basic Usage\n\n${preserved['basic usage'] ?? ''}`, + ...(includeFunding + ? [ + '## 🦷 FLOSS Funding\n\nThis free software project accepts funding support when configured by the package maintainer.' + ] + : []), + ...(hasSecurity ? ['## 🔐 Security\n\nSee [SECURITY.md](SECURITY.md).'] : []), + '## 🤝 Contributing\n\nContributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges.', + '## 📌 Versioning\n\nThis project follows semantic versioning for its public API where practical.', + `## 📄 License\n\n${licenseParagraph(license)}`, + '## 🤑 A request for help\n\nPlease support the project by using it, reporting issues, and contributing improvements.' + ]; + const finalContent = ensureTrailingNewline(sections.join('\n\n')); + return { + readmePath: 'README.md', + changed: finalContent !== destination, + style: config.style, + preservedSections: ['Synopsis', 'Configuration', 'Basic Usage'], + renderedSections, + omittedSections, + missingIntegrations, + disabledIntegrations, + unresolvedLogoSlugs, + licenseFilesChanged: false, + copyrightAuthors: [], + finalContent + }; +} + +function defaultReadmeConfig(config: ReadmeConfig): Required> & ReadmeConfig { + return { + ...config, + style: config.style ?? 'thin', + project_emoji: config.project_emoji ?? '💎', + preserve_sections: config.preserve_sections ?? ['Synopsis', 'Configuration', 'Basic Usage'], + section_aliases: { + summary: 'synopsis', + usage: 'basic usage', + 'configuration options': 'configuration', + setup: 'basic usage', + ...Object.fromEntries( + Object.entries(config.section_aliases ?? {}).map(([from, to]) => [ + normalizeReadmeHeading(from), + normalizeReadmeHeading(to) + ]) + ) + } + }; +} + +function preservedReadmeSections( + content: string, + config: Required> & ReadmeConfig +): Record { + const sections = markdownSectionBodies(content); + return Object.fromEntries( + config.preserve_sections.map((section) => { + const key = normalizeReadmeHeading(section); + const alias = Object.entries(config.section_aliases).find(([, to]) => to === key)?.[0]; + return [key, sections[key] ?? (alias ? sections[alias] : undefined) ?? '']; + }) + ); +} + +function markdownSectionBodies(content: string): Record { + const lines = content.split('\n'); + const headings = lines.flatMap((line, index) => { + const match = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/); + return match ? [{ index, level: match[1]!.length, key: normalizeReadmeHeading(match[2]!) }] : []; + }); + return Object.fromEntries( + headings.map((heading, index) => { + const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level); + return [ + heading.key, + lines + .slice(heading.index + 1, next?.index ?? lines.length) + .join('\n') + .trim() + ]; + }) + ); +} + +function normalizeReadmeHeading(value: string): string { + const fields = value.trim().split(/\s+/); + return fields.length > 1 && !/^[A-Za-z0-9]/.test(fields[0]!) + ? fields.slice(1).join(' ').toLowerCase() + : value.trim().toLowerCase(); +} + +function readmeLicense(config: ReadmeConfig, facts: PackageFacts): string { + return config.license?.spdx?.join(' OR ') || facts.package.licenseExpression || 'MIT'; +} + +function readmeLogoRow(config: ReadmeConfig): readonly [string, readonly string[]] { + if (config.logo_row?.enabled === false) return ['', []]; + const maxCount = Math.min(Math.max(config.logo_row?.max_count ?? 3, 1), 3); + const unresolved: string[] = []; + const parts = (config.logo_row?.logos ?? []).slice(0, maxCount).flatMap((logo) => { + const type = logo.type?.trim().toLowerCase().replaceAll('-', '_') ?? ''; + const slug = logo.slug?.trim() ?? ''; + if (!['language', 'org', 'project', 'affiliated_project'].includes(type) || slug.length === 0) { + unresolved.push(slug); + return []; + } + const ref = slug.replaceAll('/', '-'); + const alt = logo.alt?.trim() || slug; + const href = logo.href?.trim() || `https://logos.galtzo.com/assets/images/${slug}/`; + return [ + `[![${alt}][🖼️${ref}-i]][🖼️${ref}]\n[🖼️${ref}-i]: https://logos.galtzo.com/assets/images/${slug}/avatar-192px.svg\n[🖼️${ref}]: ${href}` + ]; + }); + return [parts.join('\n'), unresolved]; +} + +function readmeBadgeCloud( + config: ReadmeConfig, + facts: PackageFacts, + license: string, + hasSecurity: boolean +): readonly [string, readonly string[], readonly string[]] { + const disabled = [...(config.badges?.disabled ?? [])]; + const missing = ['codecov', 'coveralls', 'qlty', 'codeql'].filter( + (integration) => !disabled.includes(integration) + ); + const badges = [ + ...(facts.package.sourceUrl + ? [`[![Source](https://img.shields.io/badge/source-github-238636.svg)](${facts.package.sourceUrl})`] + : []), + `![License](https://img.shields.io/badge/license-${license.replaceAll(' ', '%20')}-259D6C.svg)`, + ...(hasSecurity + ? ['[![Security](https://img.shields.io/badge/security-policy-259D6C.svg)](SECURITY.md)'] + : []) + ]; + return [badges.join(' '), missing, disabled]; +} + +function shouldIncludeFunding(config: ReadmeConfig, license: string): boolean { + const policy = config.conditional_sections?.floss_funding?.trim().toLowerCase(); + if (['disabled', 'false', 'never'].includes(policy ?? '')) return false; + if (['enabled', 'true', 'always'].includes(policy ?? '')) return true; + return license === 'MIT'; +} + +function licenseParagraph(license: string): string { + return license === 'MIT' + ? 'This project is made available under the terms of the MIT License.' + : `This project is made available under the following license expression: ${license}.`; +} + function normalizeFundingUrls(funding: PackageJson['funding']): readonly string[] { if (!funding) return []; const entries = Array.isArray(funding) ? funding : [funding]; diff --git a/packages/kettle-nodule/test/thin-slice.test.ts b/packages/kettle-nodule/test/thin-slice.test.ts index 2e73bad..8683b21 100644 --- a/packages/kettle-nodule/test/thin-slice.test.ts +++ b/packages/kettle-nodule/test/thin-slice.test.ts @@ -4,8 +4,10 @@ import { describe, expect, it } from 'vitest'; import { applyPackagedTemplateInventory, applyProject, + applyReadmeStyle, planPackagedTemplateInventory, - planProject + planProject, + planReadmeStyle } from '../src/index'; interface ThinSliceFixture { @@ -136,4 +138,141 @@ describe('kettle-nodule thin vertical slice', () => { expect(readFileSync(path.join(projectRoot, '.github/workflows/ci.yml'), 'utf8')).toBe(ci); rmSync(projectRoot, { force: true, recursive: true }); }); + + it('conforms to the README style profile', () => { + const fixturePath = path.resolve( + import.meta.dirname, + '..', + '..', + '..', + '..', + 'fixtures', + 'diagnostics', + 'slice-740-kettle-readme-style-profile', + 'kettle-readme-style-profile.json' + ); + const styleFixture = JSON.parse(readFileSync(fixturePath, 'utf8')) as { + readonly profile: { readonly name: string }; + }; + expect(styleFixture.profile.name).toBe('kettle-readme-style-profile'); + const projectRoot = path.resolve(import.meta.dirname, '..', 'tmp', 'readme-style-profile'); + rmSync(projectRoot, { force: true, recursive: true }); + writeTree(projectRoot, { + 'package.json': JSON.stringify( + { + name: '@acme/widget', + version: '0.1.0', + description: 'Example npm package', + repository: { url: 'https://github.com/acme/widget' }, + license: 'MIT', + type: 'module', + packageManager: 'pnpm@9.1.0' + }, + null, + 2 + ), + 'kettle.yml': [ + 'readme:', + ' style: thin', + ' project_emoji: "📦"', + ' logo_row:', + ' enabled: true', + ' max_count: 3', + ' logos:', + ' - type: language', + ' slug: typescript-lang', + ' alt: TypeScript language logo', + ' - type: org', + ' slug: acme', + ' alt: Acme org logo', + ' - type: affiliated_project', + ' slug: tree-sitter/tree-sitter', + ' alt: Tree-sitter project logo', + ' - type: project', + ' slug: acme/ignored', + ' alt: Ignored fourth logo', + ' preserve_sections:', + ' - Synopsis', + ' - Configuration', + ' - Basic Usage', + ' section_aliases:', + ' Usage: Basic Usage', + ' conditional_sections:', + ' floss_funding: default_for_mit_opt_in_otherwise', + ' badges:', + ' disabled:', + ' - coveralls', + ' license:', + ' spdx:', + ' - MIT', + '' + ].join('\n'), + 'SECURITY.md': '# Security\n', + 'README.md': [ + '# Old Package', + '', + '## Summary', + '', + 'Destination synopsis.', + '', + '## Configuration', + '', + 'Destination configuration.', + '', + '## Usage', + '', + 'Destination usage.', + '' + ].join('\n') + }); + + const plan = planReadmeStyle(projectRoot); + expect(plan.changed).toBe(true); + expect(plan.style).toBe('thin'); + expect(plan.preservedSections).toEqual( + expect.arrayContaining(['Synopsis', 'Configuration', 'Basic Usage']) + ); + expect(plan.renderedSections).toEqual( + expect.arrayContaining([ + 'Logos', + 'Project Name', + 'Badges', + 'Synopsis', + 'Installation', + 'Configuration', + 'Basic Usage', + 'FLOSS Funding', + 'Security', + 'Contributing', + 'Versioning', + 'License', + 'A request for help' + ]) + ); + expect(plan.omittedSections).toEqual( + expect.arrayContaining(['Hostile RubyGems Takeover', 'Secure Installation']) + ); + expect(plan.missingIntegrations).toEqual(expect.arrayContaining(['codecov', 'qlty'])); + expect(plan.missingIntegrations).not.toContain('coveralls'); + expect(plan.disabledIntegrations).toContain('coveralls'); + expect(plan.finalContent).not.toContain('Ignored fourth logo'); + for (const snippet of [ + '# 📦 @acme/widget', + '## 🌻 Synopsis\n\nDestination synopsis.', + '## ⚙️ Configuration\n\nDestination configuration.', + '## 🔧 Basic Usage\n\nDestination usage.', + '## 🔐 Security\n\nSee [SECURITY.md](SECURITY.md).', + '## 🦷 FLOSS Funding', + 'pnpm add @acme/widget', + 'https://logos.galtzo.com/assets/images/tree-sitter/tree-sitter/avatar-192px.svg' + ]) { + expect(plan.finalContent).toContain(snippet); + } + + const apply = applyReadmeStyle(projectRoot); + expect(apply.changed).toBe(true); + const second = applyReadmeStyle(projectRoot); + expect(second.changed).toBe(false); + rmSync(projectRoot, { force: true, recursive: true }); + }); }); From b280562ee8ef2888df5301b8a737526781937a82 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:05:32 -0600 Subject: [PATCH 005/130] Add README to TypeScript template inventory --- packages/kettle-nodule/src/index.ts | 5 ++++- packages/kettle-nodule/test/thin-slice.test.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index e44fc24..9742b51 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -207,7 +207,8 @@ export function packagedTemplateInventoryPack(): RecipePack { templateRecipe('.github/workflows/ci.yml'), templateRecipe('.gitignore'), templateRecipe('.npmrc'), - templateRecipe('.prettierrc.json') + templateRecipe('.prettierrc.json'), + templateRecipe('README.md') ] }; } @@ -548,6 +549,8 @@ function packagedTemplateContent(targetPath: string): string { return 'engine-strict=true\nfund=true\n'; case '.prettierrc.json': return '{\n "singleQuote": true,\n "trailingComma": "none"\n}\n'; + case 'README.md': + return '# {{PACKAGE_NAME}}\n\n## Synopsis\n\n## Installation\n\n```sh\n{{PACKAGE_MANAGER_COMMAND}} add {{PACKAGE_NAME}}\n```\n\n## Configuration\n\n## Basic Usage\n'; default: return ''; } diff --git a/packages/kettle-nodule/test/thin-slice.test.ts b/packages/kettle-nodule/test/thin-slice.test.ts index 8683b21..dc292b5 100644 --- a/packages/kettle-nodule/test/thin-slice.test.ts +++ b/packages/kettle-nodule/test/thin-slice.test.ts @@ -121,7 +121,8 @@ describe('kettle-nodule thin vertical slice', () => { '.github/workflows/ci.yml', '.gitignore', '.npmrc', - '.prettierrc.json' + '.prettierrc.json', + 'README.md' ]; const plan = planPackagedTemplateInventory(projectRoot); expect(plan.recipePack.name).toBe('kettle-nodule-packaged-template-inventory'); @@ -132,10 +133,18 @@ describe('kettle-nodule thin vertical slice', () => { const ci = readFileSync(path.join(projectRoot, '.github/workflows/ci.yml'), 'utf8'); expect(ci).toContain('node-version: "20"'); expect(ci).toContain('- run: pnpm test'); + const readme = readFileSync(path.join(projectRoot, 'README.md'), 'utf8'); + expect(readme).toContain('# @acme/widget'); + expect(readme).toContain('## Synopsis'); + expect(readme).toContain('## Installation'); + expect(readme).toContain('pnpm add @acme/widget'); + expect(readme).toContain('## Configuration'); + expect(readme).toContain('## Basic Usage'); const second = applyPackagedTemplateInventory(projectRoot); expect(second.changedFiles).toEqual([]); expect(readFileSync(path.join(projectRoot, '.github/workflows/ci.yml'), 'utf8')).toBe(ci); + expect(readFileSync(path.join(projectRoot, 'README.md'), 'utf8')).toBe(readme); rmSync(projectRoot, { force: true, recursive: true }); }); From 18657e821bc8ebcc7b6a4be6036ffc3126ed0c41 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:21:58 -0600 Subject: [PATCH 006/130] Add README family command profile --- packages/ast-template/src/index.ts | 57 ++++++++++++++++++- .../test/session.integration.test.ts | 11 ++++ 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/ast-template/src/index.ts b/packages/ast-template/src/index.ts index da40e9c..b27c44b 100644 --- a/packages/ast-template/src/index.ts +++ b/packages/ast-template/src/index.ts @@ -149,11 +149,62 @@ export interface ReadmeFamilyPackageReport { entries: ReadmeFamilyPackageReportEntry[]; } +export interface ReadmeFamilySectionCommand { + profile_name: string; + mode?: DirectorySessionMode; + root: string; + template_partial: string; + packages: readonly ReadmeFamilyPackage[]; +} + +export interface ReadmeFamilySectionCommandReport { + profile_name: string; + mode: DirectorySessionMode; + runner: ReadmeFamilyPackageReport; +} + export function applyReadmeFamilySectionsToPackageDirectories( root: string, templatePartial: string, packages: readonly ReadmeFamilyPackage[], config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): ReadmeFamilyPackageReport { + return runReadmeFamilySectionsForPackageDirectories(root, templatePartial, packages, true, config); +} + +export function planReadmeFamilySectionsForPackageDirectories( + root: string, + templatePartial: string, + packages: readonly ReadmeFamilyPackage[], + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): ReadmeFamilyPackageReport { + return runReadmeFamilySectionsForPackageDirectories(root, templatePartial, packages, false, config); +} + +export function runReadmeFamilySectionCommand( + command: ReadmeFamilySectionCommand, + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG +): ReadmeFamilySectionCommandReport { + const mode = command.mode ?? 'plan'; + return { + profile_name: command.profile_name, + mode, + runner: runReadmeFamilySectionsForPackageDirectories( + command.root, + command.template_partial, + command.packages, + mode === 'apply' || mode === 'reapply', + config + ) + }; +} + +function runReadmeFamilySectionsForPackageDirectories( + root: string, + templatePartial: string, + packages: readonly ReadmeFamilyPackage[], + writeChanges: boolean, + config: TemplateTokenConfig = DEFAULT_TEMPLATE_TOKEN_CONFIG ): ReadmeFamilyPackageReport { const report: ReadmeFamilyPackageReport = { package_count: packages.length, @@ -184,8 +235,10 @@ export function applyReadmeFamilySectionsToPackageDirectories( config ); if (application.changed) { - mkdirSync(path.dirname(readmePath), { recursive: true }); - writeFileSync(readmePath, application.content); + if (writeChanges) { + mkdirSync(path.dirname(readmePath), { recursive: true }); + writeFileSync(readmePath, application.content); + } report.changed_count += 1; if (created) { report.created_count += 1; diff --git a/packages/ast-template/test/session.integration.test.ts b/packages/ast-template/test/session.integration.test.ts index cce8725..25b2cf8 100644 --- a/packages/ast-template/test/session.integration.test.ts +++ b/packages/ast-template/test/session.integration.test.ts @@ -79,6 +79,7 @@ import { readmeFamilyLanguageAliases, readmeFamilyTokenValues, renderReadmeFamilySection, + runReadmeFamilySectionCommand, reapplyTemplateDirectorySessionToDirectory, sessionCommandEnvelope, sessionCommandPayloadEnvelope, @@ -196,6 +197,16 @@ describe('README family section template contract fixture', () => { fixture.package_directory_case.packages ); expect(report).toEqual(fixture.package_directory_case.expected_report); + const commandReport = runReadmeFamilySectionCommand({ + profile_name: 'update-readme-family-section', + mode: 'plan', + root: tempRoot, + template_partial: fixture.template_partial, + packages: fixture.package_directory_case.packages + }); + expect(commandReport.profile_name).toBe('update-readme-family-section'); + expect(commandReport.mode).toBe('plan'); + expect(commandReport.runner.changed_count).toBe(0); for (const packageCase of fixture.package_directory_case.packages) { const readmePath = path.join(tempRoot, ...packageCase.readme_path.split('/')); expect(readFileSync(readmePath, 'utf8')).toBe(packageCase.expected_content); From c444a0a0c1eb705d2cf15d2dd3bc95664b0937f5 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:26:22 -0600 Subject: [PATCH 007/130] Render README family backend matrix --- packages/kettle-nodule/src/index.ts | 32 ++++++++++++++++++- .../kettle-nodule/test/thin-slice.test.ts | 4 ++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index 9742b51..58eeecf 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -761,7 +761,7 @@ function renderReadmeStyle( `# ${config.project_emoji} ${facts.package.name}`, ...(badgeCloud ? [badgeCloud] : []), `## 🌻 Synopsis\n\n${preserved.synopsis ?? ''}`, - `## 💡 Info you can shake a stick at\n\nCompatible with ${facts.npm.packageManager ?? 'the configured npm runtime'}.\n\n
\nCompatibility, federated DVCS, and enterprise support\n\nAdditional compatibility and support details are generated from package metadata and shared fixtures.\n\n
`, + `## 💡 Info you can shake a stick at\n\nCompatible with ${facts.npm.packageManager ?? 'the configured npm runtime'}.\n\n${readmeFamilyIntroAndBackendMatrix()}`, `## ✨ Installation\n\n\`\`\`console\n${packageManagerCommand(facts.npm.packageManager)} add ${facts.package.name}\n\`\`\``, `## ⚙️ Configuration\n\n${preserved.configuration ?? ''}`, `## 🔧 Basic Usage\n\n${preserved['basic usage'] ?? ''}`, @@ -793,6 +793,36 @@ function renderReadmeStyle( }; } +function readmeFamilyIntroAndBackendMatrix(): string { + return [ + '
', + 'StructuredMerge package family and backend compatibility', + '', + 'StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior.', + '', + '| Package | Layer | Families | Status | README role |', + '|---|---|---|---|---|', + '| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows |', + '| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports |', + '| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting |', + '| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior |', + '| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded |', + '| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior |', + '| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior |', + '| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art |', + '| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior |', + '| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior |', + '', + '| Backend | Languages | Families | Note |', + '|---|---|---|---|', + '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. |', + '| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. |', + '| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. |', + '', + '
' + ].join('\n'); +} + function defaultReadmeConfig(config: ReadmeConfig): Required> & ReadmeConfig { return { ...config, diff --git a/packages/kettle-nodule/test/thin-slice.test.ts b/packages/kettle-nodule/test/thin-slice.test.ts index dc292b5..f4b517f 100644 --- a/packages/kettle-nodule/test/thin-slice.test.ts +++ b/packages/kettle-nodule/test/thin-slice.test.ts @@ -273,7 +273,9 @@ describe('kettle-nodule thin vertical slice', () => { '## 🔐 Security\n\nSee [SECURITY.md](SECURITY.md).', '## 🦷 FLOSS Funding', 'pnpm add @acme/widget', - 'https://logos.galtzo.com/assets/images/tree-sitter/tree-sitter/avatar-192px.svg' + 'https://logos.galtzo.com/assets/images/tree-sitter/tree-sitter/avatar-192px.svg', + 'StructuredMerge packages provide fixture-backed merge behavior', + '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source |' ]) { expect(plan.finalContent).toContain(snippet); } From 1a4b7df47e1e4fd3bd27c1e4dbc9fcce7eebbad1 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:33:47 -0600 Subject: [PATCH 008/130] Generate package READMEs --- packages/ast-merge/README.md | 71 ++++++++++++++++++++ packages/ast-template/README.md | 71 ++++++++++++++++++++ packages/binary-merge/README.md | 71 ++++++++++++++++++++ packages/go-merge/README.md | 71 ++++++++++++++++++++ packages/js-yaml-merge/README.md | 71 ++++++++++++++++++++ packages/json-merge/README.md | 71 ++++++++++++++++++++ packages/kettle-nodule/README.md | 71 ++++++++++++++++++++ packages/markdown-it-merge/README.md | 71 ++++++++++++++++++++ packages/markdown-merge/README.md | 71 ++++++++++++++++++++ packages/peggy-toml-merge/README.md | 71 ++++++++++++++++++++ packages/plain-merge/README.md | 71 ++++++++++++++++++++ packages/ruby-merge/README.md | 71 ++++++++++++++++++++ packages/rust-merge/README.md | 71 ++++++++++++++++++++ packages/toml-merge/README.md | 71 ++++++++++++++++++++ packages/tree-haver/README.md | 71 ++++++++++++++++++++ packages/typescript-compiler-merge/README.md | 71 ++++++++++++++++++++ packages/typescript-merge/README.md | 71 ++++++++++++++++++++ packages/yaml-merge/README.md | 71 ++++++++++++++++++++ packages/zip-merge/README.md | 71 ++++++++++++++++++++ 19 files changed, 1349 insertions(+) create mode 100644 packages/ast-merge/README.md create mode 100644 packages/ast-template/README.md create mode 100644 packages/binary-merge/README.md create mode 100644 packages/go-merge/README.md create mode 100644 packages/js-yaml-merge/README.md create mode 100644 packages/json-merge/README.md create mode 100644 packages/kettle-nodule/README.md create mode 100644 packages/markdown-it-merge/README.md create mode 100644 packages/markdown-merge/README.md create mode 100644 packages/peggy-toml-merge/README.md create mode 100644 packages/plain-merge/README.md create mode 100644 packages/ruby-merge/README.md create mode 100644 packages/rust-merge/README.md create mode 100644 packages/toml-merge/README.md create mode 100644 packages/tree-haver/README.md create mode 100644 packages/typescript-compiler-merge/README.md create mode 100644 packages/typescript-merge/README.md create mode 100644 packages/yaml-merge/README.md create mode 100644 packages/zip-merge/README.md diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md new file mode 100644 index 0000000..980957b --- /dev/null +++ b/packages/ast-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/ast-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/ast-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md new file mode 100644 index 0000000..53225ac --- /dev/null +++ b/packages/ast-template/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/ast-template + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/ast-template +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md new file mode 100644 index 0000000..3584d14 --- /dev/null +++ b/packages/binary-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/binary-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/binary-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md new file mode 100644 index 0000000..c031ace --- /dev/null +++ b/packages/go-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/go-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/go-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md new file mode 100644 index 0000000..63a9bfc --- /dev/null +++ b/packages/js-yaml-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/js-yaml-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/js-yaml-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md new file mode 100644 index 0000000..05cbe15 --- /dev/null +++ b/packages/json-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/json-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/json-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md new file mode 100644 index 0000000..57177a8 --- /dev/null +++ b/packages/kettle-nodule/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/kettle-nodule + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/kettle-nodule +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md new file mode 100644 index 0000000..6aa7ebc --- /dev/null +++ b/packages/markdown-it-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/markdown-it-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/markdown-it-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md new file mode 100644 index 0000000..b2a3781 --- /dev/null +++ b/packages/markdown-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/markdown-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/markdown-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md new file mode 100644 index 0000000..ceb4eb9 --- /dev/null +++ b/packages/peggy-toml-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/peggy-toml-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/peggy-toml-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md new file mode 100644 index 0000000..2530aa9 --- /dev/null +++ b/packages/plain-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/plain-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/plain-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md new file mode 100644 index 0000000..05a2eae --- /dev/null +++ b/packages/ruby-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/ruby-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/ruby-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md new file mode 100644 index 0000000..482b6a9 --- /dev/null +++ b/packages/rust-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/rust-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/rust-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md new file mode 100644 index 0000000..24c2f03 --- /dev/null +++ b/packages/toml-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/toml-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/toml-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md new file mode 100644 index 0000000..f4f5597 --- /dev/null +++ b/packages/tree-haver/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/tree-haver + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/tree-haver +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md new file mode 100644 index 0000000..5be9d21 --- /dev/null +++ b/packages/typescript-compiler-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/typescript-compiler-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/typescript-compiler-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md new file mode 100644 index 0000000..e1c5f6e --- /dev/null +++ b/packages/typescript-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/typescript-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/typescript-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md new file mode 100644 index 0000000..afc5c5b --- /dev/null +++ b/packages/yaml-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/yaml-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/yaml-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md new file mode 100644 index 0000000..5b60541 --- /dev/null +++ b/packages/zip-merge/README.md @@ -0,0 +1,71 @@ +# 💎 @structuredmerge/zip-merge + +[![Source](https://img.shields.io/badge/source-github-238636.svg)](git+https://github.com/structuredmerge/structuredmerge-typescript.git) ![License](https://img.shields.io/badge/license-MIT-259D6C.svg) + +## 🌻 Synopsis + + + +## 💡 Info you can shake a stick at + +Compatible with the configured npm runtime. + +
+StructuredMerge package family and backend compatibility + +StructuredMerge packages provide fixture-backed merge behavior for document, configuration, source, archive, and binary formats. Shared contracts live in fixtures, while Go, Ruby, Rust, and TypeScript packages expose language-native APIs over the same behavior. + +| Package | Layer | Families | Status | README role | +|---|---|---|---|---| +| ast-template | workflow | template, readme | active | applies shared templates, package README sections, and package-directory sync workflows | +| ast-merge | core | template, review, structured-edit | active | documents provider-neutral contracts, token resolution, review state, and execution reports | +| tree-haver | backend substrate | parser, backend | active | documents backend selection, language-pack integration, position data, and capability reporting | +| markdown-merge | family | markdown | active | documents Markdown heading, fenced-code, nested-family, and provider behavior | +| json-merge | family | json, jsonc | active | documents JSON and JSONC merge behavior; old jsonc-merge is superseded | +| toml-merge | family | toml | active | documents TOML table, value, parser, and backend behavior | +| yaml-merge | family | yaml | active | documents YAML mapping, sequence, scalar, and backend behavior | +| ruby-merge | family | ruby-source | active | documents Ruby source merge behavior; old prism-merge is backend/provider prior art | +| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | +| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | + +| Backend | Languages | Families | Note | +|---|---|---|---| +| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | +| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | +| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | + +
+ +## ✨ Installation + +```console +npm add @structuredmerge/zip-merge +``` + +## ⚙️ Configuration + + + +## 🔧 Basic Usage + + + +## 🦷 FLOSS Funding + +This free software project accepts funding support when configured by the package maintainer. + +## 🤝 Contributing + +Contributions are welcome. Missing optional service integrations are reported by the generator instead of rendered as broken badges. + +## 📌 Versioning + +This project follows semantic versioning for its public API where practical. + +## 📄 License + +This project is made available under the terms of the MIT License. + +## 🤑 A request for help + +Please support the project by using it, reporting issues, and contributing improvements. From 01b2e07f2ed4080481c8f955b664702bda2fc2ba Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:39:51 -0600 Subject: [PATCH 009/130] Render backend compatibility reconciliation --- packages/ast-merge/README.md | 7 +++++++ packages/ast-template/README.md | 7 +++++++ packages/binary-merge/README.md | 7 +++++++ packages/go-merge/README.md | 7 +++++++ packages/js-yaml-merge/README.md | 7 +++++++ packages/json-merge/README.md | 7 +++++++ packages/kettle-nodule/README.md | 7 +++++++ packages/kettle-nodule/src/index.ts | 7 +++++++ packages/kettle-nodule/test/thin-slice.test.ts | 3 ++- packages/markdown-it-merge/README.md | 7 +++++++ packages/markdown-merge/README.md | 7 +++++++ packages/peggy-toml-merge/README.md | 7 +++++++ packages/plain-merge/README.md | 7 +++++++ packages/ruby-merge/README.md | 7 +++++++ packages/rust-merge/README.md | 7 +++++++ packages/toml-merge/README.md | 7 +++++++ packages/tree-haver/README.md | 7 +++++++ packages/typescript-compiler-merge/README.md | 7 +++++++ packages/typescript-merge/README.md | 7 +++++++ packages/yaml-merge/README.md | 7 +++++++ packages/zip-merge/README.md | 7 +++++++ 21 files changed, 142 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md index 980957b..39b4ae6 100644 --- a/packages/ast-merge/README.md +++ b/packages/ast-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md index 53225ac..7af119c 100644 --- a/packages/ast-template/README.md +++ b/packages/ast-template/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md index 3584d14..5a686f8 100644 --- a/packages/binary-merge/README.md +++ b/packages/binary-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md index c031ace..c6a6509 100644 --- a/packages/go-merge/README.md +++ b/packages/go-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md index 63a9bfc..f77ad5b 100644 --- a/packages/js-yaml-merge/README.md +++ b/packages/js-yaml-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md index 05cbe15..97d84d2 100644 --- a/packages/json-merge/README.md +++ b/packages/json-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md index 57177a8..9a58194 100644 --- a/packages/kettle-nodule/README.md +++ b/packages/kettle-nodule/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index 58eeecf..189a9ab 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -819,6 +819,13 @@ function readmeFamilyIntroAndBackendMatrix(): string { '| native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. |', '| plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. |', '', + '| Compatibility claim | Current disposition | Fixture source |', + '|---|---|---|', + '| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation |', + '| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 |', + '| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 |', + '| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list |', + '', '' ].join('\n'); } diff --git a/packages/kettle-nodule/test/thin-slice.test.ts b/packages/kettle-nodule/test/thin-slice.test.ts index f4b517f..f590f1f 100644 --- a/packages/kettle-nodule/test/thin-slice.test.ts +++ b/packages/kettle-nodule/test/thin-slice.test.ts @@ -275,7 +275,8 @@ describe('kettle-nodule thin vertical slice', () => { 'pnpm add @acme/widget', 'https://logos.galtzo.com/assets/images/tree-sitter/tree-sitter/avatar-192px.svg', 'StructuredMerge packages provide fixture-backed merge behavior', - '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source |' + '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source |', + '| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist |' ]) { expect(plan.finalContent).toContain(snippet); } diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md index 6aa7ebc..18054d5 100644 --- a/packages/markdown-it-merge/README.md +++ b/packages/markdown-it-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md index b2a3781..d3fdbc9 100644 --- a/packages/markdown-merge/README.md +++ b/packages/markdown-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md index ceb4eb9..f37618d 100644 --- a/packages/peggy-toml-merge/README.md +++ b/packages/peggy-toml-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md index 2530aa9..05ba658 100644 --- a/packages/plain-merge/README.md +++ b/packages/plain-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md index 05a2eae..cf8fe8c 100644 --- a/packages/ruby-merge/README.md +++ b/packages/ruby-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md index 482b6a9..090d43a 100644 --- a/packages/rust-merge/README.md +++ b/packages/rust-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md index 24c2f03..386e9d0 100644 --- a/packages/toml-merge/README.md +++ b/packages/toml-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md index f4f5597..a70b454 100644 --- a/packages/tree-haver/README.md +++ b/packages/tree-haver/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md index 5be9d21..a465363 100644 --- a/packages/typescript-compiler-merge/README.md +++ b/packages/typescript-compiler-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md index e1c5f6e..61944d6 100644 --- a/packages/typescript-merge/README.md +++ b/packages/typescript-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md index afc5c5b..e1b9a40 100644 --- a/packages/yaml-merge/README.md +++ b/packages/yaml-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md index 5b60541..1bbb0e1 100644 --- a/packages/zip-merge/README.md +++ b/packages/zip-merge/README.md @@ -34,6 +34,13 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | native ecosystem parser | Ruby | ruby, yaml, markdown, toml | Backend-specific Ruby packages are provider prior art or adapters, not the source schema. | | plain structured text | Go, Ruby, Rust, TypeScript | plain, binary, zip | Families without parser requirements document preservation, byte ranges, archive members, and diagnostics. | +| Compatibility claim | Current disposition | Fixture source | +|---|---|---| +| Old Ruby runtime backend tables | Prior art only; not a cross-language support promise | slice-741 backend/platform reconciliation | +| tree-sitter-language-pack | Current portable parser substrate for Go, Ruby, Rust, and TypeScript | slices 122, 135, 171, 195, 215 | +| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | +| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | + ## ✨ Installation From 98fdd4869e5d5fb930c40b6417d3a2ec98063d44 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 03:43:56 -0600 Subject: [PATCH 010/130] Render reusable README examples --- packages/ast-merge/README.md | 9 +++++++++ packages/ast-template/README.md | 9 +++++++++ packages/binary-merge/README.md | 9 +++++++++ packages/go-merge/README.md | 9 +++++++++ packages/js-yaml-merge/README.md | 9 +++++++++ packages/json-merge/README.md | 9 +++++++++ packages/kettle-nodule/README.md | 9 +++++++++ packages/kettle-nodule/src/index.ts | 9 +++++++++ packages/kettle-nodule/test/thin-slice.test.ts | 3 ++- packages/markdown-it-merge/README.md | 9 +++++++++ packages/markdown-merge/README.md | 9 +++++++++ packages/peggy-toml-merge/README.md | 9 +++++++++ packages/plain-merge/README.md | 9 +++++++++ packages/ruby-merge/README.md | 9 +++++++++ packages/rust-merge/README.md | 9 +++++++++ packages/toml-merge/README.md | 9 +++++++++ packages/tree-haver/README.md | 9 +++++++++ packages/typescript-compiler-merge/README.md | 9 +++++++++ packages/typescript-merge/README.md | 9 +++++++++ packages/yaml-merge/README.md | 9 +++++++++ packages/zip-merge/README.md | 9 +++++++++ 21 files changed, 182 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md index 39b4ae6..63b616f 100644 --- a/packages/ast-merge/README.md +++ b/packages/ast-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md index 7af119c..9cc28d3 100644 --- a/packages/ast-template/README.md +++ b/packages/ast-template/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md index 5a686f8..969312d 100644 --- a/packages/binary-merge/README.md +++ b/packages/binary-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md index c6a6509..6b77d59 100644 --- a/packages/go-merge/README.md +++ b/packages/go-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md index f77ad5b..09507b8 100644 --- a/packages/js-yaml-merge/README.md +++ b/packages/js-yaml-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md index 97d84d2..38df3e5 100644 --- a/packages/json-merge/README.md +++ b/packages/json-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md index 9a58194..d209ee8 100644 --- a/packages/kettle-nodule/README.md +++ b/packages/kettle-nodule/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index 189a9ab..baa7b7b 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -826,6 +826,15 @@ function readmeFamilyIntroAndBackendMatrix(): string { '| Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 |', '| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list |', '', + '| Reusable example | README role | Source fixture |', + '|---|---|---|', + '| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples |', + '| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples |', + '| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples |', + '| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples |', + '| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples |', + '| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples |', + '', '' ].join('\n'); } diff --git a/packages/kettle-nodule/test/thin-slice.test.ts b/packages/kettle-nodule/test/thin-slice.test.ts index f590f1f..b9f9ac6 100644 --- a/packages/kettle-nodule/test/thin-slice.test.ts +++ b/packages/kettle-nodule/test/thin-slice.test.ts @@ -276,7 +276,8 @@ describe('kettle-nodule thin vertical slice', () => { 'https://logos.galtzo.com/assets/images/tree-sitter/tree-sitter/avatar-192px.svg', 'StructuredMerge packages provide fixture-backed merge behavior', '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source |', - '| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist |' + '| bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist |', + '| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections |' ]) { expect(plan.finalContent).toContain(snippet); } diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md index 18054d5..0631a51 100644 --- a/packages/markdown-it-merge/README.md +++ b/packages/markdown-it-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md index d3fdbc9..d730361 100644 --- a/packages/markdown-merge/README.md +++ b/packages/markdown-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md index f37618d..810f323 100644 --- a/packages/peggy-toml-merge/README.md +++ b/packages/peggy-toml-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md index 05ba658..9e4c03e 100644 --- a/packages/plain-merge/README.md +++ b/packages/plain-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md index cf8fe8c..df76f1e 100644 --- a/packages/ruby-merge/README.md +++ b/packages/ruby-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md index 090d43a..8d627c1 100644 --- a/packages/rust-merge/README.md +++ b/packages/rust-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md index 386e9d0..3e43c26 100644 --- a/packages/toml-merge/README.md +++ b/packages/toml-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md index a70b454..e4e6ed7 100644 --- a/packages/tree-haver/README.md +++ b/packages/tree-haver/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md index a465363..06c0d04 100644 --- a/packages/typescript-compiler-merge/README.md +++ b/packages/typescript-compiler-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md index 61944d6..b169d95 100644 --- a/packages/typescript-merge/README.md +++ b/packages/typescript-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md index e1b9a40..58cb800 100644 --- a/packages/yaml-merge/README.md +++ b/packages/yaml-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md index 1bbb0e1..55c6e3f 100644 --- a/packages/zip-merge/README.md +++ b/packages/zip-merge/README.md @@ -41,6 +41,15 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | Native parser/adaptor backends | Implementation-specific providers documented through family fixtures | slices 122 and 183 | | bash-merge, dotenv-merge, rbs-merge | Excluded from generated support tables until explicit scope decisions exist | slice-741 unresolved package list | +| Reusable example | README role | Source fixture | +|---|---|---| +| Freeze tokens | Show how destination-owned regions are preserved without filling project-specific usage sections | slice-743 reusable README configuration examples | +| Match preference | Summarize template-wins and destination-wins conflict choices through current policy vocabulary | slice-743 reusable README configuration examples | +| Template-only behavior | Explain accept/skip handling for unmatched template entries | slice-743 reusable README configuration examples | +| Debug report inspection | Point users to structured reports and diagnostics instead of ad hoc debug prose | slice-743 reusable README configuration examples | +| Backend selection | Describe portable backend selection without old Ruby runtime support tables | slice-743 reusable README configuration examples | +| Package-directory README command | Document plan/apply/convergence workflow for shared README updates | slice-743 reusable README configuration examples | + ## ✨ Installation From 72cdc2286f7244c42c4a15b4af5809775ecf53dc Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 04:00:14 -0600 Subject: [PATCH 011/130] Render JSONC migration note --- packages/ast-merge/README.md | 2 ++ packages/ast-template/README.md | 2 ++ packages/binary-merge/README.md | 2 ++ packages/go-merge/README.md | 2 ++ packages/js-yaml-merge/README.md | 2 ++ packages/json-merge/README.md | 2 ++ packages/kettle-nodule/README.md | 2 ++ packages/kettle-nodule/src/index.ts | 2 ++ packages/markdown-it-merge/README.md | 2 ++ packages/markdown-merge/README.md | 2 ++ packages/peggy-toml-merge/README.md | 2 ++ packages/plain-merge/README.md | 2 ++ packages/ruby-merge/README.md | 2 ++ packages/rust-merge/README.md | 2 ++ packages/toml-merge/README.md | 2 ++ packages/tree-haver/README.md | 2 ++ packages/typescript-compiler-merge/README.md | 2 ++ packages/typescript-merge/README.md | 2 ++ packages/yaml-merge/README.md | 2 ++ packages/zip-merge/README.md | 2 ++ 20 files changed, 40 insertions(+) diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md index 63b616f..d396f92 100644 --- a/packages/ast-merge/README.md +++ b/packages/ast-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md index 9cc28d3..b31fab6 100644 --- a/packages/ast-template/README.md +++ b/packages/ast-template/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md index 969312d..67df47f 100644 --- a/packages/binary-merge/README.md +++ b/packages/binary-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md index 6b77d59..db0c91e 100644 --- a/packages/go-merge/README.md +++ b/packages/go-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md index 09507b8..9e50e27 100644 --- a/packages/js-yaml-merge/README.md +++ b/packages/js-yaml-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md index 38df3e5..40ca56f 100644 --- a/packages/json-merge/README.md +++ b/packages/json-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md index d209ee8..f7c3a04 100644 --- a/packages/kettle-nodule/README.md +++ b/packages/kettle-nodule/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index baa7b7b..e00e3a6 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -813,6 +813,8 @@ function readmeFamilyIntroAndBackendMatrix(): string { '| zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior |', '| binary-merge | family | binary | active | documents binary preservation and diagnostics behavior |', '', + 'JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples.', + '', '| Backend | Languages | Families | Note |', '|---|---|---|---|', '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. |', diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md index 0631a51..911f51c 100644 --- a/packages/markdown-it-merge/README.md +++ b/packages/markdown-it-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md index d730361..1e5c328 100644 --- a/packages/markdown-merge/README.md +++ b/packages/markdown-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md index 810f323..4491a65 100644 --- a/packages/peggy-toml-merge/README.md +++ b/packages/peggy-toml-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md index 9e4c03e..8d15407 100644 --- a/packages/plain-merge/README.md +++ b/packages/plain-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md index df76f1e..cf8c545 100644 --- a/packages/ruby-merge/README.md +++ b/packages/ruby-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md index 8d627c1..edeaa07 100644 --- a/packages/rust-merge/README.md +++ b/packages/rust-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md index 3e43c26..0786f20 100644 --- a/packages/toml-merge/README.md +++ b/packages/toml-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md index e4e6ed7..3b17445 100644 --- a/packages/tree-haver/README.md +++ b/packages/tree-haver/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md index 06c0d04..b0feda1 100644 --- a/packages/typescript-compiler-merge/README.md +++ b/packages/typescript-compiler-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md index b169d95..9bf9554 100644 --- a/packages/typescript-merge/README.md +++ b/packages/typescript-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md index 58cb800..2f6d248 100644 --- a/packages/yaml-merge/README.md +++ b/packages/yaml-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md index 55c6e3f..ae4c6d2 100644 --- a/packages/zip-merge/README.md +++ b/packages/zip-merge/README.md @@ -28,6 +28,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con | zip-merge | family | zip, archive | active | documents ZIP member planning and raw-preservation behavior | | binary-merge | family | binary | active | documents binary preservation and diagnostics behavior | +JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | From ba8c85623f6defee1948430f554c644cb4e9f402 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 04:07:53 -0600 Subject: [PATCH 012/130] Render YAML provider note --- packages/ast-merge/README.md | 2 ++ packages/ast-template/README.md | 2 ++ packages/binary-merge/README.md | 2 ++ packages/go-merge/README.md | 2 ++ packages/js-yaml-merge/README.md | 2 ++ packages/json-merge/README.md | 2 ++ packages/kettle-nodule/README.md | 2 ++ packages/kettle-nodule/src/index.ts | 2 ++ packages/markdown-it-merge/README.md | 2 ++ packages/markdown-merge/README.md | 2 ++ packages/peggy-toml-merge/README.md | 2 ++ packages/plain-merge/README.md | 2 ++ packages/ruby-merge/README.md | 2 ++ packages/rust-merge/README.md | 2 ++ packages/toml-merge/README.md | 2 ++ packages/tree-haver/README.md | 2 ++ packages/typescript-compiler-merge/README.md | 2 ++ packages/typescript-merge/README.md | 2 ++ packages/yaml-merge/README.md | 2 ++ packages/zip-merge/README.md | 2 ++ 20 files changed, 40 insertions(+) diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md index d396f92..057e5b7 100644 --- a/packages/ast-merge/README.md +++ b/packages/ast-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md index b31fab6..70e7fd8 100644 --- a/packages/ast-template/README.md +++ b/packages/ast-template/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md index 67df47f..0b7c9bc 100644 --- a/packages/binary-merge/README.md +++ b/packages/binary-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md index db0c91e..e7cbc3c 100644 --- a/packages/go-merge/README.md +++ b/packages/go-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md index 9e50e27..9b94316 100644 --- a/packages/js-yaml-merge/README.md +++ b/packages/js-yaml-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md index 40ca56f..5c1863c 100644 --- a/packages/json-merge/README.md +++ b/packages/json-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md index f7c3a04..8ce37cf 100644 --- a/packages/kettle-nodule/README.md +++ b/packages/kettle-nodule/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index e00e3a6..d750f4c 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -815,6 +815,8 @@ function readmeFamilyIntroAndBackendMatrix(): string { '', 'JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples.', '', + 'YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby\'s `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior.', + '', '| Backend | Languages | Families | Note |', '|---|---|---|---|', '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. |', diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md index 911f51c..8c82488 100644 --- a/packages/markdown-it-merge/README.md +++ b/packages/markdown-it-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md index 1e5c328..66c4d72 100644 --- a/packages/markdown-merge/README.md +++ b/packages/markdown-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md index 4491a65..23343f1 100644 --- a/packages/peggy-toml-merge/README.md +++ b/packages/peggy-toml-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md index 8d15407..1564877 100644 --- a/packages/plain-merge/README.md +++ b/packages/plain-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md index cf8c545..edcd9be 100644 --- a/packages/ruby-merge/README.md +++ b/packages/ruby-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md index edeaa07..8152e76 100644 --- a/packages/rust-merge/README.md +++ b/packages/rust-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md index 0786f20..a595f76 100644 --- a/packages/toml-merge/README.md +++ b/packages/toml-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md index 3b17445..dc5e127 100644 --- a/packages/tree-haver/README.md +++ b/packages/tree-haver/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md index b0feda1..a1ea418 100644 --- a/packages/typescript-compiler-merge/README.md +++ b/packages/typescript-compiler-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md index 9bf9554..225f0ca 100644 --- a/packages/typescript-merge/README.md +++ b/packages/typescript-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md index 2f6d248..e83c1ec 100644 --- a/packages/yaml-merge/README.md +++ b/packages/yaml-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md index ae4c6d2..1b93e77 100644 --- a/packages/zip-merge/README.md +++ b/packages/zip-merge/README.md @@ -30,6 +30,8 @@ StructuredMerge packages provide fixture-backed merge behavior for document, con JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. The old `jsonc-merge` package name is superseded in the cross-language toolset; only Ruby may grow a legacy `require "jsonc/merge"` wrapper if packaging compatibility requires it. Current fixture-backed JSONC claims are parse support and comment-neutral owner structure; comment-preserving merge output, freeze blocks, and JSONC emitter behavior need dedicated fixtures before they appear in package examples. +YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | From 3987da2954d41825339cb45f6dcd743ade37f542 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 04:17:22 -0600 Subject: [PATCH 013/130] Render Markdown provider note --- packages/ast-merge/README.md | 2 ++ packages/ast-template/README.md | 2 ++ packages/binary-merge/README.md | 2 ++ packages/go-merge/README.md | 2 ++ packages/js-yaml-merge/README.md | 2 ++ packages/json-merge/README.md | 2 ++ packages/kettle-nodule/README.md | 2 ++ packages/kettle-nodule/src/index.ts | 2 ++ packages/markdown-it-merge/README.md | 2 ++ packages/markdown-merge/README.md | 2 ++ packages/peggy-toml-merge/README.md | 2 ++ packages/plain-merge/README.md | 2 ++ packages/ruby-merge/README.md | 2 ++ packages/rust-merge/README.md | 2 ++ packages/toml-merge/README.md | 2 ++ packages/tree-haver/README.md | 2 ++ packages/typescript-compiler-merge/README.md | 2 ++ packages/typescript-merge/README.md | 2 ++ packages/yaml-merge/README.md | 2 ++ packages/zip-merge/README.md | 2 ++ 20 files changed, 40 insertions(+) diff --git a/packages/ast-merge/README.md b/packages/ast-merge/README.md index 057e5b7..d4f4327 100644 --- a/packages/ast-merge/README.md +++ b/packages/ast-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ast-template/README.md b/packages/ast-template/README.md index 70e7fd8..a2e8368 100644 --- a/packages/ast-template/README.md +++ b/packages/ast-template/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/binary-merge/README.md b/packages/binary-merge/README.md index 0b7c9bc..882106c 100644 --- a/packages/binary-merge/README.md +++ b/packages/binary-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/go-merge/README.md b/packages/go-merge/README.md index e7cbc3c..842f374 100644 --- a/packages/go-merge/README.md +++ b/packages/go-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/js-yaml-merge/README.md b/packages/js-yaml-merge/README.md index 9b94316..8c536d2 100644 --- a/packages/js-yaml-merge/README.md +++ b/packages/js-yaml-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/json-merge/README.md b/packages/json-merge/README.md index 5c1863c..0a48d31 100644 --- a/packages/json-merge/README.md +++ b/packages/json-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/README.md b/packages/kettle-nodule/README.md index 8ce37cf..9f480c6 100644 --- a/packages/kettle-nodule/README.md +++ b/packages/kettle-nodule/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/kettle-nodule/src/index.ts b/packages/kettle-nodule/src/index.ts index d750f4c..2757496 100644 --- a/packages/kettle-nodule/src/index.ts +++ b/packages/kettle-nodule/src/index.ts @@ -817,6 +817,8 @@ function readmeFamilyIntroAndBackendMatrix(): string { '', 'YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby\'s `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior.', '', + 'Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`.', + '', '| Backend | Languages | Families | Note |', '|---|---|---|---|', '| tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. |', diff --git a/packages/markdown-it-merge/README.md b/packages/markdown-it-merge/README.md index 8c82488..9b9b38f 100644 --- a/packages/markdown-it-merge/README.md +++ b/packages/markdown-it-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/markdown-merge/README.md b/packages/markdown-merge/README.md index 66c4d72..fa67ee9 100644 --- a/packages/markdown-merge/README.md +++ b/packages/markdown-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/peggy-toml-merge/README.md b/packages/peggy-toml-merge/README.md index 23343f1..2873bf7 100644 --- a/packages/peggy-toml-merge/README.md +++ b/packages/peggy-toml-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/plain-merge/README.md b/packages/plain-merge/README.md index 1564877..8b1d2aa 100644 --- a/packages/plain-merge/README.md +++ b/packages/plain-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/ruby-merge/README.md b/packages/ruby-merge/README.md index edcd9be..b83a84f 100644 --- a/packages/ruby-merge/README.md +++ b/packages/ruby-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/rust-merge/README.md b/packages/rust-merge/README.md index 8152e76..4788736 100644 --- a/packages/rust-merge/README.md +++ b/packages/rust-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/toml-merge/README.md b/packages/toml-merge/README.md index a595f76..a811fb3 100644 --- a/packages/toml-merge/README.md +++ b/packages/toml-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/tree-haver/README.md b/packages/tree-haver/README.md index dc5e127..f60496c 100644 --- a/packages/tree-haver/README.md +++ b/packages/tree-haver/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-compiler-merge/README.md b/packages/typescript-compiler-merge/README.md index a1ea418..dccaccc 100644 --- a/packages/typescript-compiler-merge/README.md +++ b/packages/typescript-compiler-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/typescript-merge/README.md b/packages/typescript-merge/README.md index 225f0ca..173e59b 100644 --- a/packages/typescript-merge/README.md +++ b/packages/typescript-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/yaml-merge/README.md b/packages/yaml-merge/README.md index e83c1ec..227691b 100644 --- a/packages/yaml-merge/README.md +++ b/packages/yaml-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | diff --git a/packages/zip-merge/README.md b/packages/zip-merge/README.md index 1b93e77..9ae71b7 100644 --- a/packages/zip-merge/README.md +++ b/packages/zip-merge/README.md @@ -32,6 +32,8 @@ JSONC migration note: JSONC is handled by `json-merge` as the `jsonc` dialect. T YAML provider note: `yaml-merge` is the canonical YAML family package. Ruby's `psych-merge` package is the Psych provider for that family, not a separate YAML family; old `Psych::Merge::*` examples remain provider-specific until portable fixtures cover the behavior. +Markdown provider note: `markdown-merge` is the canonical Markdown family package. Provider packages own parser-specific docs and backend defaults: Go `goldmarkmerge`, Ruby `commonmarker-merge`, `markly-merge`, and `kramdown-merge`, Rust `pulldown-cmark-merge`, and TypeScript `@structuredmerge/markdown-it-merge`. + | Backend | Languages | Families | Note | |---|---|---|---| | tree-sitter-language-pack | Go, Ruby, Rust, TypeScript | markdown, toml, yaml, source | Preferred cross-language parser substrate where a family has language-pack support. | From ec8dd1e66c4d794c289fb69aa85013b339566f8a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:20:07 -0600 Subject: [PATCH 014/130] Support draft 02 compact ruleset directives --- packages/ast-merge/src/contracts.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 9d60d1d..50e07bf 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -125,9 +125,14 @@ const compactRulesetSingletonDirectives = new Set([ 'read', 'attach', 'comment_style', - 'render' + 'render', + 'render_strategy' ]); const compactRulesetRepeatableKeyedDirectives = new Set([ + 'backend', + 'node_role', + 'atomic', + 'child_group', 'capability', 'logical_owner', 'repair', @@ -206,7 +211,7 @@ export function parseCompactRuleset(source: string): ParseResult ); } if (compactRulesetRepeatableKeyedDirectives.has(name)) { - const key = `${name}\u0000${args[0]}`; + const key = compactRulesetRepeatableKey(name, args); if (seenRepeatableKeys.has(key)) { diagnostics.push( compactRulesetDiagnostic( @@ -251,6 +256,13 @@ function compactRulesetKnownDirective(name: string): boolean { ); } +function compactRulesetRepeatableKey(name: string, args: readonly string[]): string { + if (name === 'child_group' && args.length > 1) { + return `${name}\u0000${args[0]}\u0000${args[1]}`; + } + return `${name}\u0000${args[0]}`; +} + function compactRulesetDiagnostic(message: string, path?: string): Diagnostic { return { severity: 'error', From 22250c7b9522d66c5240da70deafb4ba6115bd1f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:27:06 -0600 Subject: [PATCH 015/130] Add compact ruleset profile extraction --- packages/ast-merge/src/contracts.ts | 147 ++++++++++++++++++ packages/ast-merge/src/index.ts | 9 ++ .../ast-merge/test/compact-ruleset.test.ts | 35 ++++- 3 files changed, 190 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 50e07bf..242f796 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -115,6 +115,62 @@ export interface CompactRuleset { readonly comments: readonly string[]; } +export interface CompactRulesetBackendDeclaration { + readonly backend: string; + readonly support: string; +} + +export interface CompactRulesetNodeRole { + readonly selector: string; + readonly role: string; +} + +export interface CompactRulesetAtomicNode { + readonly selector: string; + readonly atomic: boolean; +} + +export interface CompactRulesetChildGroup { + readonly parent_selector: string; + readonly name: string; + readonly policy: string; +} + +export interface CompactRulesetNamedValue { + readonly name: string; + readonly value: string; +} + +export interface CompactRulesetSurfaceDeclaration { + readonly name: string; + readonly selector: string; +} + +export interface CompactRulesetDelegateDeclaration { + readonly surface: string; + readonly policy: string; +} + +export interface CompactRulesetProfile { + readonly format: string; + readonly owners: string; + readonly match: string; + readonly read: string; + readonly attach: string; + readonly comment_style?: string; + readonly render?: string; + readonly render_strategy?: string; + readonly backends: readonly CompactRulesetBackendDeclaration[]; + readonly node_roles: readonly CompactRulesetNodeRole[]; + readonly atomic_nodes: readonly CompactRulesetAtomicNode[]; + readonly child_groups: readonly CompactRulesetChildGroup[]; + readonly capabilities: readonly CompactRulesetNamedValue[]; + readonly logical_owners: readonly CompactRulesetNamedValue[]; + readonly repairs: readonly CompactRulesetNamedValue[]; + readonly surfaces: readonly CompactRulesetSurfaceDeclaration[]; + readonly delegates: readonly CompactRulesetDelegateDeclaration[]; +} + const compactRulesetIdentifierPattern = /^[A-Za-z][A-Za-z0-9_.-]*$/; const compactRulesetTokenPattern = /^[\x21\x24-\x7e]+$/; const compactRulesetRequiredDirectives = ['format', 'owners', 'match', 'read', 'attach'] as const; @@ -250,6 +306,97 @@ export function parseCompactRuleset(source: string): ParseResult : { ok: false, diagnostics, policies: [] }; } +export function compactRulesetFeatureProfile(ruleset: CompactRuleset): CompactRulesetProfile { + const profile: MutableCompactRulesetProfile = { + format: '', + owners: '', + match: '', + read: '', + attach: '', + backends: [], + node_roles: [], + atomic_nodes: [], + child_groups: [], + capabilities: [], + logical_owners: [], + repairs: [], + surfaces: [], + delegates: [] + }; + + for (const directive of ruleset.directives) { + const args = directive.arguments; + if (args.length === 0) continue; + switch (directive.name) { + case 'format': + profile.format = args[0]; + break; + case 'owners': + profile.owners = args[0]; + break; + case 'match': + profile.match = args[0]; + break; + case 'read': + profile.read = args[0]; + break; + case 'attach': + profile.attach = args[0]; + break; + case 'comment_style': + profile.comment_style = args[0]; + break; + case 'render': + profile.render = args[0]; + break; + case 'render_strategy': + profile.render_strategy = args[0]; + break; + case 'backend': + if (args.length > 1) profile.backends.push({ backend: args[0], support: args[1] }); + break; + case 'node_role': + if (args.length > 1) profile.node_roles.push({ selector: args[0], role: args[1] }); + break; + case 'atomic': + if (args.length > 1) profile.atomic_nodes.push({ selector: args[0], atomic: args[1] === 'true' }); + break; + case 'child_group': + if (args.length > 2) { + profile.child_groups.push({ + parent_selector: args[0], + name: args[1], + policy: args[2] + }); + } + break; + case 'capability': + if (args.length > 1) profile.capabilities.push({ name: args[0], value: args[1] }); + break; + case 'logical_owner': + if (args.length > 1) profile.logical_owners.push({ name: args[0], value: args[1] }); + break; + case 'repair': + if (args.length > 1) profile.repairs.push({ name: args[0], value: args[1] }); + break; + case 'surface': + if (args.length > 1) profile.surfaces.push({ name: args[0], selector: args[1] }); + break; + case 'delegate': + if (args.length > 1) profile.delegates.push({ surface: args[0], policy: args[1] }); + break; + } + } + + return profile; +} + +type MutableCompactRulesetProfile = { + -readonly [K in keyof CompactRulesetProfile]: CompactRulesetProfile[K] extends readonly (infer T)[] + ? T[] + : CompactRulesetProfile[K]; +}; + function compactRulesetKnownDirective(name: string): boolean { return ( compactRulesetSingletonDirectives.has(name) || compactRulesetRepeatableKeyedDirectives.has(name) diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index a04881a..67aba6a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -35,7 +35,15 @@ export type { DiagnosticCategory, DiagnosticSeverity, CompactRuleset, + CompactRulesetAtomicNode, + CompactRulesetBackendDeclaration, + CompactRulesetChildGroup, + CompactRulesetDelegateDeclaration, CompactRulesetDirective, + CompactRulesetNamedValue, + CompactRulesetNodeRole, + CompactRulesetProfile, + CompactRulesetSurfaceDeclaration, DiscoveredSurface, DelegatedChildOperation, ReviewDiagnosticReason, @@ -246,6 +254,7 @@ export { STRUCTURED_EDIT_TRANSPORT_VERSION, REVIEW_TRANSPORT_VERSION, conformanceManifestReplayContext, + compactRulesetFeatureProfile, parseCompactRuleset, conformanceManifestReviewStateEnvelope, conformanceManifestReviewRequestIds, diff --git a/packages/ast-merge/test/compact-ruleset.test.ts b/packages/ast-merge/test/compact-ruleset.test.ts index 55705c5..d7c324a 100644 --- a/packages/ast-merge/test/compact-ruleset.test.ts +++ b/packages/ast-merge/test/compact-ruleset.test.ts @@ -1,7 +1,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { parseCompactRuleset } from '../src/index'; +import { compactRulesetFeatureProfile, parseCompactRuleset } from '../src/index'; function rulesetFixtures(root: string): string[] { return readdirSync(root).flatMap((entry) => { @@ -40,4 +40,37 @@ describe('parseCompactRuleset', () => { expect(result.diagnostics.length, name).toBeGreaterThan(0); } }); + + it('derives the shared compact ruleset feature profile fixture', () => { + const fixture = JSON.parse( + readFileSync( + path.resolve( + import.meta.dirname, + '..', + '..', + '..', + '..', + 'fixtures', + 'diagnostics', + 'slice-781-compact-ruleset-profile', + 'module-profile.json' + ), + 'utf8' + ) + ) as { ruleset_path: string[]; profile: unknown }; + + const rulesetPath = path.resolve( + import.meta.dirname, + '..', + '..', + '..', + '..', + 'fixtures', + ...fixture.ruleset_path + ); + const result = parseCompactRuleset(readFileSync(rulesetPath, 'utf8')); + expect(result.ok, JSON.stringify(result.diagnostics)).toBe(true); + expect(result.analysis).toBeDefined(); + expect(compactRulesetFeatureProfile(result.analysis!)).toEqual(fixture.profile); + }); }); From 847039fe009360e954565feb804db5cf1135271d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:35:04 -0600 Subject: [PATCH 016/130] Add normalized tree node contract --- packages/tree-haver/src/contracts.ts | 39 ++++++++++++ packages/tree-haver/src/index.ts | 3 + .../test/fixtures.integration.test.ts | 61 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 8efa295..c013732 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -103,6 +103,45 @@ export interface SourceSpan { readonly endPoint: SourcePoint; } +export type NodeRole = + | 'structural' + | 'token' + | 'trivia' + | 'comment' + | 'delimiter' + | 'separator' + | 'virtual' + | 'error' + | 'opaque'; + +export interface NormalizedTreeNode { + readonly id: string; + readonly kind: string; + readonly role: NodeRole; + readonly parentId?: string; + readonly childIds: readonly string[]; + readonly span: SourceSpan; + readonly fieldName?: string; + readonly named: boolean; + readonly anonymous: boolean; + readonly hasSourceText: boolean; + readonly sourceFragment: string; +} + +export function nodeRoles(): readonly NodeRole[] { + return [ + 'structural', + 'token', + 'trivia', + 'comment', + 'delimiter', + 'separator', + 'virtual', + 'error', + 'opaque' + ]; +} + export interface ByteEditSpan { readonly startByte: number; readonly oldEndByte: number; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index ffacdd6..338c738 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -22,6 +22,8 @@ export type { KaitaiTreeNode, LanguagePackAnalysis, LanguagePackProcessAnalysis, + NodeRole, + NormalizedTreeNode, ParserAdapter, ParserDiagnostics, ParserRequest, @@ -59,6 +61,7 @@ export { kaitaiFeatureProfile, KREUZBERG_LANGUAGE_PACK_BACKEND, languagePackAdapterInfo, + nodeRoles, PEGGY_BACKEND, peggyAdapterInfo, peggyFeatureProfile, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 370cb99..eb3db81 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -12,6 +12,7 @@ import type { BinaryScalarValue, ByteEditSpan, FeatureProfile, + NormalizedTreeNode, ParserRequest, PolicyReference, ZipUnsafeEntry @@ -34,6 +35,7 @@ import { PEGGY_BACKEND, peggyAdapterInfo, peggyFeatureProfile, + nodeRoles, registerBackend, registeredBackends, sliceByteRange, @@ -193,6 +195,24 @@ interface ConformanceManifest { >; } +interface NormalizedTreeNodeFixture { + readonly id: string; + readonly kind: string; + readonly role: NormalizedTreeNode['role']; + readonly parent_id: string | null; + readonly child_ids: readonly string[]; + readonly span: { + readonly range: { readonly start_byte: number; readonly end_byte: number }; + readonly start_point: { readonly row: number; readonly column: number }; + readonly end_point: { readonly row: number; readonly column: number }; + }; + readonly field_name: string | null; + readonly named: boolean; + readonly anonymous: boolean; + readonly has_source_text: boolean; + readonly source_fragment: string; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -214,6 +234,29 @@ function diagnosticsFixturePath(role: string): string[] { return [...entry.path]; } +function normalizedTreeNode(fixture: NormalizedTreeNodeFixture): NormalizedTreeNode { + return { + id: fixture.id, + kind: fixture.kind, + role: fixture.role, + parentId: fixture.parent_id ?? undefined, + childIds: fixture.child_ids, + span: { + range: { + startByte: fixture.span.range.start_byte, + endByte: fixture.span.range.end_byte + }, + startPoint: fixture.span.start_point, + endPoint: fixture.span.end_point + }, + fieldName: fixture.field_name ?? undefined, + named: fixture.named, + anonymous: fixture.anonymous, + hasSourceText: fixture.has_source_text, + sourceFragment: fixture.source_fragment + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -236,6 +279,24 @@ describe('tree-haver shared fixtures', () => { }).toEqual(fixture.adapter_info); }); + it('conforms to the slice-782 normalized tree node fixture', () => { + const fixture = readFixture<{ + node_roles: string[]; + node: NormalizedTreeNodeFixture; + child: NormalizedTreeNodeFixture; + }>('diagnostics', 'slice-782-normalized-tree-node', 'normalized-tree-node.json'); + + expect(nodeRoles()).toEqual(fixture.node_roles); + const node = normalizedTreeNode(fixture.node); + const child = normalizedTreeNode(fixture.child); + + expect(node.role).toBe('structural'); + expect(node.childIds[1]).toBe(child.id); + expect(child.parentId).toBe(node.id); + expect(child.fieldName).toBe('declaration'); + expect(child.hasSourceText).toBe(true); + }); + it('conforms to the slice-19 adapter policy support fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('adapter_policy_support') From 5d69706d20dea48418e21aea991cf06dfe0a3c6b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:37:36 -0600 Subject: [PATCH 017/130] Add backend capability contract --- packages/tree-haver/src/contracts.ts | 26 ++++++++ packages/tree-haver/src/index.ts | 3 +- .../test/fixtures.integration.test.ts | 60 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index c013732..addecb2 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -62,6 +62,32 @@ export interface FeatureProfile { readonly supportedPolicies?: readonly PolicyReference[]; } +export interface ParserIdentity { + readonly name: string; + readonly version: string; + readonly implementation: string; +} + +export interface LanguageVersion { + readonly version: string; + readonly dialect?: string; +} + +export interface BackendCapability { + readonly backendRef: BackendReference; + readonly language: string; + readonly parserIdentity: ParserIdentity; + readonly languageVersion: LanguageVersion; + readonly parseErrorBehavior: string; + readonly sourceSpanSupport: string; + readonly sourceFragmentSupport: string; + readonly renderStrategies: readonly string[]; + readonly semanticRoleSupport: string; + readonly normalizedTreeSupport: boolean; + readonly nativeNodeAccess: boolean; + readonly diagnostics: readonly string[]; +} + export interface ParserAdapter { readonly info: AdapterInfo; parse(request: ParserRequest): ParseResult; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 338c738..5c2c6fd 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -3,6 +3,8 @@ export const packageName = '@structuredmerge/tree-haver'; export type { AdapterInfo, AnalysisHandle, + BackendCapability, + BackendReference, BinaryDiagnostic, BinaryMergeReport, BinaryNestedDispatch, @@ -10,7 +12,6 @@ export type { BinaryRawPayload, BinaryRenderPolicy, BinaryScalarValue, - BackendReference, ByteEditSpan, ByteRange, Diagnostic, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index eb3db81..4a3765b 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; import type { AdapterInfo, + BackendCapability, BackendReference, BinaryDiagnostic, BinaryMergeReport, @@ -213,6 +214,28 @@ interface NormalizedTreeNodeFixture { readonly source_fragment: string; } +interface BackendCapabilityFixture { + readonly backend_ref: BackendReference; + readonly language: string; + readonly parser_identity: { + readonly name: string; + readonly version: string; + readonly implementation: string; + }; + readonly language_version: { + readonly version: string; + readonly dialect: string | null; + }; + readonly parse_error_behavior: string; + readonly source_span_support: string; + readonly source_fragment_support: string; + readonly render_strategies: readonly string[]; + readonly semantic_role_support: string; + readonly normalized_tree_support: boolean; + readonly native_node_access: boolean; + readonly diagnostics: readonly string[]; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -257,6 +280,26 @@ function normalizedTreeNode(fixture: NormalizedTreeNodeFixture): NormalizedTreeN }; } +function backendCapability(fixture: BackendCapabilityFixture): BackendCapability { + return { + backendRef: fixture.backend_ref, + language: fixture.language, + parserIdentity: fixture.parser_identity, + languageVersion: { + version: fixture.language_version.version, + dialect: fixture.language_version.dialect ?? undefined + }, + parseErrorBehavior: fixture.parse_error_behavior, + sourceSpanSupport: fixture.source_span_support, + sourceFragmentSupport: fixture.source_fragment_support, + renderStrategies: fixture.render_strategies, + semanticRoleSupport: fixture.semantic_role_support, + normalizedTreeSupport: fixture.normalized_tree_support, + nativeNodeAccess: fixture.native_node_access, + diagnostics: fixture.diagnostics + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -297,6 +340,23 @@ describe('tree-haver shared fixtures', () => { expect(child.hasSourceText).toBe(true); }); + it('conforms to the slice-783 backend capability report fixture', () => { + const fixture = readFixture<{ capability: BackendCapabilityFixture }>( + 'diagnostics', + 'slice-783-backend-capability-report', + 'backend-capability-report.json' + ); + const capability = backendCapability(fixture.capability); + + expect(capability.backendRef).toEqual({ id: 'go-dst', family: 'native' }); + expect(capability.language).toBe('go'); + expect(capability.parserIdentity.name).toBe('github.com/dave/dst'); + expect(capability.parseErrorBehavior).toBe('diagnostic_and_partial_tree'); + expect(capability.renderStrategies[0]).toBe('source_fragment_reuse'); + expect(capability.normalizedTreeSupport).toBe(true); + expect(capability.nativeNodeAccess).toBe(true); + }); + it('conforms to the slice-19 adapter policy support fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('adapter_policy_support') From bfff40c0e8810f63503b73758359e05f80dd2b9f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:40:47 -0600 Subject: [PATCH 018/130] Add source fragment extraction contract --- packages/tree-haver/src/contracts.ts | 37 +++++++++++++++ packages/tree-haver/src/index.ts | 2 + .../test/fixtures.integration.test.ts | 45 +++++++++++++++++++ 3 files changed, 84 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index addecb2..3fa8da6 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -154,6 +154,15 @@ export interface NormalizedTreeNode { readonly sourceFragment: string; } +export interface SourceFragment { + readonly text: string; + readonly span: SourceSpan; + readonly available: boolean; + readonly strategy: string; + readonly byteLength: number; + readonly diagnostics: readonly string[]; +} + export function nodeRoles(): readonly NodeRole[] { return [ 'structural', @@ -319,6 +328,34 @@ export function sliceByteRange(source: string, range: ByteRange): string { return sourceBytes.subarray(range.startByte, range.endByte).toString('utf8'); } +export function extractSourceFragment( + source: string, + span: SourceSpan, + strategy: string +): SourceFragment { + try { + const text = sliceByteRange(source, span.range); + + return { + text, + span, + available: true, + strategy, + byteLength: Buffer.from(text, 'utf8').length, + diagnostics: [] + }; + } catch (error) { + return { + text: '', + span, + available: false, + strategy, + byteLength: 0, + diagnostics: [error instanceof Error ? error.message : String(error)] + }; + } +} + export function byteOffsetForPoint(source: string, point: SourcePoint): number { if (point.row < 0 || point.column < 0) { throw new RangeError(`invalid source point (${point.row}, ${point.column})`); diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 5c2c6fd..f40216b 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -36,6 +36,7 @@ export type { ProcessRequest, ProcessSpan, ProcessStructureItem, + SourceFragment, SourcePoint, SourceSpan, ZipArchiveEntry, @@ -57,6 +58,7 @@ export { byteRangeOverlaps, createPeggyParser, currentBackendId, + extractSourceFragment, KAITAI_STRUCT_BACKEND, kaitaiAdapterInfo, kaitaiFeatureProfile, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 4a3765b..c4eaac3 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -16,6 +16,7 @@ import type { NormalizedTreeNode, ParserRequest, PolicyReference, + SourceSpan, ZipUnsafeEntry } from '../src/index'; import { processWithLanguagePack } from '../src/index'; @@ -28,6 +29,7 @@ import { byteRangeLength, byteRangeOverlaps, currentBackendId, + extractSourceFragment, kaitaiAdapterInfo, kaitaiFeatureProfile, KAITAI_STRUCT_BACKEND, @@ -236,6 +238,23 @@ interface BackendCapabilityFixture { readonly diagnostics: readonly string[]; } +interface SourceFragmentExtractionFixture { + readonly source: string; + readonly strategy: string; + readonly span: { + readonly range: { readonly start_byte: number; readonly end_byte: number }; + readonly start_point: { readonly row: number; readonly column: number }; + readonly end_point: { readonly row: number; readonly column: number }; + }; + readonly fragment: { + readonly text: string; + readonly available: boolean; + readonly strategy: string; + readonly byte_length: number; + readonly diagnostics: readonly string[]; + }; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -300,6 +319,17 @@ function backendCapability(fixture: BackendCapabilityFixture): BackendCapability }; } +function sourceSpan(fixture: SourceFragmentExtractionFixture['span']): SourceSpan { + return { + range: { + startByte: fixture.range.start_byte, + endByte: fixture.range.end_byte + }, + startPoint: fixture.start_point, + endPoint: fixture.end_point + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -357,6 +387,21 @@ describe('tree-haver shared fixtures', () => { expect(capability.nativeNodeAccess).toBe(true); }); + it('conforms to the slice-784 source fragment extraction fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-784-source-fragment-extraction', + 'source-fragment-extraction.json' + ); + const fragment = extractSourceFragment(fixture.source, sourceSpan(fixture.span), fixture.strategy); + + expect(fragment.text).toBe(fixture.fragment.text); + expect(fragment.available).toBe(fixture.fragment.available); + expect(fragment.strategy).toBe(fixture.fragment.strategy); + expect(fragment.byteLength).toBe(fixture.fragment.byte_length); + expect(fragment.diagnostics).toHaveLength(fixture.fragment.diagnostics.length); + }); + it('conforms to the slice-19 adapter policy support fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('adapter_policy_support') From a3829245decbeceffd736de248c97f2a50c1c892 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 15:42:41 -0600 Subject: [PATCH 019/130] Add parse error tolerance contract --- packages/tree-haver/src/contracts.ts | 15 +++++++ packages/tree-haver/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 44 +++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 3fa8da6..613f480 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -88,6 +88,21 @@ export interface BackendCapability { readonly diagnostics: readonly string[]; } +export interface ParseErrorNode { + readonly kind: string; + readonly span: SourceSpan; + readonly message: string; +} + +export interface ParseErrorTolerance { + readonly backendRef: BackendReference; + readonly language: string; + readonly behavior: string; + readonly toleratesErrors: boolean; + readonly errorNodes: readonly ParseErrorNode[]; + readonly diagnostics: readonly string[]; +} + export interface ParserAdapter { readonly info: AdapterInfo; parse(request: ParserRequest): ParseResult; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index f40216b..9be51e0 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -25,6 +25,7 @@ export type { LanguagePackProcessAnalysis, NodeRole, NormalizedTreeNode, + ParseErrorTolerance, ParserAdapter, ParserDiagnostics, ParserRequest, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index c4eaac3..32d8b89 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -14,6 +14,7 @@ import type { ByteEditSpan, FeatureProfile, NormalizedTreeNode, + ParseErrorTolerance, ParserRequest, PolicyReference, SourceSpan, @@ -255,6 +256,19 @@ interface SourceFragmentExtractionFixture { }; } +interface ParseErrorToleranceFixture { + readonly backend_ref: BackendReference; + readonly language: string; + readonly behavior: string; + readonly tolerates_errors: boolean; + readonly error_nodes: Array<{ + readonly kind: string; + readonly span: SourceFragmentExtractionFixture['span']; + readonly message: string; + }>; + readonly diagnostics: readonly string[]; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -330,6 +344,21 @@ function sourceSpan(fixture: SourceFragmentExtractionFixture['span']): SourceSpa }; } +function parseErrorTolerance(fixture: ParseErrorToleranceFixture): ParseErrorTolerance { + return { + backendRef: fixture.backend_ref, + language: fixture.language, + behavior: fixture.behavior, + toleratesErrors: fixture.tolerates_errors, + errorNodes: fixture.error_nodes.map((node) => ({ + kind: node.kind, + span: sourceSpan(node.span), + message: node.message + })), + diagnostics: fixture.diagnostics + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -402,6 +431,21 @@ describe('tree-haver shared fixtures', () => { expect(fragment.diagnostics).toHaveLength(fixture.fragment.diagnostics.length); }); + it('conforms to the slice-785 parse error tolerance fixture', () => { + const fixture = readFixture<{ parse_error_tolerance: ParseErrorToleranceFixture }>( + 'diagnostics', + 'slice-785-parse-error-tolerance', + 'parse-error-tolerance.json' + ); + const tolerance = parseErrorTolerance(fixture.parse_error_tolerance); + + expect(tolerance.backendRef.id).toBe('tree-sitter-go'); + expect(tolerance.behavior).toBe('diagnostic_and_partial_tree'); + expect(tolerance.toleratesErrors).toBe(true); + expect(tolerance.errorNodes[0]?.span.range.startByte).toBe(27); + expect(tolerance.diagnostics[0]).toBe('partial tree contains parser error nodes'); + }); + it('conforms to the slice-19 adapter policy support fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('adapter_policy_support') From 180fed13e319e721d48044e87e08d953caef7843 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:02:02 -0600 Subject: [PATCH 020/130] Add progressive node metadata contract --- packages/tree-haver/src/contracts.ts | 5 ++++ .../test/fixtures.integration.test.ts | 28 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 613f480..a64c3c9 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -167,6 +167,11 @@ export interface NormalizedTreeNode { readonly anonymous: boolean; readonly hasSourceText: boolean; readonly sourceFragment: string; + readonly backendKind?: string; + readonly semanticRoles: readonly string[]; + readonly backendRoles: readonly string[]; + readonly unsupportedFeatures: readonly string[]; + readonly metadata: Readonly>>>; } export interface SourceFragment { diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 32d8b89..5e3c6dc 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -215,6 +215,11 @@ interface NormalizedTreeNodeFixture { readonly anonymous: boolean; readonly has_source_text: boolean; readonly source_fragment: string; + readonly backend_kind?: string; + readonly semantic_roles?: readonly string[]; + readonly backend_roles?: readonly string[]; + readonly unsupported_features?: readonly string[]; + readonly metadata?: Readonly>>>; } interface BackendCapabilityFixture { @@ -309,7 +314,12 @@ function normalizedTreeNode(fixture: NormalizedTreeNodeFixture): NormalizedTreeN named: fixture.named, anonymous: fixture.anonymous, hasSourceText: fixture.has_source_text, - sourceFragment: fixture.source_fragment + sourceFragment: fixture.source_fragment, + backendKind: fixture.backend_kind, + semanticRoles: fixture.semantic_roles ?? [], + backendRoles: fixture.backend_roles ?? [], + unsupportedFeatures: fixture.unsupported_features ?? [], + metadata: fixture.metadata ?? {} }; } @@ -399,6 +409,22 @@ describe('tree-haver shared fixtures', () => { expect(child.hasSourceText).toBe(true); }); + it('conforms to the slice-786 progressive node metadata fixture', () => { + const fixture = readFixture<{ + enhanced_node: NormalizedTreeNodeFixture; + limited_node: NormalizedTreeNodeFixture; + }>('diagnostics', 'slice-786-progressive-node-metadata', 'progressive-node-metadata.json'); + const enhanced = normalizedTreeNode(fixture.enhanced_node); + const limited = normalizedTreeNode(fixture.limited_node); + + expect(enhanced.backendKind).toBe('FuncDecl'); + expect(enhanced.semanticRoles[0]).toBe('declaration'); + expect(enhanced.metadata.go_dst?.node_path).toBe('decls[0]'); + expect(limited.hasSourceText).toBe(false); + expect(limited.unsupportedFeatures[1]).toBe('source_fragment'); + expect(limited.metadata.psych?.location_support).toBe('line_column_only'); + }); + it('conforms to the slice-783 backend capability report fixture', () => { const fixture = readFixture<{ capability: BackendCapabilityFixture }>( 'diagnostics', From e3ac3a5a19c612711e3d0962144ef7be2d8a5c55 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:24:20 -0600 Subject: [PATCH 021/130] Add native parser adapter contract --- packages/tree-haver/src/contracts.ts | 21 ++++++ packages/tree-haver/src/index.ts | 2 + .../test/fixtures.integration.test.ts | 65 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index a64c3c9..28663f8 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -103,6 +103,27 @@ export interface ParseErrorTolerance { readonly diagnostics: readonly string[]; } +export interface NativeParserProvider { + readonly id: string; + readonly family: string; + readonly language: string; + readonly operations: readonly string[]; + readonly retainsNativeTree: boolean; + readonly nativeTreeVisibility: string; + readonly metadataPolicy: string; +} + +export interface NormalizedParseResult { + readonly ok: boolean; + readonly backendCapability: BackendCapability; + readonly rootId: string; + readonly nodes: readonly NormalizedTreeNode[]; + readonly parseErrorTolerance: ParseErrorTolerance; + readonly sourceFragmentsAvailable: boolean; + readonly diagnostics: readonly string[]; + readonly metadata: Readonly>>>; +} + export interface ParserAdapter { readonly info: AdapterInfo; parse(request: ParserRequest): ParseResult; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 9be51e0..865f88f 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -23,6 +23,8 @@ export type { KaitaiTreeNode, LanguagePackAnalysis, LanguagePackProcessAnalysis, + NativeParserProvider, + NormalizedParseResult, NodeRole, NormalizedTreeNode, ParseErrorTolerance, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 5e3c6dc..4b13aca 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -13,6 +13,8 @@ import type { BinaryScalarValue, ByteEditSpan, FeatureProfile, + NativeParserProvider, + NormalizedParseResult, NormalizedTreeNode, ParseErrorTolerance, ParserRequest, @@ -274,6 +276,27 @@ interface ParseErrorToleranceFixture { readonly diagnostics: readonly string[]; } +interface NativeParserProviderFixture { + readonly id: string; + readonly family: string; + readonly language: string; + readonly operations: readonly string[]; + readonly retains_native_tree: boolean; + readonly native_tree_visibility: string; + readonly metadata_policy: string; +} + +interface NormalizedParseResultFixture { + readonly ok: boolean; + readonly backend_capability: BackendCapabilityFixture; + readonly root_id: string; + readonly nodes: readonly NormalizedTreeNodeFixture[]; + readonly parse_error_tolerance: ParseErrorToleranceFixture; + readonly source_fragments_available: boolean; + readonly diagnostics: readonly string[]; + readonly metadata: Readonly>>>; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -369,6 +392,31 @@ function parseErrorTolerance(fixture: ParseErrorToleranceFixture): ParseErrorTol }; } +function nativeParserProvider(fixture: NativeParserProviderFixture): NativeParserProvider { + return { + id: fixture.id, + family: fixture.family, + language: fixture.language, + operations: fixture.operations, + retainsNativeTree: fixture.retains_native_tree, + nativeTreeVisibility: fixture.native_tree_visibility, + metadataPolicy: fixture.metadata_policy + }; +} + +function normalizedParseResult(fixture: NormalizedParseResultFixture): NormalizedParseResult { + return { + ok: fixture.ok, + backendCapability: backendCapability(fixture.backend_capability), + rootId: fixture.root_id, + nodes: fixture.nodes.map(normalizedTreeNode), + parseErrorTolerance: parseErrorTolerance(fixture.parse_error_tolerance), + sourceFragmentsAvailable: fixture.source_fragments_available, + diagnostics: fixture.diagnostics, + metadata: fixture.metadata + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -425,6 +473,23 @@ describe('tree-haver shared fixtures', () => { expect(limited.metadata.psych?.location_support).toBe('line_column_only'); }); + it('conforms to the slice-787 native parser adapter contract fixture', () => { + const fixture = readFixture<{ + provider: NativeParserProviderFixture; + parse_result: NormalizedParseResultFixture; + }>('diagnostics', 'slice-787-native-parser-adapter-contract', 'native-parser-adapter-contract.json'); + const provider = nativeParserProvider(fixture.provider); + const result = normalizedParseResult(fixture.parse_result); + + expect(provider.id).toBe('go-dst'); + expect(provider.retainsNativeTree).toBe(true); + expect(provider.nativeTreeVisibility).toBe('provider_internal'); + expect(result.rootId).toBe(result.nodes[0]?.id); + expect(result.nodes[1]?.semanticRoles[1]).toBe('function'); + expect(result.metadata.go_dst?.native_tree_visibility).toBe('provider_internal'); + expect(result.sourceFragmentsAvailable).toBe(true); + }); + it('conforms to the slice-783 backend capability report fixture', () => { const fixture = readFixture<{ capability: BackendCapabilityFixture }>( 'diagnostics', From 15ddef07b5dd6cbda920bccc276aeae0b326ef94 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:26:51 -0600 Subject: [PATCH 022/130] Add tree-haver profile contract --- packages/tree-haver/src/contracts.ts | 14 ++++++ packages/tree-haver/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 48 +++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 28663f8..34d77cc 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -124,6 +124,20 @@ export interface NormalizedParseResult { readonly metadata: Readonly>>>; } +export interface TreeHaverProfile { + readonly profileId: string; + readonly language: string; + readonly backendRef: BackendReference; + readonly providerId: string; + readonly nodeRoles: readonly NodeRole[]; + readonly normalizedNodeFields: readonly string[]; + readonly optionalNodeFeatures: readonly string[]; + readonly unsupportedDefaults: Readonly>; + readonly capability: BackendCapability; + readonly fixtureSlices: readonly string[]; + readonly diagnostics: readonly string[]; +} + export interface ParserAdapter { readonly info: AdapterInfo; parse(request: ParserRequest): ParseResult; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 865f88f..2487780 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -42,6 +42,7 @@ export type { SourceFragment, SourcePoint, SourceSpan, + TreeHaverProfile, ZipArchiveEntry, ZipArchiveInfo, ZipFamilyReport, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 4b13aca..d35c618 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -20,6 +20,7 @@ import type { ParserRequest, PolicyReference, SourceSpan, + TreeHaverProfile, ZipUnsafeEntry } from '../src/index'; import { processWithLanguagePack } from '../src/index'; @@ -297,6 +298,20 @@ interface NormalizedParseResultFixture { readonly metadata: Readonly>>>; } +interface TreeHaverProfileFixture { + readonly profile_id: string; + readonly language: string; + readonly backend_ref: BackendReference; + readonly provider_id: string; + readonly node_roles: readonly NormalizedTreeNode['role'][]; + readonly normalized_node_fields: readonly string[]; + readonly optional_node_features: readonly string[]; + readonly unsupported_defaults: Readonly>; + readonly capability: BackendCapabilityFixture; + readonly fixture_slices: readonly string[]; + readonly diagnostics: readonly string[]; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -417,6 +432,22 @@ function normalizedParseResult(fixture: NormalizedParseResultFixture): Normalize }; } +function treeHaverProfile(fixture: TreeHaverProfileFixture): TreeHaverProfile { + return { + profileId: fixture.profile_id, + language: fixture.language, + backendRef: fixture.backend_ref, + providerId: fixture.provider_id, + nodeRoles: fixture.node_roles, + normalizedNodeFields: fixture.normalized_node_fields, + optionalNodeFeatures: fixture.optional_node_features, + unsupportedDefaults: fixture.unsupported_defaults, + capability: backendCapability(fixture.capability), + fixtureSlices: fixture.fixture_slices, + diagnostics: fixture.diagnostics + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -490,6 +521,23 @@ describe('tree-haver shared fixtures', () => { expect(result.sourceFragmentsAvailable).toBe(true); }); + it('conforms to the slice-788 tree-haver profile fixture', () => { + const fixture = readFixture<{ profile: TreeHaverProfileFixture }>( + 'diagnostics', + 'slice-788-tree-haver-profile', + 'tree-haver-profile.json' + ); + const profile = treeHaverProfile(fixture.profile); + + expect(profile.profileId).toBe('go-dst-normalized-tree-v1'); + expect(profile.backendRef.id).toBe('go-dst'); + expect(profile.nodeRoles[0]).toBe('structural'); + expect(profile.normalizedNodeFields.at(-1)).toBe('metadata'); + expect(profile.unsupportedDefaults.field_name).toBe('null'); + expect(profile.capability.parserIdentity.name).toBe('github.com/dave/dst'); + expect(profile.fixtureSlices[0]).toBe('slice-782-normalized-tree-node'); + }); + it('conforms to the slice-783 backend capability report fixture', () => { const fixture = readFixture<{ capability: BackendCapabilityFixture }>( 'diagnostics', From 1b654b32dc4b1ede1d8f3ae8ba23bb01cf5d541b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:33:29 -0600 Subject: [PATCH 023/130] Add ordered tree primitives contract --- packages/tree-haver/src/contracts.ts | 14 +++++ packages/tree-haver/src/index.ts | 2 + .../test/fixtures.integration.test.ts | 60 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 34d77cc..89c457a 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -138,6 +138,20 @@ export interface TreeHaverProfile { readonly diagnostics: readonly string[]; } +export interface OrderedSiblingEdge { + readonly parentId: string; + readonly nodeId: string; + readonly previousSiblingId?: string; + readonly nextSiblingId?: string; +} + +export interface OrderedTreePrimitives { + readonly rootId: string; + readonly childOrder: Readonly>; + readonly siblingEdges: readonly OrderedSiblingEdge[]; + readonly diagnostics: readonly string[]; +} + export interface ParserAdapter { readonly info: AdapterInfo; parse(request: ParserRequest): ParseResult; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 2487780..352b695 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -27,6 +27,8 @@ export type { NormalizedParseResult, NodeRole, NormalizedTreeNode, + OrderedSiblingEdge, + OrderedTreePrimitives, ParseErrorTolerance, ParserAdapter, ParserDiagnostics, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index d35c618..8a4fa2f 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -16,6 +16,7 @@ import type { NativeParserProvider, NormalizedParseResult, NormalizedTreeNode, + OrderedTreePrimitives, ParseErrorTolerance, ParserRequest, PolicyReference, @@ -312,6 +313,18 @@ interface TreeHaverProfileFixture { readonly diagnostics: readonly string[]; } +interface OrderedTreePrimitivesFixture { + readonly root_id: string; + readonly child_order: Readonly>; + readonly sibling_edges: readonly { + readonly parent_id: string; + readonly node_id: string; + readonly previous_sibling_id: string | null; + readonly next_sibling_id: string | null; + }[]; + readonly diagnostics: readonly string[]; +} + function readFixture(...segments: string[]): T { const fixturePath = path.resolve(process.cwd(), '..', 'fixtures', ...segments); @@ -448,6 +461,20 @@ function treeHaverProfile(fixture: TreeHaverProfileFixture): TreeHaverProfile { }; } +function orderedTreePrimitives(fixture: OrderedTreePrimitivesFixture): OrderedTreePrimitives { + return { + rootId: fixture.root_id, + childOrder: fixture.child_order, + siblingEdges: fixture.sibling_edges.map((edge) => ({ + parentId: edge.parent_id, + nodeId: edge.node_id, + previousSiblingId: edge.previous_sibling_id ?? undefined, + nextSiblingId: edge.next_sibling_id ?? undefined + })), + diagnostics: fixture.diagnostics + }; +} + describe('tree-haver shared fixtures', () => { it('conforms to the slice-06 parser request fixture', () => { const fixture = readFixture(...diagnosticsFixturePath('parser_request')); @@ -508,7 +535,11 @@ describe('tree-haver shared fixtures', () => { const fixture = readFixture<{ provider: NativeParserProviderFixture; parse_result: NormalizedParseResultFixture; - }>('diagnostics', 'slice-787-native-parser-adapter-contract', 'native-parser-adapter-contract.json'); + }>( + 'diagnostics', + 'slice-787-native-parser-adapter-contract', + 'native-parser-adapter-contract.json' + ); const provider = nativeParserProvider(fixture.provider); const result = normalizedParseResult(fixture.parse_result); @@ -538,6 +569,27 @@ describe('tree-haver shared fixtures', () => { expect(profile.fixtureSlices[0]).toBe('slice-782-normalized-tree-node'); }); + it('conforms to the slice-789 ordered tree primitives fixture', () => { + const fixture = readFixture<{ + root_id: string; + ordered_tree: OrderedTreePrimitivesFixture; + forbidden_merge_terms: readonly string[]; + }>('diagnostics', 'slice-789-ordered-tree-primitives', 'ordered-tree-primitives.json'); + const ordered = orderedTreePrimitives(fixture.ordered_tree); + + for (const diagnostic of ordered.diagnostics) { + for (const term of fixture.forbidden_merge_terms) { + expect(diagnostic.toLowerCase()).not.toContain(term.toLowerCase()); + } + } + + expect(ordered.rootId).toBe(fixture.root_id); + expect(ordered.childOrder.file?.[0]).toBe('imports'); + expect(ordered.childOrder.imports?.[1]).toBe('import-strings'); + expect(ordered.siblingEdges[2]?.previousSiblingId).toBeUndefined(); + expect(ordered.siblingEdges[2]?.nextSiblingId).toBe('import-strings'); + }); + it('conforms to the slice-783 backend capability report fixture', () => { const fixture = readFixture<{ capability: BackendCapabilityFixture }>( 'diagnostics', @@ -561,7 +613,11 @@ describe('tree-haver shared fixtures', () => { 'slice-784-source-fragment-extraction', 'source-fragment-extraction.json' ); - const fragment = extractSourceFragment(fixture.source, sourceSpan(fixture.span), fixture.strategy); + const fragment = extractSourceFragment( + fixture.source, + sourceSpan(fixture.span), + fixture.strategy + ); expect(fragment.text).toBe(fixture.fragment.text); expect(fragment.available).toBe(fixture.fragment.available); From ca87d6e4ca533b6850975e08c0de7dc285937f89 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:38:13 -0600 Subject: [PATCH 024/130] Add generic merge IR contract --- packages/ast-merge/src/contracts.ts | 40 ++++++++++++++++++- packages/ast-merge/src/index.ts | 4 ++ .../test/fixtures.integration.test.ts | 21 ++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 242f796..ff083f9 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -359,7 +359,8 @@ export function compactRulesetFeatureProfile(ruleset: CompactRuleset): CompactRu if (args.length > 1) profile.node_roles.push({ selector: args[0], role: args[1] }); break; case 'atomic': - if (args.length > 1) profile.atomic_nodes.push({ selector: args[0], atomic: args[1] === 'true' }); + if (args.length > 1) + profile.atomic_nodes.push({ selector: args[0], atomic: args[1] === 'true' }); break; case 'child_group': if (args.length > 2) { @@ -426,6 +427,43 @@ export interface MergeResult { readonly policies?: readonly PolicyReference[]; } +export interface MergeIRNodeClass { + readonly class_id: string; + readonly signature: string; + readonly node_ids: Readonly>; + readonly roles: readonly string[]; +} + +export interface MergeIROrderedNode { + readonly node_id: string; + readonly parent_id: string; + readonly child_ids: readonly string[]; + readonly previous_sibling_id: string | null; + readonly next_sibling_id: string | null; +} + +export interface MergeIRChange { + readonly change_id: string; + readonly side: string; + readonly kind: string; + readonly node_id: string; + readonly class_id: string | null; + readonly parent_id: string; + readonly previous_sibling_id: string | null; + readonly next_sibling_id: string | null; + readonly content_hash: string; +} + +export interface MergeIR { + readonly version: string; + readonly tree_id: string; + readonly source: string; + readonly node_classes: readonly MergeIRNodeClass[]; + readonly ordered_nodes: readonly MergeIROrderedNode[]; + readonly changes: readonly MergeIRChange[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 67aba6a..2631bfa 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -47,6 +47,10 @@ export type { DiscoveredSurface, DelegatedChildOperation, ReviewDiagnosticReason, + MergeIR, + MergeIRChange, + MergeIRNodeClass, + MergeIROrderedNode, MergeResult, FamilyFeatureProfile, StructuredEditStructureProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index d7f490e..0735d47 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -37,6 +37,7 @@ import type { DiagnosticSeverity, DiscoveredSurface, FamilyFeatureProfile, + MergeIR, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5562,6 +5563,26 @@ function normalizeProjectedChildReviewGroupProgress( } describe('ast-merge shared fixtures', () => { + it('conforms to the slice-790 generic merge IR fixture', () => { + const fixture = readFixture<{ + merge_ir: MergeIR; + expected: { + version: string; + node_class_count: number; + change_kinds: readonly string[]; + ordered_node_count: number; + }; + }>('diagnostics', 'slice-790-generic-merge-ir', 'generic-merge-ir.json'); + const mergeIR = fixture.merge_ir; + + expect(mergeIR.version).toBe(fixture.expected.version); + expect(mergeIR.node_classes).toHaveLength(fixture.expected.node_class_count); + expect(mergeIR.ordered_nodes).toHaveLength(fixture.expected.ordered_node_count); + expect(mergeIR.changes.map((change) => change.kind)).toEqual(fixture.expected.change_kinds); + expect(mergeIR.node_classes[0]?.node_ids.left).toBe('left-import-fmt'); + expect(mergeIR.changes[1]?.class_id).toBe('class-import-strings'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From ea83bd10245d3d58e761daf864f2c611c484e671 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:41:11 -0600 Subject: [PATCH 025/130] Add pairwise matching contract --- packages/ast-merge/src/contracts.ts | 18 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 24 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index ff083f9..e17fed7 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -464,6 +464,24 @@ export interface MergeIR { readonly diagnostics: readonly string[]; } +export interface PairwiseNodeMatch { + readonly from_node_id: string; + readonly to_node_id: string; + readonly class_id: string; + readonly strategy: string; + readonly confidence: number; + readonly diagnostics: readonly string[]; +} + +export interface PairwiseMatching { + readonly matching_id: string; + readonly from_revision: string; + readonly to_revision: string; + readonly matches: readonly PairwiseNodeMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 2631bfa..2ee1b1a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -52,6 +52,8 @@ export type { MergeIRNodeClass, MergeIROrderedNode, MergeResult, + PairwiseMatching, + PairwiseNodeMatch, FamilyFeatureProfile, StructuredEditStructureProfile, StructuredEditSelectionProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 0735d47..57b817b 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -38,6 +38,7 @@ import type { DiscoveredSurface, FamilyFeatureProfile, MergeIR, + PairwiseMatching, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5583,6 +5584,29 @@ describe('ast-merge shared fixtures', () => { expect(mergeIR.changes[1]?.class_id).toBe('class-import-strings'); }); + it('conforms to the slice-791 pairwise matchings fixture', () => { + const fixture = readFixture<{ + pairwise_matchings: readonly PairwiseMatching[]; + expected: { + matching_ids: readonly string[]; + total_match_count: number; + unmatched_insertions: readonly string[]; + unmatched_deletions: readonly string[]; + }; + }>('diagnostics', 'slice-791-pairwise-matchings', 'pairwise-matchings.json'); + const matchings = fixture.pairwise_matchings; + + expect(matchings.map((matching) => matching.matching_id)).toEqual( + fixture.expected.matching_ids + ); + expect(matchings.reduce((sum, matching) => sum + matching.matches.length, 0)).toBe( + fixture.expected.total_match_count + ); + expect(matchings[0]?.unmatched_to[0]).toBe('left-import-os'); + expect(matchings[1]?.unmatched_from[0]).toBe('base-decl-greet'); + expect(matchings[2]?.matches[1]?.diagnostics[0]).toBe('sibling position changed'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From d918034fc3177e4051b38898886287fa8de99568 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:43:42 -0600 Subject: [PATCH 026/130] Add class mapping contract --- packages/ast-merge/src/contracts.ts | 23 +++++++++++++++++++ packages/ast-merge/src/index.ts | 3 +++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index e17fed7..74b628d 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -482,6 +482,29 @@ export interface PairwiseMatching { readonly unmatched_to: readonly string[]; } +export interface ClassMappingNodeClass { + readonly class_id: string; + readonly signature: string; + readonly node_ids: Readonly>; + readonly matching_ids: readonly string[]; + readonly diagnostics: readonly string[]; +} + +export interface ClassMappingDiagnostic { + readonly severity: string; + readonly category: string; + readonly class_id: string; + readonly message: string; + readonly matching_ids: readonly string[]; +} + +export interface ClassMappingReport { + readonly mapping_id: string; + readonly source_matching_ids: readonly string[]; + readonly node_classes: readonly ClassMappingNodeClass[]; + readonly diagnostics: readonly ClassMappingDiagnostic[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 2ee1b1a..c6169d9 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -1,6 +1,9 @@ export const packageName = '@structuredmerge/ast-merge'; export type { + ClassMappingDiagnostic, + ClassMappingNodeClass, + ClassMappingReport, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseRequirements, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 57b817b..ee6f5c4 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -36,6 +36,7 @@ import type { DiagnosticCategory, DiagnosticSeverity, DiscoveredSurface, + ClassMappingReport, FamilyFeatureProfile, MergeIR, PairwiseMatching, @@ -5607,6 +5608,28 @@ describe('ast-merge shared fixtures', () => { expect(matchings[2]?.matches[1]?.diagnostics[0]).toBe('sibling position changed'); }); + it('conforms to the slice-792 class mapping fixture', () => { + const fixture = readFixture<{ + class_mapping: ClassMappingReport; + expected: { + class_count: number; + diagnostic_categories: readonly string[]; + conflicted_class_ids: readonly string[]; + }; + }>('diagnostics', 'slice-792-class-mapping', 'class-mapping.json'); + const report = fixture.class_mapping; + + expect(report.node_classes).toHaveLength(fixture.expected.class_count); + expect(report.diagnostics.map((diagnostic) => diagnostic.category)).toEqual( + fixture.expected.diagnostic_categories + ); + expect(report.diagnostics.map((diagnostic) => diagnostic.class_id)).toEqual( + fixture.expected.conflicted_class_ids + ); + expect(report.node_classes[2]?.node_ids.right).toBeUndefined(); + expect(report.diagnostics[1]?.category).toBe('delete_edit_disagreement'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From cf12482894355bd9159cb6837c722c5279fe0c16 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:52:23 -0600 Subject: [PATCH 027/130] Add PCS change-set contract --- packages/ast-merge/src/contracts.ts | 33 +++++++++++++++++++ packages/ast-merge/src/index.ts | 4 +++ .../test/fixtures.integration.test.ts | 29 ++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 74b628d..cde0d18 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -505,6 +505,39 @@ export interface ClassMappingReport { readonly diagnostics: readonly ClassMappingDiagnostic[]; } +export interface PCSConstraint { + readonly constraint_id: string; + readonly revision: string; + readonly parent_class_id: string; + readonly predecessor_class_id: string | null; + readonly successor_class_id: string | null; + readonly relation: string; +} + +export interface PCS { + readonly pcs_id: string; + readonly tree_id: string; + readonly base_revision: string; + readonly constraints: readonly PCSConstraint[]; +} + +export interface ChangeSetChange { + readonly change_id: string; + readonly kind: string; + readonly class_id: string; + readonly parent_class_id: string; + readonly predecessor_class_id: string | null; + readonly successor_class_id: string | null; + readonly content_hash: string; +} + +export interface ChangeSet { + readonly change_set_id: string; + readonly side: string; + readonly changes: readonly ChangeSetChange[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index c6169d9..806499d 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -1,6 +1,8 @@ export const packageName = '@structuredmerge/ast-merge'; export type { + ChangeSet, + ChangeSetChange, ClassMappingDiagnostic, ClassMappingNodeClass, ClassMappingReport, @@ -57,6 +59,8 @@ export type { MergeResult, PairwiseMatching, PairwiseNodeMatch, + PCS, + PCSConstraint, FamilyFeatureProfile, StructuredEditStructureProfile, StructuredEditSelectionProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index ee6f5c4..f95a695 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -36,10 +36,12 @@ import type { DiagnosticCategory, DiagnosticSeverity, DiscoveredSurface, + ChangeSet, ClassMappingReport, FamilyFeatureProfile, MergeIR, PairwiseMatching, + PCS, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5630,6 +5632,33 @@ describe('ast-merge shared fixtures', () => { expect(report.diagnostics[1]?.category).toBe('delete_edit_disagreement'); }); + it('conforms to the slice-793 PCS change-set generation fixture', () => { + const fixture = readFixture<{ + pcs: PCS; + change_sets: readonly ChangeSet[]; + expected: { + pcs_constraint_count: number; + change_set_count: number; + change_kinds: readonly string[]; + diagnostic_count: number; + }; + }>('diagnostics', 'slice-793-pcs-change-set-generation', 'pcs-change-set-generation.json'); + const changeKinds = fixture.change_sets.flatMap((changeSet) => + changeSet.changes.map((change) => change.kind) + ); + const diagnosticCount = fixture.change_sets.reduce( + (sum, changeSet) => sum + changeSet.diagnostics.length, + 0 + ); + + expect(fixture.pcs.constraints).toHaveLength(fixture.expected.pcs_constraint_count); + expect(fixture.change_sets).toHaveLength(fixture.expected.change_set_count); + expect(changeKinds).toEqual(fixture.expected.change_kinds); + expect(diagnosticCount).toBe(fixture.expected.diagnostic_count); + expect(fixture.pcs.constraints[2]?.predecessor_class_id).toBe('class-import-strings'); + expect(fixture.change_sets[1]?.changes[1]?.kind).toBe('delete'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From b2644c7568e34e88299982f81214dc7586cb187d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:54:55 -0600 Subject: [PATCH 028/130] Add raw merge union contract --- packages/ast-merge/src/contracts.ts | 19 ++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 30 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index cde0d18..78bc0ec 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -538,6 +538,25 @@ export interface ChangeSet { readonly diagnostics: readonly string[]; } +export interface RawMergeChange { + readonly change_id: string; + readonly source_change_set_id: string; + readonly side: string; + readonly kind: string; + readonly class_id: string; + readonly parent_class_id: string; + readonly predecessor_class_id: string | null; + readonly successor_class_id: string | null; + readonly content_hash: string; +} + +export interface RawMerge { + readonly raw_merge_id: string; + readonly input_change_set_ids: readonly string[]; + readonly changes: readonly RawMergeChange[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 806499d..f32ff1f 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -61,6 +61,8 @@ export type { PairwiseNodeMatch, PCS, PCSConstraint, + RawMerge, + RawMergeChange, FamilyFeatureProfile, StructuredEditStructureProfile, StructuredEditSelectionProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index f95a695..5443266 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -42,6 +42,7 @@ import type { MergeIR, PairwiseMatching, PCS, + RawMerge, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5659,6 +5660,35 @@ describe('ast-merge shared fixtures', () => { expect(fixture.change_sets[1]?.changes[1]?.kind).toBe('delete'); }); + it('conforms to the slice-794 raw merge change-set union fixture', () => { + const fixture = readFixture<{ + raw_merge: RawMerge; + expected: { + raw_change_count: number; + input_change_set_count: number; + sides: readonly string[]; + conflicting_class_candidates: readonly string[]; + }; + }>('diagnostics', 'slice-794-raw-merge-change-set-union', 'raw-merge-change-set-union.json'); + const sides = fixture.raw_merge.changes.reduce((memo, change) => { + if (!memo.includes(change.side)) memo.push(change.side); + return memo; + }, []); + const classDeclGreetCount = fixture.raw_merge.changes.filter( + (change) => change.class_id === 'class-decl-greet' + ).length; + + expect(fixture.raw_merge.changes).toHaveLength(fixture.expected.raw_change_count); + expect(fixture.raw_merge.input_change_set_ids).toHaveLength( + fixture.expected.input_change_set_count + ); + expect(sides).toEqual(fixture.expected.sides); + expect(classDeclGreetCount).toBe(2); + expect(fixture.raw_merge.diagnostics[0]).toBe( + 'raw merge intentionally preserves both sides before inconsistency detection' + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From b303806f156a967d20b72de1b33f0564f9706a60 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:56:51 -0600 Subject: [PATCH 029/130] Add inconsistency detection contract --- packages/ast-merge/src/contracts.ts | 16 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 78bc0ec..40aeccb 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -557,6 +557,22 @@ export interface RawMerge { readonly diagnostics: readonly string[]; } +export interface MergeInconsistency { + readonly inconsistency_id: string; + readonly category: string; + readonly severity: string; + readonly class_ids: readonly string[]; + readonly change_ids: readonly string[]; + readonly message: string; +} + +export interface InconsistencyReport { + readonly report_id: string; + readonly raw_merge_id: string; + readonly inconsistencies: readonly MergeInconsistency[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index f32ff1f..674a9c2 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -39,6 +39,8 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + InconsistencyReport, + MergeInconsistency, CompactRuleset, CompactRulesetAtomicNode, CompactRulesetBackendDeclaration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 5443266..7911683 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -39,6 +39,7 @@ import type { ChangeSet, ClassMappingReport, FamilyFeatureProfile, + InconsistencyReport, MergeIR, PairwiseMatching, PCS, @@ -5689,6 +5690,27 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-795 inconsistency detection fixture', () => { + const fixture = readFixture<{ + inconsistency_report: InconsistencyReport; + expected: { + inconsistency_count: number; + categories: readonly string[]; + blocking_count: number; + }; + }>('diagnostics', 'slice-795-inconsistency-detection', 'inconsistency-detection.json'); + const report = fixture.inconsistency_report; + + expect(report.inconsistencies).toHaveLength(fixture.expected.inconsistency_count); + expect(report.inconsistencies.map((item) => item.category)).toEqual( + fixture.expected.categories + ); + expect(report.inconsistencies.filter((item) => item.severity === 'error')).toHaveLength( + fixture.expected.blocking_count + ); + expect(report.inconsistencies[1]?.change_ids[1]).toBe('right-delete-greet'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From c8e5f4a15b29b207e3c5fd6f896999a0f3bfc83a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 17:59:15 -0600 Subject: [PATCH 030/130] Add merge IR comparison contract --- packages/ast-merge/src/contracts.ts | 26 +++++++++++++++++++ packages/ast-merge/src/index.ts | 3 +++ .../test/fixtures.integration.test.ts | 20 ++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 40aeccb..d01e1bb 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -573,6 +573,32 @@ export interface InconsistencyReport { readonly diagnostics: readonly string[]; } +export interface MergeIRComparisonCase { + readonly case_id: string; + readonly family: string; + readonly scenario: string; + readonly owner_path_outcome: string; + readonly merge_ir_outcome: string; + readonly merge_ir_advantage: string; + readonly diagnostics: readonly string[]; +} + +export interface MergeIRComparisonSummary { + readonly owner_path_wins: number; + readonly merge_ir_wins: number; + readonly neutral: number; + readonly defer: number; + readonly recommendation: string; +} + +export interface MergeIRComparisonReport { + readonly comparison_id: string; + readonly baseline: string; + readonly prototype: string; + readonly cases: readonly MergeIRComparisonCase[]; + readonly summary: MergeIRComparisonSummary; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 674a9c2..2f14eba 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -41,6 +41,9 @@ export type { DiagnosticSeverity, InconsistencyReport, MergeInconsistency, + MergeIRComparisonCase, + MergeIRComparisonReport, + MergeIRComparisonSummary, CompactRuleset, CompactRulesetAtomicNode, CompactRulesetBackendDeclaration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 7911683..06fbf9a 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -41,6 +41,7 @@ import type { FamilyFeatureProfile, InconsistencyReport, MergeIR, + MergeIRComparisonReport, PairwiseMatching, PCS, RawMerge, @@ -5711,6 +5712,25 @@ describe('ast-merge shared fixtures', () => { expect(report.inconsistencies[1]?.change_ids[1]).toBe('right-delete-greet'); }); + it('conforms to the slice-796 merge IR comparison fixture', () => { + const fixture = readFixture<{ + comparison: MergeIRComparisonReport; + expected: { + case_count: number; + families: readonly string[]; + merge_ir_wins: number; + recommendation: string; + }; + }>('diagnostics', 'slice-796-merge-ir-comparison', 'merge-ir-comparison.json'); + const report = fixture.comparison; + + expect(report.cases).toHaveLength(fixture.expected.case_count); + expect(report.cases.map((testCase) => testCase.family)).toEqual(fixture.expected.families); + expect(report.summary.merge_ir_wins).toBe(fixture.expected.merge_ir_wins); + expect(report.summary.recommendation).toBe(fixture.expected.recommendation); + expect(report.cases[4]?.merge_ir_advantage).toBe('defer'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 0ca1cbc64fe9c3d9132db562b6fe4383be0aadd2 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:03:04 -0600 Subject: [PATCH 031/130] Add structural matching baseline contract --- packages/ast-merge/src/contracts.ts | 19 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 26 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index d01e1bb..17c263b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -599,6 +599,25 @@ export interface MergeIRComparisonReport { readonly summary: MergeIRComparisonSummary; } +export interface StructuralPathMatch { + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly confidence: number; +} + +export interface StructuralMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly from_revision: string; + readonly to_revision: string; + readonly matches: readonly StructuralPathMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 2f14eba..b9adc45 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -44,6 +44,8 @@ export type { MergeIRComparisonCase, MergeIRComparisonReport, MergeIRComparisonSummary, + StructuralMatchingReport, + StructuralPathMatch, CompactRuleset, CompactRulesetAtomicNode, CompactRulesetBackendDeclaration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 06fbf9a..da412b2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -45,6 +45,7 @@ import type { PairwiseMatching, PCS, RawMerge, + StructuralMatchingReport, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5731,6 +5732,31 @@ describe('ast-merge shared fixtures', () => { expect(report.cases[4]?.merge_ir_advantage).toBe('defer'); }); + it('conforms to the slice-797 structural matching baseline fixture', () => { + const fixture = readFixture<{ + matching: StructuralMatchingReport; + expected: { + strategy: string; + match_count: number; + unmatched_from_count: number; + unmatched_to_count: number; + move_detection: boolean; + }; + }>( + 'diagnostics', + 'slice-797-structural-matching-baseline', + 'structural-matching-baseline.json' + ); + const report = fixture.matching; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.unmatched_from).toHaveLength(fixture.expected.unmatched_from_count); + expect(report.unmatched_to).toHaveLength(fixture.expected.unmatched_to_count); + expect(fixture.expected.move_detection).toBe(false); + expect(report.matches[1]?.from_path).toBe('/declarations/Greet'); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From b1361252d5760d18ac6156008ecc40928f2f83cc Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:06:42 -0600 Subject: [PATCH 032/130] Add signature matching commutative parent contract --- packages/ast-merge/src/contracts.ts | 33 +++++++++++++++++ packages/ast-merge/src/index.ts | 3 ++ .../test/fixtures.integration.test.ts | 36 +++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 17c263b..f8e941c 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -618,6 +618,39 @@ export interface StructuralMatchingReport { readonly diagnostics: readonly string[]; } +export interface SignatureMatchingParent { + readonly kind: string; + readonly role: string; + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly child_order: string; +} + +export interface SignatureNodeMatch { + readonly signature: string; + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly confidence: number; + readonly diagnostics: readonly string[]; +} + +export interface SignatureMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly parent_policy: string; + readonly signature_components: readonly string[]; + readonly from_revision: string; + readonly to_revision: string; + readonly matches: readonly SignatureNodeMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index b9adc45..6645c5f 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -44,6 +44,9 @@ export type { MergeIRComparisonCase, MergeIRComparisonReport, MergeIRComparisonSummary, + SignatureMatchingParent, + SignatureMatchingReport, + SignatureNodeMatch, StructuralMatchingReport, StructuralPathMatch, CompactRuleset, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index da412b2..1dff872 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -45,6 +45,8 @@ import type { PairwiseMatching, PCS, RawMerge, + SignatureMatchingParent, + SignatureMatchingReport, StructuralMatchingReport, StructuredEditStructureProfile, StructuredEditSelectionProfile, @@ -5757,6 +5759,40 @@ describe('ast-merge shared fixtures', () => { expect(report.matches[1]?.from_path).toBe('/declarations/Greet'); }); + it('conforms to the slice-798 signature matching commutative parent fixture', () => { + const fixture = readFixture<{ + parent: SignatureMatchingParent; + matching: SignatureMatchingReport; + expected: { + strategy: string; + parent_policy: string; + signature_components: readonly string[]; + match_count: number; + unmatched_from_count: number; + unmatched_to_count: number; + order_sensitive: boolean; + first_match_signature: string; + first_match_to_path: string; + }; + }>( + 'diagnostics', + 'slice-798-signature-matching-commutative-parent', + 'signature-matching-commutative-parent.json' + ); + const report = fixture.matching; + + expect(fixture.parent.child_order).toBe(fixture.expected.parent_policy); + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.parent_policy).toBe(fixture.expected.parent_policy); + expect(report.signature_components).toEqual(fixture.expected.signature_components); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.unmatched_from).toHaveLength(fixture.expected.unmatched_from_count); + expect(report.unmatched_to).toHaveLength(fixture.expected.unmatched_to_count); + expect(fixture.expected.order_sensitive).toBe(false); + expect(report.matches[0]?.signature).toBe(fixture.expected.first_match_signature); + expect(report.matches[0]?.to_path).toBe(fixture.expected.first_match_to_path); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From c72825734a23c9b545df3a0072f18f2489d2bb77 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:09:40 -0600 Subject: [PATCH 033/130] Add source text normalized matching contract --- packages/ast-merge/src/contracts.ts | 25 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 33 +++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index f8e941c..562ef77 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -651,6 +651,31 @@ export interface SignatureMatchingReport { readonly diagnostics: readonly string[]; } +export interface SourceTextNormalizedMatch { + readonly normalized_text: string; + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly from_source_text: string; + readonly to_source_text: string; + readonly confidence: number; + readonly diagnostics: readonly string[]; +} + +export interface SourceTextNormalizedMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly from_revision: string; + readonly to_revision: string; + readonly normalization: readonly string[]; + readonly leaf_kinds: readonly string[]; + readonly matches: readonly SourceTextNormalizedMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 6645c5f..080f833 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -47,6 +47,8 @@ export type { SignatureMatchingParent, SignatureMatchingReport, SignatureNodeMatch, + SourceTextNormalizedMatch, + SourceTextNormalizedMatchingReport, StructuralMatchingReport, StructuralPathMatch, CompactRuleset, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 1dff872..6d327b2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -47,6 +47,7 @@ import type { RawMerge, SignatureMatchingParent, SignatureMatchingReport, + SourceTextNormalizedMatchingReport, StructuralMatchingReport, StructuredEditStructureProfile, StructuredEditSelectionProfile, @@ -5793,6 +5794,38 @@ describe('ast-merge shared fixtures', () => { expect(report.matches[0]?.to_path).toBe(fixture.expected.first_match_to_path); }); + it('conforms to the slice-799 source-text normalized leaf matching fixture', () => { + const fixture = readFixture<{ + matching: SourceTextNormalizedMatchingReport; + expected: { + strategy: string; + normalization: readonly string[]; + leaf_kinds: readonly string[]; + match_count: number; + unmatched_from_count: number; + unmatched_to_count: number; + first_match_normalized_text: string; + minimum_confidence: number; + }; + }>( + 'diagnostics', + 'slice-799-source-text-normalized-leaf-matching', + 'source-text-normalized-leaf-matching.json' + ); + const report = fixture.matching; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.normalization).toEqual(fixture.expected.normalization); + expect(report.leaf_kinds).toEqual(fixture.expected.leaf_kinds); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.unmatched_from).toHaveLength(fixture.expected.unmatched_from_count); + expect(report.unmatched_to).toHaveLength(fixture.expected.unmatched_to_count); + expect(report.matches[0]?.normalized_text).toBe(fixture.expected.first_match_normalized_text); + expect(report.matches[0]?.confidence).toBeGreaterThanOrEqual( + fixture.expected.minimum_confidence + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From a63cc6f30fb7122ad9cac9aac72186805dc1708b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:12:57 -0600 Subject: [PATCH 034/130] Add move detection opt-in contract --- packages/ast-merge/src/contracts.ts | 34 +++++++++++++++++++ packages/ast-merge/src/index.ts | 3 ++ .../test/fixtures.integration.test.ts | 34 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 562ef77..4f9788a 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -676,6 +676,40 @@ export interface SourceTextNormalizedMatchingReport { readonly diagnostics: readonly string[]; } +export interface MoveDetectionCapability { + readonly name: string; + readonly enabled: boolean; + readonly default_enabled: boolean; + readonly requires_stable_node_identity: boolean; +} + +export interface MoveDetectionMatch { + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly signature: string; + readonly moved: boolean; + readonly from_parent_path: string; + readonly to_parent_path: string; + readonly from_index: number; + readonly to_index: number; + readonly confidence: number; + readonly diagnostics: readonly string[]; +} + +export interface MoveDetectionMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly from_revision: string; + readonly to_revision: string; + readonly capability: MoveDetectionCapability; + readonly matches: readonly MoveDetectionMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 080f833..be72b0b 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -44,6 +44,9 @@ export type { MergeIRComparisonCase, MergeIRComparisonReport, MergeIRComparisonSummary, + MoveDetectionCapability, + MoveDetectionMatch, + MoveDetectionMatchingReport, SignatureMatchingParent, SignatureMatchingReport, SignatureNodeMatch, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 6d327b2..7d02024 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -42,6 +42,7 @@ import type { InconsistencyReport, MergeIR, MergeIRComparisonReport, + MoveDetectionMatchingReport, PairwiseMatching, PCS, RawMerge, @@ -5826,6 +5827,39 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-800 move detection opt-in fixture', () => { + const fixture = readFixture<{ + matching: MoveDetectionMatchingReport; + expected: { + strategy: string; + capability: string; + enabled: boolean; + default_enabled: boolean; + requires_stable_node_identity: boolean; + match_count: number; + move_count: number; + first_moved_signature: string; + first_moved_from_index: number; + first_moved_to_index: number; + }; + }>('diagnostics', 'slice-800-move-detection-opt-in', 'move-detection-opt-in.json'); + const report = fixture.matching; + const moveCount = report.matches.filter((match) => match.moved).length; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.capability.name).toBe(fixture.expected.capability); + expect(report.capability.enabled).toBe(fixture.expected.enabled); + expect(report.capability.default_enabled).toBe(fixture.expected.default_enabled); + expect(report.capability.requires_stable_node_identity).toBe( + fixture.expected.requires_stable_node_identity + ); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(moveCount).toBe(fixture.expected.move_count); + expect(report.matches[0]?.signature).toBe(fixture.expected.first_moved_signature); + expect(report.matches[0]?.from_index).toBe(fixture.expected.first_moved_from_index); + expect(report.matches[0]?.to_index).toBe(fixture.expected.first_moved_to_index); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 29976a8a6e8aa34b0881c3a7fe2c739664d1baab Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:15:47 -0600 Subject: [PATCH 035/130] Add rename-aware matching gated contract --- packages/ast-merge/src/contracts.ts | 34 +++++++++++++++++++ packages/ast-merge/src/index.ts | 3 ++ .../test/fixtures.integration.test.ts | 33 ++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 4f9788a..db27321 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -710,6 +710,40 @@ export interface MoveDetectionMatchingReport { readonly diagnostics: readonly string[]; } +export interface RenameAwareCapability { + readonly name: string; + readonly status: string; + readonly enabled: boolean; + readonly requires_explicit_profile: boolean; + readonly requires_diagnostics: boolean; +} + +export interface RenameAwareCandidate { + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly from_signature: string; + readonly to_signature: string; + readonly stable_body_hash: string; + readonly rename_distance: number; + readonly selected: boolean; + readonly diagnostics: readonly string[]; +} + +export interface RenameAwareMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly from_revision: string; + readonly to_revision: string; + readonly capability: RenameAwareCapability; + readonly candidates: readonly RenameAwareCandidate[]; + readonly matches: readonly SignatureNodeMatch[]; + readonly unmatched_from: readonly string[]; + readonly unmatched_to: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index be72b0b..48cee93 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -47,6 +47,9 @@ export type { MoveDetectionCapability, MoveDetectionMatch, MoveDetectionMatchingReport, + RenameAwareCapability, + RenameAwareCandidate, + RenameAwareMatchingReport, SignatureMatchingParent, SignatureMatchingReport, SignatureNodeMatch, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 7d02024..42b7e04 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -46,6 +46,7 @@ import type { PairwiseMatching, PCS, RawMerge, + RenameAwareMatchingReport, SignatureMatchingParent, SignatureMatchingReport, SourceTextNormalizedMatchingReport, @@ -5860,6 +5861,38 @@ describe('ast-merge shared fixtures', () => { expect(report.matches[0]?.to_index).toBe(fixture.expected.first_moved_to_index); }); + it('conforms to the slice-801 rename-aware matching gated fixture', () => { + const fixture = readFixture<{ + matching: RenameAwareMatchingReport; + expected: { + strategy: string; + capability: string; + status: string; + enabled: boolean; + requires_explicit_profile: boolean; + requires_diagnostics: boolean; + candidate_count: number; + match_count: number; + first_candidate_selected: boolean; + first_candidate_body_hash: string; + }; + }>('diagnostics', 'slice-801-rename-aware-matching-gated', 'rename-aware-matching-gated.json'); + const report = fixture.matching; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.capability.name).toBe(fixture.expected.capability); + expect(report.capability.status).toBe(fixture.expected.status); + expect(report.capability.enabled).toBe(fixture.expected.enabled); + expect(report.capability.requires_explicit_profile).toBe( + fixture.expected.requires_explicit_profile + ); + expect(report.capability.requires_diagnostics).toBe(fixture.expected.requires_diagnostics); + expect(report.candidates).toHaveLength(fixture.expected.candidate_count); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.candidates[0]?.selected).toBe(fixture.expected.first_candidate_selected); + expect(report.candidates[0]?.stable_body_hash).toBe(fixture.expected.first_candidate_body_hash); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 6ab6123651d414422a7c50f70132f219ad983a36 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:19:00 -0600 Subject: [PATCH 036/130] Add ambiguity diagnostics contract --- packages/ast-merge/src/contracts.ts | 20 +++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 29 +++++++++++++++++++ 3 files changed, 51 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index db27321..33c8715 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -744,6 +744,26 @@ export interface RenameAwareMatchingReport { readonly diagnostics: readonly string[]; } +export interface MatchingAmbiguity { + readonly signature: string; + readonly scope_path: string; + readonly from_candidates: readonly string[]; + readonly to_candidates: readonly string[]; + readonly selected: boolean; + readonly reason: string; + readonly diagnostics: readonly string[]; +} + +export interface AmbiguityMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly scope_path: string; + readonly ambiguous: boolean; + readonly matches: readonly SignatureNodeMatch[]; + readonly ambiguities: readonly MatchingAmbiguity[]; + readonly diagnostics: readonly Diagnostic[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 48cee93..24b3e08 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -1,6 +1,7 @@ export const packageName = '@structuredmerge/ast-merge'; export type { + AmbiguityMatchingReport, ChangeSet, ChangeSetChange, ClassMappingDiagnostic, @@ -44,6 +45,7 @@ export type { MergeIRComparisonCase, MergeIRComparisonReport, MergeIRComparisonSummary, + MatchingAmbiguity, MoveDetectionCapability, MoveDetectionMatch, MoveDetectionMatchingReport, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 42b7e04..bb061b2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -7,6 +7,7 @@ import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; import type { + AmbiguityMatchingReport, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseExecution, @@ -5893,6 +5894,34 @@ describe('ast-merge shared fixtures', () => { expect(report.candidates[0]?.stable_body_hash).toBe(fixture.expected.first_candidate_body_hash); }); + it('conforms to the slice-802 ambiguity diagnostics fixture', () => { + const fixture = readFixture<{ + matching: AmbiguityMatchingReport; + expected: { + strategy: string; + scope_path: string; + ambiguous: boolean; + match_count: number; + ambiguity_count: number; + diagnostic_category: string; + first_ambiguity_signature: string; + first_ambiguity_reason: string; + first_ambiguity_selected: boolean; + }; + }>('diagnostics', 'slice-802-ambiguity-diagnostics', 'ambiguity-diagnostics.json'); + const report = fixture.matching; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.scope_path).toBe(fixture.expected.scope_path); + expect(report.ambiguous).toBe(fixture.expected.ambiguous); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.ambiguities).toHaveLength(fixture.expected.ambiguity_count); + expect(report.diagnostics[0]?.category).toBe(fixture.expected.diagnostic_category); + expect(report.ambiguities[0]?.signature).toBe(fixture.expected.first_ambiguity_signature); + expect(report.ambiguities[0]?.reason).toBe(fixture.expected.first_ambiguity_reason); + expect(report.ambiguities[0]?.selected).toBe(fixture.expected.first_ambiguity_selected); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From d28d7185559af8e81b057c3ba752934eddb7cc48 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:22:16 -0600 Subject: [PATCH 037/130] Add duplicate signature tie-break contract --- packages/ast-merge/src/contracts.ts | 28 +++++++++++++++ packages/ast-merge/src/index.ts | 3 ++ .../test/fixtures.integration.test.ts | 35 +++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 33c8715..920d9d8 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -764,6 +764,34 @@ export interface AmbiguityMatchingReport { readonly diagnostics: readonly Diagnostic[]; } +export interface RejectedTieBreakCandidate { + readonly from_path: string; + readonly from_node_id: string; + readonly confidence: number; + readonly rejected_by: string; +} + +export interface TieBreakMatch { + readonly signature: string; + readonly from_path: string; + readonly to_path: string; + readonly from_node_id: string; + readonly to_node_id: string; + readonly confidence: number; + readonly selected_by: string; + readonly rejected_candidates: readonly RejectedTieBreakCandidate[]; + readonly diagnostics: readonly string[]; +} + +export interface TieBreakMatchingReport { + readonly matching_id: string; + readonly strategy: string; + readonly scope_path: string; + readonly tie_break_rules: readonly string[]; + readonly matches: readonly TieBreakMatch[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 24b3e08..080f2e1 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -52,6 +52,7 @@ export type { RenameAwareCapability, RenameAwareCandidate, RenameAwareMatchingReport, + RejectedTieBreakCandidate, SignatureMatchingParent, SignatureMatchingReport, SignatureNodeMatch, @@ -59,6 +60,8 @@ export type { SourceTextNormalizedMatchingReport, StructuralMatchingReport, StructuralPathMatch, + TieBreakMatch, + TieBreakMatchingReport, CompactRuleset, CompactRulesetAtomicNode, CompactRulesetBackendDeclaration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index bb061b2..eb35742 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -52,6 +52,7 @@ import type { SignatureMatchingReport, SourceTextNormalizedMatchingReport, StructuralMatchingReport, + TieBreakMatchingReport, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -5922,6 +5923,40 @@ describe('ast-merge shared fixtures', () => { expect(report.ambiguities[0]?.selected).toBe(fixture.expected.first_ambiguity_selected); }); + it('conforms to the slice-803 duplicate signature tie-break fixture', () => { + const fixture = readFixture<{ + matching: TieBreakMatchingReport; + expected: { + strategy: string; + scope_path: string; + tie_break_rules: readonly string[]; + match_count: number; + first_match_signature: string; + first_match_selected_by: string; + rejected_candidate_count: number; + first_rejected_by: string; + }; + }>( + 'diagnostics', + 'slice-803-duplicate-signature-tie-break', + 'duplicate-signature-tie-break.json' + ); + const report = fixture.matching; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.scope_path).toBe(fixture.expected.scope_path); + expect(report.tie_break_rules).toEqual(fixture.expected.tie_break_rules); + expect(report.matches).toHaveLength(fixture.expected.match_count); + expect(report.matches[0]?.signature).toBe(fixture.expected.first_match_signature); + expect(report.matches[0]?.selected_by).toBe(fixture.expected.first_match_selected_by); + expect(report.matches[0]?.rejected_candidates).toHaveLength( + fixture.expected.rejected_candidate_count + ); + expect(report.matches[0]?.rejected_candidates[0]?.rejected_by).toBe( + fixture.expected.first_rejected_by + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 3368938e914b77016d03a4fdf33a78be2c0a02c7 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:25:13 -0600 Subject: [PATCH 038/130] Add matching debug artifacts contract --- packages/ast-merge/src/contracts.ts | 37 +++++++++++++++++++ packages/ast-merge/src/index.ts | 5 +++ .../test/fixtures.integration.test.ts | 23 ++++++++++++ 3 files changed, 65 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 920d9d8..52ec598 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -792,6 +792,43 @@ export interface TieBreakMatchingReport { readonly diagnostics: readonly string[]; } +export interface MatchingDebugOwnerSet { + readonly owner_id: string; + readonly scope_path: string; + readonly node_paths: readonly string[]; +} + +export interface MatchingDebugCandidate { + readonly candidate_id: string; + readonly signature: string; + readonly from_path: string; + readonly to_path: string; + readonly confidence: number; + readonly reason: string; +} + +export interface MatchingDebugSelectedMatch { + readonly candidate_id: string; + readonly selected_by: string; +} + +export interface MatchingDebugRejectedMatch { + readonly candidate_id: string; + readonly rejected_by: string; + readonly reason: string; +} + +export interface MatchingDebugArtifacts { + readonly artifact_id: string; + readonly matching_id: string; + readonly enabled: boolean; + readonly owner_sets: readonly MatchingDebugOwnerSet[]; + readonly candidates: readonly MatchingDebugCandidate[]; + readonly selected_matches: readonly MatchingDebugSelectedMatch[]; + readonly rejected_matches: readonly MatchingDebugRejectedMatch[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 080f2e1..9da7e1d 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -45,6 +45,11 @@ export type { MergeIRComparisonCase, MergeIRComparisonReport, MergeIRComparisonSummary, + MatchingDebugArtifacts, + MatchingDebugCandidate, + MatchingDebugOwnerSet, + MatchingDebugRejectedMatch, + MatchingDebugSelectedMatch, MatchingAmbiguity, MoveDetectionCapability, MoveDetectionMatch, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index eb35742..b6b0d0f 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -41,6 +41,7 @@ import type { ClassMappingReport, FamilyFeatureProfile, InconsistencyReport, + MatchingDebugArtifacts, MergeIR, MergeIRComparisonReport, MoveDetectionMatchingReport, @@ -5957,6 +5958,28 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-804 matching debug artifacts fixture', () => { + const fixture = readFixture<{ + debug_artifacts: MatchingDebugArtifacts; + expected: { + enabled: boolean; + owner_set_count: number; + candidate_count: number; + selected_count: number; + rejected_count: number; + first_rejection_reason: string; + }; + }>('diagnostics', 'slice-804-matching-debug-artifacts', 'matching-debug-artifacts.json'); + const artifacts = fixture.debug_artifacts; + + expect(artifacts.enabled).toBe(fixture.expected.enabled); + expect(artifacts.owner_sets).toHaveLength(fixture.expected.owner_set_count); + expect(artifacts.candidates).toHaveLength(fixture.expected.candidate_count); + expect(artifacts.selected_matches).toHaveLength(fixture.expected.selected_count); + expect(artifacts.rejected_matches).toHaveLength(fixture.expected.rejected_count); + expect(artifacts.rejected_matches[0]?.reason).toBe(fixture.expected.first_rejection_reason); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 807f9344f80898f1b8dc9541d238e83910bb9baa Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:41:56 -0600 Subject: [PATCH 039/130] Add fallback scopes contract --- packages/ast-merge/src/contracts.ts | 17 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 52ec598..97370b5 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -829,6 +829,23 @@ export interface MatchingDebugArtifacts { readonly diagnostics: readonly string[]; } +export interface FallbackScopeDefinition { + readonly scope: string; + readonly path: string; + readonly owner_path: string; + readonly covers_children: boolean; + readonly requires_source_span: boolean; + readonly description: string; +} + +export interface FallbackScopeReport { + readonly report_id: string; + readonly version: string; + readonly scopes: readonly FallbackScopeDefinition[]; + readonly default_order: readonly string[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 9da7e1d..52d57cb 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -40,6 +40,8 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + FallbackScopeDefinition, + FallbackScopeReport, InconsistencyReport, MergeInconsistency, MergeIRComparisonCase, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index b6b0d0f..1f85870 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -37,6 +37,7 @@ import type { DiagnosticCategory, DiagnosticSeverity, DiscoveredSurface, + FallbackScopeReport, ChangeSet, ClassMappingReport, FamilyFeatureProfile, @@ -5980,6 +5981,28 @@ describe('ast-merge shared fixtures', () => { expect(artifacts.rejected_matches[0]?.reason).toBe(fixture.expected.first_rejection_reason); }); + it('conforms to the slice-805 fallback scopes fixture', () => { + const fixture = readFixture<{ + fallback: FallbackScopeReport; + expected: { + scope_count: number; + default_order: readonly string[]; + first_scope: string; + last_scope: string; + whole_file_requires_source_span: boolean; + }; + }>('diagnostics', 'slice-805-fallback-scopes', 'fallback-scopes.json'); + const report = fixture.fallback; + + expect(report.scopes).toHaveLength(fixture.expected.scope_count); + expect(report.default_order).toEqual(fixture.expected.default_order); + expect(report.scopes[0]?.scope).toBe(fixture.expected.first_scope); + expect(report.scopes.at(-1)?.scope).toBe(fixture.expected.last_scope); + expect(report.scopes.at(-1)?.requires_source_span).toBe( + fixture.expected.whole_file_requires_source_span + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From b3193f8e3cc7095ab38ad48ce684b79c4f341e50 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:44:35 -0600 Subject: [PATCH 040/130] Add conflict categories contract --- packages/ast-merge/src/contracts.ts | 16 ++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 22 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 97370b5..b644c5b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -846,6 +846,22 @@ export interface FallbackScopeReport { readonly diagnostics: readonly string[]; } +export interface MergeConflict { + readonly conflict_id: string; + readonly category: string; + readonly path: string; + readonly fallback_scope: string; + readonly message: string; +} + +export interface ConflictCategoryReport { + readonly report_id: string; + readonly version: string; + readonly categories: readonly string[]; + readonly conflicts: readonly MergeConflict[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 52d57cb..c7b797e 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -7,6 +7,7 @@ export type { ClassMappingDiagnostic, ClassMappingNodeClass, ClassMappingReport, + ConflictCategoryReport, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseRequirements, @@ -43,6 +44,7 @@ export type { FallbackScopeDefinition, FallbackScopeReport, InconsistencyReport, + MergeConflict, MergeInconsistency, MergeIRComparisonCase, MergeIRComparisonReport, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 1f85870..8b42e4d 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -40,6 +40,7 @@ import type { FallbackScopeReport, ChangeSet, ClassMappingReport, + ConflictCategoryReport, FamilyFeatureProfile, InconsistencyReport, MatchingDebugArtifacts, @@ -6003,6 +6004,27 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-806 conflict categories fixture', () => { + const fixture = readFixture<{ + conflicts: ConflictCategoryReport; + expected: { + category_count: number; + conflict_count: number; + first_category: string; + last_category: string; + parse_limited_fallback_scope: string; + }; + }>('diagnostics', 'slice-806-conflict-categories', 'conflict-categories.json'); + const report = fixture.conflicts; + const parseLimited = report.conflicts.find((conflict) => conflict.category === 'parse_limited'); + + expect(report.categories).toHaveLength(fixture.expected.category_count); + expect(report.conflicts).toHaveLength(fixture.expected.conflict_count); + expect(report.categories[0]).toBe(fixture.expected.first_category); + expect(report.categories.at(-1)).toBe(fixture.expected.last_category); + expect(parseLimited?.fallback_scope).toBe(fixture.expected.parse_limited_fallback_scope); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 6c7bf2479b0c0e284c8b71f7e21d3b2412a7283d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:47:26 -0600 Subject: [PATCH 041/130] Add local line fallback contract --- packages/ast-merge/src/contracts.ts | 19 ++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 29 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index b644c5b..1f82550 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -862,6 +862,25 @@ export interface ConflictCategoryReport { readonly diagnostics: readonly string[]; } +export interface LineSpan { + readonly start_line: number; + readonly end_line: number; +} + +export interface LocalLineFallbackReport { + readonly fallback_id: string; + readonly strategy: string; + readonly scope: string; + readonly path: string; + readonly owner_path: string; + readonly base_span: LineSpan; + readonly left_span: LineSpan; + readonly right_span: LineSpan; + readonly result: string; + readonly conflict_category: string; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index c7b797e..2da41af 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -44,6 +44,8 @@ export type { FallbackScopeDefinition, FallbackScopeReport, InconsistencyReport, + LineSpan, + LocalLineFallbackReport, MergeConflict, MergeInconsistency, MergeIRComparisonCase, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 8b42e4d..570c3da 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -43,6 +43,7 @@ import type { ConflictCategoryReport, FamilyFeatureProfile, InconsistencyReport, + LocalLineFallbackReport, MatchingDebugArtifacts, MergeIR, MergeIRComparisonReport, @@ -6025,6 +6026,34 @@ describe('ast-merge shared fixtures', () => { expect(parseLimited?.fallback_scope).toBe(fixture.expected.parse_limited_fallback_scope); }); + it('conforms to the slice-807 local line-based fallback fixture', () => { + const fixture = readFixture<{ + fallback: LocalLineFallbackReport; + expected: { + strategy: string; + scope: string; + path: string; + result: string; + conflict_category: string; + left_line_count: number; + right_line_count: number; + }; + }>('diagnostics', 'slice-807-local-line-based-fallback', 'local-line-based-fallback.json'); + const report = fixture.fallback; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.scope).toBe(fixture.expected.scope); + expect(report.path).toBe(fixture.expected.path); + expect(report.result).toBe(fixture.expected.result); + expect(report.conflict_category).toBe(fixture.expected.conflict_category); + expect(report.left_span.end_line - report.left_span.start_line + 1).toBe( + fixture.expected.left_line_count + ); + expect(report.right_span.end_line - report.right_span.start_line + 1).toBe( + fixture.expected.right_line_count + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From e972e2d6eb5bceeb1a417463c7021cdd563500a1 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:49:47 -0600 Subject: [PATCH 042/130] Add conflict marker rendering contract --- packages/ast-merge/src/contracts.ts | 13 ++++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 25 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 1f82550..9a74ad1 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -881,6 +881,19 @@ export interface LocalLineFallbackReport { readonly diagnostics: readonly string[]; } +export interface ConflictMarkerRenderingReport { + readonly render_id: string; + readonly strategy: string; + readonly marker_size: number; + readonly path_label: string; + readonly left_label: string; + readonly base_label: string; + readonly right_label: string; + readonly include_base: boolean; + readonly output: string; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 2da41af..1b04fdf 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -8,6 +8,7 @@ export type { ClassMappingNodeClass, ClassMappingReport, ConflictCategoryReport, + ConflictMarkerRenderingReport, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseRequirements, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 570c3da..de3f108 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -41,6 +41,7 @@ import type { ChangeSet, ClassMappingReport, ConflictCategoryReport, + ConflictMarkerRenderingReport, FamilyFeatureProfile, InconsistencyReport, LocalLineFallbackReport, @@ -6054,6 +6055,30 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-808 conflict marker rendering fixture', () => { + const fixture = readFixture<{ + rendering: ConflictMarkerRenderingReport; + expected: { + strategy: string; + marker_size: number; + path_label: string; + include_base: boolean; + starts_with: string; + contains_base_marker: string; + ends_with: string; + }; + }>('diagnostics', 'slice-808-conflict-marker-rendering', 'conflict-marker-rendering.json'); + const report = fixture.rendering; + + expect(report.strategy).toBe(fixture.expected.strategy); + expect(report.marker_size).toBe(fixture.expected.marker_size); + expect(report.path_label).toBe(fixture.expected.path_label); + expect(report.include_base).toBe(fixture.expected.include_base); + expect(report.output.startsWith(fixture.expected.starts_with)).toBe(true); + expect(report.output.includes(fixture.expected.contains_base_marker)).toBe(true); + expect(report.output.endsWith(fixture.expected.ends_with)).toBe(true); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From a43f36f85a199530430a9f13c1cde96f8f3d60bf Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 18:52:26 -0600 Subject: [PATCH 043/130] Add typed conflict handler contract --- packages/ast-merge/src/contracts.ts | 16 +++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 9a74ad1..7ec9b78 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -894,6 +894,22 @@ export interface ConflictMarkerRenderingReport { readonly diagnostics: readonly string[]; } +export interface ConflictHandlerRegistration { + readonly handler_id: string; + readonly conflict_category: string; + readonly fallback_scope: string; + readonly node_roles: readonly string[]; + readonly capability: string; + readonly enabled: boolean; +} + +export interface ConflictHandlerRegistryReport { + readonly registry_id: string; + readonly version: string; + readonly handlers: readonly ConflictHandlerRegistration[]; + readonly diagnostics: readonly string[]; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 1b04fdf..bd9c99d 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -8,6 +8,8 @@ export type { ClassMappingNodeClass, ClassMappingReport, ConflictCategoryReport, + ConflictHandlerRegistration, + ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, ConformanceCaseRef, ConformanceCaseRun, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index de3f108..fb32744 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -41,6 +41,7 @@ import type { ChangeSet, ClassMappingReport, ConflictCategoryReport, + ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, InconsistencyReport, @@ -6079,6 +6080,29 @@ describe('ast-merge shared fixtures', () => { expect(report.output.endsWith(fixture.expected.ends_with)).toBe(true); }); + it('conforms to the slice-809 typed conflict handler extension points fixture', () => { + const fixture = readFixture<{ + handlers: ConflictHandlerRegistryReport; + expected: { + handler_count: number; + enabled_count: number; + first_handler_category: string; + second_handler_scope: string; + }; + }>( + 'diagnostics', + 'slice-809-typed-conflict-handler-extension-points', + 'typed-conflict-handler-extension-points.json' + ); + const report = fixture.handlers; + const enabledCount = report.handlers.filter((handler) => handler.enabled).length; + + expect(report.handlers).toHaveLength(fixture.expected.handler_count); + expect(enabledCount).toBe(fixture.expected.enabled_count); + expect(report.handlers[0]?.conflict_category).toBe(fixture.expected.first_handler_category); + expect(report.handlers[1]?.fallback_scope).toBe(fixture.expected.second_handler_scope); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 988a640eea0696844619d228a45f8950bbb34239 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:14:38 -0600 Subject: [PATCH 044/130] Add generic conflict handler execution --- packages/ast-merge/src/contracts.ts | 124 ++++++++++++++++++ packages/ast-merge/src/index.ts | 6 + .../test/fixtures.integration.test.ts | 38 ++++++ 3 files changed, 168 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 7ec9b78..69f641b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -910,6 +910,130 @@ export interface ConflictHandlerRegistryReport { readonly diagnostics: readonly string[]; } +export interface HandlerChildNode { + readonly node_id: string; + readonly signature: string; + readonly source: string; +} + +export interface HandlerKeyedMember { + readonly key: string; + readonly value: string; +} + +export interface GenericConflictHandlerResult { + readonly resolved: boolean; + readonly merged_children?: readonly HandlerChildNode[]; + readonly merged_members?: readonly HandlerKeyedMember[]; + readonly diagnostics: readonly string[]; +} + +export interface GenericConflictHandlerCase { + readonly case_id: string; + readonly handler_id: string; + readonly conflict_category: string; + readonly parent_policy?: string; + readonly base_children?: readonly HandlerChildNode[]; + readonly left_insertions?: readonly HandlerChildNode[]; + readonly right_insertions?: readonly HandlerChildNode[]; + readonly base_members?: readonly HandlerKeyedMember[]; + readonly left_edits?: readonly HandlerKeyedMember[]; + readonly right_edits?: readonly HandlerKeyedMember[]; + readonly expected_result: GenericConflictHandlerResult; +} + +export interface GenericConflictHandlerExecution { + readonly execution_id: string; + readonly version: string; + readonly cases: readonly GenericConflictHandlerCase[]; + readonly diagnostics: readonly string[]; +} + +export const genericIndependentCommutativeInsertionsHandler = + 'generic-independent-commutative-insertions'; +export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; + +export function executeGenericConflictHandler( + handlerCase: GenericConflictHandlerCase +): GenericConflictHandlerResult { + if (handlerCase.handler_id === genericIndependentCommutativeInsertionsHandler) { + return executeIndependentCommutativeInsertions(handlerCase); + } + if (handlerCase.handler_id === genericKeyedMemberEditHandler) { + return executeIndependentKeyedMemberEdits(handlerCase); + } + + return { + resolved: false, + diagnostics: ['unsupported generic conflict handler'] + }; +} + +function executeIndependentCommutativeInsertions( + handlerCase: GenericConflictHandlerCase +): GenericConflictHandlerResult { + if (handlerCase.parent_policy !== 'commutative') { + return { + resolved: false, + diagnostics: ['independent insertion handler requires a commutative parent'] + }; + } + + const seen = new Set(); + const mergedChildren: HandlerChildNode[] = []; + const appendUnique = (nodes: readonly HandlerChildNode[] | undefined) => { + for (const node of nodes ?? []) { + const key = node.signature || node.node_id; + if (seen.has(key)) continue; + seen.add(key); + mergedChildren.push(node); + } + }; + + appendUnique(handlerCase.base_children); + appendUnique(handlerCase.left_insertions); + appendUnique(handlerCase.right_insertions); + + return { + resolved: true, + merged_children: mergedChildren, + diagnostics: ['independent insertions into a commutative parent were unioned deterministically'] + }; +} + +function executeIndependentKeyedMemberEdits( + handlerCase: GenericConflictHandlerCase +): GenericConflictHandlerResult { + const order: string[] = []; + const values = new Map(); + const setMember = (member: HandlerKeyedMember) => { + if (!values.has(member.key)) order.push(member.key); + values.set(member.key, member.value); + }; + + for (const member of handlerCase.base_members ?? []) setMember(member); + for (const member of handlerCase.left_edits ?? []) setMember(member); + for (const member of handlerCase.right_edits ?? []) { + if ( + values.has(member.key) && + values.get(member.key) !== member.value && + (handlerCase.left_edits ?? []).some((left) => left.key === member.key) + ) { + return { + resolved: false, + diagnostics: ['keyed member was edited differently on both sides'] + }; + } + setMember(member); + } + + return { + resolved: true, + merged_members: order.map((key) => ({ key, value: values.get(key) ?? '' })), + diagnostics: ['independent keyed member edits were merged by key'] + }; +} + export type PolicySurface = 'fallback' | 'array'; export interface PolicyReference { diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index bd9c99d..c06170b 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -46,6 +46,11 @@ export type { DiagnosticSeverity, FallbackScopeDefinition, FallbackScopeReport, + GenericConflictHandlerCase, + GenericConflictHandlerExecution, + GenericConflictHandlerResult, + HandlerChildNode, + HandlerKeyedMember, InconsistencyReport, LineSpan, LocalLineFallbackReport, @@ -374,6 +379,7 @@ export { structuredEditProviderBatchExecutionHandoffEnvelope, structuredEditProviderBatchExecutionPlanEnvelope, structuredEditExecutionReportEnvelope, + executeGenericConflictHandler, executeNestedMerge, executeDelegatedChildApplyPlan, executeReviewedNestedMerge, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index fb32744..cde8642 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'; import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; +import { executeGenericConflictHandler } from '../src/index'; import type { AmbiguityMatchingReport, ConformanceCaseRef, @@ -44,6 +45,7 @@ import type { ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, + GenericConflictHandlerExecution, InconsistencyReport, LocalLineFallbackReport, MatchingDebugArtifacts, @@ -6103,6 +6105,42 @@ describe('ast-merge shared fixtures', () => { expect(report.handlers[1]?.fallback_scope).toBe(fixture.expected.second_handler_scope); }); + it('conforms to the slice-810 generic conflict handler execution fixture', () => { + const fixture = readFixture<{ + execution: GenericConflictHandlerExecution; + expected: { + case_count: number; + resolved_count: number; + first_handler_id: string; + first_merged_child_count: number; + second_handler_id: string; + second_merged_member_count: number; + }; + }>( + 'diagnostics', + 'slice-810-generic-conflict-handler-execution', + 'generic-conflict-handler-execution.json' + ); + const execution = fixture.execution; + const resolvedCount = execution.cases.filter( + (handlerCase) => handlerCase.expected_result.resolved + ).length; + const results = execution.cases.map((handlerCase) => + executeGenericConflictHandler(handlerCase) + ); + + execution.cases.forEach((handlerCase, index) => { + expect(results[index]).toEqual(handlerCase.expected_result); + }); + + expect(execution.cases).toHaveLength(fixture.expected.case_count); + expect(resolvedCount).toBe(fixture.expected.resolved_count); + expect(execution.cases[0]?.handler_id).toBe(fixture.expected.first_handler_id); + expect(results[0]?.merged_children).toHaveLength(fixture.expected.first_merged_child_count); + expect(execution.cases[1]?.handler_id).toBe(fixture.expected.second_handler_id); + expect(results[1]?.merged_members).toHaveLength(fixture.expected.second_merged_member_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From c3b46daf87dcf4a52d3c50f62bb1f03f2b25e27e Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:17:55 -0600 Subject: [PATCH 045/130] Add language profile handler registration contract --- packages/ast-merge/src/contracts.ts | 15 +++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 32 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 69f641b..bf2930b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -949,6 +949,21 @@ export interface GenericConflictHandlerExecution { readonly diagnostics: readonly string[]; } +export interface LanguageProfileHandlerRegistration { + readonly role: string; + readonly handler_id: string; + readonly conflict_categories: readonly string[]; + readonly enabled: boolean; +} + +export interface LanguageProfileHandlerRegistry { + readonly profile_id: string; + readonly language: string; + readonly version: string; + readonly registrations: readonly LanguageProfileHandlerRegistration[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index c06170b..67b1c89 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -52,6 +52,8 @@ export type { HandlerChildNode, HandlerKeyedMember, InconsistencyReport, + LanguageProfileHandlerRegistration, + LanguageProfileHandlerRegistry, LineSpan, LocalLineFallbackReport, MergeConflict, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index cde8642..bc73da5 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -47,6 +47,7 @@ import type { FamilyFeatureProfile, GenericConflictHandlerExecution, InconsistencyReport, + LanguageProfileHandlerRegistry, LocalLineFallbackReport, MatchingDebugArtifacts, MergeIR, @@ -6141,6 +6142,37 @@ describe('ast-merge shared fixtures', () => { expect(results[1]?.merged_members).toHaveLength(fixture.expected.second_merged_member_count); }); + it('conforms to the slice-811 language profile handler registration fixture', () => { + const fixture = readFixture<{ + profile_handlers: LanguageProfileHandlerRegistry; + expected: { + language: string; + registration_count: number; + enabled_count: number; + roles: readonly string[]; + duplicate_member_handler: string; + }; + }>( + 'diagnostics', + 'slice-811-language-profile-handler-registration', + 'language-profile-handler-registration.json' + ); + const registry = fixture.profile_handlers; + const enabledCount = registry.registrations.filter( + (registration) => registration.enabled + ).length; + const roles = registry.registrations.map((registration) => registration.role); + const duplicateMemberHandler = registry.registrations.find( + (registration) => registration.role === 'duplicate_members' + )?.handler_id; + + expect(registry.language).toBe(fixture.expected.language); + expect(registry.registrations).toHaveLength(fixture.expected.registration_count); + expect(enabledCount).toBe(fixture.expected.enabled_count); + expect(roles).toEqual(fixture.expected.roles); + expect(duplicateMemberHandler).toBe(fixture.expected.duplicate_member_handler); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 2370baa04151d08e81f214724c2c2f56bb28e3d8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:22:56 -0600 Subject: [PATCH 046/130] Add fallback usage machine output contract --- packages/ast-merge/src/contracts.ts | 35 +++++++++++++++++++ packages/ast-merge/src/index.ts | 5 +++ .../test/fixtures.integration.test.ts | 31 ++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index bf2930b..636b117 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -964,6 +964,41 @@ export interface LanguageProfileHandlerRegistry { readonly diagnostics: readonly string[]; } +export interface FallbackUsageEntry { + readonly fallback_id: string; + readonly strategy: string; + readonly scope: string; + readonly path: string; + readonly conflict_category: string; +} + +export interface FallbackUsageSummary { + readonly fallback_count: number; + readonly conflict_count: number; + readonly resolved_count: number; +} + +export interface FallbackUsageMachineOutput { + readonly fallbacks: readonly FallbackUsageEntry[]; + readonly summary: FallbackUsageSummary; +} + +export interface GitDriverOutput { + readonly stdout: string; + readonly stderr: string; + readonly exit_code: number; +} + +export interface FallbackUsageReport { + readonly report_id: string; + readonly version: string; + readonly mode: string; + readonly quiet_by_default: boolean; + readonly machine_output: FallbackUsageMachineOutput; + readonly git_driver_output: GitDriverOutput; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 67b1c89..e9e0195 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -46,11 +46,16 @@ export type { DiagnosticSeverity, FallbackScopeDefinition, FallbackScopeReport, + FallbackUsageEntry, + FallbackUsageMachineOutput, + FallbackUsageReport, + FallbackUsageSummary, GenericConflictHandlerCase, GenericConflictHandlerExecution, GenericConflictHandlerResult, HandlerChildNode, HandlerKeyedMember, + GitDriverOutput, InconsistencyReport, LanguageProfileHandlerRegistration, LanguageProfileHandlerRegistry, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index bc73da5..b14200e 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -39,6 +39,7 @@ import type { DiagnosticSeverity, DiscoveredSurface, FallbackScopeReport, + FallbackUsageReport, ChangeSet, ClassMappingReport, ConflictCategoryReport, @@ -6173,6 +6174,36 @@ describe('ast-merge shared fixtures', () => { expect(duplicateMemberHandler).toBe(fixture.expected.duplicate_member_handler); }); + it('conforms to the slice-812 fallback usage machine output fixture', () => { + const fixture = readFixture<{ + fallback_usage: FallbackUsageReport; + expected: { + mode: string; + quiet_by_default: boolean; + fallback_count: number; + conflict_count: number; + stdout: string; + stderr: string; + exit_code: number; + first_fallback_scope: string; + }; + }>( + 'diagnostics', + 'slice-812-fallback-usage-machine-output', + 'fallback-usage-machine-output.json' + ); + const report = fixture.fallback_usage; + + expect(report.mode).toBe(fixture.expected.mode); + expect(report.quiet_by_default).toBe(fixture.expected.quiet_by_default); + expect(report.machine_output.summary.fallback_count).toBe(fixture.expected.fallback_count); + expect(report.machine_output.summary.conflict_count).toBe(fixture.expected.conflict_count); + expect(report.git_driver_output.stdout).toBe(fixture.expected.stdout); + expect(report.git_driver_output.stderr).toBe(fixture.expected.stderr); + expect(report.git_driver_output.exit_code).toBe(fixture.expected.exit_code); + expect(report.machine_output.fallbacks[0]?.scope).toBe(fixture.expected.first_fallback_scope); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 8a6fe22640727534fa5934b0028684faa115a452 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:25:32 -0600 Subject: [PATCH 047/130] Add render strategy metadata contract --- packages/ast-merge/src/contracts.ts | 21 +++++++++++++++ packages/ast-merge/src/index.ts | 3 +++ .../test/fixtures.integration.test.ts | 26 +++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 636b117..0abb2ce 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -999,6 +999,27 @@ export interface FallbackUsageReport { readonly diagnostics: readonly string[]; } +export interface RenderByteSpan { + readonly start_byte: number; + readonly end_byte: number; +} + +export interface RenderStrategyMetadata { + readonly strategy: string; + readonly path: string; + readonly span: RenderByteSpan | null; + readonly preserves_source_fragment: boolean; + readonly requires_reparse: boolean; +} + +export interface RenderPlanReport { + readonly plan_id: string; + readonly version: string; + readonly language: string; + readonly strategies: readonly RenderStrategyMetadata[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index e9e0195..3f0e09f 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -78,6 +78,9 @@ export type { RenameAwareCapability, RenameAwareCandidate, RenameAwareMatchingReport, + RenderByteSpan, + RenderPlanReport, + RenderStrategyMetadata, RejectedTieBreakCandidate, SignatureMatchingParent, SignatureMatchingReport, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index b14200e..e851b1c 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -58,6 +58,7 @@ import type { PCS, RawMerge, RenameAwareMatchingReport, + RenderPlanReport, SignatureMatchingParent, SignatureMatchingReport, SourceTextNormalizedMatchingReport, @@ -6204,6 +6205,31 @@ describe('ast-merge shared fixtures', () => { expect(report.machine_output.fallbacks[0]?.scope).toBe(fixture.expected.first_fallback_scope); }); + it('conforms to the slice-813 render strategy metadata fixture', () => { + const fixture = readFixture<{ + render_plan: RenderPlanReport; + expected: { + language: string; + strategy_count: number; + strategies: readonly string[]; + source_reuse_preserves_fragment: boolean; + full_file_requires_reparse: boolean; + }; + }>('diagnostics', 'slice-813-render-strategy-metadata', 'render-strategy-metadata.json'); + const report = fixture.render_plan; + const strategies = report.strategies.map((strategy) => strategy.strategy); + + expect(report.language).toBe(fixture.expected.language); + expect(report.strategies).toHaveLength(fixture.expected.strategy_count); + expect(strategies).toEqual(fixture.expected.strategies); + expect(report.strategies[0]?.preserves_source_fragment).toBe( + fixture.expected.source_reuse_preserves_fragment + ); + expect(report.strategies.at(-1)?.requires_reparse).toBe( + fixture.expected.full_file_requires_reparse + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From f29fadc6d09c26ac5a52d87c6b46e0f6604208ee Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:27:32 -0600 Subject: [PATCH 048/130] Add reparse after render contract --- packages/ast-merge/src/contracts.ts | 13 +++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 29 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 0abb2ce..dcc29fd 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1020,6 +1020,19 @@ export interface RenderPlanReport { readonly diagnostics: readonly string[]; } +export interface RenderVerificationReport { + readonly verification_id: string; + readonly version: string; + readonly mode: string; + readonly language: string; + readonly render_strategy: string; + readonly attempted: boolean; + readonly passed: boolean; + readonly hard_gate: boolean; + readonly parse_errors: readonly string[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 3f0e09f..208d966 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -81,6 +81,7 @@ export type { RenderByteSpan, RenderPlanReport, RenderStrategyMetadata, + RenderVerificationReport, RejectedTieBreakCandidate, SignatureMatchingParent, SignatureMatchingReport, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index e851b1c..b8d20f0 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -59,6 +59,7 @@ import type { RawMerge, RenameAwareMatchingReport, RenderPlanReport, + RenderVerificationReport, SignatureMatchingParent, SignatureMatchingReport, SourceTextNormalizedMatchingReport, @@ -6230,6 +6231,34 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-814 reparse after render verification fixture', () => { + const fixture = readFixture<{ + render_verification: RenderVerificationReport; + expected: { + mode: string; + language: string; + attempted: boolean; + passed: boolean; + hard_gate: boolean; + parse_error_count: number; + render_strategy: string; + }; + }>( + 'diagnostics', + 'slice-814-reparse-after-render-verification', + 'reparse-after-render-verification.json' + ); + const report = fixture.render_verification; + + expect(report.mode).toBe(fixture.expected.mode); + expect(report.language).toBe(fixture.expected.language); + expect(report.attempted).toBe(fixture.expected.attempted); + expect(report.passed).toBe(fixture.expected.passed); + expect(report.hard_gate).toBe(fixture.expected.hard_gate); + expect(report.parse_errors).toHaveLength(fixture.expected.parse_error_count); + expect(report.render_strategy).toBe(fixture.expected.render_strategy); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 2ce2ea7fdb066cd83a6f13d09d17ae3841324ec9 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:29:58 -0600 Subject: [PATCH 049/130] Add formatting preservation metrics contract --- packages/ast-merge/src/contracts.ts | 16 ++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 29 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index dcc29fd..053550b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1033,6 +1033,22 @@ export interface RenderVerificationReport { readonly diagnostics: readonly string[]; } +export interface FormattingPreservationMetrics { + readonly expected_output_line_diff_size: number; + readonly expected_output_character_diff_size: number; + readonly formatting_preservation_score: number; +} + +export interface FormattingPreservationConformanceReport { + readonly report_id: string; + readonly version: string; + readonly suite: string; + readonly case_id: string; + readonly language: string; + readonly formatting_metrics: FormattingPreservationMetrics; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 208d966..4e7683b 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -50,6 +50,8 @@ export type { FallbackUsageMachineOutput, FallbackUsageReport, FallbackUsageSummary, + FormattingPreservationConformanceReport, + FormattingPreservationMetrics, GenericConflictHandlerCase, GenericConflictHandlerExecution, GenericConflictHandlerResult, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index b8d20f0..dc6c900 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -46,6 +46,7 @@ import type { ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, + FormattingPreservationConformanceReport, GenericConflictHandlerExecution, InconsistencyReport, LanguageProfileHandlerRegistry, @@ -6259,6 +6260,34 @@ describe('ast-merge shared fixtures', () => { expect(report.render_strategy).toBe(fixture.expected.render_strategy); }); + it('conforms to the slice-815 formatting preservation metrics fixture', () => { + const fixture = readFixture<{ + conformance_report: FormattingPreservationConformanceReport; + expected: { + suite: string; + language: string; + line_diff_size: number; + character_diff_size: number; + score: number; + }; + }>( + 'diagnostics', + 'slice-815-formatting-preservation-metrics', + 'formatting-preservation-metrics.json' + ); + const report = fixture.conformance_report; + + expect(report.suite).toBe(fixture.expected.suite); + expect(report.language).toBe(fixture.expected.language); + expect(report.formatting_metrics.expected_output_line_diff_size).toBe( + fixture.expected.line_diff_size + ); + expect(report.formatting_metrics.expected_output_character_diff_size).toBe( + fixture.expected.character_diff_size + ); + expect(report.formatting_metrics.formatting_preservation_score).toBe(fixture.expected.score); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From daa2cb1efb7d0afbb1895be83b9750107e3f1f87 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:32:26 -0600 Subject: [PATCH 050/130] Add formatting recommendation gate contract --- packages/ast-merge/src/contracts.ts | 15 +++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 27 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 053550b..fc9dcbc 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1049,6 +1049,21 @@ export interface FormattingPreservationConformanceReport { readonly diagnostics: readonly string[]; } +export interface FormattingRecommendationWeights { + readonly expected_output_line_diff_size: number; + readonly expected_output_character_diff_size: number; +} + +export interface FormattingRecommendationGate { + readonly gate_id: string; + readonly version: string; + readonly threshold: number; + readonly passed: boolean; + readonly weights: FormattingRecommendationWeights; + readonly metrics: FormattingPreservationMetrics; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 4e7683b..f9f31b6 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -52,6 +52,8 @@ export type { FallbackUsageSummary, FormattingPreservationConformanceReport, FormattingPreservationMetrics, + FormattingRecommendationGate, + FormattingRecommendationWeights, GenericConflictHandlerCase, GenericConflictHandlerExecution, GenericConflictHandlerResult, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index dc6c900..ae4b1e2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -47,6 +47,7 @@ import type { ConflictMarkerRenderingReport, FamilyFeatureProfile, FormattingPreservationConformanceReport, + FormattingRecommendationGate, GenericConflictHandlerExecution, InconsistencyReport, LanguageProfileHandlerRegistry, @@ -6288,6 +6289,32 @@ describe('ast-merge shared fixtures', () => { expect(report.formatting_metrics.formatting_preservation_score).toBe(fixture.expected.score); }); + it('conforms to the slice-816 formatting recommendation gate fixture', () => { + const fixture = readFixture<{ + recommendation_gate: FormattingRecommendationGate; + expected: { + threshold: number; + passed: boolean; + line_weight: number; + character_weight: number; + score: number; + }; + }>( + 'diagnostics', + 'slice-816-formatting-recommendation-gate', + 'formatting-recommendation-gate.json' + ); + const gate = fixture.recommendation_gate; + + expect(gate.threshold).toBe(fixture.expected.threshold); + expect(gate.passed).toBe(fixture.expected.passed); + expect(gate.weights.expected_output_line_diff_size).toBe(fixture.expected.line_weight); + expect(gate.weights.expected_output_character_diff_size).toBe( + fixture.expected.character_weight + ); + expect(gate.metrics.formatting_preservation_score).toBe(fixture.expected.score); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 6a251ed48a6584a3c51b4f2ac30d30ac39f7ab68 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:35:01 -0600 Subject: [PATCH 051/130] Add formatting hard gates contract --- packages/ast-merge/src/contracts.ts | 13 +++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index fc9dcbc..4cb0760 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1064,6 +1064,19 @@ export interface FormattingRecommendationGate { readonly diagnostics: readonly string[]; } +export interface FormattingHardGate { + readonly name: string; + readonly passed: boolean; + readonly weighted: boolean; +} + +export interface FormattingHardGateReport { + readonly report_id: string; + readonly version: string; + readonly gates: readonly FormattingHardGate[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index f9f31b6..6612746 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -50,6 +50,8 @@ export type { FallbackUsageMachineOutput, FallbackUsageReport, FallbackUsageSummary, + FormattingHardGate, + FormattingHardGateReport, FormattingPreservationConformanceReport, FormattingPreservationMetrics, FormattingRecommendationGate, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index ae4b1e2..bc7463d 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -46,6 +46,7 @@ import type { ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, + FormattingHardGateReport, FormattingPreservationConformanceReport, FormattingRecommendationGate, GenericConflictHandlerExecution, @@ -6315,6 +6316,28 @@ describe('ast-merge shared fixtures', () => { expect(gate.metrics.formatting_preservation_score).toBe(fixture.expected.score); }); + it('conforms to the slice-817 formatting hard gates fixture', () => { + const fixture = readFixture<{ + hard_gate_report: FormattingHardGateReport; + expected: { + gate_count: number; + all_passed: boolean; + weighted_gate_count: number; + first_gate: string; + second_gate: string; + }; + }>('diagnostics', 'slice-817-formatting-hard-gates', 'formatting-hard-gates.json'); + const report = fixture.hard_gate_report; + const passedCount = report.gates.filter((gate) => gate.passed).length; + const weightedCount = report.gates.filter((gate) => gate.weighted).length; + + expect(report.gates).toHaveLength(fixture.expected.gate_count); + expect(passedCount === report.gates.length).toBe(fixture.expected.all_passed); + expect(weightedCount).toBe(fixture.expected.weighted_gate_count); + expect(report.gates[0]?.name).toBe(fixture.expected.first_gate); + expect(report.gates[1]?.name).toBe(fixture.expected.second_gate); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 8d46d4fb8b54592afc2e88cda859c6512563cf7d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:37:16 -0600 Subject: [PATCH 052/130] Add secondary formatting metrics contract --- packages/ast-merge/src/contracts.ts | 10 ++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 4cb0760..702ddfd 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1077,6 +1077,16 @@ export interface FormattingHardGateReport { readonly diagnostics: readonly string[]; } +export interface SecondaryFormattingMetricsReport { + readonly report_id: string; + readonly version: string; + readonly unchanged_line_churn: number; + readonly output_diff_size: number; + readonly source_fragment_retention: number; + readonly weighted: boolean; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 6612746..c0c9537 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -89,6 +89,7 @@ export type { RenderStrategyMetadata, RenderVerificationReport, RejectedTieBreakCandidate, + SecondaryFormattingMetricsReport, SignatureMatchingParent, SignatureMatchingReport, SignatureNodeMatch, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index bc7463d..1715375 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -63,6 +63,7 @@ import type { RenameAwareMatchingReport, RenderPlanReport, RenderVerificationReport, + SecondaryFormattingMetricsReport, SignatureMatchingParent, SignatureMatchingReport, SourceTextNormalizedMatchingReport, @@ -6338,6 +6339,28 @@ describe('ast-merge shared fixtures', () => { expect(report.gates[1]?.name).toBe(fixture.expected.second_gate); }); + it('conforms to the slice-818 secondary formatting metrics fixture', () => { + const fixture = readFixture<{ + secondary_metrics: SecondaryFormattingMetricsReport; + expected: { + unchanged_line_churn: number; + output_diff_size: number; + source_fragment_retention: number; + weighted: boolean; + }; + }>( + 'diagnostics', + 'slice-818-secondary-formatting-metrics', + 'secondary-formatting-metrics.json' + ); + const report = fixture.secondary_metrics; + + expect(report.unchanged_line_churn).toBe(fixture.expected.unchanged_line_churn); + expect(report.output_diff_size).toBe(fixture.expected.output_diff_size); + expect(report.source_fragment_retention).toBe(fixture.expected.source_fragment_retention); + expect(report.weighted).toBe(fixture.expected.weighted); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 1b3fa946ba6de049d6847d4c4e119a4e0edd1d93 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:39:41 -0600 Subject: [PATCH 053/130] Add token span preservation metrics contract --- packages/ast-merge/src/contracts.ts | 10 ++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 702ddfd..adb9369 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1087,6 +1087,16 @@ export interface SecondaryFormattingMetricsReport { readonly diagnostics: readonly string[]; } +export interface TokenSpanPreservationMetricsReport { + readonly report_id: string; + readonly version: string; + readonly source_spans_available: boolean; + readonly token_preservation: number; + readonly span_preservation: number; + readonly weighted: boolean; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index c0c9537..8a4e1e6 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -99,6 +99,7 @@ export type { StructuralPathMatch, TieBreakMatch, TieBreakMatchingReport, + TokenSpanPreservationMetricsReport, CompactRuleset, CompactRulesetAtomicNode, CompactRulesetBackendDeclaration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 1715375..13625ab 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -69,6 +69,7 @@ import type { SourceTextNormalizedMatchingReport, StructuralMatchingReport, TieBreakMatchingReport, + TokenSpanPreservationMetricsReport, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, @@ -6361,6 +6362,28 @@ describe('ast-merge shared fixtures', () => { expect(report.weighted).toBe(fixture.expected.weighted); }); + it('conforms to the slice-819 token span preservation metrics fixture', () => { + const fixture = readFixture<{ + token_span_metrics: TokenSpanPreservationMetricsReport; + expected: { + source_spans_available: boolean; + token_preservation: number; + span_preservation: number; + weighted: boolean; + }; + }>( + 'diagnostics', + 'slice-819-token-span-preservation-metrics', + 'token-span-preservation-metrics.json' + ); + const report = fixture.token_span_metrics; + + expect(report.source_spans_available).toBe(fixture.expected.source_spans_available); + expect(report.token_preservation).toBe(fixture.expected.token_preservation); + expect(report.span_preservation).toBe(fixture.expected.span_preservation); + expect(report.weighted).toBe(fixture.expected.weighted); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 1de28b55cc67339c85ab12006d44ae62228257a6 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:42:05 -0600 Subject: [PATCH 054/130] Add formatting edge fixture contract --- packages/ast-merge/src/contracts.ts | 13 ++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index adb9369..28c26d7 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1097,6 +1097,19 @@ export interface TokenSpanPreservationMetricsReport { readonly diagnostics: readonly string[]; } +export interface FormattingEdgeFixtureCase { + readonly case_id: string; + readonly category: string; + readonly requires_conflict_markers: boolean; +} + +export interface FormattingEdgeFixtureSuite { + readonly suite_id: string; + readonly version: string; + readonly cases: readonly FormattingEdgeFixtureCase[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 8a4e1e6..59cb2ec 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -50,6 +50,8 @@ export type { FallbackUsageMachineOutput, FallbackUsageReport, FallbackUsageSummary, + FormattingEdgeFixtureCase, + FormattingEdgeFixtureSuite, FormattingHardGate, FormattingHardGateReport, FormattingPreservationConformanceReport, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 13625ab..98ccda9 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -46,6 +46,7 @@ import type { ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, + FormattingEdgeFixtureSuite, FormattingHardGateReport, FormattingPreservationConformanceReport, FormattingRecommendationGate, @@ -6384,6 +6385,26 @@ describe('ast-merge shared fixtures', () => { expect(report.weighted).toBe(fixture.expected.weighted); }); + it('conforms to the slice-820 formatting edge fixtures fixture', () => { + const fixture = readFixture<{ + fixture_suite: FormattingEdgeFixtureSuite; + expected: { + case_count: number; + categories: readonly string[]; + conflict_marker_case_count: number; + }; + }>('diagnostics', 'slice-820-formatting-edge-fixtures', 'formatting-edge-fixtures.json'); + const suite = fixture.fixture_suite; + const categories = suite.cases.map((fixtureCase) => fixtureCase.category); + const conflictMarkerCaseCount = suite.cases.filter( + (fixtureCase) => fixtureCase.requires_conflict_markers + ).length; + + expect(suite.cases).toHaveLength(fixture.expected.case_count); + expect(categories).toEqual(fixture.expected.categories); + expect(conflictMarkerCaseCount).toBe(fixture.expected.conflict_marker_case_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 5fd1e5d1f5d78fb6c5937a67e60a2de6b7828b4d Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 19:44:34 -0600 Subject: [PATCH 055/130] Add unsafe render fallback contract --- packages/ast-merge/src/contracts.ts | 10 ++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 28c26d7..ea6022a 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1110,6 +1110,16 @@ export interface FormattingEdgeFixtureSuite { readonly diagnostics: readonly string[]; } +export interface RenderSafetyReport { + readonly report_id: string; + readonly version: string; + readonly provider_id: string; + readonly safe_to_render: boolean; + readonly outcome: string; + readonly fallback_strategy: string; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 59cb2ec..5014f9a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -88,6 +88,7 @@ export type { RenameAwareMatchingReport, RenderByteSpan, RenderPlanReport, + RenderSafetyReport, RenderStrategyMetadata, RenderVerificationReport, RejectedTieBreakCandidate, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 98ccda9..490948d 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -63,6 +63,7 @@ import type { RawMerge, RenameAwareMatchingReport, RenderPlanReport, + RenderSafetyReport, RenderVerificationReport, SecondaryFormattingMetricsReport, SignatureMatchingParent, @@ -6405,6 +6406,30 @@ describe('ast-merge shared fixtures', () => { expect(conflictMarkerCaseCount).toBe(fixture.expected.conflict_marker_case_count); }); + it('conforms to the slice-821 unsafe render fallback or failure fixture', () => { + const fixture = readFixture<{ + render_safety: RenderSafetyReport; + expected: { + safe_to_render: boolean; + allowed_outcomes: readonly string[]; + outcome: string; + fallback_strategy: string; + diagnostic_count: number; + }; + }>( + 'diagnostics', + 'slice-821-unsafe-render-fallback-or-failure', + 'unsafe-render-fallback-or-failure.json' + ); + const report = fixture.render_safety; + + expect(report.safe_to_render).toBe(fixture.expected.safe_to_render); + expect(fixture.expected.allowed_outcomes).toContain(report.outcome); + expect(report.outcome).toBe(fixture.expected.outcome); + expect(report.fallback_strategy).toBe(fixture.expected.fallback_strategy); + expect(report.diagnostics).toHaveLength(fixture.expected.diagnostic_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From f45752a28d7da69dbe764af260276573e0d46f6a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:34:20 -0600 Subject: [PATCH 056/130] Add native provider metadata contracts --- packages/ast-merge/src/contracts.ts | 19 +++++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 33 +++++++++++++++++++ packages/tree-haver/src/contracts.ts | 19 +++++++++++ packages/tree-haver/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 33 +++++++++++++++++++ 6 files changed, 106 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index ea6022a..4eeaeef 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1120,6 +1120,25 @@ export interface RenderSafetyReport { readonly diagnostics: readonly string[]; } +export interface NativeProviderMetadataReport { + readonly provider_id: string; + readonly family: string; + readonly host_language: string; + readonly target_language: string; + readonly parser_name: string; + readonly parser_version: string; + readonly language_version: string; + readonly dialect: string; + readonly parse_error_behavior: string; + readonly source_span_support: string; + readonly render_support: string; + readonly semantic_role_support: string; + readonly retains_native_tree: boolean; + readonly native_tree_visibility: string; + readonly metadata_policy: string; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 5014f9a..496c410 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -121,6 +121,7 @@ export type { MergeIRNodeClass, MergeIROrderedNode, MergeResult, + NativeProviderMetadataReport, PairwiseMatching, PairwiseNodeMatch, PCS, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 490948d..9f74810 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -218,6 +218,7 @@ import type { StructuredEditTransportImportError, ReviewRequest, MergeResult, + NativeProviderMetadataReport, TemplateTokenConfig, TemplateExecutionPlanEntry, TemplateDirectoryApplyReport, @@ -6430,6 +6431,38 @@ describe('ast-merge shared fixtures', () => { expect(report.diagnostics).toHaveLength(fixture.expected.diagnostic_count); }); + it('conforms to the slice-822 native provider metadata fixture', () => { + const fixture = readFixture<{ + provider_metadata: NativeProviderMetadataReport; + expected: { + provider_id: string; + family: string; + host_language: string; + target_language: string; + parser_name: string; + parse_error_behavior: string; + source_span_support: string; + render_support: string; + semantic_role_support: string; + retains_native_tree: boolean; + metadata_policy: string; + }; + }>('diagnostics', 'slice-822-native-provider-metadata', 'native-provider-metadata.json'); + const report = fixture.provider_metadata; + + expect(report.provider_id).toBe(fixture.expected.provider_id); + expect(report.family).toBe(fixture.expected.family); + expect(report.host_language).toBe(fixture.expected.host_language); + expect(report.target_language).toBe(fixture.expected.target_language); + expect(report.parser_name).toBe(fixture.expected.parser_name); + expect(report.parse_error_behavior).toBe(fixture.expected.parse_error_behavior); + expect(report.source_span_support).toBe(fixture.expected.source_span_support); + expect(report.render_support).toBe(fixture.expected.render_support); + expect(report.semantic_role_support).toBe(fixture.expected.semantic_role_support); + expect(report.retains_native_tree).toBe(fixture.expected.retains_native_tree); + expect(report.metadata_policy).toBe(fixture.expected.metadata_policy); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 89c457a..a5a6b88 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -113,6 +113,25 @@ export interface NativeParserProvider { readonly metadataPolicy: string; } +export interface NativeProviderMetadata { + readonly provider_id: string; + readonly family: string; + readonly host_language: string; + readonly target_language: string; + readonly parser_name: string; + readonly parser_version: string; + readonly language_version: string; + readonly dialect: string; + readonly parse_error_behavior: string; + readonly source_span_support: string; + readonly render_support: string; + readonly semantic_role_support: string; + readonly retains_native_tree: boolean; + readonly native_tree_visibility: string; + readonly metadata_policy: string; + readonly diagnostics: readonly string[]; +} + export interface NormalizedParseResult { readonly ok: boolean; readonly backendCapability: BackendCapability; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 352b695..8430f0d 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -24,6 +24,7 @@ export type { LanguagePackAnalysis, LanguagePackProcessAnalysis, NativeParserProvider, + NativeProviderMetadata, NormalizedParseResult, NodeRole, NormalizedTreeNode, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 8a4fa2f..a7ecdc2 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -14,6 +14,7 @@ import type { ByteEditSpan, FeatureProfile, NativeParserProvider, + NativeProviderMetadata, NormalizedParseResult, NormalizedTreeNode, OrderedTreePrimitives, @@ -552,6 +553,38 @@ describe('tree-haver shared fixtures', () => { expect(result.sourceFragmentsAvailable).toBe(true); }); + it('conforms to the slice-822 native provider metadata fixture', () => { + const fixture = readFixture<{ + provider_metadata: NativeProviderMetadata; + expected: { + provider_id: string; + family: string; + host_language: string; + target_language: string; + parser_name: string; + parse_error_behavior: string; + source_span_support: string; + render_support: string; + semantic_role_support: string; + retains_native_tree: boolean; + metadata_policy: string; + }; + }>('diagnostics', 'slice-822-native-provider-metadata', 'native-provider-metadata.json'); + const metadata = fixture.provider_metadata; + + expect(metadata.provider_id).toBe(fixture.expected.provider_id); + expect(metadata.family).toBe(fixture.expected.family); + expect(metadata.host_language).toBe(fixture.expected.host_language); + expect(metadata.target_language).toBe(fixture.expected.target_language); + expect(metadata.parser_name).toBe(fixture.expected.parser_name); + expect(metadata.parse_error_behavior).toBe(fixture.expected.parse_error_behavior); + expect(metadata.source_span_support).toBe(fixture.expected.source_span_support); + expect(metadata.render_support).toBe(fixture.expected.render_support); + expect(metadata.semantic_role_support).toBe(fixture.expected.semantic_role_support); + expect(metadata.retains_native_tree).toBe(fixture.expected.retains_native_tree); + expect(metadata.metadata_policy).toBe(fixture.expected.metadata_policy); + }); + it('conforms to the slice-788 tree-haver profile fixture', () => { const fixture = readFixture<{ profile: TreeHaverProfileFixture }>( 'diagnostics', From b37ac9c628a2c29e9326935d4eb81b9db3b7e91a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:37:22 -0600 Subject: [PATCH 057/130] Add host language native provider contracts --- packages/ast-merge/src/contracts.ts | 14 ++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 4eeaeef..476b2bb 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1139,6 +1139,20 @@ export interface NativeProviderMetadataReport { readonly diagnostics: readonly string[]; } +export interface HostLanguageNativeProviderContract { + readonly provider_id: string; + readonly host_language: string; + readonly target_language: string; + readonly parser_name: string; +} + +export interface HostLanguageNativeProviderContracts { + readonly suite_id: string; + readonly version: string; + readonly providers: readonly HostLanguageNativeProviderContract[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 496c410..a3b40e5 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -63,6 +63,8 @@ export type { GenericConflictHandlerResult, HandlerChildNode, HandlerKeyedMember, + HostLanguageNativeProviderContract, + HostLanguageNativeProviderContracts, GitDriverOutput, InconsistencyReport, LanguageProfileHandlerRegistration, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 9f74810..9597b1c 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -51,6 +51,7 @@ import type { FormattingPreservationConformanceReport, FormattingRecommendationGate, GenericConflictHandlerExecution, + HostLanguageNativeProviderContracts, InconsistencyReport, LanguageProfileHandlerRegistry, LocalLineFallbackReport, @@ -6463,6 +6464,32 @@ describe('ast-merge shared fixtures', () => { expect(report.metadata_policy).toBe(fixture.expected.metadata_policy); }); + it('conforms to the slice-823 host language native provider contracts fixture', () => { + const fixture = readFixture<{ + native_provider_contracts: HostLanguageNativeProviderContracts; + expected: { + provider_count: number; + provider_ids: readonly string[]; + ruby_provider_count: number; + first_provider_parser: string; + }; + }>( + 'diagnostics', + 'slice-823-host-language-native-provider-contracts', + 'host-language-native-provider-contracts.json' + ); + const contracts = fixture.native_provider_contracts; + const providerIds = contracts.providers.map((provider) => provider.provider_id); + const rubyProviderCount = contracts.providers.filter( + (provider) => provider.host_language === 'ruby' + ).length; + + expect(contracts.providers).toHaveLength(fixture.expected.provider_count); + expect(providerIds).toEqual(fixture.expected.provider_ids); + expect(rubyProviderCount).toBe(fixture.expected.ruby_provider_count); + expect(contracts.providers[0]?.parser_name).toBe(fixture.expected.first_provider_parser); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 6818a743cdf9e62a31ffda9b52a039a76a30e3bb Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:40:12 -0600 Subject: [PATCH 058/130] Add Go native proving ground contract --- packages/ast-merge/src/contracts.ts | 9 +++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 476b2bb..83de22c 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1153,6 +1153,15 @@ export interface HostLanguageNativeProviderContracts { readonly diagnostics: readonly string[]; } +export interface NativeProviderProvingGroundReport { + readonly report_id: string; + readonly version: string; + readonly language: string; + readonly providers: readonly string[]; + readonly checks: readonly string[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index a3b40e5..10d5141 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -124,6 +124,7 @@ export type { MergeIROrderedNode, MergeResult, NativeProviderMetadataReport, + NativeProviderProvingGroundReport, PairwiseMatching, PairwiseNodeMatch, PCS, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 9597b1c..59d97b2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -220,6 +220,7 @@ import type { ReviewRequest, MergeResult, NativeProviderMetadataReport, + NativeProviderProvingGroundReport, TemplateTokenConfig, TemplateExecutionPlanEntry, TemplateDirectoryApplyReport, @@ -6490,6 +6491,24 @@ describe('ast-merge shared fixtures', () => { expect(contracts.providers[0]?.parser_name).toBe(fixture.expected.first_provider_parser); }); + it('conforms to the slice-824 Go native proving ground fixture', () => { + const fixture = readFixture<{ + proving_ground: NativeProviderProvingGroundReport; + expected: { + language: string; + provider_count: number; + providers: readonly string[]; + checks: readonly string[]; + }; + }>('diagnostics', 'slice-824-go-native-proving-ground', 'go-native-proving-ground.json'); + const report = fixture.proving_ground; + + expect(report.language).toBe(fixture.expected.language); + expect(report.providers).toHaveLength(fixture.expected.provider_count); + expect(report.providers).toEqual(fixture.expected.providers); + expect(report.checks).toEqual(fixture.expected.checks); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From ed835c9313c84c0873763bc3e691f8d7b9839bda Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:43:33 -0600 Subject: [PATCH 059/130] Add go-dst provider stack contract --- packages/ast-merge/src/contracts.ts | 10 +++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 21 +++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 83de22c..4d572f2 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1162,6 +1162,16 @@ export interface NativeProviderProvingGroundReport { readonly diagnostics: readonly string[]; } +export interface GoDSTProviderStackReport { + readonly provider_id: string; + readonly module: string; + readonly backend_family: string; + readonly language: string; + readonly role: string; + readonly compares_with: readonly string[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 10d5141..f2e1d66 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -61,6 +61,7 @@ export type { GenericConflictHandlerCase, GenericConflictHandlerExecution, GenericConflictHandlerResult, + GoDSTProviderStackReport, HandlerChildNode, HandlerKeyedMember, HostLanguageNativeProviderContract, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 59d97b2..59d6b22 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -51,6 +51,7 @@ import type { FormattingPreservationConformanceReport, FormattingRecommendationGate, GenericConflictHandlerExecution, + GoDSTProviderStackReport, HostLanguageNativeProviderContracts, InconsistencyReport, LanguageProfileHandlerRegistry, @@ -6509,6 +6510,26 @@ describe('ast-merge shared fixtures', () => { expect(report.checks).toEqual(fixture.expected.checks); }); + it('conforms to the slice-825 go-dst provider stack fixture', () => { + const fixture = readFixture<{ + provider_stack: GoDSTProviderStackReport; + expected: { + provider_id: string; + module: string; + backend_family: string; + language: string; + comparison_count: number; + }; + }>('diagnostics', 'slice-825-go-dst-provider-stack', 'go-dst-provider-stack.json'); + const report = fixture.provider_stack; + + expect(report.provider_id).toBe(fixture.expected.provider_id); + expect(report.module).toBe(fixture.expected.module); + expect(report.backend_family).toBe(fixture.expected.backend_family); + expect(report.language).toBe(fixture.expected.language); + expect(report.compares_with).toHaveLength(fixture.expected.comparison_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 9f59a3aba433b0585f966e243044f7c7ada21bce Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:46:54 -0600 Subject: [PATCH 060/130] Add Go provider comparison contract --- packages/ast-merge/src/contracts.ts | 9 ++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 4d572f2..425c041 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1172,6 +1172,15 @@ export interface GoDSTProviderStackReport { readonly diagnostics: readonly string[]; } +export interface GoProviderComparisonReport { + readonly comparison_id: string; + readonly version: string; + readonly language: string; + readonly providers: readonly string[]; + readonly dimensions: readonly string[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index f2e1d66..b35896d 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -62,6 +62,7 @@ export type { GenericConflictHandlerExecution, GenericConflictHandlerResult, GoDSTProviderStackReport, + GoProviderComparisonReport, HandlerChildNode, HandlerKeyedMember, HostLanguageNativeProviderContract, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 59d6b22..382aea0 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -52,6 +52,7 @@ import type { FormattingRecommendationGate, GenericConflictHandlerExecution, GoDSTProviderStackReport, + GoProviderComparisonReport, HostLanguageNativeProviderContracts, InconsistencyReport, LanguageProfileHandlerRegistry, @@ -6530,6 +6531,26 @@ describe('ast-merge shared fixtures', () => { expect(report.compares_with).toHaveLength(fixture.expected.comparison_count); }); + it('conforms to the slice-826 Go provider comparison fixture', () => { + const fixture = readFixture<{ + comparison: GoProviderComparisonReport; + expected: { + language: string; + provider_count: number; + dimension_count: number; + includes_backend_deficiencies: boolean; + }; + }>('diagnostics', 'slice-826-go-provider-comparison', 'go-provider-comparison.json'); + const report = fixture.comparison; + + expect(report.language).toBe(fixture.expected.language); + expect(report.providers).toHaveLength(fixture.expected.provider_count); + expect(report.dimensions).toHaveLength(fixture.expected.dimension_count); + expect(report.dimensions.includes('backend_deficiencies')).toBe( + fixture.expected.includes_backend_deficiencies + ); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 2c1ef04d3eaca16070c158cc9e7ee065ca11f34e Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 20:58:09 -0600 Subject: [PATCH 061/130] Add backend parity fixture contract --- packages/ast-merge/src/contracts.ts | 15 +++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 425c041..b3e0117 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1181,6 +1181,21 @@ export interface GoProviderComparisonReport { readonly diagnostics: readonly string[]; } +export interface BackendParityCase { + readonly case_id: string; + readonly native_provider: string; + readonly tree_sitter_provider: string; + readonly dimensions: readonly string[]; +} + +export interface BackendParitySuite { + readonly suite_id: string; + readonly version: string; + readonly language: string; + readonly cases: readonly BackendParityCase[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index b35896d..02d0e29 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -2,6 +2,8 @@ export const packageName = '@structuredmerge/ast-merge'; export type { AmbiguityMatchingReport, + BackendParityCase, + BackendParitySuite, ChangeSet, ChangeSetChange, ClassMappingDiagnostic, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 382aea0..8b9f36b 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -9,6 +9,7 @@ import { mergeRuby } from '../../ruby-merge/src/index'; import { executeGenericConflictHandler } from '../src/index'; import type { AmbiguityMatchingReport, + BackendParitySuite, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseExecution, @@ -6551,6 +6552,30 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-827 backend parity fixtures fixture', () => { + const fixture = readFixture<{ + parity_suite: BackendParitySuite; + expected: { + language: string; + case_count: number; + native_providers: readonly string[]; + tree_sitter_provider: string; + source_span_case_count: number; + }; + }>('diagnostics', 'slice-827-backend-parity-fixtures', 'backend-parity-fixtures.json'); + const suite = fixture.parity_suite; + const nativeProviders = suite.cases.map((parityCase) => parityCase.native_provider); + const sourceSpanCaseCount = suite.cases.filter((parityCase) => + parityCase.dimensions.includes('source_spans') + ).length; + + expect(suite.language).toBe(fixture.expected.language); + expect(suite.cases).toHaveLength(fixture.expected.case_count); + expect(nativeProviders).toEqual(fixture.expected.native_providers); + expect(suite.cases[0]?.tree_sitter_provider).toBe(fixture.expected.tree_sitter_provider); + expect(sourceSpanCaseCount).toBe(fixture.expected.source_span_case_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 3af2084c0379102eca69ffe48076738ae1edcab5 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:00:50 -0600 Subject: [PATCH 062/130] Add provider richness projection contract --- packages/ast-merge/src/contracts.ts | 19 +++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 27 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index b3e0117..1f6e75f 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1196,6 +1196,25 @@ export interface BackendParitySuite { readonly diagnostics: readonly string[]; } +export interface ProviderRichnessSignature { + readonly kind: string; + readonly name: string; + readonly parameters: readonly string[]; + readonly result: string; +} + +export interface ProviderRichnessProjection { + readonly projection_id: string; + readonly version: string; + readonly provider_id: string; + readonly node_path: string; + readonly generic_roles: readonly string[]; + readonly generic_signature: ProviderRichnessSignature; + readonly private_metadata: Readonly>>>; + readonly requires_private_fields: boolean; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 02d0e29..eb929f9 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -133,6 +133,8 @@ export type { PairwiseNodeMatch, PCS, PCSConstraint, + ProviderRichnessProjection, + ProviderRichnessSignature, RawMerge, RawMergeChange, FamilyFeatureProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 8b9f36b..8e31ad1 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -224,6 +224,7 @@ import type { MergeResult, NativeProviderMetadataReport, NativeProviderProvingGroundReport, + ProviderRichnessProjection, TemplateTokenConfig, TemplateExecutionPlanEntry, TemplateDirectoryApplyReport, @@ -6576,6 +6577,32 @@ describe('ast-merge shared fixtures', () => { expect(sourceSpanCaseCount).toBe(fixture.expected.source_span_case_count); }); + it('conforms to the slice-828 provider richness projection fixture', () => { + const fixture = readFixture<{ + projection: ProviderRichnessProjection; + expected: { + provider_id: string; + role_count: number; + signature_kind: string; + signature_name: string; + requires_private_fields: boolean; + private_metadata_namespace: string; + }; + }>( + 'diagnostics', + 'slice-828-provider-richness-projection', + 'provider-richness-projection.json' + ); + const projection = fixture.projection; + + expect(projection.provider_id).toBe(fixture.expected.provider_id); + expect(projection.generic_roles).toHaveLength(fixture.expected.role_count); + expect(projection.generic_signature.kind).toBe(fixture.expected.signature_kind); + expect(projection.generic_signature.name).toBe(fixture.expected.signature_name); + expect(projection.requires_private_fields).toBe(fixture.expected.requires_private_fields); + expect(projection.private_metadata[fixture.expected.private_metadata_namespace]).toBeDefined(); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 1bf0fc5e02cf0fa9ba0fad506537782f26b36ec8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:03:25 -0600 Subject: [PATCH 063/130] Add backend gap conformance contract --- packages/ast-merge/src/contracts.ts | 25 ++++++++++++++++ packages/ast-merge/src/index.ts | 3 ++ .../test/fixtures.integration.test.ts | 29 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 1f6e75f..4f63899 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1215,6 +1215,31 @@ export interface ProviderRichnessProjection { readonly diagnostics: readonly string[]; } +export interface BackendGapConformanceGap { + readonly capability: string; + readonly status: string; + readonly impact: string; + readonly diagnostic_code: string; + readonly normalized_fallback: string; +} + +export interface BackendGapConformanceSummary { + readonly gap_count: number; + readonly fallback_count: number; + readonly silently_normalized: boolean; +} + +export interface BackendGapConformanceReport { + readonly report_id: string; + readonly version: string; + readonly language: string; + readonly provider_id: string; + readonly compared_provider_id: string; + readonly gaps: readonly BackendGapConformanceGap[]; + readonly summary: BackendGapConformanceSummary; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index eb929f9..229eda1 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -2,6 +2,9 @@ export const packageName = '@structuredmerge/ast-merge'; export type { AmbiguityMatchingReport, + BackendGapConformanceGap, + BackendGapConformanceReport, + BackendGapConformanceSummary, BackendParityCase, BackendParitySuite, ChangeSet, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 8e31ad1..969285e 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -9,6 +9,7 @@ import { mergeRuby } from '../../ruby-merge/src/index'; import { executeGenericConflictHandler } from '../src/index'; import type { AmbiguityMatchingReport, + BackendGapConformanceReport, BackendParitySuite, ConformanceCaseRef, ConformanceCaseRun, @@ -6603,6 +6604,34 @@ describe('ast-merge shared fixtures', () => { expect(projection.private_metadata[fixture.expected.private_metadata_namespace]).toBeDefined(); }); + it('conforms to the slice-829 backend gap conformance report fixture', () => { + const fixture = readFixture<{ + report: BackendGapConformanceReport; + expected: { + language: string; + provider_id: string; + compared_provider_id: string; + gap_count: number; + fallback_count: number; + silently_normalized: boolean; + first_diagnostic_code: string; + }; + }>( + 'diagnostics', + 'slice-829-backend-gap-conformance-report', + 'backend-gap-conformance-report.json' + ); + const report = fixture.report; + + expect(report.language).toBe(fixture.expected.language); + expect(report.provider_id).toBe(fixture.expected.provider_id); + expect(report.compared_provider_id).toBe(fixture.expected.compared_provider_id); + expect(report.gaps).toHaveLength(fixture.expected.gap_count); + expect(report.summary.fallback_count).toBe(fixture.expected.fallback_count); + expect(report.summary.silently_normalized).toBe(fixture.expected.silently_normalized); + expect(report.gaps[0]?.diagnostic_code).toBe(fixture.expected.first_diagnostic_code); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 0a74346efd6699a829783298a954cde06892c736 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:06:31 -0600 Subject: [PATCH 064/130] Add false textual conflict fixture contract --- packages/ast-merge/src/contracts.ts | 19 +++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 24 +++++++++++++++++++ 3 files changed, 45 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 4f63899..436d48b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1240,6 +1240,25 @@ export interface BackendGapConformanceReport { readonly diagnostics: readonly string[]; } +export interface FalseTextualConflictCase { + readonly case_id: string; + readonly language: string; + readonly category: string; + readonly base_path: string; + readonly ours_path: string; + readonly theirs_path: string; + readonly expected_strategy: string; + readonly expected_unresolved_conflict: boolean; +} + +export interface FalseTextualConflictSuite { + readonly suite_id: string; + readonly version: string; + readonly source: string; + readonly cases: readonly FalseTextualConflictCase[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 229eda1..67fefb0 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -55,6 +55,8 @@ export type { FallbackUsageMachineOutput, FallbackUsageReport, FallbackUsageSummary, + FalseTextualConflictCase, + FalseTextualConflictSuite, FormattingEdgeFixtureCase, FormattingEdgeFixtureSuite, FormattingHardGate, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 969285e..4205692 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -48,6 +48,7 @@ import type { ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, FamilyFeatureProfile, + FalseTextualConflictSuite, FormattingEdgeFixtureSuite, FormattingHardGateReport, FormattingPreservationConformanceReport, @@ -6632,6 +6633,29 @@ describe('ast-merge shared fixtures', () => { expect(report.gaps[0]?.diagnostic_code).toBe(fixture.expected.first_diagnostic_code); }); + it('conforms to the slice-901 false textual conflicts fixture', () => { + const fixture = readFixture<{ + suite: FalseTextualConflictSuite; + expected: { + case_count: number; + languages: readonly string[]; + categories: readonly string[]; + expected_unresolved_conflict_count: number; + }; + }>('diagnostics', 'slice-901-false-textual-conflicts', 'false-textual-conflicts.json'); + const suite = fixture.suite; + const languages = suite.cases.map((conflictCase) => conflictCase.language); + const categories = suite.cases.map((conflictCase) => conflictCase.category); + const unresolvedConflictCount = suite.cases.filter( + (conflictCase) => conflictCase.expected_unresolved_conflict + ).length; + + expect(suite.cases).toHaveLength(fixture.expected.case_count); + expect(languages).toEqual(fixture.expected.languages); + expect(categories).toEqual(fixture.expected.categories); + expect(unresolvedConflictCount).toBe(fixture.expected.expected_unresolved_conflict_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 88089d87db8db885c042cb9ce4b7959ad95e7f80 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:10:01 -0600 Subject: [PATCH 065/130] Add git driver smoke fixture contract --- packages/ast-merge/src/contracts.ts | 19 ++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 31 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 436d48b..164615a 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1259,6 +1259,25 @@ export interface FalseTextualConflictSuite { readonly diagnostics: readonly string[]; } +export interface GitDriverSmokeCase { + readonly case_id: string; + readonly family: string; + readonly ancestor_placeholder: string; + readonly current_placeholder: string; + readonly other_placeholder: string; + readonly path_placeholder: string; + readonly expected_exit_code: number; + readonly expected_current_file_updated: boolean; +} + +export interface GitDriverSmokeSuite { + readonly suite_id: string; + readonly version: string; + readonly driver_name: string; + readonly cases: readonly GitDriverSmokeCase[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 67fefb0..92fd557 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -65,6 +65,8 @@ export type { FormattingPreservationMetrics, FormattingRecommendationGate, FormattingRecommendationWeights, + GitDriverSmokeCase, + GitDriverSmokeSuite, GenericConflictHandlerCase, GenericConflictHandlerExecution, GenericConflictHandlerResult, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 4205692..0dff78a 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -53,6 +53,7 @@ import type { FormattingHardGateReport, FormattingPreservationConformanceReport, FormattingRecommendationGate, + GitDriverSmokeSuite, GenericConflictHandlerExecution, GoDSTProviderStackReport, GoProviderComparisonReport, @@ -6656,6 +6657,36 @@ describe('ast-merge shared fixtures', () => { expect(unresolvedConflictCount).toBe(fixture.expected.expected_unresolved_conflict_count); }); + it('conforms to the slice-902 git driver smoke fixtures fixture', () => { + const fixture = readFixture<{ + suite: GitDriverSmokeSuite; + expected: { + driver_name: string; + case_count: number; + placeholder_set: readonly string[]; + updated_current_file_count: number; + }; + }>('diagnostics', 'slice-902-git-driver-smoke-fixtures', 'git-driver-smoke-fixtures.json'); + const suite = fixture.suite; + const firstCase = suite.cases[0]; + const placeholderSet = firstCase + ? [ + firstCase.ancestor_placeholder, + firstCase.current_placeholder, + firstCase.other_placeholder, + firstCase.path_placeholder + ] + : []; + const updatedCurrentFileCount = suite.cases.filter( + (smokeCase) => smokeCase.expected_current_file_updated + ).length; + + expect(suite.driver_name).toBe(fixture.expected.driver_name); + expect(suite.cases).toHaveLength(fixture.expected.case_count); + expect(placeholderSet).toEqual(fixture.expected.placeholder_set); + expect(updatedCurrentFileCount).toBe(fixture.expected.updated_current_file_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 1081c3f8962b249565cd6b4747637875b00dbf7f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:12:42 -0600 Subject: [PATCH 066/130] Add diff driver smoke fixture contract --- packages/ast-merge/src/contracts.ts | 16 +++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 164615a..76bcfa3 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1278,6 +1278,22 @@ export interface GitDriverSmokeSuite { readonly diagnostics: readonly string[]; } +export interface DiffDriverSmokeCase { + readonly case_id: string; + readonly argument_count: number; + readonly argument_roles: readonly string[]; + readonly expected_exit_code: number; + readonly expected_output_kind: string; +} + +export interface DiffDriverSmokeSuite { + readonly suite_id: string; + readonly version: string; + readonly driver_name: string; + readonly cases: readonly DiffDriverSmokeCase[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 92fd557..f9a725c 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -49,6 +49,8 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + DiffDriverSmokeCase, + DiffDriverSmokeSuite, FallbackScopeDefinition, FallbackScopeReport, FallbackUsageEntry, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 0dff78a..d7382f8 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -39,6 +39,7 @@ import type { DelegatedChildSurfaceOutput, DiagnosticCategory, DiagnosticSeverity, + DiffDriverSmokeSuite, DiscoveredSurface, FallbackScopeReport, FallbackUsageReport, @@ -6687,6 +6688,28 @@ describe('ast-merge shared fixtures', () => { expect(updatedCurrentFileCount).toBe(fixture.expected.updated_current_file_count); }); + it('conforms to the slice-903 diff driver smoke fixtures fixture', () => { + const fixture = readFixture<{ + suite: DiffDriverSmokeSuite; + expected: { + driver_name: string; + case_count: number; + argument_counts: readonly number[]; + structured_diff_count: number; + }; + }>('diagnostics', 'slice-903-diff-driver-smoke-fixtures', 'diff-driver-smoke-fixtures.json'); + const suite = fixture.suite; + const argumentCounts = suite.cases.map((smokeCase) => smokeCase.argument_count); + const structuredDiffCount = suite.cases.filter( + (smokeCase) => smokeCase.expected_output_kind === 'structured_diff' + ).length; + + expect(suite.driver_name).toBe(fixture.expected.driver_name); + expect(suite.cases).toHaveLength(fixture.expected.case_count); + expect(argumentCounts).toEqual(fixture.expected.argument_counts); + expect(structuredDiffCount).toBe(fixture.expected.structured_diff_count); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 843babf736d8427accd20a345ad80ea79ccf3650 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:15:41 -0600 Subject: [PATCH 067/130] Add performance guardrails contract --- packages/ast-merge/src/contracts.ts | 18 +++++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 76bcfa3..e8e2e3e 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1294,6 +1294,24 @@ export interface DiffDriverSmokeSuite { readonly diagnostics: readonly string[]; } +export interface PerformanceTimeoutDiagnostic { + readonly severity: string; + readonly category: string; + readonly code: string; + readonly fallback: string; +} + +export interface PerformanceGuardrails { + readonly guardrail_id: string; + readonly version: string; + readonly max_bytes: number; + readonly max_nodes: number; + readonly max_match_candidates: number; + readonly timeout_ms: number; + readonly timeout_diagnostic: PerformanceTimeoutDiagnostic; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index f9a725c..d7527b1 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -142,6 +142,8 @@ export type { PairwiseNodeMatch, PCS, PCSConstraint, + PerformanceGuardrails, + PerformanceTimeoutDiagnostic, ProviderRichnessProjection, ProviderRichnessSignature, RawMerge, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index d7382f8..d64453d 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -68,6 +68,7 @@ import type { MoveDetectionMatchingReport, PairwiseMatching, PCS, + PerformanceGuardrails, RawMerge, RenameAwareMatchingReport, RenderPlanReport, @@ -6710,6 +6711,28 @@ describe('ast-merge shared fixtures', () => { expect(structuredDiffCount).toBe(fixture.expected.structured_diff_count); }); + it('conforms to the slice-904 performance guardrails fixture', () => { + const fixture = readFixture<{ + guardrails: PerformanceGuardrails; + expected: { + max_bytes: number; + max_nodes: number; + max_match_candidates: number; + timeout_ms: number; + timeout_code: string; + fallback: string; + }; + }>('diagnostics', 'slice-904-performance-guardrails', 'performance-guardrails.json'); + const guardrails = fixture.guardrails; + + expect(guardrails.max_bytes).toBe(fixture.expected.max_bytes); + expect(guardrails.max_nodes).toBe(fixture.expected.max_nodes); + expect(guardrails.max_match_candidates).toBe(fixture.expected.max_match_candidates); + expect(guardrails.timeout_ms).toBe(fixture.expected.timeout_ms); + expect(guardrails.timeout_diagnostic.code).toBe(fixture.expected.timeout_code); + expect(guardrails.timeout_diagnostic.fallback).toBe(fixture.expected.fallback); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 7ab65f973771fa66dbc1b7f276e2dc7f050281af Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 21:18:35 -0600 Subject: [PATCH 068/130] Add profile conformance report contract --- packages/ast-merge/src/contracts.ts | 16 +++++++++++++ packages/ast-merge/src/index.ts | 2 ++ .../test/fixtures.integration.test.ts | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index e8e2e3e..f87dde5 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1312,6 +1312,22 @@ export interface PerformanceGuardrails { readonly diagnostics: readonly string[]; } +export interface ProfileSkippedRule { + readonly rule: string; + readonly reason: string; +} + +export interface ProfileConformanceReport { + readonly report_id: string; + readonly version: string; + readonly profile: string; + readonly enabled_rules: readonly string[]; + readonly skipped_rules: readonly ProfileSkippedRule[]; + readonly fallback_count: number; + readonly unresolved_conflict_count: number; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index d7527b1..18f7fe0 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -146,6 +146,8 @@ export type { PerformanceTimeoutDiagnostic, ProviderRichnessProjection, ProviderRichnessSignature, + ProfileConformanceReport, + ProfileSkippedRule, RawMerge, RawMergeChange, FamilyFeatureProfile, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index d64453d..bcf7744 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -69,6 +69,7 @@ import type { PairwiseMatching, PCS, PerformanceGuardrails, + ProfileConformanceReport, RawMerge, RenameAwareMatchingReport, RenderPlanReport, @@ -6733,6 +6734,28 @@ describe('ast-merge shared fixtures', () => { expect(guardrails.timeout_diagnostic.fallback).toBe(fixture.expected.fallback); }); + it('conforms to the slice-905 profile conformance reports fixture', () => { + const fixture = readFixture<{ + report: ProfileConformanceReport; + expected: { + profile: string; + enabled_rule_count: number; + skipped_rule_count: number; + fallback_count: number; + unresolved_conflict_count: number; + skipped_rule: string; + }; + }>('diagnostics', 'slice-905-profile-conformance-reports', 'profile-conformance-reports.json'); + const report = fixture.report; + + expect(report.profile).toBe(fixture.expected.profile); + expect(report.enabled_rules).toHaveLength(fixture.expected.enabled_rule_count); + expect(report.skipped_rules).toHaveLength(fixture.expected.skipped_rule_count); + expect(report.fallback_count).toBe(fixture.expected.fallback_count); + expect(report.unresolved_conflict_count).toBe(fixture.expected.unresolved_conflict_count); + expect(report.skipped_rules[0]?.rule).toBe(fixture.expected.skipped_rule); + }); + it('conforms to the slice-02 diagnostic vocabulary fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('diagnostic_vocabulary') From 8bf222c8061dbe8b3640c4dc823c21e50d5bf770 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 22:32:05 -0600 Subject: [PATCH 069/130] Add merge engine suite setting contract --- packages/ast-merge/src/contracts.ts | 79 ++++++++++++++++--- packages/ast-merge/src/index.ts | 5 ++ .../test/fixtures.integration.test.ts | 76 ++++++++++++++++++ 3 files changed, 148 insertions(+), 12 deletions(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index f87dde5..8447740 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -3,6 +3,20 @@ import path from 'node:path'; export type DiagnosticSeverity = 'info' | 'warning' | 'error'; +export type MergeEngine = 'owner_path' | 'merge_ir_experimental'; + +export const mergeEngineEnvironmentVariable = 'SMORG_MERGE_ENGINE'; + +export function normalizeMergeEngine(engine?: string): MergeEngine { + return engine === 'merge_ir_experimental' ? 'merge_ir_experimental' : 'owner_path'; +} + +export function mergeEngineFromEnvironment( + env: Readonly> +): MergeEngine { + return normalizeMergeEngine(env[mergeEngineEnvironmentVariable]); +} + export type DiagnosticCategory = | 'parse_error' | 'destination_parse_error' @@ -2578,6 +2592,7 @@ export interface ConformanceCaseRun { readonly requirements: ConformanceCaseRequirements; readonly familyProfile: FamilyFeatureProfile; readonly featureProfile?: ConformanceFeatureProfileView; + readonly mergeEngine?: MergeEngine; } export interface ConformanceCaseExecution { @@ -2625,6 +2640,7 @@ export interface NamedConformanceSuiteReport { export interface ConformanceFamilyPlanContext { readonly familyProfile: FamilyFeatureProfile; readonly featureProfile?: ConformanceFeatureProfileView; + readonly mergeEngine?: MergeEngine; } export interface NamedConformanceSuitePlan { @@ -2646,6 +2662,7 @@ export interface ConformanceManifestPlanningOptions { readonly contexts?: Readonly>; readonly familyProfiles?: Readonly>; readonly requireExplicitContexts?: boolean; + readonly mergeEngine?: MergeEngine; } export interface ConformanceManifestReport { @@ -2843,6 +2860,7 @@ export interface ConformanceSuitePlanEntry { export interface ConformanceSuitePlan { readonly family: string; + readonly mergeEngine?: MergeEngine; readonly entries: readonly ConformanceSuitePlanEntry[]; readonly missingRoles: readonly string[]; } @@ -6691,6 +6709,20 @@ export function defaultConformanceFamilyContext( }; } +function applyPlanningMergeEngine( + context: ConformanceFamilyPlanContext, + options: ConformanceManifestPlanningOptions +): ConformanceFamilyPlanContext { + if (context.mergeEngine || !options.mergeEngine) { + return context; + } + + return { + ...context, + mergeEngine: normalizeMergeEngine(options.mergeEngine) + }; +} + export function reviewRequestIdForFamilyContext(family: string): string { return `family_context:${family}`; } @@ -7369,7 +7401,7 @@ export function resolveConformanceFamilyContext( if (explicitContext) { return { - context: explicitContext, + context: applyPlanningMergeEngine(explicitContext, options), diagnostics: [] }; } @@ -7401,7 +7433,7 @@ export function resolveConformanceFamilyContext( } return { - context: defaultConformanceFamilyContext(familyProfile), + context: applyPlanningMergeEngine(defaultConformanceFamilyContext(familyProfile), options), diagnostics: [ { severity: 'warning', @@ -7874,6 +7906,23 @@ export function planConformanceSuite( roles: readonly string[], familyProfile: FamilyFeatureProfile, featureProfile?: ConformanceFeatureProfileView +): ConformanceSuitePlan { + return planConformanceSuiteWithMergeEngine( + manifest, + family, + roles, + familyProfile, + featureProfile + ); +} + +export function planConformanceSuiteWithMergeEngine( + manifest: ConformanceManifest, + family: string, + roles: readonly string[], + familyProfile: FamilyFeatureProfile, + featureProfile?: ConformanceFeatureProfileView, + mergeEngine?: MergeEngine ): ConformanceSuitePlan { const entries: ConformanceSuitePlanEntry[] = []; const missingRoles: string[] = []; @@ -7901,13 +7950,15 @@ export function planConformanceSuite( ref, requirements: entry.requirements ?? {}, familyProfile, - featureProfile + featureProfile, + mergeEngine } }); } return { family, + mergeEngine, entries, missingRoles }; @@ -7925,7 +7976,7 @@ export function planNamedConformanceSuite( return undefined; } - return planConformanceSuite( + return planConformanceSuiteWithMergeEngine( manifest, definition.subject.grammar, definition.roles, @@ -7939,19 +7990,23 @@ export function planNamedConformanceSuiteEntry( suiteSelector: ConformanceSuiteSelector, context: ConformanceFamilyPlanContext ): NamedConformanceSuitePlan | undefined { - const plan = planNamedConformanceSuite( - manifest, - suiteSelector, - context.familyProfile, - context.featureProfile - ); + const definition = conformanceSuiteDefinition(manifest, suiteSelector); - if (!plan) { + if (!definition) { return undefined; } + const plan = planConformanceSuiteWithMergeEngine( + manifest, + definition.subject.grammar, + definition.roles, + context.familyProfile, + context.featureProfile, + context.mergeEngine + ); + return { - suite: conformanceSuiteDefinition(manifest, suiteSelector)!, + suite: definition, plan }; } diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 18f7fe0..ae6690d 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -49,6 +49,7 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + MergeEngine, DiffDriverSmokeCase, DiffDriverSmokeSuite, FallbackScopeDefinition, @@ -471,7 +472,11 @@ export { conformanceSuiteDefinition, conformanceSuiteSelectors, defaultConformanceFamilyContext, + mergeEngineEnvironmentVariable, + mergeEngineFromEnvironment, + normalizeMergeEngine, planConformanceSuite, + planConformanceSuiteWithMergeEngine, planNamedConformanceSuiteEntry, planNamedConformanceSuitesWithDiagnostics, planNamedConformanceSuites, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index bcf7744..9570927 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -228,6 +228,7 @@ import type { StructuredEditTransportImportError, ReviewRequest, MergeResult, + MergeEngine, NativeProviderMetadataReport, NativeProviderProvingGroundReport, ProviderRichnessProjection, @@ -347,6 +348,9 @@ import { conformanceSuiteDefinition, conformanceSuiteSelectors, defaultConformanceFamilyContext, + mergeEngineEnvironmentVariable, + mergeEngineFromEnvironment, + normalizeMergeEngine, planConformanceSuite, planNamedConformanceSuiteEntry, planNamedConformanceSuitesWithDiagnostics, @@ -5630,6 +5634,78 @@ describe('ast-merge shared fixtures', () => { expect(mergeIR.changes[1]?.class_id).toBe('class-import-strings'); }); + it('conforms to the slice-906 merge engine suite setting fixture', () => { + const fixture = readFixture<{ + settings: { + default_engine: MergeEngine; + experimental_engine: MergeEngine; + supported_engines: readonly MergeEngine[]; + environment_variable: string; + experimental_policy: string; + runs_same_suite: boolean; + }; + expected: { + default_engine: MergeEngine; + experimental_engine: MergeEngine; + supported_engine_count: number; + environment_variable: string; + experimental_policy: string; + runs_same_suite: boolean; + }; + }>('diagnostics', 'slice-906-merge-engine-suite-setting', 'merge-engine-suite-setting.json'); + + expect(normalizeMergeEngine()).toBe(fixture.expected.default_engine); + expect(normalizeMergeEngine(fixture.settings.experimental_engine)).toBe( + fixture.expected.experimental_engine + ); + expect(fixture.settings.supported_engines).toHaveLength( + fixture.expected.supported_engine_count + ); + expect(mergeEngineEnvironmentVariable).toBe(fixture.expected.environment_variable); + expect(fixture.settings.experimental_policy).toBe(fixture.expected.experimental_policy); + expect(fixture.settings.runs_same_suite).toBe(fixture.expected.runs_same_suite); + expect( + mergeEngineFromEnvironment({ + [mergeEngineEnvironmentVariable]: fixture.settings.experimental_engine + }) + ).toBe('merge_ir_experimental'); + + const plan = planNamedConformanceSuitesWithDiagnostics( + { + family_feature_profiles: [], + suite_descriptors: [ + { + kind: 'family', + subject: { grammar: 'go' }, + roles: ['case'] + } + ], + families: { + go: [ + { + role: 'case', + path: ['go', 'case.json'] + } + ] + } + }, + { + familyProfiles: { + go: { + family: 'go', + supportedDialects: [], + supportedPolicies: [] + } + }, + mergeEngine: 'merge_ir_experimental' + } + ); + + expect(plan.entries).toHaveLength(1); + expect(plan.entries[0]?.plan.mergeEngine).toBe('merge_ir_experimental'); + expect(plan.entries[0]?.plan.entries[0]?.run.mergeEngine).toBe('merge_ir_experimental'); + }); + it('conforms to the slice-791 pairwise matchings fixture', () => { const fixture = readFixture<{ pairwise_matchings: readonly PairwiseMatching[]; From 713604bba47610b91a981fe8f96112e98b611911 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 22:41:00 -0600 Subject: [PATCH 070/130] Evaluate merge IR change sets behind engine flag --- packages/ast-merge/src/contracts.ts | 129 ++++++++++++++++++ packages/ast-merge/src/index.ts | 4 + .../test/fixtures.integration.test.ts | 45 ++++++ 3 files changed, 178 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 8447740..58a23a2 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -587,6 +587,14 @@ export interface InconsistencyReport { readonly diagnostics: readonly string[]; } +export interface MergeIREvaluationReport { + readonly merge_engine: MergeEngine; + readonly raw_merge: RawMerge; + readonly inconsistency_report: InconsistencyReport; + readonly outcome: string; + readonly diagnostics: readonly string[]; +} + export interface MergeIRComparisonCase { readonly case_id: string; readonly family: string; @@ -613,6 +621,127 @@ export interface MergeIRComparisonReport { readonly summary: MergeIRComparisonSummary; } +export function rawMergeChangeSets(rawMergeId: string, changeSets: readonly ChangeSet[]): RawMerge { + return { + raw_merge_id: rawMergeId, + input_change_set_ids: changeSets.map((changeSet) => changeSet.change_set_id), + changes: changeSets.flatMap((changeSet) => + changeSet.changes.map((change) => ({ + change_id: change.change_id, + source_change_set_id: changeSet.change_set_id, + side: changeSet.side, + kind: change.kind, + class_id: change.class_id, + parent_class_id: change.parent_class_id, + predecessor_class_id: change.predecessor_class_id, + successor_class_id: change.successor_class_id, + content_hash: change.content_hash + })) + ), + diagnostics: ['raw merge intentionally preserves both sides before inconsistency detection'] + }; +} + +export function detectRawMergeInconsistencies( + reportId: string, + rawMerge: RawMerge +): InconsistencyReport { + const changesByClass = new Map(); + for (const change of rawMerge.changes) { + changesByClass.set(change.class_id, [...(changesByClass.get(change.class_id) ?? []), change]); + } + + const inconsistencies: MergeInconsistency[] = []; + for (const change of rawMerge.changes) { + if (change.kind === 'move') { + inconsistencies.push({ + inconsistency_id: `order-${change.class_id}`, + category: 'order_conflict', + severity: 'warning', + class_ids: [change.class_id], + change_ids: [change.change_id], + message: 'branch changes predecessor/successor ordering relation' + }); + } + } + + for (const [classId, changes] of changesByClass) { + const changesOfKind = (kind: string) => changes.filter((change) => change.kind === kind); + const hashesOfKind = (kind: string) => + new Set(changesOfKind(kind).map((change) => change.content_hash)); + const insertions = changesOfKind('insert'); + const deletes = changesOfKind('delete'); + const contentChanges = changesOfKind('content_change'); + + if (insertions.length > 1 && hashesOfKind('insert').size > 1) { + inconsistencies.push({ + inconsistency_id: `duplicate-${classId}`, + category: 'duplicate_insertion_conflict', + severity: 'error', + class_ids: [classId], + change_ids: insertions.map((change) => change.change_id), + message: 'branches insert the same class with incompatible content hashes' + }); + } + if (deletes.length > 0 && contentChanges.length > 0) { + inconsistencies.push({ + inconsistency_id: `delete-edit-${classId}`, + category: 'delete_edit_conflict', + severity: 'error', + class_ids: [classId], + change_ids: [ + ...contentChanges.map((change) => change.change_id), + ...deletes.map((change) => change.change_id) + ], + message: 'one branch edits a class that another branch deletes' + }); + } + if (contentChanges.length > 1 && hashesOfKind('content_change').size > 1) { + inconsistencies.push({ + inconsistency_id: `content-${classId}`, + category: 'content_conflict', + severity: 'error', + class_ids: [classId], + change_ids: contentChanges.map((change) => change.change_id), + message: 'branches change class content differently' + }); + } + } + + return { + report_id: reportId, + raw_merge_id: rawMerge.raw_merge_id, + inconsistencies, + diagnostics: [ + 'inconsistency detection classifies raw merge candidates before any conflict rendering' + ] + }; +} + +export function evaluateMergeIRChangeSets( + engine: MergeEngine | undefined, + rawMergeId: string, + reportId: string, + changeSets: readonly ChangeSet[] +): MergeIREvaluationReport { + const mergeEngine = normalizeMergeEngine(engine); + const rawMerge = rawMergeChangeSets(rawMergeId, changeSets); + const inconsistencyReport = detectRawMergeInconsistencies(reportId, rawMerge); + const blockingCount = inconsistencyReport.inconsistencies.filter( + (inconsistency) => inconsistency.severity === 'error' + ).length; + + return { + merge_engine: mergeEngine, + raw_merge: rawMerge, + inconsistency_report: inconsistencyReport, + outcome: blockingCount > 0 ? 'blocked_by_inconsistency' : 'clean', + diagnostics: [ + 'merge_ir_experimental evaluates PCS-style change sets behind the opt-in engine flag' + ] + }; +} + export interface StructuralPathMatch { readonly from_path: string; readonly to_path: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index ae6690d..781c012 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -50,6 +50,7 @@ export type { DiagnosticCategory, DiagnosticSeverity, MergeEngine, + MergeIREvaluationReport, DiffDriverSmokeCase, DiffDriverSmokeSuite, FallbackScopeDefinition, @@ -475,6 +476,9 @@ export { mergeEngineEnvironmentVariable, mergeEngineFromEnvironment, normalizeMergeEngine, + detectRawMergeInconsistencies, + evaluateMergeIRChangeSets, + rawMergeChangeSets, planConformanceSuite, planConformanceSuiteWithMergeEngine, planNamedConformanceSuiteEntry, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 9570927..4f129a1 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -351,6 +351,7 @@ import { mergeEngineEnvironmentVariable, mergeEngineFromEnvironment, normalizeMergeEngine, + evaluateMergeIRChangeSets, planConformanceSuite, planNamedConformanceSuiteEntry, planNamedConformanceSuitesWithDiagnostics, @@ -5828,6 +5829,50 @@ describe('ast-merge shared fixtures', () => { expect(report.inconsistencies[1]?.change_ids[1]).toBe('right-delete-greet'); }); + it('conforms to the slice-907 merge IR experimental evaluation fixture', () => { + const fixture = readFixture<{ + request: { + merge_engine: MergeEngine; + raw_merge_id: string; + report_id: string; + change_sets: readonly ChangeSet[]; + }; + expected: { + merge_engine: MergeEngine; + raw_change_count: number; + input_change_set_count: number; + categories: readonly string[]; + blocking_count: number; + outcome: string; + }; + }>( + 'diagnostics', + 'slice-907-merge-ir-experimental-evaluation', + 'merge-ir-experimental-evaluation.json' + ); + const report = evaluateMergeIRChangeSets( + fixture.request.merge_engine, + fixture.request.raw_merge_id, + fixture.request.report_id, + fixture.request.change_sets + ); + const categories = report.inconsistency_report.inconsistencies.map( + (inconsistency) => inconsistency.category + ); + const blockingCount = report.inconsistency_report.inconsistencies.filter( + (inconsistency) => inconsistency.severity === 'error' + ).length; + + expect(report.merge_engine).toBe(fixture.expected.merge_engine); + expect(report.raw_merge.changes).toHaveLength(fixture.expected.raw_change_count); + expect(report.raw_merge.input_change_set_ids).toHaveLength( + fixture.expected.input_change_set_count + ); + expect(categories).toEqual(fixture.expected.categories); + expect(blockingCount).toBe(fixture.expected.blocking_count); + expect(report.outcome).toBe(fixture.expected.outcome); + }); + it('conforms to the slice-796 merge IR comparison fixture', () => { const fixture = readFixture<{ comparison: MergeIRComparisonReport; From a7863e9e5c0fb6cbe5edf177dc8317215a7f8995 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:06:15 -0600 Subject: [PATCH 071/130] Add smorg-ts git command surface --- README.md | 19 ++ packages/smorg-ts/package.json | 25 ++ packages/smorg-ts/src/cli.ts | 520 +++++++++++++++++++++++++++++ packages/smorg-ts/test/cli.test.ts | 170 ++++++++++ tsconfig.json | 1 + vitest.config.ts | 3 + 6 files changed, 738 insertions(+) create mode 100644 packages/smorg-ts/package.json create mode 100644 packages/smorg-ts/src/cli.ts create mode 100644 packages/smorg-ts/test/cli.test.ts diff --git a/README.md b/README.md index 32a482c..1754b0f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,25 @@ pnpm add @structuredmerge/ast-merge @structuredmerge/json-merge The packages are published under the `@structuredmerge` npm scope. +## Command + +The TypeScript implementation ships the implementation-specific `smorg-ts` +command. Use that name in git configuration unless a package manager or local +install has provided a `smorg` symlink. + +```sh +git config merge.smorg-ts.driver 'smorg-ts merge-driver %O %A %B %P' +git config diff.smorg-ts.command 'smorg-ts diff-driver' +smorg-ts conflicts diff path/to/file-with-conflicts.go +smorg-ts languages --gitattributes +``` + +`merge-driver` updates Git's `%A` file by default, or writes to `--output` when +used outside git. `diff-driver` accepts both the two-argument local form and the +seven- or nine-argument forms Git passes to external diff commands. +`conflicts diff` reports conflict-marker regions in a file that already contains +Git conflict markers. + ## Packages Core: diff --git a/packages/smorg-ts/package.json b/packages/smorg-ts/package.json new file mode 100644 index 0000000..26c8252 --- /dev/null +++ b/packages/smorg-ts/package.json @@ -0,0 +1,25 @@ +{ + "name": "@structuredmerge/smorg-ts", + "version": "0.2.0", + "private": false, + "type": "module", + "main": "./src/cli.ts", + "bin": { + "smorg-ts": "./src/cli.ts" + }, + "author": "Peter H. Boling", + "homepage": "https://structuredmerge.org", + "repository": { + "type": "git", + "url": "git+https://github.com/structuredmerge/structuredmerge-typescript.git" + }, + "bugs": { + "url": "https://github.com/structuredmerge/structuredmerge-typescript/issues" + }, + "dependencies": { + "@structuredmerge/ast-merge": "workspace:*", + "@structuredmerge/go-merge": "workspace:*", + "@structuredmerge/json-merge": "workspace:*", + "@structuredmerge/plain-merge": "workspace:*" + } +} diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts new file mode 100644 index 0000000..1a9cae1 --- /dev/null +++ b/packages/smorg-ts/src/cli.ts @@ -0,0 +1,520 @@ +#!/usr/bin/env node +import { readFileSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; + +import type { MergeResult } from '@structuredmerge/ast-merge'; +import { mergeGo } from '@structuredmerge/go-merge'; +import { mergeJson } from '@structuredmerge/json-merge'; +import { mergeText } from '@structuredmerge/plain-merge'; + +export const exitSuccess = 0; +export const exitUnresolvedConflict = 1; +export const exitUserError = 2; +export const exitInternalError = 3; + +interface MergeDriverOptions { + readonly ancestor: string; + readonly current: string; + readonly other: string; + readonly pathName?: string; + readonly output?: string; + readonly strict: boolean; + readonly fallback: string; + readonly checkOnly: boolean; + readonly exitCode: boolean; +} + +interface DiffDriverOptions { + readonly pathName?: string; + readonly oldPath: string; + readonly newPath: string; +} + +interface ConflictDiffOptions { + readonly pathName?: string; + readonly filePath: string; + readonly exitCode: boolean; +} + +interface PathSettings { + language?: string; + conflictMarkerSize: number; +} + +interface ConflictRegion { + readonly startLine: number; + readonly separatorLine: number; + readonly endLine: number; +} + +export function run( + args: readonly string[], + stdout: Pick, + stderr: Pick +): number { + const [command, ...rest] = args; + switch (command) { + case 'merge-driver': + return runMergeDriver(rest, stderr); + case 'diff-driver': + return runDiffDriver(rest, stdout, stderr); + case 'conflicts': + return runConflicts(rest, stdout, stderr); + case 'languages': + return runLanguages(rest, stdout, stderr); + case 'help': + case '-h': + case '--help': + printUsage(stdout); + return exitSuccess; + default: + if (command) { + stderr.write(`unknown command ${JSON.stringify(command)}\n`); + } + printUsage(stderr); + return exitUserError; + } +} + +function printUsage(out: Pick): void { + out.write( + [ + 'usage: smorg-ts merge-driver [--path-name PATH] [--output PATH] [--strict] [--fallback=none|line|local|full-file] %O %A %B [%P]', + ' smorg-ts merge-driver --ancestor %O --current %A --other %B --path-name %P', + ' smorg-ts diff-driver [--path-name PATH] OLD NEW', + ' smorg-ts diff-driver PATH OLD-FILE OLD-HEX OLD-MODE NEW-FILE NEW-HEX NEW-MODE [OLD-PREFIX NEW-PREFIX]', + ' smorg-ts conflicts diff [--path-name PATH] [--exit-code] FILE', + ' smorg-ts languages --gitattributes' + ].join('\n') + '\n' + ); +} + +function runMergeDriver( + args: readonly string[], + stderr: Pick +): number { + const options = parseMergeDriverOptions(args, stderr); + if (!options) return exitUserError; + + let ancestorSource: string; + let currentSource: string; + let otherSource: string; + try { + ancestorSource = readFileSync(options.ancestor, 'utf8'); + currentSource = readFileSync(options.current, 'utf8'); + otherSource = readFileSync(options.other, 'utf8'); + } catch (error) { + stderr.write(`read merge input: ${String(error)}\n`); + return exitUserError; + } + void ancestorSource; + + const effectivePath = options.pathName ?? options.current; + const settings = loadPathSettings(effectivePath); + const result = mergeByPath(effectivePath, settings.language, otherSource, currentSource); + let output = result.output; + if (!result.ok || output === undefined) { + if (options.strict || options.fallback === 'none') { + printDiagnostics(stderr, result); + return exitUnresolvedConflict; + } + output = currentSource; + } + + if (options.checkOnly) { + return options.exitCode && output !== currentSource ? exitUnresolvedConflict : exitSuccess; + } + + try { + writeFileSync(options.output ?? options.current, output); + } catch (error) { + stderr.write(`write output: ${String(error)}\n`); + return exitInternalError; + } + return exitSuccess; +} + +function parseMergeDriverOptions( + args: readonly string[], + stderr: Pick +): MergeDriverOptions | undefined { + let ancestor: string | undefined; + let current: string | undefined; + let other: string | undefined; + let pathName: string | undefined; + let output: string | undefined; + let strict = false; + let fallback = 'full-file'; + let checkOnly = false; + let exitCode = false; + const positionals: string[] = []; + + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + switch (value) { + case '--ancestor': + ancestor = args[++index]; + break; + case '--current': + current = args[++index]; + break; + case '--other': + other = args[++index]; + break; + case '--path-name': + pathName = args[++index]; + break; + case '--output': + output = args[++index]; + break; + case '--strict': + strict = true; + break; + case '--check-only': + checkOnly = true; + break; + case '--exit-code': + exitCode = true; + break; + case '--fallback': + fallback = args[++index] ?? ''; + break; + default: + if (value.startsWith('--fallback=')) { + fallback = value.slice('--fallback='.length); + } else if (value.startsWith('--')) { + stderr.write(`unknown merge-driver option ${JSON.stringify(value)}\n`); + return undefined; + } else { + positionals.push(value); + } + } + } + + ancestor ??= positionals[0]; + current ??= positionals[1]; + other ??= positionals[2]; + pathName ??= positionals[3]; + + if (!ancestor || !current || !other) { + stderr.write('merge-driver requires ancestor, current, and other paths\n'); + return undefined; + } + if (!['none', 'line', 'local', 'full-file'].includes(fallback)) { + stderr.write(`unsupported fallback mode ${JSON.stringify(fallback)}\n`); + return undefined; + } + return { ancestor, current, other, pathName, output, strict, fallback, checkOnly, exitCode }; +} + +function runDiffDriver( + args: readonly string[], + stdout: Pick, + stderr: Pick +): number { + const options = parseDiffDriverOptions(args, stderr); + if (!options) return exitUserError; + try { + printStructuredDiff( + stdout, + options.pathName ?? options.newPath, + readFileSync(options.oldPath, 'utf8'), + readFileSync(options.newPath, 'utf8') + ); + } catch (error) { + stderr.write(`read diff input: ${String(error)}\n`); + return exitUserError; + } + return exitSuccess; +} + +function parseDiffDriverOptions( + args: readonly string[], + stderr: Pick +): DiffDriverOptions | undefined { + let pathName: string | undefined; + const positionals: string[] = []; + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + if (value === '--path-name') { + pathName = args[++index]; + } else if (value.startsWith('--')) { + stderr.write(`unknown diff-driver option ${JSON.stringify(value)}\n`); + return undefined; + } else { + positionals.push(value); + } + } + + if (positionals.length === 2) { + return { pathName, oldPath: positionals[0], newPath: positionals[1] }; + } + if (positionals.length === 7 || positionals.length === 9) { + return { + pathName: pathName ?? positionals[0], + oldPath: positionals[1], + newPath: positionals[4] + }; + } + stderr.write('diff-driver requires either 2, 7, or 9 positional arguments\n'); + return undefined; +} + +function printStructuredDiff( + stdout: Pick, + pathName: string, + oldSource: string, + newSource: string +): void { + stdout.write(`structured-diff ${pathName}\n`); + if (oldSource === newSource) { + stdout.write('status unchanged\n'); + return; + } + stdout.write( + `status changed\nold-lines ${lineCount(oldSource)}\nnew-lines ${lineCount(newSource)}\n` + ); +} + +function runConflicts( + args: readonly string[], + stdout: Pick, + stderr: Pick +): number { + const [subcommand, ...rest] = args; + if (subcommand !== 'diff') { + stderr.write('conflicts requires the diff subcommand\n'); + return exitUserError; + } + return runConflictsDiff(rest, stdout, stderr); +} + +function runConflictsDiff( + args: readonly string[], + stdout: Pick, + stderr: Pick +): number { + const options = parseConflictsDiffOptions(args, stderr); + if (!options) return exitUserError; + const effectivePath = options.pathName ?? options.filePath; + try { + const settings = loadPathSettings(effectivePath); + const regions = findConflictRegions( + readFileSync(options.filePath, 'utf8'), + settings.conflictMarkerSize + ); + printConflictDiff(stdout, effectivePath, regions); + return options.exitCode && regions.length > 0 ? exitUnresolvedConflict : exitSuccess; + } catch (error) { + stderr.write(`read conflicted file: ${String(error)}\n`); + return exitUserError; + } +} + +function parseConflictsDiffOptions( + args: readonly string[], + stderr: Pick +): ConflictDiffOptions | undefined { + let pathName: string | undefined; + let exitCode = false; + const positionals: string[] = []; + for (let index = 0; index < args.length; index += 1) { + const value = args[index]; + if (value === '--path-name') { + pathName = args[++index]; + } else if (value === '--exit-code') { + exitCode = true; + } else if (value.startsWith('--')) { + stderr.write(`unknown conflicts diff option ${JSON.stringify(value)}\n`); + return undefined; + } else { + positionals.push(value); + } + } + if (positionals.length !== 1) { + stderr.write('conflicts diff requires exactly one file path\n'); + return undefined; + } + return { pathName, filePath: positionals[0], exitCode }; +} + +function runLanguages( + args: readonly string[], + stdout: Pick, + stderr: Pick +): number { + if (args.length !== 1 || args[0] !== '--gitattributes') { + stderr.write('languages currently requires --gitattributes\n'); + return exitUserError; + } + for (const line of [ + '*.go merge=smorg-ts diff=smorg-ts smorg.language=go', + '*.json merge=smorg-ts diff=smorg-ts smorg.language=json', + '*.jsonc merge=smorg-ts diff=smorg-ts smorg.language=jsonc' + ]) { + stdout.write(`${line}\n`); + } + return exitSuccess; +} + +function mergeByPath( + pathName: string, + language: string | undefined, + otherSource: string, + currentSource: string +): MergeResult { + switch (normalizeLanguage(language, pathName)) { + case 'go': + return mergeGo(otherSource, currentSource, 'go'); + case 'json': + return mergeJson(otherSource, currentSource, 'json'); + case 'jsonc': + return mergeJson(otherSource, currentSource, 'jsonc'); + default: + return mergeText(otherSource, currentSource); + } +} + +function normalizeLanguage(language: string | undefined, pathName: string): string { + switch (language?.trim().toLowerCase()) { + case 'go': + case 'golang': + return 'go'; + case 'json': + return 'json'; + case 'jsonc': + case 'json with comments': + return 'jsonc'; + case 'plain': + case 'text': + case 'plaintext': + case 'text/plain': + return 'text'; + } + switch (path.extname(pathName).toLowerCase()) { + case '.go': + return 'go'; + case '.json': + return 'json'; + case '.jsonc': + return 'jsonc'; + default: + return 'text'; + } +} + +function loadPathSettings(pathName: string): PathSettings { + const settings: PathSettings = { conflictMarkerSize: 7 }; + for (const attributesPath of attributeFilesForPath(pathName)) { + try { + applyAttributes(settings, pathName, readFileSync(attributesPath, 'utf8')); + } catch { + // Missing .gitattributes files are expected. + } + } + return settings; +} + +function attributeFilesForPath(pathName: string): string[] { + const cleanPath = path.normalize(pathName); + const dir = path.dirname(cleanPath); + if (dir === '.' || path.isAbsolute(cleanPath) || cleanPath.startsWith('..')) { + return ['.gitattributes']; + } + const files = ['.gitattributes']; + const parts = dir.split(path.sep).filter(Boolean); + for (let index = 0; index < parts.length; index += 1) { + files.push(path.join(...parts.slice(0, index + 1), '.gitattributes')); + } + return files; +} + +function applyAttributes(settings: PathSettings, pathName: string, source: string): void { + for (const rawLine of source.split('\n')) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) continue; + const [pattern, ...fields] = line.split(/\s+/u); + if (!pattern || fields.length === 0 || !attributePatternMatches(pattern, pathName)) continue; + for (const field of fields) { + const [key, value] = field.split('=', 2); + if (!value) continue; + if (key === 'smorg.language' || key === 'linguist-language') { + settings.language = value; + } else if (key === 'conflict-marker-size') { + const markerSize = Number.parseInt(value, 10); + if (markerSize > 0) settings.conflictMarkerSize = markerSize; + } + } + } +} + +function attributePatternMatches(pattern: string, pathName: string): boolean { + if (pattern === pathName) return true; + if (!pattern.includes('/')) { + return simpleGlobMatches(pattern, path.basename(pathName)); + } + return simpleGlobMatches(pattern, pathName); +} + +function simpleGlobMatches(pattern: string, value: string): boolean { + if (pattern === '*') return true; + const wildcard = pattern.indexOf('*'); + if (wildcard < 0) return pattern === value; + return ( + value.startsWith(pattern.slice(0, wildcard)) && value.endsWith(pattern.slice(wildcard + 1)) + ); +} + +function findConflictRegions(source: string, markerSize: number): ConflictRegion[] { + const width = Math.max(1, markerSize); + const startPrefix = '<'.repeat(width); + const separatorPrefix = '='.repeat(width); + const endPrefix = '>'.repeat(width); + const regions: ConflictRegion[] = []; + let current: { startLine: number; separatorLine: number } | undefined; + + source.split('\n').forEach((line, index) => { + const lineNumber = index + 1; + if (line.startsWith(startPrefix)) { + current = { startLine: lineNumber, separatorLine: 0 }; + } else if (current && current.separatorLine === 0 && line.startsWith(separatorPrefix)) { + current.separatorLine = lineNumber; + } else if (current && line.startsWith(endPrefix)) { + regions.push({ ...current, endLine: lineNumber }); + current = undefined; + } + }); + return regions; +} + +function printConflictDiff( + stdout: Pick, + pathName: string, + regions: readonly ConflictRegion[] +): void { + stdout.write(`conflicts ${pathName}\n`); + stdout.write(`count ${regions.length}\n`); + regions.forEach((region, index) => { + stdout.write( + `conflict ${index + 1} lines ${region.startLine}-${region.endLine} separator ${region.separatorLine}\n` + ); + }); +} + +function lineCount(source: string): number { + if (source.length === 0) return 0; + return source.endsWith('\n') ? source.split('\n').length - 1 : source.split('\n').length; +} + +function printDiagnostics( + stderr: Pick, + result: MergeResult +): void { + result.diagnostics.forEach((diagnostic) => { + stderr.write(`${diagnostic.category}: ${diagnostic.message}\n`); + }); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + process.exit(run(process.argv.slice(2), process.stdout, process.stderr)); +} diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts new file mode 100644 index 0000000..719ec5f --- /dev/null +++ b/packages/smorg-ts/test/cli.test.ts @@ -0,0 +1,170 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { exitSuccess, exitUnresolvedConflict, run } from '../src/cli'; + +function writer() { + let output = ''; + return { + stream: { + write: (chunk: string) => { + output += chunk; + return true; + } + }, + output: () => output + }; +} + +describe('smorg-ts cli', () => { + let previousCwd: string; + let dir: string; + + beforeEach(() => { + previousCwd = process.cwd(); + dir = mkdtempSync(path.join(tmpdir(), 'smorg-ts-test-')); + process.chdir(dir); + }); + + afterEach(() => { + process.chdir(previousCwd); + rmSync(dir, { force: true, recursive: true }); + }); + + function write(name: string, source: string): string { + const filePath = path.join(dir, name); + writeFileSync(filePath, source); + return filePath; + } + + it('updates the current file in merge-driver mode', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.tmp', '{"name":"structuredmerge","current":true}'); + const other = write('other.tmp', '{"name":"structuredmerge","other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--path-name', 'package.json', ancestor, current, other], + stdout.stream, + stderr.stream + ); + + expect(exit, stderr.output()).toBe(exitSuccess); + const merged = readFileSync(current, 'utf8'); + expect(merged).toContain('"current":true'); + expect(merged).toContain('"other":true'); + expect(stdout.output()).toBe(''); + }); + + it('uses smorg.language from gitattributes', () => { + writeFileSync('.gitattributes', '*.data smorg.language=json\n'); + const ancestor = write('ancestor.tmp', '{"name":"structuredmerge"}'); + const current = write('current.tmp', '{"name":"structuredmerge","current":true}'); + const other = write('other.tmp', '{"name":"structuredmerge","other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', ancestor, current, other, 'package.data'], + stdout.stream, + stderr.stream + ); + + expect(exit, stderr.output()).toBe(exitSuccess); + const merged = readFileSync(current, 'utf8'); + expect(merged).toContain('"current":true'); + expect(merged).toContain('"other":true'); + }); + + it('returns conflict exit code for strict merge failures', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":'); + const other = write('other.json', '{"other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--strict', ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + expect(stderr.output()).toContain('destination_parse_error'); + }); + + it('supports check-only exit-code without writing', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":"structuredmerge","current":true}'); + const other = write('other.json', '{"name":"structuredmerge","other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--check-only', '--exit-code', ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + expect(readFileSync(current, 'utf8')).not.toContain('"other":true'); + }); + + it('supports diff-driver git arities', () => { + for (const argumentCount of [7, 9]) { + const oldPath = write(`old-${argumentCount}.json`, '{"old":true}'); + const newPath = write(`new-${argumentCount}.json`, '{"new":true}'); + const args = [ + 'diff-driver', + 'package.json', + oldPath, + 'abc123', + '100644', + newPath, + 'def456', + '100644' + ]; + if (argumentCount === 9) args.push('a/', 'b/'); + const stdout = writer(); + const stderr = writer(); + + const exit = run(args, stdout.stream, stderr.stream); + + expect(exit, stderr.output()).toBe(exitSuccess); + expect(stdout.output()).toContain('structured-diff package.json'); + } + }); + + it('reports conflict regions', () => { + const conflicted = write( + 'conflicted.go', + 'package main\n<<<<<<< ours\nfunc Current() {}\n=======\nfunc Other() {}\n>>>>>>> theirs\n' + ); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['conflicts', 'diff', '--path-name', 'main.go', '--exit-code', conflicted], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + expect(stdout.output()).toContain('conflicts main.go'); + expect(stdout.output()).toContain('conflict 1 lines 2-6 separator 4'); + }); + + it('prints gitattributes', () => { + const stdout = writer(); + const stderr = writer(); + + const exit = run(['languages', '--gitattributes'], stdout.stream, stderr.stream); + + expect(exit, stderr.output()).toBe(exitSuccess); + expect(stdout.output()).toContain('*.go merge=smorg-ts diff=smorg-ts smorg.language=go'); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index cc0c9be..b966149 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "@structuredmerge/tree-haver": ["./packages/tree-haver/src/index.ts"], "@structuredmerge/plain-merge": ["./packages/plain-merge/src/index.ts"], "@structuredmerge/json-merge": ["./packages/json-merge/src/index.ts"], + "@structuredmerge/go-merge": ["./packages/go-merge/src/index.ts"], "@structuredmerge/toml-merge": ["./packages/toml-merge/src/index.ts"], "@structuredmerge/peggy-toml-merge": ["./packages/peggy-toml-merge/src/index.ts"], "@structuredmerge/typescript-merge": ["./packages/typescript-merge/src/index.ts"], diff --git a/vitest.config.ts b/vitest.config.ts index 2552365..ec7ac66 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -18,6 +18,9 @@ export default defineConfig({ '@structuredmerge/json-merge': fileURLToPath( new URL('./packages/json-merge/src/index.ts', import.meta.url) ), + '@structuredmerge/go-merge': fileURLToPath( + new URL('./packages/go-merge/src/index.ts', import.meta.url) + ), '@structuredmerge/toml-merge': fileURLToPath( new URL('./packages/toml-merge/src/index.ts', import.meta.url) ), From 639b55cd5f2cdca1c3185a88a96bb6013fed5442 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:11:18 -0600 Subject: [PATCH 072/130] Document smorg-ts symlink guidance --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 1754b0f..913300d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,13 @@ The TypeScript implementation ships the implementation-specific `smorg-ts` command. Use that name in git configuration unless a package manager or local install has provided a `smorg` symlink. +Package-manager formulas may expose the selected implementation as `smorg`. +For a local user-created symlink: + +```sh +ln -s "$(command -v smorg-ts)" ~/.local/bin/smorg +``` + ```sh git config merge.smorg-ts.driver 'smorg-ts merge-driver %O %A %B %P' git config diff.smorg-ts.command 'smorg-ts diff-driver' From 2c2033f2bf787983de5124150de6341d4babeb12 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:17:35 -0600 Subject: [PATCH 073/130] Add language backend profile schema --- packages/ast-merge/src/contracts.ts | 73 +++++++++++++++++++ packages/ast-merge/src/index.ts | 10 +++ .../test/fixtures.integration.test.ts | 32 ++++++++ 3 files changed, 115 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 58a23a2..5faae59 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1569,6 +1569,79 @@ export interface FamilyFeatureProfile { readonly supportedPolicies: readonly PolicyReference[]; } +export interface ParserIdentity { + readonly parser: string; + readonly backend: string; + readonly backend_family: string; + readonly parser_version: string; + readonly language_version: string; +} + +export interface GitAttributeProfile { + readonly attribute_namespace: string; + readonly language_attributes: readonly string[]; + readonly language: string; + readonly merge_driver: string; + readonly diff_driver: string; + readonly conflict_marker_size_attribute: string; +} + +export interface BackendProfile { + readonly backend: string; + readonly family: string; + readonly default: boolean; + readonly capabilities: readonly string[]; +} + +export interface AtomicNodeRule { + readonly selector: string; + readonly reason: string; +} + +export interface SignatureDefinition { + readonly name: string; + readonly selector: string; + readonly extractor: string; +} + +export interface CommutativeParentDefinition { + readonly selector: string; + readonly child_group: string; +} + +export interface ChildGroupDefinition { + readonly name: string; + readonly separator: string; + readonly delimiter: string | null; +} + +export interface CommentAttachmentRule { + readonly selector: string; + readonly strategy: string; +} + +export interface LanguageBackendProfileRules { + readonly node_roles: readonly string[]; + readonly atomic_nodes: readonly AtomicNodeRule[]; + readonly signatures: readonly SignatureDefinition[]; + readonly commutative_parents: readonly CommutativeParentDefinition[]; + readonly child_groups: readonly ChildGroupDefinition[]; + readonly comment_attachment: readonly CommentAttachmentRule[]; +} + +export interface LanguageBackendProfile { + readonly profile_id: string; + readonly family: string; + readonly version: string; + readonly parser_identity: ParserIdentity; + readonly extensions: readonly string[]; + readonly aliases: readonly string[]; + readonly git_attributes: GitAttributeProfile; + readonly supported_dialects: readonly string[]; + readonly backends: readonly BackendProfile[]; + readonly rules: LanguageBackendProfileRules; +} + export interface StructuredEditStructureProfile { readonly ownerScope: string; readonly ownerSelector: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 781c012..3f56eac 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -2,11 +2,13 @@ export const packageName = '@structuredmerge/ast-merge'; export type { AmbiguityMatchingReport, + AtomicNodeRule, BackendGapConformanceGap, BackendGapConformanceReport, BackendGapConformanceSummary, BackendParityCase, BackendParitySuite, + BackendProfile, ChangeSet, ChangeSetChange, ClassMappingDiagnostic, @@ -16,6 +18,9 @@ export type { ConflictHandlerRegistration, ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, + ChildGroupDefinition, + CommentAttachmentRule, + CommutativeParentDefinition, ConformanceCaseRef, ConformanceCaseRun, ConformanceCaseRequirements, @@ -82,6 +87,8 @@ export type { HostLanguageNativeProviderContracts, GitDriverOutput, InconsistencyReport, + LanguageBackendProfile, + LanguageBackendProfileRules, LanguageProfileHandlerRegistration, LanguageProfileHandlerRegistry, LineSpan, @@ -146,6 +153,7 @@ export type { PCSConstraint, PerformanceGuardrails, PerformanceTimeoutDiagnostic, + ParserIdentity, ProviderRichnessProjection, ProviderRichnessSignature, ProfileConformanceReport, @@ -153,6 +161,8 @@ export type { RawMerge, RawMergeChange, FamilyFeatureProfile, + GitAttributeProfile, + SignatureDefinition, StructuredEditStructureProfile, StructuredEditSelectionProfile, StructuredEditTargetSelection, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 4f129a1..e30891a 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -60,6 +60,7 @@ import type { GoProviderComparisonReport, HostLanguageNativeProviderContracts, InconsistencyReport, + LanguageBackendProfile, LanguageProfileHandlerRegistry, LocalLineFallbackReport, MatchingDebugArtifacts, @@ -489,6 +490,18 @@ interface FamilyFeatureProfileFixture { }; } +interface LanguageBackendProfileSchemaFixture { + profile: LanguageBackendProfile; + expected: { + profile_id: string; + family: string; + default_backend: string; + primary_language_attribute: string; + first_signature: string; + first_commutative_parent: string; + }; +} + interface TemplateSourcePathMappingFixture { cases: Array<{ template_source_path: string; @@ -6965,6 +6978,25 @@ describe('ast-merge shared fixtures', () => { }).toEqual(fixture.feature_profile); }); + it('conforms to the slice-908 language backend profile schema fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-908-language-backend-profile-schema', + 'language-backend-profile-schema.json' + ); + + expect(fixture.profile.profile_id).toBe(fixture.expected.profile_id); + expect(fixture.profile.family).toBe(fixture.expected.family); + expect(fixture.profile.backends[0]?.backend).toBe(fixture.expected.default_backend); + expect(fixture.profile.git_attributes.language_attributes[0]).toBe( + fixture.expected.primary_language_attribute + ); + expect(fixture.profile.rules.signatures[0]?.name).toBe(fixture.expected.first_signature); + expect(fixture.profile.rules.commutative_parents[0]?.selector).toBe( + fixture.expected.first_commutative_parent + ); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From 4780a97d945d3e1aa8bdfcb4c224df4dd515e1fa Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:34:00 -0600 Subject: [PATCH 074/130] Validate language backend profiles --- packages/ast-merge/src/contracts.ts | 135 ++++++++++++++++++ packages/ast-merge/src/index.ts | 6 +- .../test/fixtures.integration.test.ts | 52 ++++++- packages/tree-haver/src/contracts.ts | 3 + 4 files changed, 194 insertions(+), 2 deletions(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 5faae59..0975c34 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1642,6 +1642,141 @@ export interface LanguageBackendProfile { readonly rules: LanguageBackendProfileRules; } +export interface BackendGrammarInventory { + readonly backend_ref: { + readonly id: string; + readonly family: string; + }; + readonly known_node_kinds?: readonly string[]; + readonly known_fields?: readonly string[]; + readonly grammar_inventory?: string; +} + +export interface ProfileValidationDiagnostic { + readonly severity: 'error' | 'warning'; + readonly message: string; +} + +export interface ProfileValidationResult { + readonly ok: boolean; + readonly errors: readonly ProfileValidationDiagnostic[]; + readonly warnings: readonly ProfileValidationDiagnostic[]; + readonly diagnostics: readonly ProfileValidationDiagnostic[]; +} + +export function validateLanguageBackendProfile( + profile: LanguageBackendProfile, + capability?: BackendGrammarInventory +): ProfileValidationResult { + const errors: ProfileValidationDiagnostic[] = []; + const warnings: ProfileValidationDiagnostic[] = []; + const diagnostics: ProfileValidationDiagnostic[] = []; + const addError = (message: string) => { + const diagnostic: ProfileValidationDiagnostic = { severity: 'error', message }; + errors.push(diagnostic); + diagnostics.push(diagnostic); + }; + const addWarning = (message: string) => { + const diagnostic: ProfileValidationDiagnostic = { severity: 'warning', message }; + warnings.push(diagnostic); + diagnostics.push(diagnostic); + }; + + const validRoles = new Set([ + 'structural', + 'token', + 'trivia', + 'comment', + 'delimiter', + 'separator', + 'virtual', + 'error', + 'opaque' + ]); + profile.rules.node_roles.forEach((role) => { + if (!validRoles.has(role)) addError(`invalid node role ${role}`); + }); + + const signatureNames = new Set(); + profile.rules.signatures.forEach((signature) => { + if (!signature.name) addError('signature name is required'); + else if (signatureNames.has(signature.name)) + addError(`duplicate signature name ${signature.name}`); + signatureNames.add(signature.name); + if (!signature.selector) addError('signature selector is required'); + if (!validSignatureExtractor(signature.extractor)) { + addError(`unsupported signature extractor ${signature.extractor}`); + } + }); + + const childGroups = new Set(); + profile.rules.child_groups.forEach((group) => { + if (!group.name) addError('child group name is required'); + else if (childGroups.has(group.name)) addError(`duplicate child group name ${group.name}`); + childGroups.add(group.name); + }); + profile.rules.commutative_parents.forEach((parent) => { + if (!parent.selector) addError('commutative parent selector is required'); + if (!childGroups.has(parent.child_group)) { + addError( + `commutative parent ${parent.selector} references unknown child group ${parent.child_group}` + ); + } + }); + profile.rules.atomic_nodes.forEach((atomic) => { + if (!atomic.selector) addError('atomic node selector is required'); + }); + profile.rules.comment_attachment.forEach((attachment) => { + if (!attachment.selector) addError('comment attachment selector is required'); + if (!attachment.strategy) addError('comment attachment strategy is required'); + }); + + if (capability?.grammar_inventory) { + validateBackendInventory(profile, capability, addError, addWarning); + } + + return { ok: errors.length === 0, errors, warnings, diagnostics }; +} + +function validSignatureExtractor(extractor: string): boolean { + return ( + extractor === 'text' || + extractor.startsWith('field:') || + extractor.startsWith('kind:') || + extractor.startsWith('custom:') + ); +} + +function validateBackendInventory( + profile: LanguageBackendProfile, + capability: BackendGrammarInventory, + addError: (message: string) => void, + addWarning: (message: string) => void +): void { + const nodeKinds = new Set(capability.known_node_kinds ?? []); + const fields = new Set(capability.known_fields ?? []); + const report = capability.grammar_inventory === 'exhaustive' ? addError : addWarning; + const checkSelector = (prefix: string, selector: string) => { + if (selector && nodeKinds.size > 0 && !nodeKinds.has(selector)) report(`${prefix} ${selector}`); + }; + profile.rules.atomic_nodes.forEach((atomic) => { + checkSelector('unknown atomic node selector', atomic.selector); + }); + profile.rules.signatures.forEach((signature) => { + checkSelector('unknown signature selector', signature.selector); + if (signature.extractor.startsWith('field:')) { + const field = signature.extractor.slice('field:'.length); + if (fields.size > 0 && !fields.has(field)) report(`unknown signature field ${field}`); + } + }); + profile.rules.commutative_parents.forEach((parent) => { + checkSelector('unknown commutative parent selector', parent.selector); + }); + profile.rules.comment_attachment.forEach((attachment) => { + checkSelector('unknown comment attachment selector', attachment.selector); + }); +} + export interface StructuredEditStructureProfile { readonly ownerScope: string; readonly ownerSelector: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 3f56eac..06ebb80 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -6,6 +6,7 @@ export type { BackendGapConformanceGap, BackendGapConformanceReport, BackendGapConformanceSummary, + BackendGrammarInventory, BackendParityCase, BackendParitySuite, BackendProfile, @@ -157,6 +158,8 @@ export type { ProviderRichnessProjection, ProviderRichnessSignature, ProfileConformanceReport, + ProfileValidationDiagnostic, + ProfileValidationResult, ProfileSkippedRule, RawMerge, RawMergeChange, @@ -583,5 +586,6 @@ export { resolveConformanceFamilyContext, selectConformanceCase, summarizeNamedConformanceSuiteReports, - summarizeConformanceResults + summarizeConformanceResults, + validateLanguageBackendProfile } from './contracts'; diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index e30891a..7495658 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -6,9 +6,10 @@ import { describe, expect, it } from 'vitest'; import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; -import { executeGenericConflictHandler } from '../src/index'; +import { executeGenericConflictHandler, validateLanguageBackendProfile } from '../src/index'; import type { AmbiguityMatchingReport, + BackendGrammarInventory, BackendGapConformanceReport, BackendParitySuite, ConformanceCaseRef, @@ -71,6 +72,7 @@ import type { PCS, PerformanceGuardrails, ProfileConformanceReport, + ProfileValidationDiagnostic, RawMerge, RenameAwareMatchingReport, RenderPlanReport, @@ -502,6 +504,24 @@ interface LanguageBackendProfileSchemaFixture { }; } +interface ProfileValidationFixture { + structural_profile: LanguageBackendProfile; + backend_metadata: BackendGrammarInventory; + partial_backend_metadata: BackendGrammarInventory; + unknown_selector_profile: LanguageBackendProfile; + expected: { + structural_errors: string[]; + exhaustive_backend_errors: string[]; + partial_backend_warnings: string[]; + }; +} + +function sortedProfileValidationMessages( + diagnostics: readonly ProfileValidationDiagnostic[] +): string[] { + return diagnostics.map((diagnostic) => diagnostic.message).sort(); +} + interface TemplateSourcePathMappingFixture { cases: Array<{ template_source_path: string; @@ -6997,6 +7017,36 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-909 profile validation fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-909-profile-validation', + 'profile-validation.json' + ); + + const structural = validateLanguageBackendProfile(fixture.structural_profile); + expect(sortedProfileValidationMessages(structural.errors)).toEqual( + [...fixture.expected.structural_errors].sort() + ); + + const exhaustive = validateLanguageBackendProfile( + fixture.unknown_selector_profile, + fixture.backend_metadata + ); + expect(sortedProfileValidationMessages(exhaustive.errors)).toEqual( + [...fixture.expected.exhaustive_backend_errors].sort() + ); + + const partial = validateLanguageBackendProfile( + fixture.unknown_selector_profile, + fixture.partial_backend_metadata + ); + expect(partial.errors).toHaveLength(0); + expect(sortedProfileValidationMessages(partial.warnings)).toEqual( + [...fixture.expected.partial_backend_warnings].sort() + ); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index a5a6b88..085df2d 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -85,6 +85,9 @@ export interface BackendCapability { readonly semanticRoleSupport: string; readonly normalizedTreeSupport: boolean; readonly nativeNodeAccess: boolean; + readonly knownNodeKinds?: readonly string[]; + readonly knownFields?: readonly string[]; + readonly grammarInventory?: string; readonly diagnostics: readonly string[]; } From 4e7cb6e4ad99c5dd4c7a2be5fee328f1ec77294a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:40:24 -0600 Subject: [PATCH 075/130] Expose active profile reporting contracts --- packages/ast-merge/src/contracts.ts | 36 +++++++++++++++++++ packages/ast-merge/src/index.ts | 4 +++ .../test/fixtures.integration.test.ts | 35 ++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 0975c34..588f24b 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1460,10 +1460,40 @@ export interface ProfileSkippedRule { readonly reason: string; } +export interface ActiveProfileRuleCounts { + readonly node_roles: number; + readonly atomic_nodes: number; + readonly signatures: number; + readonly commutative_parents: number; + readonly child_groups: number; + readonly comment_attachment: number; +} + +export interface ActiveProfileValidationSummary { + readonly ok: boolean; + readonly error_count: number; + readonly warning_count: number; +} + +export interface ActiveProfileView { + readonly profile_id: string; + readonly family: string; + readonly backend: string; + readonly backend_family: string; + readonly parser: string; + readonly parser_version: string; + readonly language_version: string; + readonly dialect: string; + readonly supported_dialects: readonly string[]; + readonly rule_counts: ActiveProfileRuleCounts; + readonly validation: ActiveProfileValidationSummary; +} + export interface ProfileConformanceReport { readonly report_id: string; readonly version: string; readonly profile: string; + readonly active_profile?: ActiveProfileView; readonly enabled_rules: readonly string[]; readonly skipped_rules: readonly ProfileSkippedRule[]; readonly fallback_count: number; @@ -1471,6 +1501,12 @@ export interface ProfileConformanceReport { readonly diagnostics: readonly string[]; } +export interface ProfileDebugOutput { + readonly mode: string; + readonly active_profile: ActiveProfileView; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 06ebb80..2297216 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -1,6 +1,9 @@ export const packageName = '@structuredmerge/ast-merge'; export type { + ActiveProfileRuleCounts, + ActiveProfileValidationSummary, + ActiveProfileView, AmbiguityMatchingReport, AtomicNodeRule, BackendGapConformanceGap, @@ -158,6 +161,7 @@ export type { ProviderRichnessProjection, ProviderRichnessSignature, ProfileConformanceReport, + ProfileDebugOutput, ProfileValidationDiagnostic, ProfileValidationResult, ProfileSkippedRule, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 7495658..fa488d8 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -8,6 +8,7 @@ import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; import { executeGenericConflictHandler, validateLanguageBackendProfile } from '../src/index'; import type { + ActiveProfileView, AmbiguityMatchingReport, BackendGrammarInventory, BackendGapConformanceReport, @@ -72,6 +73,7 @@ import type { PCS, PerformanceGuardrails, ProfileConformanceReport, + ProfileDebugOutput, ProfileValidationDiagnostic, RawMerge, RenameAwareMatchingReport, @@ -516,6 +518,21 @@ interface ProfileValidationFixture { }; } +interface ActiveProfileReportingFixture { + active_profile: ActiveProfileView; + conformance_report: ProfileConformanceReport; + debug_output: ProfileDebugOutput; + expected: { + profile_id: string; + family: string; + backend: string; + parser: string; + signature_count: number; + validation_ok: boolean; + debug_mode: string; + }; +} + function sortedProfileValidationMessages( diagnostics: readonly ProfileValidationDiagnostic[] ): string[] { @@ -7047,6 +7064,24 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-910 active profile reporting fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-910-active-profile-reporting', + 'active-profile-reporting.json' + ); + + expect(fixture.active_profile.profile_id).toBe(fixture.expected.profile_id); + expect(fixture.active_profile.family).toBe(fixture.expected.family); + expect(fixture.active_profile.backend).toBe(fixture.expected.backend); + expect(fixture.active_profile.parser).toBe(fixture.expected.parser); + expect(fixture.active_profile.rule_counts.signatures).toBe(fixture.expected.signature_count); + expect(fixture.active_profile.validation.ok).toBe(fixture.expected.validation_ok); + expect(fixture.conformance_report.active_profile?.profile_id).toBe(fixture.expected.profile_id); + expect(fixture.debug_output.mode).toBe(fixture.expected.debug_mode); + expect(fixture.debug_output.active_profile.profile_id).toBe(fixture.expected.profile_id); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From f51fa2b72400f6f181654a6b5f78ca6e817ef00a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Thu, 14 May 2026 23:59:22 -0600 Subject: [PATCH 076/130] Add profile promotion reporting contracts --- packages/ast-merge/src/contracts.ts | 39 +++++++++++++++++++ packages/ast-merge/src/index.ts | 4 ++ .../test/fixtures.integration.test.ts | 36 +++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 588f24b..6964291 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1507,6 +1507,45 @@ export interface ProfileDebugOutput { readonly diagnostics: readonly string[]; } +export type ProfilePromotionStatus = + | 'experimental' + | 'available' + | 'recommended' + | 'default' + | 'disabled'; + +export interface ProfilePromotionHardGate { + readonly name: string; + readonly passed: boolean; + readonly required: boolean; + readonly diagnostics: readonly string[]; +} + +export interface ProfilePromotionMetrics { + readonly required_fixture_count: number; + readonly passed_fixture_count: number; + readonly formatting_preservation_score: number; + readonly formatting_threshold: number; + readonly fallback_count: number; + readonly fallback_threshold: number; + readonly unresolved_conflict_count: number; + readonly backend_parity_passed: boolean; +} + +export interface ProfilePromotionReport { + readonly report_id: string; + readonly version: string; + readonly profile_id: string; + readonly backend: string; + readonly status: ProfilePromotionStatus; + readonly active_profile?: ActiveProfileView; + readonly hard_gates: readonly ProfilePromotionHardGate[]; + readonly metrics: ProfilePromotionMetrics; + readonly required_suites: readonly string[]; + readonly blocking_reasons: readonly string[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 2297216..d61310f 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -162,6 +162,10 @@ export type { ProviderRichnessSignature, ProfileConformanceReport, ProfileDebugOutput, + ProfilePromotionHardGate, + ProfilePromotionMetrics, + ProfilePromotionReport, + ProfilePromotionStatus, ProfileValidationDiagnostic, ProfileValidationResult, ProfileSkippedRule, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index fa488d8..93d92ec 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -74,6 +74,7 @@ import type { PerformanceGuardrails, ProfileConformanceReport, ProfileDebugOutput, + ProfilePromotionReport, ProfileValidationDiagnostic, RawMerge, RenameAwareMatchingReport, @@ -533,6 +534,20 @@ interface ActiveProfileReportingFixture { }; } +interface ProfilePromotionReportFixture { + report: ProfilePromotionReport; + blocked_report: ProfilePromotionReport; + expected: { + profile_id: string; + recommended_status: string; + blocked_status: string; + hard_gate_count: number; + required_fixture_count: number; + formatting_threshold: number; + blocking_reason_count: number; + }; +} + function sortedProfileValidationMessages( diagnostics: readonly ProfileValidationDiagnostic[] ): string[] { @@ -7082,6 +7097,27 @@ describe('ast-merge shared fixtures', () => { expect(fixture.debug_output.active_profile.profile_id).toBe(fixture.expected.profile_id); }); + it('conforms to the slice-911 profile promotion report fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-911-profile-promotion-report', + 'profile-promotion-report.json' + ); + + expect(fixture.report.profile_id).toBe(fixture.expected.profile_id); + expect(fixture.report.status).toBe(fixture.expected.recommended_status); + expect(fixture.report.hard_gates).toHaveLength(fixture.expected.hard_gate_count); + expect(fixture.report.metrics.required_fixture_count).toBe( + fixture.expected.required_fixture_count + ); + expect(fixture.report.metrics.formatting_threshold).toBe(fixture.expected.formatting_threshold); + expect(fixture.report.active_profile?.profile_id).toBe(fixture.expected.profile_id); + expect(fixture.blocked_report.status).toBe(fixture.expected.blocked_status); + expect(fixture.blocked_report.blocking_reasons).toHaveLength( + fixture.expected.blocking_reason_count + ); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From 411ddd9b750e4123937195019730cdba713b2532 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 00:53:06 -0600 Subject: [PATCH 077/130] Add profile promotion policy contracts --- packages/ast-merge/src/contracts.ts | 37 +++++++++++++ packages/ast-merge/src/index.ts | 5 ++ .../test/fixtures.integration.test.ts | 55 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 6964291..6528688 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1546,6 +1546,43 @@ export interface ProfilePromotionReport { readonly diagnostics: readonly string[]; } +export type ProfilePromotionScope = 'data_format' | 'source_subprofile'; + +export interface ProfileRecommendationGate { + readonly required_fixture_count: number; + readonly formatting_threshold: number; + readonly fallback_threshold: number; + readonly unresolved_conflict_threshold: number; + readonly requires_backend_parity: boolean; + readonly requires_cross_implementation_parity: boolean; +} + +export interface ProfileDefaultGate { + readonly requires_recommended_status: boolean; + readonly requires_explicit_package_rollout: boolean; + readonly minimum_recommended_days: number; + readonly requires_narrow_scope: boolean; +} + +export interface ProfilePromotionPolicyEntry { + readonly profile_id: string; + readonly family: string; + readonly scope: ProfilePromotionScope; + readonly eligible_statuses: readonly ProfilePromotionStatus[]; + readonly recommendation_gate: ProfileRecommendationGate; + readonly default_gate: ProfileDefaultGate; + readonly required_suites: readonly string[]; + readonly diagnostics: readonly string[]; +} + +export interface ProfilePromotionPolicy { + readonly policy_id: string; + readonly version: string; + readonly global_hard_gates: readonly string[]; + readonly profiles: readonly ProfilePromotionPolicyEntry[]; + readonly diagnostics: readonly string[]; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index d61310f..1e29e86 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -164,8 +164,13 @@ export type { ProfileDebugOutput, ProfilePromotionHardGate, ProfilePromotionMetrics, + ProfilePromotionPolicy, + ProfilePromotionPolicyEntry, ProfilePromotionReport, + ProfilePromotionScope, ProfilePromotionStatus, + ProfileRecommendationGate, + ProfileDefaultGate, ProfileValidationDiagnostic, ProfileValidationResult, ProfileSkippedRule, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 93d92ec..878edc0 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -74,6 +74,7 @@ import type { PerformanceGuardrails, ProfileConformanceReport, ProfileDebugOutput, + ProfilePromotionPolicy, ProfilePromotionReport, ProfileValidationDiagnostic, RawMerge, @@ -548,6 +549,21 @@ interface ProfilePromotionReportFixture { }; } +interface ProfilePromotionPolicyFixture { + policy: ProfilePromotionPolicy; + expected: { + policy_id: string; + profile_count: number; + global_hard_gate_count: number; + recommended_eligible_count: number; + default_eligible_count: number; + source_subprofile_count: number; + json_requires_cross_implementation_parity: boolean; + ruby_requires_backend_parity: boolean; + formatting_threshold: number; + }; +} + function sortedProfileValidationMessages( diagnostics: readonly ProfileValidationDiagnostic[] ): string[] { @@ -7118,6 +7134,45 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-912 profile promotion policy fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-912-profile-promotion-policy', + 'profile-promotion-policy.json' + ); + const recommendedEligible = fixture.policy.profiles.filter((entry) => + entry.eligible_statuses.includes('recommended') + ).length; + const defaultEligible = fixture.policy.profiles.filter((entry) => + entry.eligible_statuses.includes('default') + ).length; + const sourceSubprofiles = fixture.policy.profiles.filter( + (entry) => entry.scope === 'source_subprofile' + ).length; + const jsonPolicy = fixture.policy.profiles.find( + (entry) => entry.profile_id === 'json.keyed-object' + ); + const rubyPolicy = fixture.policy.profiles.find( + (entry) => entry.profile_id === 'ruby.gemspec-dependencies' + ); + + expect(fixture.policy.policy_id).toBe(fixture.expected.policy_id); + expect(fixture.policy.profiles).toHaveLength(fixture.expected.profile_count); + expect(fixture.policy.global_hard_gates).toHaveLength(fixture.expected.global_hard_gate_count); + expect(recommendedEligible).toBe(fixture.expected.recommended_eligible_count); + expect(defaultEligible).toBe(fixture.expected.default_eligible_count); + expect(sourceSubprofiles).toBe(fixture.expected.source_subprofile_count); + expect(jsonPolicy?.recommendation_gate.requires_cross_implementation_parity).toBe( + fixture.expected.json_requires_cross_implementation_parity + ); + expect(rubyPolicy?.recommendation_gate.requires_backend_parity).toBe( + fixture.expected.ruby_requires_backend_parity + ); + expect(jsonPolicy?.recommendation_gate.formatting_threshold).toBe( + fixture.expected.formatting_threshold + ); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From c3ebc267895599ba3bb5d54544368d069863430b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 00:57:26 -0600 Subject: [PATCH 078/130] Evaluate profile promotion policies --- packages/ast-merge/src/contracts.ts | 64 +++++++++++++++++++ packages/ast-merge/src/index.ts | 2 + .../test/fixtures.integration.test.ts | 45 ++++++++++++- 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 6528688..a9143b7 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1583,6 +1583,70 @@ export interface ProfilePromotionPolicy { readonly diagnostics: readonly string[]; } +export interface ProfilePromotionEvaluation { + readonly profile_id: string; + readonly status: ProfilePromotionStatus; + readonly blocking_reasons: readonly string[]; + readonly diagnostics: readonly string[]; +} + +export function evaluateProfilePromotion( + policy: ProfilePromotionPolicy, + report: ProfilePromotionReport +): ProfilePromotionEvaluation { + const entry = policy.profiles.find((profile) => profile.profile_id === report.profile_id); + if (!entry) { + return { + profile_id: report.profile_id, + status: 'experimental', + blocking_reasons: ['profile has no promotion policy'], + diagnostics: [] + }; + } + const blockingReasons = profilePromotionBlockingReasons(entry, report); + const status = + blockingReasons.length === 0 && entry.eligible_statuses.includes('recommended') + ? 'recommended' + : 'available'; + return { + profile_id: report.profile_id, + status, + blocking_reasons: blockingReasons, + diagnostics: [] + }; +} + +function profilePromotionBlockingReasons( + entry: ProfilePromotionPolicyEntry, + report: ProfilePromotionReport +): string[] { + const reasons: string[] = []; + report.hard_gates.forEach((gate) => { + if (gate.required && !gate.passed) reasons.push(`required hard gate ${gate.name} failed`); + }); + if (report.metrics.passed_fixture_count < entry.recommendation_gate.required_fixture_count) { + reasons.push('passed fixture count is below required fixture count'); + } + if ( + report.metrics.formatting_preservation_score < entry.recommendation_gate.formatting_threshold + ) { + reasons.push('formatting preservation score is below threshold'); + } + if (report.metrics.fallback_count > entry.recommendation_gate.fallback_threshold) { + reasons.push('fallback count exceeds threshold'); + } + if ( + report.metrics.unresolved_conflict_count > + entry.recommendation_gate.unresolved_conflict_threshold + ) { + reasons.push('unresolved conflict count exceeds threshold'); + } + if (entry.recommendation_gate.requires_backend_parity && !report.metrics.backend_parity_passed) { + reasons.push('backend parity did not pass'); + } + return reasons; +} + export const genericIndependentCommutativeInsertionsHandler = 'generic-independent-commutative-insertions'; export const genericKeyedMemberEditHandler = 'generic-keyed-member-edit'; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 1e29e86..562883c 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -163,6 +163,7 @@ export type { ProfileConformanceReport, ProfileDebugOutput, ProfilePromotionHardGate, + ProfilePromotionEvaluation, ProfilePromotionMetrics, ProfilePromotionPolicy, ProfilePromotionPolicyEntry, @@ -600,5 +601,6 @@ export { selectConformanceCase, summarizeNamedConformanceSuiteReports, summarizeConformanceResults, + evaluateProfilePromotion, validateLanguageBackendProfile } from './contracts'; diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 878edc0..b8383eb 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -6,7 +6,11 @@ import { describe, expect, it } from 'vitest'; import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; -import { executeGenericConflictHandler, validateLanguageBackendProfile } from '../src/index'; +import { + evaluateProfilePromotion, + executeGenericConflictHandler, + validateLanguageBackendProfile +} from '../src/index'; import type { ActiveProfileView, AmbiguityMatchingReport, @@ -564,6 +568,20 @@ interface ProfilePromotionPolicyFixture { }; } +interface ProfilePromotionEvaluationFixture { + policy: ProfilePromotionPolicy; + recommended_report: ProfilePromotionReport; + blocked_report: ProfilePromotionReport; + expected: { + recommended_status: string; + recommended_blocking_reason_count: number; + blocked_status: string; + blocked_blocking_reason_count: number; + first_blocking_reason: string; + unknown_profile_status: string; + }; +} + function sortedProfileValidationMessages( diagnostics: readonly ProfileValidationDiagnostic[] ): string[] { @@ -7173,6 +7191,31 @@ describe('ast-merge shared fixtures', () => { ); }); + it('conforms to the slice-913 profile promotion evaluation fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-913-profile-promotion-evaluation', + 'profile-promotion-evaluation.json' + ); + + const recommended = evaluateProfilePromotion(fixture.policy, fixture.recommended_report); + expect(recommended.status).toBe(fixture.expected.recommended_status); + expect(recommended.blocking_reasons).toHaveLength( + fixture.expected.recommended_blocking_reason_count + ); + + const blocked = evaluateProfilePromotion(fixture.policy, fixture.blocked_report); + expect(blocked.status).toBe(fixture.expected.blocked_status); + expect(blocked.blocking_reasons).toHaveLength(fixture.expected.blocked_blocking_reason_count); + expect(blocked.blocking_reasons[0]).toBe(fixture.expected.first_blocking_reason); + + const unknown = evaluateProfilePromotion(fixture.policy, { + ...fixture.recommended_report, + profile_id: 'unknown.profile' + }); + expect(unknown.status).toBe(fixture.expected.unknown_profile_status); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From 2dbaf5f5ecf475434c5295125ef9783e8b73f0ab Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 01:20:25 -0600 Subject: [PATCH 079/130] Use canonical promotion policy profile ids --- packages/ast-merge/test/fixtures.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index b8383eb..3b91ed7 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -7171,7 +7171,7 @@ describe('ast-merge shared fixtures', () => { (entry) => entry.profile_id === 'json.keyed-object' ); const rubyPolicy = fixture.policy.profiles.find( - (entry) => entry.profile_id === 'ruby.gemspec-dependencies' + (entry) => entry.profile_id === 'ruby.gemspec-dependency-declarations' ); expect(fixture.policy.policy_id).toBe(fixture.expected.policy_id); From aadec88df368e019a03c43949bc8f90e277bc2bd Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 01:23:13 -0600 Subject: [PATCH 080/130] Expose canonical promotion profile ids --- packages/ast-merge/src/contracts.ts | 7 +++++++ packages/ast-merge/src/index.ts | 5 +++++ packages/ast-merge/test/fixtures.integration.test.ts | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index a9143b7..890b010 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1548,6 +1548,13 @@ export interface ProfilePromotionReport { export type ProfilePromotionScope = 'data_format' | 'source_subprofile'; +export const promotionProfileJsonKeyedObject = 'json.keyed-object'; +export const promotionProfileGoImportDeclarations = 'go.import-declarations'; +export const promotionProfileRustUseDeclarations = 'rust.use-declarations'; +export const promotionProfileTypeScriptImportDeclarations = 'typescript.import-declarations'; +export const promotionProfileRubyGemspecDependencyDeclarations = + 'ruby.gemspec-dependency-declarations'; + export interface ProfileRecommendationGate { readonly required_fixture_count: number; readonly formatting_threshold: number; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 562883c..e90327a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -602,5 +602,10 @@ export { summarizeNamedConformanceSuiteReports, summarizeConformanceResults, evaluateProfilePromotion, + promotionProfileGoImportDeclarations, + promotionProfileJsonKeyedObject, + promotionProfileRubyGemspecDependencyDeclarations, + promotionProfileRustUseDeclarations, + promotionProfileTypeScriptImportDeclarations, validateLanguageBackendProfile } from './contracts'; diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 3b91ed7..89b8dbe 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -9,6 +9,8 @@ import { mergeRuby } from '../../ruby-merge/src/index'; import { evaluateProfilePromotion, executeGenericConflictHandler, + promotionProfileJsonKeyedObject, + promotionProfileRubyGemspecDependencyDeclarations, validateLanguageBackendProfile } from '../src/index'; import type { @@ -7168,10 +7170,10 @@ describe('ast-merge shared fixtures', () => { (entry) => entry.scope === 'source_subprofile' ).length; const jsonPolicy = fixture.policy.profiles.find( - (entry) => entry.profile_id === 'json.keyed-object' + (entry) => entry.profile_id === promotionProfileJsonKeyedObject ); const rubyPolicy = fixture.policy.profiles.find( - (entry) => entry.profile_id === 'ruby.gemspec-dependency-declarations' + (entry) => entry.profile_id === promotionProfileRubyGemspecDependencyDeclarations ); expect(fixture.policy.policy_id).toBe(fixture.expected.policy_id); From 50f5afa81955ea175272be553f803457ba706a4b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 01:26:42 -0600 Subject: [PATCH 081/130] Expose initial profile promotion policy --- packages/ast-merge/src/contracts.ts | 88 +++++++++++++++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 2 + 3 files changed, 91 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 890b010..9657dbb 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1590,6 +1590,94 @@ export interface ProfilePromotionPolicy { readonly diagnostics: readonly string[]; } +export function initialProfilePromotionPolicy(): ProfilePromotionPolicy { + const sourceSubprofile = (profileId: string, family: string): ProfilePromotionPolicyEntry => ({ + profile_id: profileId, + family, + scope: 'source_subprofile', + eligible_statuses: ['available', 'recommended'], + recommendation_gate: { + required_fixture_count: 16, + formatting_threshold: 0.95, + fallback_threshold: 2, + unresolved_conflict_threshold: 0, + requires_backend_parity: true, + requires_cross_implementation_parity: false + }, + default_gate: { + requires_recommended_status: true, + requires_explicit_package_rollout: true, + minimum_recommended_days: 30, + requires_narrow_scope: true + }, + required_suites: [ + 'slice-827-backend-parity-fixtures', + 'slice-815-formatting-preservation-metrics' + ], + diagnostics: ['source-language profile is narrow and not language-wide'] + }); + const rubyProfile = sourceSubprofile(promotionProfileRubyGemspecDependencyDeclarations, 'ruby'); + return { + policy_id: 'initial-profile-promotion-policy', + version: '1', + global_hard_gates: [ + 'parse_or_fail_closed', + 'render_or_fail_closed', + 'coherent_conflict_markers', + 'performance_guardrails' + ], + profiles: [ + { + profile_id: promotionProfileJsonKeyedObject, + family: 'json', + scope: 'data_format', + eligible_statuses: ['available', 'recommended', 'default'], + recommendation_gate: { + required_fixture_count: 12, + formatting_threshold: 0.95, + fallback_threshold: 1, + unresolved_conflict_threshold: 0, + requires_backend_parity: true, + requires_cross_implementation_parity: true + }, + default_gate: { + requires_recommended_status: true, + requires_explicit_package_rollout: true, + minimum_recommended_days: 30, + requires_narrow_scope: true + }, + required_suites: [ + 'slice-901-false-textual-conflicts', + 'slice-902-git-driver-smoke-fixtures', + 'slice-815-formatting-preservation-metrics' + ], + diagnostics: ['data-format profile may become default after recommendation soak time'] + }, + sourceSubprofile(promotionProfileGoImportDeclarations, 'go'), + sourceSubprofile(promotionProfileRustUseDeclarations, 'rust'), + sourceSubprofile(promotionProfileTypeScriptImportDeclarations, 'typescript'), + { + ...rubyProfile, + recommendation_gate: { + ...rubyProfile.recommendation_gate, + required_fixture_count: 10, + fallback_threshold: 1, + requires_backend_parity: false + }, + required_suites: [ + 'slice-702-ruby-gemspec-signature-merge-acceptance', + 'slice-703-ruby-gemspec-field-policy-acceptance', + 'slice-704-ruby-gemspec-dependency-section-policy-acceptance' + ], + diagnostics: ['Ruby source subprofile is limited to dependency declarations'] + } + ], + diagnostics: [ + 'default status is allowed only after recommendation status and explicit package rollout' + ] + }; +} + export interface ProfilePromotionEvaluation { readonly profile_id: string; readonly status: ProfilePromotionStatus; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index e90327a..ba4253c 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -602,6 +602,7 @@ export { summarizeNamedConformanceSuiteReports, summarizeConformanceResults, evaluateProfilePromotion, + initialProfilePromotionPolicy, promotionProfileGoImportDeclarations, promotionProfileJsonKeyedObject, promotionProfileRubyGemspecDependencyDeclarations, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 89b8dbe..4670cb6 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -9,6 +9,7 @@ import { mergeRuby } from '../../ruby-merge/src/index'; import { evaluateProfilePromotion, executeGenericConflictHandler, + initialProfilePromotionPolicy, promotionProfileJsonKeyedObject, promotionProfileRubyGemspecDependencyDeclarations, validateLanguageBackendProfile @@ -7191,6 +7192,7 @@ describe('ast-merge shared fixtures', () => { expect(jsonPolicy?.recommendation_gate.formatting_threshold).toBe( fixture.expected.formatting_threshold ); + expect(initialProfilePromotionPolicy()).toEqual(fixture.policy); }); it('conforms to the slice-913 profile promotion evaluation fixture', () => { From 6126ea71f44615974b7bfa5bdf7eaa4f37b567a4 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 08:34:44 -0600 Subject: [PATCH 082/130] Add profile selection enforcement contract --- packages/ast-merge/src/contracts.ts | 80 +++++++++++++++++++ packages/ast-merge/src/index.ts | 4 + .../test/fixtures.integration.test.ts | 67 ++++++++++++++++ 3 files changed, 151 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 9657dbb..8aa8506 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -1685,6 +1685,31 @@ export interface ProfilePromotionEvaluation { readonly diagnostics: readonly string[]; } +export type ProfileSelectionEnforcementMode = 'advisory' | 'required'; + +export interface ProfileSelectionRequirement { + readonly profile_id: string; + readonly promotion_policy_id: string; + readonly minimum_profile_status: ProfilePromotionStatus; + readonly enforcement_mode: ProfileSelectionEnforcementMode; +} + +export interface ProfileSelectionDecision { + readonly profile_id: string; + readonly promotion_policy_id: string; + readonly minimum_profile_status: ProfilePromotionStatus; + readonly evaluated_status: ProfilePromotionStatus; + readonly enforcement_mode: ProfileSelectionEnforcementMode; + readonly satisfied: boolean; + readonly enforced: boolean; + readonly allowed: boolean; + readonly rejection_code?: string; + readonly active_profile?: ActiveProfileView; + readonly profile_promotion_evaluation: ProfilePromotionEvaluation; + readonly blocking_reasons: readonly string[]; + readonly diagnostics: readonly string[]; +} + export function evaluateProfilePromotion( policy: ProfilePromotionPolicy, report: ProfilePromotionReport @@ -1711,6 +1736,61 @@ export function evaluateProfilePromotion( }; } +export function evaluateProfileSelectionRequirement( + requirement: ProfileSelectionRequirement, + activeProfile: ActiveProfileView | undefined, + evaluation: ProfilePromotionEvaluation +): ProfileSelectionDecision { + const satisfied = + profilePromotionStatusRank(evaluation.status) >= + profilePromotionStatusRank(requirement.minimum_profile_status) && + evaluation.status !== 'disabled'; + const enforced = requirement.enforcement_mode === 'required'; + const allowed = satisfied || !enforced; + const blockingReasons = [...evaluation.blocking_reasons]; + let rejectionCode = ''; + if (!satisfied) { + blockingReasons.unshift( + `profile status ${evaluation.status} is below required ${requirement.minimum_profile_status}` + ); + if (enforced) rejectionCode = 'profile_status_unmet'; + } + const diagnostics = [...evaluation.diagnostics]; + if (requirement.profile_id !== evaluation.profile_id) { + diagnostics.push('selected profile does not match promotion evaluation profile'); + } + return { + profile_id: requirement.profile_id, + promotion_policy_id: requirement.promotion_policy_id, + minimum_profile_status: requirement.minimum_profile_status, + evaluated_status: evaluation.status, + enforcement_mode: requirement.enforcement_mode, + satisfied, + enforced, + allowed, + rejection_code: rejectionCode, + active_profile: activeProfile, + profile_promotion_evaluation: evaluation, + blocking_reasons: blockingReasons, + diagnostics + }; +} + +function profilePromotionStatusRank(status: ProfilePromotionStatus): number { + switch (status) { + case 'experimental': + return 1; + case 'available': + return 2; + case 'recommended': + return 3; + case 'default': + return 4; + default: + return 0; + } +} + function profilePromotionBlockingReasons( entry: ProfilePromotionPolicyEntry, report: ProfilePromotionReport diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index ba4253c..e533375 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -170,6 +170,9 @@ export type { ProfilePromotionReport, ProfilePromotionScope, ProfilePromotionStatus, + ProfileSelectionDecision, + ProfileSelectionEnforcementMode, + ProfileSelectionRequirement, ProfileRecommendationGate, ProfileDefaultGate, ProfileValidationDiagnostic, @@ -602,6 +605,7 @@ export { summarizeNamedConformanceSuiteReports, summarizeConformanceResults, evaluateProfilePromotion, + evaluateProfileSelectionRequirement, initialProfilePromotionPolicy, promotionProfileGoImportDeclarations, promotionProfileJsonKeyedObject, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 4670cb6..95a8e2b 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -8,6 +8,7 @@ import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; import { evaluateProfilePromotion, + evaluateProfileSelectionRequirement, executeGenericConflictHandler, initialProfilePromotionPolicy, promotionProfileJsonKeyedObject, @@ -81,8 +82,10 @@ import type { PerformanceGuardrails, ProfileConformanceReport, ProfileDebugOutput, + ProfilePromotionEvaluation, ProfilePromotionPolicy, ProfilePromotionReport, + ProfileSelectionRequirement, ProfileValidationDiagnostic, RawMerge, RenameAwareMatchingReport, @@ -585,6 +588,30 @@ interface ProfilePromotionEvaluationFixture { }; } +interface ProfileSelectionEnforcementFixture { + active_profile: ActiveProfileView; + available_evaluation: ProfilePromotionEvaluation; + recommended_evaluation: ProfilePromotionEvaluation; + advisory_requirement: ProfileSelectionRequirement; + required_requirement: ProfileSelectionRequirement; + satisfied_requirement: ProfileSelectionRequirement; + expected: { + advisory_allowed: boolean; + advisory_satisfied: boolean; + advisory_enforced: boolean; + advisory_rejection_code: string; + required_allowed: boolean; + required_satisfied: boolean; + required_enforced: boolean; + required_rejection_code: string; + required_first_blocking_reason: string; + satisfied_allowed: boolean; + satisfied_satisfied: boolean; + satisfied_enforced: boolean; + satisfied_rejection_code: string; + }; +} + function sortedProfileValidationMessages( diagnostics: readonly ProfileValidationDiagnostic[] ): string[] { @@ -7220,6 +7247,46 @@ describe('ast-merge shared fixtures', () => { expect(unknown.status).toBe(fixture.expected.unknown_profile_status); }); + it('conforms to the slice-914 profile selection enforcement fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-914-profile-selection-enforcement', + 'profile-selection-enforcement.json' + ); + + const advisory = evaluateProfileSelectionRequirement( + fixture.advisory_requirement, + fixture.active_profile, + fixture.available_evaluation + ); + expect(advisory.allowed).toBe(fixture.expected.advisory_allowed); + expect(advisory.satisfied).toBe(fixture.expected.advisory_satisfied); + expect(advisory.enforced).toBe(fixture.expected.advisory_enforced); + expect(advisory.rejection_code).toBe(fixture.expected.advisory_rejection_code); + + const required = evaluateProfileSelectionRequirement( + fixture.required_requirement, + fixture.active_profile, + fixture.available_evaluation + ); + expect(required.allowed).toBe(fixture.expected.required_allowed); + expect(required.satisfied).toBe(fixture.expected.required_satisfied); + expect(required.enforced).toBe(fixture.expected.required_enforced); + expect(required.rejection_code).toBe(fixture.expected.required_rejection_code); + expect(required.blocking_reasons[0]).toBe(fixture.expected.required_first_blocking_reason); + + const satisfied = evaluateProfileSelectionRequirement( + fixture.satisfied_requirement, + fixture.active_profile, + fixture.recommended_evaluation + ); + expect(satisfied.allowed).toBe(fixture.expected.satisfied_allowed); + expect(satisfied.satisfied).toBe(fixture.expected.satisfied_satisfied); + expect(satisfied.enforced).toBe(fixture.expected.satisfied_enforced); + expect(satisfied.rejection_code).toBe(fixture.expected.satisfied_rejection_code); + expect(satisfied.blocking_reasons).toHaveLength(0); + }); + it('conforms to the template source path mapping fixture', () => { const manifest = readFixture( 'conformance', From cd644a737a5696ac5ff8d1c5d38cbf06cdf42503 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 08:42:29 -0600 Subject: [PATCH 083/130] Expose profile promotion transport fields --- packages/ast-merge/src/contracts.ts | 21 +++++++ packages/ast-merge/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 59 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 8aa8506..9100d58 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -2278,6 +2278,9 @@ export interface StructuredEditApplicationEnvelope { export interface StructuredEditRequestEnvelope { readonly kind: 'structured_edit_request'; readonly version: typeof STRUCTURED_EDIT_TRANSPORT_VERSION; + readonly profile_id?: string; + readonly minimum_profile_status?: ProfilePromotionStatus; + readonly promotion_policy_id?: string; readonly request: StructuredEditRequest; } @@ -2285,6 +2288,10 @@ export interface StructuredEditExecutionReport { readonly application: StructuredEditApplication; readonly providerFamily: string; readonly providerBackend?: string; + readonly active_profile?: ActiveProfileView; + readonly profile_promotion_evaluation?: ProfilePromotionEvaluation; + readonly profile_selection_decision?: ProfileSelectionDecision; + readonly profile_blocking_reasons?: readonly string[]; readonly diagnostics: readonly Diagnostic[]; readonly metadata?: Readonly>; } @@ -3655,6 +3662,20 @@ export function structuredEditRequestEnvelope( }; } +export function profileSelectionRequirementFromRequestEnvelope( + envelope: StructuredEditRequestEnvelope +): ProfileSelectionRequirement | undefined { + if (!envelope.profile_id && !envelope.minimum_profile_status && !envelope.promotion_policy_id) { + return undefined; + } + return { + profile_id: envelope.profile_id ?? '', + promotion_policy_id: envelope.promotion_policy_id ?? '', + minimum_profile_status: envelope.minimum_profile_status ?? 'available', + enforcement_mode: 'required' + }; +} + export function importStructuredEditRequestEnvelope(value: unknown): { request?: StructuredEditRequest; error?: StructuredEditTransportImportError; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index e533375..e55a06a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -607,6 +607,7 @@ export { evaluateProfilePromotion, evaluateProfileSelectionRequirement, initialProfilePromotionPolicy, + profileSelectionRequirementFromRequestEnvelope, promotionProfileGoImportDeclarations, promotionProfileJsonKeyedObject, promotionProfileRubyGemspecDependencyDeclarations, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 95a8e2b..4d0af6d 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -85,6 +85,7 @@ import type { ProfilePromotionEvaluation, ProfilePromotionPolicy, ProfilePromotionReport, + ProfileSelectionDecision, ProfileSelectionRequirement, ProfileValidationDiagnostic, RawMerge, @@ -227,6 +228,7 @@ import type { StructuredEditKettleJemPrimitiveGapReport, StructuredEditExecutionReport, StructuredEditExecutionReportEnvelope, + ProfileSelectionRequirement, PolicyReference, PolicySurface, Diagnostic, @@ -447,6 +449,7 @@ import { importStructuredEditProviderBatchExecutionHandoffEnvelope, importStructuredEditProviderBatchExecutionPlanEnvelope, importStructuredEditExecutionReportEnvelope, + profileSelectionRequirementFromRequestEnvelope, reviewReplayBundleInputs, reviewReplayBundleEnvelope, reviewedNestedExecution, @@ -1395,6 +1398,9 @@ interface StructuredEditRequestEnvelopeFixture { expected_envelope: { kind: StructuredEditRequestEnvelope['kind']; version: number; + profile_id?: string; + minimum_profile_status?: StructuredEditRequestEnvelope['minimum_profile_status']; + promotion_policy_id?: string; request: StructuredEditRequestFixture['cases'][number]['request']; }; } @@ -1428,6 +1434,10 @@ interface StructuredEditExecutionReportFixture { application: StructuredEditApplicationFixture['cases'][number]['application']; provider_family: string; provider_backend?: string | null; + active_profile?: ActiveProfileView; + profile_promotion_evaluation?: ProfilePromotionEvaluation; + profile_selection_decision?: ProfileSelectionDecision; + profile_blocking_reasons?: readonly string[]; diagnostics: DiagnosticFixture['diagnostics']; metadata?: Record; }; @@ -2672,6 +2682,18 @@ interface StructuredEditExecutionReportEnvelopeFixture { }; } +interface StructuredEditProfilePromotionEnvelopeFixture { + structured_edit_request: StructuredEditRequestFixture['cases'][number]['request']; + profile_selection_requirement: ProfileSelectionRequirement; + expected_request_envelope: StructuredEditRequestEnvelopeFixture['expected_envelope']; + structured_edit_execution_report: StructuredEditExecutionReportFixture['cases'][number]['report']; + expected_execution_report_envelope: StructuredEditExecutionReportEnvelopeFixture['expected_envelope']; + expected: { + rejection_code: string; + profile_blocking_reason_count: number; + }; +} + interface StructuredEditExecutionReportEnvelopeRejectionFixture { cases: Array<{ label: string; @@ -4349,6 +4371,9 @@ function normalizeStructuredEditRequestEnvelope( return { kind: raw.kind, version: STRUCTURED_EDIT_TRANSPORT_VERSION, + profile_id: raw.profile_id, + minimum_profile_status: raw.minimum_profile_status, + promotion_policy_id: raw.promotion_policy_id, request: normalizeStructuredEditRequest(raw.request) }; } @@ -4360,6 +4385,10 @@ function normalizeStructuredEditExecutionReport( application: normalizeStructuredEditApplication(raw.application), providerFamily: raw.provider_family, providerBackend: raw.provider_backend ?? undefined, + active_profile: raw.active_profile, + profile_promotion_evaluation: raw.profile_promotion_evaluation, + profile_selection_decision: raw.profile_selection_decision, + profile_blocking_reasons: raw.profile_blocking_reasons, diagnostics: raw.diagnostics.map((diagnostic) => normalizeDiagnostic(diagnostic)), metadata: raw.metadata }; @@ -11088,6 +11117,36 @@ describe('ast-merge shared fixtures', () => { } }); + it('conforms to the slice-915 structured-edit profile promotion envelope fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-915-structured-edit-profile-promotion-envelope', + 'structured-edit-profile-promotion-envelope.json' + ); + const request = normalizeStructuredEditRequest(fixture.structured_edit_request); + const requestEnvelope = { + ...structuredEditRequestEnvelope(request), + profile_id: fixture.expected_request_envelope.profile_id, + minimum_profile_status: fixture.expected_request_envelope.minimum_profile_status, + promotion_policy_id: fixture.expected_request_envelope.promotion_policy_id + }; + expect(requestEnvelope).toEqual( + normalizeStructuredEditRequestEnvelope(fixture.expected_request_envelope) + ); + expect(profileSelectionRequirementFromRequestEnvelope(requestEnvelope)).toEqual( + fixture.profile_selection_requirement + ); + + const report = normalizeStructuredEditExecutionReport(fixture.structured_edit_execution_report); + expect(structuredEditExecutionReportEnvelope(report)).toEqual( + normalizeStructuredEditExecutionReportEnvelope(fixture.expected_execution_report_envelope) + ); + expect(report.profile_selection_decision?.rejection_code).toBe(fixture.expected.rejection_code); + expect(report.profile_blocking_reasons).toHaveLength( + fixture.expected.profile_blocking_reason_count + ); + }); + it('conforms to the slice-438 structured-edit execution report fixture', () => { const fixture = readFixture( ...diagnosticsFixturePath('structured_edit_execution_report') From 4920679925d21ac7f2e98e42bc5c64d7150d604a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 08:46:16 -0600 Subject: [PATCH 084/130] Add profile promotion CLI flags --- packages/smorg-ts/src/cli.ts | 81 ++++++++++++++++++++++++++++-- packages/smorg-ts/test/cli.test.ts | 31 +++++++++++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 1a9cae1..8cf3781 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -2,7 +2,12 @@ import { readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; -import type { MergeResult } from '@structuredmerge/ast-merge'; +import { + evaluateProfileSelectionRequirement, + initialProfilePromotionPolicy, + promotionProfileJsonKeyedObject +} from '@structuredmerge/ast-merge'; +import type { MergeResult, ProfilePromotionStatus } from '@structuredmerge/ast-merge'; import { mergeGo } from '@structuredmerge/go-merge'; import { mergeJson } from '@structuredmerge/json-merge'; import { mergeText } from '@structuredmerge/plain-merge'; @@ -22,6 +27,9 @@ interface MergeDriverOptions { readonly fallback: string; readonly checkOnly: boolean; readonly exitCode: boolean; + readonly profileId?: string; + readonly profileReport: boolean; + readonly requireProfileStatus?: string; } interface DiffDriverOptions { @@ -55,7 +63,7 @@ export function run( const [command, ...rest] = args; switch (command) { case 'merge-driver': - return runMergeDriver(rest, stderr); + return runMergeDriver(rest, stdout, stderr); case 'diff-driver': return runDiffDriver(rest, stdout, stderr); case 'conflicts': @@ -91,10 +99,19 @@ function printUsage(out: Pick): void { function runMergeDriver( args: readonly string[], + stdout: Pick, stderr: Pick ): number { const options = parseMergeDriverOptions(args, stderr); if (!options) return exitUserError; + const profileExit = reportAndEnforceProfile( + options.profileId, + options.profileReport, + options.requireProfileStatus, + stdout, + stderr + ); + if (profileExit !== exitSuccess) return profileExit; let ancestorSource: string; let currentSource: string; @@ -147,6 +164,9 @@ function parseMergeDriverOptions( let fallback = 'full-file'; let checkOnly = false; let exitCode = false; + let profileId: string | undefined; + let profileReport = false; + let requireProfileStatus: string | undefined; const positionals: string[] = []; for (let index = 0; index < args.length; index += 1) { @@ -176,6 +196,15 @@ function parseMergeDriverOptions( case '--exit-code': exitCode = true; break; + case '--profile': + profileId = args[++index]; + break; + case '--profile-report': + profileReport = true; + break; + case '--require-profile-status': + requireProfileStatus = args[++index]; + break; case '--fallback': fallback = args[++index] ?? ''; break; @@ -204,7 +233,53 @@ function parseMergeDriverOptions( stderr.write(`unsupported fallback mode ${JSON.stringify(fallback)}\n`); return undefined; } - return { ancestor, current, other, pathName, output, strict, fallback, checkOnly, exitCode }; + return { + ancestor, + current, + other, + pathName, + output, + strict, + fallback, + checkOnly, + exitCode, + profileId, + profileReport, + requireProfileStatus + }; +} + +function reportAndEnforceProfile( + profileId: string | undefined, + profileReport: boolean, + requireStatus: string | undefined, + stdout: Pick, + stderr: Pick +): number { + if (!profileId && !profileReport && !requireStatus) return exitSuccess; + const selectedProfile = profileId ?? promotionProfileJsonKeyedObject; + const evaluation = { + profile_id: selectedProfile, + status: 'available' as ProfilePromotionStatus, + blocking_reasons: ['profile promotion evidence is not loaded by this CLI command'], + diagnostics: [] + }; + const decision = evaluateProfileSelectionRequirement( + { + profile_id: selectedProfile, + promotion_policy_id: initialProfilePromotionPolicy().policy_id, + minimum_profile_status: (requireStatus ?? 'available') as ProfilePromotionStatus, + enforcement_mode: requireStatus ? 'required' : 'advisory' + }, + undefined, + evaluation + ); + if (profileReport) stdout.write(`${JSON.stringify(decision)}\n`); + if (!decision.allowed) { + stderr.write(`${decision.blocking_reasons[0]}\n`); + return exitUserError; + } + return exitSuccess; } function runDiffDriver( diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 719ec5f..c56e440 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; -import { exitSuccess, exitUnresolvedConflict, run } from '../src/cli'; +import { exitSuccess, exitUnresolvedConflict, exitUserError, run } from '../src/cli'; function writer() { let output = ''; @@ -114,6 +114,35 @@ describe('smorg-ts cli', () => { expect(readFileSync(current, 'utf8')).not.toContain('"other":true'); }); + it('prints profile report and blocks unmet required profile status', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":"structuredmerge","current":true}'); + const other = write('other.json', '{"name":"structuredmerge","other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + [ + 'merge-driver', + '--profile', + 'json.keyed-object', + '--profile-report', + '--require-profile-status', + 'recommended', + ancestor, + current, + other, + 'package.json' + ], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUserError); + expect(stdout.output()).toContain('"rejection_code":"profile_status_unmet"'); + expect(stderr.output()).toContain('profile status available is below required recommended'); + }); + it('supports diff-driver git arities', () => { for (const argumentCount of [7, 9]) { const oldPath = write(`old-${argumentCount}.json`, '{"old":true}'); From 9f479439e04b8632c5d1870aace331819fea3d50 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 08:48:38 -0600 Subject: [PATCH 085/130] Support profile promotion git attributes --- packages/smorg-ts/src/cli.ts | 22 ++++++++++++++-------- packages/smorg-ts/test/cli.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 8cf3781..2f23dfe 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -47,6 +47,8 @@ interface ConflictDiffOptions { interface PathSettings { language?: string; conflictMarkerSize: number; + profileId?: string; + requireProfileStatus?: string; } interface ConflictRegion { @@ -104,14 +106,6 @@ function runMergeDriver( ): number { const options = parseMergeDriverOptions(args, stderr); if (!options) return exitUserError; - const profileExit = reportAndEnforceProfile( - options.profileId, - options.profileReport, - options.requireProfileStatus, - stdout, - stderr - ); - if (profileExit !== exitSuccess) return profileExit; let ancestorSource: string; let currentSource: string; @@ -128,6 +122,14 @@ function runMergeDriver( const effectivePath = options.pathName ?? options.current; const settings = loadPathSettings(effectivePath); + const profileExit = reportAndEnforceProfile( + options.profileId ?? settings.profileId, + options.profileReport, + options.requireProfileStatus ?? settings.requireProfileStatus, + stdout, + stderr + ); + if (profileExit !== exitSuccess) return profileExit; const result = mergeByPath(effectivePath, settings.language, otherSource, currentSource); let output = result.output; if (!result.ok || output === undefined) { @@ -515,6 +517,10 @@ function applyAttributes(settings: PathSettings, pathName: string, source: strin if (!value) continue; if (key === 'smorg.language' || key === 'linguist-language') { settings.language = value; + } else if (key === 'smorg.profile') { + settings.profileId = value; + } else if (key === 'smorg.requireProfileStatus') { + settings.requireProfileStatus = value; } else if (key === 'conflict-marker-size') { const markerSize = Number.parseInt(value, 10); if (markerSize > 0) settings.conflictMarkerSize = markerSize; diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index c56e440..93d645a 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -143,6 +143,28 @@ describe('smorg-ts cli', () => { expect(stderr.output()).toContain('profile status available is below required recommended'); }); + it('uses smorg profile attributes', () => { + writeFileSync( + '.gitattributes', + '*.json smorg.profile=json.keyed-object smorg.requireProfileStatus=recommended\n' + ); + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":"structuredmerge","current":true}'); + const other = write('other.json', '{"name":"structuredmerge","other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--profile-report', ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUserError); + expect(stdout.output()).toContain('"profile_id":"json.keyed-object"'); + expect(stdout.output()).toContain('"rejection_code":"profile_status_unmet"'); + }); + it('supports diff-driver git arities', () => { for (const argumentCount of [7, 9]) { const oldPath = write(`old-${argumentCount}.json`, '{"old":true}'); From fd2f2c8f34e6add365d7d6afc19bc4b1e2b1df32 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:13:25 -0600 Subject: [PATCH 086/130] Add ast-crispr package scaffold --- packages/ast-crispr/package.json | 20 +++++ packages/ast-crispr/src/index.ts | 81 +++++++++++++++++++ packages/ast-crispr/test/boundary.test.ts | 28 +++++++ .../test/fixtures.integration.test.ts | 1 - pnpm-lock.yaml | 21 +++++ tsconfig.json | 1 + 6 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 packages/ast-crispr/package.json create mode 100644 packages/ast-crispr/src/index.ts create mode 100644 packages/ast-crispr/test/boundary.test.ts diff --git a/packages/ast-crispr/package.json b/packages/ast-crispr/package.json new file mode 100644 index 0000000..d98fb3b --- /dev/null +++ b/packages/ast-crispr/package.json @@ -0,0 +1,20 @@ +{ + "name": "@structuredmerge/ast-crispr", + "version": "0.2.0", + "private": false, + "type": "module", + "main": "./src/index.ts", + "author": "Peter H. Boling", + "homepage": "https://structuredmerge.org", + "repository": { + "type": "git", + "url": "git+https://github.com/structuredmerge/structuredmerge-typescript.git" + }, + "bugs": { + "url": "https://github.com/structuredmerge/structuredmerge-typescript/issues" + }, + "dependencies": { + "@structuredmerge/ast-merge": "workspace:*" + } +} + diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts new file mode 100644 index 0000000..124c346 --- /dev/null +++ b/packages/ast-crispr/src/index.ts @@ -0,0 +1,81 @@ +import type { StructuredEditCrisprExampleParityReport } from '@structuredmerge/ast-merge'; + +export const packageName = '@structuredmerge/ast-crispr'; + +export function astMergeContractAnchor(): string { + const _anchor: StructuredEditCrisprExampleParityReport | null = null; + void _anchor; + return 'StructuredEditCrisprExampleParityReport'; +} + +export function boundaryReport(): Readonly> { + return { + package: 'ast-crispr', + layer: 'structural_edit_tool', + status: 'active_thin_package', + base_contract_package: 'ast-merge', + relationship: { + ast_merge: [ + 'owns portable structured-edit envelope contracts', + 'owns transport, report, replay, review, and provider handoff vocabulary', + 'remains the substrate for provider-neutral fixtures' + ], + ast_crispr: [ + 'owns ergonomic structural-edit selectors, profiles, and operation helpers', + 'wraps ast-merge contracts instead of forking them', + 'may grow compatibility helpers for old ast-crispr concepts after fixture-backed review' + ], + provider_packages: [ + 'own parser-specific execution and metadata projection', + 'may expose provider adapters consumed by ast-crispr', + 'keep raw parser details behind normalized tree metadata or semantic sidecars' + ], + ast_template: [ + 'orchestrates template and directory workflows', + 'invokes structural edits through ast-merge or ast-crispr registries/envelopes', + 'does not own parser-specific selectors' + ] + }, + implementations: [ + { + language: 'go', + package_name: 'astcrispr', + import: 'github.com/structuredmerge/structuredmerge-go/astcrispr' + }, + { + language: 'ruby', + package_name: 'ast-crispr', + require: 'ast/crispr' + }, + { + language: 'rust', + package_name: 'ast-crispr', + crate: 'ast_crispr' + }, + { + language: 'typescript', + package_name: '@structuredmerge/ast-crispr', + import: '@structuredmerge/ast-crispr' + } + ], + initial_exports: [ + 'package identity', + 'boundary report', + 'ast-merge structured-edit contract anchor' + ], + future_exports: [ + 'limit helpers', + 'match profile helpers', + 'selection profile helpers', + 'destination profile helpers', + 'operation profile helpers', + 'replace/delete/insert/move helpers', + 'batch operation helpers' + ], + metadata: { + source: 'legacy_crispr_reference', + decision: + 'Keep ast-merge as the base contract layer and revive ast-crispr as a separate thin package in every implementation.' + } + }; +} diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts new file mode 100644 index 0000000..7d1b922 --- /dev/null +++ b/packages/ast-crispr/test/boundary.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { astMergeContractAnchor, boundaryReport } from '../src/index'; + +interface BoundaryFixture { + boundary: Readonly>; +} + +function readFixture(): BoundaryFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-916-ast-crispr-package-boundary', + 'ast-crispr-package-boundary.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as BoundaryFixture; +} + +describe('@structuredmerge/ast-crispr', () => { + it('conforms to the package boundary fixture', () => { + expect(boundaryReport()).toEqual(readFixture().boundary); + expect(astMergeContractAnchor()).toBe('StructuredEditCrisprExampleParityReport'); + }); +}); diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 4d0af6d..0446be2 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -228,7 +228,6 @@ import type { StructuredEditKettleJemPrimitiveGapReport, StructuredEditExecutionReport, StructuredEditExecutionReportEnvelope, - ProfileSelectionRequirement, PolicyReference, PolicySurface, Diagnostic, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20ec660..a6f711b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,12 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/node@24.12.2)(yaml@2.8.3) + packages/ast-crispr: + dependencies: + '@structuredmerge/ast-merge': + specifier: workspace:* + version: link:../ast-merge + packages/ast-merge: devDependencies: '@structuredmerge/markdown-merge': @@ -174,6 +180,21 @@ importers: specifier: workspace:* version: link:../tree-haver + packages/smorg-ts: + dependencies: + '@structuredmerge/ast-merge': + specifier: workspace:* + version: link:../ast-merge + '@structuredmerge/go-merge': + specifier: workspace:* + version: link:../go-merge + '@structuredmerge/json-merge': + specifier: workspace:* + version: link:../json-merge + '@structuredmerge/plain-merge': + specifier: workspace:* + version: link:../plain-merge + packages/toml-merge: dependencies: '@structuredmerge/ast-merge': diff --git a/tsconfig.json b/tsconfig.json index b966149..595ab0e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "noEmit": true, "paths": { "@structuredmerge/ast-merge": ["./packages/ast-merge/src/index.ts"], + "@structuredmerge/ast-crispr": ["./packages/ast-crispr/src/index.ts"], "@structuredmerge/tree-haver": ["./packages/tree-haver/src/index.ts"], "@structuredmerge/plain-merge": ["./packages/plain-merge/src/index.ts"], "@structuredmerge/json-merge": ["./packages/json-merge/src/index.ts"], From f44490060a5d4141e1cc1f98c1eb5b4912a400a2 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:30:47 -0600 Subject: [PATCH 087/130] Add ast-crispr limit helpers --- packages/ast-crispr/src/index.ts | 120 +++++++++++++++++++++- packages/ast-crispr/test/boundary.test.ts | 50 ++++++++- 2 files changed, 167 insertions(+), 3 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index 124c346..bb89be1 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -2,6 +2,122 @@ import type { StructuredEditCrisprExampleParityReport } from '@structuredmerge/a export const packageName = '@structuredmerge/ast-crispr'; +export class AstCrisprError extends Error { + readonly code: string; + + constructor(message: string, code: string) { + super(message); + this.name = 'AstCrisprError'; + this.code = code; + } +} + +type Predicate = (count: number) => boolean; + +interface LimitConstraint { + readonly description: string; + readonly predicate: Predicate; +} + +export class Limit { + private readonly constraints: readonly LimitConstraint[]; + + constructor(spec: unknown = null) { + this.constraints = normalizeLimit(spec === null || spec === undefined ? { exactly: 1 } : spec); + } + + allows(count: number): boolean { + return this.constraints.every((constraint) => constraint.predicate(count)); + } + + describe(): string { + return this.constraints.map((constraint) => constraint.description).join(' and '); + } +} + +export function limit(spec: unknown = null): Limit { + return spec instanceof Limit ? spec : new Limit(spec); +} + +function normalizeLimit(spec: unknown): readonly LimitConstraint[] { + if (spec instanceof Limit) { + return normalizeLimit({ exactly: 1 }); + } + if (Array.isArray(spec)) { + return spec.flatMap((entry) => normalizeLimit(entry)); + } + if (typeof spec === 'string') { + return [constraintForOperator(spec)]; + } + if (spec && typeof spec === 'object') { + return normalizeLimitRecord(spec as Readonly>); + } + throw new AstCrisprError( + 'Unsupported ast-crispr limit specification', + 'ast_crispr_limit_unsupported' + ); +} + +function normalizeLimitRecord(spec: Readonly>): readonly LimitConstraint[] { + const constraints: LimitConstraint[] = []; + if (typeof spec.exactly === 'number') { + const value = spec.exactly; + constraints.push({ description: `== ${value}`, predicate: (count) => count === value }); + } + if (typeof spec.at_most === 'number') { + const value = spec.at_most; + constraints.push({ description: `<= ${value}`, predicate: (count) => count <= value }); + } + if (typeof spec.at_least === 'number') { + const value = spec.at_least; + constraints.push({ description: `>= ${value}`, predicate: (count) => count >= value }); + } + if (spec.none_or_one === true) { + constraints.push({ description: '<= 1', predicate: (count) => count <= 1 }); + } + if (constraints.length === 0) { + throw new AstCrisprError( + 'ast-crispr limit must define at least one constraint', + 'ast_crispr_limit_empty' + ); + } + + return constraints; +} + +function constraintForOperator(spec: string): LimitConstraint { + const match = /^(==|!=|<=|>=|<|>)\s*(\d+)$/u.exec(spec.trim()); + if (!match) { + throw new AstCrisprError( + 'Invalid ast-crispr limit expression', + 'ast_crispr_limit_invalid_expression' + ); + } + const operator = match[1]; + const value = Number.parseInt(match[2], 10); + return { + description: `${operator} ${value}`, + predicate: (count) => { + switch (operator) { + case '==': + return count === value; + case '!=': + return count !== value; + case '<=': + return count <= value; + case '>=': + return count >= value; + case '<': + return count < value; + case '>': + return count > value; + default: + return false; + } + } + }; +} + export function astMergeContractAnchor(): string { const _anchor: StructuredEditCrisprExampleParityReport | null = null; void _anchor; @@ -61,10 +177,10 @@ export function boundaryReport(): Readonly> { initial_exports: [ 'package identity', 'boundary report', - 'ast-merge structured-edit contract anchor' + 'ast-merge structured-edit contract anchor', + 'limit helpers' ], future_exports: [ - 'limit helpers', 'match profile helpers', 'selection profile helpers', 'destination profile helpers', diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index 7d1b922..b30a88e 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { astMergeContractAnchor, boundaryReport } from '../src/index'; +import { AstCrisprError, Limit, astMergeContractAnchor, boundaryReport } from '../src/index'; interface BoundaryFixture { boundary: Readonly>; @@ -20,9 +20,57 @@ function readFixture(): BoundaryFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as BoundaryFixture; } +interface LimitFixture { + cases: Array<{ + name: string; + spec: unknown; + expected_description: string; + expectations: Array<{ count: number; allowed: boolean }>; + }>; + invalid_cases: Array<{ + name: string; + spec: unknown; + expected_error: string; + }>; +} + +function readLimitFixture(): LimitFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-917-ast-crispr-limit-helpers', + 'ast-crispr-limit-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as LimitFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); expect(astMergeContractAnchor()).toBe('StructuredEditCrisprExampleParityReport'); }); + + it('conforms to the limit helper fixture', () => { + const fixture = readLimitFixture(); + + for (const testCase of fixture.cases) { + const limit = new Limit(testCase.spec); + expect(limit.describe()).toBe(testCase.expected_description); + for (const expectation of testCase.expectations) { + expect(limit.allows(expectation.count)).toBe(expectation.allowed); + } + } + + for (const testCase of fixture.invalid_cases) { + expect(() => new Limit(testCase.spec)).toThrow(AstCrisprError); + try { + new Limit(testCase.spec); + } catch (error) { + expect((error as AstCrisprError).code).toBe(testCase.expected_error); + } + } + }); }); From db38552e53b041c8f73c77310c7b462eddfb61db Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:34:27 -0600 Subject: [PATCH 088/130] Add ast-crispr match profile helpers --- packages/ast-crispr/src/index.ts | 81 ++++++++++++++++++++++- packages/ast-crispr/test/boundary.test.ts | 41 +++++++++++- 2 files changed, 119 insertions(+), 3 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index bb89be1..7fb4941 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -35,6 +35,83 @@ export class Limit { } } +export interface MatchProfileReport { + readonly start_boundary: string; + readonly start_boundary_family: string; + readonly known_start_boundary: boolean; + readonly end_boundary: string; + readonly end_boundary_family: string; + readonly known_end_boundary: boolean; + readonly payload_kind: string; + readonly payload_family: string; + readonly known_payload_kind: boolean; + readonly comment_anchored: boolean; + readonly trailing_gap_extended: boolean; +} + +interface ProfileDescriptor { + readonly family: string; +} + +const KNOWN_START_BOUNDARIES: Readonly> = { + owner_start: { family: 'structural_owner' }, + comment_region_start: { family: 'comment_anchor' } +}; + +const KNOWN_END_BOUNDARIES: Readonly> = { + owner_end: { family: 'structural_owner' }, + owner_end_plus_trailing_gap: { family: 'gap_extension' } +}; + +const KNOWN_PAYLOAD_KINDS: Readonly> = { + structural_owner_body: { family: 'owner_body' }, + comment_owned_body: { family: 'comment_owned' }, + section_branch: { family: 'section_branch' } +}; + +export class MatchProfile { + readonly startBoundary: string; + readonly endBoundary: string; + readonly payloadKind: string; + + constructor({ + start_boundary = 'owner_start', + end_boundary = 'owner_end', + payload_kind = 'structural_owner_body' + }: { + readonly start_boundary?: string; + readonly end_boundary?: string; + readonly payload_kind?: string; + } = {}) { + this.startBoundary = start_boundary; + this.endBoundary = end_boundary; + this.payloadKind = payload_kind; + } + + report(): MatchProfileReport { + const startFamily = KNOWN_START_BOUNDARIES[this.startBoundary]?.family ?? 'unknown'; + const endFamily = KNOWN_END_BOUNDARIES[this.endBoundary]?.family ?? 'unknown'; + const payloadFamily = KNOWN_PAYLOAD_KINDS[this.payloadKind]?.family ?? 'unknown'; + return { + start_boundary: this.startBoundary, + start_boundary_family: startFamily, + known_start_boundary: this.startBoundary in KNOWN_START_BOUNDARIES, + end_boundary: this.endBoundary, + end_boundary_family: endFamily, + known_end_boundary: this.endBoundary in KNOWN_END_BOUNDARIES, + payload_kind: this.payloadKind, + payload_family: payloadFamily, + known_payload_kind: this.payloadKind in KNOWN_PAYLOAD_KINDS, + comment_anchored: startFamily === 'comment_anchor' || payloadFamily === 'comment_owned', + trailing_gap_extended: endFamily === 'gap_extension' + }; + } +} + +export function matchProfile(profile: ConstructorParameters[0]): MatchProfile { + return new MatchProfile(profile); +} + export function limit(spec: unknown = null): Limit { return spec instanceof Limit ? spec : new Limit(spec); } @@ -178,10 +255,10 @@ export function boundaryReport(): Readonly> { 'package identity', 'boundary report', 'ast-merge structured-edit contract anchor', - 'limit helpers' + 'limit helpers', + 'match profile helpers' ], future_exports: [ - 'match profile helpers', 'selection profile helpers', 'destination profile helpers', 'operation profile helpers', diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index b30a88e..ee21e7a 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -1,7 +1,13 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { AstCrisprError, Limit, astMergeContractAnchor, boundaryReport } from '../src/index'; +import { + AstCrisprError, + Limit, + MatchProfile, + astMergeContractAnchor, + boundaryReport +} from '../src/index'; interface BoundaryFixture { boundary: Readonly>; @@ -47,6 +53,31 @@ function readLimitFixture(): LimitFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as LimitFixture; } +interface MatchProfileFixture { + cases: Array<{ + name: string; + profile: { + start_boundary: string; + end_boundary: string; + payload_kind: string; + }; + expected: Readonly>; + }>; +} + +function readMatchProfileFixture(): MatchProfileFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-918-ast-crispr-match-profile-helpers', + 'ast-crispr-match-profile-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as MatchProfileFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -73,4 +104,12 @@ describe('@structuredmerge/ast-crispr', () => { } } }); + + it('conforms to the match profile helper fixture', () => { + const fixture = readMatchProfileFixture(); + + for (const testCase of fixture.cases) { + expect(new MatchProfile(testCase.profile).report()).toEqual(testCase.expected); + } + }); }); From 6a585cc6580a4b44c6d7aca3d09423d2801b69ce Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:38:35 -0600 Subject: [PATCH 089/130] Add ast-crispr selection profile helpers --- packages/ast-crispr/src/index.ts | 116 +++++++++++++++++++++- packages/ast-crispr/test/boundary.test.ts | 30 ++++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index 7fb4941..57430be 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -49,6 +49,24 @@ export interface MatchProfileReport { readonly trailing_gap_extended: boolean; } +export interface SelectionProfileReport { + readonly owner_scope: string; + readonly owner_selector: string; + readonly owner_selector_family: string; + readonly known_owner_selector: boolean; + readonly selector_kind: string; + readonly selector_kind_family: string; + readonly known_selector_kind: boolean; + readonly selection_intent: string; + readonly selection_intent_family: string; + readonly known_selection_intent: boolean; + readonly comment_region: string | null; + readonly comment_region_family: string; + readonly known_comment_region: boolean; + readonly comment_anchored: boolean; + readonly include_trailing_gap: boolean; +} + interface ProfileDescriptor { readonly family: string; } @@ -69,6 +87,29 @@ const KNOWN_PAYLOAD_KINDS: Readonly> = { section_branch: { family: 'section_branch' } }; +const KNOWN_OWNER_SELECTORS: Readonly> = { + line_bound_statements: { family: 'line_oriented' }, + heading_sections: { family: 'section' } +}; + +const KNOWN_SELECTOR_KINDS: Readonly> = { + owner_filter: { family: 'owner_filter' }, + comment_region_owner: { family: 'comment_anchor' }, + heading_section: { family: 'section_branch' } +}; + +const KNOWN_SELECTION_INTENTS: Readonly> = { + predicate_filter: { family: 'predicate' }, + comment_region_filter: { family: 'comment' }, + section_heading: { family: 'section' } +}; + +const KNOWN_COMMENT_REGIONS: Readonly> = { + leading: { family: 'leading' }, + trailing: { family: 'trailing' }, + inline: { family: 'inline' } +}; + export class MatchProfile { readonly startBoundary: string; readonly endBoundary: string; @@ -108,10 +149,81 @@ export class MatchProfile { } } +export class SelectionProfile { + readonly ownerScope: string; + readonly ownerSelector: string; + readonly selectorKind: string; + readonly selectionIntent: string; + readonly commentRegion: string | null; + readonly includeTrailingGap: boolean; + + constructor({ + owner_scope = 'shared_default', + owner_selector = 'line_bound_statements', + selector_kind = 'owner_filter', + selection_intent = 'predicate_filter', + comment_region = null, + include_trailing_gap = false + }: { + readonly owner_scope?: string; + readonly owner_selector?: string; + readonly selector_kind?: string; + readonly selection_intent?: string; + readonly comment_region?: string | null; + readonly include_trailing_gap?: boolean; + } = {}) { + this.ownerScope = owner_scope; + this.ownerSelector = owner_selector; + this.selectorKind = selector_kind; + this.selectionIntent = selection_intent; + this.commentRegion = comment_region; + this.includeTrailingGap = include_trailing_gap; + } + + report(): SelectionProfileReport { + const ownerSelectorFamily = KNOWN_OWNER_SELECTORS[this.ownerSelector]?.family ?? 'unknown'; + const selectorKindFamily = KNOWN_SELECTOR_KINDS[this.selectorKind]?.family ?? 'unknown'; + const selectionIntentFamily = + KNOWN_SELECTION_INTENTS[this.selectionIntent]?.family ?? 'unknown'; + const commentRegionFamily = + this.commentRegion === null + ? 'none' + : (KNOWN_COMMENT_REGIONS[this.commentRegion]?.family ?? 'unknown'); + const knownCommentRegion = + this.commentRegion !== null && this.commentRegion in KNOWN_COMMENT_REGIONS; + return { + owner_scope: this.ownerScope, + owner_selector: this.ownerSelector, + owner_selector_family: ownerSelectorFamily, + known_owner_selector: this.ownerSelector in KNOWN_OWNER_SELECTORS, + selector_kind: this.selectorKind, + selector_kind_family: selectorKindFamily, + known_selector_kind: this.selectorKind in KNOWN_SELECTOR_KINDS, + selection_intent: this.selectionIntent, + selection_intent_family: selectionIntentFamily, + known_selection_intent: this.selectionIntent in KNOWN_SELECTION_INTENTS, + comment_region: this.commentRegion, + comment_region_family: commentRegionFamily, + known_comment_region: knownCommentRegion, + comment_anchored: + selectorKindFamily === 'comment_anchor' || + selectionIntentFamily === 'comment' || + knownCommentRegion, + include_trailing_gap: this.includeTrailingGap + }; + } +} + export function matchProfile(profile: ConstructorParameters[0]): MatchProfile { return new MatchProfile(profile); } +export function selectionProfile( + profile: ConstructorParameters[0] +): SelectionProfile { + return new SelectionProfile(profile); +} + export function limit(spec: unknown = null): Limit { return spec instanceof Limit ? spec : new Limit(spec); } @@ -256,10 +368,10 @@ export function boundaryReport(): Readonly> { 'boundary report', 'ast-merge structured-edit contract anchor', 'limit helpers', - 'match profile helpers' + 'match profile helpers', + 'selection profile helpers' ], future_exports: [ - 'selection profile helpers', 'destination profile helpers', 'operation profile helpers', 'replace/delete/insert/move helpers', diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index ee21e7a..a3015c3 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -5,6 +5,7 @@ import { AstCrisprError, Limit, MatchProfile, + SelectionProfile, astMergeContractAnchor, boundaryReport } from '../src/index'; @@ -78,6 +79,27 @@ function readMatchProfileFixture(): MatchProfileFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as MatchProfileFixture; } +interface SelectionProfileFixture { + cases: Array<{ + name: string; + profile: ConstructorParameters[0]; + expected: Readonly>; + }>; +} + +function readSelectionProfileFixture(): SelectionProfileFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-919-ast-crispr-selection-profile-helpers', + 'ast-crispr-selection-profile-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as SelectionProfileFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -112,4 +134,12 @@ describe('@structuredmerge/ast-crispr', () => { expect(new MatchProfile(testCase.profile).report()).toEqual(testCase.expected); } }); + + it('conforms to the selection profile helper fixture', () => { + const fixture = readSelectionProfileFixture(); + + for (const testCase of fixture.cases) { + expect(new SelectionProfile(testCase.profile).report()).toEqual(testCase.expected); + } + }); }); From c304205bf9c773a03aa5cbec51ddcf1a0e0e6085 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:41:59 -0600 Subject: [PATCH 090/130] Add ast-crispr destination profile helpers --- packages/ast-crispr/src/index.ts | 86 ++++++++++++++++++++++- packages/ast-crispr/test/boundary.test.ts | 30 ++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index 57430be..3186c1f 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -67,6 +67,21 @@ export interface SelectionProfileReport { readonly include_trailing_gap: boolean; } +export interface DestinationProfileReport { + readonly resolution_kind: string; + readonly resolution_family: string; + readonly known_resolution_kind: boolean; + readonly resolution_source: string; + readonly resolution_source_family: string; + readonly known_resolution_source: boolean; + readonly anchor_boundary: string; + readonly anchor_boundary_family: string; + readonly known_anchor_boundary: boolean; + readonly used_if_missing: boolean; + readonly append_fallback: boolean; + readonly anchored: boolean; +} + interface ProfileDescriptor { readonly family: string; } @@ -110,6 +125,22 @@ const KNOWN_COMMENT_REGIONS: Readonly> = { inline: { family: 'inline' } }; +const KNOWN_RESOLUTION_KINDS: Readonly> = { + append_fallback: { family: 'append' }, + anchor_after_statement: { family: 'anchored' } +}; + +const KNOWN_RESOLUTION_SOURCES: Readonly> = { + none: { family: 'implicit' }, + callable: { family: 'callable' }, + selector: { family: 'selector' } +}; + +const KNOWN_ANCHOR_BOUNDARIES: Readonly> = { + none: { family: 'none' }, + statement_end_plus_following_gap: { family: 'gap_preserving_statement' } +}; + export class MatchProfile { readonly startBoundary: string; readonly endBoundary: string; @@ -214,10 +245,61 @@ export class SelectionProfile { } } +export class DestinationProfile { + readonly resolutionKind: string; + readonly resolutionSource: string; + readonly anchorBoundary: string; + readonly usedIfMissing: boolean; + + constructor({ + resolution_kind = 'append_fallback', + resolution_source = 'none', + anchor_boundary = 'none', + used_if_missing = false + }: { + readonly resolution_kind?: string; + readonly resolution_source?: string; + readonly anchor_boundary?: string; + readonly used_if_missing?: boolean; + } = {}) { + this.resolutionKind = resolution_kind; + this.resolutionSource = resolution_source; + this.anchorBoundary = anchor_boundary; + this.usedIfMissing = used_if_missing; + } + + report(): DestinationProfileReport { + const resolutionFamily = KNOWN_RESOLUTION_KINDS[this.resolutionKind]?.family ?? 'unknown'; + const resolutionSourceFamily = + KNOWN_RESOLUTION_SOURCES[this.resolutionSource]?.family ?? 'unknown'; + const anchorBoundaryFamily = KNOWN_ANCHOR_BOUNDARIES[this.anchorBoundary]?.family ?? 'unknown'; + return { + resolution_kind: this.resolutionKind, + resolution_family: resolutionFamily, + known_resolution_kind: this.resolutionKind in KNOWN_RESOLUTION_KINDS, + resolution_source: this.resolutionSource, + resolution_source_family: resolutionSourceFamily, + known_resolution_source: this.resolutionSource in KNOWN_RESOLUTION_SOURCES, + anchor_boundary: this.anchorBoundary, + anchor_boundary_family: anchorBoundaryFamily, + known_anchor_boundary: this.anchorBoundary in KNOWN_ANCHOR_BOUNDARIES, + used_if_missing: this.usedIfMissing, + append_fallback: this.resolutionKind === 'append_fallback', + anchored: resolutionFamily === 'anchored' + }; + } +} + export function matchProfile(profile: ConstructorParameters[0]): MatchProfile { return new MatchProfile(profile); } +export function destinationProfile( + profile: ConstructorParameters[0] +): DestinationProfile { + return new DestinationProfile(profile); +} + export function selectionProfile( profile: ConstructorParameters[0] ): SelectionProfile { @@ -369,10 +451,10 @@ export function boundaryReport(): Readonly> { 'ast-merge structured-edit contract anchor', 'limit helpers', 'match profile helpers', - 'selection profile helpers' + 'selection profile helpers', + 'destination profile helpers' ], future_exports: [ - 'destination profile helpers', 'operation profile helpers', 'replace/delete/insert/move helpers', 'batch operation helpers' diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index a3015c3..564d639 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; import { AstCrisprError, + DestinationProfile, Limit, MatchProfile, SelectionProfile, @@ -100,6 +101,27 @@ function readSelectionProfileFixture(): SelectionProfileFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as SelectionProfileFixture; } +interface DestinationProfileFixture { + cases: Array<{ + name: string; + profile: ConstructorParameters[0]; + expected: Readonly>; + }>; +} + +function readDestinationProfileFixture(): DestinationProfileFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-920-ast-crispr-destination-profile-helpers', + 'ast-crispr-destination-profile-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as DestinationProfileFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -142,4 +164,12 @@ describe('@structuredmerge/ast-crispr', () => { expect(new SelectionProfile(testCase.profile).report()).toEqual(testCase.expected); } }); + + it('conforms to the destination profile helper fixture', () => { + const fixture = readDestinationProfileFixture(); + + for (const testCase of fixture.cases) { + expect(new DestinationProfile(testCase.profile).report()).toEqual(testCase.expected); + } + }); }); From 6e5b1e709498e4813df776b6397d1df6992c4bdf Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:45:03 -0600 Subject: [PATCH 091/130] Add ast-crispr operation profile helpers --- packages/ast-crispr/src/index.ts | 100 ++++++++++++++++++++-- packages/ast-crispr/test/boundary.test.ts | 30 +++++++ 2 files changed, 124 insertions(+), 6 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index 3186c1f..ca643a4 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -82,6 +82,26 @@ export interface DestinationProfileReport { readonly anchored: boolean; } +export interface OperationProfileReport { + readonly operation_kind: string; + readonly operation_family: string; + readonly known_operation_kind: boolean; + readonly source_requirement: string; + readonly known_source_requirement: boolean; + readonly destination_requirement: string; + readonly known_destination_requirement: boolean; + readonly replacement_source: string; + readonly known_replacement_source: boolean; + readonly captures_source_text: boolean; + readonly supports_if_missing: boolean; + readonly selects_source: boolean; + readonly requires_source: boolean; + readonly supports_destination: boolean; + readonly requires_destination: boolean; + readonly explicit_replacement: boolean; + readonly may_reuse_captured_text: boolean; +} + interface ProfileDescriptor { readonly family: string; } @@ -141,6 +161,16 @@ const KNOWN_ANCHOR_BOUNDARIES: Readonly> = { statement_end_plus_following_gap: { family: 'gap_preserving_statement' } }; +const KNOWN_OPERATION_KINDS: Readonly> = { + replace: { family: 'rewrite' }, + delete: { family: 'removal' }, + insert: { family: 'insertion' }, + move: { family: 'relocation' } +}; + +const KNOWN_REQUIREMENTS = new Set(['none', 'optional', 'required']); +const KNOWN_REPLACEMENT_SOURCES = new Set(['none', 'explicit_text', 'captured_text_or_explicit']); + export class MatchProfile { readonly startBoundary: string; readonly endBoundary: string; @@ -290,6 +320,61 @@ export class DestinationProfile { } } +export class OperationProfile { + readonly operationKind: string; + readonly sourceRequirement: string; + readonly destinationRequirement: string; + readonly replacementSource: string; + readonly capturesSourceText: boolean; + readonly supportsIfMissing: boolean; + + constructor({ + operation_kind = 'replace', + source_requirement = 'required', + destination_requirement = 'none', + replacement_source = 'explicit_text', + captures_source_text = false, + supports_if_missing = false + }: { + readonly operation_kind?: string; + readonly source_requirement?: string; + readonly destination_requirement?: string; + readonly replacement_source?: string; + readonly captures_source_text?: boolean; + readonly supports_if_missing?: boolean; + } = {}) { + this.operationKind = operation_kind; + this.sourceRequirement = source_requirement; + this.destinationRequirement = destination_requirement; + this.replacementSource = replacement_source; + this.capturesSourceText = captures_source_text; + this.supportsIfMissing = supports_if_missing; + } + + report(): OperationProfileReport { + const operationFamily = KNOWN_OPERATION_KINDS[this.operationKind]?.family ?? 'unknown'; + return { + operation_kind: this.operationKind, + operation_family: operationFamily, + known_operation_kind: this.operationKind in KNOWN_OPERATION_KINDS, + source_requirement: this.sourceRequirement, + known_source_requirement: KNOWN_REQUIREMENTS.has(this.sourceRequirement), + destination_requirement: this.destinationRequirement, + known_destination_requirement: KNOWN_REQUIREMENTS.has(this.destinationRequirement), + replacement_source: this.replacementSource, + known_replacement_source: KNOWN_REPLACEMENT_SOURCES.has(this.replacementSource), + captures_source_text: this.capturesSourceText, + supports_if_missing: this.supportsIfMissing, + selects_source: this.sourceRequirement !== 'none', + requires_source: this.sourceRequirement === 'required', + supports_destination: this.destinationRequirement !== 'none', + requires_destination: this.destinationRequirement === 'required', + explicit_replacement: this.replacementSource === 'explicit_text', + may_reuse_captured_text: this.replacementSource === 'captured_text_or_explicit' + }; + } +} + export function matchProfile(profile: ConstructorParameters[0]): MatchProfile { return new MatchProfile(profile); } @@ -300,6 +385,12 @@ export function destinationProfile( return new DestinationProfile(profile); } +export function operationProfile( + profile: ConstructorParameters[0] +): OperationProfile { + return new OperationProfile(profile); +} + export function selectionProfile( profile: ConstructorParameters[0] ): SelectionProfile { @@ -452,13 +543,10 @@ export function boundaryReport(): Readonly> { 'limit helpers', 'match profile helpers', 'selection profile helpers', - 'destination profile helpers' - ], - future_exports: [ - 'operation profile helpers', - 'replace/delete/insert/move helpers', - 'batch operation helpers' + 'destination profile helpers', + 'operation profile helpers' ], + future_exports: ['replace/delete/insert/move helpers', 'batch operation helpers'], metadata: { source: 'legacy_crispr_reference', decision: diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index 564d639..81d89ff 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -6,6 +6,7 @@ import { DestinationProfile, Limit, MatchProfile, + OperationProfile, SelectionProfile, astMergeContractAnchor, boundaryReport @@ -122,6 +123,27 @@ function readDestinationProfileFixture(): DestinationProfileFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as DestinationProfileFixture; } +interface OperationProfileFixture { + cases: Array<{ + name: string; + profile: ConstructorParameters[0]; + expected: Readonly>; + }>; +} + +function readOperationProfileFixture(): OperationProfileFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-921-ast-crispr-operation-profile-helpers', + 'ast-crispr-operation-profile-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as OperationProfileFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -172,4 +194,12 @@ describe('@structuredmerge/ast-crispr', () => { expect(new DestinationProfile(testCase.profile).report()).toEqual(testCase.expected); } }); + + it('conforms to the operation profile helper fixture', () => { + const fixture = readOperationProfileFixture(); + + for (const testCase of fixture.cases) { + expect(new OperationProfile(testCase.profile).report()).toEqual(testCase.expected); + } + }); }); From b1cff161c6fbd01c4accb3d38e6e2816d7bb66bc Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:48:07 -0600 Subject: [PATCH 092/130] Add ast-crispr operation helpers --- packages/ast-crispr/src/index.ts | 49 ++++++++++++++++++++++- packages/ast-crispr/test/boundary.test.ts | 41 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index ca643a4..0462110 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -391,6 +391,50 @@ export function operationProfile( return new OperationProfile(profile); } +export function replaceOperation(): OperationProfile { + return new OperationProfile({ + operation_kind: 'replace', + source_requirement: 'required', + destination_requirement: 'none', + replacement_source: 'explicit_text', + captures_source_text: true, + supports_if_missing: false + }); +} + +export function deleteOperation(): OperationProfile { + return new OperationProfile({ + operation_kind: 'delete', + source_requirement: 'required', + destination_requirement: 'none', + replacement_source: 'none', + captures_source_text: true, + supports_if_missing: false + }); +} + +export function insertOperation(): OperationProfile { + return new OperationProfile({ + operation_kind: 'insert', + source_requirement: 'none', + destination_requirement: 'optional', + replacement_source: 'explicit_text', + captures_source_text: false, + supports_if_missing: true + }); +} + +export function moveOperation(): OperationProfile { + return new OperationProfile({ + operation_kind: 'move', + source_requirement: 'optional', + destination_requirement: 'optional', + replacement_source: 'captured_text_or_explicit', + captures_source_text: true, + supports_if_missing: true + }); +} + export function selectionProfile( profile: ConstructorParameters[0] ): SelectionProfile { @@ -544,9 +588,10 @@ export function boundaryReport(): Readonly> { 'match profile helpers', 'selection profile helpers', 'destination profile helpers', - 'operation profile helpers' + 'operation profile helpers', + 'replace/delete/insert/move helpers' ], - future_exports: ['replace/delete/insert/move helpers', 'batch operation helpers'], + future_exports: ['batch operation helpers'], metadata: { source: 'legacy_crispr_reference', decision: diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index 81d89ff..ae734c8 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -9,7 +9,11 @@ import { OperationProfile, SelectionProfile, astMergeContractAnchor, - boundaryReport + boundaryReport, + deleteOperation, + insertOperation, + moveOperation, + replaceOperation } from '../src/index'; interface BoundaryFixture { @@ -144,6 +148,27 @@ function readOperationProfileFixture(): OperationProfileFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as OperationProfileFixture; } +interface OperationHelperFixture { + cases: Array<{ + name: string; + helper: 'replace' | 'delete' | 'insert' | 'move'; + expected_operation_profile: Readonly>; + }>; +} + +function readOperationHelperFixture(): OperationHelperFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-922-ast-crispr-operation-helpers', + 'ast-crispr-operation-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as OperationHelperFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -202,4 +227,18 @@ describe('@structuredmerge/ast-crispr', () => { expect(new OperationProfile(testCase.profile).report()).toEqual(testCase.expected); } }); + + it('conforms to the operation helper fixture', () => { + const fixture = readOperationHelperFixture(); + const helpers = { + replace: replaceOperation, + delete: deleteOperation, + insert: insertOperation, + move: moveOperation + }; + + for (const testCase of fixture.cases) { + expect(helpers[testCase.helper]().report()).toEqual(testCase.expected_operation_profile); + } + }); }); From a0ae5097d2605ac82ed1651da2f3ae2c0fdc8eb7 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:52:01 -0600 Subject: [PATCH 093/130] Add ast-crispr batch operation helpers --- packages/ast-crispr/src/index.ts | 19 ++++++++++-- packages/ast-crispr/test/boundary.test.ts | 37 +++++++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/ast-crispr/src/index.ts b/packages/ast-crispr/src/index.ts index 0462110..afd1cbe 100644 --- a/packages/ast-crispr/src/index.ts +++ b/packages/ast-crispr/src/index.ts @@ -102,6 +102,12 @@ export interface OperationProfileReport { readonly may_reuse_captured_text: boolean; } +export interface BatchOperationReport { + readonly operation_count: number; + readonly operation_kinds: readonly string[]; + readonly operation_profiles: readonly OperationProfileReport[]; +} + interface ProfileDescriptor { readonly family: string; } @@ -435,6 +441,14 @@ export function moveOperation(): OperationProfile { }); } +export function batchOperationReport(profiles: readonly OperationProfile[]): BatchOperationReport { + return { + operation_count: profiles.length, + operation_kinds: profiles.map((profile) => profile.operationKind), + operation_profiles: profiles.map((profile) => profile.report()) + }; +} + export function selectionProfile( profile: ConstructorParameters[0] ): SelectionProfile { @@ -589,9 +603,10 @@ export function boundaryReport(): Readonly> { 'selection profile helpers', 'destination profile helpers', 'operation profile helpers', - 'replace/delete/insert/move helpers' + 'replace/delete/insert/move helpers', + 'batch operation helpers' ], - future_exports: ['batch operation helpers'], + future_exports: [], metadata: { source: 'legacy_crispr_reference', decision: diff --git a/packages/ast-crispr/test/boundary.test.ts b/packages/ast-crispr/test/boundary.test.ts index ae734c8..ba0b7bf 100644 --- a/packages/ast-crispr/test/boundary.test.ts +++ b/packages/ast-crispr/test/boundary.test.ts @@ -9,6 +9,7 @@ import { OperationProfile, SelectionProfile, astMergeContractAnchor, + batchOperationReport, boundaryReport, deleteOperation, insertOperation, @@ -169,6 +170,27 @@ function readOperationHelperFixture(): OperationHelperFixture { return JSON.parse(readFileSync(fixturePath, 'utf8')) as OperationHelperFixture; } +interface BatchOperationHelperFixture { + cases: Array<{ + name: string; + helpers: Array<'replace' | 'delete' | 'insert' | 'move'>; + expected: Readonly>; + }>; +} + +function readBatchOperationHelperFixture(): BatchOperationHelperFixture { + const fixturePath = path.resolve( + process.cwd(), + '..', + 'fixtures', + 'diagnostics', + 'slice-923-ast-crispr-batch-operation-helpers', + 'ast-crispr-batch-operation-helpers.json' + ); + + return JSON.parse(readFileSync(fixturePath, 'utf8')) as BatchOperationHelperFixture; +} + describe('@structuredmerge/ast-crispr', () => { it('conforms to the package boundary fixture', () => { expect(boundaryReport()).toEqual(readFixture().boundary); @@ -241,4 +263,19 @@ describe('@structuredmerge/ast-crispr', () => { expect(helpers[testCase.helper]().report()).toEqual(testCase.expected_operation_profile); } }); + + it('conforms to the batch operation helper fixture', () => { + const fixture = readBatchOperationHelperFixture(); + const helpers = { + replace: replaceOperation, + delete: deleteOperation, + insert: insertOperation, + move: moveOperation + }; + + for (const testCase of fixture.cases) { + const profiles = testCase.helpers.map((helper) => helpers[helper]()); + expect(batchOperationReport(profiles)).toEqual(testCase.expected); + } + }); }); From fbfaec1b17f6cc53284e5e10ad55014193ba0846 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 14:58:23 -0600 Subject: [PATCH 094/130] Add tree-haver edit projection support --- packages/tree-haver/src/contracts.ts | 14 +++++ packages/tree-haver/src/index.ts | 1 + .../test/fixtures.integration.test.ts | 59 +++++++++++++++++++ 3 files changed, 74 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 085df2d..965e4a5 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -160,6 +160,20 @@ export interface TreeHaverProfile { readonly diagnostics: readonly string[]; } +export interface EditProjectionSupport { + readonly backendRef: BackendReference; + readonly language: string; + readonly supportsEditProjection: boolean; + readonly nativeEditTarget: string; + readonly normalizedEditTarget: string; + readonly supportedOperations: readonly string[]; + readonly requiredNodeFields: readonly string[]; + readonly correlationKeys: readonly string[]; + readonly preservesSourceFragments: boolean; + readonly unsupportedReason?: string; + readonly diagnostics: readonly string[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 8430f0d..cb2bd66 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -17,6 +17,7 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + EditProjectionSupport, FeatureProfile, KaitaiByteSpan, KaitaiTreeAnalysis, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index a7ecdc2..4f6e31c 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -12,6 +12,7 @@ import type { BinaryRenderPolicy, BinaryScalarValue, ByteEditSpan, + EditProjectionSupport, FeatureProfile, NativeParserProvider, NativeProviderMetadata, @@ -76,6 +77,36 @@ interface BackendRegistryFixture { backends: BackendReference[]; } +interface EditProjectionSupportFixture { + backend_ref: BackendReference; + language: string; + supports_edit_projection: boolean; + native_edit_target: string; + normalized_edit_target: string; + supported_operations: string[]; + required_node_fields: string[]; + correlation_keys: string[]; + preserves_source_fragments: boolean; + unsupported_reason: string | null; + diagnostics: string[]; +} + +function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { + return { + backendRef: fixture.backend_ref, + language: fixture.language, + supportsEditProjection: fixture.supports_edit_projection, + nativeEditTarget: fixture.native_edit_target, + normalizedEditTarget: fixture.normalized_edit_target, + supportedOperations: fixture.supported_operations, + requiredNodeFields: fixture.required_node_fields, + correlationKeys: fixture.correlation_keys, + preservesSourceFragments: fixture.preserves_source_fragments, + unsupportedReason: fixture.unsupported_reason ?? undefined, + diagnostics: fixture.diagnostics + }; +} + interface KaitaiSubstrateFixture { backend: BackendReference; adapter_info: { @@ -975,6 +1006,34 @@ describe('tree-haver shared fixtures', () => { expect(unsafeEntries[2]?.category).toBe('encrypted_member'); }); + it('conforms to the slice-924 tree_haver edit projection support fixture', () => { + const fixture = readFixture<{ + support: EditProjectionSupportFixture; + unsupported: EditProjectionSupportFixture; + }>( + 'diagnostics', + 'slice-924-tree-haver-edit-projection-support', + 'edit-projection-support.json' + ); + const support = editProjectionSupport(fixture.support); + const unsupported = editProjectionSupport(fixture.unsupported); + + expect(support.supportsEditProjection).toBe(true); + expect(support.backendRef.id).toBe('go-dst'); + expect(support.supportedOperations[0]).toBe('replace_node'); + expect(support.correlationKeys[1]).toBe('metadata.go_dst.node_path'); + expect(support.preservesSourceFragments).toBe(true); + expect(support.unsupportedReason).toBeUndefined(); + + expect(unsupported.supportsEditProjection).toBe(false); + expect(unsupported.backendRef.id).toBe('psych'); + expect(unsupported.unsupportedReason).toBe('backend_does_not_retain_native_tree'); + expect(unsupported.supportedOperations).toHaveLength(0); + expect(unsupported.diagnostics[0]).toBe( + 'edit projection unavailable: native tree not retained' + ); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From dce3e9befe8a4ea8669a1d36c3ccc4636fd5a201 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 15:24:10 -0600 Subject: [PATCH 095/130] Add tree-haver path validation --- packages/tree-haver/src/contracts.ts | 95 +++++++++++++++++++ packages/tree-haver/src/index.ts | 8 ++ .../test/fixtures.integration.test.ts | 58 +++++++++++ 3 files changed, 161 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 965e4a5..f32979e 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -174,6 +174,12 @@ export interface EditProjectionSupport { readonly diagnostics: readonly string[]; } +export interface LibraryPathValidation { + readonly path: string; + readonly valid: boolean; + readonly errors: readonly string[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; @@ -607,6 +613,13 @@ const backendRegistry = new Map([ ]); let currentBackend: string | undefined; +export const MAX_LIBRARY_PATH_LENGTH = 4096; + +const VALID_LIBRARY_FILENAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u; +const VALID_LANGUAGE_NAME_PATTERN = /^[a-z][a-z0-9_]*$/u; +const VALID_SYMBOL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u; +const VERSIONED_SHARED_OBJECT_PATTERN = /\.so\.\d+$/u; + export function registerBackend(backend: BackendReference): void { backendRegistry.set(backend.id, { ...backend }); } @@ -621,6 +634,69 @@ export function registeredBackends(): BackendReference[] { return [...backendRegistry.values()].map((backend) => ({ ...backend })); } +export function validateLibraryPath(libraryPath: string): LibraryPathValidation { + const errors = libraryPathErrors(libraryPath); + + return { + path: libraryPath, + valid: errors.length === 0, + errors + }; +} + +export function libraryPathErrors(libraryPath: string): string[] { + const errors: string[] = []; + + if (libraryPath.length === 0) { + errors.push('path_empty'); + } + if (libraryPath.length > MAX_LIBRARY_PATH_LENGTH) { + errors.push('path_too_long'); + } + if (libraryPath.includes('\0')) { + errors.push('path_contains_null_byte'); + } + if (!libraryPath.startsWith('/') && !windowsAbsolutePath(libraryPath)) { + errors.push('path_not_absolute'); + } + + const segments = libraryPath.split(/[\\/]/u); + if (segments.includes('..')) { + errors.push('path_contains_parent_traversal'); + } + if (segments.includes('.')) { + errors.push('path_contains_current_directory_traversal'); + } + if (!hasAllowedLibraryExtension(libraryPath)) { + errors.push('path_extension_not_allowed'); + } + + const filename = libraryFilename(libraryPath); + if (!VALID_LIBRARY_FILENAME_PATTERN.test(filename)) { + errors.push('filename_contains_invalid_characters'); + } + + return errors; +} + +export function safeLanguageName(name: string): boolean { + return VALID_LANGUAGE_NAME_PATTERN.test(name); +} + +export function sanitizeLanguageName(name: string): string | undefined { + const sanitized = name.toLowerCase().replace(/[^a-z0-9_]/gu, ''); + + return safeLanguageName(sanitized) ? sanitized : undefined; +} + +export function safeSymbolName(symbol: string): boolean { + return VALID_SYMBOL_NAME_PATTERN.test(symbol); +} + +export function safeBackendName(name: string): boolean { + return name === 'auto' || backendRegistry.has(name); +} + export function currentBackendId(): string | undefined { return currentBackend; } @@ -639,6 +715,25 @@ export function withBackend(backendId: string, fn: () => T): T { } } +function windowsAbsolutePath(libraryPath: string): boolean { + return /^[A-Za-z]:[\\/]/u.test(libraryPath); +} + +function hasAllowedLibraryExtension(libraryPath: string): boolean { + const normalizedPath = libraryPath.toLowerCase(); + + return ( + normalizedPath.endsWith('.so') || + VERSIONED_SHARED_OBJECT_PATTERN.test(normalizedPath) || + normalizedPath.endsWith('.dylib') || + normalizedPath.endsWith('.dll') + ); +} + +function libraryFilename(libraryPath: string): string { + return libraryPath.split(/[\\/]/u).at(-1) ?? ''; +} + export function createPeggyParser( grammar: string, options?: peggy.ParserBuildOptions diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index cb2bd66..b06820d 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -24,6 +24,7 @@ export type { KaitaiTreeNode, LanguagePackAnalysis, LanguagePackProcessAnalysis, + LibraryPathValidation, NativeParserProvider, NativeProviderMetadata, NormalizedParseResult, @@ -79,8 +80,15 @@ export { processWithLanguagePack, parseWithLanguagePack, parseWithPeggy, + libraryPathErrors, + MAX_LIBRARY_PATH_LENGTH, registerBackend, registeredBackends, + safeBackendName, + safeLanguageName, + safeSymbolName, + sanitizeLanguageName, sliceByteRange, + validateLibraryPath, withBackend } from './contracts'; diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 4f6e31c..fdfb6ee 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -37,6 +37,7 @@ import { byteRangeOverlaps, currentBackendId, extractSourceFragment, + libraryPathErrors, kaitaiAdapterInfo, kaitaiFeatureProfile, KAITAI_STRUCT_BACKEND, @@ -48,7 +49,12 @@ import { nodeRoles, registerBackend, registeredBackends, + safeBackendName, + safeLanguageName, + safeSymbolName, + sanitizeLanguageName, sliceByteRange, + validateLibraryPath, withBackend } from '../src/index'; @@ -91,6 +97,27 @@ interface EditProjectionSupportFixture { diagnostics: string[]; } +interface PathValidationCase { + name: string; + path: string; + expected_valid: boolean; + expected_errors: string[]; +} + +interface NameValidationCase { + name: string; + value: string; + expected_valid: boolean; + expected_sanitized?: string | null; +} + +interface PathValidationFixture { + library_path_cases: PathValidationCase[]; + language_name_cases: NameValidationCase[]; + symbol_name_cases: NameValidationCase[]; + backend_name_cases: NameValidationCase[]; +} + function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { return { backendRef: fixture.backend_ref, @@ -1034,6 +1061,37 @@ describe('tree-haver shared fixtures', () => { ); }); + it('conforms to the slice-925 tree_haver path validation fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-925-tree-haver-path-validation', + 'path-validation.json' + ); + + for (const testCase of fixture.library_path_cases) { + const validation = validateLibraryPath(testCase.path); + expect(validation.path).toBe(testCase.path); + expect(validation.valid, testCase.name).toBe(testCase.expected_valid); + expect(validation.errors, testCase.name).toEqual(testCase.expected_errors); + expect(libraryPathErrors(testCase.path), testCase.name).toEqual(testCase.expected_errors); + } + + for (const testCase of fixture.language_name_cases) { + expect(safeLanguageName(testCase.value), testCase.name).toBe(testCase.expected_valid); + expect(sanitizeLanguageName(testCase.value), testCase.name).toBe( + testCase.expected_sanitized ?? undefined + ); + } + + for (const testCase of fixture.symbol_name_cases) { + expect(safeSymbolName(testCase.value), testCase.name).toBe(testCase.expected_valid); + } + + for (const testCase of fixture.backend_name_cases) { + expect(safeBackendName(testCase.value), testCase.name).toBe(testCase.expected_valid); + } + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 4d45a5e21c193a9dd53d66df84f5f928af08b645 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 15:28:22 -0600 Subject: [PATCH 096/130] Add tree-haver backend availability reports --- packages/tree-haver/src/contracts.ts | 41 +++++++++++++++++++ packages/tree-haver/src/index.ts | 4 ++ .../test/fixtures.integration.test.ts | 36 ++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index f32979e..d51ed83 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -180,6 +180,22 @@ export interface LibraryPathValidation { readonly errors: readonly string[]; } +export type BackendAvailabilityStatus = 'available' | 'unavailable' | 'unknown'; + +export interface BackendAvailabilityCheck { + readonly name: string; + readonly status: BackendAvailabilityStatus; + readonly required: boolean; + readonly diagnostics: readonly string[]; +} + +export interface BackendAvailabilityReport { + readonly backendRef: BackendReference; + readonly status: BackendAvailabilityStatus; + readonly checks: readonly BackendAvailabilityCheck[]; + readonly diagnostics: readonly string[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; @@ -697,6 +713,31 @@ export function safeBackendName(name: string): boolean { return name === 'auto' || backendRegistry.has(name); } +export function buildBackendAvailabilityReport( + backendRef: BackendReference, + checks: readonly BackendAvailabilityCheck[] +): BackendAvailabilityReport { + if (checks.length === 0) { + return { + backendRef, + status: 'unknown', + checks, + diagnostics: ['backend availability unknown: no checks supplied'] + }; + } + + const diagnostics: string[] = []; + let status: BackendAvailabilityStatus = 'available'; + for (const check of checks) { + if (check.required && check.status !== 'available') { + status = 'unavailable'; + diagnostics.push(`backend unavailable: required check ${check.name} is ${check.status}`); + } + } + + return { backendRef, status, checks, diagnostics }; +} + export function currentBackendId(): string | undefined { return currentBackend; } diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index b06820d..992b7eb 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -3,6 +3,9 @@ export const packageName = '@structuredmerge/tree-haver'; export type { AdapterInfo, AnalysisHandle, + BackendAvailabilityCheck, + BackendAvailabilityReport, + BackendAvailabilityStatus, BackendCapability, BackendReference, BinaryDiagnostic, @@ -56,6 +59,7 @@ export type { } from './contracts'; export { backendReference, + buildBackendAvailabilityReport, byteEditDelta, byteEditNewRange, byteEditOldRange, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index fdfb6ee..6036b15 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -3,6 +3,8 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; import type { AdapterInfo, + BackendAvailabilityCheck, + BackendAvailabilityReport, BackendCapability, BackendReference, BinaryDiagnostic, @@ -28,6 +30,7 @@ import type { } from '../src/index'; import { processWithLanguagePack } from '../src/index'; import { + buildBackendAvailabilityReport, byteEditDelta, byteEditNewRange, byteEditOldRange, @@ -118,6 +121,13 @@ interface PathValidationFixture { backend_name_cases: NameValidationCase[]; } +interface BackendAvailabilityReportFixture { + backend_ref: BackendReference; + status: 'available' | 'unavailable' | 'unknown'; + checks: BackendAvailabilityCheck[]; + diagnostics: string[]; +} + function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { return { backendRef: fixture.backend_ref, @@ -134,6 +144,17 @@ function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProje }; } +function backendAvailabilityReport( + fixture: BackendAvailabilityReportFixture +): BackendAvailabilityReport { + return { + backendRef: fixture.backend_ref, + status: fixture.status, + checks: fixture.checks, + diagnostics: fixture.diagnostics + }; +} + interface KaitaiSubstrateFixture { backend: BackendReference; adapter_info: { @@ -1092,6 +1113,21 @@ describe('tree-haver shared fixtures', () => { } }); + it('conforms to the slice-926 tree_haver backend availability fixture', () => { + const fixture = readFixture>( + 'diagnostics', + 'slice-926-tree-haver-backend-availability', + 'backend-availability.json' + ); + + for (const name of ['available_report', 'unavailable_report', 'unknown_report']) { + const expected = backendAvailabilityReport(fixture[name]!); + expect(buildBackendAvailabilityReport(expected.backendRef, expected.checks)).toEqual( + expected + ); + } + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 567274f54f30acc7a149c84e94d7be259fb863bb Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 15:31:58 -0600 Subject: [PATCH 097/130] Add tree-haver provider diagnostics reports --- packages/tree-haver/src/contracts.ts | 39 +++++++++++++++++ packages/tree-haver/src/index.ts | 4 ++ .../test/fixtures.integration.test.ts | 43 +++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index d51ed83..33ec477 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -196,6 +196,25 @@ export interface BackendAvailabilityReport { readonly diagnostics: readonly string[]; } +export type ProviderDiagnosticsStatus = 'clean' | 'warning' | 'blocked'; + +export interface ProviderDiagnostic { + readonly severity: DiagnosticSeverity; + readonly category: string; + readonly code: string; + readonly message: string; + readonly path: string; + readonly blocking: boolean; +} + +export interface ProviderDiagnosticsReport { + readonly providerId: string; + readonly backendRef: BackendReference; + readonly language: string; + readonly status: ProviderDiagnosticsStatus; + readonly diagnostics: readonly ProviderDiagnostic[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; @@ -738,6 +757,26 @@ export function buildBackendAvailabilityReport( return { backendRef, status, checks, diagnostics }; } +export function buildProviderDiagnosticsReport( + providerId: string, + backendRef: BackendReference, + language: string, + diagnostics: readonly ProviderDiagnostic[] +): ProviderDiagnosticsReport { + let status: ProviderDiagnosticsStatus = 'clean'; + for (const diagnostic of diagnostics) { + if (diagnostic.blocking) { + status = 'blocked'; + break; + } + if (diagnostic.severity === 'warning') { + status = 'warning'; + } + } + + return { providerId, backendRef, language, status, diagnostics }; +} + export function currentBackendId(): string | undefined { return currentBackend; } diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 992b7eb..67cae73 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -42,6 +42,9 @@ export type { ParseResult, PolicyReference, PolicySurface, + ProviderDiagnostic, + ProviderDiagnosticsReport, + ProviderDiagnosticsStatus, ProcessDiagnostic, ProcessImportInfo, ProcessRequest, @@ -60,6 +63,7 @@ export type { export { backendReference, buildBackendAvailabilityReport, + buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, byteEditOldRange, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 6036b15..7cb51d3 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -24,6 +24,8 @@ import type { ParseErrorTolerance, ParserRequest, PolicyReference, + ProviderDiagnostic, + ProviderDiagnosticsReport, SourceSpan, TreeHaverProfile, ZipUnsafeEntry @@ -31,6 +33,7 @@ import type { import { processWithLanguagePack } from '../src/index'; import { buildBackendAvailabilityReport, + buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, byteEditOldRange, @@ -128,6 +131,14 @@ interface BackendAvailabilityReportFixture { diagnostics: string[]; } +interface ProviderDiagnosticsReportFixture { + provider_id: string; + backend_ref: BackendReference; + language: string; + status: 'clean' | 'warning' | 'blocked'; + diagnostics: ProviderDiagnostic[]; +} + function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { return { backendRef: fixture.backend_ref, @@ -155,6 +166,18 @@ function backendAvailabilityReport( }; } +function providerDiagnosticsReport( + fixture: ProviderDiagnosticsReportFixture +): ProviderDiagnosticsReport { + return { + providerId: fixture.provider_id, + backendRef: fixture.backend_ref, + language: fixture.language, + status: fixture.status, + diagnostics: fixture.diagnostics + }; +} + interface KaitaiSubstrateFixture { backend: BackendReference; adapter_info: { @@ -1128,6 +1151,26 @@ describe('tree-haver shared fixtures', () => { } }); + it('conforms to the slice-927 tree_haver provider diagnostics fixture', () => { + const fixture = readFixture>( + 'diagnostics', + 'slice-927-tree-haver-provider-diagnostics', + 'provider-diagnostics.json' + ); + + for (const name of ['clean_report', 'warning_report', 'blocked_report']) { + const expected = providerDiagnosticsReport(fixture[name]!); + expect( + buildProviderDiagnosticsReport( + expected.providerId, + expected.backendRef, + expected.language, + expected.diagnostics + ) + ).toEqual(expected); + } + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 85053349adb7f40225044e9486839ab1e1bd5da6 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 15:39:13 -0600 Subject: [PATCH 098/130] Add tree-haver edit projection execution contracts --- packages/tree-haver/src/contracts.ts | 54 ++++++++++++++++ packages/tree-haver/src/index.ts | 5 ++ .../test/fixtures.integration.test.ts | 62 +++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 33ec477..1b209c2 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -215,6 +215,36 @@ export interface ProviderDiagnosticsReport { readonly diagnostics: readonly ProviderDiagnostic[]; } +export interface EditProjectionOperationRequest { + readonly operation: string; + readonly targetNodeId: string; + readonly targetNodePath: string; + readonly replacementSource: string; +} + +export interface EditProjectionExecutionRequest { + readonly providerId: string; + readonly backendRef: BackendReference; + readonly language: string; + readonly source: string; + readonly operations: readonly EditProjectionOperationRequest[]; +} + +export interface AppliedEditProjectionOperation { + readonly operation: string; + readonly targetNodeId: string; + readonly correlationKey: string; + readonly correlationValue: string; +} + +export interface EditProjectionExecutionResult { + readonly ok: boolean; + readonly status: 'applied' | 'rejected'; + readonly source: string; + readonly appliedOperations: readonly AppliedEditProjectionOperation[]; + readonly diagnostics: readonly ProviderDiagnostic[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; @@ -777,6 +807,30 @@ export function buildProviderDiagnosticsReport( return { providerId, backendRef, language, status, diagnostics }; } +export function buildEditProjectionExecutionResult( + source: string, + appliedOperations: readonly AppliedEditProjectionOperation[], + diagnostics: readonly ProviderDiagnostic[] +): EditProjectionExecutionResult { + if (diagnostics.some((diagnostic) => diagnostic.blocking)) { + return { + ok: false, + status: 'rejected', + source, + appliedOperations: [], + diagnostics + }; + } + + return { + ok: true, + status: 'applied', + source, + appliedOperations, + diagnostics + }; +} + export function currentBackendId(): string | undefined { return currentBackend; } diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index 67cae73..b0e6530 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -3,6 +3,7 @@ export const packageName = '@structuredmerge/tree-haver'; export type { AdapterInfo, AnalysisHandle, + AppliedEditProjectionOperation, BackendAvailabilityCheck, BackendAvailabilityReport, BackendAvailabilityStatus, @@ -20,6 +21,9 @@ export type { Diagnostic, DiagnosticCategory, DiagnosticSeverity, + EditProjectionExecutionRequest, + EditProjectionExecutionResult, + EditProjectionOperationRequest, EditProjectionSupport, FeatureProfile, KaitaiByteSpan, @@ -63,6 +67,7 @@ export type { export { backendReference, buildBackendAvailabilityReport, + buildEditProjectionExecutionResult, buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 7cb51d3..9e5063e 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { describe, expect, it } from 'vitest'; import type { AdapterInfo, + AppliedEditProjectionOperation, BackendAvailabilityCheck, BackendAvailabilityReport, BackendCapability, @@ -14,6 +15,7 @@ import type { BinaryRenderPolicy, BinaryScalarValue, ByteEditSpan, + EditProjectionExecutionResult, EditProjectionSupport, FeatureProfile, NativeParserProvider, @@ -33,6 +35,7 @@ import type { import { processWithLanguagePack } from '../src/index'; import { buildBackendAvailabilityReport, + buildEditProjectionExecutionResult, buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, @@ -139,6 +142,21 @@ interface ProviderDiagnosticsReportFixture { diagnostics: ProviderDiagnostic[]; } +interface AppliedEditProjectionOperationFixture { + operation: string; + target_node_id: string; + correlation_key: string; + correlation_value: string; +} + +interface EditProjectionExecutionResultFixture { + ok: boolean; + status: 'applied' | 'rejected'; + source: string; + applied_operations: AppliedEditProjectionOperationFixture[]; + diagnostics: ProviderDiagnostic[]; +} + function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { return { backendRef: fixture.backend_ref, @@ -178,6 +196,25 @@ function providerDiagnosticsReport( }; } +function editProjectionExecutionResult( + fixture: EditProjectionExecutionResultFixture +): EditProjectionExecutionResult { + return { + ok: fixture.ok, + status: fixture.status, + source: fixture.source, + appliedOperations: fixture.applied_operations.map( + (operation): AppliedEditProjectionOperation => ({ + operation: operation.operation, + targetNodeId: operation.target_node_id, + correlationKey: operation.correlation_key, + correlationValue: operation.correlation_value + }) + ), + diagnostics: fixture.diagnostics + }; +} + interface KaitaiSubstrateFixture { backend: BackendReference; adapter_info: { @@ -1171,6 +1208,31 @@ describe('tree-haver shared fixtures', () => { } }); + it('conforms to the slice-928 edit projection execution contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + unsupported_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-928-go-dst-edit-projection-execution', + 'edit-projection-execution.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + + const unsupported = editProjectionExecutionResult(fixture.unsupported_result); + expect( + buildEditProjectionExecutionResult(unsupported.source, [], unsupported.diagnostics) + ).toEqual(unsupported); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 1a4ad7357660c0228ab1b01c80b7e5aa816fde6b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 15:49:32 -0600 Subject: [PATCH 099/130] Add insert-child edit projection contract fixture --- .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 9e5063e..07fd65c 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -1233,6 +1233,25 @@ describe('tree-haver shared fixtures', () => { ).toEqual(unsupported); }); + it('conforms to the slice-929 insert-child edit projection contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-929-go-dst-insert-child-edit-projection', + 'insert-child-edit-projection.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 2364f533e711662626bac45bec7a32ba5b28cfe8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 18:07:07 -0600 Subject: [PATCH 100/130] Add delete-node edit projection contract fixture --- .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 07fd65c..25ac766 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -1252,6 +1252,25 @@ describe('tree-haver shared fixtures', () => { ).toEqual(expected); }); + it('conforms to the slice-930 delete-node edit projection contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-930-go-dst-delete-node-edit-projection', + 'delete-node-edit-projection.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 4e34ca3e66571844cd272dae3a50df06084fb8cc Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 18:11:12 -0600 Subject: [PATCH 101/130] Add go-parser edit projection contract fixture --- .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 25ac766..009160f 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -1271,6 +1271,25 @@ describe('tree-haver shared fixtures', () => { ).toEqual(expected); }); + it('conforms to the slice-931 go-parser edit projection contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-931-go-parser-edit-projection-execution', + 'edit-projection-execution.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From c41259718f5ca5ff62a5870618525089fadef14f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 18:15:36 -0600 Subject: [PATCH 102/130] Add edit projection provider operation matrix --- packages/tree-haver/src/contracts.ts | 35 +++++++++ packages/tree-haver/src/index.ts | 5 ++ .../test/fixtures.integration.test.ts | 78 +++++++++++++++++++ 3 files changed, 118 insertions(+) diff --git a/packages/tree-haver/src/contracts.ts b/packages/tree-haver/src/contracts.ts index 1b209c2..c71e522 100644 --- a/packages/tree-haver/src/contracts.ts +++ b/packages/tree-haver/src/contracts.ts @@ -245,6 +245,33 @@ export interface EditProjectionExecutionResult { readonly diagnostics: readonly ProviderDiagnostic[]; } +export type EditProjectionProviderOperationStatus = 'implemented' | 'planned' | 'unsupported'; + +export interface EditProjectionProviderOperation { + readonly operation: string; + readonly status: EditProjectionProviderOperationStatus; + readonly nodeScope: string; + readonly correlationKeys: readonly string[]; + readonly fixtureSlices: readonly string[]; + readonly formattingPreservation: string; + readonly diagnostics: readonly string[]; +} + +export interface EditProjectionProviderMatrixEntry { + readonly providerId: string; + readonly backendRef: BackendReference; + readonly language: string; + readonly formattingPreservation: string; + readonly preservesSourceFragments: boolean; + readonly operations: readonly EditProjectionProviderOperation[]; +} + +export interface EditProjectionProviderMatrix { + readonly operations: readonly string[]; + readonly providers: readonly EditProjectionProviderMatrixEntry[]; + readonly diagnostics: readonly string[]; +} + export interface OrderedSiblingEdge { readonly parentId: string; readonly nodeId: string; @@ -831,6 +858,14 @@ export function buildEditProjectionExecutionResult( }; } +export function buildEditProjectionProviderMatrix( + operations: readonly string[], + providers: readonly EditProjectionProviderMatrixEntry[], + diagnostics: readonly string[] +): EditProjectionProviderMatrix { + return { operations, providers, diagnostics }; +} + export function currentBackendId(): string | undefined { return currentBackend; } diff --git a/packages/tree-haver/src/index.ts b/packages/tree-haver/src/index.ts index b0e6530..675abb6 100644 --- a/packages/tree-haver/src/index.ts +++ b/packages/tree-haver/src/index.ts @@ -24,6 +24,10 @@ export type { EditProjectionExecutionRequest, EditProjectionExecutionResult, EditProjectionOperationRequest, + EditProjectionProviderMatrix, + EditProjectionProviderMatrixEntry, + EditProjectionProviderOperation, + EditProjectionProviderOperationStatus, EditProjectionSupport, FeatureProfile, KaitaiByteSpan, @@ -68,6 +72,7 @@ export { backendReference, buildBackendAvailabilityReport, buildEditProjectionExecutionResult, + buildEditProjectionProviderMatrix, buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 009160f..d145413 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -16,6 +16,9 @@ import type { BinaryScalarValue, ByteEditSpan, EditProjectionExecutionResult, + EditProjectionProviderMatrix, + EditProjectionProviderMatrixEntry, + EditProjectionProviderOperation, EditProjectionSupport, FeatureProfile, NativeParserProvider, @@ -36,6 +39,7 @@ import { processWithLanguagePack } from '../src/index'; import { buildBackendAvailabilityReport, buildEditProjectionExecutionResult, + buildEditProjectionProviderMatrix, buildProviderDiagnosticsReport, byteEditDelta, byteEditNewRange, @@ -157,6 +161,31 @@ interface EditProjectionExecutionResultFixture { diagnostics: ProviderDiagnostic[]; } +interface EditProjectionProviderOperationFixture { + operation: string; + status: 'implemented' | 'planned' | 'unsupported'; + node_scope: string; + correlation_keys: string[]; + fixture_slices: string[]; + formatting_preservation: string; + diagnostics: string[]; +} + +interface EditProjectionProviderMatrixEntryFixture { + provider_id: string; + backend_ref: BackendReference; + language: string; + formatting_preservation: string; + preserves_source_fragments: boolean; + operations: EditProjectionProviderOperationFixture[]; +} + +interface EditProjectionProviderMatrixFixture { + operations: string[]; + providers: EditProjectionProviderMatrixEntryFixture[]; + diagnostics: string[]; +} + function editProjectionSupport(fixture: EditProjectionSupportFixture): EditProjectionSupport { return { backendRef: fixture.backend_ref, @@ -215,6 +244,39 @@ function editProjectionExecutionResult( }; } +function editProjectionProviderMatrixEntry( + fixture: EditProjectionProviderMatrixEntryFixture +): EditProjectionProviderMatrixEntry { + return { + providerId: fixture.provider_id, + backendRef: fixture.backend_ref, + language: fixture.language, + formattingPreservation: fixture.formatting_preservation, + preservesSourceFragments: fixture.preserves_source_fragments, + operations: fixture.operations.map( + (operation): EditProjectionProviderOperation => ({ + operation: operation.operation, + status: operation.status, + nodeScope: operation.node_scope, + correlationKeys: operation.correlation_keys, + fixtureSlices: operation.fixture_slices, + formattingPreservation: operation.formatting_preservation, + diagnostics: operation.diagnostics + }) + ) + }; +} + +function editProjectionProviderMatrix( + fixture: EditProjectionProviderMatrixFixture +): EditProjectionProviderMatrix { + return { + operations: fixture.operations, + providers: fixture.providers.map(editProjectionProviderMatrixEntry), + diagnostics: fixture.diagnostics + }; +} + interface KaitaiSubstrateFixture { backend: BackendReference; adapter_info: { @@ -1290,6 +1352,22 @@ describe('tree-haver shared fixtures', () => { ).toEqual(expected); }); + it('conforms to the slice-932 edit projection provider operation matrix fixture', () => { + const fixture = readFixture<{ + operations: string[]; + providers: EditProjectionProviderMatrixEntryFixture[]; + expected_matrix: EditProjectionProviderMatrixFixture; + }>( + 'diagnostics', + 'slice-932-edit-projection-provider-operation-matrix', + 'provider-operation-matrix.json' + ); + + const providers = fixture.providers.map(editProjectionProviderMatrixEntry); + const expected = editProjectionProviderMatrix(fixture.expected_matrix); + expect(buildEditProjectionProviderMatrix(fixture.operations, providers, [])).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From c83e8aa61e6ea9862cb9f74908c8ef90d472306a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 18:18:41 -0600 Subject: [PATCH 103/130] Add go-parser insert child edit projection contract fixture --- .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index d145413..49d4b9f 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -1368,6 +1368,25 @@ describe('tree-haver shared fixtures', () => { expect(buildEditProjectionProviderMatrix(fixture.operations, providers, [])).toEqual(expected); }); + it('conforms to the slice-933 go-parser insert-child edit projection contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-933-go-parser-insert-child-edit-projection', + 'insert-child-edit-projection.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From b63dca5463bd33d7cc59f82f91997499caba1f8b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Fri, 15 May 2026 18:20:32 -0600 Subject: [PATCH 104/130] Add go-parser delete node edit projection contract fixture --- .../test/fixtures.integration.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/tree-haver/test/fixtures.integration.test.ts b/packages/tree-haver/test/fixtures.integration.test.ts index 49d4b9f..ced7008 100644 --- a/packages/tree-haver/test/fixtures.integration.test.ts +++ b/packages/tree-haver/test/fixtures.integration.test.ts @@ -1387,6 +1387,25 @@ describe('tree-haver shared fixtures', () => { ).toEqual(expected); }); + it('conforms to the slice-934 go-parser delete-node edit projection contract fixture', () => { + const fixture = readFixture<{ + expected_result: EditProjectionExecutionResultFixture; + }>( + 'diagnostics', + 'slice-934-go-parser-delete-node-edit-projection', + 'delete-node-edit-projection.json' + ); + + const expected = editProjectionExecutionResult(fixture.expected_result); + expect( + buildEditProjectionExecutionResult( + expected.source, + expected.appliedOperations, + expected.diagnostics + ) + ).toEqual(expected); + }); + it('supports temporary backend context selection', () => { expect(currentBackendId()).toBeUndefined(); From 06ea243deef592c1fab638ee595432fe755ee0c8 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 14:25:24 -0600 Subject: [PATCH 105/130] Add TypeScript ast merge git merge3 --- packages/ast-merge-git/package.json | 19 ++ packages/ast-merge-git/src/index.ts | 245 ++++++++++++++++++ .../test/fixtures.integration.test.ts | 63 +++++ packages/smorg-ts/package.json | 1 + packages/smorg-ts/src/cli.ts | 40 ++- packages/smorg-ts/test/cli.test.ts | 21 +- pnpm-lock.yaml | 9 + tsconfig.json | 1 + vitest.config.ts | 3 + 9 files changed, 395 insertions(+), 7 deletions(-) create mode 100644 packages/ast-merge-git/package.json create mode 100644 packages/ast-merge-git/src/index.ts create mode 100644 packages/ast-merge-git/test/fixtures.integration.test.ts diff --git a/packages/ast-merge-git/package.json b/packages/ast-merge-git/package.json new file mode 100644 index 0000000..4d8f20e --- /dev/null +++ b/packages/ast-merge-git/package.json @@ -0,0 +1,19 @@ +{ + "name": "@structuredmerge/ast-merge-git", + "version": "0.2.0", + "private": false, + "type": "module", + "main": "./src/index.ts", + "author": "Peter H. Boling", + "homepage": "https://structuredmerge.org", + "repository": { + "type": "git", + "url": "git+https://github.com/structuredmerge/structuredmerge-typescript.git" + }, + "bugs": { + "url": "https://github.com/structuredmerge/structuredmerge-typescript/issues" + }, + "dependencies": { + "@structuredmerge/ast-merge": "workspace:*" + } +} diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts new file mode 100644 index 0000000..8f2099c --- /dev/null +++ b/packages/ast-merge-git/src/index.ts @@ -0,0 +1,245 @@ +import type { Diagnostic } from '@structuredmerge/ast-merge'; + +export const packageName = '@structuredmerge/ast-merge-git'; + +export interface Merge3Request { + readonly base_source: string; + readonly ours_source: string; + readonly theirs_source: string; + readonly path_name?: string; + readonly language?: string; + readonly dialect?: string; + readonly profile_id?: string; + readonly fallback_policy?: string; + readonly conflict_marker_size?: number; + readonly render_policy?: string; +} + +export interface Merge3Conflict { + readonly conflict_id: string; + readonly category: string; + readonly path: string; + readonly message: string; +} + +export interface Merge3Response { + readonly ok: boolean; + readonly merged_source?: string; + readonly conflicts: readonly Merge3Conflict[]; + readonly diagnostics: readonly Diagnostic[]; + readonly fallbacks: readonly string[]; + readonly profile: Readonly>; + readonly render_report: { + readonly strategy: string; + }; + readonly formatting_preservation: { + readonly line_diff_score: number; + readonly character_diff_score: number; + }; + readonly reparse_after_render: boolean | null; +} + +const absent = Symbol('absent'); +type MaybeAbsent = unknown | typeof absent; + +export function merge3(request: Merge3Request): Merge3Response { + switch (normalizeLanguage(request)) { + case 'json': + return merge3Json(request); + default: + return response(request, { + ok: false, + diagnostics: [ + { + severity: 'error', + category: 'unsupported_feature', + message: 'ast-merge-git currently supports only json merge3.' + } + ] + }); + } +} + +export function merge3Json(request: Merge3Request): Merge3Response { + try { + const base = parseJsonRole('base', request.base_source); + const ours = parseJsonRole('ours', request.ours_source); + const theirs = parseJsonRole('theirs', request.theirs_source); + const conflicts: Merge3Conflict[] = []; + const merged = mergeJsonValue(base, ours, theirs, '', conflicts); + if (conflicts.length > 0) { + return response(request, { + ok: false, + conflicts, + diagnostics: [ + { + severity: 'error', + category: 'configuration_error', + message: `merge_conflict: merge3 found ${conflicts.length} unresolved conflict(s).` + } + ] + }); + } + + const mergedSource = JSON.stringify(merged); + return response(request, { + ok: true, + merged_source: mergedSource, + reparse_after_render: JSON.parse(mergedSource) !== undefined, + formatting_preservation: { + line_diff_score: 1, + character_diff_score: 1 + } + }); + } catch (error) { + return response(request, { + ok: false, + diagnostics: [ + { + severity: 'error', + category: 'parse_error', + message: String(error) + } + ] + }); + } +} + +function response( + request: Merge3Request, + fields: Partial & { readonly ok: boolean } +): Merge3Response { + return { + ok: fields.ok, + merged_source: fields.merged_source, + conflicts: fields.conflicts ?? [], + diagnostics: fields.diagnostics ?? [], + fallbacks: fields.fallbacks ?? [], + profile: { + profile_id: request.profile_id ?? '', + language: normalizeLanguage(request), + dialect: request.dialect ?? '' + }, + render_report: { + strategy: request.render_policy || 'canonical' + }, + formatting_preservation: fields.formatting_preservation ?? { + line_diff_score: 0, + character_diff_score: 0 + }, + reparse_after_render: fields.reparse_after_render ?? null + }; +} + +function parseJsonRole(role: string, source: string): unknown { + try { + return JSON.parse(source); + } catch (error) { + throw new Error(`${role} parse error: ${String(error)}`); + } +} + +function mergeJsonValue( + base: unknown, + ours: unknown, + theirs: unknown, + path: string, + conflicts: Merge3Conflict[] +): unknown { + if (jsonEqual(ours, theirs)) return ours; + if (jsonEqual(base, ours)) return theirs; + if (jsonEqual(base, theirs)) return ours; + if (isRecord(base) && isRecord(ours) && isRecord(theirs)) { + return mergeJsonObjects(base, ours, theirs, path, conflicts); + } + + addConflict(conflicts, 'edit_edit', path, 'value changed differently in ours and theirs'); + return ours; +} + +function mergeJsonObjects( + base: Readonly>, + ours: Readonly>, + theirs: Readonly>, + path: string, + conflicts: Merge3Conflict[] +): Readonly> { + const result: Record = {}; + const keys = [...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])].sort(); + for (const key of keys) { + const [merged, keep] = mergeJsonEntry( + Object.hasOwn(base, key) ? base[key] : absent, + Object.hasOwn(ours, key) ? ours[key] : absent, + Object.hasOwn(theirs, key) ? theirs[key] : absent, + jsonPointerJoin(path, key), + conflicts + ); + if (keep) result[key] = merged; + } + return result; +} + +function mergeJsonEntry( + base: MaybeAbsent, + ours: MaybeAbsent, + theirs: MaybeAbsent, + path: string, + conflicts: Merge3Conflict[] +): readonly [unknown, boolean] { + const baseAbsent = base === absent; + const oursAbsent = ours === absent; + const theirsAbsent = theirs === absent; + if (baseAbsent && oursAbsent && theirsAbsent) return [undefined, false]; + if (baseAbsent && oursAbsent) return [theirs, true]; + if (baseAbsent && theirsAbsent) return [ours, true]; + if (baseAbsent && jsonEqual(ours, theirs)) return [ours, true]; + if (baseAbsent) { + addConflict(conflicts, 'add_add', path, 'same path added differently in ours and theirs'); + return [ours, true]; + } + if (oursAbsent && theirsAbsent) return [undefined, false]; + if (oursAbsent && jsonEqual(base, theirs)) return [undefined, false]; + if (theirsAbsent && jsonEqual(base, ours)) return [undefined, false]; + if (oursAbsent) { + addConflict(conflicts, 'delete_edit', path, 'ours deleted a value that theirs edited'); + return [theirs, true]; + } + if (theirsAbsent) { + addConflict(conflicts, 'delete_edit', path, 'theirs deleted a value that ours edited'); + return [ours, true]; + } + return [mergeJsonValue(base, ours, theirs, path, conflicts), true]; +} + +function addConflict( + conflicts: Merge3Conflict[], + category: string, + path: string, + message: string +): void { + conflicts.push({ + conflict_id: `conflict-${conflicts.length + 1}`, + category, + path: path || '/', + message + }); +} + +function jsonPointerJoin(parent: string, token: string): string { + const escaped = token.replaceAll('~', '~0').replaceAll('/', '~1'); + return parent ? `${parent}/${escaped}` : `/${escaped}`; +} + +function jsonEqual(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + +function isRecord(value: unknown): value is Readonly> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function normalizeLanguage(request: Merge3Request): string { + if (request.language?.trim().toLowerCase() === 'json') return 'json'; + if (request.path_name?.toLowerCase().endsWith('.json')) return 'json'; + return request.language?.trim().toLowerCase() ?? ''; +} diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts new file mode 100644 index 0000000..0c0a4dd --- /dev/null +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { merge3, type Merge3Request } from '../src/index'; + +interface Fixture { + readonly contract: { + readonly package: string; + readonly operation: string; + }; + readonly cases: readonly { + readonly case_id: string; + readonly request: Merge3Request; + readonly expected: { + readonly ok: boolean; + readonly merged_json: unknown | null; + readonly conflict_count: number; + readonly conflict_categories?: readonly string[]; + readonly conflict_paths?: readonly string[]; + readonly reparse_after_render: boolean | null; + }; + }[]; +} + +function readFixture(...parts: readonly string[]): Fixture { + const source = readFileSync(path.join('..', 'fixtures', ...parts), 'utf8'); + return JSON.parse(source) as Fixture; +} + +describe('@structuredmerge/ast-merge-git', () => { + it('conforms to the git merge3 contract fixture', () => { + const fixture = readFixture( + 'diagnostics', + 'slice-950-git-merge3-contract', + 'git-merge3-contract.json' + ); + expect(fixture.contract.package).toBe('ast-merge-git'); + expect(fixture.contract.operation).toBe('merge3'); + + for (const testCase of fixture.cases) { + const result = merge3(testCase.request); + expect(result.ok, testCase.case_id).toBe(testCase.expected.ok); + expect(result.conflicts, testCase.case_id).toHaveLength(testCase.expected.conflict_count); + expect(result.reparse_after_render, testCase.case_id).toBe( + testCase.expected.reparse_after_render + ); + if (result.ok) { + expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( + testCase.expected.merged_json + ); + } else { + expect( + result.conflicts.map((conflict) => conflict.category), + testCase.case_id + ).toEqual(testCase.expected.conflict_categories); + expect( + result.conflicts.map((conflict) => conflict.path), + testCase.case_id + ).toEqual(testCase.expected.conflict_paths); + } + } + }); +}); diff --git a/packages/smorg-ts/package.json b/packages/smorg-ts/package.json index 26c8252..84e70af 100644 --- a/packages/smorg-ts/package.json +++ b/packages/smorg-ts/package.json @@ -18,6 +18,7 @@ }, "dependencies": { "@structuredmerge/ast-merge": "workspace:*", + "@structuredmerge/ast-merge-git": "workspace:*", "@structuredmerge/go-merge": "workspace:*", "@structuredmerge/json-merge": "workspace:*", "@structuredmerge/plain-merge": "workspace:*" diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 2f23dfe..aaa3ea8 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -8,6 +8,7 @@ import { promotionProfileJsonKeyedObject } from '@structuredmerge/ast-merge'; import type { MergeResult, ProfilePromotionStatus } from '@structuredmerge/ast-merge'; +import { merge3 } from '@structuredmerge/ast-merge-git'; import { mergeGo } from '@structuredmerge/go-merge'; import { mergeJson } from '@structuredmerge/json-merge'; import { mergeText } from '@structuredmerge/plain-merge'; @@ -118,8 +119,6 @@ function runMergeDriver( stderr.write(`read merge input: ${String(error)}\n`); return exitUserError; } - void ancestorSource; - const effectivePath = options.pathName ?? options.current; const settings = loadPathSettings(effectivePath); const profileExit = reportAndEnforceProfile( @@ -130,7 +129,7 @@ function runMergeDriver( stderr ); if (profileExit !== exitSuccess) return profileExit; - const result = mergeByPath(effectivePath, settings.language, otherSource, currentSource); + const result = mergeByPath(effectivePath, settings.language, ancestorSource, currentSource, otherSource); let output = result.output; if (!result.ok || output === undefined) { if (options.strict || options.fallback === 'none') { @@ -437,14 +436,27 @@ function runLanguages( function mergeByPath( pathName: string, language: string | undefined, - otherSource: string, - currentSource: string + ancestorSource: string, + currentSource: string, + otherSource: string ): MergeResult { switch (normalizeLanguage(language, pathName)) { case 'go': return mergeGo(otherSource, currentSource, 'go'); case 'json': - return mergeJson(otherSource, currentSource, 'json'); + return merge3Result( + merge3({ + base_source: ancestorSource, + ours_source: currentSource, + theirs_source: otherSource, + path_name: pathName, + language: 'json', + dialect: 'json', + profile_id: 'json.keyed-object', + fallback_policy: 'none', + render_policy: 'canonical' + }) + ); case 'jsonc': return mergeJson(otherSource, currentSource, 'jsonc'); default: @@ -452,6 +464,22 @@ function mergeByPath( } } +function merge3Result(result: ReturnType): MergeResult { + if (result.ok && result.merged_source !== undefined) { + return { + ok: true, + diagnostics: result.diagnostics, + output: result.merged_source, + policies: [] + }; + } + return { + ok: false, + diagnostics: result.diagnostics, + policies: [] + }; +} + function normalizeLanguage(language: string | undefined, pathName: string): string { switch (language?.trim().toLowerCase()) { case 'go': diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 93d645a..7775e50 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -94,7 +94,26 @@ describe('smorg-ts cli', () => { ); expect(exit).toBe(exitUnresolvedConflict); - expect(stderr.output()).toContain('destination_parse_error'); + expect(stderr.output()).toContain('parse_error'); + expect(stderr.output()).toContain('ours parse error'); + }); + + it('uses the ancestor for JSON same-key conflicts', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":"ours"}'); + const other = write('other.json', '{"name":"theirs"}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--strict', ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + expect(stderr.output()).toContain('merge_conflict'); + expect(readFileSync(current, 'utf8')).toBe('{"name":"ours"}'); }); it('supports check-only exit-code without writing', () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6f711b..40bf9fd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,12 @@ importers: specifier: workspace:* version: link:../markdown-merge + packages/ast-merge-git: + dependencies: + '@structuredmerge/ast-merge': + specifier: workspace:* + version: link:../ast-merge + packages/ast-template: dependencies: '@structuredmerge/ast-merge': @@ -185,6 +191,9 @@ importers: '@structuredmerge/ast-merge': specifier: workspace:* version: link:../ast-merge + '@structuredmerge/ast-merge-git': + specifier: workspace:* + version: link:../ast-merge-git '@structuredmerge/go-merge': specifier: workspace:* version: link:../go-merge diff --git a/tsconfig.json b/tsconfig.json index 595ab0e..ea852a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,7 @@ "noEmit": true, "paths": { "@structuredmerge/ast-merge": ["./packages/ast-merge/src/index.ts"], + "@structuredmerge/ast-merge-git": ["./packages/ast-merge-git/src/index.ts"], "@structuredmerge/ast-crispr": ["./packages/ast-crispr/src/index.ts"], "@structuredmerge/tree-haver": ["./packages/tree-haver/src/index.ts"], "@structuredmerge/plain-merge": ["./packages/plain-merge/src/index.ts"], diff --git a/vitest.config.ts b/vitest.config.ts index ec7ac66..bf9e407 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ '@structuredmerge/ast-merge': fileURLToPath( new URL('./packages/ast-merge/src/index.ts', import.meta.url) ), + '@structuredmerge/ast-merge-git': fileURLToPath( + new URL('./packages/ast-merge-git/src/index.ts', import.meta.url) + ), '@structuredmerge/tree-haver': fileURLToPath( new URL('./packages/tree-haver/src/index.ts', import.meta.url) ), From ba15b4a49f1a05de78ee4c666d8592d7a8cc00ef Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 14:31:43 -0600 Subject: [PATCH 106/130] Add TypeScript git driver JSON integration test --- packages/smorg-ts/test/cli.test.ts | 95 ++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 7775e50..8dfea1c 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -1,11 +1,14 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import { execFileSync } from 'node:child_process'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { exitSuccess, exitUnresolvedConflict, exitUserError, run } from '../src/cli'; +const repoRoot = process.cwd(); + function writer() { let output = ''; return { @@ -19,6 +22,47 @@ function writer() { }; } +interface GitDriverJsonFixture { + readonly cases: readonly GitDriverJsonCase[]; +} + +interface GitDriverJsonCase { + readonly case_id: string; + readonly path_name: string; + readonly base_source: string; + readonly ours_source: string; + readonly theirs_source: string; + readonly expected: { + readonly exit_code: number; + readonly merged_json?: unknown; + readonly merged_source?: string; + readonly stderr_contains: readonly string[]; + }; +} + +function readGitDriverJsonFixture(): GitDriverJsonFixture { + const source = readFileSync( + path.join( + repoRoot, + '..', + 'fixtures', + 'diagnostics', + 'slice-951-git-driver-json-integration', + 'git-driver-json-integration.json' + ), + 'utf8' + ); + return JSON.parse(source) as GitDriverJsonFixture; +} + +function runGit(dir: string, ...args: readonly string[]): void { + execFileSync('git', args, { + cwd: dir, + env: { ...process.env, GIT_CONFIG_NOSYSTEM: '1' }, + stdio: 'pipe' + }); +} + describe('smorg-ts cli', () => { let previousCwd: string; let dir: string; @@ -116,6 +160,57 @@ describe('smorg-ts cli', () => { expect(readFileSync(current, 'utf8')).toBe('{"name":"ours"}'); }); + it('conforms to the git-driver JSON integration fixture in a repository', () => { + try { + execFileSync('git', ['--version'], { stdio: 'pipe' }); + } catch { + return; + } + const fixture = readGitDriverJsonFixture(); + for (const testCase of fixture.cases) { + const caseDir = mkdtempSync(path.join(tmpdir(), 'smorg-ts-git-driver-')); + try { + runGit(caseDir, 'init'); + runGit(caseDir, 'config', 'user.email', 'smorg-ts@example.invalid'); + runGit(caseDir, 'config', 'user.name', 'smorg-ts test'); + writeFileSync(path.join(caseDir, '.gitattributes'), '*.json merge=smorg-ts smorg.language=json\n'); + writeFileSync(path.join(caseDir, testCase.path_name), testCase.base_source); + runGit(caseDir, 'add', '.'); + runGit(caseDir, 'commit', '-m', 'base'); + + const ancestor = path.join(caseDir, 'ancestor.tmp'); + const current = path.join(caseDir, testCase.path_name); + const other = path.join(caseDir, 'other.tmp'); + writeFileSync(ancestor, testCase.base_source); + writeFileSync(current, testCase.ours_source); + writeFileSync(other, testCase.theirs_source); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--strict', ancestor, current, other, testCase.path_name], + stdout.stream, + stderr.stream + ); + + expect(exit, `${testCase.case_id} stderr=${stderr.output()}`).toBe( + testCase.expected.exit_code + ); + for (const expected of testCase.expected.stderr_contains) { + expect(stderr.output(), testCase.case_id).toContain(expected); + } + const mergedSource = readFileSync(current, 'utf8'); + if (testCase.expected.merged_json !== undefined) { + expect(JSON.parse(mergedSource), testCase.case_id).toEqual(testCase.expected.merged_json); + } else if (testCase.expected.merged_source !== undefined) { + expect(mergedSource, testCase.case_id).toBe(testCase.expected.merged_source); + } + } finally { + rmSync(caseDir, { force: true, recursive: true }); + } + } + }); + it('supports check-only exit-code without writing', () => { const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); const current = write('current.json', '{"name":"structuredmerge","current":true}'); From 0e604d5df643306b016f545fe7bdbf4f44e1d975 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 14:41:53 -0600 Subject: [PATCH 107/130] Clarify merge driver semantic coverage --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 913300d..911fb39 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,10 @@ seven- or nine-argument forms Git passes to external diff commands. `conflicts diff` reports conflict-marker regions in a file that already contains Git conflict markers. +Current semantic merge-driver coverage is fixture-backed for JSON. Other +language and format paths should be treated as git-compatible command surfaces +until their `ast-merge-git` coverage is promoted. + ## Packages Core: From 9e76a196f44972b7a0ae132319bdcb7fd2e8c9f2 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 15:11:40 -0600 Subject: [PATCH 108/130] Write conflicted merge output in smorg-ts --- packages/ast-merge-git/src/index.ts | 23 +++++++++- .../test/fixtures.integration.test.ts | 4 ++ packages/smorg-ts/src/cli.ts | 42 ++++++++++++++++--- packages/smorg-ts/test/cli.test.ts | 9 +++- 4 files changed, 70 insertions(+), 8 deletions(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 8f2099c..d9a68ca 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -25,6 +25,7 @@ export interface Merge3Conflict { export interface Merge3Response { readonly ok: boolean; readonly merged_source?: string; + readonly conflicted_source?: string; readonly conflicts: readonly Merge3Conflict[]; readonly diagnostics: readonly Diagnostic[]; readonly fallbacks: readonly string[]; @@ -71,6 +72,10 @@ export function merge3Json(request: Merge3Request): Merge3Response { return response(request, { ok: false, conflicts, + conflicted_source: renderConflictSource(request, conflicts), + render_report: { + strategy: 'full_file_conflict_markers' + }, diagnostics: [ { severity: 'error', @@ -112,6 +117,7 @@ function response( return { ok: fields.ok, merged_source: fields.merged_source, + conflicted_source: fields.conflicted_source, conflicts: fields.conflicts ?? [], diagnostics: fields.diagnostics ?? [], fallbacks: fields.fallbacks ?? [], @@ -121,7 +127,7 @@ function response( dialect: request.dialect ?? '' }, render_report: { - strategy: request.render_policy || 'canonical' + strategy: fields.render_report?.strategy ?? request.render_policy ?? 'canonical' }, formatting_preservation: fields.formatting_preservation ?? { line_diff_score: 0, @@ -131,6 +137,21 @@ function response( }; } +function renderConflictSource(request: Merge3Request, conflicts: readonly Merge3Conflict[]): string { + const markerSize = Math.max(request.conflict_marker_size ?? 7, 1); + return [ + `/* smorg structured conflicts: ${conflicts.length} unresolved */`, + `${'<'.repeat(markerSize)} ours`, + request.ours_source, + `${'|'.repeat(markerSize)} base`, + request.base_source, + '='.repeat(markerSize), + request.theirs_source, + `${'>'.repeat(markerSize)} theirs`, + '' + ].join('\n'); +} + function parseJsonRole(role: string, source: string): unknown { try { return JSON.parse(source); diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 0c0a4dd..c1e27c6 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -17,6 +17,7 @@ interface Fixture { readonly conflict_count: number; readonly conflict_categories?: readonly string[]; readonly conflict_paths?: readonly string[]; + readonly conflicted_source_contains?: readonly string[]; readonly reparse_after_render: boolean | null; }; }[]; @@ -57,6 +58,9 @@ describe('@structuredmerge/ast-merge-git', () => { result.conflicts.map((conflict) => conflict.path), testCase.case_id ).toEqual(testCase.expected.conflict_paths); + for (const needle of testCase.expected.conflicted_source_contains ?? []) { + expect(result.conflicted_source, testCase.case_id).toContain(needle); + } } } }); diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index aaa3ea8..8934018 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -129,14 +129,34 @@ function runMergeDriver( stderr ); if (profileExit !== exitSuccess) return profileExit; - const result = mergeByPath(effectivePath, settings.language, ancestorSource, currentSource, otherSource); + const result = mergeByPath( + effectivePath, + settings.language, + settings.conflictMarkerSize, + ancestorSource, + currentSource, + otherSource + ); let output = result.output; - if (!result.ok || output === undefined) { - if (options.strict || options.fallback === 'none') { - printDiagnostics(stderr, result); - return exitUnresolvedConflict; + if (!result.ok) { + printDiagnostics(stderr, result); + if (output === undefined && !options.strict && options.fallback !== 'none') { + output = currentSource; + } + if (options.checkOnly) return exitUnresolvedConflict; + if (output !== undefined) { + try { + writeFileSync(options.output ?? options.current, output); + } catch (error) { + stderr.write(`write output: ${String(error)}\n`); + return exitInternalError; + } } - output = currentSource; + return exitUnresolvedConflict; + } + if (output === undefined) { + stderr.write('merge completed without output\n'); + return exitInternalError; } if (options.checkOnly) { @@ -436,6 +456,7 @@ function runLanguages( function mergeByPath( pathName: string, language: string | undefined, + conflictMarkerSize: number, ancestorSource: string, currentSource: string, otherSource: string @@ -454,6 +475,7 @@ function mergeByPath( dialect: 'json', profile_id: 'json.keyed-object', fallback_policy: 'none', + conflict_marker_size: conflictMarkerSize, render_policy: 'canonical' }) ); @@ -473,6 +495,14 @@ function merge3Result(result: ReturnType): MergeResult { policies: [] }; } + if (!result.ok && result.conflicted_source !== undefined) { + return { + ok: false, + diagnostics: result.diagnostics, + output: result.conflicted_source, + policies: [] + }; + } return { ok: false, diagnostics: result.diagnostics, diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 8dfea1c..52c4374 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -36,6 +36,7 @@ interface GitDriverJsonCase { readonly exit_code: number; readonly merged_json?: unknown; readonly merged_source?: string; + readonly conflicted_source_contains?: readonly string[]; readonly stderr_contains: readonly string[]; }; } @@ -157,7 +158,10 @@ describe('smorg-ts cli', () => { expect(exit).toBe(exitUnresolvedConflict); expect(stderr.output()).toContain('merge_conflict'); - expect(readFileSync(current, 'utf8')).toBe('{"name":"ours"}'); + const currentSource = readFileSync(current, 'utf8'); + for (const needle of ['<<<<<<< ours', '||||||| base', '=======', '>>>>>>> theirs']) { + expect(currentSource).toContain(needle); + } }); it('conforms to the git-driver JSON integration fixture in a repository', () => { @@ -205,6 +209,9 @@ describe('smorg-ts cli', () => { } else if (testCase.expected.merged_source !== undefined) { expect(mergedSource, testCase.case_id).toBe(testCase.expected.merged_source); } + for (const expected of testCase.expected.conflicted_source_contains ?? []) { + expect(mergedSource, testCase.case_id).toContain(expected); + } } finally { rmSync(caseDir, { force: true, recursive: true }); } From 7a6c70f5c9fc9a6c26f2d0471da7cce24a845b83 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 15:14:39 -0600 Subject: [PATCH 109/130] Write full-file fallback conflicts in smorg-ts --- packages/smorg-ts/src/cli.ts | 26 +++++++++++++++++++++++++- packages/smorg-ts/test/cli.test.ts | 21 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 8934018..f76dcce 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -141,7 +141,12 @@ function runMergeDriver( if (!result.ok) { printDiagnostics(stderr, result); if (output === undefined && !options.strict && options.fallback !== 'none') { - output = currentSource; + output = fullFileConflictOutput( + settings.conflictMarkerSize, + ancestorSource, + currentSource, + otherSource + ); } if (options.checkOnly) return exitUnresolvedConflict; if (output !== undefined) { @@ -172,6 +177,25 @@ function runMergeDriver( return exitSuccess; } +function fullFileConflictOutput( + markerSize: number, + ancestorSource: string, + currentSource: string, + otherSource: string +): string { + const size = Math.max(markerSize, 1); + return [ + `${'<'.repeat(size)} ours`, + currentSource, + `${'|'.repeat(size)} base`, + ancestorSource, + '='.repeat(size), + otherSource, + `${'>'.repeat(size)} theirs`, + '' + ].join('\n'); +} + function parseMergeDriverOptions( args: readonly string[], stderr: Pick diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 52c4374..fd98015 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -143,6 +143,27 @@ describe('smorg-ts cli', () => { expect(stderr.output()).toContain('ours parse error'); }); + it('writes full-file conflict markers for non-strict fallback failures', () => { + const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); + const current = write('current.json', '{"name":'); + const other = write('other.json', '{"other":true}'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + const currentSource = readFileSync(current, 'utf8'); + for (const needle of ['<<<<<<< ours', '||||||| base', '=======', '>>>>>>> theirs']) { + expect(currentSource).toContain(needle); + } + expect(stderr.output()).toContain('parse_error'); + }); + it('uses the ancestor for JSON same-key conflicts', () => { const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); const current = write('current.json', '{"name":"ours"}'); From 9f372eacdee80d529bdf804ff9101b79e6dc4d48 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 15:18:44 -0600 Subject: [PATCH 110/130] Exercise fallback fixture in smorg-ts --- packages/smorg-ts/test/cli.test.ts | 69 ++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index fd98015..2ebf38d 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -41,6 +41,28 @@ interface GitDriverJsonCase { }; } +interface GitDriverFallbackFixture { + readonly cases: readonly GitDriverFallbackCase[]; +} + +interface GitDriverFallbackCase { + readonly case_id: string; + readonly path_name: string; + readonly base_source: string; + readonly ours_source: string; + readonly theirs_source: string; + readonly options: { + readonly strict?: boolean; + readonly fallback?: string; + }; + readonly expected: { + readonly exit_code: number; + readonly merged_source?: string; + readonly source_contains?: readonly string[]; + readonly stderr_contains: readonly string[]; + }; +} + function readGitDriverJsonFixture(): GitDriverJsonFixture { const source = readFileSync( path.join( @@ -56,6 +78,21 @@ function readGitDriverJsonFixture(): GitDriverJsonFixture { return JSON.parse(source) as GitDriverJsonFixture; } +function readGitDriverFallbackFixture(): GitDriverFallbackFixture { + const source = readFileSync( + path.join( + repoRoot, + '..', + 'fixtures', + 'diagnostics', + 'slice-954-git-driver-fallback', + 'git-driver-fallback.json' + ), + 'utf8' + ); + return JSON.parse(source) as GitDriverFallbackFixture; +} + function runGit(dir: string, ...args: readonly string[]): void { execFileSync('git', args, { cwd: dir, @@ -164,6 +201,38 @@ describe('smorg-ts cli', () => { expect(stderr.output()).toContain('parse_error'); }); + it('conforms to the git-driver fallback fixture', () => { + const fixture = readGitDriverFallbackFixture(); + for (const testCase of fixture.cases) { + const ancestor = write('ancestor.json', testCase.base_source); + const current = write('current.json', testCase.ours_source); + const other = write('other.json', testCase.theirs_source); + const args = ['merge-driver']; + if (testCase.options.strict) args.push('--strict'); + if (testCase.options.fallback && testCase.options.fallback !== 'full-file') { + args.push('--fallback', testCase.options.fallback); + } + args.push(ancestor, current, other, testCase.path_name); + const stdout = writer(); + const stderr = writer(); + + const exit = run(args, stdout.stream, stderr.stream); + const currentSource = readFileSync(current, 'utf8'); + expect(exit, `${testCase.case_id} stderr=${stderr.output()}`).toBe( + testCase.expected.exit_code + ); + if (testCase.expected.merged_source !== undefined) { + expect(currentSource, testCase.case_id).toBe(testCase.expected.merged_source); + } + for (const needle of testCase.expected.source_contains ?? []) { + expect(currentSource, testCase.case_id).toContain(needle); + } + for (const needle of testCase.expected.stderr_contains) { + expect(stderr.output(), testCase.case_id).toContain(needle); + } + } + }); + it('uses the ancestor for JSON same-key conflicts', () => { const ancestor = write('ancestor.json', '{"name":"structuredmerge"}'); const current = write('current.json', '{"name":"ours"}'); From d54ca377db23d3e0965cfc954f020f96027671b7 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 15:48:17 -0600 Subject: [PATCH 111/130] Port comment trivia attachment contract to TypeScript --- packages/ast-merge/src/contracts.ts | 228 ++++++++++++++++++ packages/ast-merge/src/index.ts | 28 +++ .../test/fixtures.integration.test.ts | 121 ++++++++++ 3 files changed, 377 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 9100d58..90465ff 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -478,6 +478,234 @@ export interface MergeIR { readonly diagnostics: readonly string[]; } +export interface LineRange { + readonly start_line: number; + readonly end_line: number; +} + +export interface CommentOwnerNode { + readonly id: string; + readonly kind: string; + readonly semantic_roles: readonly string[]; + readonly line_range: LineRange; +} + +export interface CommentStyleDefinition { + readonly style: string; + readonly line_prefix: string | null; + readonly block_prefix: string | null; + readonly block_suffix: string | null; +} + +export interface CommentLine { + readonly text: string; + readonly line_number: number; + readonly normalized_content: string; +} + +export interface CommentRegion { + readonly id: string; + readonly kind: string; + readonly style: string; + readonly owner_id: string; + readonly floating: boolean; + readonly nodes: readonly CommentLine[]; +} + +export function commentRegionStartLine(region: CommentRegion): number | undefined { + return region.nodes.reduce( + (start, node) => (start === undefined ? node.line_number : Math.min(start, node.line_number)), + undefined + ); +} + +export function commentRegionEndLine(region: CommentRegion): number | undefined { + return region.nodes.reduce( + (end, node) => (end === undefined ? node.line_number : Math.max(end, node.line_number)), + undefined + ); +} + +export function commentRegionText(region: CommentRegion): string { + return region.nodes.map((node) => node.text).join('\n'); +} + +export function commentRegionNormalizedContent(region: CommentRegion): string { + return region.nodes.map((node) => node.normalized_content).join('\n'); +} + +export function commentRegionSignature(region: CommentRegion): readonly string[] { + return ['comment_region', region.kind, commentRegionNormalizedContent(region).slice(0, 121)]; +} + +export function commentRegionFreezeActions( + region: CommentRegion, + token: string +): readonly string[] { + if (token.length === 0) return []; + const prefix = `${token.toLowerCase()}:`; + return region.nodes.flatMap((node) => { + const lower = node.text.toLowerCase(); + if (lower.includes(`${prefix}freeze`)) return ['freeze']; + if (lower.includes(`${prefix}unfreeze`)) return ['unfreeze']; + return []; + }); +} + +export interface LayoutGap { + readonly id: string; + readonly kind: string; + readonly start_line: number; + readonly end_line: number; + readonly lines: readonly string[]; + readonly before_owner_id: string | null; + readonly after_owner_id: string | null; + readonly controller_side: string; + readonly metadata?: Readonly>; +} + +export function layoutGapLineCount(gap: LayoutGap): number { + return gap.end_line - gap.start_line + 1; +} + +export function layoutGapBlankLineCount(gap: LayoutGap): number { + return gap.lines.filter((line) => line.trim().length === 0).length; +} + +export function layoutGapOwnerIdFor(gap: LayoutGap, side: string): string | undefined { + if (side === 'before') return gap.before_owner_id ?? undefined; + if (side === 'after') return gap.after_owner_id ?? undefined; + return undefined; +} + +export function layoutGapControllerOwnerId(gap: LayoutGap): string | undefined { + return layoutGapOwnerIdFor(gap, gap.controller_side); +} + +export function layoutGapFallbackOwnerId(gap: LayoutGap): string | undefined { + if (gap.controller_side === 'before') return gap.after_owner_id ?? undefined; + if (gap.controller_side === 'after') return gap.before_owner_id ?? undefined; + return undefined; +} + +export function layoutGapEffectiveControllerOwnerId( + gap: LayoutGap, + removedOwners: ReadonlySet +): string | undefined { + const controller = layoutGapControllerOwnerId(gap); + if (controller !== undefined && !removedOwners.has(controller)) return controller; + const fallback = layoutGapFallbackOwnerId(gap); + if (fallback !== undefined && !removedOwners.has(fallback)) return fallback; + return undefined; +} + +export function layoutGapLeadingFor(gap: LayoutGap, ownerId: string): boolean { + return gap.after_owner_id === ownerId; +} + +export function layoutGapTrailingFor(gap: LayoutGap, ownerId: string): boolean { + return gap.before_owner_id === ownerId; +} + +export function layoutGapControlsOutputFor(gap: LayoutGap, ownerId: string): boolean { + return layoutGapControllerOwnerId(gap) === ownerId; +} + +export interface CommentAttachment { + readonly owner_id: string; + readonly leading_region_id: string | null; + readonly inline_region_id: string | null; + readonly trailing_region_id: string | null; + readonly orphan_region_ids: readonly string[]; + readonly leading_gap_id: string | null; + readonly trailing_gap_id: string | null; + readonly metadata: Readonly>; +} + +export function commentAttachmentRegionCount( + attachment: CommentAttachment, + regions: ReadonlyMap +): number { + const direct = [ + attachment.leading_region_id, + attachment.inline_region_id, + attachment.trailing_region_id + ].filter((id): id is string => id !== null && regions.has(id)).length; + return direct + attachment.orphan_region_ids.filter((id) => regions.has(id)).length; +} + +export function commentAttachmentLayoutGapCount( + attachment: CommentAttachment, + gaps: ReadonlyMap +): number { + return new Set( + [attachment.leading_gap_id, attachment.trailing_gap_id].filter( + (id): id is string => id !== null && gaps.has(id) + ) + ).size; +} + +export function commentAttachmentEmpty( + attachment: CommentAttachment, + regions: ReadonlyMap +): boolean { + return commentAttachmentRegionCount(attachment, regions) === 0; +} + +export function commentAttachmentLeadingRegionLayoutOwned( + attachment: CommentAttachment, + regions: ReadonlyMap, + gaps: ReadonlyMap +): boolean { + if (attachment.leading_region_id === null || attachment.leading_gap_id === null) return false; + const region = regions.get(attachment.leading_region_id); + const gap = gaps.get(attachment.leading_gap_id); + return ( + region !== undefined && + gap !== undefined && + region.floating && + layoutGapLeadingFor(gap, attachment.owner_id) && + layoutGapControlsOutputFor(gap, attachment.owner_id) + ); +} + +export function commentAttachmentTrailingRegionLayoutOwned( + attachment: CommentAttachment, + regions: ReadonlyMap, + gaps: ReadonlyMap +): boolean { + if (attachment.trailing_region_id === null || attachment.trailing_gap_id === null) return false; + const region = regions.get(attachment.trailing_region_id); + const gap = gaps.get(attachment.trailing_gap_id); + return ( + region !== undefined && + gap !== undefined && + region.floating && + layoutGapTrailingFor(gap, attachment.owner_id) && + layoutGapControlsOutputFor(gap, attachment.owner_id) + ); +} + +export function commentAttachmentFreezeMarker( + attachment: CommentAttachment, + regions: ReadonlyMap, + token: string +): boolean { + return [ + attachment.leading_region_id, + attachment.inline_region_id, + attachment.trailing_region_id, + ...attachment.orphan_region_ids + ] + .filter((id): id is string => id !== null) + .some((id) => { + const region = regions.get(id); + if (region === undefined) return false; + const actions = commentRegionFreezeActions(region, token); + return actions.includes('freeze') || actions.includes('unfreeze'); + }); +} + export interface PairwiseNodeMatch { readonly from_node_id: string; readonly to_node_id: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index e55a06a..19db02a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -18,6 +18,11 @@ export type { ClassMappingDiagnostic, ClassMappingNodeClass, ClassMappingReport, + CommentAttachment, + CommentLine, + CommentOwnerNode, + CommentRegion, + CommentStyleDefinition, ConflictCategoryReport, ConflictHandlerRegistration, ConflictHandlerRegistryReport, @@ -95,6 +100,8 @@ export type { LanguageBackendProfileRules, LanguageProfileHandlerRegistration, LanguageProfileHandlerRegistry, + LayoutGap, + LineRange, LineSpan, LocalLineFallbackReport, MergeConflict, @@ -496,7 +503,28 @@ export { runTemplateTreeExecutionFromDirectories, planTemplateTreeExecutionFromDirectories, applyTemplateTreeExecutionToDirectory, + commentAttachmentEmpty, + commentAttachmentFreezeMarker, + commentAttachmentLayoutGapCount, + commentAttachmentLeadingRegionLayoutOwned, + commentAttachmentRegionCount, + commentAttachmentTrailingRegionLayoutOwned, + commentRegionEndLine, + commentRegionFreezeActions, + commentRegionNormalizedContent, + commentRegionSignature, + commentRegionStartLine, + commentRegionText, reportTemplateTreeRun, + layoutGapBlankLineCount, + layoutGapControlsOutputFor, + layoutGapControllerOwnerId, + layoutGapEffectiveControllerOwnerId, + layoutGapFallbackOwnerId, + layoutGapLeadingFor, + layoutGapLineCount, + layoutGapOwnerIdFor, + layoutGapTrailingFor, reportTemplateDirectoryApply, reportTemplateDirectoryPlan, reportTemplateDirectoryRunner, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 0446be2..421682b 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -7,10 +7,27 @@ import { mergeMarkdown } from '../../markdown-merge/src/index'; import { mergeToml } from '../../toml-merge/src/index'; import { mergeRuby } from '../../ruby-merge/src/index'; import { + commentAttachmentEmpty, + commentAttachmentFreezeMarker, + commentAttachmentLayoutGapCount, + commentAttachmentLeadingRegionLayoutOwned, + commentAttachmentRegionCount, + commentAttachmentTrailingRegionLayoutOwned, + commentRegionEndLine, + commentRegionFreezeActions, + commentRegionNormalizedContent, + commentRegionSignature, + commentRegionStartLine, + commentRegionText, evaluateProfilePromotion, evaluateProfileSelectionRequirement, executeGenericConflictHandler, initialProfilePromotionPolicy, + layoutGapBlankLineCount, + layoutGapControllerOwnerId, + layoutGapEffectiveControllerOwnerId, + layoutGapFallbackOwnerId, + layoutGapLineCount, promotionProfileJsonKeyedObject, promotionProfileRubyGemspecDependencyDeclarations, validateLanguageBackendProfile @@ -55,6 +72,10 @@ import type { FallbackUsageReport, ChangeSet, ClassMappingReport, + CommentAttachment, + CommentOwnerNode, + CommentRegion, + CommentStyleDefinition, ConflictCategoryReport, ConflictHandlerRegistryReport, ConflictMarkerRenderingReport, @@ -72,6 +93,7 @@ import type { InconsistencyReport, LanguageBackendProfile, LanguageProfileHandlerRegistry, + LayoutGap, LocalLineFallbackReport, MatchingDebugArtifacts, MergeIR, @@ -5792,6 +5814,105 @@ describe('ast-merge shared fixtures', () => { expect(mergeIR.changes[1]?.class_id).toBe('class-import-strings'); }); + it('conforms to the slice-955 comment trivia attachment contract fixture', () => { + const fixture = readFixture<{ + owner_nodes: readonly CommentOwnerNode[]; + comment_styles: readonly CommentStyleDefinition[]; + comment_regions: readonly (CommentRegion & { + expected: { + start_line: number; + end_line: number; + text: string; + normalized_content: string; + signature: readonly string[]; + freeze_actions: readonly string[]; + }; + })[]; + layout_gaps: readonly (LayoutGap & { + expected: { + line_count: number; + blank_line_count: number; + controller_owner_id: string | null; + fallback_owner_id: string | null; + effective_controller_with_after_removed?: string; + }; + })[]; + attachments: readonly (CommentAttachment & { + expected: { + region_count: number; + layout_gap_count: number; + empty: boolean; + leading_region_layout_owned: boolean; + trailing_region_layout_owned: boolean; + freeze_marker: boolean; + }; + })[]; + contract_rules: readonly string[]; + expected: { + owner_count: number; + comment_region_count: number; + layout_gap_count: number; + attachment_count: number; + }; + }>( + 'diagnostics', + 'slice-955-comment-trivia-attachment-contract', + 'comment-trivia-attachment-contract.json' + ); + + const regions = new Map(fixture.comment_regions.map((region) => [region.id, region])); + const gaps = new Map(fixture.layout_gaps.map((gap) => [gap.id, gap])); + + expect(fixture.owner_nodes).toHaveLength(fixture.expected.owner_count); + expect(fixture.comment_regions).toHaveLength(fixture.expected.comment_region_count); + expect(fixture.layout_gaps).toHaveLength(fixture.expected.layout_gap_count); + expect(fixture.attachments).toHaveLength(fixture.expected.attachment_count); + expect(fixture.comment_styles[0]?.style).toBe('hash_comment'); + expect(fixture.comment_styles[0]?.line_prefix).toBe('#'); + + for (const region of fixture.comment_regions) { + expect(commentRegionStartLine(region)).toBe(region.expected.start_line); + expect(commentRegionEndLine(region)).toBe(region.expected.end_line); + expect(commentRegionText(region)).toBe(region.expected.text); + expect(commentRegionNormalizedContent(region)).toBe(region.expected.normalized_content); + expect(commentRegionSignature(region)).toEqual(region.expected.signature); + expect(commentRegionFreezeActions(region, 'smorg')).toEqual(region.expected.freeze_actions); + } + + for (const gap of fixture.layout_gaps) { + expect(layoutGapLineCount(gap)).toBe(gap.expected.line_count); + expect(layoutGapBlankLineCount(gap)).toBe(gap.expected.blank_line_count); + expect(layoutGapControllerOwnerId(gap) ?? null).toBe(gap.expected.controller_owner_id); + expect(layoutGapFallbackOwnerId(gap) ?? null).toBe(gap.expected.fallback_owner_id); + if (gap.expected.effective_controller_with_after_removed !== undefined) { + expect(layoutGapEffectiveControllerOwnerId(gap, new Set([gap.after_owner_id!]))).toBe( + gap.expected.effective_controller_with_after_removed + ); + } + } + + for (const attachment of fixture.attachments) { + expect(commentAttachmentRegionCount(attachment, regions)).toBe( + attachment.expected.region_count + ); + expect(commentAttachmentLayoutGapCount(attachment, gaps)).toBe( + attachment.expected.layout_gap_count + ); + expect(commentAttachmentEmpty(attachment, regions)).toBe(attachment.expected.empty); + expect(commentAttachmentLeadingRegionLayoutOwned(attachment, regions, gaps)).toBe( + attachment.expected.leading_region_layout_owned + ); + expect(commentAttachmentTrailingRegionLayoutOwned(attachment, regions, gaps)).toBe( + attachment.expected.trailing_region_layout_owned + ); + expect(commentAttachmentFreezeMarker(attachment, regions, 'smorg')).toBe( + attachment.expected.freeze_marker + ); + } + + expect(fixture.contract_rules.some((rule) => rule.includes('passive data'))).toBe(true); + }); + it('conforms to the slice-906 merge engine suite setting fixture', () => { const fixture = readFixture<{ settings: { From b22e0b907b5ea2fc637a73c69e400fde3e27d2e5 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 17:52:04 -0600 Subject: [PATCH 112/130] Conform to merge result decision fixture --- packages/ast-merge/src/contracts.ts | 32 +++++++++++++++++ packages/ast-merge/src/index.ts | 4 +++ .../test/fixtures.integration.test.ts | 35 +++++++++++++++++++ 3 files changed, 71 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 90465ff..1ba65b8 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -441,6 +441,38 @@ export interface MergeResult { readonly policies?: readonly PolicyReference[]; } +export interface MergeDecisionRecord { + readonly id: string; + readonly decision: string; + readonly source: string; + readonly line: number; + readonly owner_id: string; + readonly reason: string; + readonly metadata?: Readonly>; +} + +export function mergeDecisionSummary( + decisions: readonly MergeDecisionRecord[] +): Readonly> { + return decisions.reduce>((summary, decision) => { + summary[decision.decision] = (summary[decision.decision] ?? 0) + 1; + return summary; + }, {}); +} + +export function mergeDecisionSourceSummary( + decisions: readonly MergeDecisionRecord[] +): Readonly> { + return decisions.reduce>((summary, decision) => { + summary[decision.source] = (summary[decision.source] ?? 0) + 1; + return summary; + }, {}); +} + +export function mergeDecisionReviewRequired(decisions: readonly MergeDecisionRecord[]): boolean { + return decisions.some((decision) => decision.decision === 'unresolved'); +} + export interface MergeIRNodeClass { readonly class_id: string; readonly signature: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 19db02a..825880a 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -151,6 +151,7 @@ export type { DiscoveredSurface, DelegatedChildOperation, ReviewDiagnosticReason, + MergeDecisionRecord, MergeIR, MergeIRChange, MergeIRNodeClass, @@ -515,6 +516,9 @@ export { commentRegionSignature, commentRegionStartLine, commentRegionText, + mergeDecisionReviewRequired, + mergeDecisionSourceSummary, + mergeDecisionSummary, reportTemplateTreeRun, layoutGapBlankLineCount, layoutGapControlsOutputFor, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 421682b..217b240 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -28,6 +28,9 @@ import { layoutGapEffectiveControllerOwnerId, layoutGapFallbackOwnerId, layoutGapLineCount, + mergeDecisionReviewRequired, + mergeDecisionSourceSummary, + mergeDecisionSummary, promotionProfileJsonKeyedObject, promotionProfileRubyGemspecDependencyDeclarations, validateLanguageBackendProfile @@ -96,6 +99,7 @@ import type { LayoutGap, LocalLineFallbackReport, MatchingDebugArtifacts, + MergeDecisionRecord, MergeIR, MergeIRComparisonReport, MoveDetectionMatchingReport, @@ -5913,6 +5917,37 @@ describe('ast-merge shared fixtures', () => { expect(fixture.contract_rules.some((rule) => rule.includes('passive data'))).toBe(true); }); + it('conforms to the slice-956 merge result decision contract fixture', () => { + const fixture = readFixture<{ + decisions: readonly MergeDecisionRecord[]; + expected: { + decision_count: number; + ordered_decision_ids: readonly string[]; + decision_summary: Readonly>; + source_summary: Readonly>; + unresolved_count: number; + review_required: boolean; + line_count: number; + }; + }>( + 'diagnostics', + 'slice-956-merge-result-decision-contract', + 'merge-result-decision-contract.json' + ); + const orderedIds = fixture.decisions.map((decision) => decision.id); + const unresolvedCount = fixture.decisions.filter( + (decision) => decision.decision === 'unresolved' + ).length; + + expect(fixture.decisions).toHaveLength(fixture.expected.decision_count); + expect(orderedIds).toEqual(fixture.expected.ordered_decision_ids); + expect(mergeDecisionSummary(fixture.decisions)).toEqual(fixture.expected.decision_summary); + expect(mergeDecisionSourceSummary(fixture.decisions)).toEqual(fixture.expected.source_summary); + expect(unresolvedCount).toBe(fixture.expected.unresolved_count); + expect(mergeDecisionReviewRequired(fixture.decisions)).toBe(fixture.expected.review_required); + expect(fixture.decisions).toHaveLength(fixture.expected.line_count); + }); + it('conforms to the slice-906 merge engine suite setting fixture', () => { const fixture = readFixture<{ settings: { From f0886d5b511fd9a8ccb9b2aa99dd9cdd7480865b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 18:15:34 -0600 Subject: [PATCH 113/130] Conform to freeze directive execution fixture --- packages/ast-merge/src/contracts.ts | 116 ++++++++++++++++++ packages/ast-merge/src/index.ts | 4 + .../test/fixtures.integration.test.ts | 72 +++++++++++ 3 files changed, 192 insertions(+) diff --git a/packages/ast-merge/src/contracts.ts b/packages/ast-merge/src/contracts.ts index 1ba65b8..7906561 100644 --- a/packages/ast-merge/src/contracts.ts +++ b/packages/ast-merge/src/contracts.ts @@ -473,6 +473,122 @@ export function mergeDecisionReviewRequired(decisions: readonly MergeDecisionRec return decisions.some((decision) => decision.decision === 'unresolved'); } +export interface FreezeDirectiveBlock { + readonly id: string; + readonly style?: string; + readonly token?: string; + readonly start_line: number; + readonly end_line: number; + readonly reason: string; + readonly content_lines: readonly string[]; + readonly merge_policy: string; + readonly decision: string; + readonly signature: readonly string[]; +} + +export interface FreezeDirectiveDiagnostic { + readonly category: string; + readonly severity: string; + readonly message?: string; + readonly line: number; +} + +export function detectFreezeDirectiveBlocks( + caseId: string, + lines: readonly string[], + token: string, + style: string +): { blocks: readonly FreezeDirectiveBlock[]; diagnostics: readonly FreezeDirectiveDiagnostic[] } { + const blocks: FreezeDirectiveBlock[] = []; + const diagnostics: FreezeDirectiveDiagnostic[] = []; + let openLine: number | undefined; + let reason = ''; + + lines.forEach((line, index) => { + const lineNumber = index + 1; + const marker = freezeDirectiveAction(line, token, style); + if (marker === undefined) return; + + if (marker.action === 'freeze') { + if (openLine !== undefined) { + diagnostics.push({ category: 'nested_freeze_open', severity: 'error', line: lineNumber }); + return; + } + openLine = lineNumber; + reason = marker.reason; + return; + } + + if (openLine === undefined) { + diagnostics.push({ category: 'unmatched_freeze_close', severity: 'error', line: lineNumber }); + return; + } + const start = openLine; + const end = lineNumber; + blocks.push({ + id: `freeze:${caseId}:${start}-${end}`, + style, + token, + start_line: start, + end_line: end, + reason, + content_lines: lines.slice(start - 1, end), + merge_policy: 'destination', + decision: 'freeze_block', + signature: ['freeze_block', String(start), String(end)] + }); + openLine = undefined; + reason = ''; + }); + + if (openLine !== undefined) { + diagnostics.push({ category: 'unclosed_freeze_open', severity: 'error', line: openLine }); + return { blocks: [], diagnostics }; + } + if (diagnostics.length > 0) return { blocks: [], diagnostics }; + return { blocks, diagnostics }; +} + +export function freezeDirectiveBlockForLine( + blocks: readonly FreezeDirectiveBlock[], + line: number +): FreezeDirectiveBlock | undefined { + return blocks.find((block) => line >= block.start_line && line <= block.end_line); +} + +function freezeDirectiveAction( + line: string, + token: string, + style: string +): { action: 'freeze' | 'unfreeze'; reason: string } | undefined { + const content = freezeDirectiveCommentContent(line, style); + if (content === undefined) return undefined; + const prefix = `${token.toLowerCase()}:`; + if (!content.toLowerCase().startsWith(prefix)) return undefined; + const remainder = content.slice(prefix.length).trim(); + const lower = remainder.toLowerCase(); + if (lower.startsWith('unfreeze')) { + return { action: 'unfreeze', reason: remainder.slice('unfreeze'.length).trim() }; + } + if (lower.startsWith('freeze')) { + return { action: 'freeze', reason: remainder.slice('freeze'.length).trim() }; + } + return undefined; +} + +function freezeDirectiveCommentContent(line: string, style: string): string | undefined { + const trimmed = line.trim(); + if (style === 'hash_comment' && trimmed.startsWith('#')) return trimmed.slice(1).trim(); + if (style === 'c_style_line' && trimmed.startsWith('//')) return trimmed.slice(2).trim(); + if (style === 'html_comment' && trimmed.startsWith('')) { + return trimmed.slice(4, -3).trim(); + } + if (style === 'c_style_block' && trimmed.startsWith('/*') && trimmed.endsWith('*/')) { + return trimmed.slice(2, -2).trim(); + } + return undefined; +} + export interface MergeIRNodeClass { readonly class_id: string; readonly signature: string; diff --git a/packages/ast-merge/src/index.ts b/packages/ast-merge/src/index.ts index 825880a..14181e4 100644 --- a/packages/ast-merge/src/index.ts +++ b/packages/ast-merge/src/index.ts @@ -75,6 +75,8 @@ export type { FallbackUsageSummary, FalseTextualConflictCase, FalseTextualConflictSuite, + FreezeDirectiveBlock, + FreezeDirectiveDiagnostic, FormattingEdgeFixtureCase, FormattingEdgeFixtureSuite, FormattingHardGate, @@ -516,6 +518,8 @@ export { commentRegionSignature, commentRegionStartLine, commentRegionText, + detectFreezeDirectiveBlocks, + freezeDirectiveBlockForLine, mergeDecisionReviewRequired, mergeDecisionSourceSummary, mergeDecisionSummary, diff --git a/packages/ast-merge/test/fixtures.integration.test.ts b/packages/ast-merge/test/fixtures.integration.test.ts index 217b240..c820c44 100644 --- a/packages/ast-merge/test/fixtures.integration.test.ts +++ b/packages/ast-merge/test/fixtures.integration.test.ts @@ -19,9 +19,11 @@ import { commentRegionSignature, commentRegionStartLine, commentRegionText, + detectFreezeDirectiveBlocks, evaluateProfilePromotion, evaluateProfileSelectionRequirement, executeGenericConflictHandler, + freezeDirectiveBlockForLine, initialProfilePromotionPolicy, layoutGapBlankLineCount, layoutGapControllerOwnerId, @@ -84,6 +86,8 @@ import type { ConflictMarkerRenderingReport, FamilyFeatureProfile, FalseTextualConflictSuite, + FreezeDirectiveBlock, + FreezeDirectiveDiagnostic, FormattingEdgeFixtureSuite, FormattingHardGateReport, FormattingPreservationConformanceReport, @@ -5948,6 +5952,74 @@ describe('ast-merge shared fixtures', () => { expect(fixture.decisions).toHaveLength(fixture.expected.line_count); }); + it('conforms to the slice-957 freeze directive execution contract fixture', () => { + const fixture = readFixture<{ + cases: readonly { + id: string; + style: string; + token: string; + lines: readonly string[]; + expected: { + valid: boolean; + block_count: number; + diagnostic_count: number; + blocks: readonly FreezeDirectiveBlock[]; + diagnostics?: readonly FreezeDirectiveDiagnostic[]; + line_queries: readonly { + line: number; + in_freeze: boolean; + block_id: string | null; + }[]; + }; + }[]; + expected: { + case_count: number; + valid_case_count: number; + invalid_case_count: number; + }; + }>( + 'diagnostics', + 'slice-957-freeze-directive-execution-contract', + 'freeze-directive-execution-contract.json' + ); + let validCount = 0; + let invalidCount = 0; + + for (const testCase of fixture.cases) { + const { blocks, diagnostics } = detectFreezeDirectiveBlocks( + testCase.id, + testCase.lines, + testCase.token, + testCase.style + ); + if (testCase.expected.valid) validCount += 1; + else invalidCount += 1; + + expect(blocks).toHaveLength(testCase.expected.block_count); + expect(diagnostics).toHaveLength(testCase.expected.diagnostic_count); + expect(diagnostics.length === 0).toBe(testCase.expected.valid); + if (blocks.length > 0) expect(blocks).toEqual(testCase.expected.blocks); + if (diagnostics.length > 0) { + expect( + diagnostics.map((diagnostic) => ({ + category: diagnostic.category, + severity: diagnostic.severity, + line: diagnostic.line + })) + ).toEqual(testCase.expected.diagnostics); + } + for (const query of testCase.expected.line_queries) { + const block = freezeDirectiveBlockForLine(blocks, query.line); + expect(block !== undefined).toBe(query.in_freeze); + if (block !== undefined) expect(block.id).toBe(query.block_id); + } + } + + expect(fixture.cases).toHaveLength(fixture.expected.case_count); + expect(validCount).toBe(fixture.expected.valid_case_count); + expect(invalidCount).toBe(fixture.expected.invalid_case_count); + }); + it('conforms to the slice-906 merge engine suite setting fixture', () => { const fixture = readFixture<{ settings: { From 66cd51b55aa378930cc2bf555e5bcbaecc360b83 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 19:07:17 -0600 Subject: [PATCH 114/130] Add git comment delta primitive --- packages/ast-merge-git/src/index.ts | 60 +++++++++++++++++- .../test/fixtures.integration.test.ts | 63 ++++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index d9a68ca..1462109 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -40,6 +40,12 @@ export interface Merge3Response { readonly reparse_after_render: boolean | null; } +export interface CommentDeltaResult { + readonly ok: boolean; + readonly merged_comment: string | null; + readonly conflicts: readonly Merge3Conflict[]; +} + const absent = Symbol('absent'); type MaybeAbsent = unknown | typeof absent; @@ -110,6 +116,42 @@ export function merge3Json(request: Merge3Request): Merge3Response { } } +export function mergeCommentDelta( + baseComment: string | null, + oursComment: string | null, + theirsComment: string | null, + ownerPath: string +): CommentDeltaResult { + const conflicts: Merge3Conflict[] = []; + let mergedComment: string | null = null; + + if (oursComment === theirsComment) { + mergedComment = oursComment; + } else if (baseComment === oursComment) { + mergedComment = theirsComment; + } else if (baseComment === theirsComment) { + mergedComment = oursComment; + } else if (oursComment === null) { + conflicts.push( + commentConflict('delete_edit', ownerPath, 'ours deleted a comment that theirs edited') + ); + } else if (theirsComment === null) { + conflicts.push( + commentConflict('delete_edit', ownerPath, 'theirs deleted a comment that ours edited') + ); + } else { + conflicts.push( + commentConflict('edit_edit', ownerPath, 'comment changed differently in ours and theirs') + ); + } + + return { + ok: conflicts.length === 0, + merged_comment: conflicts.length === 0 ? mergedComment : null, + conflicts + }; +} + function response( request: Merge3Request, fields: Partial & { readonly ok: boolean } @@ -137,7 +179,10 @@ function response( }; } -function renderConflictSource(request: Merge3Request, conflicts: readonly Merge3Conflict[]): string { +function renderConflictSource( + request: Merge3Request, + conflicts: readonly Merge3Conflict[] +): string { const markerSize = Math.max(request.conflict_marker_size ?? 7, 1); return [ `/* smorg structured conflicts: ${conflicts.length} unresolved */`, @@ -186,7 +231,9 @@ function mergeJsonObjects( conflicts: Merge3Conflict[] ): Readonly> { const result: Record = {}; - const keys = [...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)])].sort(); + const keys = [ + ...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]) + ].sort(); for (const key of keys) { const [merged, keep] = mergeJsonEntry( Object.hasOwn(base, key) ? base[key] : absent, @@ -246,6 +293,15 @@ function addConflict( }); } +function commentConflict(category: string, path: string, message: string): Merge3Conflict { + return { + conflict_id: 'comment-conflict-1', + category, + path: path || '/', + message + }; +} + function jsonPointerJoin(parent: string, token: string): string { const escaped = token.replaceAll('~', '~0').replaceAll('/', '~1'); return parent ? `${parent}/${escaped}` : `/${escaped}`; diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index c1e27c6..3204206 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; -import { merge3, type Merge3Request } from '../src/index'; +import { merge3, mergeCommentDelta, type Merge3Request } from '../src/index'; interface Fixture { readonly contract: { @@ -28,6 +28,34 @@ function readFixture(...parts: readonly string[]): Fixture { return JSON.parse(source) as Fixture; } +interface CommentDeltaFixture { + readonly contract: { + readonly package: string; + readonly operation: string; + }; + readonly owner: { + readonly path: string; + }; + readonly cases: readonly { + readonly case_id: string; + readonly base_comment: string | null; + readonly ours_comment: string | null; + readonly theirs_comment: string | null; + readonly expected: { + readonly ok: boolean; + readonly merged_comment?: string | null; + readonly conflict_count: number; + readonly conflict_categories?: readonly string[]; + readonly comment_owner_path?: string; + }; + }[]; +} + +function readCommentDeltaFixture(...parts: readonly string[]): CommentDeltaFixture { + const source = readFileSync(path.join('..', 'fixtures', ...parts), 'utf8'); + return JSON.parse(source) as CommentDeltaFixture; +} + describe('@structuredmerge/ast-merge-git', () => { it('conforms to the git merge3 contract fixture', () => { const fixture = readFixture( @@ -64,4 +92,37 @@ describe('@structuredmerge/ast-merge-git', () => { } } }); + + it('conforms to the git comment delta semantics fixture', () => { + const fixture = readCommentDeltaFixture( + 'diagnostics', + 'slice-953-git-comment-delta-semantics', + 'git-comment-delta-semantics.json' + ); + expect(fixture.contract.package).toBe('ast-merge-git'); + expect(fixture.contract.operation).toBe('comment_delta_semantics'); + + for (const testCase of fixture.cases) { + const result = mergeCommentDelta( + testCase.base_comment, + testCase.ours_comment, + testCase.theirs_comment, + fixture.owner.path + ); + expect(result.ok, testCase.case_id).toBe(testCase.expected.ok); + expect(result.conflicts, testCase.case_id).toHaveLength(testCase.expected.conflict_count); + if ('merged_comment' in testCase.expected) { + expect(result.merged_comment, testCase.case_id).toBe(testCase.expected.merged_comment); + } + if (testCase.expected.conflict_categories !== undefined) { + expect( + result.conflicts.map((conflict) => conflict.category), + testCase.case_id + ).toEqual(testCase.expected.conflict_categories); + } + if (testCase.expected.comment_owner_path !== undefined) { + expect(fixture.owner.path, testCase.case_id).toBe(testCase.expected.comment_owner_path); + } + } + }); }); From b72da66d42bfa29b13169c8718f5f293bea8d8cd Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 19:25:17 -0600 Subject: [PATCH 115/130] Report merge3 render identity --- packages/ast-merge-git/src/index.ts | 16 ++++++++++++++++ .../test/fixtures.integration.test.ts | 8 ++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 1462109..491cd6f 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -32,6 +32,8 @@ export interface Merge3Response { readonly profile: Readonly>; readonly render_report: { readonly strategy: string; + readonly backend_id?: string; + readonly parser_identity?: string; }; readonly formatting_preservation: { readonly line_diff_score: number; @@ -169,6 +171,7 @@ function response( dialect: request.dialect ?? '' }, render_report: { + ...renderIdentity(request), strategy: fields.render_report?.strategy ?? request.render_policy ?? 'canonical' }, formatting_preservation: fields.formatting_preservation ?? { @@ -179,6 +182,19 @@ function response( }; } +function renderIdentity(request: Merge3Request): { + readonly backend_id?: string; + readonly parser_identity?: string; +} { + if (normalizeLanguage(request) === 'json') { + return { + backend_id: 'native-json', + parser_identity: 'standard-json' + }; + } + return {}; +} + function renderConflictSource( request: Merge3Request, conflicts: readonly Merge3Conflict[] diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 3204206..4b39e82 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -18,6 +18,11 @@ interface Fixture { readonly conflict_categories?: readonly string[]; readonly conflict_paths?: readonly string[]; readonly conflicted_source_contains?: readonly string[]; + readonly render_report?: { + readonly strategy: string; + readonly backend_id?: string; + readonly parser_identity?: string; + }; readonly reparse_after_render: boolean | null; }; }[]; @@ -73,6 +78,9 @@ describe('@structuredmerge/ast-merge-git', () => { expect(result.reparse_after_render, testCase.case_id).toBe( testCase.expected.reparse_after_render ); + if (testCase.expected.render_report !== undefined) { + expect(result.render_report, testCase.case_id).toEqual(testCase.expected.render_report); + } if (result.ok) { expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( testCase.expected.merged_json From 80475c5bea266b1bfb408d55d651618a624b9697 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 19:41:53 -0600 Subject: [PATCH 116/130] Assert merge3 formatting preservation reports --- packages/ast-merge-git/test/fixtures.integration.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 4b39e82..303af90 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -23,6 +23,10 @@ interface Fixture { readonly backend_id?: string; readonly parser_identity?: string; }; + readonly formatting_preservation?: { + readonly line_diff_score: number; + readonly character_diff_score: number; + }; readonly reparse_after_render: boolean | null; }; }[]; @@ -81,6 +85,11 @@ describe('@structuredmerge/ast-merge-git', () => { if (testCase.expected.render_report !== undefined) { expect(result.render_report, testCase.case_id).toEqual(testCase.expected.render_report); } + if (testCase.expected.formatting_preservation !== undefined) { + expect(result.formatting_preservation, testCase.case_id).toEqual( + testCase.expected.formatting_preservation + ); + } if (result.ok) { expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( testCase.expected.merged_json From 55bf6d57ca7c00d44b7b4fa161621b70d6bbfa74 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 20:18:09 -0600 Subject: [PATCH 117/130] Report merge3 secondary formatting metrics --- packages/ast-merge-git/src/index.ts | 33 +++++++++++++++++++ .../test/fixtures.integration.test.ts | 12 +++++++ 2 files changed, 45 insertions(+) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 491cd6f..953655f 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -39,6 +39,13 @@ export interface Merge3Response { readonly line_diff_score: number; readonly character_diff_score: number; }; + readonly secondary_formatting_metrics: { + readonly unchanged_line_churn: number; + readonly output_diff_size: number; + readonly source_fragment_retention: number; + readonly weighted: boolean; + readonly diagnostics: readonly string[]; + }; readonly reparse_after_render: boolean | null; } @@ -178,10 +185,36 @@ function response( line_diff_score: 0, character_diff_score: 0 }, + secondary_formatting_metrics: + fields.secondary_formatting_metrics ?? + secondaryFormattingMetrics(fields.merged_source !== undefined), reparse_after_render: fields.reparse_after_render ?? null }; } +function secondaryFormattingMetrics( + merged: boolean +): Merge3Response['secondary_formatting_metrics'] { + if (merged) { + return { + unchanged_line_churn: 0, + output_diff_size: 0, + source_fragment_retention: 1, + weighted: false, + diagnostics: ['canonical JSON has no trivia-preserving source fragments yet'] + }; + } + return { + unchanged_line_churn: 0, + output_diff_size: 0, + source_fragment_retention: 0, + weighted: false, + diagnostics: [ + 'unresolved conflict did not produce a merged source-fragment retention measurement' + ] + }; +} + function renderIdentity(request: Merge3Request): { readonly backend_id?: string; readonly parser_identity?: string; diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 303af90..cdb16f4 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -27,6 +27,13 @@ interface Fixture { readonly line_diff_score: number; readonly character_diff_score: number; }; + readonly secondary_formatting_metrics?: { + readonly unchanged_line_churn: number; + readonly output_diff_size: number; + readonly source_fragment_retention: number; + readonly weighted: boolean; + readonly diagnostics: readonly string[]; + }; readonly reparse_after_render: boolean | null; }; }[]; @@ -90,6 +97,11 @@ describe('@structuredmerge/ast-merge-git', () => { testCase.expected.formatting_preservation ); } + if (testCase.expected.secondary_formatting_metrics !== undefined) { + expect(result.secondary_formatting_metrics, testCase.case_id).toEqual( + testCase.expected.secondary_formatting_metrics + ); + } if (result.ok) { expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( testCase.expected.merged_json From c0a504065ba9217b2655ce99e4d2217410cf6c51 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 20:31:37 -0600 Subject: [PATCH 118/130] Report merge3 default driver evaluation --- packages/ast-merge-git/src/index.ts | 72 ++++++++++++++++--- .../test/fixtures.integration.test.ts | 17 +++++ 2 files changed, 80 insertions(+), 9 deletions(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 953655f..5b1dfe1 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -46,6 +46,18 @@ export interface Merge3Response { readonly weighted: boolean; readonly diagnostics: readonly string[]; }; + readonly default_driver_evaluation: { + readonly status: string; + readonly formatting_threshold: number; + readonly formatting_score: number; + readonly hard_gates: readonly { + readonly name: string; + readonly passed: boolean; + readonly weighted: boolean; + }[]; + readonly blocking_reasons: readonly string[]; + readonly diagnostics: readonly string[]; + }; readonly reparse_after_render: boolean | null; } @@ -165,6 +177,15 @@ function response( request: Merge3Request, fields: Partial & { readonly ok: boolean } ): Merge3Response { + const renderReport = { + ...renderIdentity(request), + strategy: fields.render_report?.strategy ?? request.render_policy ?? 'canonical' + }; + const formattingPreservation = fields.formatting_preservation ?? { + line_diff_score: 0, + character_diff_score: 0 + }; + const reparseAfterRender = fields.reparse_after_render ?? null; return { ok: fields.ok, merged_source: fields.merged_source, @@ -177,18 +198,51 @@ function response( language: normalizeLanguage(request), dialect: request.dialect ?? '' }, - render_report: { - ...renderIdentity(request), - strategy: fields.render_report?.strategy ?? request.render_policy ?? 'canonical' - }, - formatting_preservation: fields.formatting_preservation ?? { - line_diff_score: 0, - character_diff_score: 0 - }, + render_report: renderReport, + formatting_preservation: formattingPreservation, secondary_formatting_metrics: fields.secondary_formatting_metrics ?? secondaryFormattingMetrics(fields.merged_source !== undefined), - reparse_after_render: fields.reparse_after_render ?? null + default_driver_evaluation: + fields.default_driver_evaluation ?? + defaultDriverEvaluation(formattingPreservation, reparseAfterRender, renderReport), + reparse_after_render: reparseAfterRender + }; +} + +function defaultDriverEvaluation( + formattingPreservation: Merge3Response['formatting_preservation'], + reparseAfterRender: boolean | null, + renderReport: Merge3Response['render_report'] +): Merge3Response['default_driver_evaluation'] { + const threshold = 0.95; + const score = + (formattingPreservation.line_diff_score + formattingPreservation.character_diff_score) / 2; + const reparsePassed = reparseAfterRender === true; + const noFullFileRewrite = renderReport.strategy !== 'full_file_conflict_markers'; + const coherentConflictMarkers = renderReport.strategy !== 'full_file_conflict_markers'; + const blockingReasons: string[] = []; + if (!reparsePassed) blockingReasons.push('rendered output did not reparse'); + if (score < threshold) blockingReasons.push('formatting score is below threshold'); + if (!noFullFileRewrite) blockingReasons.push('full-file rewrite or conflict markers were used'); + if (!coherentConflictMarkers) { + blockingReasons.push('conflict marker placement is not syntactically coherent'); + } + return { + status: blockingReasons.length === 0 ? 'recommended' : 'not_recommended', + formatting_threshold: threshold, + formatting_score: score, + hard_gates: [ + { name: 'reparse_after_render', passed: reparsePassed, weighted: false }, + { name: 'no_full_file_rewrite', passed: noFullFileRewrite, weighted: false }, + { + name: 'coherent_conflict_marker_placement', + passed: coherentConflictMarkers, + weighted: false + } + ], + blocking_reasons: blockingReasons, + diagnostics: ['default-driver evaluation is advisory unless explicitly required'] }; } diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index cdb16f4..a9167a0 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -34,6 +34,18 @@ interface Fixture { readonly weighted: boolean; readonly diagnostics: readonly string[]; }; + readonly default_driver_evaluation?: { + readonly status: string; + readonly formatting_threshold: number; + readonly formatting_score: number; + readonly hard_gates: readonly { + readonly name: string; + readonly passed: boolean; + readonly weighted: boolean; + }[]; + readonly blocking_reasons: readonly string[]; + readonly diagnostics: readonly string[]; + }; readonly reparse_after_render: boolean | null; }; }[]; @@ -102,6 +114,11 @@ describe('@structuredmerge/ast-merge-git', () => { testCase.expected.secondary_formatting_metrics ); } + if (testCase.expected.default_driver_evaluation !== undefined) { + expect(result.default_driver_evaluation, testCase.case_id).toEqual( + testCase.expected.default_driver_evaluation + ); + } if (result.ok) { expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( testCase.expected.merged_json From e2e547ed49ead4a3ae5630b83cf9b2457eaca313 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 20:40:42 -0600 Subject: [PATCH 119/130] Report smorg-ts fallback use --- packages/smorg-ts/src/cli.ts | 87 ++++++++++++++++++++++++++++-- packages/smorg-ts/test/cli.test.ts | 22 ++++++++ 2 files changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index f76dcce..414f20a 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -7,7 +7,7 @@ import { initialProfilePromotionPolicy, promotionProfileJsonKeyedObject } from '@structuredmerge/ast-merge'; -import type { MergeResult, ProfilePromotionStatus } from '@structuredmerge/ast-merge'; +import type { Diagnostic, MergeResult, ProfilePromotionStatus } from '@structuredmerge/ast-merge'; import { merge3 } from '@structuredmerge/ast-merge-git'; import { mergeGo } from '@structuredmerge/go-merge'; import { mergeJson } from '@structuredmerge/json-merge'; @@ -28,6 +28,7 @@ interface MergeDriverOptions { readonly fallback: string; readonly checkOnly: boolean; readonly exitCode: boolean; + readonly report?: string; readonly profileId?: string; readonly profileReport: boolean; readonly requireProfileStatus?: string; @@ -90,7 +91,7 @@ export function run( function printUsage(out: Pick): void { out.write( [ - 'usage: smorg-ts merge-driver [--path-name PATH] [--output PATH] [--strict] [--fallback=none|line|local|full-file] %O %A %B [%P]', + 'usage: smorg-ts merge-driver [--path-name PATH] [--output PATH] [--report PATH] [--strict] [--fallback=none|line|local|full-file] %O %A %B [%P]', ' smorg-ts merge-driver --ancestor %O --current %A --other %B --path-name %P', ' smorg-ts diff-driver [--path-name PATH] OLD NEW', ' smorg-ts diff-driver PATH OLD-FILE OLD-HEX OLD-MODE NEW-FILE NEW-HEX NEW-MODE [OLD-PREFIX NEW-PREFIX]', @@ -140,7 +141,14 @@ function runMergeDriver( let output = result.output; if (!result.ok) { printDiagnostics(stderr, result); + const fallbacks: Array> = []; if (output === undefined && !options.strict && options.fallback !== 'none') { + fallbacks.push({ + mode: 'full_file', + requested_mode: options.fallback, + reason: fallbackReason(result.diagnostics), + applied: true + }); output = fullFileConflictOutput( settings.conflictMarkerSize, ancestorSource, @@ -148,6 +156,16 @@ function runMergeDriver( otherSource ); } + const reportExit = writeMergeDriverMachineReport( + options.report, + effectivePath, + false, + exitUnresolvedConflict, + fallbacks, + result.diagnostics, + stderr + ); + if (reportExit !== exitSuccess) return reportExit; if (options.checkOnly) return exitUnresolvedConflict; if (output !== undefined) { try { @@ -165,7 +183,18 @@ function runMergeDriver( } if (options.checkOnly) { - return options.exitCode && output !== currentSource ? exitUnresolvedConflict : exitSuccess; + const exit = options.exitCode && output !== currentSource ? exitUnresolvedConflict : exitSuccess; + const reportExit = writeMergeDriverMachineReport( + options.report, + effectivePath, + true, + exit, + [], + result.diagnostics, + stderr + ); + if (reportExit !== exitSuccess) return reportExit; + return exit; } try { @@ -174,9 +203,56 @@ function runMergeDriver( stderr.write(`write output: ${String(error)}\n`); return exitInternalError; } + const reportExit = writeMergeDriverMachineReport( + options.report, + effectivePath, + true, + exitSuccess, + [], + result.diagnostics, + stderr + ); + if (reportExit !== exitSuccess) return reportExit; return exitSuccess; } +function writeMergeDriverMachineReport( + reportPath: string | undefined, + pathName: string, + ok: boolean, + exitCode: number, + fallbacks: Array>, + diagnostics: readonly Diagnostic[], + stderr: Pick +): number { + if (!reportPath) return exitSuccess; + try { + writeFileSync( + reportPath, + `${JSON.stringify( + { + command: 'merge-driver', + path_name: pathName, + ok, + exit_code: exitCode, + fallbacks, + diagnostics + }, + undefined, + 2 + )}\n` + ); + return exitSuccess; + } catch (error) { + stderr.write(`write report: ${String(error)}\n`); + return exitInternalError; + } +} + +function fallbackReason(diagnostics: readonly Diagnostic[]): string { + return diagnostics[0]?.category ?? 'structured_merge_failed'; +} + function fullFileConflictOutput( markerSize: number, ancestorSource: string, @@ -209,6 +285,7 @@ function parseMergeDriverOptions( let fallback = 'full-file'; let checkOnly = false; let exitCode = false; + let report: string | undefined; let profileId: string | undefined; let profileReport = false; let requireProfileStatus: string | undefined; @@ -232,6 +309,9 @@ function parseMergeDriverOptions( case '--output': output = args[++index]; break; + case '--report': + report = args[++index]; + break; case '--strict': strict = true; break; @@ -288,6 +368,7 @@ function parseMergeDriverOptions( fallback, checkOnly, exitCode, + report, profileId, profileReport, requireProfileStatus diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 2ebf38d..e4a8143 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -60,6 +60,12 @@ interface GitDriverFallbackCase { readonly merged_source?: string; readonly source_contains?: readonly string[]; readonly stderr_contains: readonly string[]; + readonly machine_report: { + readonly ok: boolean; + readonly exit_code: number; + readonly fallbacks: readonly unknown[]; + readonly diagnostics_contain: readonly string[]; + }; }; } @@ -207,11 +213,13 @@ describe('smorg-ts cli', () => { const ancestor = write('ancestor.json', testCase.base_source); const current = write('current.json', testCase.ours_source); const other = write('other.json', testCase.theirs_source); + const reportPath = path.join(dir, 'merge-report.json'); const args = ['merge-driver']; if (testCase.options.strict) args.push('--strict'); if (testCase.options.fallback && testCase.options.fallback !== 'full-file') { args.push('--fallback', testCase.options.fallback); } + args.push('--report', reportPath); args.push(ancestor, current, other, testCase.path_name); const stdout = writer(); const stderr = writer(); @@ -230,6 +238,20 @@ describe('smorg-ts cli', () => { for (const needle of testCase.expected.stderr_contains) { expect(stderr.output(), testCase.case_id).toContain(needle); } + const report = JSON.parse(readFileSync(reportPath, 'utf8')) as { + ok: boolean; + exit_code: number; + fallbacks: unknown[]; + diagnostics: unknown[]; + }; + const expectedReport = testCase.expected.machine_report; + expect(report.ok, testCase.case_id).toBe(expectedReport.ok); + expect(report.exit_code, testCase.case_id).toBe(expectedReport.exit_code); + expect(report.fallbacks, testCase.case_id).toEqual(expectedReport.fallbacks); + const diagnostics = JSON.stringify(report.diagnostics); + for (const needle of expectedReport.diagnostics_contain) { + expect(diagnostics, testCase.case_id).toContain(needle); + } } }); From 2d9b138a2e2492e97134eb8f75bdeb3ac1aa9ffe Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 20:42:00 -0600 Subject: [PATCH 120/130] Assert smorg-ts fallback stderr stays concise --- packages/smorg-ts/test/cli.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index e4a8143..f7c6b48 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -60,6 +60,7 @@ interface GitDriverFallbackCase { readonly merged_source?: string; readonly source_contains?: readonly string[]; readonly stderr_contains: readonly string[]; + readonly stderr_not_contains?: readonly string[]; readonly machine_report: { readonly ok: boolean; readonly exit_code: number; @@ -238,6 +239,9 @@ describe('smorg-ts cli', () => { for (const needle of testCase.expected.stderr_contains) { expect(stderr.output(), testCase.case_id).toContain(needle); } + for (const needle of testCase.expected.stderr_not_contains ?? []) { + expect(stderr.output(), testCase.case_id).not.toContain(needle); + } const report = JSON.parse(readFileSync(reportPath, 'utf8')) as { ok: boolean; exit_code: number; From 0e7de2820a1b450df7e0467afa57ebba90e2c895 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 21:44:49 -0600 Subject: [PATCH 121/130] Report JSON owned regions in TypeScript --- packages/ast-merge-git/src/index.ts | 68 ++++++++++++++++++- .../test/fixtures.integration.test.ts | 20 ++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 5b1dfe1..076c3be 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -35,6 +35,7 @@ export interface Merge3Response { readonly backend_id?: string; readonly parser_identity?: string; }; + readonly owned_regions: readonly OwnedRegionReport[]; readonly formatting_preservation: { readonly line_diff_score: number; readonly character_diff_score: number; @@ -61,6 +62,31 @@ export interface Merge3Response { readonly reparse_after_render: boolean | null; } +export interface SourceRange { + readonly start: number; + readonly end: number; +} + +export interface AttachedSpan { + readonly kind: string; + readonly line_range: SourceRange; + readonly byte_range?: SourceRange; +} + +export interface OwnedRegionReport { + readonly owner_path: string; + readonly node_id: string; + readonly region_kind: string; + readonly byte_range: SourceRange; + readonly line_range: SourceRange; + readonly attached_spans: readonly AttachedSpan[]; + readonly backend_id: string; + readonly parser_identity: string; + readonly can_replace: boolean; + readonly can_line_merge: boolean; + readonly requires_reparse: boolean; +} + export interface CommentDeltaResult { readonly ok: boolean; readonly merged_comment: string | null; @@ -96,12 +122,17 @@ export function merge3Json(request: Merge3Request): Merge3Response { const conflicts: Merge3Conflict[] = []; const merged = mergeJsonValue(base, ours, theirs, '', conflicts); if (conflicts.length > 0) { + const ownedRegions = jsonOwnedRegionsForConflicts(request, conflicts); return response(request, { ok: false, conflicts, conflicted_source: renderConflictSource(request, conflicts), + owned_regions: ownedRegions, render_report: { - strategy: 'full_file_conflict_markers' + strategy: + ownedRegions.length === 0 + ? 'full_file_conflict_markers' + : 'owned_region_conflict_markers' }, diagnostics: [ { @@ -193,6 +224,7 @@ function response( conflicts: fields.conflicts ?? [], diagnostics: fields.diagnostics ?? [], fallbacks: fields.fallbacks ?? [], + owned_regions: fields.owned_regions ?? [], profile: { profile_id: request.profile_id ?? '', language: normalizeLanguage(request), @@ -300,6 +332,40 @@ function renderConflictSource( ].join('\n'); } +function jsonOwnedRegionsForConflicts( + request: Merge3Request, + conflicts: readonly Merge3Conflict[] +): OwnedRegionReport[] { + return conflicts.flatMap((conflict) => { + if (!conflict.path.startsWith('/') || conflict.path.split('/').length !== 2) return []; + const key = conflict.path.slice(1); + return [ + { + owner_path: conflict.path, + node_id: `json:key:${key}`, + region_kind: 'node', + byte_range: jsonKeyByteRange(request.base_source, key), + line_range: { start: 1, end: 1 }, + attached_spans: [], + backend_id: 'native-json', + parser_identity: 'standard-json', + can_replace: true, + can_line_merge: false, + requires_reparse: true + } + ]; + }); +} + +function jsonKeyByteRange(source: string, key: string): SourceRange { + const needle = `"${key}"`; + const start = source.indexOf(needle); + if (start < 0) return { start: 0, end: source.length }; + let end = start + needle.length; + while (end < source.length && source[end] !== ',' && source[end] !== '}') end += 1; + return { start, end }; +} + function parseJsonRole(role: string, source: string): unknown { try { return JSON.parse(source); diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index a9167a0..095cec0 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -46,6 +46,18 @@ interface Fixture { readonly blocking_reasons: readonly string[]; readonly diagnostics: readonly string[]; }; + readonly owned_regions?: readonly { + readonly owner_path: string; + readonly node_id: string; + readonly region_kind: string; + readonly line_range: { readonly start: number; readonly end: number }; + readonly attached_spans: readonly unknown[]; + readonly backend_id: string; + readonly parser_identity: string; + readonly can_replace: boolean; + readonly can_line_merge: boolean; + readonly requires_reparse: boolean; + }[]; readonly reparse_after_render: boolean | null; }; }[]; @@ -119,6 +131,14 @@ describe('@structuredmerge/ast-merge-git', () => { testCase.expected.default_driver_evaluation ); } + if (testCase.expected.owned_regions !== undefined) { + expect(result.owned_regions, testCase.case_id).toHaveLength( + testCase.expected.owned_regions.length + ); + for (const [index, expectedRegion] of testCase.expected.owned_regions.entries()) { + expect(result.owned_regions[index], testCase.case_id).toMatchObject(expectedRegion); + } + } if (result.ok) { expect(JSON.parse(result.merged_source ?? ''), testCase.case_id).toEqual( testCase.expected.merged_json From 546d48e255767a0e302291a1779bafa770b12622 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 21:53:34 -0600 Subject: [PATCH 122/130] Report smorg-ts owned regions --- packages/smorg-ts/src/cli.ts | 26 ++++++++++++++++++++++++-- packages/smorg-ts/test/cli.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 414f20a..7efb02c 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -12,6 +12,7 @@ import { merge3 } from '@structuredmerge/ast-merge-git'; import { mergeGo } from '@structuredmerge/go-merge'; import { mergeJson } from '@structuredmerge/json-merge'; import { mergeText } from '@structuredmerge/plain-merge'; +import type { Merge3Response } from '@structuredmerge/ast-merge-git'; export const exitSuccess = 0; export const exitUnresolvedConflict = 1; @@ -59,6 +60,11 @@ interface ConflictRegion { readonly endLine: number; } +type MergeDriverResult = MergeResult & { + readonly owned_regions?: Merge3Response['owned_regions']; + readonly render_report?: Merge3Response['render_report']; +}; + export function run( args: readonly string[], stdout: Pick, @@ -162,6 +168,8 @@ function runMergeDriver( false, exitUnresolvedConflict, fallbacks, + result.owned_regions ?? [], + result.render_report, result.diagnostics, stderr ); @@ -190,6 +198,8 @@ function runMergeDriver( true, exit, [], + result.owned_regions ?? [], + result.render_report, result.diagnostics, stderr ); @@ -209,6 +219,8 @@ function runMergeDriver( true, exitSuccess, [], + result.owned_regions ?? [], + result.render_report, result.diagnostics, stderr ); @@ -222,6 +234,8 @@ function writeMergeDriverMachineReport( ok: boolean, exitCode: number, fallbacks: Array>, + ownedRegions: Merge3Response['owned_regions'], + renderReport: Merge3Response['render_report'] | undefined, diagnostics: readonly Diagnostic[], stderr: Pick ): number { @@ -236,6 +250,8 @@ function writeMergeDriverMachineReport( ok, exit_code: exitCode, fallbacks, + owned_regions: ownedRegions, + render_report: renderReport, diagnostics }, undefined, @@ -565,7 +581,7 @@ function mergeByPath( ancestorSource: string, currentSource: string, otherSource: string -): MergeResult { +): MergeDriverResult { switch (normalizeLanguage(language, pathName)) { case 'go': return mergeGo(otherSource, currentSource, 'go'); @@ -591,12 +607,14 @@ function mergeByPath( } } -function merge3Result(result: ReturnType): MergeResult { +function merge3Result(result: ReturnType): MergeDriverResult { if (result.ok && result.merged_source !== undefined) { return { ok: true, diagnostics: result.diagnostics, output: result.merged_source, + owned_regions: result.owned_regions, + render_report: result.render_report, policies: [] }; } @@ -605,12 +623,16 @@ function merge3Result(result: ReturnType): MergeResult { ok: false, diagnostics: result.diagnostics, output: result.conflicted_source, + owned_regions: result.owned_regions, + render_report: result.render_report, policies: [] }; } return { ok: false, diagnostics: result.diagnostics, + owned_regions: result.owned_regions, + render_report: result.render_report, policies: [] }; } diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index f7c6b48..fbf7c0a 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -280,6 +280,30 @@ describe('smorg-ts cli', () => { } }); + it('includes owned-region placement in merge-driver reports', () => { + const ancestor = write('ancestor.json', '{"name":"demo","enabled":true}'); + const current = write('current.json', '{"name":"demo","enabled":false}'); + const other = write('other.json', '{"name":"demo","enabled":"yes"}'); + const reportPath = path.join(dir, 'merge-report.json'); + const stdout = writer(); + const stderr = writer(); + + const exit = run( + ['merge-driver', '--report', reportPath, ancestor, current, other, 'package.json'], + stdout.stream, + stderr.stream + ); + + expect(exit).toBe(exitUnresolvedConflict); + const report = JSON.parse(readFileSync(reportPath, 'utf8')) as { + render_report: { strategy: string }; + owned_regions: Array<{ owner_path: string; region_kind: string }>; + }; + expect(report.render_report.strategy).toBe('owned_region_conflict_markers'); + expect(report.owned_regions[0]?.owner_path).toBe('/enabled'); + expect(report.owned_regions[0]?.region_kind).toBe('node'); + }); + it('conforms to the git-driver JSON integration fixture in a repository', () => { try { execFileSync('git', ['--version'], { stdio: 'pipe' }); From 3076ef61fddfe89484bdec3ba46512f7f11a4de0 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 22:04:54 -0600 Subject: [PATCH 123/130] Render TypeScript JSON owned-region conflicts --- packages/ast-merge-git/src/index.ts | 43 ++++++++++++++++++- .../test/fixtures.integration.test.ts | 4 ++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 076c3be..12a3dbc 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -123,10 +123,13 @@ export function merge3Json(request: Merge3Request): Merge3Response { const merged = mergeJsonValue(base, ours, theirs, '', conflicts); if (conflicts.length > 0) { const ownedRegions = jsonOwnedRegionsForConflicts(request, conflicts); + const conflictedSource = + renderJsonOwnedRegionConflictSource(request, ownedRegions[0]) ?? + renderConflictSource(request, conflicts); return response(request, { ok: false, conflicts, - conflicted_source: renderConflictSource(request, conflicts), + conflicted_source: conflictedSource, owned_regions: ownedRegions, render_report: { strategy: @@ -332,6 +335,44 @@ function renderConflictSource( ].join('\n'); } +function renderJsonOwnedRegionConflictSource( + request: Merge3Request, + region: OwnedRegionReport | undefined +): string | undefined { + if (region === undefined || region.region_kind !== 'node') return undefined; + const key = region.owner_path.slice(1); + const oursRegion = jsonMemberSource(request.ours_source, key); + const baseRegion = jsonMemberSource(request.base_source, key); + const theirsRegion = jsonMemberSource(request.theirs_source, key); + if (oursRegion === undefined || baseRegion === undefined || theirsRegion === undefined) { + return undefined; + } + const markerSize = Math.max(request.conflict_marker_size ?? 7, 1); + const replacement = [ + `${'<'.repeat(markerSize)} ours`, + oursRegion.text, + `${'|'.repeat(markerSize)} base`, + baseRegion.text, + '='.repeat(markerSize), + theirsRegion.text, + `${'>'.repeat(markerSize)} theirs` + ].join('\n'); + return ( + request.ours_source.slice(0, oursRegion.byteRange.start) + + replacement + + request.ours_source.slice(oursRegion.byteRange.end) + ); +} + +function jsonMemberSource( + source: string, + key: string +): { readonly byteRange: SourceRange; readonly text: string } | undefined { + const byteRange = jsonKeyByteRange(source, key); + if (byteRange.end <= byteRange.start || byteRange.end > source.length) return undefined; + return { byteRange, text: source.slice(byteRange.start, byteRange.end) }; +} + function jsonOwnedRegionsForConflicts( request: Merge3Request, conflicts: readonly Merge3Conflict[] diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 095cec0..2eae5b2 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -18,6 +18,7 @@ interface Fixture { readonly conflict_categories?: readonly string[]; readonly conflict_paths?: readonly string[]; readonly conflicted_source_contains?: readonly string[]; + readonly conflicted_source_not_contains?: readonly string[]; readonly render_report?: { readonly strategy: string; readonly backend_id?: string; @@ -155,6 +156,9 @@ describe('@structuredmerge/ast-merge-git', () => { for (const needle of testCase.expected.conflicted_source_contains ?? []) { expect(result.conflicted_source, testCase.case_id).toContain(needle); } + for (const needle of testCase.expected.conflicted_source_not_contains ?? []) { + expect(result.conflicted_source, testCase.case_id).not.toContain(needle); + } } } }); From d1c4f6f168e8e043bb24c3a66dba8e2aa602ad1f Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 22:08:09 -0600 Subject: [PATCH 124/130] Fail closed for unsafe TypeScript JSON regions --- packages/ast-merge-git/src/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 12a3dbc..5a5af75 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -368,6 +368,7 @@ function jsonMemberSource( source: string, key: string ): { readonly byteRange: SourceRange; readonly text: string } | undefined { + if (!source.includes(`"${key}"`)) return undefined; const byteRange = jsonKeyByteRange(source, key); if (byteRange.end <= byteRange.start || byteRange.end > source.length) return undefined; return { byteRange, text: source.slice(byteRange.start, byteRange.end) }; @@ -380,12 +381,20 @@ function jsonOwnedRegionsForConflicts( return conflicts.flatMap((conflict) => { if (!conflict.path.startsWith('/') || conflict.path.split('/').length !== 2) return []; const key = conflict.path.slice(1); + const baseRegion = jsonMemberSource(request.base_source, key); + if ( + baseRegion === undefined || + jsonMemberSource(request.ours_source, key) === undefined || + jsonMemberSource(request.theirs_source, key) === undefined + ) { + return []; + } return [ { owner_path: conflict.path, node_id: `json:key:${key}`, region_kind: 'node', - byte_range: jsonKeyByteRange(request.base_source, key), + byte_range: baseRegion.byteRange, line_range: { start: 1, end: 1 }, attached_spans: [], backend_id: 'native-json', From 6420a3ceaaf0597fda985d43a914d61c71a783c3 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 22:32:41 -0600 Subject: [PATCH 125/130] Report smorg-ts active profile --- packages/smorg-ts/src/cli.ts | 9 +++++++++ packages/smorg-ts/test/cli.test.ts | 3 +++ 2 files changed, 12 insertions(+) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 7efb02c..4861da8 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -63,6 +63,7 @@ interface ConflictRegion { type MergeDriverResult = MergeResult & { readonly owned_regions?: Merge3Response['owned_regions']; readonly render_report?: Merge3Response['render_report']; + readonly profile?: Merge3Response['profile']; }; export function run( @@ -170,6 +171,7 @@ function runMergeDriver( fallbacks, result.owned_regions ?? [], result.render_report, + result.profile, result.diagnostics, stderr ); @@ -200,6 +202,7 @@ function runMergeDriver( [], result.owned_regions ?? [], result.render_report, + result.profile, result.diagnostics, stderr ); @@ -221,6 +224,7 @@ function runMergeDriver( [], result.owned_regions ?? [], result.render_report, + result.profile, result.diagnostics, stderr ); @@ -236,6 +240,7 @@ function writeMergeDriverMachineReport( fallbacks: Array>, ownedRegions: Merge3Response['owned_regions'], renderReport: Merge3Response['render_report'] | undefined, + profile: Merge3Response['profile'] | undefined, diagnostics: readonly Diagnostic[], stderr: Pick ): number { @@ -252,6 +257,7 @@ function writeMergeDriverMachineReport( fallbacks, owned_regions: ownedRegions, render_report: renderReport, + profile, diagnostics }, undefined, @@ -615,6 +621,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { output: result.merged_source, owned_regions: result.owned_regions, render_report: result.render_report, + profile: result.profile, policies: [] }; } @@ -625,6 +632,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { output: result.conflicted_source, owned_regions: result.owned_regions, render_report: result.render_report, + profile: result.profile, policies: [] }; } @@ -633,6 +641,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { diagnostics: result.diagnostics, owned_regions: result.owned_regions, render_report: result.render_report, + profile: result.profile, policies: [] }; } diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index fbf7c0a..aa9b522 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -298,10 +298,13 @@ describe('smorg-ts cli', () => { const report = JSON.parse(readFileSync(reportPath, 'utf8')) as { render_report: { strategy: string }; owned_regions: Array<{ owner_path: string; region_kind: string }>; + profile: { profile_id: string; language: string }; }; expect(report.render_report.strategy).toBe('owned_region_conflict_markers'); expect(report.owned_regions[0]?.owner_path).toBe('/enabled'); expect(report.owned_regions[0]?.region_kind).toBe('node'); + expect(report.profile.profile_id).toBe('json.keyed-object'); + expect(report.profile.language).toBe('json'); }); it('conforms to the git-driver JSON integration fixture in a repository', () => { From 91a47a25ab79657b7f6d0d4f6527f4f3b029bfc9 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 22:46:31 -0600 Subject: [PATCH 126/130] Report smorg-ts merge3 gate metrics --- packages/smorg-ts/src/cli.ts | 36 ++++++++++++++++++++++++++++++ packages/smorg-ts/test/cli.test.ts | 4 ++++ 2 files changed, 40 insertions(+) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 4861da8..47f8505 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -64,6 +64,10 @@ type MergeDriverResult = MergeResult & { readonly owned_regions?: Merge3Response['owned_regions']; readonly render_report?: Merge3Response['render_report']; readonly profile?: Merge3Response['profile']; + readonly reparse_after_render?: Merge3Response['reparse_after_render']; + readonly formatting_preservation?: Merge3Response['formatting_preservation']; + readonly secondary_formatting_metrics?: Merge3Response['secondary_formatting_metrics']; + readonly default_driver_evaluation?: Merge3Response['default_driver_evaluation']; }; export function run( @@ -172,6 +176,10 @@ function runMergeDriver( result.owned_regions ?? [], result.render_report, result.profile, + result.reparse_after_render, + result.formatting_preservation, + result.secondary_formatting_metrics, + result.default_driver_evaluation, result.diagnostics, stderr ); @@ -203,6 +211,10 @@ function runMergeDriver( result.owned_regions ?? [], result.render_report, result.profile, + result.reparse_after_render, + result.formatting_preservation, + result.secondary_formatting_metrics, + result.default_driver_evaluation, result.diagnostics, stderr ); @@ -225,6 +237,10 @@ function runMergeDriver( result.owned_regions ?? [], result.render_report, result.profile, + result.reparse_after_render, + result.formatting_preservation, + result.secondary_formatting_metrics, + result.default_driver_evaluation, result.diagnostics, stderr ); @@ -241,6 +257,10 @@ function writeMergeDriverMachineReport( ownedRegions: Merge3Response['owned_regions'], renderReport: Merge3Response['render_report'] | undefined, profile: Merge3Response['profile'] | undefined, + reparseAfterRender: Merge3Response['reparse_after_render'] | undefined, + formattingPreservation: Merge3Response['formatting_preservation'] | undefined, + secondaryFormattingMetrics: Merge3Response['secondary_formatting_metrics'] | undefined, + defaultDriverEvaluation: Merge3Response['default_driver_evaluation'] | undefined, diagnostics: readonly Diagnostic[], stderr: Pick ): number { @@ -257,6 +277,10 @@ function writeMergeDriverMachineReport( fallbacks, owned_regions: ownedRegions, render_report: renderReport, + reparse_after_render: reparseAfterRender, + formatting_preservation: formattingPreservation, + secondary_formatting_metrics: secondaryFormattingMetrics, + default_driver_evaluation: defaultDriverEvaluation, profile, diagnostics }, @@ -622,6 +646,10 @@ function merge3Result(result: ReturnType): MergeDriverResult { owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, + reparse_after_render: result.reparse_after_render, + formatting_preservation: result.formatting_preservation, + secondary_formatting_metrics: result.secondary_formatting_metrics, + default_driver_evaluation: result.default_driver_evaluation, policies: [] }; } @@ -633,6 +661,10 @@ function merge3Result(result: ReturnType): MergeDriverResult { owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, + reparse_after_render: result.reparse_after_render, + formatting_preservation: result.formatting_preservation, + secondary_formatting_metrics: result.secondary_formatting_metrics, + default_driver_evaluation: result.default_driver_evaluation, policies: [] }; } @@ -642,6 +674,10 @@ function merge3Result(result: ReturnType): MergeDriverResult { owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, + reparse_after_render: result.reparse_after_render, + formatting_preservation: result.formatting_preservation, + secondary_formatting_metrics: result.secondary_formatting_metrics, + default_driver_evaluation: result.default_driver_evaluation, policies: [] }; } diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index aa9b522..27322ce 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -299,12 +299,16 @@ describe('smorg-ts cli', () => { render_report: { strategy: string }; owned_regions: Array<{ owner_path: string; region_kind: string }>; profile: { profile_id: string; language: string }; + formatting_preservation: { line_diff_score: number }; + default_driver_evaluation: { status: string }; }; expect(report.render_report.strategy).toBe('owned_region_conflict_markers'); expect(report.owned_regions[0]?.owner_path).toBe('/enabled'); expect(report.owned_regions[0]?.region_kind).toBe('node'); expect(report.profile.profile_id).toBe('json.keyed-object'); expect(report.profile.language).toBe('json'); + expect(typeof report.formatting_preservation.line_diff_score).toBe('number'); + expect(typeof report.default_driver_evaluation.status).toBe('string'); }); it('conforms to the git-driver JSON integration fixture in a repository', () => { From 91cbed889c62e865faecafb927d63ef869fbc5b1 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 22:49:35 -0600 Subject: [PATCH 127/130] Assert smorg-ts fallback report fields --- packages/smorg-ts/test/cli.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index 27322ce..ff77408 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -66,6 +66,7 @@ interface GitDriverFallbackCase { readonly exit_code: number; readonly fallbacks: readonly unknown[]; readonly diagnostics_contain: readonly string[]; + readonly required_fields?: readonly string[]; }; }; } @@ -256,6 +257,9 @@ describe('smorg-ts cli', () => { for (const needle of expectedReport.diagnostics_contain) { expect(diagnostics, testCase.case_id).toContain(needle); } + for (const field of expectedReport.required_fields ?? []) { + expect(Object.prototype.hasOwnProperty.call(report, field), testCase.case_id).toBe(true); + } } }); From 3b0aca029eaeaaa254a70a26cd3eeb9200aa1ce1 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 23:03:39 -0600 Subject: [PATCH 128/130] Classify JSON merge3 changes in TypeScript --- packages/ast-merge-git/src/index.ts | 43 +++++++++++++++++++ .../test/fixtures.integration.test.ts | 10 +++++ 2 files changed, 53 insertions(+) diff --git a/packages/ast-merge-git/src/index.ts b/packages/ast-merge-git/src/index.ts index 5a5af75..f84d893 100644 --- a/packages/ast-merge-git/src/index.ts +++ b/packages/ast-merge-git/src/index.ts @@ -22,11 +22,18 @@ export interface Merge3Conflict { readonly message: string; } +export interface ChangeClassification { + readonly path: string; + readonly ours: string; + readonly theirs: string; +} + export interface Merge3Response { readonly ok: boolean; readonly merged_source?: string; readonly conflicted_source?: string; readonly conflicts: readonly Merge3Conflict[]; + readonly change_classifications: readonly ChangeClassification[]; readonly diagnostics: readonly Diagnostic[]; readonly fallbacks: readonly string[]; readonly profile: Readonly>; @@ -120,6 +127,7 @@ export function merge3Json(request: Merge3Request): Merge3Response { const ours = parseJsonRole('ours', request.ours_source); const theirs = parseJsonRole('theirs', request.theirs_source); const conflicts: Merge3Conflict[] = []; + const changeClassifications = classifyJsonChanges(base, ours, theirs); const merged = mergeJsonValue(base, ours, theirs, '', conflicts); if (conflicts.length > 0) { const ownedRegions = jsonOwnedRegionsForConflicts(request, conflicts); @@ -129,6 +137,7 @@ export function merge3Json(request: Merge3Request): Merge3Response { return response(request, { ok: false, conflicts, + change_classifications: changeClassifications, conflicted_source: conflictedSource, owned_regions: ownedRegions, render_report: { @@ -151,6 +160,7 @@ export function merge3Json(request: Merge3Request): Merge3Response { return response(request, { ok: true, merged_source: mergedSource, + change_classifications: changeClassifications, reparse_after_render: JSON.parse(mergedSource) !== undefined, formatting_preservation: { line_diff_score: 1, @@ -225,6 +235,7 @@ function response( merged_source: fields.merged_source, conflicted_source: fields.conflicted_source, conflicts: fields.conflicts ?? [], + change_classifications: fields.change_classifications ?? [], diagnostics: fields.diagnostics ?? [], fallbacks: fields.fallbacks ?? [], owned_regions: fields.owned_regions ?? [], @@ -466,6 +477,38 @@ function mergeJsonObjects( return result; } +function classifyJsonChanges(base: unknown, ours: unknown, theirs: unknown): readonly ChangeClassification[] { + if (isRecord(base) && isRecord(ours) && isRecord(theirs)) { + const keys = [ + ...new Set([...Object.keys(base), ...Object.keys(ours), ...Object.keys(theirs)]) + ].sort(); + return keys.flatMap((key) => { + const oursChange = classifyJsonValueChange( + Object.hasOwn(base, key) ? base[key] : absent, + Object.hasOwn(ours, key) ? ours[key] : absent + ); + const theirsChange = classifyJsonValueChange( + Object.hasOwn(base, key) ? base[key] : absent, + Object.hasOwn(theirs, key) ? theirs[key] : absent + ); + if (oursChange === 'unchanged' && theirsChange === 'unchanged') return []; + return [{ path: jsonPointerJoin('', key), ours: oursChange, theirs: theirsChange }]; + }); + } + const oursChange = classifyJsonValueChange(base, ours); + const theirsChange = classifyJsonValueChange(base, theirs); + if (oursChange === 'unchanged' && theirsChange === 'unchanged') return []; + return [{ path: '/', ours: oursChange, theirs: theirsChange }]; +} + +function classifyJsonValueChange(base: MaybeAbsent, value: MaybeAbsent): string { + if (base === absent && value === absent) return 'unchanged'; + if (base === absent && value !== absent) return 'added'; + if (base !== absent && value === absent) return 'deleted'; + if (jsonEqual(base, value)) return 'unchanged'; + return 'edited'; +} + function mergeJsonEntry( base: MaybeAbsent, ours: MaybeAbsent, diff --git a/packages/ast-merge-git/test/fixtures.integration.test.ts b/packages/ast-merge-git/test/fixtures.integration.test.ts index 2eae5b2..7571594 100644 --- a/packages/ast-merge-git/test/fixtures.integration.test.ts +++ b/packages/ast-merge-git/test/fixtures.integration.test.ts @@ -15,6 +15,11 @@ interface Fixture { readonly ok: boolean; readonly merged_json: unknown | null; readonly conflict_count: number; + readonly change_classifications?: readonly { + readonly path: string; + readonly ours: string; + readonly theirs: string; + }[]; readonly conflict_categories?: readonly string[]; readonly conflict_paths?: readonly string[]; readonly conflicted_source_contains?: readonly string[]; @@ -111,6 +116,11 @@ describe('@structuredmerge/ast-merge-git', () => { const result = merge3(testCase.request); expect(result.ok, testCase.case_id).toBe(testCase.expected.ok); expect(result.conflicts, testCase.case_id).toHaveLength(testCase.expected.conflict_count); + if (testCase.expected.change_classifications !== undefined) { + expect(result.change_classifications, testCase.case_id).toEqual( + testCase.expected.change_classifications + ); + } expect(result.reparse_after_render, testCase.case_id).toBe( testCase.expected.reparse_after_render ); From 3bfe0391cd3b8da4ab7f996756f56e1a279a9b52 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 23:07:50 -0600 Subject: [PATCH 129/130] Report smorg-ts change classifications --- packages/smorg-ts/src/cli.ts | 9 +++++++++ packages/smorg-ts/test/cli.test.ts | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 47f8505..391c1fd 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -61,6 +61,7 @@ interface ConflictRegion { } type MergeDriverResult = MergeResult & { + readonly change_classifications?: Merge3Response['change_classifications']; readonly owned_regions?: Merge3Response['owned_regions']; readonly render_report?: Merge3Response['render_report']; readonly profile?: Merge3Response['profile']; @@ -173,6 +174,7 @@ function runMergeDriver( false, exitUnresolvedConflict, fallbacks, + result.change_classifications ?? [], result.owned_regions ?? [], result.render_report, result.profile, @@ -208,6 +210,7 @@ function runMergeDriver( true, exit, [], + result.change_classifications ?? [], result.owned_regions ?? [], result.render_report, result.profile, @@ -234,6 +237,7 @@ function runMergeDriver( true, exitSuccess, [], + result.change_classifications ?? [], result.owned_regions ?? [], result.render_report, result.profile, @@ -254,6 +258,7 @@ function writeMergeDriverMachineReport( ok: boolean, exitCode: number, fallbacks: Array>, + changeClassifications: Merge3Response['change_classifications'], ownedRegions: Merge3Response['owned_regions'], renderReport: Merge3Response['render_report'] | undefined, profile: Merge3Response['profile'] | undefined, @@ -275,6 +280,7 @@ function writeMergeDriverMachineReport( ok, exit_code: exitCode, fallbacks, + change_classifications: changeClassifications, owned_regions: ownedRegions, render_report: renderReport, reparse_after_render: reparseAfterRender, @@ -643,6 +649,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { ok: true, diagnostics: result.diagnostics, output: result.merged_source, + change_classifications: result.change_classifications, owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, @@ -658,6 +665,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { ok: false, diagnostics: result.diagnostics, output: result.conflicted_source, + change_classifications: result.change_classifications, owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, @@ -671,6 +679,7 @@ function merge3Result(result: ReturnType): MergeDriverResult { return { ok: false, diagnostics: result.diagnostics, + change_classifications: result.change_classifications, owned_regions: result.owned_regions, render_report: result.render_report, profile: result.profile, diff --git a/packages/smorg-ts/test/cli.test.ts b/packages/smorg-ts/test/cli.test.ts index ff77408..1a40014 100644 --- a/packages/smorg-ts/test/cli.test.ts +++ b/packages/smorg-ts/test/cli.test.ts @@ -301,12 +301,16 @@ describe('smorg-ts cli', () => { expect(exit).toBe(exitUnresolvedConflict); const report = JSON.parse(readFileSync(reportPath, 'utf8')) as { render_report: { strategy: string }; + change_classifications: Array<{ path: string; ours: string; theirs: string }>; owned_regions: Array<{ owner_path: string; region_kind: string }>; profile: { profile_id: string; language: string }; formatting_preservation: { line_diff_score: number }; default_driver_evaluation: { status: string }; }; expect(report.render_report.strategy).toBe('owned_region_conflict_markers'); + expect(report.change_classifications).toEqual([ + { path: '/enabled', ours: 'edited', theirs: 'edited' } + ]); expect(report.owned_regions[0]?.owner_path).toBe('/enabled'); expect(report.owned_regions[0]?.region_kind).toBe('node'); expect(report.profile.profile_id).toBe('json.keyed-object'); From 8a53d09ffdc27dc966739125fb0d213e2867428e Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 17 May 2026 23:15:41 -0600 Subject: [PATCH 130/130] Pass smorg-ts fallback policy to merge3 --- packages/smorg-ts/src/cli.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/smorg-ts/src/cli.ts b/packages/smorg-ts/src/cli.ts index 391c1fd..1e1ab1b 100644 --- a/packages/smorg-ts/src/cli.ts +++ b/packages/smorg-ts/src/cli.ts @@ -146,6 +146,7 @@ function runMergeDriver( effectivePath, settings.language, settings.conflictMarkerSize, + options.strict ? 'none' : options.fallback, ancestorSource, currentSource, otherSource @@ -614,6 +615,7 @@ function mergeByPath( pathName: string, language: string | undefined, conflictMarkerSize: number, + fallbackPolicy: string, ancestorSource: string, currentSource: string, otherSource: string @@ -631,7 +633,7 @@ function mergeByPath( language: 'json', dialect: 'json', profile_id: 'json.keyed-object', - fallback_policy: 'none', + fallback_policy: fallbackPolicy, conflict_marker_size: conflictMarkerSize, render_policy: 'canonical' })