From c481e1e570a0c7ca9f5f24f117c47d8ab38f8db9 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Thu, 10 Sep 2026 20:46:50 +0200 Subject: [PATCH] [ticket-105] Integrate the public merge conflict evidence contract --- package.json | 3 +- project/ticket-105/README.md | 31 ++++++++++++ project/ticket-105/intent.json | 44 ++++++++++++++++ scripts/verify-merge-conflict-contract.mjs | 50 +++++++++++++++++++ src/core/types.ts | 31 ++++++++++++ .../fixtures/contracts/merge-conflict-type.ts | 20 ++++++++ 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 project/ticket-105/README.md create mode 100644 project/ticket-105/intent.json create mode 100644 scripts/verify-merge-conflict-contract.mjs create mode 100644 test/fixtures/contracts/merge-conflict-type.ts diff --git a/package.json b/package.json index 9ed728da..552a2fa6 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "verify:schemas": "node scripts/generate-response-schemas.mjs --check", "verify:structured-responses": "node scripts/verify-structured-responses.mjs", "schemas:generate": "npm run build && node scripts/generate-response-schemas.mjs", - "verify": "npm run check && npm run verify:no-llm && npm run verify:modules && npm run verify:env && npm run verify:workflows && npm run verify:generated-analysis && npm run verify:structured-responses && npm run build && npm run verify:schemas && npm test", + "verify": "npm run check && npm run verify:no-llm && npm run verify:modules && npm run verify:env && npm run verify:workflows && npm run verify:generated-analysis && npm run verify:structured-responses && npm run build && npm run verify:schemas && npm run verify:merge-conflict-contract && npm test", + "verify:merge-conflict-contract": "node scripts/verify-merge-conflict-contract.mjs", "start": "node dist/src/cli.js", "mcp": "node dist/src/interfaces/mcp.js", "a2a": "node dist/src/interfaces/a2a.js", diff --git a/project/ticket-105/README.md b/project/ticket-105/README.md new file mode 100644 index 00000000..7d8d8ad2 --- /dev/null +++ b/project/ticket-105/README.md @@ -0,0 +1,31 @@ +# Ticket 105: Integrate the canonical merge conflict evidence contract + +- **ID**: ticket-105 +- **Owner**: unresolved:human +- **Status**: IN_PROGRESS +- **Workflow state**: PUBLICATION +- **Created**: 2026-09-10 + +## Goal and scope + +Integrate the merge-conflict fact data introduced by PR #125 as an explicit +public data contract owned by the integration workstream. Preserve the existing +extractor and t2c.intent/v1 wire format, and add a typed projection plus a CI +contract check against real extraction and JSON round-tripping. The records are +observed marker syntax, never Git-index verification or merge authorization. + +SESSION_EXECUTION_AUTHORIZATION: the user requested continuation of the +publication-protection and data-contract repair, push, independent merge and +tests. Do not rewrite the already merged ticket-104 or claim its failed +governance run passed. Validator profile strengthening is owned separately by +subactor/validator-agent issue #437 and must be deployed before this publication. + +## Acceptance criteria + +- [x] AC-01: The additive public conflict-fact type and positive/negative contract tests bind the existing data without changing extraction behavior. +- [ ] AC-02: Local verification, smoke and Docker smoke pass; strengthened independent publication and fresh main CI pass. + +## Tracking boundary + +This directory contains the minimal reviewed intent. Optional participant prose +and raw command logs are not required delivery output. diff --git a/project/ticket-105/intent.json b/project/ticket-105/intent.json new file mode 100644 index 00000000..f80d854e --- /dev/null +++ b/project/ticket-105/intent.json @@ -0,0 +1,44 @@ +{ + "schema": "new-project.intent/v3", + "ticket": "ticket-105", + "summary": "Integrate the canonical merge conflict evidence contract", + "workstream": "integration", + "classification": { + "kind": "BUG", + "priority": "P1", + "origin": "requested" + }, + "allowedPaths": ["project/ticket-105/**", "src/core/types.ts", "scripts/verify-merge-conflict-contract.mjs", "test/fixtures/contracts/merge-conflict-type.ts", "package.json"], + "forbiddenPaths": ["project/ticket-*/user-*.md"], + "stacks": ["node", "docker"], + "dependsOn": [], + "conflictsWith": [], + "integrationTicket": null, + "delivery": { + "acceptedBaseSha": "42c16669791cfda69ae6548503b5d4b288ac27e3", + "targetBranch": "main", + "outcome": "The integration-owned public conflict-fact contract binds the already emitted data and is verified through the normal application CI and strengthened independent publication.", + "nonGoals": ["No historical ticket rewrites or false success for PR 125.", "No extractor behavior, wire version or Git-index authority changes.", "No weakening or bypass of protected checks."], + "complexity": "M", + "estimatedMinutes": 90, + "budgets": {"maxImplementationFiles": 4, "maxAffectedComponents": 3, "maxPublicInterfaceChanges": 1, "maxRuntimeDependencies": 0}, + "architecture": { + "status": "accepted", + "decision": "Integrate the existing merge_conflict_fact data under the shared contract owner. Publish an additive narrowed IntentRecord type, compile-time negative checks and real-extractor wire invariants in verify. Preserve all PR 125 source and historical receipts.", + "components": [ + {"name": "public-conflict-contract", "paths": ["src/core/types.ts", "test/fixtures/contracts/merge-conflict-type.ts"]}, + {"name": "contract-verification", "paths": ["scripts/verify-merge-conflict-contract.mjs", "package.json"]} + ], + "responsibilityChanges": false, + "interfaceChanges": ["Add the MergeConflictFact public type without changing IntentRecord."], + "dataChanges": ["Integrate the existing merge_conflict_fact projection; preserve its t2c.intent/v1 wire representation and non-authoritative provenance."], + "ui": {"impact": "none", "states": [], "evidence": []}, + "rollback": "Revert only the additive public type and its contract gate through independently reviewed publication; preserve the existing extractor." + }, + "runtimeDependencies": [], + "validation": [ + {"criterion": "AC-01", "commands": ["npm run check", "npm run verify:merge-conflict-contract"], "evidence": "Compile-time invalid-field regressions and real-extractor JSON invariants."}, + {"criterion": "AC-02", "commands": ["make verify", "make smoke", "make docker-smoke"], "evidence": "Exact-head local test receipts, independently strengthened governance checks and observed fresh main CI."} + ] + } +} diff --git a/scripts/verify-merge-conflict-contract.mjs b/scripts/verify-merge-conflict-contract.mjs new file mode 100644 index 00000000..4cdc2baa --- /dev/null +++ b/scripts/verify-merge-conflict-contract.mjs @@ -0,0 +1,50 @@ +#!/usr/bin/env node +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { extractMergeConflicts } from '../dist/src/extractors/merge-conflicts.js'; +import { assertIntentRecord } from '../dist/src/core/schema.js'; + +// Check the actual wire projection, independently of the compile-only public +// type. No LLM, network, repository state or merge operation is involved. +const root = await fs.mkdtemp(path.join(os.tmpdir(), 't2c-conflict-contract-')); +try { + let checked = 0; + for (const style of ['merge', 'diff3']) { + for (const eol of ['\n', '\r\n']) { + const lines = ['<<<<<<< ours', 'x'.repeat(2100), + ...(style === 'diff3' ? ['||||||| base', 'base'] : []), + '=======', 'theirs', '>>>>>>> theirs']; + const body = lines.join(eol); + await fs.writeFile(path.join(root, 'conflict.toml'), body); + const result = await extractMergeConflicts(root, { maxFileBytes: 10_000 }); + assert.equal(result.warnings.length, 0); + assert.equal(result.records.length, 1); + const record = JSON.parse(JSON.stringify(result.records[0])); + assertIntentRecord(record); + assert.equal(record.schemaVersion, 't2c.intent/v1'); + assert.equal(record.statement.kind, 'merge_conflict_fact'); + assert.equal(record.statement.action, 'block'); + assert.equal(record.statement.modality, 'observed'); + assert.equal(record.lifecycle.status, 'blocked'); + assert.equal(record.epistemic.class, 'fact'); + assert.equal(record.source.kind, 'git'); + assert.equal(record.source.path, 'conflict.toml'); + assert.equal(record.source.extractor, 't2c/merge-conflict-markers@1'); + assert.deepEqual(record.source.lines, { start: 1, end: lines.length }); + assert.deepEqual(record.statement.target.paths, ['conflict.toml']); + assert.equal(record.metadata.llmUsed, false); + assert.equal(record.metadata.gitIndexVerified, false); + assert.equal(record.metadata.conflictStyle, style); + assert.equal(record.metadata.markerWidth, 7); + assert.equal(record.source.rawExcerpt, body.slice(0, 2000)); + assert.equal(record.metadata.blockSha256, createHash('sha256').update(body).digest('hex')); + checked++; + } + } + console.log(`merge conflict data contract: PASS (${checked} real wire projections)`); +} finally { + await fs.rm(root, { recursive: true, force: true }); +} diff --git a/src/core/types.ts b/src/core/types.ts index c7701056..bb512c46 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,3 +1,34 @@ +import type { IntentRecord } from './types-foundation.js'; + export * from './types-foundation.js'; export * from './types-code-change.js'; export * from './types-runtime.js'; + +/** + * Additive view of observed conflict-marker syntax in t2c.intent/v1. + * This is neither Git-index verification nor authority to choose a merge side. + * blockSha256 binds the complete block; rawExcerpt may be truncated. + */ +export type MergeConflictFact = IntentRecord & { + statement: IntentRecord['statement'] & { + kind: 'merge_conflict_fact'; + action: 'block'; + modality: 'observed'; + }; + lifecycle: { status: 'blocked' }; + source: IntentRecord['source'] & { + kind: 'git'; + path: string; + lines: { start: number; end: number }; + extractor: 't2c/merge-conflict-markers@1'; + rawExcerpt: string; + }; + epistemic: IntentRecord['epistemic'] & { class: 'fact' }; + metadata: IntentRecord['metadata'] & { + llmUsed: false; + gitIndexVerified: false; + conflictStyle: 'merge' | 'diff3'; + markerWidth: number; + blockSha256: string; + }; +}; diff --git a/test/fixtures/contracts/merge-conflict-type.ts b/test/fixtures/contracts/merge-conflict-type.ts new file mode 100644 index 00000000..319c379b --- /dev/null +++ b/test/fixtures/contracts/merge-conflict-type.ts @@ -0,0 +1,20 @@ +import type { IntentRecord, MergeConflictFact } from '../../../src/core/types.js'; + +// Compile-only consumers: the public view remains assignable to IntentRecord, +// but generic facts and authority claims cannot masquerade as marker evidence. +export function contract(fact: MergeConflictFact, generic: IntentRecord): IntentRecord { + const observed: 'observed' = fact.statement.modality; + const notVerified: false = fact.metadata.gitIndexVerified; + const source: string = fact.source.path; + void [observed, notVerified, source]; + // @ts-expect-error a generic record has no conflict-fact guarantees + const missingContract: MergeConflictFact = generic; + // @ts-expect-error syntactic observations do not verify the Git index + const verified: MergeConflictFact['metadata'] = { ...fact.metadata, gitIndexVerified: true }; + // @ts-expect-error marker evidence cannot authorize a merge + const approval: MergeConflictFact['statement'] = { ...fact.statement, action: 'approve' }; + // @ts-expect-error the source must identify a file + const missingSource: MergeConflictFact['source'] = { ...fact.source, path: null }; + void [missingContract, verified, approval, missingSource]; + return fact; +}