From a322dba7e09bc3a8f1c4540ee0f2f48462b7816e Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 25 Oct 2026 12:06:57 +0100 Subject: [PATCH 1/2] hotfix-3.10.5: derive SDD budgets and traceability from one case inventory --- build/cli/index.js | 68 +++++++++-- build/github_action/index.js | 68 +++++++++-- docs/issues/pre-branch-sdds.mdx | 12 ++ specs/CATALOG.md | 6 +- specs/catalog.json | 5 +- specs/pre-branch-sdd-gate.md | 77 +++++++++++++ .../sdd/pre_branch_sdd_draft_workflow.test.ts | 59 ++++++++++ .../sdd/pre_branch_sdd_draft_workflow.ts | 10 +- .../__tests__/sdd_derived_tables.test.ts | 107 ++++++++++++++++++ src/domain/sdd_draft_consistency.ts | 51 ++++++++- 10 files changed, 437 insertions(+), 26 deletions(-) create mode 100644 src/domain/__tests__/sdd_derived_tables.test.ts diff --git a/build/cli/index.js b/build/cli/index.js index 0960966fa..bd1ec78e5 100755 --- a/build/cli/index.js +++ b/build/cli/index.js @@ -61163,7 +61163,7 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { try { value = await agent.query({ configuration, agentId: 'pre-branch-sdd-draft', - prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${sdd_draft_consistency_1.SDD_CONSISTENCY_PROMPT}`, + prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${sdd_draft_consistency_1.SDD_CONSISTENCY_PROMPT}\n\n${sdd_draft_consistency_1.SDD_DERIVED_TABLES_PROMPT}`, options: { expectJson: true, schemaName: 'pre_branch_sdd_draft', schema }, }); } @@ -61185,8 +61185,13 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { continue; } try { - await validate?.(markdown); - return markdown; + const assembled = (0, sdd_draft_consistency_1.renderSddDerivedTables)(markdown); + if (assembled.length > maximum) { + problem = 'size'; + continue; + } + await validate?.(assembled); + return assembled; } catch (error) { if (!(error instanceof pre_branch_sdd_1.SddDraftFormatFailure)) @@ -93609,7 +93614,8 @@ function replyRequestBodyDigest(body) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.SDD_CONSISTENCY_PROMPT = void 0; +exports.SDD_DERIVED_TABLES_PROMPT = exports.SDD_CONSISTENCY_PROMPT = void 0; +exports.renderSddDerivedTables = renderSddDerivedTables; exports.reservedSddCaseIds = reservedSddCaseIds; exports.validateSddDraftConsistency = validateSddDraftConsistency; const pre_branch_sdd_1 = __nccwpck_require__(34730); @@ -93624,6 +93630,10 @@ Also in section 14 include one inventory table: Case ID | Area | Requirements | 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. 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, or relative Markdown file links. Put every planned new file in this table. These tables are publication consistency checks, not runtime/security acceptance.`; +const BUDGET_SLOT = '{{SDD_CASE_BUDGET}}'; +const TRACEABILITY_SLOT = '{{SDD_REQUIREMENT_CASES}}'; +/** The provider writes definitions once; trusted code renders their two redundant projections. */ +exports.SDD_DERIVED_TABLES_PROMPT = `Draft assembly: the consistency contract above describes the final document. In your raw Markdown, write the complete Requirement | Behavior table in section 4 and Case ID | Area | Requirements | Scenario inventory in section 14. Preserve every resolved minimum, per-area allocation and observable case as actual inventory rows, never ranges or summaries. Instead of independently writing the budget table, put ${BUDGET_SLOT} once on its own visible line in section 14. Instead of writing the reverse Requirement | Cases table, put ${TRACEABILITY_SLOT} once on its own visible line in section 17. Do not include either derived table yourself or put these slots in examples, fences or comments. Trusted code derives exact counts, Total and reciprocal links from your unchanged definitions, then runs every publication check. The compact character target is advisory: use more space when required to preserve the full resolved inventory and scope. The hard limit applies after assembly too.`; function fail(reason) { // Closed diagnostics: rejected Markdown and repository text never become repair instructions. throw new pre_branch_sdd_1.SddDraftFormatFailure(reason, `The SDD draft violates its ${reason} consistency contract.`); @@ -93661,14 +93671,17 @@ function visible(source) { }).join('\n'); } function clean(value) { return value.trim().replace(/^(?:\*\*|__|`)(.*?)(?:\*\*|__|`)$/u, '$1').trim(); } -function table(source, section, headers, reason) { +function section(source, number, reason) { const sections = [...source.matchAll(/^## (\d+)\. [^\n]*$/gmu)]; - const matches = sections.filter(heading => Number(heading[1]) === section); + const matches = sections.filter(heading => Number(heading[1]) === number); if (matches.length !== 1) return fail(reason); const start = matches[0]; const end = sections.find(heading => heading.index > start.index)?.index ?? source.length; - const lines = source.slice(start.index + start[0].length, end).split('\n'); + return source.slice(start.index + start[0].length, end); +} +function table(source, number, headers, reason) { + const lines = section(source, number, reason).split('\n'); const found = []; for (let index = 0; index < lines.length; index++) { if (!/^\s*\|/u.test(lines[index])) @@ -93699,6 +93712,47 @@ function ids(value, pattern, reason) { return fail(reason); return values; } +/** Never repairs definitions or contradictory supplied tables; the writer independently revalidates. */ +function renderSddDerivedTables(markdown) { + if (!markdown.includes(BUDGET_SLOT) && !markdown.includes(TRACEABILITY_SLOT)) + return markdown; + const source = visible(markdown); + for (const [slot, number, reason] of [ + [BUDGET_SLOT, 14, 'case-budget'], [TRACEABILITY_SLOT, 17, 'requirement-mapping'], + ]) { + if (markdown.split(slot).length !== 2 + || section(source, number, reason).split('\n').filter(line => line.trim() === slot).length !== 1) + fail(reason); + } + const requirements = table(source, 4, ['Requirement', 'Behavior'], 'requirement-mapping').map(row => row[0]); + if (new Set(requirements).size !== requirements.length || requirements.some(id => !REQUIREMENT_ID.test(id))) + fail('requirement-mapping'); + const cases = table(source, 14, ['Case ID', 'Area', 'Requirements', 'Scenario'], 'case-identity'); + const caseIds = cases.map(row => row[0]); + if (new Set(caseIds).size !== caseIds.length || caseIds.some(id => !CASE_ID.test(id))) + fail('case-identity'); + const counts = new Map(); + const edges = new Map(requirements.map(id => [id, []])); + for (const [id, area, links] of cases) { + if (area === 'Total') + fail('case-budget'); + counts.set(area, (counts.get(area) ?? 0) + 1); + for (const requirement of ids(links, REQUIREMENT_ID, 'requirement-mapping')) { + const linked = edges.get(requirement); + if (!linked) + fail('requirement-mapping'); + linked.push(id); + } + } + if ([...edges.values()].some(links => links.length === 0)) + fail('requirement-mapping'); + const budget = ['| Area | Minimum distinct cases | Behaviors/risks covered |', '|---|---:|---|', + ...[...counts].map(([area, count]) => `| ${area} | ${count} | Observable scenarios in the case inventory. |`), + `| Total | ${cases.length} | Distinct inventory cases; repeated executions do not add cases. |`].join('\n'); + const traceability = ['| Requirement | Cases |', '|---|---|', + ...[...edges].map(([requirement, links]) => `| ${requirement} | ${links.join(', ')} |`)].join('\n'); + return markdown.replace(BUDGET_SLOT, () => budget).replace(TRACEABILITY_SLOT, () => traceability); +} function reservedSddCaseIds(documents) { const reserved = new Set(); for (const document of documents) { diff --git a/build/github_action/index.js b/build/github_action/index.js index 2fd7f15bf..d2eb3b3e4 100644 --- a/build/github_action/index.js +++ b/build/github_action/index.js @@ -59163,7 +59163,7 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { try { value = await agent.query({ configuration, agentId: 'pre-branch-sdd-draft', - prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${sdd_draft_consistency_1.SDD_CONSISTENCY_PROMPT}`, + prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${sdd_draft_consistency_1.SDD_CONSISTENCY_PROMPT}\n\n${sdd_draft_consistency_1.SDD_DERIVED_TABLES_PROMPT}`, options: { expectJson: true, schemaName: 'pre_branch_sdd_draft', schema }, }); } @@ -59185,8 +59185,13 @@ async function draftSddMarkdown(agent, configuration, plan, prompt, validate) { continue; } try { - await validate?.(markdown); - return markdown; + const assembled = (0, sdd_draft_consistency_1.renderSddDerivedTables)(markdown); + if (assembled.length > maximum) { + problem = 'size'; + continue; + } + await validate?.(assembled); + return assembled; } catch (error) { if (!(error instanceof pre_branch_sdd_1.SddDraftFormatFailure)) @@ -84053,7 +84058,8 @@ function replyRequestBodyDigest(body) { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.SDD_CONSISTENCY_PROMPT = void 0; +exports.SDD_DERIVED_TABLES_PROMPT = exports.SDD_CONSISTENCY_PROMPT = void 0; +exports.renderSddDerivedTables = renderSddDerivedTables; exports.reservedSddCaseIds = reservedSddCaseIds; exports.validateSddDraftConsistency = validateSddDraftConsistency; const pre_branch_sdd_1 = __nccwpck_require__(34730); @@ -84068,6 +84074,10 @@ Also in section 14 include one inventory table: Case ID | Area | Requirements | 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. 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, or relative Markdown file links. Put every planned new file in this table. These tables are publication consistency checks, not runtime/security acceptance.`; +const BUDGET_SLOT = '{{SDD_CASE_BUDGET}}'; +const TRACEABILITY_SLOT = '{{SDD_REQUIREMENT_CASES}}'; +/** The provider writes definitions once; trusted code renders their two redundant projections. */ +exports.SDD_DERIVED_TABLES_PROMPT = `Draft assembly: the consistency contract above describes the final document. In your raw Markdown, write the complete Requirement | Behavior table in section 4 and Case ID | Area | Requirements | Scenario inventory in section 14. Preserve every resolved minimum, per-area allocation and observable case as actual inventory rows, never ranges or summaries. Instead of independently writing the budget table, put ${BUDGET_SLOT} once on its own visible line in section 14. Instead of writing the reverse Requirement | Cases table, put ${TRACEABILITY_SLOT} once on its own visible line in section 17. Do not include either derived table yourself or put these slots in examples, fences or comments. Trusted code derives exact counts, Total and reciprocal links from your unchanged definitions, then runs every publication check. The compact character target is advisory: use more space when required to preserve the full resolved inventory and scope. The hard limit applies after assembly too.`; function fail(reason) { // Closed diagnostics: rejected Markdown and repository text never become repair instructions. throw new pre_branch_sdd_1.SddDraftFormatFailure(reason, `The SDD draft violates its ${reason} consistency contract.`); @@ -84105,14 +84115,17 @@ function visible(source) { }).join('\n'); } function clean(value) { return value.trim().replace(/^(?:\*\*|__|`)(.*?)(?:\*\*|__|`)$/u, '$1').trim(); } -function table(source, section, headers, reason) { +function section(source, number, reason) { const sections = [...source.matchAll(/^## (\d+)\. [^\n]*$/gmu)]; - const matches = sections.filter(heading => Number(heading[1]) === section); + const matches = sections.filter(heading => Number(heading[1]) === number); if (matches.length !== 1) return fail(reason); const start = matches[0]; const end = sections.find(heading => heading.index > start.index)?.index ?? source.length; - const lines = source.slice(start.index + start[0].length, end).split('\n'); + return source.slice(start.index + start[0].length, end); +} +function table(source, number, headers, reason) { + const lines = section(source, number, reason).split('\n'); const found = []; for (let index = 0; index < lines.length; index++) { if (!/^\s*\|/u.test(lines[index])) @@ -84143,6 +84156,47 @@ function ids(value, pattern, reason) { return fail(reason); return values; } +/** Never repairs definitions or contradictory supplied tables; the writer independently revalidates. */ +function renderSddDerivedTables(markdown) { + if (!markdown.includes(BUDGET_SLOT) && !markdown.includes(TRACEABILITY_SLOT)) + return markdown; + const source = visible(markdown); + for (const [slot, number, reason] of [ + [BUDGET_SLOT, 14, 'case-budget'], [TRACEABILITY_SLOT, 17, 'requirement-mapping'], + ]) { + if (markdown.split(slot).length !== 2 + || section(source, number, reason).split('\n').filter(line => line.trim() === slot).length !== 1) + fail(reason); + } + const requirements = table(source, 4, ['Requirement', 'Behavior'], 'requirement-mapping').map(row => row[0]); + if (new Set(requirements).size !== requirements.length || requirements.some(id => !REQUIREMENT_ID.test(id))) + fail('requirement-mapping'); + const cases = table(source, 14, ['Case ID', 'Area', 'Requirements', 'Scenario'], 'case-identity'); + const caseIds = cases.map(row => row[0]); + if (new Set(caseIds).size !== caseIds.length || caseIds.some(id => !CASE_ID.test(id))) + fail('case-identity'); + const counts = new Map(); + const edges = new Map(requirements.map(id => [id, []])); + for (const [id, area, links] of cases) { + if (area === 'Total') + fail('case-budget'); + counts.set(area, (counts.get(area) ?? 0) + 1); + for (const requirement of ids(links, REQUIREMENT_ID, 'requirement-mapping')) { + const linked = edges.get(requirement); + if (!linked) + fail('requirement-mapping'); + linked.push(id); + } + } + if ([...edges.values()].some(links => links.length === 0)) + fail('requirement-mapping'); + const budget = ['| Area | Minimum distinct cases | Behaviors/risks covered |', '|---|---:|---|', + ...[...counts].map(([area, count]) => `| ${area} | ${count} | Observable scenarios in the case inventory. |`), + `| Total | ${cases.length} | Distinct inventory cases; repeated executions do not add cases. |`].join('\n'); + const traceability = ['| Requirement | Cases |', '|---|---|', + ...[...edges].map(([requirement, links]) => `| ${requirement} | ${links.join(', ')} |`)].join('\n'); + return markdown.replace(BUDGET_SLOT, () => budget).replace(TRACEABILITY_SLOT, () => traceability); +} function reservedSddCaseIds(documents) { const reserved = new Set(); for (const document of documents) { diff --git a/docs/issues/pre-branch-sdds.mdx b/docs/issues/pre-branch-sdds.mdx index ffb335860..152cf523c 100644 --- a/docs/issues/pre-branch-sdds.mdx +++ b/docs/issues/pre-branch-sdds.mdx @@ -186,6 +186,18 @@ requirements or change the acceptance limits below. Codex receives a compatible closed JSON shape; Copilot enforces the complete original string-length schema locally before any document reaches the writer. +The provider writes the requirements and explicit case inventory once. Copilot +calculates the final area counts, total and reciprocal requirement links before +validating the document. Every resolved acceptance minimum and scenario must +remain in the inventory; the compact target is advisory, and the hard character +limit also applies after the calculated tables are inserted. + +Assembly never changes case IDs, scenarios, requirement edges, supplied tables +or paths to make a contradiction pass. Malformed definitions, colliding IDs, +uncovered requirements and invalid paths still block publication. Existing +complete drafts retain their bytes and checks. Structural consistency does not +establish completeness or runtime acceptance; human review remains required. + Copilot limits proposed update appendices to 18,000 characters and complete new or companion specifications to 70,000. If a generated draft exceeds its limit, it tries one concise regeneration for the same owner and answers. Missing diff --git a/specs/CATALOG.md b/specs/CATALOG.md index a0d046df3..1b3116ea9 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 | 247 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 | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 119 paths · 2026-10-21 | +| `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 | [Uniform issue start and pre-branch SDD readiness](./issue-start-and-branch-readiness.md) + 3 companion | 120 paths · 2026-10-25 | | `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) | 71 paths · 2026-10-20 | | `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) | 79 paths · 2026-10-11 | | `bugbot-analysis-and-autofix` | Implemented | Select one canonical PR, exhaustively analyze its bounded diff partitions, publish stable findings atomically, and apply authorized verified fixes | [Bugbot analysis, finding publication, and autofix](./bugbot-analysis-publication-and-autofix.md) + 2 companion | 115 paths · 2026-10-21 | @@ -147,12 +147,12 @@ debt or convert unknown historic intent into a design decision. ### `issue-start-and-sdd-readiness` — Uniform issue start and pre-branch SDD readiness - Owner: Copilot maintainers -- Last verified: 2026-10-21 +- Last verified: 2026-10-25 - 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) - 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) -- 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) +- 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) - 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) ### `managed-issue-lifecycle` — Managed issue and branch lifecycle diff --git a/specs/catalog.json b/specs/catalog.json index 57b8cf4ab..5a259947a 100644 --- a/specs/catalog.json +++ b/specs/catalog.json @@ -1502,7 +1502,7 @@ "status": "implemented", "scope": "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", "owner": "Copilot maintainers", - "lastVerified": "2026-10-21", + "lastVerified": "2026-10-25", "specs": [ "specs/issue-start-and-branch-readiness.md", "specs/pre-branch-sdd-gate.md", @@ -1624,7 +1624,8 @@ "src/application/usecases/setup/__tests__/doctor_use_case.test.ts", "src/infrastructure/__tests__/branch_sync_sdd_catalog.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" ], "documentation": [ "docs/issues/branch-management.mdx", diff --git a/specs/pre-branch-sdd-gate.md b/specs/pre-branch-sdd-gate.md index 6823b7f4f..624d5459b 100644 --- a/specs/pre-branch-sdd-gate.md +++ b/specs/pre-branch-sdd-gate.md @@ -1308,3 +1308,80 @@ deeper depths and accept siblings and distinct pathname prefixes. They raise the pure floor to 95 and the amendment total to 126. Existing closed path diagnostics, one-repair budget, immutable evidence and no-publication behavior remain unchanged; the pre-branch SDD guide documents this boundary. + +### Deterministic draft table assembly (2026-10-25, #695) + +Observed defect: [#694](https://github.com/vypdev/copilot/issues/694) repeatedly +stopped before its first branch publication because independently generated +budgets and reciprocal requirement links disagreed with the case inventory. +[Run 40168044978](https://github.com/vypdev/copilot/actions/runs/40168044978) +exhausted the shared repair with `case-budget`. Strict consistency checks remain +intentional; this correction changes drafting, not publication readiness or the +resolved acceptance scope. No rejected draft was published. + +The provider MUST write each requirement and observable case once, including +the case's exact area and requirement edges. Raw Markdown uses +`{{SDD_CASE_BUDGET}}` on a standalone visible line in section 14 and +`{{SDD_REQUIREMENT_CASES}}` in section 17. Trusted pure code derives exact area +counts, one Total and reciprocal links from those unchanged definitions. Each +slot MUST occur exactly once in the raw response and in its correct visible +section. Missing, duplicate, hidden, fenced, inline and misplaced slots fail +closed. Responses containing neither slot retain their bytes and all checks. + +Assembly MUST NOT invent or drop cases, change scenarios, relabel areas, alter +edges, overwrite contradictory supplied tables or normalize repository paths. +Duplicate/invalid IDs, undefined/duplicate edges, uncovered requirements and a +reserved Total area block. The unchanged validator independently checks supplied +tables, immutable owner collisions and paths afterward. All resolved minima and +area allocations remain drafting requirements; they cannot be reduced to meet +the advisory size target. Human review still assesses natural-language scope +and risk coverage; structural consistency cannot certify completeness. + +```text +Resolved issue/answers -> explicit requirements and case inventory + -> pure derived tables -> assembled-size check -> unchanged workspace checks + -> freshness and remote branch/commit proof -> proposed draft publication +``` + +Text equivalent: trusted assembly projects definitions before writer validation; +branch creation and runtime acceptance remain separate. The application passes +only assembled Markdown to the existing writer port. The domain policy imports +no provider, filesystem or Git capability. Both raw and assembled candidates +MUST obey the existing 18,000/70,000-character limits. Assembly errors use closed +format categories and share the existing two-attempt size/format ceiling. +Rejected Markdown, IDs, paths and diagnostics never enter repair prompts or +public messages. No input, permission, durable schema, label or card is added. + +Pending, blocked, partial-push recovery and completed UI retain existing localized +content and noise limits. Slots disappear before successful publication. +Completed means a consistent proposed draft exists, with implementation/runtime +acceptance still open. Exhaustion publishes nothing and retains the answered +plan and prior commits. In-flight drafts use assembly on their next generation; +historical SDDs and owner bytes stay intact. Rollback selects the prior Action +build. No navigation or configuration migration is needed. + +The additional floor is **40 distinct cases**, separate from earlier budgets: + +| Area | Minimum distinct cases | Behaviors/risks covered | +|---|---:|---| +| Pure assembly and validator boundaries | 36 | Legacy bytes, exact counts/edges, 96-case seven-area reproduction, multi-edge counting, CRLF/formatting, invalid slot visibility/placement, malformed definitions, supplied contradictions and owner/path rejection | +| Application and real findings adapter | 4 | Native response reaches strict validation after assembly, closed malformed-slot repair, assembled-size rejection and exhausted repair without writer acceptance | +| Total | 40 | Existing cases are not recounted | + +New pure assembly targets at least 95% line and branch coverage, using +deterministic fixtures without providers or sleeps. The 96-case fixture tests +assembly arithmetic; it is not 96 executed extension acceptance tests. Existing +all-owner, real Git worktree, replay, localization and publication suites remain +required. Documentation updates `docs/issues/pre-branch-sdds.mdx`; catalog +evidence registers `src/domain/__tests__/sdd_derived_tables.test.ts` and the +review date. Existing architecture constraints enforce the dependency boundary. + +Acceptance/traceability: the 96-case fixture MUST yield F12/N14/A12/I16/V14/S16/ +E12 and Total96 with exact reciprocal edges and unchanged scenarios. Invalid +definitions, owner IDs, paths and supplied tables MUST remain rejected. The +real findings adapter MUST send assembled Markdown through both existing +validators. An oversized result or repeated malformed slot MUST stop after two +attempts without writer acceptance. DoD includes these 40 cases, the unchanged +broader publication suites, type/lint/architecture, specification/documentation +validation, rebuilt bundles and package checks. The live #694 workflow is a +separate post-promotion acceptance check. 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 b49beaaf5..10d1bcdf8 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 @@ -2,11 +2,70 @@ import { draftSddMarkdown } from './pre_branch_sdd_draft_workflow'; import { SddDraftFormatFailure, validateSddMarkdown, type SddPlan } from '../../../domain/pre_branch_sdd'; 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'; 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; describe('draftSddMarkdown', () => { + const assemblyDraft = () => sddConsistencyFixture() + .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'); + + it('assembles the native findings response before the unchanged publication checks run', async () => { + const execute = jest.fn().mockResolvedValue(JSON.stringify({ markdown: assemblyDraft() })); + const adapter = new FindingsAgentAdapter({ cli: { execute } }); + const validate = jest.fn(async (candidate: string) => { + validateSddMarkdown(candidate); + validateSddDraftConsistency(candidate, [], { + files: new Set(['docs/payments.mdx']), directories: new Set(['docs']), blocked: new Set(), + }, 'specs/change.md'); + }); + const markdown = await draftSddMarkdown(adapter, { provider: 'codex', modelProvider: 'openai', model: 'gpt-5' }, + { ...plan, action: 'new' }, 'Preserve all three distinct cases and both requirements.', validate); + expect(markdown).toContain('| Policy | 2 |'); + expect(markdown).toContain('| Total | 3 |'); + expect(markdown).toContain('| G-01 | payments-change-C01, payments-change-C02 |'); + expect(markdown).not.toContain('{{SDD_'); + expect(validate).toHaveBeenCalledTimes(1); + expect(execute).toHaveBeenCalledTimes(1); + expect(execute.mock.calls[0][0].prompt).toContain('Preserve every resolved minimum, per-area allocation'); + expect(execute.mock.calls[0][0].prompt).toContain('hard limit applies after assembly'); + }); + + it('spends the existing shared repair when generated slots are malformed without echoing that draft', async () => { + const invalid = assemblyDraft().replace('{{SDD_REQUIREMENT_CASES}}', 'Untrusted rejected content @actor'); + const query = jest.fn().mockResolvedValueOnce({ markdown: invalid }).mockResolvedValueOnce({ markdown: assemblyDraft() }); + const validate = jest.fn().mockResolvedValue(undefined); + await expect(draftSddMarkdown({ query }, configuration, plan, 'Original resolved inventory', validate)).resolves.toContain('| Total | 3 |'); + expect(query).toHaveBeenCalledTimes(2); + expect(validate).toHaveBeenCalledTimes(1); + expect(query.mock.calls[1][0].prompt).toContain('SDD requirement-mapping contract'); + expect(query.mock.calls[1][0].prompt).not.toContain('Untrusted rejected content @actor'); + }); + + it('enforces the final assembled character bound even when both raw responses fit their schema', async () => { + const raw = assemblyDraft(); + const atLimit = `${'x'.repeat(18000 - raw.length - 1)}\n${raw}`; + expect(atLimit).toHaveLength(18000); + const query = jest.fn().mockResolvedValue({ markdown: atLimit }); + const validate = jest.fn(); + await expect(draftSddMarkdown({ query }, configuration, plan, 'Original', validate)) + .rejects.toMatchObject({ recovery: { id: 'sdd-draft-size-exceeded' } }); + expect(query).toHaveBeenCalledTimes(2); + expect(validate).not.toHaveBeenCalled(); + }); + + it('stops after the single assembly repair with the existing closed recovery and no writer call', async () => { + const query = jest.fn().mockResolvedValue({ markdown: assemblyDraft().replace('{{SDD_CASE_BUDGET}}', '') }); + const validate = jest.fn(); + await expect(draftSddMarkdown({ query }, configuration, plan, 'Original', validate)) + .rejects.toMatchObject({ recovery: { id: 'sdd-draft-case-budget-invalid', variables: {} } }); + expect(query).toHaveBeenCalledTimes(2); + expect(validate).not.toHaveBeenCalled(); + }); + it.each(['case-identity', 'case-budget', 'requirement-mapping', 'repository-path'] as const)( 'repairs %s once with closed feedback and returns a specific non-ready recovery on repeated failure', async reason => { const query = jest.fn().mockResolvedValue({ markdown: 'Untrusted invalid document' }); diff --git a/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts b/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts index 4c0d6cdd6..75e1544dd 100644 --- a/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts +++ b/src/application/usecases/sdd/pre_branch_sdd_draft_workflow.ts @@ -8,7 +8,7 @@ import { type SddPlan, } from '../../../domain/pre_branch_sdd'; import { AgentResponseLengthFailure } from '../../../domain/agent_response_length_failure'; -import { SDD_CONSISTENCY_PROMPT } from '../../../domain/sdd_draft_consistency'; +import { renderSddDerivedTables, SDD_CONSISTENCY_PROMPT, SDD_DERIVED_TABLES_PROMPT } from '../../../domain/sdd_draft_consistency'; /** Size and typed format failures share one regeneration; the writer still validates every candidate. */ export async function draftSddMarkdown( @@ -33,7 +33,7 @@ export async function draftSddMarkdown( try { value = await agent.query({ configuration, agentId: 'pre-branch-sdd-draft', - prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${SDD_CONSISTENCY_PROMPT}`, + prompt: `${attempt === 0 ? contractPrompt : `${contractPrompt}\n\n${problem === 'size' ? `The previous response exceeded the ${maximum}-character draft limit.` : `The previous response failed the required SDD ${formatReason} contract.`} Regenerate a concise complete draft for this same owner and plan. Use the exact numbered canonical headings from the supplied template. Preserve all resolved requirements, architecture, documentation and acceptance. Do not change ownership or truncate a requirement.`}\n\n${SDD_CONSISTENCY_PROMPT}\n\n${SDD_DERIVED_TABLES_PROMPT}`, options: { expectJson: true, schemaName: 'pre_branch_sdd_draft', schema }, }); } catch (error) { @@ -50,8 +50,10 @@ export async function draftSddMarkdown( const markdown = (value as { markdown: string }).markdown; if (markdown.length > maximum) { problem = 'size'; continue; } try { - await validate?.(markdown); - return markdown; + const assembled = renderSddDerivedTables(markdown); + if (assembled.length > maximum) { problem = 'size'; continue; } + await validate?.(assembled); + return assembled; } catch (error) { if (!(error instanceof SddDraftFormatFailure)) throw error; problem = 'format'; diff --git a/src/domain/__tests__/sdd_derived_tables.test.ts b/src/domain/__tests__/sdd_derived_tables.test.ts new file mode 100644 index 000000000..95a035f23 --- /dev/null +++ b/src/domain/__tests__/sdd_derived_tables.test.ts @@ -0,0 +1,107 @@ +import { renderSddDerivedTables, validateSddDraftConsistency } from '../sdd_draft_consistency'; +import { sddConsistencyFixture } from '../../testing/sdd_consistency_fixture'; + +const budgetSlot = '{{SDD_CASE_BUDGET}}'; +const traceabilitySlot = '{{SDD_REQUIREMENT_CASES}}'; +const draft = () => sddConsistencyFixture() + .replace(/\| Area \| Minimum distinct cases[\s\S]*?(?=\| Case ID)/u, `${budgetSlot}\n\n`) + .replace(/\| Requirement \| Cases \|[\s\S]*$/u, `${traceabilitySlot}\n`); +const inventory = { files: new Set(['docs/payments.mdx']), directories: new Set(['docs']), blocked: new Set() }; +const validate = (markdown: string) => validateSddDraftConsistency(markdown, [], inventory, 'specs/change.md'); + +describe('SDD derived tables', () => { + it.each([['invalid legacy draft', 'legacy invalid draft'], ['valid complete draft', sddConsistencyFixture()]])('preserves bytes when slots are absent: %s', (_name, markdown) => { + expect(renderSddDerivedTables(markdown)).toBe(markdown); + }); + + it('derives exact area counts, total and reciprocal links without changing case definitions', () => { + const markdown = renderSddDerivedTables(draft()); + expect(markdown).toContain('| Policy | 2 |'); + expect(markdown).toContain('| Adapter | 1 |'); + expect(markdown).toContain('| Total | 3 |'); + expect(markdown).toContain('| G-01 | payments-change-C01, payments-change-C02 |'); + expect(markdown).toContain('| G-02 | payments-change-C03 |'); + expect(markdown).toContain('| payments-change-C03 | Adapter | G-02 | Publishing preserves the principal. |'); + expect(markdown).not.toContain('{{SDD_'); + expect(() => validate(markdown)).not.toThrow(); + }); + + it('compiles the 96-case seven-area distribution from #694 without independent arithmetic or reverse edges', () => { + const areas = [['F', 12], ['N', 14], ['A', 12], ['I', 16], ['V', 14], ['S', 16], ['E', 12]] as const; + const rows = areas.flatMap(([area, count]) => Array.from({ length: count }, (_, index) => + `| browser694-${area}-C${String(index + 1).padStart(2, '0')} | ${area} | G-01, G-02 | Observable ${area} behavior ${index + 1}. |`)); + const source = draft().replace(/\| payments-change-C01[\s\S]*?(?=\n## 15\.)/u, `${rows.join('\n')}\n`); + const markdown = renderSddDerivedTables(source); + for (const [area, count] of areas) expect(markdown).toContain(`| ${area} | ${count} |`); + expect(markdown).toContain('| Total | 96 |'); + const expectedIds = areas.flatMap(([area, count]) => Array.from({ length: count }, (_, index) => + `browser694-${area}-C${String(index + 1).padStart(2, '0')}`)).join(', '); + expect(markdown).toContain(`| G-01 | ${expectedIds} |`); + expect(markdown).toContain(`| G-02 | ${expectedIds} |`); + expect(() => validate(markdown)).not.toThrow(); + }); + + it('keeps a multi-requirement case in each reverse mapping and counts it only once', () => { + const markdown = renderSddDerivedTables(draft().replace('| Adapter | G-02 |', '| Adapter | G-01, G-02 |')); + expect(markdown).toContain('| G-01 | payments-change-C01, payments-change-C02, payments-change-C03 |'); + expect(markdown).toContain('| Total | 3 |'); + expect(() => validate(markdown)).not.toThrow(); + }); + + it('handles CRLF and emphasized definition cells through the same visible-table parser', () => { + const markdown = renderSddDerivedTables(draft().replace('| Policy | G-01 |', '| **Policy** | `G-01` |').replace(/\n/gu, '\r\n')); + expect(markdown).toContain('| Policy | 2 |'); + expect(() => validate(markdown)).not.toThrow(); + }); + + it.each([ + ['missing budget', (s: string) => s.replace(budgetSlot, ''), 'case-budget'], + ['missing reverse links', (s: string) => s.replace(traceabilitySlot, ''), 'requirement-mapping'], + ['duplicate budget', (s: string) => s.replace(budgetSlot, `${budgetSlot}\n${budgetSlot}`), 'case-budget'], + ['duplicate reverse links', (s: string) => s.replace(traceabilitySlot, `${traceabilitySlot}\n${traceabilitySlot}`), 'requirement-mapping'], + ['hidden duplicate budget', (s: string) => `${s}\n`, 'case-budget'], + ['hidden duplicate reverse links', (s: string) => `${s}\n`, 'requirement-mapping'], + ['wrong budget section', (s: string) => s.replace(budgetSlot, '').replace(traceabilitySlot, `${budgetSlot}\n${traceabilitySlot}`), 'case-budget'], + ['wrong reverse section', (s: string) => s.replace(traceabilitySlot, '').replace(budgetSlot, `${budgetSlot}\n${traceabilitySlot}`), 'requirement-mapping'], + ['fenced budget', (s: string) => s.replace(budgetSlot, `\`\`\`text\n${budgetSlot}\n\`\`\``), 'case-budget'], + ['fenced reverse links', (s: string) => s.replace(traceabilitySlot, `~~~text\n${traceabilitySlot}\n~~~`), 'requirement-mapping'], + ['commented budget', (s: string) => s.replace(budgetSlot, ``), 'case-budget'], + ['commented reverse links', (s: string) => s.replace(traceabilitySlot, ``), 'requirement-mapping'], + ['inline budget', (s: string) => s.replace(budgetSlot, `Example ${budgetSlot}`), 'case-budget'], + ['inline reverse links', (s: string) => s.replace(traceabilitySlot, `Example ${traceabilitySlot}`), 'requirement-mapping'], + ] as const)('rejects %s instead of interpreting an example or hiding a declaration', (_name, mutate, reason) => { + expect(() => renderSddDerivedTables(mutate(draft()))).toThrow(expect.objectContaining({ reason })); + }); + + it.each([ + ['missing requirements', (s: string) => s.replace('Requirement | Behavior', 'Goal | Behavior'), 'requirement-mapping'], + ['duplicate requirements', (s: string) => s.replace('| G-02 | Preserve', '| G-01 | Preserve'), 'requirement-mapping'], + ['invalid requirement ID', (s: string) => s.replace(/G-02/gu, 'goal'), 'requirement-mapping'], + ['missing inventory', (s: string) => s.replace('Case ID | Area', 'Test | Area'), 'case-identity'], + ['malformed inventory', (s: string) => s.replace('|---|---|---|---|', '|---|---|bad|---|'), 'case-identity'], + ['duplicate cases', (s: string) => s.replace(/payments-change-C02/gu, 'payments-change-C01'), 'case-identity'], + ['shorthand case', (s: string) => s.replace(/payments-change-C02/gu, 'C02'), 'case-identity'], + ['undefined requirement', (s: string) => s.replace('| Adapter | G-02 |', '| Adapter | G-09 |'), 'requirement-mapping'], + ['duplicate edge', (s: string) => s.replace('| Adapter | G-02 |', '| Adapter | G-02, G-02 |'), 'requirement-mapping'], + ['uncovered requirement', (s: string) => s.replace('| Adapter | G-02 |', '| Adapter | G-01 |'), 'requirement-mapping'], + ['reserved total area', (s: string) => s.replace('| Adapter | G-02 |', '| Total | G-02 |'), 'case-budget'], + ] as const)('rejects %s without inventing, dropping or relabeling a definition', (_name, mutate, reason) => { + expect(() => renderSddDerivedTables(mutate(draft()))).toThrow(expect.objectContaining({ reason })); + }); + + it.each([ + ['contradictory supplied budget', (s: string) => s.replace(budgetSlot, `${budgetSlot}\n| Area | Minimum distinct cases | Behaviors/risks covered |\n|---|---|---|\n| Total | 1 | Wrong |`), 'case-budget'], + ['contradictory supplied reverse links', (s: string) => s.replace(traceabilitySlot, `${traceabilitySlot}\n| Requirement | Cases |\n|---|---|\n| G-01 | payments-change-C03 |`), 'requirement-mapping'], + ['missing immutable path', (s: string) => s.replace('docs/payments.mdx', 'docs/absent.mdx'), 'repository-path'], + ['bare case alias in prose', (s: string) => `${s}\nSee C03.`, 'case-identity'], + ] as const)('preserves the writer rejection for %s', (_name, mutate, reason) => { + expect(() => validate(renderSddDerivedTables(mutate(draft())))).toThrow(expect.objectContaining({ reason })); + }); + + it('does not authorize a case identity reserved by an immutable owner', () => { + const markdown = renderSddDerivedTables(draft()); + expect(() => validateSddDraftConsistency(markdown, + [{ path: 'specs/owner.md', markdown: 'payments-change-C01 already owns a different behavior.' }], inventory, 'specs/change.md')) + .toThrow(expect.objectContaining({ reason: 'case-identity' })); + }); +}); diff --git a/src/domain/sdd_draft_consistency.ts b/src/domain/sdd_draft_consistency.ts index 060c4d176..f01b94964 100644 --- a/src/domain/sdd_draft_consistency.ts +++ b/src/domain/sdd_draft_consistency.ts @@ -17,6 +17,12 @@ Each row is one distinct executable case with a fully qualified ID such as capab 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, or relative Markdown file links. Put every planned new file in this table. These tables are publication consistency checks, not runtime/security acceptance.`; +const BUDGET_SLOT = '{{SDD_CASE_BUDGET}}'; +const TRACEABILITY_SLOT = '{{SDD_REQUIREMENT_CASES}}'; + +/** The provider writes definitions once; trusted code renders their two redundant projections. */ +export const SDD_DERIVED_TABLES_PROMPT = `Draft assembly: the consistency contract above describes the final document. In your raw Markdown, write the complete Requirement | Behavior table in section 4 and Case ID | Area | Requirements | Scenario inventory in section 14. Preserve every resolved minimum, per-area allocation and observable case as actual inventory rows, never ranges or summaries. Instead of independently writing the budget table, put ${BUDGET_SLOT} once on its own visible line in section 14. Instead of writing the reverse Requirement | Cases table, put ${TRACEABILITY_SLOT} once on its own visible line in section 17. Do not include either derived table yourself or put these slots in examples, fences or comments. Trusted code derives exact counts, Total and reciprocal links from your unchanged definitions, then runs every publication check. The compact character target is advisory: use more space when required to preserve the full resolved inventory and scope. The hard limit applies after assembly too.`; + function fail(reason: SddConsistencyReason): never { // Closed diagnostics: rejected Markdown and repository text never become repair instructions. throw new SddDraftFormatFailure(reason, `The SDD draft violates its ${reason} consistency contract.`); @@ -51,13 +57,17 @@ function visible(source: string): string { function clean(value: string): string { return value.trim().replace(/^(?:\*\*|__|`)(.*?)(?:\*\*|__|`)$/u, '$1').trim(); } -function table(source: string, section: number, headers: readonly string[], reason: SddConsistencyReason): string[][] { +function section(source: string, number: number, reason: SddConsistencyReason): string { const sections = [...source.matchAll(/^## (\d+)\. [^\n]*$/gmu)]; - const matches = sections.filter(heading => Number(heading[1]) === section); + const matches = sections.filter(heading => Number(heading[1]) === number); if (matches.length !== 1) return fail(reason); const start = matches[0]; const end = sections.find(heading => heading.index > start.index)?.index ?? source.length; - const lines = source.slice(start.index + start[0].length, end).split('\n'); + return source.slice(start.index + start[0].length, end); +} + +function table(source: string, number: number, headers: readonly string[], reason: SddConsistencyReason): string[][] { + const lines = section(source, number, reason).split('\n'); const found: string[][][] = []; for (let index = 0; index < lines.length; index++) { if (!/^\s*\|/u.test(lines[index])) continue; @@ -84,6 +94,41 @@ function ids(value: string, pattern: RegExp, reason: SddConsistencyReason): stri return values; } +/** Never repairs definitions or contradictory supplied tables; the writer independently revalidates. */ +export function renderSddDerivedTables(markdown: string): string { + if (!markdown.includes(BUDGET_SLOT) && !markdown.includes(TRACEABILITY_SLOT)) return markdown; + const source = visible(markdown); + for (const [slot, number, reason] of [ + [BUDGET_SLOT, 14, 'case-budget'], [TRACEABILITY_SLOT, 17, 'requirement-mapping'], + ] as const) { + if (markdown.split(slot).length !== 2 + || section(source, number, reason).split('\n').filter(line => line.trim() === slot).length !== 1) fail(reason); + } + const requirements = table(source, 4, ['Requirement', 'Behavior'], 'requirement-mapping').map(row => row[0]); + if (new Set(requirements).size !== requirements.length || requirements.some(id => !REQUIREMENT_ID.test(id))) fail('requirement-mapping'); + const cases = table(source, 14, ['Case ID', 'Area', 'Requirements', 'Scenario'], 'case-identity'); + const caseIds = cases.map(row => row[0]); + if (new Set(caseIds).size !== caseIds.length || caseIds.some(id => !CASE_ID.test(id))) fail('case-identity'); + const counts = new Map(); + const edges = new Map(requirements.map(id => [id, [] as string[]])); + for (const [id, area, links] of cases) { + if (area === 'Total') fail('case-budget'); + counts.set(area, (counts.get(area) ?? 0) + 1); + for (const requirement of ids(links, REQUIREMENT_ID, 'requirement-mapping')) { + const linked = edges.get(requirement); + if (!linked) fail('requirement-mapping'); + linked.push(id); + } + } + if ([...edges.values()].some(links => links.length === 0)) fail('requirement-mapping'); + const budget = ['| Area | Minimum distinct cases | Behaviors/risks covered |', '|---|---:|---|', + ...[...counts].map(([area, count]) => `| ${area} | ${count} | Observable scenarios in the case inventory. |`), + `| Total | ${cases.length} | Distinct inventory cases; repeated executions do not add cases. |`].join('\n'); + const traceability = ['| Requirement | Cases |', '|---|---|', + ...[...edges].map(([requirement, links]) => `| ${requirement} | ${links.join(', ')} |`)].join('\n'); + return markdown.replace(BUDGET_SLOT, () => budget).replace(TRACEABILITY_SLOT, () => traceability); +} + export function reservedSddCaseIds(documents: readonly SddOwnerDocument[]): string[] { const reserved = new Set(); for (const document of documents) { From 2a6a43789609acd679f54597707f5fdb31d547b5 Mon Sep 17 00:00:00 2001 From: Efra Espada Date: Sun, 25 Oct 2026 12:24:37 +0100 Subject: [PATCH 2/2] hotfix-3.10.5: prepare hotfix 3.10.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3fbd08712..2a335a086 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vypdev/copilot", - "version": "3.10.4", + "version": "3.10.5", "packageManager": "pnpm@10.12.4", "pnpm": { "overrides": {