Skip to content

feat(api): style-source assignment — manual template pick for non-DOCX masters#206

Merged
thewrz merged 2 commits into
mainfrom
feat/issue-138
Jun 17, 2026
Merged

feat(api): style-source assignment — manual template pick for non-DOCX masters#206
thewrz merged 2 commits into
mainfrom
feat/issue-138

Conversation

@thewrz

@thewrz thewrz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Why

.SEC/.txt/PDF masters carry no adopted visual style — there is nothing to capture (#125). The narrowest useful slice of #125's three style sources is the manual pick: associate an existing style template (#30/#31) with a spec so the generator (#32 / WT-6) can later render it with a real house style. The other two sources stay with #125 (manual per-property building is #31's CRUD; library inherit waits for ADR-015 lineage, Phase 2d).

This is Wave 3 (O-12) of the Onboarding & Editability program and the first concrete slice of #125. Design: docs/superpowers/specs/2026-06-16-issue-138-design.md (approved by the owner).

What

  • Migration 027 — nullable specs.style_template_id FK with ON DELETE RESTRICT (owner decision: a spec must never silently lose its formatting because a template was deleted), plus an index for the reference-count check. NULL = "no style source yet"; never auto-nulled.
  • POST /specs/:id/style-source { templateId } assigns (replaces on re-assign); DELETE /specs/:id/style-source clears (idempotent, falls back to generator defaults). Existence is pre-checked explicitly — unknown spec or unknown template → 404 (never via the FK error).
  • styleSource: { templateId, templateName } | null surfaced on GET /specs/:id (a separate getSpecStyleSource query merged in the handler — getSpecTree untouched, disjoint from the parallel feat(db): persist editability classification + user override per paragraph #134) and on MCP get_spec.
  • Deletion protection: deleteTemplate now pre-checks countSpecsUsingTemplate and returns a discriminated result; deleteTemplateHandler maps the in-use case to 409 "template in use by N spec(s)". The pre-check sidesteps the pg 23503 ambiguity (same code means 404 on assign-missing-template, 409 on delete-referenced).
  • New DB module src/db/queries/style-source.ts (get/set/clear + count), re-exported from the barrel. OpenAPI + README updated.

Out of scope (per issue + design §10): project-level style + inheritance / resolution chain (#125, Phase 2d lineage), generator consumption of the association (#32 / WT-6), per-paragraph overrides (WT-4), conformance flagging (#145).

Testing

  • Unit tests pass (pnpm test — 940 passed)
  • Integration tests pass (pnpm test:integration — 437 passed, 110 pre-existing skips), including: assign → read back, unknown spec/template → 404, re-assign replaces, DELETE clears, DOCX-imported spec re-pointed, DELETE /templates/:id while referenced → 409 + template still exists, and MCP get_spec surfaces styleSource.
  • pnpm lint clean (eslint + tsc + prettier)
  • Migration 027 reverses and re-applies cleanly
  • CI green

🤖 Co-authored by Claude Opus 4.8. Closes #138.

Summary by CodeRabbit

  • New Features

    • Added endpoints to assign (POST /specs/:id/style-source) and clear (DELETE /specs/:id/style-source) a spec’s style template.
    • Spec responses (GET /specs/:id) now include styleSource ({ templateId, templateName } or null).
    • Template deletion protection: DELETE /templates/:id returns 409 when still referenced by a spec.
  • Documentation

    • Updated OpenAPI and README endpoint/response schemas for style-source operations and deletion conflict behavior.
  • Tests

    • Added/expanded integration and API coverage for assigning, clearing, and deletion-restriction flows.

…X masters

Lets a user manually associate an existing style template with a spec — the
narrowest useful slice of #125's three style sources. `.SEC`/`.txt`/PDF masters
carry no adopted visual style, so this gives the generator (#32 / WT-6) a real
house style to render with. One template per spec.

- migration 027: nullable specs.style_template_id FK, ON DELETE RESTRICT
  (a spec must never silently lose formatting; a referenced template can't be
  deleted), with an index for the reference-count pre-check.
- new src/db/queries/style-source.ts: get/set/clear + countSpecsUsingTemplate.
- POST/DELETE /specs/:id/style-source; styleSource surfaced on GET /specs/:id
  (separate query merged in the handler — getSpecTree untouched) and MCP get_spec.
- deleteTemplate now pre-checks the reference count and returns a discriminated
  result; deleteTemplateHandler maps in_use → 409 (sidesteps the pg 23503
  ambiguity: same code means 404 on assign-missing, 409 on delete-referenced).
- openapi.yaml + README updated.

Closes #138.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Implements style-source assignment (Issue #138) end-to-end: a new nullable style_template_id FK column on specs (migration 027), a new DB query module for get/set/clear/count operations, POST/DELETE /specs/:id/style-source HTTP endpoints, styleSource surfaced on GET /specs/:id and the MCP get_spec tool, ON DELETE RESTRICT enforcement returning 409 on DELETE /templates/:id when in use, and full OpenAPI/README/design-doc coverage.

Changes

Style-source assignment (Issue #138)

Layer / File(s) Summary
DB migration: specs.style_template_id FK
src/db/migrations/027_spec_style_source.ts
Adds a nullable style_template_id UUID column to specs with a style_templates FK (ON DELETE RESTRICT) and a supporting index; includes reversible down migration.
DB query layer: style-source queries and updated deleteTemplate
src/db/queries/style-source.ts, src/db/queries/templates.ts, src/db/index.ts
Introduces SpecStyleSource interface and four query functions (getSpecStyleSource, setSpecStyleSource, clearSpecStyleSource, countSpecsUsingTemplate). Replaces the boolean-returning deleteTemplate with a discriminated DeleteTemplateResult that pre-checks countSpecsUsingTemplate before deletion, returning in_use/not_found/deleted variants. Both types and functions are re-exported through the db barrel.
Request body schema: SetStyleSourceBody
src/ast/schemas.ts, src/ast/index.ts
Adds SetStyleSourceBodySchema (Zod object requiring a UUID templateId) and the inferred SetStyleSourceBody type, re-exported through the AST barrel.
API handlers: style-source assign/clear, spec read-path, template delete, router wiring
src/api/style-source.ts, src/api/specs.ts, src/api/templates-crud.ts, src/api/router.ts
Adds setStyleSourceHandler (template pre-check → setSpecStyleSource → 404/200) and clearStyleSourceHandler (idempotent clearSpecStyleSource → 404/200). Updates getSpecHandler to call getSpecStyleSource and return styleSource on the response. Updates deleteTemplateHandler to map the in_use discriminant to 409 Conflict. Registers both new routes on the Express router with body validation.
MCP get_spec: styleSource field and error handling
src/mcp/handlers.ts, src/mcp/tools.ts, src/mcp/handlers.test.ts
Updates handleGetSpec to call getSpecStyleSource and include styleSource in the JSON response; standardizes error paths to use toolError. Updates the get_spec tool description to document styleSource.
DB query integration tests
src/db/queries/style-source.integration.test.ts
Verifies null/found behavior for getSpecStyleSource, false-on-missing-spec and replace for setSpecStyleSource, idempotent clearing for clearSpecStyleSource, and multi-spec count for countSpecsUsingTemplate; includes per-test DB cleanup.
API and MCP integration tests
src/api/style-source.integration.test.ts, src/api/specs.test.ts, src/mcp/server.integration.test.ts
Full integration coverage for POST/DELETE /specs/:id/style-source (404/400/422/200 paths, idempotency, reassignment), RESTRICT enforcement returning 409 on DELETE /templates/:id when in use. Unit tests updated for getSpecHandler styleSource field. MCP integration tests for get_spec returning null and populated styleSource.
OpenAPI, README, and design doc
openapi.yaml, README.md, docs/superpowers/specs/2026-06-16-issue-138-design.md
OpenAPI adds /specs/{id}/style-source POST/DELETE operations, StyleSource component schema, styleSource property on SpecTree, and 409 response on deleteTemplate. README documents new endpoints and 409 behavior. Design doc specifies storage model, HTTP surface, read-path changes, deletion protection, and FK error-mapping rationale for Issue #138.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router as Express Router
  participant setStyleSourceHandler
  participant clearStyleSourceHandler
  participant StyleSourceDB as style-source queries
  participant TemplatesDB as deleteTemplate

  rect rgba(100, 149, 237, 0.5)
    note over Client,StyleSourceDB: POST /specs/:id/style-source
    Client->>Router: POST /specs/:id/style-source { templateId }
    Router->>setStyleSourceHandler: validateBody → handler
    setStyleSourceHandler->>StyleSourceDB: getTemplate(templateId) — existence check
    StyleSourceDB-->>setStyleSourceHandler: null → 404 template not found
    setStyleSourceHandler->>StyleSourceDB: setSpecStyleSource(specId, templateId)
    StyleSourceDB-->>setStyleSourceHandler: false → 404 spec not found
    StyleSourceDB-->>setStyleSourceHandler: true → 200 { styleSource }
    setStyleSourceHandler-->>Client: response
  end

  rect rgba(60, 179, 113, 0.5)
    note over Client,StyleSourceDB: DELETE /specs/:id/style-source
    Client->>Router: DELETE /specs/:id/style-source
    Router->>clearStyleSourceHandler: handler
    clearStyleSourceHandler->>StyleSourceDB: clearSpecStyleSource(specId)
    StyleSourceDB-->>clearStyleSourceHandler: false → 404 spec not found
    StyleSourceDB-->>clearStyleSourceHandler: true → 200 { styleSource: null }
    clearStyleSourceHandler-->>Client: response
  end

  rect rgba(210, 105, 30, 0.5)
    note over Client,TemplatesDB: DELETE /templates/:id — RESTRICT enforcement
    Client->>Router: DELETE /templates/:id
    Router->>TemplatesDB: deleteTemplate(id)
    TemplatesDB->>StyleSourceDB: countSpecsUsingTemplate(id)
    StyleSourceDB-->>TemplatesDB: count > 0 → { deleted: false, reason: in_use, inUseBy }
    TemplatesDB-->>Router: in_use result → 409 Conflict
    Router-->>Client: 409
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • wrzonance/SpecR#24: Both PRs extend the MCP get_spec response surface; PR #24 builds the initial get_spec/SpecTree rendering, while this PR adds the styleSource field to the same response.
  • wrzonance/SpecR#87: Both PRs involve the style_templates table and template query exports; PR #87 introduces the table and baseline query functions, while this PR extends deletion logic to reference-check against specs using a template.
  • wrzonance/SpecR#156: Both PRs modify DELETE /templates/:id behavior and the template deletion DB/API layer; PR #156 implements baseline template CRUD, while this PR adds style-source-based RESTRICT enforcement returning 409 when in use.

Suggested labels

style-fidelity:1

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature: manual style-source assignment allowing template association with specs.
Linked Issues check ✅ Passed All acceptance criteria from issue #138 are satisfied: assignment and retrieval on GET /specs/:id [138], unknown IDs return 404 [138], re-assignment replaces and DELETE clears [138], works on all spec source formats [138].
Out of Scope Changes check ✅ Passed All changes are scoped to issue #138 requirements: style-source endpoints, DB persistence, API exposure via GET /specs/:id and MCP, and deletion protection with pre-checking.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-138

Comment @coderabbitai help to get the list of available commands and usage tips.

@thewrz

thewrz commented Jun 17, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/specs/2026-06-16-issue-138-design.md`:
- Line 40: Add the language identifier "text" to the fenced code block opening
on line 40 by changing the opening triple backticks to include the language
specification. On line 56, replace the markdown heading syntax "`#134`" with plain
text by rewriting it as "Issue `#134`" to prevent it from being parsed as a
malformed ATX heading. These changes will resolve the MD040 and MD018
markdownlint violations in the design document.
- Around line 66-68: The documented return types for setSpecStyleSource and
clearSpecStyleSource in this design spec are incorrectly specified as
Promise<void>, but the actual implementation in src/api/style-source.ts returns
boolean values (assigned/cleared flags) that are used to determine 404 responses
for missing specs. Update the return type documentation for both
setSpecStyleSource and clearSpecStyleSource from Promise<void> to
Promise<boolean> to accurately reflect the actual handler contract and prevent
confusion for future implementers.

In `@openapi.yaml`:
- Around line 1756-1757: The 409 response at lines 1756-1757 currently uses a
generic Conflict reference that describes a resource identity conflict, but the
actual endpoint implementation returns 409 when a template is in use by N specs.
Replace the generic $ref to `#/components/responses/Conflict` with an
operation-specific response description that accurately conveys the actual error
condition (template in use), ensuring the OpenAPI contract precisely matches the
behavior defined in the templates-crud.ts endpoint implementation.

In `@src/api/style-source.integration.test.ts`:
- Around line 79-84: The makeTemplate function does not validate the HTTP
response status before parsing JSON, so failed POST requests to /templates
endpoint result in cryptic JSON parsing errors instead of revealing the actual
HTTP failure. Add a guard check after the post call to verify the response is
successful (check the response status code is 2xx range), and if not, throw an
error that includes the actual HTTP status and response details before
attempting to parse the JSON. This will ensure that real HTTP failures are
properly surfaced rather than being masked by JSON parsing errors.

In `@src/api/style-source.ts`:
- Around line 24-40: There is a race condition between the template existence
check at line 24 and the setSpecStyleSource call at line 29. If the template is
deleted between these operations, the FK constraint violation error will be
caught by the generic error handler and returned as a 500 error. To fix this,
wrap the setSpecStyleSource call in its own try-catch block to specifically
handle the foreign key violation error (PostgreSQL error code 23503 or similar
FK constraint failures). When such an error occurs, respond with a 404 status
and 'template not found' error message instead of allowing it to propagate to
the outer catch block that returns 500. This maps the template deletion race
condition to the correct HTTP status code.

In `@src/db/queries/style-source.integration.test.ts`:
- Around line 65-66: The template fixture names created with Date.now() suffix
in the makeTemplate function calls can collide across parallel test workers at
millisecond granularity, causing flaky tests. Replace all instances where
makeTemplate is called with a Date.now() suffix (appearing in the template name
patterns like ss-get-${Date.now()}, ss-update-${Date.now()},
ss-delete-${Date.now()}, etc.) with UUID generation instead. Import a UUID
generation utility and use it to create unique suffixes for each template
fixture name to ensure consistent uniqueness across all parallel test
executions.

In `@src/db/queries/templates.ts`:
- Around line 243-250: The deleteTemplate function has a race condition between
the countSpecsUsingTemplate check at line 243 and the DELETE query at line 245.
Another request could assign a spec to the template between these two
operations, causing the DELETE to fail with a foreign key constraint error
instead of returning the expected in_use response. Wrap both the count check and
the delete operation in a database transaction, and use row-level locking
(SELECT FOR UPDATE) on the style_templates table to prevent concurrent
modifications to specs that reference this template during the check-and-delete
sequence. This makes the entire operation atomic and ensures that if a spec is
assigned after checking, the delete will properly fail with the FK RESTRICT
error instead of racing.

In `@src/mcp/server.integration.test.ts`:
- Around line 227-253: The try/finally block in this test starts after the
UPDATE statement that sets style_template_id, which means if that UPDATE
operation fails, the finally cleanup block will not execute. Move the `try`
keyword to begin before the UPDATE specs statement that sets style_template_id
to ensure the finally block runs and properly cleans up the database state
(resetting style_template_id to NULL and deleting the template) regardless of
whether any operation fails.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2b0ba7d1-c368-418c-a671-57f73741e648

📥 Commits

Reviewing files that changed from the base of the PR and between c761bc6 and 1aff208.

📒 Files selected for processing (20)
  • README.md
  • docs/superpowers/specs/2026-06-16-issue-138-design.md
  • openapi.yaml
  • src/api/router.ts
  • src/api/specs.test.ts
  • src/api/specs.ts
  • src/api/style-source.integration.test.ts
  • src/api/style-source.ts
  • src/api/templates-crud.ts
  • src/ast/index.ts
  • src/ast/schemas.ts
  • src/db/index.ts
  • src/db/migrations/027_spec_style_source.ts
  • src/db/queries/style-source.integration.test.ts
  • src/db/queries/style-source.ts
  • src/db/queries/templates.ts
  • src/mcp/handlers.test.ts
  • src/mcp/handlers.ts
  • src/mcp/server.integration.test.ts
  • src/mcp/tools.ts

Comment thread docs/superpowers/specs/2026-06-16-issue-138-design.md Outdated
Comment thread docs/superpowers/specs/2026-06-16-issue-138-design.md Outdated
Comment thread openapi.yaml Outdated
Comment thread src/api/style-source.integration.test.ts
Comment thread src/api/style-source.ts
Comment thread src/db/queries/style-source.integration.test.ts Outdated
Comment thread src/db/queries/templates.ts Outdated
Comment thread src/mcp/server.integration.test.ts
…n + doc/test hardening

CodeRabbit findings on PR #206 (8 threads):
- style-source assign: map the check-then-update race (template deleted between
  the existence pre-check and the UPDATE → 23503) to 404, not a leaked 500.
- deleteTemplate: catch the 23503 FK violation from the DELETE as the
  authoritative in_use signal (RESTRICT race). A SELECT … FOR UPDATE on the
  template row would not close this — the concurrent assign UPDATEs `specs`,
  not the locked row. Reuses getPgCode.
- openapi: operation-specific 409 on DELETE /templates/:id ("in use by N specs")
  instead of the generic Conflict $ref.
- tests: guard makeTemplate against non-2xx; randomUUID (not Date.now) fixture
  names; start the MCP test try/ before the style_template_id UPDATE.
- design doc: markdownlint (fence language, escaped #134); void → boolean return
  types to match the handler contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@thewrz thewrz merged commit f8836ef into main Jun 17, 2026
37 checks passed
@thewrz thewrz deleted the feat/issue-138 branch June 17, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(api): style-source assignment — manual template pick for non-DOCX masters

1 participant