diff --git a/build/cli/index.js b/build/cli/index.js index 834b725c5..f93c57c05 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -100930,6 +100930,109 @@ function replyRequestBodyDigest(body) { } +/***/ }), + +/***/ 28518: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.countSddShorthandCaseReferences = countSddShorthandCaseReferences; +const sdd_visible_references_1 = __nccwpck_require__(71766); +// Full namespaces retain their established meaning; a bare technical token has no role by itself. +const TOKEN = /(?#=+\-\u2013\u2014]*$/iu; +const CONTINUATION = /^[\s,;.…\-\u2013\u2014*_`()[\]#]*(?:(?:and|or|to|through|y|o|a|hasta|e)\b[\s,;.…\-\u2013\u2014*_`()[\]#]*)*$/iu; +const CASE_COLUMN = /^(?:cases?(?:[ _-]?ids?| references?)?|test cases?(?: ids?)?|casos?(?: de prueba)?|ids? de casos?|identificadores de casos?)$/iu; +/** Unescaped pipes delimit cells, including when the pipe occurs in an inline code span. */ +function row(text, offset) { + const cells = []; + let start = 0, escaped = false, hasPipe = false; + for (let index = 0; index < text.length; index++) { + const character = text[index]; + if (character === '|' && !escaped) { + cells.push({ text: text.slice(start, index), start: offset + start, end: offset + index }); + start = index + 1; + hasPipe = true; + } + escaped = character === '\\' && !escaped; + } + cells.push({ text: text.slice(start), start: offset + start, end: offset + text.length }); + if (hasPipe && cells[0].text.trim() === '') + cells.shift(); + if (hasPipe && cells.length && cells[cells.length - 1].text.trim() === '') + cells.pop(); + return { cells, hasPipe }; +} +function caseColumn(value) { + const label = (0, sdd_visible_references_1.readSddVisibleReferences)(value).parts.map(part => part.value).join('').trim().replace(/\s+/gu, ' '); + return CASE_COLUMN.test(label); +} +/** CommonMark paragraphs bound the GFM table projection, including nested quote/list blocks. */ +function tableCellRoles(source) { + const lines = source.split('\n'); + const offsets = []; + let offset = 0; + for (const line of lines) { + offsets.push(offset); + offset += line.length + 1; + } + const roles = []; + for (const block of (0, sdd_visible_references_1.readSddTextBlockLocations)(source)) { + const rows = []; + for (let line = block.startLine - 1; line < block.endLine; line++) { + const raw = lines[line]; + const prefix = line === block.startLine - 1 ? block.startColumn - 1 + : (/^ {0,3}(?:>[ \t]?)+/u.exec(raw)?.[0].length ?? 0); + rows.push(row(raw.slice(prefix), offsets[line] + prefix)); + } + let columns; + for (let index = 0; index < rows.length; index++) { + const header = rows[index], separator = rows[index + 1]; + if (separator && (header.hasPipe || separator.hasPipe) && header.cells.length > 0 + && separator.cells.length === header.cells.length && separator.cells.every(cell => /^:?-+:?$/u.test(cell.text.trim()))) { + columns = header.cells.map(cell => caseColumn(cell.text)); + roles.push(...header.cells.map(cell => ({ start: cell.start, end: cell.end, caseRole: false }))); + index++; + } + else if (columns) { + // GFM permits missing cells and body rows without outer pipes. Extra cells cannot gain a role. + const activeColumns = columns; + roles.push(...header.cells.map((cell, column) => ({ start: cell.start, end: cell.end, caseRole: activeColumns[column] === true }))); + } + } + } + return roles; +} +/** Rendered inline labels/code retain references; link destinations are not case IDs. */ +function countReferences(source, caseRole) { + const text = (0, sdd_visible_references_1.readSddVisibleReferences)(source).parts.map(part => part.value).join(''); + let previousEnd = 0, previousWasCase = false, count = 0; + for (const match of text.matchAll(TOKEN)) { + const gap = text.slice(previousEnd, match.index); + const reference = caseRole || EXPLICIT_REFERENCE.test(gap) + || (previousWasCase && CONTINUATION.test(gap)); + if (!match[1] && reference) + count++; + previousWasCase = Boolean(match[1]) || reference; + previousEnd = match.index + match[0].length; + } + return count; +} +/** Receives the owner's existing hidden/fenced-text projection; never rewrites caller Markdown. */ +function countSddShorthandCaseReferences(visibleSource) { + const source = visibleSource.replace(/\r\n?/gu, '\n'); + let previousEnd = 0, count = 0; + for (const cell of tableCellRoles(source)) { + count += countReferences(source.slice(previousEnd, cell.start), false); + count += countReferences(source.slice(cell.start, cell.end), cell.caseRole); + previousEnd = cell.end; + } + return count + countReferences(source.slice(previousEnd), false); +} + + /***/ }), /***/ 95312: @@ -101067,6 +101170,7 @@ exports.validateSddDraftConsistency = validateSddDraftConsistency; const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_repository_reference_1 = __nccwpck_require__(17791); const sdd_visible_references_1 = __nccwpck_require__(71766); +const sdd_case_reference_1 = __nccwpck_require__(28518); const CASE = /\b(?:[A-Za-z][A-Za-z0-9]*-)+C\d+\b/gu; const CASE_ID = /^(?:[A-Za-z][A-Za-z0-9]*-)+C\d+$/u; const REQUIREMENT_ID = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+$/u; @@ -101075,7 +101179,7 @@ exports.SDD_CONSISTENCY_PROMPT = `Consistency contract for the proposed draft/ap In section 4 include one table: Requirement | Behavior. Give every goal a stable requirement ID. In section 14 include one budget table: Area | Minimum distinct cases | Behaviors/risks covered; positive integers and one Total. Also in section 14 include one inventory table: Case ID | Area | Requirements | Scenario. -Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. +Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. Ordinary technical designators in prose, scenario text or non-case columns are not acceptance-case references solely because they resemble short IDs; preserve their original terminology. Case-reference fields, explicit references and adjacent case-ID lists still require full identities. In section 17 include one table: Requirement | Cases. List comma-separated full case IDs for every requirement. Its links must exactly match the case inventory in both directions, with no undefined or generic documentation checks. In section 15 include one table: Path | Change. Change is update, reference or create. Include the selected SDD path itself with update for an existing SDD or create for a new/companion SDD. Use existing immutable-base files for update/reference. Only explicitly proposed new files may use create; never mark a missing current document as an existing artifact. Cite repository files as full repository-relative paths in backticks with a File or Path label when ambiguous, or relative Markdown file links. Put every planned new file in this table; a proposed parent directory is valid only as an ancestor of a declared new file, never as a file declaration. HTTP endpoints, media types, mode alternatives and module names are protocol notation, not file-existence claims. Label file-shaped relative asset addresses with URL or URI; do not relabel a repository citation to evade validation. Explicit Path/Change rows and relative Markdown destinations always receive strict repository checks. These tables are publication consistency checks, not runtime/security acceptance.`; const BUDGET_SLOT = '{{SDD_CASE_BUDGET}}'; @@ -101246,9 +101350,9 @@ function validateSddDraftConsistency(markdown, owners, inventory, sddPath) { const caseIds = cases.map(row => row[0]); const reserved = new Set(reservedSddCaseIds(owners)); uniqueIdentifiers(caseIds, CASE_ID, 'case-identity', 14); - const shorthand = [...source.matchAll(/(? reserved.has(id))) fail('case-identity', { detail: 'case-reserved', section: 14, expected: 0, observed: caseIds.filter(id => reserved.has(id)).length }); // References to existing owner cases are allowed, but cannot count as newly defined cases. @@ -101624,9 +101728,25 @@ function boundedInteger(value, minimum) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readSddTextBlockLocations = readSddTextBlockLocations; exports.readSddVisibleReferences = readSddVisibleReferences; exports.isSddMarkdownLinkExample = isSddMarkdownLinkExample; const commonmark_1 = __nccwpck_require__(86076); +/** CommonMark supplies block boundaries; callers retain raw table-cell escapes. */ +function readSddTextBlockLocations(source) { + const blocks = []; + const walker = new commonmark_1.Parser().parse(source).walker(); + let event; + while ((event = walker.next())) { + const { node, entering } = event; + if (entering && ['paragraph', 'heading'].includes(node.type)) { + // These concrete block nodes always have source positions in CommonMark. + const [start, end] = node.sourcepos; + blocks.push({ startLine: start[0], startColumn: start[1], endLine: end[0] }); + } + } + return blocks; +} /** Pure syntax projection; callers first remove the gate's hidden comments/fences. */ function readSddVisibleReferences(source) { const destinations = []; diff --git a/build/github_action/index.js b/build/github_action/index.js index 4355caeac..afab56d5e 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -91930,6 +91930,109 @@ function replyRequestBodyDigest(body) { } +/***/ }), + +/***/ 28518: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.countSddShorthandCaseReferences = countSddShorthandCaseReferences; +const sdd_visible_references_1 = __nccwpck_require__(71766); +// Full namespaces retain their established meaning; a bare technical token has no role by itself. +const TOKEN = /(?#=+\-\u2013\u2014]*$/iu; +const CONTINUATION = /^[\s,;.…\-\u2013\u2014*_`()[\]#]*(?:(?:and|or|to|through|y|o|a|hasta|e)\b[\s,;.…\-\u2013\u2014*_`()[\]#]*)*$/iu; +const CASE_COLUMN = /^(?:cases?(?:[ _-]?ids?| references?)?|test cases?(?: ids?)?|casos?(?: de prueba)?|ids? de casos?|identificadores de casos?)$/iu; +/** Unescaped pipes delimit cells, including when the pipe occurs in an inline code span. */ +function row(text, offset) { + const cells = []; + let start = 0, escaped = false, hasPipe = false; + for (let index = 0; index < text.length; index++) { + const character = text[index]; + if (character === '|' && !escaped) { + cells.push({ text: text.slice(start, index), start: offset + start, end: offset + index }); + start = index + 1; + hasPipe = true; + } + escaped = character === '\\' && !escaped; + } + cells.push({ text: text.slice(start), start: offset + start, end: offset + text.length }); + if (hasPipe && cells[0].text.trim() === '') + cells.shift(); + if (hasPipe && cells.length && cells[cells.length - 1].text.trim() === '') + cells.pop(); + return { cells, hasPipe }; +} +function caseColumn(value) { + const label = (0, sdd_visible_references_1.readSddVisibleReferences)(value).parts.map(part => part.value).join('').trim().replace(/\s+/gu, ' '); + return CASE_COLUMN.test(label); +} +/** CommonMark paragraphs bound the GFM table projection, including nested quote/list blocks. */ +function tableCellRoles(source) { + const lines = source.split('\n'); + const offsets = []; + let offset = 0; + for (const line of lines) { + offsets.push(offset); + offset += line.length + 1; + } + const roles = []; + for (const block of (0, sdd_visible_references_1.readSddTextBlockLocations)(source)) { + const rows = []; + for (let line = block.startLine - 1; line < block.endLine; line++) { + const raw = lines[line]; + const prefix = line === block.startLine - 1 ? block.startColumn - 1 + : (/^ {0,3}(?:>[ \t]?)+/u.exec(raw)?.[0].length ?? 0); + rows.push(row(raw.slice(prefix), offsets[line] + prefix)); + } + let columns; + for (let index = 0; index < rows.length; index++) { + const header = rows[index], separator = rows[index + 1]; + if (separator && (header.hasPipe || separator.hasPipe) && header.cells.length > 0 + && separator.cells.length === header.cells.length && separator.cells.every(cell => /^:?-+:?$/u.test(cell.text.trim()))) { + columns = header.cells.map(cell => caseColumn(cell.text)); + roles.push(...header.cells.map(cell => ({ start: cell.start, end: cell.end, caseRole: false }))); + index++; + } + else if (columns) { + // GFM permits missing cells and body rows without outer pipes. Extra cells cannot gain a role. + const activeColumns = columns; + roles.push(...header.cells.map((cell, column) => ({ start: cell.start, end: cell.end, caseRole: activeColumns[column] === true }))); + } + } + } + return roles; +} +/** Rendered inline labels/code retain references; link destinations are not case IDs. */ +function countReferences(source, caseRole) { + const text = (0, sdd_visible_references_1.readSddVisibleReferences)(source).parts.map(part => part.value).join(''); + let previousEnd = 0, previousWasCase = false, count = 0; + for (const match of text.matchAll(TOKEN)) { + const gap = text.slice(previousEnd, match.index); + const reference = caseRole || EXPLICIT_REFERENCE.test(gap) + || (previousWasCase && CONTINUATION.test(gap)); + if (!match[1] && reference) + count++; + previousWasCase = Boolean(match[1]) || reference; + previousEnd = match.index + match[0].length; + } + return count; +} +/** Receives the owner's existing hidden/fenced-text projection; never rewrites caller Markdown. */ +function countSddShorthandCaseReferences(visibleSource) { + const source = visibleSource.replace(/\r\n?/gu, '\n'); + let previousEnd = 0, count = 0; + for (const cell of tableCellRoles(source)) { + count += countReferences(source.slice(previousEnd, cell.start), false); + count += countReferences(source.slice(cell.start, cell.end), cell.caseRole); + previousEnd = cell.end; + } + return count + countReferences(source.slice(previousEnd), false); +} + + /***/ }), /***/ 95312: @@ -92067,6 +92170,7 @@ exports.validateSddDraftConsistency = validateSddDraftConsistency; const pre_branch_sdd_1 = __nccwpck_require__(34730); const sdd_repository_reference_1 = __nccwpck_require__(17791); const sdd_visible_references_1 = __nccwpck_require__(71766); +const sdd_case_reference_1 = __nccwpck_require__(28518); const CASE = /\b(?:[A-Za-z][A-Za-z0-9]*-)+C\d+\b/gu; const CASE_ID = /^(?:[A-Za-z][A-Za-z0-9]*-)+C\d+$/u; const REQUIREMENT_ID = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)+$/u; @@ -92075,7 +92179,7 @@ exports.SDD_CONSISTENCY_PROMPT = `Consistency contract for the proposed draft/ap In section 4 include one table: Requirement | Behavior. Give every goal a stable requirement ID. In section 14 include one budget table: Area | Minimum distinct cases | Behaviors/risks covered; positive integers and one Total. Also in section 14 include one inventory table: Case ID | Area | Requirements | Scenario. -Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. +Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. Ordinary technical designators in prose, scenario text or non-case columns are not acceptance-case references solely because they resemble short IDs; preserve their original terminology. Case-reference fields, explicit references and adjacent case-ID lists still require full identities. In section 17 include one table: Requirement | Cases. List comma-separated full case IDs for every requirement. Its links must exactly match the case inventory in both directions, with no undefined or generic documentation checks. In section 15 include one table: Path | Change. Change is update, reference or create. Include the selected SDD path itself with update for an existing SDD or create for a new/companion SDD. Use existing immutable-base files for update/reference. Only explicitly proposed new files may use create; never mark a missing current document as an existing artifact. Cite repository files as full repository-relative paths in backticks with a File or Path label when ambiguous, or relative Markdown file links. Put every planned new file in this table; a proposed parent directory is valid only as an ancestor of a declared new file, never as a file declaration. HTTP endpoints, media types, mode alternatives and module names are protocol notation, not file-existence claims. Label file-shaped relative asset addresses with URL or URI; do not relabel a repository citation to evade validation. Explicit Path/Change rows and relative Markdown destinations always receive strict repository checks. These tables are publication consistency checks, not runtime/security acceptance.`; const BUDGET_SLOT = '{{SDD_CASE_BUDGET}}'; @@ -92246,9 +92350,9 @@ function validateSddDraftConsistency(markdown, owners, inventory, sddPath) { const caseIds = cases.map(row => row[0]); const reserved = new Set(reservedSddCaseIds(owners)); uniqueIdentifiers(caseIds, CASE_ID, 'case-identity', 14); - const shorthand = [...source.matchAll(/(? reserved.has(id))) fail('case-identity', { detail: 'case-reserved', section: 14, expected: 0, observed: caseIds.filter(id => reserved.has(id)).length }); // References to existing owner cases are allowed, but cannot count as newly defined cases. @@ -92624,9 +92728,25 @@ function boundedInteger(value, minimum) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.readSddTextBlockLocations = readSddTextBlockLocations; exports.readSddVisibleReferences = readSddVisibleReferences; exports.isSddMarkdownLinkExample = isSddMarkdownLinkExample; const commonmark_1 = __nccwpck_require__(86076); +/** CommonMark supplies block boundaries; callers retain raw table-cell escapes. */ +function readSddTextBlockLocations(source) { + const blocks = []; + const walker = new commonmark_1.Parser().parse(source).walker(); + let event; + while ((event = walker.next())) { + const { node, entering } = event; + if (entering && ['paragraph', 'heading'].includes(node.type)) { + // These concrete block nodes always have source positions in CommonMark. + const [start, end] = node.sourcepos; + blocks.push({ startLine: start[0], startColumn: start[1], endLine: end[0] }); + } + } + return blocks; +} /** Pure syntax projection; callers first remove the gate's hidden comments/fences. */ function readSddVisibleReferences(source) { const destinations = []; diff --git a/docs/development/specifications.mdx b/docs/development/specifications.mdx index 8036ba0f0..251d8e26d 100644 --- a/docs/development/specifications.mdx +++ b/docs/development/specifications.mdx @@ -76,6 +76,12 @@ do not pre-encode paths in `catalog.json`. Regenerate the catalog with the norma commands below after updating the shared SDK validator. The same renderer serves primary SDDs, companion SDDs and every evidence role. +Technical terminology may retain designators such as `DEL/C1` and connector +`C1`. Use fully qualified IDs when referring to acceptance cases in case-role +fields, explicit references and case-ID lists. Ordinary technical operands are +not counted as cases; identity, budget, reciprocal mapping and immutable owner +checks still apply. See [the SDD consistency guide](/issues/pre-branch-sdds). + ## Capability titles Capability titles appear as literal text in the catalog's specification link diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index 4233c6b64..1ced70e0d 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -63,6 +63,16 @@ and ranges do not establish new cases. Each case belongs to one budget area. Equal grand totals cannot hide an over-allocation in one area and a shortage in another. Generic documentation checks cannot replace defined acceptance cases. +Technical designators such as `DEL/C1`, Unicode `C1`, or connector `C1` are +ordinary prose operands. They do not become case references simply because +of their spelling, including in scenario/behavior or non-case table cells. +Keep full IDs in case-ID/reference columns and explicit case references or +case-ID lists; shorthand and abbreviated ranges there remain invalid. Link +labels can refer to cases, while a technical value inside the destination URI +does not itself declare a case. Fenced examples and comments retain their +existing visibility rules. Accepting technical prose does not create a case, +change a budget, relax an owner reservation or prove a test executed. + Cite repository files with full repository-relative paths in backticks or relative Markdown file links. Existing paths are checked against the selected Git commit, including files outside the catalog evidence arrays. Relative links diff --git a/quality/sdd/evidence/issue-845.json b/quality/sdd/evidence/issue-845.json new file mode 100644 index 000000000..850cf3489 --- /dev/null +++ b/quality/sdd/evidence/issue-845.json @@ -0,0 +1,438 @@ +{ + "schemaVersion": 1, + "issue": 845, + "kind": "Actual current domain validators with independently authored fixture data; no live provider or consumer execution", + "gitBase": "16bf97fff42670bc8718a40f02a518a0f855b634", + "approvedSDK": { + "sourceCommit": "771dc505ee24d5ea37883749ae298c27f1d6b337", + "mergedAs": "16bf97fff42670bc8718a40f02a518a0f855b634", + "cliSha256": "8cb351789cd82f9ef9d825fd97d3b46a87c50683ec551ee71b76bc9028313451" + }, + "scope": "Actual current SDK validation functions loaded from exact source. Independently authored 48-row fixture and identical owners/inventory across phrase variants. No provider, consumer implementation execution, hosted Action or historical checkpoint-input equality claim.", + "readOnlyConsumerContext": { + "path": "specs/household-entry-interface.md", + "sha": "ff87750ca63faed8a650718900a52e9ce38e648d", + "size": 50477, + "html_url": "https://github.com/vypdev/domus/blob/ca55cdb56779573a0dd45b47b9546b74b4ffcc74/specs/household-entry-interface.md", + "sha256": "68c3f90d903290913ea517886487bbbfa12ca392ff0050a720571b866fcb10d4", + "ref": "ca55cdb56779573a0dd45b47b9546b74b4ffcc74", + "gitBlobVerified": true, + "literalRows": 48, + "limitations": "Reference document was read at the reported immutable commit; its historical owner/inventory checkpoint was not reconstructed or relabeled as this controlled fixture." + }, + "before": { + "fixture": { + "baseSha256": "9a967037bf597e9e5035b5574e332f0a63fd747531b45e9e50cc3b571e5e3e8e", + "literalRows": 48, + "caseRowsSha256": "5598a710b58146acf599161334323f7d0f1188411ceae8b1c9fed1f9c65d5f83", + "reverseMappingSha256": "7eca819470e2e134200e223289dbde40b47d1606aa8dc655f9af15e7aef54522", + "baseline": { + "valid": true + } + }, + "positives": [ + { + "prose": "Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.", + "inputSha256": "73cf04eea53bcaf6027144e271ef7e4267ee5499b04f873396e92810ed43dc35", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 1 + } + ] + } + } + }, + { + "prose": "Connector C1 is connected to component C2.", + "inputSha256": "7d56f8d8a513c14e380fcd92344c9b570ba6ce04a6e51b366ce0bb6b1c5a50b1", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 2 + } + ] + } + } + }, + { + "prose": "The Unicode C1 control set remains readable.", + "inputSha256": "6fab92eb599362d156a4b81cdc851960ef8ef7392404e9e78fed5143ed263ffe", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 1 + } + ] + } + } + } + ], + "negatives": [ + { + "name": "explicit shorthand", + "inputSha256": "873527d5846a20d40e83c226cdb0781d3bf22327f8cc960a688f51f836036f0a", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 2 + } + ] + } + } + }, + { + "name": "shorthand range", + "inputSha256": "3e38ccea842dacb5f3439abe56463756e57e2a09c16160d1138b215e3458ae8a", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 2 + } + ] + } + } + }, + { + "name": "unknown full reference", + "inputSha256": "a3a6c51de6f2a06d7ab4d6eab91067b00e0fb882d0f5e555715b94562da081bf", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-reference", + "section": 0 + } + ] + } + } + }, + { + "name": "identity shorthand", + "inputSha256": "8305613a63898812cc24f7c678f176cc900485e63f774f5d0f5e401d8358c8ca", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "identifier-list", + "section": 14, + "row": 1, + "expected": 0, + "observed": 1 + } + ] + } + } + }, + { + "name": "reserved identity", + "inputSha256": "c06a1138eb2502068c5890d369fde7341175bd4b61153fb1e2e1272aecd32d7e", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-reserved", + "section": 14, + "expected": 0, + "observed": 1 + } + ] + } + } + }, + { + "name": "invalid reverse mapping", + "inputSha256": "32ff4787c3f6eade0bd71ec36a6bd7d64af7a5422c5ae7612c2c47ffd01673f6", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its requirement-mapping consistency contract.", + "reason": "requirement-mapping", + "diagnostic": { + "reason": "requirement-mapping", + "phase": "validation", + "facts": [ + { + "detail": "traceability-links", + "section": 17, + "row": 1, + "expected": 24, + "observed": 23 + } + ] + } + } + } + ], + "loaded": { + "src/domain/pre_branch_sdd.ts": "636db20b18cd7b694c2d68e2d5fc53f5387daae90bcb231c98c2c2534d6125c3", + "src/domain/sdd_catalog_compatibility.ts": "150d56f78b12133d61c60d3321abbde254b23be8d55144f865bddae2b53b5d11", + "src/domain/sdd_validation_diagnostic.ts": "0ed4365e2e3739b5e3baf083bbdc73ecda3f09dc5a6e105b1cc12f72c90d7fb0", + "src/domain/sdd_draft_consistency.ts": "8fc8cb8632e99b0c7509143e7fefce807ed6d962ae797221862ef37e17db63d5", + "src/domain/sdd_repository_reference.ts": "32ab430eb76e7fc8058b678e9b7e2bfdc7f1808c85a0451300fa961d223d374c", + "src/domain/sdd_visible_references.ts": "b31e3804f0f18dfc5f7132d87b40922a0fc038a18f0df9723cade82422c9a407" + } + }, + "after": { + "fixture": { + "baseSha256": "9a967037bf597e9e5035b5574e332f0a63fd747531b45e9e50cc3b571e5e3e8e", + "literalRows": 48, + "caseRowsSha256": "5598a710b58146acf599161334323f7d0f1188411ceae8b1c9fed1f9c65d5f83", + "reverseMappingSha256": "7eca819470e2e134200e223289dbde40b47d1606aa8dc655f9af15e7aef54522", + "baseline": { + "valid": true + } + }, + "positives": [ + { + "prose": "Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.", + "inputSha256": "73cf04eea53bcaf6027144e271ef7e4267ee5499b04f873396e92810ed43dc35", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": true + } + }, + { + "prose": "Connector C1 is connected to component C2.", + "inputSha256": "7d56f8d8a513c14e380fcd92344c9b570ba6ce04a6e51b366ce0bb6b1c5a50b1", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": true + } + }, + { + "prose": "The Unicode C1 control set remains readable.", + "inputSha256": "6fab92eb599362d156a4b81cdc851960ef8ef7392404e9e78fed5143ed263ffe", + "literalRowsUnchanged": true, + "reverseMappingUnchanged": true, + "result": { + "valid": true + } + } + ], + "negatives": [ + { + "name": "explicit shorthand", + "inputSha256": "873527d5846a20d40e83c226cdb0781d3bf22327f8cc960a688f51f836036f0a", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 2 + } + ] + } + } + }, + { + "name": "shorthand range", + "inputSha256": "3e38ccea842dacb5f3439abe56463756e57e2a09c16160d1138b215e3458ae8a", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-shorthand", + "section": 0, + "expected": 0, + "observed": 2 + } + ] + } + } + }, + { + "name": "unknown full reference", + "inputSha256": "a3a6c51de6f2a06d7ab4d6eab91067b00e0fb882d0f5e555715b94562da081bf", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-reference", + "section": 0 + } + ] + } + } + }, + { + "name": "identity shorthand", + "inputSha256": "8305613a63898812cc24f7c678f176cc900485e63f774f5d0f5e401d8358c8ca", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "identifier-list", + "section": 14, + "row": 1, + "expected": 0, + "observed": 1 + } + ] + } + } + }, + { + "name": "reserved identity", + "inputSha256": "c06a1138eb2502068c5890d369fde7341175bd4b61153fb1e2e1272aecd32d7e", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its case-identity consistency contract.", + "reason": "case-identity", + "diagnostic": { + "reason": "case-identity", + "phase": "validation", + "facts": [ + { + "detail": "case-reserved", + "section": 14, + "expected": 0, + "observed": 1 + } + ] + } + } + }, + { + "name": "invalid reverse mapping", + "inputSha256": "32ff4787c3f6eade0bd71ec36a6bd7d64af7a5422c5ae7612c2c47ffd01673f6", + "result": { + "valid": false, + "name": "Error", + "message": "The SDD draft violates its requirement-mapping consistency contract.", + "reason": "requirement-mapping", + "diagnostic": { + "reason": "requirement-mapping", + "phase": "validation", + "facts": [ + { + "detail": "traceability-links", + "section": 17, + "row": 1, + "expected": 24, + "observed": 23 + } + ] + } + } + } + ], + "loaded": { + "src/domain/pre_branch_sdd.ts": "636db20b18cd7b694c2d68e2d5fc53f5387daae90bcb231c98c2c2534d6125c3", + "src/domain/sdd_catalog_compatibility.ts": "150d56f78b12133d61c60d3321abbde254b23be8d55144f865bddae2b53b5d11", + "src/domain/sdd_validation_diagnostic.ts": "0ed4365e2e3739b5e3baf083bbdc73ecda3f09dc5a6e105b1cc12f72c90d7fb0", + "src/domain/sdd_draft_consistency.ts": "f0c1db213a8485218ed9b4465e70f421490308c8f7874d8e47cf12d64e2becf8", + "src/domain/sdd_repository_reference.ts": "32ab430eb76e7fc8058b678e9b7e2bfdc7f1808c85a0451300fa961d223d374c", + "src/domain/sdd_visible_references.ts": "8705d87566ca663d43073d6f946340e2a41f88db37d59d2df4ce11320c825a59", + "src/domain/sdd_case_reference.ts": "23bfba93d7b18f65adf4fa67349a2478b07286ddbb3a432bbc59a64d6b78c167" + } + }, + "limits": [ + "The 48 fixture rows are not 48 executed SDK tests.", + "The after result binds working-tree source hashes; the Git base is not represented as an immutable after commit.", + "Historical consumer owner/inventory checkpoint and raw candidate hashes were not reconstructed." + ] +} diff --git a/scripts/coverage-budgets.json b/scripts/coverage-budgets.json index 3e4665c6f..470e7b2c7 100644 --- a/scripts/coverage-budgets.json +++ b/scripts/coverage-budgets.json @@ -1424,6 +1424,20 @@ } ], "successMessage": "branch sync publication recovery: PASS (closed facts, bounded classification and locale content100%)" + }, + { + "name": "SDD case-reference classification", + "missingEntryLabel": "SDD case-reference classification", + "rules": [ + { + "files": [ + "src/domain/sdd_case_reference.ts" + ], + "mode": "each", + "thresholdProfile": "exhaustive" + } + ], + "successMessage": "SDD case-reference classification: PASS (100% lines/statements/functions/branches)" } ] } diff --git a/specs/CATALOG.md b/specs/CATALOG.md index dff457e3c..fd02d8fbf 100644 --- a/specs/CATALOG.md +++ b/specs/CATALOG.md @@ -20,7 +20,7 @@ debt or convert unknown historic intent into a design decision. | `local-web-setup-assistant` | Proposed | Offer a packaged, loopback-only Svelte setup interface over the existing engine with four-language guidance, English CLI help, basic/custom first-run paths, editable question and plan review, source-labelled GitHub facts, assisted CI and Project evidence, role-separated PATs, and truthful itemized outcomes; inspect existing configuration and apply independent, scope-aware runtime adjustments with permission, drift and read-back verification | [Local web setup assistant](./local-web-setup-assistant.md) + 2 companion | 248 paths · 2026-10-21 | | `guided-bot-pat-onboarding` | Proposed | Guide creation of the persistent workflow PAT, verify bot identity and grants, recover explicitly from wrong-account input within the session, and install the approved Actions Secret | [Guided bot PAT onboarding](./guided-bot-pat-onboarding.md) | 39 paths · 2026-10-07 | | `temporary-setup-operator-authorization` | Proposed | Guide creation of the one-run operator PAT through GitHub's official form, verify final setup access, and report user-owned deletion accurately | [Assisted setup PAT creation](./temporary-setup-operator-authorization.md) | 36 paths · 2026-10-18 | -| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links; report confirmed missing trusted catalogs with immutable source provenance and prerequisite recovery before model or publication; bind pre-branch analysis and drafting to a verified independent read-only view of the selected immutable source without changing the canonical checkout; distinguish visible protocol notation from repository citations, allow only constructive proposed-file directory ancestry and expose bounded private-safe origin diagnostics | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 5 companion | 162 paths · 2026-11-03 | +| `issue-start-and-sdd-readiness` | Implemented | Start every admitted issue with one explicit signal and publish a validated SDD before eligible Action-managed branch work; inspect verified SDD status and bounded structural publication comparisons and preserve complete navigable catalog links for accepted repository filenames and safely publish generated catalogs without following destination links; bootstrap truthful proposed SDK capabilities before implementation without changing readiness; reject incomplete specification discovery explicitly without following unregistered symbolic links; report confirmed missing trusted catalogs with immutable source provenance and prerequisite recovery before model or publication; bind pre-branch analysis and drafting to a verified independent read-only view of the selected immutable source without changing the canonical checkout; distinguish visible protocol notation from repository citations, allow only constructive proposed-file directory ancestry and expose bounded private-safe origin diagnostics | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 6 companion | 164 paths · 2026-11-03 | | `managed-issue-lifecycle` | As-built baseline | Convert typed issues into traceable work branches, project state, and lifecycle state | [Managed issue and branch lifecycle](./managed-issue-and-branch-lifecycle.md) | 75 paths · 2026-10-30 | | `comment-automation` | Implemented | Admit only explicit commands or exact mentions, then route them while protecting repository mutations | [Comment automation and authorization](./comment-automation-and-authorization.md) | 88 paths · 2026-10-26 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze bounded diff partitions with version-grounded dependency, policy-to-request transformation and source-bound assertion and generated-source coordinates with independent candidate verification, configuration-selected initial-session credential ownership, delegated test-path and external Action input-contract, host-aware self-repository references and Kotlin/Wasm lexical-binding and renderer-grounded Markdown path evidence, publish stable findings atomically, and apply authorized verified fixes with regression coverage for cwd/pattern-grounded indirect unittest discovery and source-bound module execution evidence, with active-instance Kotlin Json nullable-input policy grounding; verify complete large diffs through a source-bound closed framed profile with immutable local Git proof, live-base freshness and atomic global publication; ground Python exception syntax in declared interpreter versions and PEP 758 while preserving older-target and independent runtime defects; ground filesystem recovery findings in retention/no-overwrite, ownership and publication contracts while preserving required disposable cleanup and data-loss defects; independently adjudicate candidates for supplied Python source, retaining existing bounded source, identity and history controls; trace concrete helper defaults/overrides, database-file parent containment and stored query values before judging assertions, preserving exact fragment coordinates and genuine assertion failures; co-deliver complete long discussion to every source-bound partition within closed native prompt and invocation traffic limits, preserving provenance and truthful provider partial coverage | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 7 companion | 198 paths · 2026-11-03 | @@ -149,11 +149,11 @@ debt or convert unknown historic intent into a design decision. - Owner: Copilot maintainers - Last verified: 2026-11-03 -- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-diff.md`](./pre-branch-sdd-diff.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md) · [`specs/pre-branch-sdd-source-context.md`](./pre-branch-sdd-source-context.md) · [`specs/sdd-reference-classification.md`](./sdd-reference-classification.md) +- Specifications: [`specs/issue-start-and-branch-readiness.md`](./issue-start-and-branch-readiness.md) · [`specs/pre-branch-sdd-gate.md`](./pre-branch-sdd-gate.md) · [`specs/pre-branch-sdd-diff.md`](./pre-branch-sdd-diff.md) · [`specs/pre-branch-sdd-status-readiness.md`](./pre-branch-sdd-status-readiness.md) · [`specs/pre-branch-sdd-source-context.md`](./pre-branch-sdd-source-context.md) · [`specs/sdd-reference-classification.md`](./sdd-reference-classification.md) · [`specs/sdd-case-reference-classification.md`](./sdd-case-reference-classification.md) - Workflows: [`.github/workflows/copilot_issue.yml`](../.github/workflows/copilot_issue.yml) · [`.github/workflows/copilot_issue_comment.yml`](../.github/workflows/copilot_issue_comment.yml) · [`.github/workflows/copilot_commit.yml`](../.github/workflows/copilot_commit.yml) · [`.github/workflows/copilot_pull_request_comment.yml`](../.github/workflows/copilot_pull_request_comment.yml) - Entrypoints: [`action.yml`](../action.yml) · [`src/actions/common_action.ts`](../src/actions/common_action.ts) · [`src/actions/github_action.ts`](../src/actions/github_action.ts) -- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/domain/pre_branch_sdd_diff.ts`](../src/domain/pre_branch_sdd_diff.ts) · [`src/application/policies/sdd_diff_message_catalog.ts`](../src/application/policies/sdd_diff_message_catalog.ts) · [`src/application/policies/sdd_diff_presentation_policy.ts`](../src/application/policies/sdd_diff_presentation_policy.ts) · [`src/application/ports/sdd_diff_read_ports.ts`](../src/application/ports/sdd_diff_read_ports.ts) · [`src/application/usecases/sdd/sdd_diff_query_use_case.ts`](../src/application/usecases/sdd/sdd_diff_query_use_case.ts) · [`src/data/repository/sdd/sdd_diff_link_repository.ts`](../src/data/repository/sdd/sdd_diff_link_repository.ts) · [`src/infrastructure/composition/sdd_diff_composition_root.ts`](../src/infrastructure/composition/sdd_diff_composition_root.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/policies/sdd_analysis_status_policy.ts`](../src/application/policies/sdd_analysis_status_policy.ts) · [`src/domain/sdd_draft_consistency.ts`](../src/domain/sdd_draft_consistency.ts) · [`src/domain/sdd_validation_diagnostic.ts`](../src/domain/sdd_validation_diagnostic.ts) · [`src/application/errors/sdd_draft_preparation_failure.ts`](../src/application/errors/sdd_draft_preparation_failure.ts) · [`src/application/policies/sdd_preparation_message_catalog.ts`](../src/application/policies/sdd_preparation_message_catalog.ts) · [`src/application/policies/sdd_preparation_status_policy.ts`](../src/application/policies/sdd_preparation_status_policy.ts) · [`src/domain/sdd_catalog_compatibility.ts`](../src/domain/sdd_catalog_compatibility.ts) · [`src/infrastructure/specification_catalog_publication.cjs`](../src/infrastructure/specification_catalog_publication.cjs) · [`jest.config.js`](../jest.config.js) · [`src/domain/sdd_trusted_catalog_failure.ts`](../src/domain/sdd_trusted_catalog_failure.ts) · [`src/domain/agent_source_snapshot.ts`](../src/domain/agent_source_snapshot.ts) · [`src/application/policies/agent_source_snapshot_policy.ts`](../src/application/policies/agent_source_snapshot_policy.ts) · [`src/infrastructure/agents/agent_source_snapshot_git.ts`](../src/infrastructure/agents/agent_source_snapshot_git.ts) · [`src/infrastructure/agents/agent_source_snapshot_workspace.ts`](../src/infrastructure/agents/agent_source_snapshot_workspace.ts) · [`src/infrastructure/agents/agent_source_snapshot_integrity.ts`](../src/infrastructure/agents/agent_source_snapshot_integrity.ts) · [`src/domain/sdd_repository_reference.ts`](../src/domain/sdd_repository_reference.ts) · [`src/domain/sdd_visible_references.ts`](../src/domain/sdd_visible_references.ts) -- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts) · [`src/domain/__tests__/pre_branch_sdd_diff.test.ts`](../src/domain/__tests__/pre_branch_sdd_diff.test.ts) · [`src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts`](../src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts) · [`src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts`](../src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts) · [`src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts) · [`src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts`](../src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts) · [`src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts`](../src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts) · [`src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts`](../src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts) · [`src/tooling/__tests__/portable_specification_catalog.test.ts`](../src/tooling/__tests__/portable_specification_catalog.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/domain/__tests__/sdd_draft_consistency.test.ts`](../src/domain/__tests__/sdd_draft_consistency.test.ts) · [`src/domain/__tests__/sdd_derived_tables.test.ts`](../src/domain/__tests__/sdd_derived_tables.test.ts) · [`src/domain/__tests__/sdd_validation_diagnostic.test.ts`](../src/domain/__tests__/sdd_validation_diagnostic.test.ts) · [`src/application/policies/__tests__/sdd_preparation_status_policy.test.ts`](../src/application/policies/__tests__/sdd_preparation_status_policy.test.ts) · [`src/domain/__tests__/sdd_catalog_compatibility.test.ts`](../src/domain/__tests__/sdd_catalog_compatibility.test.ts) · [`src/testing/fixtures/sdd-native-catalog/catalog.json`](../src/testing/fixtures/sdd-native-catalog/catalog.json) · [`src/testing/fixtures/sdd-native-catalog/catalog.schema.json`](../src/testing/fixtures/sdd-native-catalog/catalog.schema.json) · [`src/testing/fixtures/sdd-native-catalog/provenance.json`](../src/testing/fixtures/sdd-native-catalog/provenance.json) · [`src/tooling/__tests__/specification_catalog_publication.test.ts`](../src/tooling/__tests__/specification_catalog_publication.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts) · [`src/tooling/__tests__/specification_catalog_markdown_links.test.ts`](../src/tooling/__tests__/specification_catalog_markdown_links.test.ts) · [`src/tooling/__tests__/specification_catalog_discovery.test.ts`](../src/tooling/__tests__/specification_catalog_discovery.test.ts) · [`src/domain/__tests__/sdd_trusted_catalog_failure.test.ts`](../src/domain/__tests__/sdd_trusted_catalog_failure.test.ts) · [`src/tooling/__tests__/specification_catalog_title_text.test.ts`](../src/tooling/__tests__/specification_catalog_title_text.test.ts) · [`src/domain/__tests__/sdd_repair_coordinates.test.ts`](../src/domain/__tests__/sdd_repair_coordinates.test.ts) · [`src/application/usecases/sdd/sdd_repair_feedback.test.ts`](../src/application/usecases/sdd/sdd_repair_feedback.test.ts) · [`src/application/policies/__tests__/agent_source_snapshot_policy.test.ts`](../src/application/policies/__tests__/agent_source_snapshot_policy.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_inventory.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_inventory.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_workspace.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_workspace.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_regression.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_regression.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_git.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_git.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_integrity.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_integrity.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_gate.integration.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_gate.integration.test.ts) · [`src/application/policies/__tests__/sdd_source_context_presentation.test.ts`](../src/application/policies/__tests__/sdd_source_context_presentation.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_admission.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_admission.test.ts) · [`src/domain/__tests__/sdd_repository_reference.test.ts`](../src/domain/__tests__/sdd_repository_reference.test.ts) +- Core code: [`src/domain/issue_workflow_runtime_policy.ts`](../src/domain/issue_workflow_runtime_policy.ts) · [`src/domain/copilot_lifecycle.ts`](../src/domain/copilot_lifecycle.ts) · [`src/data/model/execution.ts`](../src/data/model/execution.ts) · [`src/application/usecases/issue_workflow.ts`](../src/application/usecases/issue_workflow.ts) · [`src/application/usecases/steps/issue/prepare_branches_use_case.ts`](../src/application/usecases/steps/issue/prepare_branches_use_case.ts) · [`src/application/policies/setup_issue_workflow_policy.ts`](../src/application/policies/setup_issue_workflow_policy.ts) · [`src/application/policies/lifecycle_state_policy.ts`](../src/application/policies/lifecycle_state_policy.ts) · [`src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts`](../src/application/usecases/actions/synchronize_lifecycle_state_use_case.ts) · [`src/infrastructure/git_commit_adapter.ts`](../src/infrastructure/git_commit_adapter.ts) · [`scripts/validate-specification-catalog.cjs`](../scripts/validate-specification-catalog.cjs) · [`src/domain/issue_start_policy.ts`](../src/domain/issue_start_policy.ts) · [`src/domain/pre_branch_sdd.ts`](../src/domain/pre_branch_sdd.ts) · [`src/application/ports/pre_branch_sdd_ports.ts`](../src/application/ports/pre_branch_sdd_ports.ts) · [`src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts`](../src/application/usecases/sdd/pre_branch_sdd_gate_use_case.ts) · [`src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts`](../src/application/usecases/steps/issue/reconcile_branch_readiness_use_case.ts) · [`src/data/repository/branch/linked_branch_readiness_repository.ts`](../src/data/repository/branch/linked_branch_readiness_repository.ts) · [`src/infrastructure/pre_branch_sdd_workspace_adapter.ts`](../src/infrastructure/pre_branch_sdd_workspace_adapter.ts) · [`src/infrastructure/specification_catalog_validator.cjs`](../src/infrastructure/specification_catalog_validator.cjs) · [`src/actions/input_boolean_policy.ts`](../src/actions/input_boolean_policy.ts) · [`src/domain/issue_workflow_profile.ts`](../src/domain/issue_workflow_profile.ts) · [`src/application/usecases/read_copilot_status_workflow.ts`](../src/application/usecases/read_copilot_status_workflow.ts) · [`src/application/policies/status_command_policy.ts`](../src/application/policies/status_command_policy.ts) · [`src/application/policies/publication_message_catalog.ts`](../src/application/policies/publication_message_catalog.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts) · [`src/data/model/application_error.ts`](../src/data/model/application_error.ts) · [`src/application/policies/application_error_message_catalog.ts`](../src/application/policies/application_error_message_catalog.ts) · [`src/domain/agent_response_length_failure.ts`](../src/domain/agent_response_length_failure.ts) · [`src/data/repository/agent_json_schema_validator.ts`](../src/data/repository/agent_json_schema_validator.ts) · [`src/application/ports/sdd_evidence_read_ports.ts`](../src/application/ports/sdd_evidence_read_ports.ts) · [`src/application/policies/sdd_issue_identity_policy.ts`](../src/application/policies/sdd_issue_identity_policy.ts) · [`src/application/policies/sdd_query_card_policy.ts`](../src/application/policies/sdd_query_card_policy.ts) · [`src/application/policies/sdd_status_policy.ts`](../src/application/policies/sdd_status_policy.ts) · [`src/application/usecases/sdd/sdd_evidence_query_use_case.ts`](../src/application/usecases/sdd/sdd_evidence_query_use_case.ts) · [`src/application/usecases/comment_automation_context.ts`](../src/application/usecases/comment_automation_context.ts) · [`src/application/usecases/comment_automation_command_workflow.ts`](../src/application/usecases/comment_automation_command_workflow.ts) · [`src/application/usecases/comment_automation_contracts.ts`](../src/application/usecases/comment_automation_contracts.ts) · [`src/application/usecases/issue_comment_use_case.ts`](../src/application/usecases/issue_comment_use_case.ts) · [`src/application/policies/semantic_result_publication_policy.ts`](../src/application/policies/semantic_result_publication_policy.ts) · [`src/infrastructure/composition/sdd_evidence_composition_root.ts`](../src/infrastructure/composition/sdd_evidence_composition_root.ts) · [`src/infrastructure/composition/main_run_route_composition_root.ts`](../src/infrastructure/composition/main_run_route_composition_root.ts) · [`src/domain/pre_branch_sdd_diff.ts`](../src/domain/pre_branch_sdd_diff.ts) · [`src/application/policies/sdd_diff_message_catalog.ts`](../src/application/policies/sdd_diff_message_catalog.ts) · [`src/application/policies/sdd_diff_presentation_policy.ts`](../src/application/policies/sdd_diff_presentation_policy.ts) · [`src/application/ports/sdd_diff_read_ports.ts`](../src/application/ports/sdd_diff_read_ports.ts) · [`src/application/usecases/sdd/sdd_diff_query_use_case.ts`](../src/application/usecases/sdd/sdd_diff_query_use_case.ts) · [`src/data/repository/sdd/sdd_diff_link_repository.ts`](../src/data/repository/sdd/sdd_diff_link_repository.ts) · [`src/infrastructure/composition/sdd_diff_composition_root.ts`](../src/infrastructure/composition/sdd_diff_composition_root.ts) · [`src/application/usecases/pull_request_review_comment_use_case.ts`](../src/application/usecases/pull_request_review_comment_use_case.ts) · [`src/application/usecases/steps/common/reply_publication_workflow.ts`](../src/application/usecases/steps/common/reply_publication_workflow.ts) · [`src/application/usecases/steps/common/publish_resume_workflow.ts`](../src/application/usecases/steps/common/publish_resume_workflow.ts) · [`scripts/coverage-budgets.json`](../scripts/coverage-budgets.json) · [`src/architecture/github_publication_mutation_baseline.json`](../src/architecture/github_publication_mutation_baseline.json) · [`src/domain/specification_catalog_path_policy.cjs`](../src/domain/specification_catalog_path_policy.cjs) · [`src/application/policies/github_runtime_doctor_policy.ts`](../src/application/policies/github_runtime_doctor_policy.ts) · [`src/infrastructure/setup_workspace_adapter.ts`](../src/infrastructure/setup_workspace_adapter.ts) · [`src/application/usecases/setup/doctor_use_case.ts`](../src/application/usecases/setup/doctor_use_case.ts) · [`src/application/policies/setup_doctor_message_catalog.ts`](../src/application/policies/setup_doctor_message_catalog.ts) · [`src/domain/sdd_prerequisite_policy.cjs`](../src/domain/sdd_prerequisite_policy.cjs) · [`src/application/policies/sdd_analysis_status_policy.ts`](../src/application/policies/sdd_analysis_status_policy.ts) · [`src/domain/sdd_draft_consistency.ts`](../src/domain/sdd_draft_consistency.ts) · [`src/domain/sdd_validation_diagnostic.ts`](../src/domain/sdd_validation_diagnostic.ts) · [`src/application/errors/sdd_draft_preparation_failure.ts`](../src/application/errors/sdd_draft_preparation_failure.ts) · [`src/application/policies/sdd_preparation_message_catalog.ts`](../src/application/policies/sdd_preparation_message_catalog.ts) · [`src/application/policies/sdd_preparation_status_policy.ts`](../src/application/policies/sdd_preparation_status_policy.ts) · [`src/domain/sdd_catalog_compatibility.ts`](../src/domain/sdd_catalog_compatibility.ts) · [`src/infrastructure/specification_catalog_publication.cjs`](../src/infrastructure/specification_catalog_publication.cjs) · [`jest.config.js`](../jest.config.js) · [`src/domain/sdd_trusted_catalog_failure.ts`](../src/domain/sdd_trusted_catalog_failure.ts) · [`src/domain/agent_source_snapshot.ts`](../src/domain/agent_source_snapshot.ts) · [`src/application/policies/agent_source_snapshot_policy.ts`](../src/application/policies/agent_source_snapshot_policy.ts) · [`src/infrastructure/agents/agent_source_snapshot_git.ts`](../src/infrastructure/agents/agent_source_snapshot_git.ts) · [`src/infrastructure/agents/agent_source_snapshot_workspace.ts`](../src/infrastructure/agents/agent_source_snapshot_workspace.ts) · [`src/infrastructure/agents/agent_source_snapshot_integrity.ts`](../src/infrastructure/agents/agent_source_snapshot_integrity.ts) · [`src/domain/sdd_repository_reference.ts`](../src/domain/sdd_repository_reference.ts) · [`src/domain/sdd_visible_references.ts`](../src/domain/sdd_visible_references.ts) · [`src/domain/sdd_case_reference.ts`](../src/domain/sdd_case_reference.ts) +- Tests: [`src/domain/__tests__/issue_workflow_runtime_policy.test.ts`](../src/domain/__tests__/issue_workflow_runtime_policy.test.ts) · [`src/data/model/__tests__/execution.test.ts`](../src/data/model/__tests__/execution.test.ts) · [`src/application/usecases/__tests__/issue_use_case.test.ts`](../src/application/usecases/__tests__/issue_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/prepare_branches_use_case.test.ts) · [`src/application/policies/__tests__/lifecycle_state_policy.test.ts`](../src/application/policies/__tests__/lifecycle_state_policy.test.ts) · [`src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts`](../src/application/usecases/actions/__tests__/synchronize_lifecycle_state_use_case.test.ts) · [`src/tooling/__tests__/validate_specification_catalog.test.ts`](../src/tooling/__tests__/validate_specification_catalog.test.ts) · [`src/domain/__tests__/issue_start_policy.test.ts`](../src/domain/__tests__/issue_start_policy.test.ts) · [`src/domain/__tests__/pre_branch_sdd.test.ts`](../src/domain/__tests__/pre_branch_sdd.test.ts) · [`src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/pre_branch_sdd_gate_use_case.test.ts) · [`src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts`](../src/application/usecases/steps/issue/__tests__/reconcile_branch_readiness_use_case.test.ts) · [`src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts`](../src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts) · [`src/architecture/__tests__/github_publication_boundaries.test.ts`](../src/architecture/__tests__/github_publication_boundaries.test.ts) · [`src/actions/__tests__/input_boolean_policy.test.ts`](../src/actions/__tests__/input_boolean_policy.test.ts) · [`src/domain/__tests__/issue_workflow_profile.test.ts`](../src/domain/__tests__/issue_workflow_profile.test.ts) · [`src/application/policies/__tests__/status_command_policy.test.ts`](../src/application/policies/__tests__/status_command_policy.test.ts) · [`src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts`](../src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts) · [`src/application/policies/__tests__/application_error_presentation_policy.test.ts`](../src/application/policies/__tests__/application_error_presentation_policy.test.ts) · [`src/data/repository/__tests__/agent_json_schema_validator.test.ts`](../src/data/repository/__tests__/agent_json_schema_validator.test.ts) · [`src/application/policies/__tests__/sdd_issue_identity_policy.test.ts`](../src/application/policies/__tests__/sdd_issue_identity_policy.test.ts) · [`src/application/policies/__tests__/sdd_query_card_policy.test.ts`](../src/application/policies/__tests__/sdd_query_card_policy.test.ts) · [`src/application/policies/__tests__/sdd_status_policy.test.ts`](../src/application/policies/__tests__/sdd_status_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_evidence_query_use_case.test.ts) · [`src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts`](../src/data/repository/branch/__tests__/linked_branch_readiness_repository.test.ts) · [`src/application/usecases/__tests__/read_copilot_status_workflow.test.ts`](../src/application/usecases/__tests__/read_copilot_status_workflow.test.ts) · [`src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts`](../src/infrastructure/composition/__tests__/main_run_route_composition_root.test.ts) · [`src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_evidence_composition_root.test.ts) · [`src/domain/__tests__/pre_branch_sdd_diff.test.ts`](../src/domain/__tests__/pre_branch_sdd_diff.test.ts) · [`src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts`](../src/application/policies/__tests__/sdd_diff_presentation_policy.test.ts) · [`src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts`](../src/application/usecases/sdd/__tests__/sdd_diff_query_use_case.test.ts) · [`src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts`](../src/data/repository/sdd/__tests__/sdd_diff_link_repository.test.ts) · [`src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts`](../src/infrastructure/composition/__tests__/sdd_diff_composition_root.test.ts) · [`src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts`](../src/application/usecases/__tests__/sdd_diff_comment_interaction.test.ts) · [`src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts`](../src/application/usecases/steps/common/__tests__/sdd_diff_reply_publication.test.ts) · [`src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts`](../src/application/usecases/__tests__/sdd_status_workflow_integration.test.ts) · [`src/tooling/__tests__/portable_specification_catalog.test.ts`](../src/tooling/__tests__/portable_specification_catalog.test.ts) · [`src/application/policies/__tests__/github_runtime_doctor_policy.test.ts`](../src/application/policies/__tests__/github_runtime_doctor_policy.test.ts) · [`src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts`](../src/infrastructure/__tests__/setup_sdd_prerequisites.test.ts) · [`src/application/usecases/setup/__tests__/doctor_use_case.test.ts`](../src/application/usecases/setup/__tests__/doctor_use_case.test.ts) · [`src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts`](../src/infrastructure/__tests__/branch_sync_sdd_catalog.test.ts) · [`src/cli/__tests__/setup_presenters.test.ts`](../src/cli/__tests__/setup_presenters.test.ts) · [`src/domain/__tests__/sdd_draft_consistency.test.ts`](../src/domain/__tests__/sdd_draft_consistency.test.ts) · [`src/domain/__tests__/sdd_derived_tables.test.ts`](../src/domain/__tests__/sdd_derived_tables.test.ts) · [`src/domain/__tests__/sdd_validation_diagnostic.test.ts`](../src/domain/__tests__/sdd_validation_diagnostic.test.ts) · [`src/application/policies/__tests__/sdd_preparation_status_policy.test.ts`](../src/application/policies/__tests__/sdd_preparation_status_policy.test.ts) · [`src/domain/__tests__/sdd_catalog_compatibility.test.ts`](../src/domain/__tests__/sdd_catalog_compatibility.test.ts) · [`src/testing/fixtures/sdd-native-catalog/catalog.json`](../src/testing/fixtures/sdd-native-catalog/catalog.json) · [`src/testing/fixtures/sdd-native-catalog/catalog.schema.json`](../src/testing/fixtures/sdd-native-catalog/catalog.schema.json) · [`src/testing/fixtures/sdd-native-catalog/provenance.json`](../src/testing/fixtures/sdd-native-catalog/provenance.json) · [`src/tooling/__tests__/specification_catalog_publication.test.ts`](../src/tooling/__tests__/specification_catalog_publication.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog.test.ts) · [`src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts`](../src/tooling/__tests__/proposed_sdd_catalog_entrypoint.test.ts) · [`src/tooling/__tests__/specification_catalog_markdown_links.test.ts`](../src/tooling/__tests__/specification_catalog_markdown_links.test.ts) · [`src/tooling/__tests__/specification_catalog_discovery.test.ts`](../src/tooling/__tests__/specification_catalog_discovery.test.ts) · [`src/domain/__tests__/sdd_trusted_catalog_failure.test.ts`](../src/domain/__tests__/sdd_trusted_catalog_failure.test.ts) · [`src/tooling/__tests__/specification_catalog_title_text.test.ts`](../src/tooling/__tests__/specification_catalog_title_text.test.ts) · [`src/domain/__tests__/sdd_repair_coordinates.test.ts`](../src/domain/__tests__/sdd_repair_coordinates.test.ts) · [`src/application/usecases/sdd/sdd_repair_feedback.test.ts`](../src/application/usecases/sdd/sdd_repair_feedback.test.ts) · [`src/application/policies/__tests__/agent_source_snapshot_policy.test.ts`](../src/application/policies/__tests__/agent_source_snapshot_policy.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_inventory.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_inventory.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_workspace.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_workspace.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_regression.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_regression.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_git.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_git.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_integrity.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_integrity.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_gate.integration.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_gate.integration.test.ts) · [`src/application/policies/__tests__/sdd_source_context_presentation.test.ts`](../src/application/policies/__tests__/sdd_source_context_presentation.test.ts) · [`src/infrastructure/agents/__tests__/agent_source_snapshot_admission.test.ts`](../src/infrastructure/agents/__tests__/agent_source_snapshot_admission.test.ts) · [`src/domain/__tests__/sdd_repository_reference.test.ts`](../src/domain/__tests__/sdd_repository_reference.test.ts) · [`src/domain/__tests__/sdd_case_reference.test.ts`](../src/domain/__tests__/sdd_case_reference.test.ts) - User documentation: [`docs/issues/branch-management.mdx`](../docs/issues/branch-management.mdx) · [`docs/issues/workflow-setup.mdx`](../docs/issues/workflow-setup.mdx) · [`docs/issues/comment-commands.mdx`](../docs/issues/comment-commands.mdx) · [`docs/development/specifications.mdx`](../docs/development/specifications.mdx) · [`docs/issues/pre-branch-sdds.mdx`](../docs/issues/pre-branch-sdds.mdx) · [`docs/issues/index.mdx`](../docs/issues/index.mdx) · [`docs/issues/labels-and-branch-types.mdx`](../docs/issues/labels-and-branch-types.mdx) · [`docs/issues/configuration.mdx`](../docs/issues/configuration.mdx) · [`docs/configuration.mdx`](../docs/configuration.mdx) · [`docs/single-actions/workflow-and-cli.mdx`](../docs/single-actions/workflow-and-cli.mdx) · [`docs/security-operations/operations/error-reference.mdx`](../docs/security-operations/operations/error-reference.mdx) · [`specs/README.md`](./README.md) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle diff --git a/specs/catalog.json b/specs/catalog.json index bf264bf7c..1b909f28b 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1746,7 +1746,8 @@ "specs/pre-branch-sdd-diff.md", "specs/pre-branch-sdd-status-readiness.md", "specs/pre-branch-sdd-source-context.md", - "specs/sdd-reference-classification.md" + "specs/sdd-reference-classification.md", + "specs/sdd-case-reference-classification.md" ], "workflows": [ ".github/workflows/copilot_issue.yml", @@ -1834,7 +1835,8 @@ "src/infrastructure/agents/agent_source_snapshot_workspace.ts", "src/infrastructure/agents/agent_source_snapshot_integrity.ts", "src/domain/sdd_repository_reference.ts", - "src/domain/sdd_visible_references.ts" + "src/domain/sdd_visible_references.ts", + "src/domain/sdd_case_reference.ts" ], "tests": [ "src/domain/__tests__/issue_workflow_runtime_policy.test.ts", @@ -1904,7 +1906,8 @@ "src/infrastructure/agents/__tests__/agent_source_snapshot_gate.integration.test.ts", "src/application/policies/__tests__/sdd_source_context_presentation.test.ts", "src/infrastructure/agents/__tests__/agent_source_snapshot_admission.test.ts", - "src/domain/__tests__/sdd_repository_reference.test.ts" + "src/domain/__tests__/sdd_repository_reference.test.ts", + "src/domain/__tests__/sdd_case_reference.test.ts" ], "documentation": [ "docs/issues/branch-management.mdx", diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index ae8e33cc4..9d4871f4c 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -1156,6 +1156,16 @@ messages; tests do not claim a live consumer recovery. ### Generated draft consistency (2026-10-21, #653) +The [case-reference classification companion](./sdd-case-reference-classification.md) +refines shorthand detection for #845: ordinary technical operands retain their +original wording, while typed case fields, explicit references and case-ID list +continuations still require full identities. Its existing parser projection +preserves inline labels and table roles without treating destination URIs as +case declarations. Canonical tables, budgets, reciprocal links, qualified +references, conservative legacy owner reservations and publication gates below +remain independent and unchanged. + + Issue [#653](https://github.com/vypdev/copilot/issues/653) documented a published companion that reused a baseline case ID for the opposite authentication behavior, had offsetting area-allocation errors and one-sided requirement links, diff --git a/specs/sdd-case-reference-classification.md b/specs/sdd-case-reference-classification.md new file mode 100644 index 000000000..070b37779 --- /dev/null +++ b/specs/sdd-case-reference-classification.md @@ -0,0 +1,393 @@ +# SDD case-reference classification + +- Status: Implemented candidate; remote merge acceptance pending +- Date: 2026-11-03 +- Catalog capability ID: issue-start-and-sdd-readiness +- Last verified: 2026-11-03, local source/tests/docs review; remote gates remain independent +- Owners: Copilot maintainers +- Scope: Distinguish ordinary technical designators from shorthand acceptance-case references in proposed SDDs. +- Related issue: [#845](https://github.com/vypdev/copilot/issues/845) +- Required review gates: product UX, architecture, tests, docs, security, + complete automated review and 100% modified-line coverage +- Open decisions blocking readiness: none; execution and remote acceptance remain pending. + +## 1. Executive summary + +A technical designation in ordinary prose must not become an acceptance-case +reference merely because its characters resemble an abbreviated ID. Writers +can retain Unicode/control and component terminology. Actual case identities, +references in case roles, budgets, mappings and owner reservations remain +publication gates. Acceptance of an SDD still does not establish product runtime +acceptance. + +```text +visible draft -> classify reference roles -> validate unchanged literal inventory + -> existing bounded repair or normal publication -> independent runtime gates +``` + +## 2. Problem, current behavior, and evidence + +### 2.1 Problem and current behavior + +At approved SDK source `771dc505` (merged as `16bf97ff`), the consistency policy +checks the visible source with an unqualified-token regex. It rejects the +reported control designation, a Unicode designation and connector/component +designators with `case-identity` / `case-shorthand`, despite unchanged literal +inventory and reciprocal links. + +### 2.2 Verified evidence and limits + +The original issue and its original comment were read before admission; the +Action plan independently requires a narrower rule and retained real-reference +checks. The [controlled before/after proof](../quality/sdd/evidence/issue-845.json) +invokes the actual current domain functions from exact approved source. Its independently authored 48-row fixture is valid before +adding each technical fragment. All three intended-positive fragments fail; +all six negative controls remain rejected. Case rows and reverse mappings are +byte-identical across the wording variants. + +The [referenced consumer document](https://github.com/vypdev/domus/blob/ca55cdb56779573a0dd45b47b9546b74b4ffcc74/specs/household-entry-interface.md) +was read without cloning or modification; its Git blob is +`ff87750ca63faed8a650718900a52e9ce38e648d` and it contains 48 literal case rows. +Its historical owner/inventory checkpoint was not reconstructed. The controlled +fixture is not that historical candidate, consumer implementation execution, +a newly failed hosted run, a live provider response or 48 executed SDK tests. + +### 2.3 Retrospective classification + +Not applicable to this prospective correction. Existing lexical behavior is +verified; no unsupported historical rationale is inferred. + +## 3. Actors, surfaces, and terminology + +| Actor | Goal | Entry point | Visible surfaces | +|---|---|---|---| +| SDD author | Describe technical behavior and real acceptance precisely | Existing issue/SDD workflow | Draft, repair/status card, PR | +| Maintainer | Review a coherent inventory without unrelated prose rewrites | Existing review/CLI route | Diagnostics, docs, job summary | +| Trusted validator | Enforce literal identity and reference roles | Existing pure validation call | Closed reason/count facts | + +A **case role** is an explicit case-ID/reference field or column, an explicit +case/reference cue, or a contiguous continuation of a case-ID reference list. +A **technical operand** is ordinary text that has not acquired such a role. +Recognition does not credit an operand as a case or count it toward acceptance. + +## 4. Goals, non-goals, and fixed invariants + +| Requirement | Behavior | +|---|---| +| SDD-CASE-R1 | Accept ordinary technical designators without rewriting source, literal case definitions or reciprocal links. | +| SDD-CASE-R2 | Classify shorthand only in case-reference roles, explicit reference cues and contiguous case-ID reference lists; retain rejection there. | +| SDD-CASE-R3 | Keep canonical ID fields, unknown qualified references, owner reservations, exact budgets and bidirectional mappings strict. | +| SDD-CASE-R4 | Reuse the existing visibility projection and preserve Unicode text, fences, comments, line endings and publication bytes. | +| SDD-CASE-R5 | Keep the classifier pure and source-bounded; preserve existing immutable-source, repair, publication and cleanup authority. | +| SDD-CASE-R6 | Align generation guidance, localized recovery, SDD/catalog/docs and executable acceptance without treating document validity as runtime execution. | + +Non-goals: a general natural-language meaning engine, new case IDs or row +normalization, changes to conservative retained-owner alias/range reservation, +relaxing qualified-reference or repository-path checks, new settings/locales, +permission changes, more provider calls or expanded repair budgets. Required +canonical fields remain language independent. Prose cues supplement those +fields; they cannot certify arbitrary-language requirement completeness. + +## 5. Current versus proposed product journey + +| Stage | Current | Proposed | Effect | +|---|---|---|---| +| Author writes technical prose | Global token match rejects it | Classify its role | Original terminology retained | +| Author references an acceptance case | Global match rejects shorthand | Case role rejects shorthand | Full identity still required | +| Inventory/mapping validation | Strict canonical tables | Same strict tables | No extra credit or aliases | +| Failure | Existing bounded repair/status | Same route and safe category | No new retry or authority | + +```mermaid +flowchart LR + A[Visible draft] --> B[Reference roles] + B --> C[Literal identity and mapping checks] + C --> D{Consistent?} + D -->|Yes| E[Normal verified publication] + D -->|No| F[Existing bounded repair] + F --> G[Revalidate or non-ready status] +``` + +Text equivalent: classify the visible draft, validate the same inventory, then +publish normally or use the existing repair/non-ready path. + +## 6. Functional behavior and state model + +1. The owner policy continues to exclude fenced examples and HTML comments; + the classifier receives this existing visibility projection. +2. Canonical case-ID and reciprocal-link fields keep their full-ID schemas. + Recognize explicit case-role columns in other ordinary Markdown tables too; + reset role context between cells so a scenario does not inherit its ID cell. +3. In prose, recognize explicit case/reference labels and cues, formatting around + their values, and uninterrupted reference-list/range continuations after a + case ID. Technical words or an unrelated clause end that continuation. +4. Plain technical text is not a case role. There is no special allowlist for a + particular control designation, connector number or case-number length. +5. Token boundaries respect Unicode identifier neighbors. Detection must not + rewrite or normalize IDs, technical values or source bytes. +6. Keep unknown qualified-reference checks and retained-owner reservations in + the existing policy. No count, namespace or reciprocal link is inferred from + accepting a technical operand. + +| State | Transition | Result | +|---|---|---| +| Technical prose | No case-reference role | Continue the full existing validator | +| Case role with shorthand | Invalid identity/reference | Existing safe consistency failure | +| Invalid budget/mapping/owner/path | Independent rejection | Existing specific failure | +| Valid document | Existing freshness/write checks | Publication, without runtime claims | + +Ambiguous adjacent IDs in an established case-reference list remain references. +This is a bounded syntactic role contract, not proof of natural-language meaning. + +## 7. User-facing configuration + +| Setting/boundary | Contract | +|---|---| +| Public configuration | No new option, override or escape hatch | +| Document size | Existing 70,000-character draft bound remains unchanged | +| Repair | Existing shared two-attempt drafting ceiling remains unchanged | +| Ownership | Same immutable owner/catalog and managed branch authority | +| Roles | Fixed syntax rules tested as code, not user-supplied regex or keywords | + +No migration, credential, consent or operator reset is required. + +## 8. Clean Architecture design + +| Layer | Responsibility | Forbidden dependencies | +|---|---|---| +| New domain classifier | Count shorthand in visible case-reference roles | IO, provider SDK, process, locale service, publication | +| Existing consistency policy | Tables, IDs, reservations, budgets, mappings and paths | New external authority or source rewriting | +| Existing application | Owner reads, draft/repair and final validation | Treat accepted prose as runtime proof | +| Existing workspace | Immutable Git validation, exact composition and publication | Skip checks or touch an unrelated checkout | + +The classifier returns only a count. Reuse the existing CommonMark inline +projection for labels/code so formatting and link destinations do not split +reference lists or manufacture case operands; required ID fields remain literal. Diagnostics continue through existing +closed reason/phase/fact types, without rejected prose. Use a linear token walk +with disjoint intervening gaps and line/table role boundaries; avoid repeatedly +rescanning a growing prefix or silently truncating reference context. Tests +cover large technical text and long spacing. No new dependency is needed. + +## 9. UI/UX and content contract + +Accepted technical content proceeds with its original wording. A genuine case +reference failure keeps the existing localized status/repair surface. + +| State | Representative content | Action | +|---|---|---| +| Valid technical prose | Normal consistent-draft status | Continue existing publication checks | +| Genuine case violation | “Case identities or references do not match the literal inventory.” | Correct the real reference and retry normally | +| Repair exhausted | Existing non-ready status and retained answers/SDDs | Review closed facts; no draft is published | +| Published | Existing draft publication status | Runtime/security acceptance remains independent | + +Commands, links, categories and counts retain their canonical identities. +English/Spanish recovery uses the existing impact/action/retained-state order; +no raw Markdown, private IDs or paths enter a repair instruction or public +error. Textual headings remain mobile/screen-reader compatible. No new browser +layout or human VoiceOver journey is introduced for this domain correction. + +## 10. Failure, recovery, and cleanup + +Malformed required tables, identity aliases, unknown qualified references, +owner collisions, wrong allocations and asymmetric links still fail their +specific existing category. A classifier failure cannot authorize partial +publication. Existing exception/finally cleanup and scratch-worktree removal +remain mandatory. Keep meaningful before/after inputs, source identities, +assertions, scope limitations and diagnostics; retire redundant generated +coverage HTML/guards after verification, preserving the current SDK rollback. + +## 11. Security, permissions, and privacy + +Input Markdown and repository context are data, never instructions to grant +permissions or change validation. Case fields cannot be relabeled as technical +prose to establish an acceptance count: the canonical inventory and reciprocal +links independently enforce full IDs. No token, provider request, network read, +source export, user-setting change or new permission belongs to the classifier. +Retain the unchanged visibility, freshness, path and publication boundaries. + +## 12. Observability and operational UX + +Use existing correlation, safe category/phase/count and status-card identity. +Do not log technical source merely to explain classification. Local/source, +controlled fixture, actual native execution, complete model review and official +coverage are distinct evidence. A document containing 48 rows is not evidence +of 48 executed tests or runtime acceptance. Replays do not add notifications. + +## 13. Compatibility, migration, rollout, and rollback + +Compatible typed fields and public recovery IDs remain unchanged. Newly +accepted technical prose needs no content migration. Existing fully qualified +references remain valid; real shorthand still fails in reference roles. Legacy +owner reservations retain their conservative behavior and fixed range bound. +Adopt the corrected SDK through the normal managed runtime route. Roll back +source, prompt guidance and bundles together; never rewrite published SDDs or +undo an already published Git effect as housekeeping. + +## 14. Testing strategy and numeric budget + +Minimum **40 distinct new cases**, counted once; existing cases remain +mandatory and are not recounted. These SDK regressions are distinct from the +48 hypothetical rows used as input in the controlled reproduction. + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Pure | 24 | Technical operands, reference roles, tables, ranges, visibility and bounded deterministic scanning | +| Consistency | 10 | Literal identities, mappings, allocations, reservations and trusted revalidation | +| Publication | 4 | Real local Git acceptance/rejection, retained bytes and replay/cleanup | +| Recovery | 2 | Safe English/Spanish real-case failure content | +| Total | 40 | No repeated execution or fixture row is counted twice | + +| Case ID | Area | Requirements | Scenario | +|---|---|---|---| +| sdd-case-ref-C01 | Pure | SDD-CASE-R1 | Ordinary DEL/C1 prose is accepted without rewriting its bytes. | +| sdd-case-ref-C02 | Pure | SDD-CASE-R1 | Unicode C1 designation is accepted in ordinary prose. | +| sdd-case-ref-C03 | Pure | SDD-CASE-R1 | Connector/component designators remain technical operands. | +| sdd-case-ref-C04 | Pure | SDD-CASE-R1 | Inline code technical identifiers remain ordinary text outside case roles. | +| sdd-case-ref-C05 | Pure | SDD-CASE-R1 | Technical identifiers in non-case table columns are accepted. | +| sdd-case-ref-C06 | Pure | SDD-CASE-R1 | A canonical scenario may describe control characters without declaring a shorthand case. | +| sdd-case-ref-C07 | Pure | SDD-CASE-R1 | Requirement behavior may name a technical designator. | +| sdd-case-ref-C08 | Pure | SDD-CASE-R1 | A protocol/URI designator does not acquire case-reference authority. | +| sdd-case-ref-C09 | Pure | SDD-CASE-R2 | An explicit English case reference using a short ID is rejected. | +| sdd-case-ref-C10 | Pure | SDD-CASE-R2 | An explicit Spanish case reference using a short ID is rejected. | +| sdd-case-ref-C11 | Pure | SDD-CASE-R2 | A case-ID label enforces qualified references despite formatting. | +| sdd-case-ref-C12 | Pure | SDD-CASE-R2 | A case-role table column rejects shorthand. | +| sdd-case-ref-C13 | Pure | SDD-CASE-R2 | An adjacent reference-list continuation after a qualified ID rejects shorthand. | +| sdd-case-ref-C14 | Pure | SDD-CASE-R2 | An abbreviated range in an explicit case-reference context is rejected. | +| sdd-case-ref-C15 | Pure | SDD-CASE-R2 | An explicit see/reference cue cannot introduce an unqualified case. | +| sdd-case-ref-C16 | Pure | SDD-CASE-R2 | Hyphenated range shorthand remains a case-reference failure. | +| sdd-case-ref-C17 | Pure | SDD-CASE-R2 | Wrapped labels and long spacing cannot evade reference classification. | +| sdd-case-ref-C18 | Pure | SDD-CASE-R2 | Markdown emphasis and inline-code decoration do not hide reference intent. | +| sdd-case-ref-C19 | Pure | SDD-CASE-R4 | Unicode identifier boundaries do not manufacture standalone short tokens. | +| sdd-case-ref-C20 | Pure | SDD-CASE-R4 | CRLF and LF yield equivalent role decisions. | +| sdd-case-ref-C21 | Pure | SDD-CASE-R4 | Hidden comments remain excluded by the owner visibility projection. | +| sdd-case-ref-C22 | Pure | SDD-CASE-R4 | Fenced negative examples remain excluded by the owner visibility projection. | +| sdd-case-ref-C23 | Pure | SDD-CASE-R5 | Repeated calls are deterministic and do not retain regex cursor state. | +| sdd-case-ref-C24 | Pure | SDD-CASE-R5 | Large technical text and long reference gaps retain the same decisions without silent limits. | +| sdd-case-ref-C25 | Consistency | SDD-CASE-R1 | The reported fragment passes the actual consistency path with 48 literal fixture rows. | +| sdd-case-ref-C26 | Consistency | SDD-CASE-R3 | Technical wording variants retain byte-identical case rows and reverse mappings. | +| sdd-case-ref-C27 | Consistency | SDD-CASE-R3 | A shorthand inventory definition remains invalid. | +| sdd-case-ref-C28 | Consistency | SDD-CASE-R3 | A reserved owner-case collision remains invalid. | +| sdd-case-ref-C29 | Consistency | SDD-CASE-R3 | An undefined fully qualified reference remains invalid. | +| sdd-case-ref-C30 | Consistency | SDD-CASE-R3 | A shorthand reciprocal link remains invalid. | +| sdd-case-ref-C31 | Consistency | SDD-CASE-R3 | A one-sided or missing reverse link remains invalid. | +| sdd-case-ref-C32 | Consistency | SDD-CASE-R3 | A wrong allocation or total remains invalid. | +| sdd-case-ref-C33 | Consistency | SDD-CASE-R3 | Legacy owner aliases and bounded ranges retain their reservations. | +| sdd-case-ref-C34 | Consistency | SDD-CASE-R6 | Trusted derived tables and accepted technical prose survive independent revalidation. | +| sdd-case-ref-C35 | Publication | SDD-CASE-R5 | Real local Git publication accepts technical prose and preserves retained owner bytes. | +| sdd-case-ref-C36 | Publication | SDD-CASE-R2 | Real local Git publication blocks a genuine shorthand reference before push. | +| sdd-case-ref-C37 | Publication | SDD-CASE-R3 | Real local Git publication blocks an invalid identity field before push. | +| sdd-case-ref-C38 | Publication | SDD-CASE-R4 | Publication/replay keeps visibility, byte identity and cleanup boundaries. | +| sdd-case-ref-C39 | Recovery | SDD-CASE-R6 | English recovery preserves closed safe diagnostics and retained state for a real case violation. | +| sdd-case-ref-C40 | Recovery | SDD-CASE-R6 | Spanish recovery preserves closed safe diagnostics and retained state for a real case violation. | + +Require 100% new pure-module lines/statements/functions/branches, unchanged +repository budgets and official Codecov 100% modified lines with zero misses +or partials against the correct current base. Run full Jest and extension +regressions, all applicable native-platform checks and complete finding-free +review. Use deterministic text/fakes and local Git; no live provider, credential +grant or consumer execution is part of these tests. + +### Local acceptance evidence (2026-11-03) + +The final source passes **879 Jest suites / 17,148 tests**, with 35 existing +skips. It adds **77 distinct regressions**: 56 classifier cases, 12 consistency +cases, 4 real local Git cases, 2 localized recovery cases and 3 bounded-draft +cases. The 48 hypothetical fixture rows are not added to that count. +The new pure module measures 100% lines/statements/functions/branches +(59 lines / 39 branches), enforced by an exhaustive budget. Full-suite LCOV +covers **83/83 modified coverable lines**, zero misses or partials against +`16bf97ff`. Extension tests remain 267/267 with every metric at 100%. +These local measurements are not official Codecov, native remote acceptance +or consumer execution; the PR must separately verify its current head/base. + +## 15. Documentation and discoverability + +| Path | Change | +|---|---| +| specs/sdd-case-reference-classification.md | create | +| specs/pre-branch-sdd-gate.md | update | +| specs/catalog.json | update | +| specs/CATALOG.md | update | +| docs/issues/pre-branch-sdds.mdx | update | +| docs/development/specifications.mdx | update | +| src/domain/sdd_case_reference.ts | create | +| src/domain/sdd_visible_references.ts | update | +| src/testing/sdd_consistency_fixture.ts | update | +| src/application/policies/__tests__/sdd_preparation_status_policy.test.ts | update | +| quality/sdd/evidence/issue-845.json | create | +| src/domain/__tests__/sdd_case_reference.test.ts | create | +| src/domain/sdd_draft_consistency.ts | update | +| src/domain/__tests__/sdd_draft_consistency.test.ts | update | +| src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts | update | +| src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts | update | +| scripts/coverage-budgets.json | update | + +Explain accepted technical prose, strict case-role fields/references, unchanged +legacy reservations and the distinction between publication and runtime tests. +Update generation guidance to request full case IDs without instructing a writer +to rename unrelated technical terms. Catalog links and docs must validate. + +## 16. Acceptance scenarios + +1. Given 48 distinct literal fixture rows and exact reciprocal links, adding + the reported technical fragment passes format/consistency; no row or link + changes. The original Unicode terminology remains in the accepted bytes. +2. Connector/component and other ordinary technical operands pass outside case + roles, including scenario/behavior and non-case table cells. +3. Explicit shorthand references, abbreviated ranges and shorthand case fields + fail before publication. Unknown qualified references, collisions, wrong + budgets and bad mappings retain their previous failures. +4. Hidden/fenced examples and line-ending/Unicode boundaries retain visibility + semantics. Repeated/large inputs do not depend on shared regex state or an + undocumented dropped prefix. +5. Real local Git publication retains owner bytes and cleanup; invalid case + content never pushes. Existing replay and final validation remain required. +6. Genuine failures use safe existing English/Spanish recovery; acceptance of + technical prose does not add a case, execution claim, setting or permission. + +## 17. Requirements traceability + +| Requirement | Cases | +|---|---| +| SDD-CASE-R1 | sdd-case-ref-C01, sdd-case-ref-C02, sdd-case-ref-C03, sdd-case-ref-C04, sdd-case-ref-C05, sdd-case-ref-C06, sdd-case-ref-C07, sdd-case-ref-C08, sdd-case-ref-C25 | +| SDD-CASE-R2 | sdd-case-ref-C09, sdd-case-ref-C10, sdd-case-ref-C11, sdd-case-ref-C12, sdd-case-ref-C13, sdd-case-ref-C14, sdd-case-ref-C15, sdd-case-ref-C16, sdd-case-ref-C17, sdd-case-ref-C18, sdd-case-ref-C36 | +| SDD-CASE-R3 | sdd-case-ref-C26, sdd-case-ref-C27, sdd-case-ref-C28, sdd-case-ref-C29, sdd-case-ref-C30, sdd-case-ref-C31, sdd-case-ref-C32, sdd-case-ref-C33, sdd-case-ref-C37 | +| SDD-CASE-R4 | sdd-case-ref-C19, sdd-case-ref-C20, sdd-case-ref-C21, sdd-case-ref-C22, sdd-case-ref-C38 | +| SDD-CASE-R5 | sdd-case-ref-C23, sdd-case-ref-C24, sdd-case-ref-C35 | +| SDD-CASE-R6 | sdd-case-ref-C34, sdd-case-ref-C39, sdd-case-ref-C40 | + +## 18. Implementation sequence + +Freeze the exact current-source failure and negative controls; define/test +case-reference roles; integrate the pure counter without changing the remaining +policy; add full consistency, publication/repair and recovery regressions; +update prompt/docs/catalog and exhaustive budget; build bundles and validate +package/coverage/Graphify; publish on the exact Action-created branch; audit the +current head/base, complete review, all native gates and official Codecov before +ordinary merge. Adopt one approved SDK baseline and retire replaced artifacts. + +## 19. Definition of Done + +- [ ] SDD-CASE-R1–R6 and the six acceptance scenarios have executable evidence. +- [ ] At least 40 distinct new cases satisfy the distribution and preserve old controls. +- [ ] New pure coverage and all modified-line coverage are 100%, without exclusions. +- [ ] Source, prompt guidance, docs, catalog, bundles and Graphify agree. +- [ ] Exact-head CI/native platforms and complete review pass without findings/threads. +- [ ] Official Codecov verifies the correct head/base with zero misses/partials; + normal merge and native issue closure are verified independently. + +## 20. References and decisions + +- [Owning gate contract](./pre-branch-sdd-gate.md#generated-draft-consistency-2026-10-21-653). +- [Repository-reference classification](./sdd-reference-classification.md) remains + an independent path/notation guard; it is not a case-ID exception list. +- Decision: classify reference roles, not a whitelist for one reported operand. +- Decision: preserve conservative immutable owner reservations and qualified + namespace checks; acceptance of ordinary prose cannot credit a new case. +- Limitation: syntactic roles do not prove arbitrary natural-language meaning, + runtime execution, historical input identity or completeness of every goal. +- [GFM table syntax](https://github.github.com/gfm/#tables-extension-) informs + pipe escapes, delimiter/header alignment, variable body-cell counts and + blank/new-block boundaries. Reuse the existing CommonMark block parser rather + than inventing independent block-boundary rules. This projection does not + broaden the canonical required-table schema. +- No unresolved scope decision blocks implementation. diff --git a/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts b/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts index 4707cac35..4ab128d06 100644 --- a/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts +++ b/src/application/policies/__tests__/sdd_preparation_status_policy.test.ts @@ -9,6 +9,7 @@ import { validateCatalogDefinition } from '../../../domain/message_catalog'; import { ResolveMessageCatalogUseCase } from '../../usecases/localization/resolve_message_catalog_use_case'; import { validateSddDraftConsistency } from '../../../domain/sdd_draft_consistency'; import { SddDraftFormatFailure } from '../../../domain/pre_branch_sdd'; +import { sddCaseReferenceFixture } from '../../../testing/sdd_consistency_fixture'; import { sddReferenceFixture } from '../../../testing/sdd_reference_fixture'; const configuration = { provider: 'codex' as const, model: 'model' }; @@ -245,3 +246,21 @@ describe('missing trusted catalog presentation (#808)', () => { expect(body).toContain('a'.repeat(40)); }); }); + + +describe('Real shorthand recovery remains separate from technical prose', () => { + it.each([{id:'sdd-case-ref-C39',locale:'en-US'},{id:'sdd-case-ref-C40',locale:'es-ES'}])('$id renders safe $locale recovery for a genuine reference violation', ({locale}) => { + let evidence: SddValidationDiagnostic | undefined; + try { validateSddDraftConsistency(sddCaseReferenceFixture('Cases C01 and C02 establish acceptance. PRIVATE_PROSE'), [], { + files: new Set(['docs/payments.mdx']), directories: new Set(['docs']), blocked: new Set(), + }, 'specs/change.md'); } catch (error) { + expect(error).toBeInstanceOf(SddDraftFormatFailure); evidence=(error as SddDraftFormatFailure).diagnostic; + } + expect(evidence).toMatchObject({ reason:'case-identity', phase:'validation', facts:[{ detail:'case-shorthand', expected:0, observed:2 }] }); + const body=renderSddPreparationFailure({ issueNumber:42, locale, error:new SddDraftPreparationFailure(evidence!,2), diagnostic:evidence, attempts:2 }); + expect(body).toContain(locale==='es-ES'?'Hay referencias abreviadas a casos':'Unqualified shorthand case references are present'); + expect(body).toContain(locale==='es-ES'?'Cantidad observada: 2':'Observed count: 2'); + expect(body).toContain(locale==='es-ES'?'Se conservan las respuestas':'Existing answers and published specifications are retained'); + expect(body).not.toMatch(/PRIVATE_PROSE|entry-prose|Cases C01/); + }); +}); diff --git a/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts b/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts index b30147c82..8c263feb2 100644 --- a/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts +++ b/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.test.ts @@ -4,7 +4,7 @@ import { SddDraftFormatFailure, validateSddMarkdown, type SddPlan } from '../../ import { AgentResponseLengthFailure } from '../../../domain/agent_response_length_failure'; import { FindingsAgentAdapter } from '../../../data/repository/ai/findings_agent_adapter'; import { validateSddDraftConsistency } from '../../../domain/sdd_draft_consistency'; -import { sddConsistencyFixture } from '../../../testing/sdd_consistency_fixture'; +import { sddCaseReferenceFixture, sddConsistencyFixture } from '../../../testing/sdd_consistency_fixture'; const configuration = { provider: 'codex', model: 'model' } as never; const plan = { action: 'update', path: 'specs/payments.md', capabilityId: 'payments', reason: 'A bounded contract change.', questions: [] } as const; @@ -277,3 +277,27 @@ describe('draftSddMarkdown', () => { }, ); }); + + +describe('Case-role validation through the bounded draft workflow', () => { + const technical='Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.'; + const validate=async (markdown:string) => { + validateSddMarkdown(markdown); validateSddDraftConsistency(markdown,[],{files:new Set(['docs/payments.mdx']),directories:new Set(['docs']),blocked:new Set()},'specs/change.md'); + }; + it('accepts technical prose on the first generation without requesting a wording workaround', async () => { + const query=jest.fn().mockResolvedValue({markdown:sddCaseReferenceFixture(technical)}); + await expect(draftSddMarkdown({query},configuration,{...plan,action:'new'},'Preserve technical compatibility.',validate)).resolves.toContain(technical); + expect(query).toHaveBeenCalledTimes(1); + }); + it('still spends only the existing repair for a genuine shorthand violation', async () => { + const invalid=sddCaseReferenceFixture('Cases C01 and C02. PRIVATE_DRAFT'); + const query=jest.fn().mockResolvedValueOnce({markdown:invalid}).mockResolvedValueOnce({markdown:sddCaseReferenceFixture(technical)}); + await expect(draftSddMarkdown({query},configuration,{...plan,action:'new'},'Preserve literal identities.',validate)).resolves.toContain(technical); + expect(query).toHaveBeenCalledTimes(2); expect(query.mock.calls[1][0].prompt).not.toContain('PRIVATE_DRAFT'); + }); + it('cannot publish or spend a third generation after repeated genuine violations', async () => { + const query=jest.fn().mockResolvedValue({markdown:sddCaseReferenceFixture('Cases C01 and C02.')}); + await expect(draftSddMarkdown({query},configuration,{...plan,action:'new'},'Preserve literal identities.',validate)).rejects.toMatchObject({attempts:2,recovery:{id:'sdd-draft-case-identity-invalid'}}); + expect(query).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/domain/__tests__/sdd_case_reference.test.ts b/src/domain/__tests__/sdd_case_reference.test.ts new file mode 100644 index 000000000..874700725 --- /dev/null +++ b/src/domain/__tests__/sdd_case_reference.test.ts @@ -0,0 +1,78 @@ +import { countSddShorthandCaseReferences as count } from '../sdd_case_reference'; + +describe('SDD shorthand reference roles', () => { + it.each([ + ['sdd-case-ref-C01', 'Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.'], + ['sdd-case-ref-C02', 'The Unicode C1 control set remains readable.'], + ['sdd-case-ref-C03', 'Connector C1 is connected to component C2, C3 and C4.'], + ['sdd-case-ref-C04', 'The register `C1` supplies the value.'], + ['sdd-case-ref-C05', '| Component | Designator |\n|---|---|\n| Connector | C1 |'], + ['sdd-case-ref-C06', '| Case ID | Scenario |\n|---|---|\n| fixture-C01 | C1 controls remain readable. |'], + ['sdd-case-ref-C07', '| Requirement | Behavior |\n|---|---|\n| G-01 | Preserve Unicode C1. |'], + ['sdd-case-ref-C08', 'The endpoint URI is `/api/C1`; the model C2 is unrelated.'], + ['unicode-left-boundary', 'πC1 is an identifier.'], + ['unicode-right-boundary', 'C1β and C1\u0301 are identifiers.'], + ['compound-technical-reference', 'See C1-component for the connector model.'], + ['compound-paired-designators', 'See C1-C2-component for the connector model.'], + ['compound-identifier', 'C1-component and component-C1-name are operands.'], + ['case-sensitive-prefix-word', 'The casesensitive parameter C1 has no case-reference label.'], + ['reference-technical-role', 'See connector C1 and component C2.'], + ['description-not-continuation', 'fixture-C01: Unicode C1 controls remain readable.'], + ['case-scenario-no-spill', '| Case ID | Scenario |\n|---|---|\n| fixture-C01 | C1 controls |\n| fixture-C02 | C2 controls |'], + ['header-no-spill', '| Cases | Scenario |\n|---|---|\n| fixture-C01 | C1 |'], + ['prose-after-table', '| Cases | Notes |\n|---|---|\n| fixture-C01 | Case |\n\nC1 controls remain readable.'], + ['link-destination-is-not-a-case', 'Cases [overview](https://example.invalid/C1) remain separate.'], + ['paragraph-after-block', '> | Cases | Notes |\n> |---|---|\n> | fixture-C01 | X |\n\nThe connector C1 is separate.'], + ])('%s retains technical prose without inventing a case', (_id, source) => expect(count(source)).toBe(0)); + + it.each([ + ['sdd-case-ref-C09', 'Cases C01 and C02 establish acceptance.', 2], + ['sdd-case-ref-C10', 'Casos de prueba C01 y C02.', 2], + ['sdd-case-ref-C11', 'Case **ID**: `C01`.', 1], + ['sdd-case-ref-C12', '| Case ID | Owner |\n|---|---|\n| C01 | Fixture |', 1], + ['sdd-case-ref-C13', 'Reference fixture-C01, C02 and C03.', 2], + ['sdd-case-ref-C14', 'Cases C01–C48 establish acceptance.', 2], + ['sdd-case-ref-C15', 'See C01.', 1], + ['sdd-case-ref-C16', 'Case C01-C48.', 1], + ['sdd-case-ref-C17', 'Cases:\n- C01\n- C02', 2], + ['sdd-case-ref-C18', '**Cases**: [`C01`](#details), `C02`.', 2], + ['line-wrapped-list', 'Cases C01,\n C02 or C03.', 3], + ['multiple-range-endpoints', 'Cases C01-C02-C03.', 1], + ['numeric-range-endpoint', 'Cases C01-48.', 1], + ['case-number', 'Case no. C01.', 1], + ['spanish-reference', 'Véase C01.', 1], + ['covered-by', 'This behavior is covered by C01.', 1], + ['table-case-list', '| Requirement | Cases |\n|---|---|\n| G-01 | fixture-C01, C02 |', 1], + ['decorated-column', '| **Case IDs** | Scenario |\n|---|---|\n| C01 | Unicode C1 |', 1], + ['linked-column', '| [Case ID](#cases) | Scenario |\n|---|---|\n| C01 | Unicode C1 |', 1], + ['reference-column', '| Case references | Scenario |\n|---|---|\n| C01 | Unicode C1 |', 1], + ['header-without-outer-pipes', 'Case ID | Scenario\n---|---\nC01 | Unicode C1', 1], + ['body-without-pipes', '| Case ID | Scenario |\n|---|---|\nC01', 1], + ['gfm-single-dash', '| Cases | Notes |\n|:-:|---|\n| C01 | Fixture |', 1], + ['quoted-table', '> | Case ID | Scenario |\n> |---|---|\n> | C01 | Unicode C1 |', 1], + ['list-table', '- | Case ID | Scenario |\n |---|---|\n | C01 | Unicode C1 |', 1], + ['escaped-pipe-does-not-shift-role', '| Scenario | Cases |\n|---|---|\n| Unicode \\| C1 | C02 |', 1], + ['escaped-backslash-before-delimiter', '| Cases | Scenario |\n|---|---|\n| C01 \\\\| Unicode C1 |', 1], + ['qualified-link-continuation', 'See [fixture-C01](#details), C02.', 1], + ['quote-tab-table', '>\t| Case ID | Scenario |\n>\t|---|---|\n>\t| C01 | Unicode C1 |', 1], + ['single-case-column', '| Cases |\n|---|\n| C01 |', 1], + ])('%s rejects only shorthand that acquired a reference role', (_id, source, expected) => expect(count(source)).toBe(expected)); + + it('sdd-case-ref-C20 treats line endings equivalently', () => { + const source='Cases:\n- C01\n- C02\n\nComponent C3.'; + expect(count(source)).toBe(2); expect(count(source.replace(/\n/gu,'\r\n'))).toBe(2); expect(count(source.replace(/\n/gu,'\r'))).toBe(2); + }); + it('sdd-case-ref-C23 does not carry regex cursor or role state between calls', () => { + for (let index=0;index<3;index++) { expect(count('Cases C01.')).toBe(1); expect(count('Connector C1.')).toBe(0); } + }); + it('sdd-case-ref-C24 has no silently dropped prefix or spacing window', () => { + expect(count('Connector C1, C2. '.repeat(4000))).toBe(0); + expect(count('Cases:'+ ' '.repeat(50_000)+'C01')).toBe(1); + }); + it('does not infer a typed column from a malformed delimiter row', () => { + expect(count('| Component | Value |\n| --- |\n| Connector | C1 |')).toBe(0); + }); + it('does not treat a paragraph with a lone pipe as a case-role table', () => { + expect(count('|\n|\nUnicode C1.')).toBe(0); + }); +}); diff --git a/src/domain/__tests__/sdd_draft_consistency.test.ts b/src/domain/__tests__/sdd_draft_consistency.test.ts index 035f5044e..c715d0de0 100644 --- a/src/domain/__tests__/sdd_draft_consistency.test.ts +++ b/src/domain/__tests__/sdd_draft_consistency.test.ts @@ -1,5 +1,5 @@ -import { reservedSddCaseIds, validateSddDraftConsistency, type SddConsistencyReason } from '../sdd_draft_consistency'; -import { sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; +import { renderSddDerivedTables, reservedSddCaseIds, validateSddDraftConsistency, type SddConsistencyReason } from '../sdd_draft_consistency'; +import { sddCaseReferenceFixture, sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; const inventory = { files: new Set(['docs/payments.mdx', 'docs/my_guide.mdx', 'backend/app.kt']), @@ -212,3 +212,44 @@ describe('generated SDD consistency', () => { expect(() => reservedSddCaseIds([{ path: 'specs/legacy.md', markdown: 'identity-access-C01; C02–C99999.' }])).toThrow('review the baseline'); }); }); + + +describe('Technical prose with literal48-case consistency', () => { + const original = 'Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.'; + it('sdd-case-ref-C25 accepts the reported fragment through the full consistency policy', () => { + const markdown = sddCaseReferenceFixture(original); + expect(markdown.match(/^\| entry-prose-C\d+ \|/gm)).toHaveLength(48); + expect(() => validate(markdown)).not.toThrow(); + }); + it('sdd-case-ref-C26 retains byte-identical case rows and reverse mappings across technical wording', () => { + const a = sddCaseReferenceFixture(original), b = sddCaseReferenceFixture(original.replace('DEL/C1', 'DEL and U+0080–U+009F')); + expect(a.match(/^\| entry-prose-C\d+ \|.*$/gm)).toEqual(b.match(/^\| entry-prose-C\d+ \|.*$/gm)); + expect(a.split('## 17.')[1]).toBe(b.split('## 17.')[1]); + expect(() => validate(a)).not.toThrow(); expect(() => validate(b)).not.toThrow(); + }); + it.each([ + ['sdd-case-ref-C27', 'case-identity', (s: string) => s.replace(/entry-prose-C01/gu, 'C01')], + ['sdd-case-ref-C28', 'case-identity', (s: string) => s.replace(/entry-prose-C01/gu, 'payments-C01')], + ['sdd-case-ref-C29', 'case-identity', (s: string) => s + '\nSee entry-unknown-C99.'], + ['sdd-case-ref-C30', 'case-identity', (s: string) => s.replace('| G-01 | entry-prose-C01, ', '| G-01 | C01, ')], + ['sdd-case-ref-C31', 'requirement-mapping', (s: string) => s.replace('| G-01 | entry-prose-C01, ', '| G-01 | ')], + ['sdd-case-ref-C32', 'case-budget', (s: string) => s.replace('| Total | 48 |', '| Total | 47 |')], + ])('%s retains its independent %s rejection', (_id, reason, mutate) => { + expect(() => validate(mutate(sddCaseReferenceFixture(original)))).toThrow(expect.objectContaining({ reason })); + }); + it('sdd-case-ref-C33 retains legacy shorthand and range reservations unchanged', () => { + expect(reservedSddCaseIds([{ path: 'specs/legacy.md', markdown: 'retained-C01; C02–C05.' }])) + .toEqual(['retained-C01','retained-C02','retained-C03','retained-C04','retained-C05']); + }); + it('sdd-case-ref-C34 independently revalidates derived tables without rewriting technical prose', () => { + const source = sddCaseReferenceFixture(original) + .replace(/\| Area \| Minimum distinct cases[\s\S]*?(?=\| Case ID)/u, '{{SDD_CASE_BUDGET}}\n\n') + .replace(/\| Requirement \| Cases \|[\s\S]*$/u, '{{SDD_REQUIREMENT_CASES}}\n'); + const assembled = renderSddDerivedTables(source); + expect(assembled).toContain(original); expect(assembled.match(/^\| entry-prose-C\d+ \|/gm)).toHaveLength(48); + expect(() => validate(assembled)).not.toThrow(); + }); + it.each(['', '```md\nCase C01\n```'])('retains the owner visibility projection for a negative example: %s', example => { + expect(() => validate(sddCaseReferenceFixture(original) + '\n' + example)).not.toThrow(); + }); +}); diff --git a/src/domain/sdd_case_reference.ts b/src/domain/sdd_case_reference.ts new file mode 100644 index 000000000..78929dc70 --- /dev/null +++ b/src/domain/sdd_case_reference.ts @@ -0,0 +1,99 @@ +import { readSddTextBlockLocations, readSddVisibleReferences } from './sdd_visible_references'; + +// Full namespaces retain their established meaning; a bare technical token has no role by itself. +const TOKEN = /(?#=+\-\u2013\u2014]*$/iu; +const CONTINUATION = /^[\s,;.…\-\u2013\u2014*_`()[\]#]*(?:(?:and|or|to|through|y|o|a|hasta|e)\b[\s,;.…\-\u2013\u2014*_`()[\]#]*)*$/iu; +const CASE_COLUMN = /^(?:cases?(?:[ _-]?ids?| references?)?|test cases?(?: ids?)?|casos?(?: de prueba)?|ids? de casos?|identificadores de casos?)$/iu; + +interface Cell { + readonly text: string; + readonly start: number; + readonly end: number; +} +interface Row { readonly cells: readonly Cell[]; readonly hasPipe: boolean } +interface CellRole { readonly start: number; readonly end: number; readonly caseRole: boolean } + +/** Unescaped pipes delimit cells, including when the pipe occurs in an inline code span. */ +function row(text: string, offset: number): Row { + const cells: Cell[] = []; + let start = 0, escaped = false, hasPipe = false; + for (let index = 0; index < text.length; index++) { + const character = text[index]; + if (character === '|' && !escaped) { + cells.push({ text: text.slice(start, index), start: offset + start, end: offset + index }); + start = index + 1; + hasPipe = true; + } + escaped = character === '\\' && !escaped; + } + cells.push({ text: text.slice(start), start: offset + start, end: offset + text.length }); + if (hasPipe && cells[0].text.trim() === '') cells.shift(); + if (hasPipe && cells.length && cells[cells.length - 1].text.trim() === '') cells.pop(); + return { cells, hasPipe }; +} + +function caseColumn(value: string): boolean { + const label = readSddVisibleReferences(value).parts.map(part => part.value).join('').trim().replace(/\s+/gu, ' '); + return CASE_COLUMN.test(label); +} + +/** CommonMark paragraphs bound the GFM table projection, including nested quote/list blocks. */ +function tableCellRoles(source: string): readonly CellRole[] { + const lines = source.split('\n'); + const offsets: number[] = []; + let offset = 0; + for (const line of lines) { offsets.push(offset); offset += line.length + 1; } + const roles: CellRole[] = []; + for (const block of readSddTextBlockLocations(source)) { + const rows: Row[] = []; + for (let line = block.startLine - 1; line < block.endLine; line++) { + const raw = lines[line]; + const prefix = line === block.startLine - 1 ? block.startColumn - 1 + : (/^ {0,3}(?:>[ \t]?)+/u.exec(raw)?.[0].length ?? 0); + rows.push(row(raw.slice(prefix), offsets[line] + prefix)); + } + let columns: readonly boolean[] | undefined; + for (let index = 0; index < rows.length; index++) { + const header = rows[index], separator = rows[index + 1]; + if (separator && (header.hasPipe || separator.hasPipe) && header.cells.length > 0 + && separator.cells.length === header.cells.length && separator.cells.every(cell => /^:?-+:?$/u.test(cell.text.trim()))) { + columns = header.cells.map(cell => caseColumn(cell.text)); + roles.push(...header.cells.map(cell => ({ start: cell.start, end: cell.end, caseRole: false }))); + index++; + } else if (columns) { + // GFM permits missing cells and body rows without outer pipes. Extra cells cannot gain a role. + const activeColumns = columns; + roles.push(...header.cells.map((cell, column) => ({ start: cell.start, end: cell.end, caseRole: activeColumns[column] === true }))); + } + } + } + return roles; +} + +/** Rendered inline labels/code retain references; link destinations are not case IDs. */ +function countReferences(source: string, caseRole: boolean): number { + const text = readSddVisibleReferences(source).parts.map(part => part.value).join(''); + let previousEnd = 0, previousWasCase = false, count = 0; + for (const match of text.matchAll(TOKEN)) { + const gap = text.slice(previousEnd, match.index); + const reference: boolean = caseRole || EXPLICIT_REFERENCE.test(gap) + || (previousWasCase && CONTINUATION.test(gap)); + if (!match[1] && reference) count++; + previousWasCase = Boolean(match[1]) || reference; + previousEnd = match.index + match[0].length; + } + return count; +} + +/** Receives the owner's existing hidden/fenced-text projection; never rewrites caller Markdown. */ +export function countSddShorthandCaseReferences(visibleSource: string): number { + const source = visibleSource.replace(/\r\n?/gu, '\n'); + let previousEnd = 0, count = 0; + for (const cell of tableCellRoles(source)) { + count += countReferences(source.slice(previousEnd, cell.start), false); + count += countReferences(source.slice(cell.start, cell.end), cell.caseRole); + previousEnd = cell.end; + } + return count + countReferences(source.slice(previousEnd), false); +} diff --git a/src/domain/sdd_draft_consistency.ts b/src/domain/sdd_draft_consistency.ts index a86099804..0fcc1b458 100644 --- a/src/domain/sdd_draft_consistency.ts +++ b/src/domain/sdd_draft_consistency.ts @@ -4,6 +4,7 @@ import { appendSddReferenceContext, buildSddReferenceIndex, hasBlockedSddAncesto isSddNonFileNotation, sddReferencePrefix, type SddRepositoryInventory } from './sdd_repository_reference'; export type { SddRepositoryInventory } from './sdd_repository_reference'; import { readSddVisibleReferences } from './sdd_visible_references'; +import { countSddShorthandCaseReferences } from './sdd_case_reference'; export type SddConsistencyReason = 'case-identity' | 'case-budget' | 'requirement-mapping' | 'repository-path'; export interface SddOwnerDocument { readonly path: string; readonly markdown: string } @@ -17,7 +18,7 @@ export const SDD_CONSISTENCY_PROMPT = `Consistency contract for the proposed dra In section 4 include one table: Requirement | Behavior. Give every goal a stable requirement ID. In section 14 include one budget table: Area | Minimum distinct cases | Behaviors/risks covered; positive integers and one Total. Also in section 14 include one inventory table: Case ID | Area | Requirements | Scenario. -Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. +Each row is one distinct executable case with a fully qualified ID such as capability-change-C01, one exact budget area, comma-separated requirement IDs and an observable scenario. Never use shorthand C01, ranges or redefine an ID found in any owning SDD. Reference retained baseline cases in prose using their full IDs; count only the new inventory. Ordinary technical designators in prose, scenario text or non-case columns are not acceptance-case references solely because they resemble short IDs; preserve their original terminology. Case-reference fields, explicit references and adjacent case-ID lists still require full identities. In section 17 include one table: Requirement | Cases. List comma-separated full case IDs for every requirement. Its links must exactly match the case inventory in both directions, with no undefined or generic documentation checks. In section 15 include one table: Path | Change. Change is update, reference or create. Include the selected SDD path itself with update for an existing SDD or create for a new/companion SDD. Use existing immutable-base files for update/reference. Only explicitly proposed new files may use create; never mark a missing current document as an existing artifact. Cite repository files as full repository-relative paths in backticks with a File or Path label when ambiguous, or relative Markdown file links. Put every planned new file in this table; a proposed parent directory is valid only as an ancestor of a declared new file, never as a file declaration. HTTP endpoints, media types, mode alternatives and module names are protocol notation, not file-existence claims. Label file-shaped relative asset addresses with URL or URI; do not relabel a repository citation to evade validation. Explicit Path/Change rows and relative Markdown destinations always receive strict repository checks. These tables are publication consistency checks, not runtime/security acceptance.`; @@ -183,8 +184,8 @@ export function validateSddDraftConsistency( const caseIds = cases.map(row => row[0]); const reserved = new Set(reservedSddCaseIds(owners)); uniqueIdentifiers(caseIds, CASE_ID, 'case-identity', 14); - const shorthand = [...source.matchAll(/(? reserved.has(id))) fail('case-identity', { detail: 'case-reserved', section: 14, expected: 0, observed: caseIds.filter(id => reserved.has(id)).length }); // References to existing owner cases are allowed, but cannot count as newly defined cases. if ([...source.matchAll(CASE)].some(([id]) => !caseIds.includes(id) && !reserved.has(id))) fail('case-identity', { detail: 'case-reference', section: 0 }); diff --git a/src/domain/sdd_visible_references.ts b/src/domain/sdd_visible_references.ts index d19d465fb..fde4721c9 100644 --- a/src/domain/sdd_visible_references.ts +++ b/src/domain/sdd_visible_references.ts @@ -6,6 +6,28 @@ export interface SddVisibleReferences { readonly parts: readonly SddReferencePart[]; } +export interface SddTextBlockLocation { + readonly startLine: number; + readonly startColumn: number; + readonly endLine: number; +} + +/** CommonMark supplies block boundaries; callers retain raw table-cell escapes. */ +export function readSddTextBlockLocations(source: string): readonly SddTextBlockLocation[] { + const blocks: SddTextBlockLocation[] = []; + const walker = new Parser().parse(source).walker(); + let event; + while ((event = walker.next())) { + const { node, entering } = event; + if (entering && ['paragraph', 'heading'].includes(node.type)) { + // These concrete block nodes always have source positions in CommonMark. + const [start, end] = node.sourcepos!; + blocks.push({ startLine: start[0], startColumn: start[1], endLine: end[0] }); + } + } + return blocks; +} + /** Pure syntax projection; callers first remove the gate's hidden comments/fences. */ export function readSddVisibleReferences(source: string): SddVisibleReferences { const destinations: string[] = []; diff --git a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts index 605786472..70fe67580 100644 --- a/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts +++ b/src/infrastructure/__tests__/pre_branch_sdd_workspace_adapter.test.ts @@ -1,6 +1,6 @@ import fs from 'node:fs'; import { SddTrustedCatalogMissingFailure } from '../../domain/sdd_trusted_catalog_failure'; -import { sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; +import { sddCaseReferenceFixture, sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; import * as os from 'node:os'; import * as path from 'node:path'; import { execFileSync } from 'node:child_process'; @@ -75,6 +75,33 @@ describe('PreBranchSddWorkspaceAdapter with a local bare remote', () => { beforeEach(() => { ({ temp, repo } = fixture()); }); afterEach(() => fs.rmSync(temp, { recursive: true, force: true })); + + it.each([['sdd-case-ref-C35','technical'],['sdd-case-ref-C36','shorthand'],['sdd-case-ref-C37','identity'],['sdd-case-ref-C38','replay']])('%s preserves real Git publication boundaries for %s', async (_id, scenario) => { + const workspace = new PreBranchSddWorkspaceAdapter(repo); + const snapshot = await workspace.loadSnapshot('develop'); + const selected: SddPlan = { ...plan, action: 'companion', path: 'specs/case-reference.md' }; + const prose = scenario === 'shorthand' ? 'Cases C01 and C02 establish acceptance.' + : 'Existing household DEL/C1 read compatibility remains separate from stronger new-setup input validation.'; + const candidate = sddCaseReferenceFixture(prose, selected.path); + const markdown = scenario === 'identity' ? candidate.replace(/entry-prose-C01/gu, 'C01') : candidate; + const before = fs.readFileSync(path.join(repo, plan.path), 'utf8'); + if (scenario === 'shorthand' || scenario === 'identity') { + await expect(workspace.validateDraft(snapshot, selected, markdown)).rejects.toMatchObject({ reason: 'case-identity' }); + expect(git(repo, 'ls-remote', '--heads', 'origin', 'feature/42-change')).toBe(''); + } else { + const prepared = await workspace.validateDraft(snapshot, selected, markdown); + expect(prepared.markdown).toContain(prose); expect(prepared.markdown.match(/^\| entry-prose-C\d+ \|/gm)).toHaveLength(48); + git(repo, 'push', 'origin', 'develop:refs/heads/feature/42-change'); + const sha = await workspace.publish('feature/42-change', prepared); + expect(await workspace.verifyPublication('feature/42-change', snapshot.baseSha, sha, selected.path)).toBe(true); + if (scenario === 'replay') expect(await workspace.recoverPublished('feature/42-change', prepared)).toBe(sha); + expect(git(repo, 'show', sha + ':' + selected.path)).toContain(prose); + } + expect(fs.readFileSync(path.join(repo, plan.path), 'utf8')).toBe(before); + expect(git(repo, 'status', '--porcelain')).toBe(''); + expect(git(repo, 'worktree', 'list', '--porcelain').match(/^worktree /gm)).toHaveLength(1); + }); + it.each(['update', 'companion'] as const)('accepts protocol notation in a complete %s against immutable Git evidence', async action => { const workspace = new PreBranchSddWorkspaceAdapter(repo); const snapshot = await workspace.loadSnapshot('develop'); diff --git a/src/testing/sdd_consistency_fixture.ts b/src/testing/sdd_consistency_fixture.ts index cb1450b43..27aeed3b2 100644 --- a/src/testing/sdd_consistency_fixture.ts +++ b/src/testing/sdd_consistency_fixture.ts @@ -46,3 +46,18 @@ The three cases above are observable product outcomes; runtime and security revi | G-02 | ${prefix}-C03 | `; } + +/** Forty-eight hypothetical input rows; these are not forty-eight executed SDK regressions. */ +export function sddCaseReferenceFixture(prose: string, sddPath = 'specs/change.md', change: 'create' | 'update' = 'create'): string { + const prefix = 'entry-prose'; + const cases = Array.from({ length: 48 }, (_, index) => `${prefix}-C${String(index + 1).padStart(2, '0')}`); + const budget = '| Area | Minimum distinct cases | Behaviors/risks covered |\n|---|---:|---|\n| Policy | 24 | First fixture group |\n| Adapter | 24 | Second fixture group |\n| Total | 48 | No double counting |\n\n'; + const inventory = '| Case ID | Area | Requirements | Scenario |\n|---|---|---|---|\n' + + cases.map((id, index) => `| ${id} | ${index < 24 ? 'Policy | G-01' : 'Adapter | G-02'} | Independent fixture outcome ${index + 1}. |`).join('\n') + '\n\n'; + const reverse = '| Requirement | Cases |\n|---|---|\n| G-01 | ' + cases.slice(0, 24).join(', ') + ' |\n| G-02 | ' + cases.slice(24).join(', ') + ' |\n'; + return sddConsistencyFixture(prefix, sddPath, change) + .replace('## 8.', '## 6. Functional behavior\n' + prose + '\n\n## 8.') + .replace(/\| Area \| Minimum distinct cases[\s\S]*?(?=\| Case ID)/u, () => budget) + .replace(/\| Case ID \| Area[\s\S]*?(?=## 15\.)/u, () => inventory) + .replace(/\| Requirement \| Cases \|[\s\S]*$/u, () => reverse); +}