@bridge_gpt/mcp-server 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@ export const COMMANDS = {
15
15
  "plan-ticket.md": "Generate an implementation plan for a Jira ticket, wait for the result, and save it locally.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nExecute all steps in this command as a simple linear sequence of MCP tool calls.\n\n## Step 1 — Parse Arguments\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = \"auto\"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: \"Usage error: --provider requires a provider name (openai, anthropic, or gemini).\"\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n - If `ticket_key` is empty or missing, stop immediately and display:\n\n ```\n Usage: /plan-ticket <ticket_key> [--second-opinion [provider]] [--provider <name>] (e.g., /plan-ticket BAPI-150)\n ```\n\n## Step 2 — Resolve Docs Directory\n\nCall the `get_docs_dir` MCP tool (no parameters). Store the returned path as `docs_dir`.\n\n## Step 3 — Generate Plan\n\nCall the `request_plan_generation` MCP tool with:\n- `ticket_number`: the parsed `ticket_key`\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if non-null; omit entirely if null\n- `provider`: set to `provider_value` if non-null; omit entirely if null\n\nThis step may take 1-5 minutes while the backend processes the plan.\n\nIf the tool returns an error, stop immediately and display:\n\n```\nPlan generation failed: <error message from the tool>\n```\n\n## Step 4 — Confirm Success\n\nDisplay a confirmation message:\n\n```\nPlan generated successfully for <ticket_key>\nSaved to: {docs_dir}/plans/<ticket_key>-plan.md\n```\n\n## Final Summary\n\nDisplay a summary block:\n\n```\n## Plan Generation Report\n\n- **Ticket**: <ticket_key>\n- **Plan Status**: Generated successfully\n- **Local File**: {docs_dir}/plans/<ticket_key>-plan.md\n```\n\nOn failure at any step, stop immediately, display which step failed and the error details, and do not proceed.\n",
16
16
  "reimplement-ticket.md": "# Reimplement Ticket: $ARGUMENTS\n\n$ARGUMENTS\n\nThis command retrieves the reimplement context for a previously-implemented Jira ticket via MCP, then implements follow-up changes inline. Use this for small follow-up requests on tickets that have already been through the plan+implement cycle.\n\nIf any critical stage fails (Stage 0 or Stage 1), stop immediately and report which stage failed and why.\n\n---\n\n# Instructions\n\nYou are executing a 4-stage pipeline to implement follow-up changes on a Jira ticket using assembled reimplement context. Execute all stages in sequence.\n\n## Stage 0 — Setup and Argument Parsing\n\n1. **Parse `$ARGUMENTS`**: Extract a required `ticket_key`, an optional `--second-opinion` flag, and an optional `--provider` flag.\n - Split `$ARGUMENTS` on whitespace.\n - If `--second-opinion` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `second_opinion_value`.\n - If `--second-opinion` appears without a provider name following it (or is the last token), set `second_opinion_value = \"auto\"`.\n - If `--second-opinion` is absent, set `second_opinion_value = null`.\n - If `--provider` appears followed by a provider name (one of `openai`, `anthropic`, `gemini`), capture that provider as `provider_value`.\n - If `--provider` appears without a valid provider name following it (or is the last token), stop immediately and report: \"Usage error: --provider requires a provider name (openai, anthropic, or gemini).\"\n - If `--provider` is absent, set `provider_value = null`.\n - If both `--second-opinion` and `--provider` are present, `--second-opinion` takes precedence (set `provider_value = null`).\n - The remaining token (after removing flags and their arguments) is the `ticket_key`.\n - If `ticket_key` is empty or does not match the expected format (one or more uppercase letters, a hyphen, and one or more digits), stop immediately and display:\n\n ```\n Invalid ticket key format: '<value>'. Expected format: PROJ-123 (uppercase letters, hyphen, digits).\n Usage: /reimplement-ticket <ticket_key> [--second-opinion [provider]] [--provider <name>] (e.g., /reimplement-ticket BAPI-150)\n ```\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 1.\n\n## Stage 1 — Request and Retrieve Reimplement Context\n\nCall the `request_reimplement_context` MCP tool with:\n- `ticket_number`: the parsed `ticket_key` from Stage 0\n- `wait_for_result`: `true`\n- `save_locally`: `true`\n- `second_opinion`: set to `second_opinion_value` if it is non-null; omit the parameter entirely if `second_opinion_value` is null\n- `provider`: set to `provider_value` if it is non-null; omit the parameter entirely if `provider_value` is null\n\nIf the tool returns an error or 404 persists after polling, stop immediately and display:\n\n```\nFailed to retrieve reimplement context for <ticket_key>.\nThis may mean:\n- The ticket has not been previously processed by Bridge API\n- Background processing failed — check server logs\n- The ticket does not exist in Jira\n\nTry running /plan-ticket <ticket_key> first if this is a new ticket.\n```\n\nOn success, read and internalize the returned context markdown. This document contains:\n- A summary of changes (if applicable)\n- New/changed information since last processing (comments, description changes, attachments)\n- The original ticket description\n- The existing implementation plan (at the bottom, for reference only)\n\nThis stage is **critical** — stop immediately on failure. Do not proceed to Stage 2.\n\n## Stage 2 — Implement Follow-Up Changes\n\nExecute changes inline in this conversation. Work directly so the user can see all progress and approve tool calls.\n\nFollow these rules:\n\n1. **Focus on the new information.** The context document identifies what has changed since the last implementation. Focus your changes on addressing the new/changed requirements.\n2. **Reference the existing plan as supplementary guidance only.** The plan at the bottom of the context describes the original implementation, not the follow-up. Use it to understand the existing code structure, not as a step-by-step guide.\n3. **Make code changes** as directed by the new information.\n4. **Run tests and checks** to verify your changes don't break existing functionality.\n5. **Do NOT run `git commit` or `git push`.** Leave all changes uncommitted for developer review.\n6. **Scope guard**: If the follow-up changes are too large in scope (e.g., fundamentally restructuring the original implementation, touching more than 5-6 files, or requiring new infrastructure), stop and ask the user for guidance rather than attempting everything. Follow-up reimplementations should be small and targeted.\n7. **If a change is ambiguous or blocked**, note the issue clearly and continue with the next change rather than halting entirely.\n\nThis stage is **critical** — if a blocking error prevents further progress, stop and report the failure.\n\n## Stage 3 — Final Summary Report\n\nDisplay a structured report after all stages complete:\n\n```\n## Reimplement Complete\n\n**Ticket**: <ticket_key>\n\n**Changes Made**:\n- <brief summary of each change>\n\n**Developer Action Items**:\n- All changes are uncommitted. Review the changes with `git diff` before committing.\n- Run the project's test suite to verify nothing is broken before committing.\n\n**Warnings**:\n<If any issues arose during implementation (scope concerns, ambiguous requirements,\nfiles that couldn't be modified), list them here. If no warnings, omit this section.>\n```\n\n## Final Report\n\nOn success, display the structured report from Stage 3 confirming that the follow-up changes are complete.\n\nOn failure at any critical stage (Stage 0 or Stage 1), display which stage failed and the error details.\n",
17
17
  "review-ticket.md": "# Review Ticket\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command is recipe-driven. Do not call MCP tools directly -- the recipe determines which tools to call and with what parameters.\n\n1. Parse `$ARGUMENTS` to extract a single required `ticket_key` (e.g., `BAPI-123`). If `$ARGUMENTS` is empty or does not match the Jira key pattern (`[A-Z][A-Z0-9]+-\\d+`), stop immediately and display:\n ```\n Invalid ticket key format. Expected: PROJ-123\n Usage: /review-ticket <ticket_key>\n ```\n\n2. Call the `get_pipeline_recipe` MCP tool with:\n - `pipeline`: `\"review-ticket\"`\n - `variables`: `{ \"ticket_key\": \"<ticket_key>\" }`\n\n If the tool returns an error, stop and report the failure.\n\n3. Read and strictly obey the `agent_instructions` field in the response. Execute each step in order, announcing each as **Step N of M: <description>**.\n\n4. After all steps complete, display a summary:\n ```\n ## Pipeline Complete\n\n **Ticket**: <ticket_key>\n **Steps executed**: N of M\n **Status**: Success / Failed at step N\n ```\n",
18
- "run-tests.md": "Run the project's full test suite (unit, integration, and Playwright E2E), triage failures, fix test-code issues, and produce a structured health-check report in docs/tmp/testing/.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n## Stage 0 — Argument Parsing and Setup\n\n1. **Parse `$ARGUMENTS`** for optional flags. Supported flags:\n - `--skip-integration` — skip integration tests (they make real LLM calls)\n - `--skip-playwright` — skip Playwright E2E tests (they require a running server)\n - `--unit-only` — shorthand that implies both `--skip-integration` and `--skip-playwright`\n\n Resolve flags to boolean variables:\n - Start with: `run_integration = true`, `run_playwright = true`\n - If `--unit-only` is present: set both to `false`\n - If `--skip-integration` is present: set `run_integration = false`\n - If `--skip-playwright` is present: set `run_playwright = false`\n - Unknown flags: note them in the final report as \"Unrecognized flag ignored\" but do not fail\n\n2. **Generate a run timestamp** using the current date and time in `YYYY-MM-DD-HH-MM` format (e.g., `2026-03-10-14-35`). Store this as `run_timestamp`. Both output documents will use this value.\n\nThis stage has no failure conditions — proceed to Stage 1.\n\n## Stage 1 — Environment Setup\n\nRun the following command in the terminal:\n```\nmkdir -p docs/tmp/testing/\n```\nIf this fails, stop immediately and report: \"Cannot create output directory docs/tmp/testing/ — check permissions.\"\n\nVerify the Python venv is available:\n```\nsource .venv/bin/activate && python --version\n```\nIf the venv is not available, note \"venv not available\" in the report and skip unit and integration tests. Continue to Playwright stage if applicable.\n\n## Stage 2 — Unit Tests\n\nRun in the terminal:\n```\nsource .venv/bin/activate && pytest tests/pytest/ -v --tb=short\n```\n\nCapture the full output. Extract the pytest summary line (e.g., `47 passed, 3 failed in 12.4s`). For each failing test, apply the triage logic below, then record the result.\n\n## Stage 3 — Integration Tests\n\nIf `run_integration` is `false`, skip this stage and record: `Integration tests: SKIPPED (--skip-integration or --unit-only flag was set)`\n\nIf `run_integration` is `true`, run each subdirectory as a separate batch. **Continue to the next batch even if the current one has failures.**\n\nRun the following batches in order in the terminal:\n```\nsource .venv/bin/activate && pytest tests/integration/code_writer/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/estimator/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/ticket_updater/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/routes/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/db/ --run-integration -v -s\n```\n\nApply triage logic to any failures. Note: integration test failures involve real LLM behavior and are more likely to be implementation issues than test-code issues — apply extra caution before fixing.\n\n## Stage 4 — Playwright E2E Tests\n\nIf `run_playwright` is `false`, skip this stage and record: `Playwright tests: SKIPPED (--skip-playwright or --unit-only flag was set)`\n\nIf `run_playwright` is `true`, first check whether the server is running:\n```\ncurl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/ping\n```\n\nIf the status code is NOT `200`, skip Playwright and record:\n```\nPlaywright tests: SKIPPED — server not running.\nStart the server with: uvicorn main:app --reload\nThen re-run this command without --skip-playwright.\n```\n\nIf the server IS running, run each spec directory as a separate batch. **Continue to the next batch even if the current one has failures.**\n\n```\nnpx playwright test tests/playwright/setup/ --config=tests/playwright/playwright.config.js --reporter=list\nnpx playwright test tests/playwright/projects/ --config=tests/playwright/playwright.config.js --reporter=list\nnpx playwright test tests/playwright/optimize/ --config=tests/playwright/playwright.config.js --reporter=list\n```\n\nApply triage logic to any failures.\n\n## Triage Logic\n\nFor every failing test, examine the test file and the code it tests. Classify as ONE of the following:\n\n### TEST-CODE ISSUE — fix it directly\n\nClassify as a test-code issue if ANY of the following applies:\n- The test asserts against a hardcoded value that no longer matches current behavior (outdated mock data)\n- The test imports or calls a function that was renamed, moved, or removed\n- The test asserts on a response field that was restructured\n- The test expects a specific error message string that has since changed\n- A fixture or conftest references a removed table column or model field\n\n**Action**: Apply a minimal, targeted fix to the test file only. Then re-run just that failing test to confirm:\n```\nsource .venv/bin/activate && pytest path/to/test_file.py::test_function_name -v --tb=short\n```\nIf the re-run **still fails** after your fix, do not make further edits — escalate to implementation-code issue instead and revert your change.\n\n### IMPLEMENTATION-CODE ISSUE (or UNCERTAIN) — document it, do not fix\n\nClassify as an implementation issue if ANY of the following applies:\n- The production function raises an unexpected exception\n- A route handler returns the wrong status code for a documented behavior\n- Business logic produces incorrect output that the test correctly asserts against\n- You are not confident the test is wrong\n\n**Action**: Do NOT touch any files in `api/`, `src/`, `main.py`, or `db/`. Record this issue in the implementation-issues document.\n\n## Stage 5 — Write Output Documents\n\n### Document 1: Test Run Report (always write this)\n\nWrite to: `docs/tmp/testing/test-run-{run_timestamp}.md`\n\n```markdown\n# Test Run: {run_timestamp}\n\n## Configuration\n- Unit tests: RUN\n- Integration tests: RUN / SKIPPED — (reason)\n- Playwright tests: RUN / SKIPPED — (reason)\n\n## Unit Tests\n**Result**: X passed, Y failed\n**Duration**: ~Ns\n**Fixes applied**:\n- `path/to/test_file.py`: brief description of what was fixed\n- (or \"none\" if no fixes were needed)\n\n**Failures escalated as implementation issues**: N\n\n## Integration Tests\n\n### tests/integration/code_writer/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/estimator/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/ticket_updater/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/routes/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/db/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n## Playwright Tests\n\n### tests/playwright/setup/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/playwright/projects/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/playwright/optimize/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n## Overall Summary\n- Total test fixes applied: N\n- Suspected implementation issues found: N\n- Implementation issues document: docs/tmp/testing/implementation-issues-{run_timestamp}.md\n (or \"not created — no issues found\")\n```\n\n### Document 2: Implementation Issues (only write if issues were found)\n\nIf at least one failure was escalated as an implementation-code issue, write to:\n`docs/tmp/testing/implementation-issues-{run_timestamp}.md`\n\n```markdown\n# Suspected Implementation Issues: {run_timestamp}\n\nThese test failures were NOT fixed. They may indicate bugs in production code.\nA developer should investigate each item before merging.\n\n## Issue 1\n- **Test**: `path/to/test_file.py::test_function_name`\n- **Tier**: unit / integration / playwright\n- **Failure message**: (paste the key assertion or exception line)\n- **Why not fixed**: (brief reasoning, e.g., \"production function raises KeyError on valid input\")\n\n## Issue 2\n...\n```\n\nIf no implementation issues were found, do NOT create this file.\n\n## Final Output\n\nAfter writing all documents, print this summary:\n\n```\nTest run complete: {run_timestamp}\nReport saved to: docs/tmp/testing/test-run-{run_timestamp}.md\nImplementation issues: docs/tmp/testing/implementation-issues-{run_timestamp}.md (if applicable)\nNo suspected implementation issues found. (if none)\n```\n",
18
+ "run-tests.md": "Run the project's full test suite (unit, integration, and Playwright E2E), triage failures, fix test-code issues, and produce a structured health-check report in docs/tmp/testing/.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\n## Stage 0 — Argument Parsing and Setup\n\n1. **Parse `$ARGUMENTS`** for optional flags. Supported flags:\n - `--skip-integration` — skip integration tests (they make real LLM calls)\n - `--skip-playwright` — skip Playwright E2E tests (they require a running server)\n - `--unit-only` — shorthand that implies both `--skip-integration` and `--skip-playwright`\n\n Resolve flags to boolean variables:\n - Start with: `run_integration = true`, `run_playwright = true`\n - If `--unit-only` is present: set both to `false`\n - If `--skip-integration` is present: set `run_integration = false`\n - If `--skip-playwright` is present: set `run_playwright = false`\n - Unknown flags: note them in the final report as \"Unrecognized flag ignored\" but do not fail\n\n2. **Generate a run timestamp** using the current date and time in `YYYY-MM-DD-HH-MM` format (e.g., `2026-03-10-14-35`). Store this as `run_timestamp`. Both output documents will use this value.\n\nThis stage has no failure conditions — proceed to Stage 1.\n\n## Stage 1 — Environment Setup\n\nRun the following command in the terminal:\n```\nmkdir -p docs/tmp/testing/\n```\nIf this fails, stop immediately and report: \"Cannot create output directory docs/tmp/testing/ — check permissions.\"\n\nVerify the Python venv is available:\n```\nsource .venv/bin/activate && python --version\n```\nIf the venv is not available, note \"venv not available\" in the report and skip unit and integration tests. Continue to Playwright stage if applicable.\n\n## Stage 2 — Unit Tests\n\nRun in the terminal:\n```\nsource .venv/bin/activate && pytest tests/pytest/ -v --tb=short\n```\n\nCapture the full output. Extract the pytest summary line (e.g., `47 passed, 3 failed in 12.4s`). For each failing test, apply the triage logic below, then record the result.\n\n## Stage 3 — Integration Tests\n\nIf `run_integration` is `false`, skip this stage and record: `Integration tests: SKIPPED (--skip-integration or --unit-only flag was set)`\n\nIf `run_integration` is `true`, run each subdirectory as a separate batch. **Continue to the next batch even if the current one has failures.**\n\nRun the following batches in order in the terminal:\n```\nsource .venv/bin/activate && pytest tests/integration/code_writer/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/estimator/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/ticket_updater/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/routes/ --run-integration -v -s\nsource .venv/bin/activate && pytest tests/integration/db/ --run-integration -v -s\n```\n\nApply triage logic to any failures. Note: integration test failures involve real LLM behavior and are more likely to be implementation issues than test-code issues — apply extra caution before fixing.\n\n## Stage 4 — Playwright E2E Tests\n\nIf `run_playwright` is `false`, skip this stage and record: `Playwright tests: SKIPPED (--skip-playwright or --unit-only flag was set)`\n\nIf `run_playwright` is `true`, first check whether the server is running:\n```\ncurl -s -o /dev/null -w \"%{http_code}\" http://localhost:8000/\n```\n\nIf the status code is NOT `200`, skip Playwright and record:\n```\nPlaywright tests: SKIPPED — server not running.\nStart the server with: uvicorn main:app --reload\nThen re-run this command without --skip-playwright.\n```\n\nIf the server IS running, run each spec directory as a separate batch. **Continue to the next batch even if the current one has failures.**\n\n```\nnpx playwright test tests/playwright/setup/ --config=tests/playwright/playwright.config.js --reporter=list\nnpx playwright test tests/playwright/projects/ --config=tests/playwright/playwright.config.js --reporter=list\nnpx playwright test tests/playwright/optimize/ --config=tests/playwright/playwright.config.js --reporter=list\n```\n\nApply triage logic to any failures.\n\n## Triage Logic\n\nFor every failing test, examine the test file and the code it tests. Classify as ONE of the following:\n\n### TEST-CODE ISSUE — fix it directly\n\nClassify as a test-code issue if ANY of the following applies:\n- The test asserts against a hardcoded value that no longer matches current behavior (outdated mock data)\n- The test imports or calls a function that was renamed, moved, or removed\n- The test asserts on a response field that was restructured\n- The test expects a specific error message string that has since changed\n- A fixture or conftest references a removed table column or model field\n\n**Action**: Apply a minimal, targeted fix to the test file only. Then re-run just that failing test to confirm:\n```\nsource .venv/bin/activate && pytest path/to/test_file.py::test_function_name -v --tb=short\n```\nIf the re-run **still fails** after your fix, do not make further edits — escalate to implementation-code issue instead and revert your change.\n\n### IMPLEMENTATION-CODE ISSUE (or UNCERTAIN) — document it, do not fix\n\nClassify as an implementation issue if ANY of the following applies:\n- The production function raises an unexpected exception\n- A route handler returns the wrong status code for a documented behavior\n- Business logic produces incorrect output that the test correctly asserts against\n- You are not confident the test is wrong\n\n**Action**: Do NOT touch any files in `api/`, `src/`, `main.py`, or `db/`. Record this issue in the implementation-issues document.\n\n## Stage 5 — Write Output Documents\n\n### Document 1: Test Run Report (always write this)\n\nWrite to: `docs/tmp/testing/test-run-{run_timestamp}.md`\n\n```markdown\n# Test Run: {run_timestamp}\n\n## Configuration\n- Unit tests: RUN\n- Integration tests: RUN / SKIPPED — (reason)\n- Playwright tests: RUN / SKIPPED — (reason)\n\n## Unit Tests\n**Result**: X passed, Y failed\n**Duration**: ~Ns\n**Fixes applied**:\n- `path/to/test_file.py`: brief description of what was fixed\n- (or \"none\" if no fixes were needed)\n\n**Failures escalated as implementation issues**: N\n\n## Integration Tests\n\n### tests/integration/code_writer/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/estimator/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/ticket_updater/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/routes/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/integration/db/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n## Playwright Tests\n\n### tests/playwright/setup/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/playwright/projects/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n### tests/playwright/optimize/\n**Result**: X passed, Y failed\n**Fixes applied**: ...\n\n## Overall Summary\n- Total test fixes applied: N\n- Suspected implementation issues found: N\n- Implementation issues document: docs/tmp/testing/implementation-issues-{run_timestamp}.md\n (or \"not created — no issues found\")\n```\n\n### Document 2: Implementation Issues (only write if issues were found)\n\nIf at least one failure was escalated as an implementation-code issue, write to:\n`docs/tmp/testing/implementation-issues-{run_timestamp}.md`\n\n```markdown\n# Suspected Implementation Issues: {run_timestamp}\n\nThese test failures were NOT fixed. They may indicate bugs in production code.\nA developer should investigate each item before merging.\n\n## Issue 1\n- **Test**: `path/to/test_file.py::test_function_name`\n- **Tier**: unit / integration / playwright\n- **Failure message**: (paste the key assertion or exception line)\n- **Why not fixed**: (brief reasoning, e.g., \"production function raises KeyError on valid input\")\n\n## Issue 2\n...\n```\n\nIf no implementation issues were found, do NOT create this file.\n\n## Final Output\n\nAfter writing all documents, print this summary:\n\n```\nTest run complete: {run_timestamp}\nReport saved to: docs/tmp/testing/test-run-{run_timestamp}.md\nImplementation issues: docs/tmp/testing/implementation-issues-{run_timestamp}.md (if applicable)\nNo suspected implementation issues found. (if none)\n```\n",
19
19
  "scan-tickets.md": "$ARGUMENTS\n\n---\n\n# Instructions\n\nSynchronize recently-updated Jira tickets with the local `tickets` database table and backfill missing workflow state timestamps. Perform all work directly in the main thread.\n\n## Stage 0 — Parse Arguments and Calculate Date\n\n1. Read the value of `$ARGUMENTS`. If it is empty, whitespace-only, or not a valid integer, default `months_back` to `3`. If it contains multiple tokens, extract only the first token and attempt to parse it as an integer. If parsing fails, default to `3`.\n\n2. Calculate `updated_since` by subtracting `months_back` months from today's date. Format the result as `YYYY-MM-DD`. Example: if today is 2026-03-07 and `months_back` is 3, then `updated_since` is 2025-12-07.\n\n3. Display the parsed values: \"Scanning tickets updated since {updated_since} (months_back = {months_back})\"\n\n4. Initialize the following tracking variables:\n - `tickets_scanned` = 0 (total tickets fetched from Jira)\n - `newly_tracked` = 0 (tickets inserted into database for the first time)\n - `state_updated_list` = [] (list of objects with ticket key and fields updated)\n - `warnings` = [] (list of warning strings for any per-ticket failures)\n\n## Stage 1 — Fetch All Tickets from Jira\n\n1. Initialize an empty list `all_tickets` and set `offset` to `0`.\n\n2. Enter a pagination loop:\n - Call the `get_tickets` MCP tool with: `updated_since` set to the calculated date, `limit` set to `100`, and `offset` set to the current offset value.\n - Parse the JSON response. The response contains a `tickets` array of ticket objects. Each ticket object has a `ticket_number` field (the Jira key, e.g., `BAPI-42`), along with `summary`, `status`, `issue_type`, `assignee`, and `updated_at`.\n - Append all tickets from the response's `tickets` array to `all_tickets`.\n - If the number of tickets returned in this page equals `100`, increment `offset` by `100` and repeat the loop.\n - If fewer than `100` tickets are returned, exit the loop.\n\n3. Set `tickets_scanned` to the length of `all_tickets`.\n\n4. Display: \"Fetched {tickets_scanned} tickets from Jira. Processing...\"\n\n5. If the `get_tickets` call fails at any point during pagination, **stop** and report the error. Do not proceed to Stage 2.\n\n## Stage 2 — Track Each Ticket\n\n1. Iterate over each ticket in `all_tickets`. For each ticket:\n - Call the `track_ticket` MCP tool with `ticket_number` set to the ticket's `ticket_number` field. If the ticket object includes a `summary` field, pass it as the `description` parameter.\n - Inspect the response message. If the response indicates the ticket was newly created/inserted (look for words like \"created\" or \"inserted\" in the message, as opposed to \"already exists\" or \"updated\"), increment `newly_tracked` by 1.\n - If the `track_ticket` call fails for this ticket, add a warning to the `warnings` list (e.g., \"Warning: Failed to track ticket {ticket_number}: {error}\") and **continue** to the next ticket. Do not abort the scan.\n\n2. Display a brief progress indicator every 25 tickets, e.g., \"Tracked {N} of {tickets_scanned} tickets...\"\n\n## Stage 3 — Detect and Backfill Workflow State\n\nDisplay: \"Checking workflow state for {tickets_scanned} tickets...\"\n\nIterate over each ticket in `all_tickets`. For each ticket (referenced by its `ticket_number` field), perform the following sub-steps. Wrap the entire per-ticket block in error handling: if the `get_ticket_state` call or the subsequent `update_ticket_state` call fails for a ticket, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4a — Retrieve current state**: Call the `get_ticket_state` MCP tool with `ticket_number` set to the ticket's key. The response contains:\n\n- Five timestamp fields (each is a timestamp string or null): `clarify_called`, `clarify_answered`, `critique_called`, `critique_answered`, `plan_generated`\n- Three boolean artifact flags: `has_clarifying_questions`, `has_critique`, `has_plan`\n\nIf the call returns a 404 or any error, add a warning to `warnings` and continue to the next ticket.\n\n**Sub-step 4b — Build fields_to_update list**: Initialize an empty `fields_to_update` list, then apply the following rules:\n\n- If `has_clarifying_questions` is `true` AND `clarify_called` is null -> add `\"clarify_called\"` to `fields_to_update`\n- If `has_clarifying_questions` is `true` AND `clarify_answered` is null -> add `\"clarify_answered\"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_called` is null -> add `\"critique_called\"` to `fields_to_update`\n- If `has_critique` is `true` AND `critique_answered` is null -> add `\"critique_answered\"` to `fields_to_update`\n- If `has_plan` is `true` AND `plan_generated` is null -> add `\"plan_generated\"` to `fields_to_update`\n\n**Sub-step 4c — Call update_ticket_state if needed**: If `fields_to_update` is non-empty, call the `update_ticket_state` MCP tool with `ticket_number` set to the ticket's key and `fields` set to the `fields_to_update` array. If this succeeds, add an entry to `state_updated_list` recording the ticket key and the list of fields that were set. If `update_ticket_state` fails, add a warning to `warnings` and continue.\n\nDisplay a progress indicator every 25 tickets that includes the current ticket key, e.g., \"Checked state for {TICKET-KEY} ({N} of {tickets_scanned} tickets)\"\n\n## Stage 4 — Report Summary\n\n1. Calculate `state_updated_count` as the length of `state_updated_list`.\n\n2. Display the summary:\n\n ```\n **Scan complete**\n\n * Tickets scanned: {tickets_scanned}\n * Newly tracked: {newly_tracked}\n * State updated: {state_updated_count}\n ```\n\n3. If `state_updated_list` is non-empty, display a section titled \"Updated tickets:\" with one bullet per ticket showing the ticket key and the comma-separated list of fields that were set. Example:\n\n ```\n Updated tickets:\n * BAPI-101: clarify_called, clarify_answered\n * BAPI-105: critique_called, critique_answered, plan_generated\n ```\n\n4. If the `warnings` list is non-empty, display a section titled \"Warnings:\" listing each warning string as a bullet. Example:\n\n ```\n Warnings:\n * Warning: Failed to track ticket BAPI-99: Connection timeout\n * Warning: State query failed for BAPI-112: SQL error\n ```\n\n5. If there are no warnings, do not display the \"Warnings:\" section.\n",
20
20
  "teach-bridge.md": "Update a Bridge API configuration field via a natural-language teaching.\n\n$ARGUMENTS\n\n---\n\n# Instructions\n\nThis command takes a natural-language teaching (e.g., \"use data-testid selectors in Playwright tests\") and updates the appropriate Bridge API configuration field. The teaching is auto-classified to the correct field, merged with existing content as actionable AI instructions, and uploaded after user confirmation.\n\n`$ARGUMENTS` is required — it is the teaching text. If `$ARGUMENTS` is empty, show:\n\n```\nUsage: /teach-bridge <teaching>\n\nExamples:\n /teach-bridge use data-testid selectors in Playwright tests\n /teach-bridge always validate input DTOs with Pydantic before passing to service layer\n /teach-bridge prefer composition over inheritance for service classes\n```\n\nIf any stage fails, stop immediately and report which stage failed and why.\n\n## Stage 0 — Preflight\n\n1. **Validate arguments**: If `$ARGUMENTS` is empty or contains only whitespace, display the usage instructions above and stop.\n\n2. **Admin check**: Call the `get_my_role` MCP tool (no parameters). Inspect the response:\n - If `role` is `\"admin\"` OR `source` is `\"legacy\"`: proceed normally.\n - Otherwise: stop immediately and display:\n ```\n Admin access required. Your API key has role \"<role>\" (source: <source>).\n Only admin keys and legacy shared keys can update configuration fields.\n Contact your project administrator to request admin access.\n ```\n\nIf this stage fails, stop immediately and report the error. Do not proceed to Stage 1.\n\n## Stage 1 — Classify\n\n1. **List available fields**: Call the `list_config_fields` MCP tool (no parameters). This returns all available configuration field names with descriptions.\n\n2. **Evaluate the teaching**: Compare the user's teaching (`$ARGUMENTS`) against each field's description to determine which field it applies to.\n\n3. **Handle classification outcomes**:\n - **Clear single match**: If one field is clearly the best target, proceed to Stage 2 with that field.\n - **Multiple plausible matches**: If 2-3 fields are equally plausible, present them to the user with their descriptions and ask which one to update. Wait for user input before proceeding.\n - **No confident match**: If you cannot confidently map the teaching to any field, ask the user to elaborate or specify which field they intend. Wait for user input before proceeding.\n\n## Stage 2 — Merge\n\n1. **Read current value**: Call the `get_config_field` MCP tool with `field_name` set to the selected field from Stage 1. Capture the current value, description, and examples from the response.\n\n2. **Draft the update**:\n - **If the field is currently null or empty**: Compose initial content from the teaching. Rephrase the user's input as imperative, agent-facing instructions (e.g., convert \"I want you to use data-testid\" to \"Always use `data-testid` attributes for Playwright element locators\"). Do not use the user's exact conversational text.\n - **If the field has existing content**: Merge the teaching into the existing value at the most appropriate location. Rephrase as imperative, agent-facing instructions. Preserve the existing structure and formatting.\n\n3. **Handle contradictions**: If the teaching contradicts existing instructions in the field, present both the existing instruction and the new teaching side-by-side and ask the user which should take precedence. Wait for user input before proceeding.\n\n## Stage 3 — Confirm and Upload\n\n1. **Show the proposed update**: Display to the user:\n - **Field**: The name of the field being updated\n - **Change summary**: A brief description of what was added or changed\n - **Full proposed value**: The complete new value for the field (not just the diff)\n\n2. **Wait for confirmation**: Ask the user to confirm, request edits, or abort.\n\n3. **On confirmation**: Call the `update_config_field` MCP tool with:\n - `field_name`: the selected field name\n - `value`: the full merged value (pass inline, do not use `file_path`)\n\n Display a success message confirming the update.\n\n4. **On rejection**: Ask the user what they'd like to change. If they provide edits, revise the proposed value and show it again. If they abort, stop without making any changes.\n"
21
21
  };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Decision Page schema
