Skip to content

feat(api): spec soft-delete (withdraw/restore/tombstone) — ADR-030 - #313

Merged
thewrz merged 3 commits into
mainfrom
feat/issue-310
Jun 28, 2026
Merged

feat(api): spec soft-delete (withdraw/restore/tombstone) — ADR-030#313
thewrz merged 3 commits into
mainfrom
feat/issue-310

Conversation

@thewrz

@thewrz thewrz commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Why

The web demo ships a spec-delete UI gated behind the specDelete flag (false), which degrades to "not available in this API build" because main has no DELETE /specs/:id. ADR-030 (Accepted) settles the design: deleting a library master must be a reversible tombstone, not a hard row delete. ADR-015's layered chain-of-custody means hard-deleting a master would destroy the provenance of every derived project copy, and a master with clones can't be hard-deleted anyway (parent_spec_id FK is NO ACTION).

What

DELETE /specs/:id performs a soft withdrawal of a library master; POST /specs/:id/restore reverses it.

  • Migration 036specs.withdrawn_at timestamptz NULL (NULL = active). Reversible up/down. No change to parent_spec_id onDelete; row, paragraphs, and lineage edges stay intact.
  • DELETE /specs/:id200 { specId, withdrawnAt }; 409 on a project copy (steers to DELETE /projects/:id/specs/:specId); 404 unknown; idempotent re-withdraw returns the original withdrawnAt (COALESCE).
  • POST /specs/:id/restore200 { specId }; idempotent no-op when active; mirrors withdraw on ownership (409 project copy / 404 unknown).
  • Read-path filtering (withdrawn_at IS NULL): listLibrarySpecs, project source resolution (resolveSection), broken-ref availableFrom, the coordination present set, and the source-library catalog. GET /specs/:id still surfaces withdrawnAt (null when active) so lineage/history resolves.
  • openapi.yaml updated for both routes + withdrawnAt on SpecTree (contract gate green three ways).

Design decisions

  • No withdrawn_by column. ADR-030 / the issue specify only withdrawn_at. The projects soft-delete (ADR-031) carries deleted_by, but ADR-030 deliberately does not — kept minimal to spec. (When the user/auth model lands, feat(api): Phase 5f — authentication + multi-tenant (JWT, org isolation) #43, an actor can be added.)
  • Restore on a project copy → 409 (not 200/404). The ADR enumerates 409-on-copy only for DELETE; it is silent on restore + copies. I mirrored withdraw so the withdraw/restore pair is symmetric and a project copy is consistently the wrong target for either — copies have no withdrawal lifecycle. One-line change if a reviewer prefers a 200 no-op.
  • coordination.ts readPresent filter is a documented no-op guard. project_specs/package_specs reference project copies (never withdrawn) in today's copy model, so the filter changes nothing today — but ADR-030 enumerates the "coordination present set" as an acceptance criterion, so it is pinned verifiably and stays correct if membership ever points at masters. The meaningful coordination filter is on the source-library catalog (readCatalog), which does ingest masters.
  • readCatalog source-branch filtered too (slightly beyond the literal enumeration). The coordination report ingests source-library masters in a second place (the implied-related-sections catalog). I filtered it so a withdrawn master is fully absent from coordination output — consistent with ADR-030's "hidden from resolution" intent. Flagging in case a reviewer wants the strictest reading of the enumerated set.
  • content_version / updated_at are not bumped on withdraw/restore. Withdrawal is a lifecycle tombstone, not a content edit; bumping content_version would falsely invalidate optimistic-concurrency (edit-gate) and merge base versions.
  • Demo specDelete flag left false. The demo's delete UI was modelled as a hard delete (cascade, 409-if-pinned). Flipping the flag without re-aligning that UI to soft-withdraw semantics would surface a mismatched UX, so it is a deliberate follow-up — this issue is backend-scoped ("only the spec-level soft withdrawal").

Testing

  • Unit tests pass (pnpm test — 1190 passed; handler 200/409/404/400/500 mapping + withdrawnAt surfacing pinned)
  • Integration tests pass (pnpm test:integration — 705 passed, 0 failed: withdraw, restore, 409 project-copy, 404, idempotency, listing hide/restore, resolution hide-then-restore; ran against docker compose up -d postgres + pnpm migrate && pnpm seed)
  • pnpm lint clean (eslint complexity caps, tsc --noEmit, prettier)
  • Contract gate green (route↔spec coverage + response shapes)
  • Migration reversibility verified (migrate:down → column dropped → migrate → column re-added)
  • CI green

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

Summary by CodeRabbit

  • New Features

    • Added the ability to withdraw and restore specs.
    • Spec details now show whether a spec is withdrawn.
    • Withdrawn specs are hidden from library lists and related project views.
  • Bug Fixes

    • Project-copy specs now return a clear conflict response when withdrawal or restore is attempted.
    • Restored specs reappear in project coordination and reference resolution results.
  • Tests

    • Expanded API and integration coverage for withdrawal, restore, and related edge cases.

Implement DELETE /specs/:id as a soft withdrawal of a library master, not a
hard row delete, per ADR-030. Hard-deleting a master would destroy the
chain-of-custody provenance of every derived project copy (ADR-015); a
reversible tombstone preserves it.

- Migration 036: add specs.withdrawn_at timestamptz NULL (reversible up/down).
- DELETE /specs/:id soft-withdraws a master -> 200 { specId, withdrawnAt };
  409 on a project copy (use the membership endpoint); 404 unknown; idempotent
  re-withdraw returns the original withdrawnAt (COALESCE).
- POST /specs/:id/restore clears the tombstone -> 200; mirrors withdraw on
  ownership (409 on a project copy, 404 unknown), idempotent no-op when active.
- Read-path filtering (withdrawn_at IS NULL): listLibrarySpecs, project source
  resolution (resolveSection), broken-ref availableFrom, the coordination
  present set, and the source-library catalog. GET /specs/:id still surfaces
  withdrawnAt so lineage/history resolves.
- openapi.yaml: both routes + withdrawnAt on SpecTree (contract gate green).
- Unit + integration tests: withdraw, restore, 409 project-copy, 404,
  idempotency, read-path hiding/surfacing, resolution hide-then-restore.

Closes #310

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@thewrz, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 34 minutes and 38 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba913a36-0f25-430c-b3c6-b109c8403046

📥 Commits

Reviewing files that changed from the base of the PR and between 02fe3e7 and b240d0d.

📒 Files selected for processing (5)
  • src/api/contract.integration.test.ts
  • src/api/specs.integration.test.ts
  • src/api/specs.test.ts
  • src/api/specs.ts
  • src/db/queries/specs.ts
📝 Walkthrough

Walkthrough

Implements ADR-030 spec soft-withdrawal: adds a withdrawn_at timestamptz column via migration, introduces withdrawSpec/restoreSpec/getSpecWithdrawnAt DB queries with outcome types, adds DELETE /specs/:id and POST /specs/:id/restore API handlers, filters withdrawn masters from all read paths (listings, resolution, coordination, broken refs), exposes withdrawnAt on GET /specs/:id, documents both routes in OpenAPI, and adds unit and integration tests.

Changes

Spec Withdraw/Restore (ADR-030)

Layer / File(s) Summary
Migration and outcome types
src/db/migrations/036_add_spec_withdraw.ts, src/db/queries/specs.ts
Reversible migration adds withdrawn_at column; WithdrawSpecOutcome and RestoreSpecOutcome union types model not-found, project-copy, and success cases.
DB query implementations
src/db/queries/specs.ts, src/db/index.ts
withdrawSpec, restoreSpec, and getSpecWithdrawnAt implemented with CTE-based idempotent SQL; re-exported with their types from db/index.ts.
Read-path filtering
src/db/queries/libraries.ts, src/db/queries/coordination-implied.ts, src/db/queries/coordination.ts, src/db/queries/derive.ts, src/db/queries/project-refs.ts
withdrawn_at IS NULL predicates added to listLibrarySpecs, readCatalog, readPresent, resolveSection, and getBrokenRefs so withdrawn masters are excluded from all read paths.
API handlers and routing
src/api/specs.ts, src/api/router.ts
withdrawSpecHandler and restoreSpecHandler added with UUID validation and outcome-based status codes (200/404/409/500); getSpecHandler extended to include withdrawnAt; routes registered for DELETE /specs/:id and POST /specs/:id/restore.
OpenAPI contract
openapi.yaml
DELETE /specs/{id} (withdrawSpec) and POST /specs/{id}/restore (restoreSpec) operations documented with full response schemas; withdrawnAt field added to SpecTree.
Unit and integration tests
src/api/specs.test.ts, src/api/specs.integration.test.ts, src/api/contract.integration.test.ts
Unit tests added for both new handlers and updated getSpecHandler; integration suite covers withdraw/restore HTTP behavior, idempotency, visibility changes, 409/404/400 edge cases, and downstream resolution/advisory effects.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • wrzonance/SpecR#158: Modifies getSpecHandler and GET /specs/:id response shape in src/api/specs.ts, overlapping with this PR's addition of withdrawnAt to that same handler and schema.
  • wrzonance/SpecR#206: Extends the SpecTree response schema and getSpecHandler for GET /specs/:id, directly overlapping with the withdrawnAt field addition here.
  • wrzonance/SpecR#248: Touches src/api/specs.ts handler and SpecTree response shape and adds state-transition endpoints in the same /specs/:id area.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the ADR-030 spec soft-delete/restore change and names the main behavior.
Linked Issues check ✅ Passed The PR implements the required migration, withdraw/restore endpoints, read-path filtering, GET surfacing, OpenAPI updates, and tests for ADR-030.
Out of Scope Changes check ✅ Passed No unrelated functionality changes are evident; the edits stay within the spec soft-withdraw/restore scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/issue-310

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

…030)