3
+ *
4
+ * Zod schema for the `generate_decision_page` MCP tool's actionable-item contract.
5
+ * Extracted into its own module so unit tests can validate the schema without
6
+ * importing the full MCP server (which has startup side effects).
7
+ *
8
+ * Keep this in sync with the inputSchema registered for `generate_decision_page`
9
+ * in src/index.ts. The handler in index.ts continues to enforce duplicate-id,
10
+ * recommendation_index bounds, and reserved "None of these" rules at runtime.
11
+ */
12
+ import { z } from "zod";
13
+ export const ActionableItemSchema = z
14
+ .object({
15
+ id: z
16
+ .string()
17
+ .min(1)
18
+ .regex(/^[A-Za-z0-9_-]+$/, "id must contain only letters, digits, hyphens, or underscores"),
19
+ question: z.string().min(1),
20
+ original_question: z
21
+ .string()
22
+ .min(1)
23
+ .describe("The clarifying question or critique point as originally raised; soft cap ~30 words."),
24
+ why_it_matters: z
25
+ .string()
26
+ .min(1)
27
+ .describe("Concrete one-sentence impact of this decision; soft cap ~40 words."),
28
+ recommendation_explanation: z
29
+ .string()
30
+ .min(1)
31
+ .describe("Why the recommended branch is the best choice; soft cap ~60 words."),
32
+ codebase_evidence: z
33
+ .string()
34
+ .min(1)
35
+ .describe("Combined Assessment paragraph and Codebase Evidence bullet list. Rendered as escaped plain text inside a closed-by-default <details> block."),
36
+ source: z
37
+ .string()
38
+ .min(1)
39
+ .describe("Source reference from the combined review-and-resolution doc, e.g. 'Clarifying Q3 (prior round, weak concurrence)'"),
40
+ recommendation_index: z
41
+ .number()
42
+ .int()
43
+ .min(0)
44
+ .describe("0-based index of the recommended option in the options array"),
45
+ options: z
46
+ .array(z.string().min(1))
47
+ .min(2)
48
+ .max(4)
49
+ .describe("Option labels from the decision tree branches. Values are auto-generated. Must have 2–4 entries."),
50
+ option_consequences: z
51
+ .array(z.string().min(1))
52
+ .min(2)
53
+ .max(4)
54
+ .describe("Behavioral consequence per branch, parallel to options. Must have 2–4 entries; length must equal options.length."),
55
+ })
56
+ .superRefine((item, ctx) => {
57
+ if (item.option_consequences.length !== item.options.length) {
58
+ ctx.addIssue({
59
+ code: z.ZodIssueCode.custom,
60
+ path: ["option_consequences"],
61
+ message: `option_consequences length (${item.option_consequences.length}) must match options length (${item.options.length}).`,
62
+ });
63
+ }
64
+ if (item.recommendation_index >= item.options.length) {
65
+ ctx.addIssue({
66
+ code: z.ZodIssueCode.custom,
67
+ path: ["recommendation_index"],
68
+ message: `recommendation_index (${item.recommendation_index}) is out of bounds (${item.options.length} options).`,
69
+ });
70
+ }
71
+ });
72
+ // Raw input shape for the `generate_decision_page` tool registration.
73
+ // MCP's registerTool expects a shape object (which the SDK wraps in z.object),
74
+ // not a pre-built z.object. Exporting the shape lets index.ts and the schema
75
+ // share a single source of truth for the input contract.
76
+ export const DecisionPageInputShape = {
77
+ ticket_key: z.string().describe("Jira ticket key, e.g. BAPI-123"),
78
+ actionable_items: z
79
+ .array(ActionableItemSchema)
80
+ .optional()
81
+ .default([])
82
+ .describe("Actionable review decisions sourced from the combined review-and-resolution document. 'None of these' is auto-appended by the renderer and must not appear in options."),
83
+ clear_improvements: z
84
+ .array(z.object({
85
+ id: z
86
+ .string()
87
+ .min(1)
88
+ .describe("Stable identifier for the improvement. Stored for the rewrite/capture step but intentionally not rendered to the user."),
89
+ title: z.string().min(1),
90
+ action: z.string().min(1),
91
+ confidence: z.string().min(1),
92
+ source: z
93
+ .string()
94
+ .min(1)
95
+ .describe("Source reference from the evaluation. Stored for the rewrite/capture step but intentionally not rendered to the user — the confirmed-improvements list shows title/confidence/action only."),
96
+ }))
97
+ .optional()
98
+ .default([])
99
+ .describe("Confirmed improvements displayed as informational list, not submitted."),
100
+ };
101
+ export const DecisionPageInputSchema = z.object(DecisionPageInputShape);
@@ -0,0 +1,248 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { ActionableItemSchema } from "./decision-page-schema.js";
4
+ // ---------------------------------------------------------------------------
5
+ // Fixture helpers
6
+ // ---------------------------------------------------------------------------
7
+ function validItemRaw() {
8
+ return {
9
+ id: "E-1",
10
+ question: "E-1: Timeout behavior needs a decision",
11
+ original_question: "Should the ticket specify timeout behavior for slow upstream calls?",
12
+ why_it_matters: "Timeout behavior affects retry paths and determines whether users see stale, failed, or delayed responses.",
13
+ options: ["Keep existing", "Add configurable", "Defer"],
14
+ option_consequences: [
15
+ "No new implementation work, but slow calls keep current failure behavior.",
16
+ "Implementers add configuration and tests for timeout-specific behavior.",
17
+ "The ticket remains smaller, but timeout ambiguity persists for a later change.",
18
+ ],
19
+ recommendation_explanation: "Configurable timeout best matches code paths that already branch on external service latency.",
20
+ codebase_evidence: "Assessment: ...\n\nCodebase Evidence:\n- src/api/timeouts.ts:42 — current latency-branching helper",
21
+ source: "Clarifying Q1 (prior round, weak concurrence)",
22
+ recommendation_index: 1,
23
+ };
24
+ }
25
+ // ---------------------------------------------------------------------------
26
+ // Schema validation — happy path
27
+ // ---------------------------------------------------------------------------
28
+ describe("ActionableItemSchema — happy path", () => {
29
+ it("accepts a valid actionable item using the new contract", () => {
30
+ const result = ActionableItemSchema.safeParse(validItemRaw());
31
+ assert.equal(result.success, true);
32
+ });
33
+ it("does not require a context field", () => {
34
+ const item = validItemRaw();
35
+ const result = ActionableItemSchema.safeParse(item);
36
+ assert.equal(result.success, true);
37
+ if (result.success) {
38
+ assert.equal(result.data.context, undefined);
39
+ }
40
+ });
41
+ it("accepts the minimum branch count (2 options, 2 consequences)", () => {
42
+ const item = {
43
+ ...validItemRaw(),
44
+ options: ["A", "B"],
45
+ option_consequences: ["Consequence A.", "Consequence B."],
46
+ recommendation_index: 0,
47
+ };
48
+ const result = ActionableItemSchema.safeParse(item);
49
+ assert.equal(result.success, true);
50
+ });
51
+ it("accepts the maximum branch count (4 options, 4 consequences)", () => {
52
+ const item = {
53
+ ...validItemRaw(),
54
+ options: ["A", "B", "C", "D"],
55
+ option_consequences: ["A.", "B.", "C.", "D."],
56
+ recommendation_index: 0,
57
+ };
58
+ const result = ActionableItemSchema.safeParse(item);
59
+ assert.equal(result.success, true);
60
+ });
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // Schema validation — required fields
64
+ // ---------------------------------------------------------------------------
65
+ describe("ActionableItemSchema — required clarity fields", () => {
66
+ for (const fieldName of [
67
+ "original_question",
68
+ "why_it_matters",
69
+ "recommendation_explanation",
70
+ "codebase_evidence",
71
+ ]) {
72
+ it(`rejects payloads missing ${fieldName}`, () => {
73
+ const item = { ...validItemRaw() };
74
+ delete item[fieldName];
75
+ const result = ActionableItemSchema.safeParse(item);
76
+ assert.equal(result.success, false);
77
+ if (!result.success) {
78
+ const paths = result.error.issues.map((issue) => issue.path.join("."));
79
+ assert.ok(paths.includes(fieldName), `expected issue at path ${fieldName}, got ${paths.join(", ")}`);
80
+ }
81
+ });
82
+ }
83
+ it("rejects payloads missing option_consequences", () => {
84
+ const item = { ...validItemRaw() };
85
+ delete item.option_consequences;
86
+ const result = ActionableItemSchema.safeParse(item);
87
+ assert.equal(result.success, false);
88
+ if (!result.success) {
89
+ const paths = result.error.issues.map((issue) => issue.path.join("."));
90
+ assert.ok(paths.includes("option_consequences"));
91
+ }
92
+ });
93
+ it("rejects payloads that still use a legacy context field instead of the new clarity fields", () => {
94
+ const item = { ...validItemRaw(), context: "legacy context blob" };
95
+ delete item.original_question;
96
+ delete item.why_it_matters;
97
+ delete item.recommendation_explanation;
98
+ delete item.codebase_evidence;
99
+ const result = ActionableItemSchema.safeParse(item);
100
+ assert.equal(result.success, false);
101
+ if (!result.success) {
102
+ const paths = result.error.issues.map((issue) => issue.path.join("."));
103
+ assert.ok(paths.includes("original_question"));
104
+ assert.ok(paths.includes("why_it_matters"));
105
+ assert.ok(paths.includes("recommendation_explanation"));
106
+ assert.ok(paths.includes("codebase_evidence"));
107
+ }
108
+ });
109
+ });
110
+ // ---------------------------------------------------------------------------
111
+ // Schema validation — branch count boundaries
112
+ // ---------------------------------------------------------------------------
113
+ describe("ActionableItemSchema — 2–4 branch boundary", () => {
114
+ it("rejects options with only one branch", () => {
115
+ const item = {
116
+ ...validItemRaw(),
117
+ options: ["Only one"],
118
+ option_consequences: ["Only one."],
119
+ recommendation_index: 0,
120
+ };
121
+ const result = ActionableItemSchema.safeParse(item);
122
+ assert.equal(result.success, false);
123
+ });
124
+ it("rejects options with five branches", () => {
125
+ const item = {
126
+ ...validItemRaw(),
127
+ options: ["A", "B", "C", "D", "E"],
128
+ option_consequences: ["A.", "B.", "C.", "D.", "E."],
129
+ recommendation_index: 0,
130
+ };
131
+ const result = ActionableItemSchema.safeParse(item);
132
+ assert.equal(result.success, false);
133
+ });
134
+ it("rejects option_consequences with only one entry", () => {
135
+ const item = {
136
+ ...validItemRaw(),
137
+ options: ["A", "B"],
138
+ option_consequences: ["Only one."],
139
+ recommendation_index: 0,
140
+ };
141
+ const result = ActionableItemSchema.safeParse(item);
142
+ assert.equal(result.success, false);
143
+ });
144
+ it("rejects option_consequences with five entries", () => {
145
+ const item = {
146
+ ...validItemRaw(),
147
+ options: ["A", "B"],
148
+ option_consequences: ["A.", "B.", "C.", "D.", "E."],
149
+ recommendation_index: 0,
150
+ };
151
+ const result = ActionableItemSchema.safeParse(item);
152
+ assert.equal(result.success, false);
153
+ });
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // Schema validation — option/consequence parity
157
+ // ---------------------------------------------------------------------------
158
+ describe("ActionableItemSchema — option/consequence parity", () => {
159
+ it("rejects mismatched lengths and attaches the issue to option_consequences", () => {
160
+ const item = {
161
+ ...validItemRaw(),
162
+ options: ["A", "B", "C"],
163
+ option_consequences: ["A.", "B."],
164
+ recommendation_index: 0,
165
+ };
166
+ const result = ActionableItemSchema.safeParse(item);
167
+ assert.equal(result.success, false);
168
+ if (!result.success) {
169
+ const matchingIssue = result.error.issues.find((issue) => issue.path.join(".") === "option_consequences" && /must match options length/.test(issue.message));
170
+ assert.ok(matchingIssue, `expected an option_consequences issue with the parity message; got: ${JSON.stringify(result.error.issues)}`);
171
+ }
172
+ });
173
+ it("rejects mismatched lengths in the other direction", () => {
174
+ const item = {
175
+ ...validItemRaw(),
176
+ options: ["A", "B"],
177
+ option_consequences: ["A.", "B.", "C."],
178
+ recommendation_index: 0,
179
+ };
180
+ const result = ActionableItemSchema.safeParse(item);
181
+ assert.equal(result.success, false);
182
+ });
183
+ });
184
+ // ---------------------------------------------------------------------------
185
+ // Schema validation — recommendation_index upper bound
186
+ // ---------------------------------------------------------------------------
187
+ describe("ActionableItemSchema — recommendation_index upper bound", () => {
188
+ it("rejects recommendation_index >= options.length and attaches the issue to recommendation_index", () => {
189
+ const item = {
190
+ ...validItemRaw(),
191
+ options: ["A", "B", "C"],
192
+ option_consequences: ["A.", "B.", "C."],
193
+ recommendation_index: 99,
194
+ };
195
+ const result = ActionableItemSchema.safeParse(item);
196
+ assert.equal(result.success, false);
197
+ if (!result.success) {
198
+ const matchingIssue = result.error.issues.find((issue) => issue.path.join(".") === "recommendation_index" && /out of bounds/.test(issue.message));
199
+ assert.ok(matchingIssue, `expected a recommendation_index issue with the out-of-bounds message; got: ${JSON.stringify(result.error.issues)}`);
200
+ }
201
+ });
202
+ it("rejects recommendation_index equal to options.length (off-by-one)", () => {
203
+ const item = {
204
+ ...validItemRaw(),
205
+ options: ["A", "B"],
206
+ option_consequences: ["A.", "B."],
207
+ recommendation_index: 2,
208
+ };
209
+ const result = ActionableItemSchema.safeParse(item);
210
+ assert.equal(result.success, false);
211
+ });
212
+ });
213
+ // ---------------------------------------------------------------------------
214
+ // id format — must produce a valid HTML id so getElementById can find the
215
+ // per-item textarea/radio. Whitespace and dots silently break that lookup
216
+ // (and would skip the "no comment selected" validation), so the schema
217
+ // rejects anything outside [A-Za-z0-9_-].
218
+ // ---------------------------------------------------------------------------
219
+ describe("ActionableItemSchema — id format", () => {
220
+ it("rejects an id containing whitespace", () => {
221
+ const item = { ...validItemRaw(), id: "E 1" };
222
+ const result = ActionableItemSchema.safeParse(item);
223
+ assert.equal(result.success, false);
224
+ if (!result.success) {
225
+ assert.ok(result.error.issues.some((issue) => issue.path[0] === "id"));
226
+ }
227
+ });
228
+ it("rejects an id containing only whitespace", () => {
229
+ const item = { ...validItemRaw(), id: " " };
230
+ const result = ActionableItemSchema.safeParse(item);
231
+ assert.equal(result.success, false);
232
+ });
233
+ it("rejects an id containing a dot", () => {
234
+ const item = { ...validItemRaw(), id: "E.1" };
235
+ const result = ActionableItemSchema.safeParse(item);
236
+ assert.equal(result.success, false);
237
+ });
238
+ it("accepts an id with hyphens (matches the instruction's generator format)", () => {
239
+ const item = { ...validItemRaw(), id: "E-1" };
240
+ const result = ActionableItemSchema.safeParse(item);
241
+ assert.equal(result.success, true);
242
+ });
243
+ it("accepts an id with underscores", () => {
244
+ const item = { ...validItemRaw(), id: "decision_1" };
245
+ const result = ActionableItemSchema.safeParse(item);
246
+ assert.equal(result.success, true);
247
+ });
248
+ });