The withdraw read-path invariant "hidden from every listed read path" had
boundary tests for listLibrarySpecs and project source resolution, but the
broken-ref availableFrom advisory was covered only at the query level. Add an
isolated boundary test (own library + master + a project copy carrying a broken
ref) asserting availableFrom names the source library while the master is
active, drops it once withdrawn, and offers it again after restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thewrz
thewrz marked this pull request as ready for review June 28, 2026 05:52

@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.

🧹 Nitpick comments (4)
src/api/specs.ts (1)

38-42: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize independent spec metadata lookups.

styleSource, onboardingStatus, and withdrawnAt are independent after getSpecTree(id) succeeds, so fetching them together avoids extra serial DB round-trip latency on GET /specs/:id.

♻️ Proposed refactor
-    const styleSource = await getSpecStyleSource(id);
-    const onboardingStatus = await getOnboardingStatus(id);
     // A withdrawn master (ADR-030) is still GET-able with its tombstone surfaced
     // (null when active), so lineage/history resolves. Same sibling-field pattern.
-    const withdrawnAt = await getSpecWithdrawnAt(id);
+    const [styleSource, onboardingStatus, withdrawnAt] = await Promise.all([
+      getSpecStyleSource(id),
+      getOnboardingStatus(id),
+      getSpecWithdrawnAt(id),
+    ]);
🤖 Prompt for 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.

In `@src/api/specs.ts` around lines 38 - 42, The metadata fetches in the spec
lookup flow are still happening serially after getSpecTree(id) succeeds. Update
the logic in the spec retrieval path around the getSpecStyleSource,
getOnboardingStatus, and getSpecWithdrawnAt calls so they run in parallel (for
example by grouping them in the same async await flow) and then assign the
resolved values together. Keep the existing behavior and ordering of downstream
use in the spec response assembly.
src/db/queries/specs.ts (1)

318-321: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Skip no-op UPDATEs in withdraw/restore
src/db/queries/specs.ts:318-321,357-360
Both CTEs still issue UPDATE on already-withdrawn / already-active masters, so retries create unnecessary row versions/WAL and take extra locks. Add s.withdrawn_at IS NULL / IS NOT NULL guards so the no-op path stays read-only.

🤖 Prompt for 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.

In `@src/db/queries/specs.ts` around lines 318 - 321, The withdraw and restore
queries in the specs CTEs still perform UPDATEs even when the row is already in
the desired state. Update the predicates in the relevant `UPDATE specs s`
statements so `withdraw` only targets rows where `s.withdrawn_at IS NULL` and
`restore` only targets rows where `s.withdrawn_at IS NOT NULL`, keeping retries
as no-op reads instead of writing new row versions.
src/api/contract.integration.test.ts (1)

97-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Track these operations as response-covered, not allowlisted.

The new withdraw/restore endpoints already have explicit 200-body assertions in src/api/specs.integration.test.ts, so putting them in RESPONSE_ALLOWLIST weakens this contract meta-test. Move them into RESPONSE_COVERED so the suite still fails if those response assertions disappear later.

Suggested change
 const RESPONSE_COVERED = new Set([
   'delete /projects/{}/revision-nomenclature',
+  'delete /specs/{}',
   'get /health',
   'get /conventions',
   'get /libraries',
   'get /libraries/{}/specs',
   'get /projects',
   'get /projects/{}/revision-nomenclature',
   'get /revision-nomenclature-profiles',
+  'post /specs/{}/restore',
   'get /templates',
   'post /projects/{}/revision-nomenclature/clone',
   'put /projects/{}/revision-nomenclature',
 ]);
@@
 const RESPONSE_ALLOWLIST = new Set([
   'delete /packages/{}',
   'delete /projects/{}/specs/{}',
-  'delete /specs/{}',
   'delete /specs/{}/lock',
@@
   'patch /projects/{}',
   'delete /projects/{}',
   'post /projects/{}/restore',
-  'post /specs/{}/restore',
 ]);
🤖 Prompt for 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.

In `@src/api/contract.integration.test.ts` around lines 97 - 98, The
withdraw/restore endpoints are currently treated as allowlisted in the contract
meta-test, but they already have explicit response-body assertions elsewhere and
should be verified as response-covered instead. Update the entries for delete
/specs/{} and post /specs/{}/restore in the contract test setup so they are
included with RESPONSE_COVERED rather than RESPONSE_ALLOWLIST, keeping the
existing explicit coverage in src/api/specs.integration.test.ts as the source of
truth.
src/api/specs.test.ts (1)

252-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the missing 500-path test for restoreSpecHandler.

src/api/specs.ts gives restoreSpecHandler its own catch branch, but this suite never exercises the case where restoreSpec() rejects. That leaves the new 500 mapping unpinned while the sibling withdrawSpecHandler suite already covers the analogous branch.

Suggested test
 describe('restoreSpecHandler', () => {
   it('returns 200 with {specId} on a master', async () => {
@@
   it('returns 400 on a malformed (non-UUID) id', async () => {
     const { restoreSpecHandler } = await import('./specs.js');
     const res = makeRes();
     await restoreSpecHandler(
       { params: { id: 'nope' } } as unknown as Request,
       res as unknown as Response
     );
     expect(res.status).toHaveBeenCalledWith(400);
   });
+
+  it('returns 500 on database error', async () => {
+    const { restoreSpec } = await import('../db/index.js');
+    vi.mocked(restoreSpec).mockRejectedValueOnce(new Error('db down'));
+    const { restoreSpecHandler } = await import('./specs.js');
+    const res = makeRes();
+    await restoreSpecHandler(
+      { params: { id: VALID_ID } } as unknown as Request,
+      res as unknown as Response
+    );
+    expect(res.status).toHaveBeenCalledWith(500);
+  });
 });
🤖 Prompt for 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.

In `@src/api/specs.test.ts` around lines 252 - 303, Add a missing test in the
restoreSpecHandler suite to cover the rejection path from restoreSpec(): mock
restoreSpec to reject, invoke restoreSpecHandler, and assert it returns 500
through the existing res helper. Use the existing restoreSpecHandler and
restoreSpec symbols in specs.test.ts so the new catch-branch behavior in
specs.ts is pinned the same way the withdrawSpecHandler tests cover their
analogous error path.
🤖 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.

Nitpick comments:
In `@src/api/contract.integration.test.ts`:
- Around line 97-98: The withdraw/restore endpoints are currently treated as
allowlisted in the contract meta-test, but they already have explicit
response-body assertions elsewhere and should be verified as response-covered
instead. Update the entries for delete /specs/{} and post /specs/{}/restore in
the contract test setup so they are included with RESPONSE_COVERED rather than
RESPONSE_ALLOWLIST, keeping the existing explicit coverage in
src/api/specs.integration.test.ts as the source of truth.

In `@src/api/specs.test.ts`:
- Around line 252-303: Add a missing test in the restoreSpecHandler suite to
cover the rejection path from restoreSpec(): mock restoreSpec to reject, invoke
restoreSpecHandler, and assert it returns 500 through the existing res helper.
Use the existing restoreSpecHandler and restoreSpec symbols in specs.test.ts so
the new catch-branch behavior in specs.ts is pinned the same way the
withdrawSpecHandler tests cover their analogous error path.

In `@src/api/specs.ts`:
- Around line 38-42: The metadata fetches in the spec lookup flow are still
happening serially after getSpecTree(id) succeeds. Update the logic in the spec
retrieval path around the getSpecStyleSource, getOnboardingStatus, and
getSpecWithdrawnAt calls so they run in parallel (for example by grouping them
in the same async await flow) and then assign the resolved values together. Keep
the existing behavior and ordering of downstream use in the spec response
assembly.

In `@src/db/queries/specs.ts`:
- Around line 318-321: The withdraw and restore queries in the specs CTEs still
perform UPDATEs even when the row is already in the desired state. Update the
predicates in the relevant `UPDATE specs s` statements so `withdraw` only
targets rows where `s.withdrawn_at IS NULL` and `restore` only targets rows
where `s.withdrawn_at IS NOT NULL`, keeping retries as no-op reads instead of
writing new row versions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd6c679d-ab2b-430d-96d2-eb2d3af233fe

📥 Commits

Reviewing files that changed from the base of the PR and between 1536323 and 02fe3e7.

📒 Files selected for processing (14)
  • openapi.yaml
  • src/api/contract.integration.test.ts
  • src/api/router.ts
  • src/api/specs.integration.test.ts
  • src/api/specs.test.ts
  • src/api/specs.ts
  • src/db/index.ts
  • src/db/migrations/036_add_spec_withdraw.ts
  • src/db/queries/coordination-implied.ts
  • src/db/queries/coordination.ts
  • src/db/queries/derive.ts
  • src/db/queries/libraries.ts
  • src/db/queries/project-refs.ts
  • src/db/queries/specs.ts

@thewrz

thewrz commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

CodeRabbit body nitpicks addressed in b240d0d:

  • Parallelized independent getSpecHandler metadata lookups with Promise.all.
  • Added withdraw/restore SQL guards so idempotent no-op paths do not rewrite rows; pinned by row-version integration assertions.
  • Moved DELETE /specs/{} and POST /specs/{}/restore from RESPONSE_ALLOWLIST to RESPONSE_COVERED.
  • Added restoreSpecHandler database-error 500 coverage.

Post-push CodeRabbit re-review is currently rate-limited; local verification completed with lint, build, full unit, and full integration passing. Claude full and fallback reviews did not return usable output within their timeouts.

@thewrz
thewrz merged commit 415a965 into main Jun 28, 2026
5 checks passed
@thewrz
thewrz deleted the feat/issue-310 branch June 28, 2026 17:27
thewrz added a commit that referenced this pull request Jul 1, 2026
#325)

* docs(readme): sync capabilities to last month of merged PRs

Reflect shipped work in the README's "Included Today", "API Surface", and MCP
tool table, validated against the merged diffs and current main:

- PDF ingest (text-layer + OCR + font-encoding recovery) accepted by POST /parse
  (#287, #290, #311)
- coordination / E&O report + submittal register (#241, #269, #277, #282, #283,
  #284) and article-role tagging (#273)
- onboarding pipeline: library import, editability review/override, reclassify,
  finalize/reopen, open-comments (#243, #247, #248, #249, #272)
- spec/project soft-delete + restore (#257, #313), document concurrency (#197),
  revision/addendum manual rendering (#221), numbering profiles (#317, #322)
- add missing MCP tools get_numbering_profile, submittal_register,
  open_comments_report; document GET /docs (Scalar) (#213, #285)
- add Example Client pointer to examples/web_ui_demo (#225)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(roadmap): move shipped work to done; re-date to 2026-07-01

Reconcile the roadmap with merged reality (was stamped 2026-06-17). Moved from
planned/in-progress to Included, each validated against the diff:

- PDF ingest (#287, #290, #311) — remove from "Later"
- deep paragraph nesting pr6/pr7 (#215)
- revision nomenclature (#216) + revision/addendum manual rendering (#221) —
  the two "Near Term" Phase 2e items are done
- coordination / E&O report, required-sections, article-role, submittal register
  (#239, #241, #269, #273, #277, #282, #283, #284) — new "Coordination and
  Semantics" section; removed "coordination report" from planned Phase 4
- onboarding APIs (#243, #247, #248, #249, #272) — API done; UI remains planned
- soft-delete/withdraw (#257, #313), section-number format (#266, #271),
  external-content associations (#242), structural numbering profiles (#317)

Kept as planned (foundation only): header/footer composition (#222, #314) and
keynote surfacing (#315) — DB/AST exist, no resolution/render/export yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(architecture): reflect merged structural changes

Update the architecture spec for shipped work, validated against the diffs and
current schema/routes:

- Tech Stack + Data Flow: Parse — PDF text-layer (unpdf/pdfjs-dist) + OCR
  (tesseract.js/@napi-rs/canvas) path and numberingProfileId override (#287,
  #290, #311, #317; ADR-034, ADR-039)
- DB schema — specs.onboarding_status/withdrawn_at, projects.section_number_format
  /deleted_at/deleted_by, paragraphs.source_facts/classification/
  editability_override; "Additional tables" summary for editing_conventions,
  paragraph_associations, required_sections, keynotes, header_footer_configs,
  numbering_profiles, revision_nomenclature_profiles (foundation-only tables
  flagged) (ADR-021/022/023/028/031/032; #187, #242)
- new Coordination Report / E&O section (finding vocabulary) and Document
  Concurrency section (locks/optimistic/lifecycle) (#197, #241, #269, #277,
  #282, #283, #284; ADR-018, ADR-033/035/036/037)
- AST meta.articleRole (#273, ADR-033); API-surface note pointing at the
  CI-enforced openapi.yaml + GET /docs; refreshed MCP tool list

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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): spec soft-delete (withdraw/tombstone) — DELETE /specs/:id + restore (ADR-030)

1 participant