feat(onboarding): structural numbering profile at formatting ingress (#299) - #317
Conversation
Approved design for the structural-numbering-profile feature: dedicated numbering_profiles table (mirrors editing_conventions), per-spec assignment FK + built-in CSI default, deterministic override of the inference engine's numId/style->level mapping (pure parser + injected profile, #304 contract), and the snapshot->edit->assign->apply ingress loop. Part B split to #316. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ordered ast->db->parser->api build with the byte-for-byte backward-compat invariant (ARCAT+CPI goldens) gating the engine override, openapi updated with its routes, snapshot-extractor in scope, MCP read tool optional. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) Create src/db/queries/numbering-profiles.ts with full CRUD (listNumberingProfiles, getNumberingProfile, createNumberingProfile, updateNumberingProfile, deleteNumberingProfile), spec-assignment helpers (setSpecNumberingProfile, clearSpecNumberingProfile), and the effective-profile resolver (getEffectiveNumberingProfile). The resolver always returns a valid NumberingProfile — spec-assigned profile first, CSI Default built-in fallback otherwise. deleteNumberingProfile maps pg 23503 (ON DELETE RESTRICT from specs FK) to NumberingProfileInUseError for clean 409 handling upstream. Barrel-exports all functions, NumberingProfileRow type, and NumberingProfileInUseError from src/db/index.ts. 19 integration tests cover the required TDD scenarios: (a)-(d) from the task brief plus CRUD happy paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pure extractNumberingProfile(NumberingMap, StyleMap) → NumberingProfile: assigns tier per articleIlvl boundary, maps lvlText→labelTemplate, builds styleLadder from pStyleToNumId/pStyleToIlvl union resolvedNumPr. KNOWN AMBIGUITY: missing abstractNum → numId skipped (pinned in test). Exports wired through docx barrel and top-level parser barrel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#299) buildNumbering emitted a level set for every numId in map.nums, so a generic list numId got tier 'part' at ilvl 0 — wrong, and it made every numId look spec-shaped. Filter to map.specShapedNumIds so numbering[] describes only the structural ladder; Task 5 reconstructs specShapedNumIds as exactly new Set(profile.numbering.map(n => n.numId)). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…flicts (#299) Add applyNumberingProfile (inverse of extractNumberingProfile) and mergeProfileConflicts to numbering-profile.ts, and thread an optional NumberingProfile through ParseOptions → parse → parseDocx → runPipeline → buildClassification as the last argument at every hop. Absent a profile the pipeline is byte-for-byte today's behavior (the no-profile call stays a two-arg parseDocx call). An EMPTY 'CSI Default' profile is a passthrough, not a wipe: applyNumberingProfile overrides articleIlvl / specShapedNumIds / pStyle maps only when the profile actually specifies them. When a profile is present, paragraphs are classified twice (overridden vs base) and per-paragraph disagreements are recorded on the authoritative paragraph via the existing meta.conflicts channel — losing inference persisted, never dropped. INV1 (no-profile identity) + INV2 (CSI-default no-op) pinned at parse level against the committed LibreOffice fixture; INV3 (deterministic override) + INV4 (conflicts persisted) + round-trip pinned as pure unit tests. Existing ARCAT/CPI golden inference tests and all 1250 unit tests unchanged and green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the numbering-profile REST API layer and wires it into the OpenAPI contract in a single commit so the contract gate stays green: - GET /libraries/:id/numbering-profiles — list (library + CSI Default) - POST /libraries/:id/numbering-profiles — create (201) - GET /numbering-profiles/:id — get | 404 - PATCH /numbering-profiles/:id — partial update | 404 - DELETE /numbering-profiles/:id — 204 | 404 | 409 (NumberingProfileInUseError) - PUT /specs/:id/numbering-profile — assign with pre-check | 404 - DELETE /specs/:id/numbering-profile — clear | 204 | 404 - POST /numbering-profiles/snapshot — DOCX upload → NumberingProfile (no persist) Snapshot deviation from the task brief: the brief described GET /specs/:id/numbering-profile/snapshot (parse a stored spec's source DOCX from the DB), but that requires source-DOCX retrieval which is not yet implemented. The upload-based POST /numbering-profiles/snapshot is feasible today and covers the core use-case (preview before persisting). Also adds extractNumberingProfileFromDocx(buffer) to the parser barrel (src/parser/docx/index.ts + src/parser/index.ts) so the snapshot handler can reuse the existing numbering.xml + styles.xml extraction pipeline. 42 integration tests pass; contract gate (8 tests) green; lint + build clean. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ma (#299) The DB column is always serialized (UUID, or null for the built-in CSI Default) — never an absent key — so it belongs in required for contract accuracy. The RESPONSE_ALLOWLIST exemption means the contract gate would not catch this drift; keep openapi exact (project rule). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a read-only MCP tool that returns the effective structural numbering profile for a spec. An unassigned spec resolves to the built-in CSI Default; an unknown spec UUID returns isError. Handler lives in numbering-profile-handler.ts (sibling pattern, keeps handlers.ts within the 400-line cap). Registration extracted to registerNumberingProfileTool() to keep registerSpecTools() under the 50-line function cap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…i accuracy (#299) Guard the built-in CSI Default numbering profile (library_id IS NULL) from accidental deletion via two layers of defense: 1. DB query: deleteNumberingProfile now uses AND library_id IS NOT NULL, so the built-in row is silently excluded even from direct callers. 2. Handler: deleteProfileHandler pre-checks the fetched row and returns 409 with a clear error when libraryId === null, before reaching the DB layer (mirrors setSpecProfileHandler style). 3. Regression test: 'DELETE /numbering-profiles/:id — refuses to delete the built-in CSI Default (409), built-in remains resolvable' — verifies 409 response and confirms the row survives in the database. 4. openapi.yaml: fix two prose inaccuracies — corrected DELETE description (removes the backward 'FK chain' reasoning; states the real protection is a 409 guard) and removes the false 'Unique' claim from the create-body name field description. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds structural numbering profiles across docs, schema, database, parser, REST/OpenAPI, and MCP layers. The parser can now extract and apply profiles, the API exposes CRUD and assignment endpoints, and tests cover the new data model and parse-time override flow. ChangesNumbering Profile Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
) Codex adversarial review found the symmetric partner of the deletable-built-in bug: PATCH /numbering-profiles/:id had no guard, so a caller could mutate the built-in CSI Default's name/rules and corrupt the fallback that getEffectiveNumberingProfile resolves for every unassigned spec. Add a shared rejectMissingOrBuiltIn guard (used by both PATCH and DELETE, removing the inline duplication), a regression test asserting PATCH on the built-in → 409 with rules untouched, and document the 409 + immutability in openapi. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Independent adversarial review — Codex (GPT-5.5,
|
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (2)
docs/superpowers/plans/2026-06-30-issue-299-numbering-profile.md (1)
194-221: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low valueCASCADE/RESTRICT interaction could block library deletion indirectly.
numbering_profiles.library_idcascades on library delete, butspecs.numbering_profile_idisRESTRICT. If a spec in a different library is assigned a profile owned by this library (cross-library assignment is allowed per the design), deleting the library will attempt to cascade-delete that profile, which will then hit theRESTRICTFK on the referencing spec and fail. Worth a comment/note in the design doc so the API layer can surface a clear error rather than a raw23503from an unexpected cascade path.🤖 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 `@docs/superpowers/plans/2026-06-30-issue-299-numbering-profile.md` around lines 194 - 221, Add a note in the design doc around the numbering profile migration to call out the CASCADE/RESTRICT interaction between numbering_profiles.library_id and specs.numbering_profile_id. Explain that deleting a library can fail if any spec, even in another library, references a profile owned by that library, and reference the numbering_profiles and specs relations so the API layer can surface a clear user-facing error instead of a raw foreign-key failure.src/mcp/handlers.test.ts (1)
44-73: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a rejected-dependency path test.
This suite covers found/missing specs, but not the contract that MCP tools return
{ isError: true }when a dependency throws. Please add a case wheregetSpecTree()orgetEffectiveNumberingProfile()rejects and asserthandleGetNumberingProfile()still resolves to an error payload instead of throwing. As per coding guidelines,src/mcp/**/*.ts: MCP tools must never throw; on failure they must return{ isError: true, content: [...] }.🤖 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/mcp/handlers.test.ts` around lines 44 - 73, The handleGetNumberingProfile test suite is missing coverage for the MCP failure contract when a dependency rejects. Add a case in handleGetNumberingProfile that mocks either db.getSpecTree or db.getEffectiveNumberingProfile to throw/reject, then assert the handler still resolves to an { isError: true, content: [...] } payload instead of propagating the exception. Use the existing handleGetNumberingProfile, getSpecTree, and getEffectiveNumberingProfile symbols to place the new test alongside the current known/unknown spec cases.Source: Coding guidelines
🤖 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 `@openapi.yaml`:
- Around line 3671-3675: The endpoint contract is describing the wrong fallback
behavior for clearing a spec profile. Update the OpenAPI text for the
spec-profile clear operation (the one that clears specs.numbering_profile_id) so
it says the spec falls back to the built-in CSI Default rather than a library
default, and keep the description aligned with the actual behavior implemented
by the clear/reset flow. Check the operation summary/description in openapi.yaml
for this endpoint and make the wording consistent with the rest of the
numbering-profile API.
In `@src/api/numbering-profiles.ts`:
- Around line 197-199: The MIME equality check in the upload handling path is
too strict and rejects valid DOCX files sent as generic multipart parts. Update
the `req.file.mimetype` validation in `numbering profiles` so it does not
require an exact match to `DOCX_MIME`; instead rely on the existing
filename/extension gating and the `assertDocxSafe(buffer)` payload validation
already used in this flow. Keep the rest of the upload logic unchanged and
preserve the current error handling around the DOCX parsing path.
In `@src/db/queries/numbering-profiles.integration.test.ts`:
- Around line 18-23: The cleanup in the numbering profiles integration test is
too broad because the afterEach block in numbering-profiles.integration.test.ts
deletes every custom numbering_profiles row, not just this suite’s np-test-*
fixtures. Update the cleanup logic around afterEach to scope deletions to the
test data created here, preferably by relying on the DELETE FROM libraries WHERE
name LIKE 'np-test-%' cascade or by narrowing the numbering_profiles delete to
rows tied to those same libraries. Ensure the existing FK-safe order in
afterEach remains intact while avoiding cross-suite fixture removal.
In `@src/db/queries/numbering-profiles.ts`:
- Around line 129-136: Reject built-in numbering profile updates in the DB
helper: updateNumberingProfile currently updates any row by id, which can still
mutate the shared CSI Default fallback row when library_id IS NULL. Update the
SQL in updateNumberingProfile to exclude built-in rows the same way
deleteNumberingProfile does, and make sure any internal callers via
src/db/index.ts cannot modify profiles without a library_id.
- Around line 208-217: The spec lookup in numbering-profiles is treating a
missing spec the same as an unassigned spec by always falling back to
resolveBuiltInRules in get numbering profile flow. Update the query handling
around result.rows[0], rawRules, and parseRules so you first detect when no spec
row was returned for the given specId and surface a not-found error/404 path
before applying the CSI Default fallback; keep the built-in fallback only for
existing specs with null rules.
In `@src/mcp/numbering-profile-handler.ts`:
- Around line 26-28: The catch block in get_numbering_profile currently logs and
returns a generic MCP error while exposing only the raw exception; introduce a
module-owned typed error class for this handler that extends SpecrError, wrap
the caught failure with the original err as cause, and use that typed error for
logging/returning the toolErr payload. Update the numbering-profile handler path
so any added context is preserved through the error chain rather than swallowing
it or surfacing a plain Error.
- Around line 22-24: The existence check in the numbering profile handler is
split across getSpecTree and getEffectiveNumberingProfile, which causes an
unnecessary full spec load and can race with deletion. Update the handler in
numbering-profile-handler.ts to use a single lookup path that determines both
whether the spec exists and what profile to return, ideally by folding the “spec
missing” case into getEffectiveNumberingProfile or a cheap existence helper.
Make sure the existing logic in the handler and the getEffectiveNumberingProfile
helper remains the place where the missing-spec vs default-profile distinction
is handled.
In `@src/parser/docx/index.ts`:
- Around line 204-216: The `numberingProfile === undefined` flow in
`src/parser/docx/index.ts` is no longer preserving the original parse behavior
when `comments.xml` is present because `commentsById` is always populated and
passed into `parseDocument()`/`parseParagraphSources(...)`. Update
`classifyWithOptionalProfile` so the no-profile path continues to use the
pre-profile parsing logic from `parseDocument()` without comments affecting
source parsing, while the profile-enabled branch can still use `commentsById` as
needed. Use the `commentsById`, `classifyWithOptionalProfile`, and
`parseDocument`/`parseParagraphSources` symbols to keep the fix localized.
In `@src/parser/docx/numbering-profile-apply.integration.test.ts`:
- Around line 35-59: The backward-compat invariant tests in
numbering-profile-apply.integration.test.ts only cover one LibreOffice CSI
fixture, so the new inference override path is still missing ARCAT and CPI
coverage. Extend this suite with representative ARCAT and CPI DOCX fixtures and
assert the same no-profile vs explicit default-profile invariants through parse,
project, renderMarkdown, and sectionInference. Use the existing parse-based test
pattern and keep the checks centered on the numbering-profile behavior so
regressions in the ARCAT/CPI normalization split are caught.
In `@src/parser/docx/numbering-profile.ts`:
- Around line 48-65: `buildStyleLadder()` is serializing all `pStyleToNumId` and
`resolvedNumPr` entries, including non-spec-shaped list styles that should not
become structural overrides. Update `buildStyleLadder()` to filter entries
against the spec-shaped numbering set (the same `specShapedNumIds`/numbering
criteria used by `buildNumbering()`), so only valid structural styles are added
to `ladder`; keep `applyNumberingProfile()` unchanged except for consuming the
corrected ladder. Add a regression test covering a generic bullet/flat-list
`pStyle` mapping at `ilvl=0` to verify it is excluded and does not replay as a
`tier: 'part'` override.
---
Nitpick comments:
In `@docs/superpowers/plans/2026-06-30-issue-299-numbering-profile.md`:
- Around line 194-221: Add a note in the design doc around the numbering profile
migration to call out the CASCADE/RESTRICT interaction between
numbering_profiles.library_id and specs.numbering_profile_id. Explain that
deleting a library can fail if any spec, even in another library, references a
profile owned by that library, and reference the numbering_profiles and specs
relations so the API layer can surface a clear user-facing error instead of a
raw foreign-key failure.
In `@src/mcp/handlers.test.ts`:
- Around line 44-73: The handleGetNumberingProfile test suite is missing
coverage for the MCP failure contract when a dependency rejects. Add a case in
handleGetNumberingProfile that mocks either db.getSpecTree or
db.getEffectiveNumberingProfile to throw/reject, then assert the handler still
resolves to an { isError: true, content: [...] } payload instead of propagating
the exception. Use the existing handleGetNumberingProfile, getSpecTree, and
getEffectiveNumberingProfile symbols to place the new test alongside the current
known/unknown spec cases.
🪄 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: cc653075-4879-418c-adae-0e4fbfabd44f
📒 Files selected for processing (25)
docs/superpowers/plans/2026-06-30-issue-299-numbering-profile.mddocs/superpowers/specs/2026-06-29-issue-299-numbering-profile-design.mdopenapi.yamlsrc/api/contract.integration.test.tssrc/api/numbering-profiles.integration.test.tssrc/api/numbering-profiles.tssrc/api/router.tssrc/ast/index.tssrc/ast/numbering-profile-schema.test.tssrc/ast/numbering-profile-schema.tssrc/ast/schemas.tssrc/db/index.tssrc/db/migrations/038_create_numbering_profiles.tssrc/db/queries/numbering-profiles.integration.test.tssrc/db/queries/numbering-profiles.tssrc/mcp/handlers.test.tssrc/mcp/handlers.tssrc/mcp/numbering-profile-handler.tssrc/mcp/tools.tssrc/parser/docx/index.tssrc/parser/docx/numbering-profile-apply.integration.test.tssrc/parser/docx/numbering-profile-apply.test.tssrc/parser/docx/numbering-profile.test.tssrc/parser/docx/numbering-profile.tssrc/parser/index.ts
…#299) Defense-in-depth beneath the handler 409: updateNumberingProfile now scopes its UPDATE with `AND library_id IS NOT NULL`, so the built-in CSI Default can never be mutated even by a direct query caller — it backs getEffectiveNumberingProfile for every unassigned spec, so a mutated default would corrupt that global fallback. Mirrors the delete guard. Regression test asserts the update returns null and the seeded rules are untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the last mile: until now applyNumberingProfile ran only in tests — POST /parse applied nothing in production. POST /parse now accepts an optional multipart `numberingProfileId`; the handler resolves it (404 if missing) before the job starts and threads the profile's rules through processParseJob → parsePool → parse-worker → parse() as the numberingProfile parse option. Absent the field, the call shape is byte-for-byte unchanged (no default injected) — backward-compat preserved. WorkerInput carries the profile as a plain JSON object (structured-cloneable across the Piscina boundary). openapi documents the new field + the 404. Reparse-on-assign of an already-parsed spec stays out of scope: SpecR stores no source DOCX (ADR-021), so profile application happens at parse/upload time. Tests: parse-worker unit asserts the profile is threaded into parse() options (and omitted when absent); parse integration asserts a missing profileId → 404 and the built-in default threads through as a passthrough (same node count as no profile). The override's tier/conflict behavior is pinned at the parser unit level (INV3/INV4); a numbering-driven golden e2e needs the gitignored ARCAT fixtures (CI-only). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex adversarial review — disposition (round 2)Following maintainer review of the Codex pass, here is the final disposition of the 3 findings. 2 fixed, 1 declined. Fixed
Declined (with rationale)
StatusLocal checks green: |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/api/parse.ts (1)
182-197: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy liftType assertion across the worker boundary masks a schema gap.
workerOutputSchema.parse(workerRaw) as WorkerOutputcasts past the fact thattree.partsis only validated asz.array(z.unknown()), not as the actualSpecNode[]shapeSpecTree/WorkerOutputrequire. The strict-TS guideline bans type assertions across module boundaries precisely because this kind of cast hides a structural mismatch that later flows unchecked intofinalTree/persistParsedSpec.This is a pre-existing schema-completeness gap relocated into the new
runParseWorkerhelper, not a regression from this PR's numbering-profile change, so it's a recommended (not essential) follow-up — e.g., tighteningworkerOutputSchema'spartstyping (or a documented, narrower assertion) would remove the need for the boundary-wide cast.♻️ Illustrative direction (schema tightening, not a full fix)
- tree: z.object({ - id: z.string(), - section: z.union([SectionNumberSchema, z.literal('unknown')]), - title: z.string(), - parts: z.array(z.unknown()), - warnings: z.array(ParseWarningSchema).optional(), - }), + tree: z.object({ + id: z.string(), + section: z.union([SectionNumberSchema, z.literal('unknown')]), + title: z.string(), + parts: z.array(SpecNodeSchema), // replace z.unknown() with a real shape check + warnings: z.array(ParseWarningSchema).optional(), + }),As per coding guidelines, "TypeScript must stay strict... no type assertions across module boundaries."
🤖 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/parse.ts` around lines 182 - 197, The new runParseWorker helper is crossing the worker boundary with a type assertion that hides an incomplete schema. Remove the boundary-wide cast in runParseWorker and make workerOutputSchema validate the full WorkerOutput shape, especially tree.parts as SpecNode[] rather than z.array(z.unknown()). Tighten the schema (or introduce a narrower, documented conversion inside the worker contract) so parsePool.run results are fully type-safe before they flow into finalTree and persistParsedSpec.Source: Coding guidelines
🤖 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/parse.ts`:
- Around line 182-197: The new runParseWorker helper is crossing the worker
boundary with a type assertion that hides an incomplete schema. Remove the
boundary-wide cast in runParseWorker and make workerOutputSchema validate the
full WorkerOutput shape, especially tree.parts as SpecNode[] rather than
z.array(z.unknown()). Tighten the schema (or introduce a narrower, documented
conversion inside the worker contract) so parsePool.run results are fully
type-safe before they flow into finalTree and persistParsedSpec.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d3d0be25-ec40-460f-ac87-9bdcbaeeb383
📒 Files selected for processing (7)
openapi.yamlsrc/api/parse.integration.test.tssrc/api/parse.tssrc/db/queries/numbering-profiles.integration.test.tssrc/db/queries/numbering-profiles.tssrc/lib/parse-worker.test.tssrc/lib/parse-worker.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- src/db/queries/numbering-profiles.integration.test.ts
- openapi.yaml
- src/db/queries/numbering-profiles.ts
…onflicts (#299) Proves the full POST /parse override path with a real numbering-driven document built in-test (no committed binary): an abstractNum whose ilvl-0 lvlText declares PART makes its numId spec-shaped, so structure is decided by Signal 1 (numbering), not Signal 4 (text) — unlike the only committed DOCX fixtures, which are text-driven and immune to a numbering override. With no profile the ilvl-2 paragraph persists as pr1; re-parsed through POST /parse with a profile that sets articleIlvl=2 it shifts to article and the losing base inference (pr1) is persisted in meta.conflicts. Closes the gap left when the override behavior was only pinned at the unit level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wired the numbering profile into POST /parse — #299 override path now runs in production
Scope: the onboarding bulk-import path ( Local checks green: |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/api/parse.integration.test.ts (2)
526-533: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCreated library/profile rows aren't cleaned up.
cleanupIdsonly tracksspecId(line 520); the library created at line 526 and the numbering profile created at line 533 are never deleted. Unlike thespecscleanup at line 512 (which is idempotent across runs because it's keyed by a fixedsection), these rows use a random suffix and will accumulate indefinitely in the test database across runs.♻️ Proposed cleanup
const profile = await createNumberingProfile(lib.id, 'Shift articleIlvl', overrideRules); + cleanupIds.push(profile.id); // if cleanup logic supports multiple id "kinds", or add dedicated teardown🤖 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/parse.integration.test.ts` around lines 526 - 533, The cleanup in this test only tracks the spec identifier, so the library created with createLibrary and the numbering profile created with createNumberingProfile are left behind and accumulate across runs. Update the test setup/teardown around these calls to register both lib.id and profile.id in the cleanup list (or otherwise delete them in the existing cleanup path), using the existing createLibrary, createNumberingProfile, and cleanupIds flow so the test remains idempotent.
508-550: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winTight timeout margin for the chained two-job e2e flow.
This test runs two sequential
postBuffer+waitForJobcycles under a single 30s test timeout (line 550), butwaitForJob's own internal deadline (line 56) defaults to 20s per call. If either parse job is slow under load (e.g., CI contention), the combined wall-clock time can approach or exceed the outer 30s timeout before eitherwaitForJobcall has a chance to throw its own descriptive error, producing a generic Vitest timeout failure instead of the more informativejob ${jobId} did not complete within ${maxMs}mserror.♻️ Proposed fix — widen the outer timeout margin
- }, 30_000); + }, 50_000);🤖 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/parse.integration.test.ts` around lines 508 - 550, The chained end-to-end test in parse.integration.test.ts is too close to the 30s outer timeout when it runs two sequential postBuffer and waitForJob cycles. Update the timeout on this specific it(...) case to leave enough margin above waitForJob’s internal deadline so slow CI runs fail with the descriptive job timeout instead of Vitest’s generic timeout. Keep the test logic and assertions in place; only widen the test-level timeout for this scenario.
🤖 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/parse.integration.test.ts`:
- Around line 526-533: The cleanup in this test only tracks the spec identifier,
so the library created with createLibrary and the numbering profile created with
createNumberingProfile are left behind and accumulate across runs. Update the
test setup/teardown around these calls to register both lib.id and profile.id in
the cleanup list (or otherwise delete them in the existing cleanup path), using
the existing createLibrary, createNumberingProfile, and cleanupIds flow so the
test remains idempotent.
- Around line 508-550: The chained end-to-end test in parse.integration.test.ts
is too close to the 30s outer timeout when it runs two sequential postBuffer and
waitForJob cycles. Update the timeout on this specific it(...) case to leave
enough margin above waitForJob’s internal deadline so slow CI runs fail with the
descriptive job timeout instead of Vitest’s generic timeout. Keep the test logic
and assertions in place; only widen the test-level timeout for this scenario.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: abd577fb-2a18-4644-8eef-a94badac3284
📒 Files selected for processing (1)
src/api/parse.integration.test.ts
- api: snapshot upload accepts octet-stream/empty MIME (extension + assertDocxSafe are the real validation); strict equality rejected legit .docx parts - db: getEffectiveNumberingProfile returns null for a missing spec instead of silently resolving CSI Default, so callers can 404 a missing spec - mcp: get_numbering_profile does a single effective-profile lookup (no full-tree existence fetch, no delete-between-awaits race); wrap the catch in McpError(cause) - parser: buildStyleLadder filters to spec-shaped numIds so a generic flat-list style at ilvl=0 is not serialized as a structural 'part' tier - test: scope the integration teardown to np-test-% fixtures (was wiping every custom numbering_profiles row in the shared DB); add ARCAT+CPI no-profile backward-compat cases (skip-if-absent, CI-only fixtures) - openapi: clear-profile op falls back to the built-in CSI Default, not a library default Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeRabbit review — round handled (commit
|
…#299) CodeRabbit T4: collapsing 'spec not found' into the built-in CSI Default hid a missing spec behind the unassigned-fallback. Return null when the spec row is absent so callers map missing→404 distinctly from unassigned→built-in; JSDoc updated to match. CodeRabbit T3: drop the broad DELETE FROM numbering_profiles in teardown — the np-test-% library delete already cascades to its profiles, so the broad delete only risked clobbering other suites' rows in the shared DB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/parser/docx/numbering-profile-apply.integration.test.ts (1)
73-81: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd
sectionInferenceequality to the new ARCAT/CPI suites for parity with the LibreOffice suite.The LibreOffice backward-compat suite asserts
sectionInferenceequality in addition to tree/markdown, but these new ARCAT and CPI suites only checkproject()andrenderMarkdown(). SincesectionInferenceis part ofParseResultand should also be byte-for-byte unaffected by the no-op profile path, add the same check here for full parity and to catch any inference-metadata regression these two riskier fixture families might introduce.♻️ Proposed addition
expect(project(explicitNoProfile.tree)).toEqual(project(without.tree)); expect(project(withDefault.tree)).toEqual(project(without.tree)); expect(renderMarkdown(withDefault.tree)).toBe(renderMarkdown(without.tree)); + expect(withDefault.sectionInference).toEqual(without.sectionInference);Also applies to: 87-96
🤖 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/parser/docx/numbering-profile-apply.integration.test.ts` around lines 73 - 81, The ARCAT/CPI backward-compat tests in the numbering-profile suite are missing the same ParseResult parity check used by the LibreOffice suite. Update the relevant cases in the docx numbering-profile integration tests to also assert equality on `sectionInference` for the `parse(...)` results, alongside the existing `project(...)` and `renderMarkdown(...)` comparisons, so the no-op profile path is validated across tree, markdown, and inference metadata.
🤖 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/parser/docx/numbering-profile-apply.integration.test.ts`:
- Around line 73-81: The ARCAT/CPI backward-compat tests in the
numbering-profile suite are missing the same ParseResult parity check used by
the LibreOffice suite. Update the relevant cases in the docx numbering-profile
integration tests to also assert equality on `sectionInference` for the
`parse(...)` results, alongside the existing `project(...)` and
`renderMarkdown(...)` comparisons, so the no-op profile path is validated across
tree, markdown, and inference metadata.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6fe542f0-85e8-4491-a8b2-b0a7edf94cc9
📒 Files selected for processing (8)
openapi.yamlsrc/api/numbering-profiles.tssrc/db/queries/numbering-profiles.integration.test.tssrc/db/queries/numbering-profiles.tssrc/mcp/handlers.test.tssrc/mcp/numbering-profile-handler.tssrc/parser/docx/numbering-profile-apply.integration.test.tssrc/parser/docx/numbering-profile.ts
🚧 Files skipped from review as they are similar to previous changes (5)
- src/mcp/handlers.test.ts
- src/parser/docx/numbering-profile.ts
- src/api/numbering-profiles.ts
- src/db/queries/numbering-profiles.integration.test.ts
- openapi.yaml
…gaps An independent Codex (GPT-5.5, xhigh) adversarial pass surfaced four correctness gaps in the #299 numbering-profile feature; all verified against the code and fixed with regression tests: - parser: reparse base paragraphs before recording profile-vs-inference conflicts. parseParagraph resolves style-inherited numId/ilvl from the map at parse time, so reusing the profile-parsed paragraphs let the un-profiled comparison agree with the profile and silently drop the losing base inference from meta.conflicts. Parse a second time against the base map. (P2) - db: enforce library scoping on setSpecNumberingProfile — a spec may be assigned only the built-in CSI Default or a profile owned by its own library. Prevents a cross-library assignment that hides the profile from the spec library's scoped list and blocks the owning library's deletion via the RESTRICT FK. Handler maps the mismatch to 409. (P2) - openapi: document the required tiers.part shape (integer numberStyle, maxCount 1..5) rather than a generic object, so a docs-following client no longer gets a surprise 422. (P2) - ast: trim numbering-profile names before the length check so a whitespace-only name is a 422 at the boundary, not a pg 23514 CHECK violation surfaced as 500. (P3) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🤖 Codex (GPT-5.5, xhigh) adversarial review — additional eyes CodeRabbit reviewed this PR normally; Codex ran as a second, independent adversarial pass and surfaced 4 correctness gaps in the numbering-profile feature. All were verified against the code and fixed in
Verification: |
|
@coderabbitai review |
✅ Action performedReview finished.
|
…rialization + OpenAPI items A second Codex (GPT-5.5, xhigh) adversarial pass over the first fix batch (1c9ae66) surfaced three more findings; two are fixed here, one is documented and deferred: - parser: makeContinuationNode now carries meta.conflicts (mirrors makeNode). A profile can demote a paragraph to 'continuation' while the un-profiled base inference disagreed; that losing signal was appended to cp.conflicts but then dropped at serialization because the continuation-node builder omitted conflicts. Regression test asserts a demoted continuation keeps its conflict. (P2) - openapi: tighten the NumberingProfile numbering[] and styleLadder[] item schemas to mirror the Zod contract (required numId/levels, ilvl/tier, styleId, ...) rather than arbitrary objects, so a docs-generated client can't send a body the API then rejects with 422. (P2) - KNOWN AMBIGUITY (#319): the profile `tier` field is written by the extractor (tierForIlvl) and never read on apply — classification derives the node type from ilvl + articleIlvl, so editing `tier` alone is a silent no-op. Pinned by a KNOWN-AMBIGUITY test + a design-doc note; making `tier` independently authoritative is deferred to #319 by decision. (P2, deferred) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
🤖 Codex (GPT-5.5, xhigh) re-review of the fix batch
Verification: |
#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>
Extend the web_ui_demo reference client to exercise recently-landed API surface, all within examples/ (no src/ changes). Numbering profiles (#299/#317/#320) — a new "Numbering" view: - per-library profile CRUD (list built-in CSI Default + custom, create, delete), - DOCX snapshot extractor (POST /numbering-profiles/snapshot, no persistence), - at-ingress integration: the Library view's numbering picker forwards numberingProfileId to POST /parse — the real "profile at formatting ingress". - api.js gains the 8 numbering endpoints; sendJson now treats 204 as success (association/profile deletes carry no body). MCP chat sidebar ("Ask SpecR") — an OpenAI-backed assistant: - browser holds no key; it POSTs the conversation to the demo server's /chat, which runs the OpenAI tool-calling loop and bridges each call to SpecR's stateless POST /mcp (tools auto-discovered via tools/list — all 20 today). - OPENAI_API_KEY stays server-side; absent key degrades to a clear "not configured" note. Zero new dependencies (raw fetch). Pattern mirrors WrzDJ. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Why
The 5-signal DOCX inference engine assumes the CSI integer-PART model and guesses each document's numbering/hierarchy per file. For a genuinely non-standard source that posture is wrong: the operator should be able to declare the source's structural numbering scheme once at ingress and have it applied deterministically — the structural sibling of #125's visual style template. This adds a saveable, library-scoped structural numbering profile: its data model, the ingress snapshot→edit→assign→apply loop, a deterministic override of inference signals 1–2, and the REST + MCP surface.
Indentation (Signal 5) stays the engine's job (out of scope). Firm/client/project/package scope tiers, the onboarding UI, MCP write tools, auto-detection, and re-rendering deviant specs (#146) are all deliberately deferred.
What
A
NumberingProfileis the serializable, operator-editable projection of the parser's internalNumberingMapclassification fields (articleIlvl,pStyleladders,specShapedNumIds) plus tier bounds. The parser stays pure — it gains an optionalnumberingProfilethroughParseOptions; the API/ingress layer resolvesspec.numbering_profile_id ?? built-in 'CSI Default'and injects it (the #304 "engine stays pure, caller resolves context" contract).ast/—NumberingProfileSchema(Zod v4, open via.catchall), the foundational type consumed bydb/parser/api.db/— migration 038 (numbering_profilestable +specs.numbering_profile_idFKON DELETE RESTRICT+ a frozen, seeded built-in 'CSI Default' singleton), CRUD queries, andgetEffectiveNumberingProfileresolution.parser/—extractNumberingProfile(snapshot: map → profile) andapplyNumberingProfile(override: profile → map), round-trip inverses; the optional profile threaded throughparse → parseDocx → buildClassification. When a profile is present, paragraphs are classified twice (overridden vs. base) and per-paragraph disagreements are recorded inparagraphs.conflicts/meta.conflicts— losing inference persisted, never dropped.api/— library CRUD, spec assign/clear, a snapshot endpoint, withopenapi.yamlupdated in lockstep (contract gate green).mcp/— read-onlyget_numbering_profile.The hard invariant: absent a profile (or with the empty built-in 'CSI Default'), the produced AST is byte-for-byte today's behavior. This holds two ways — the no-profile path is the unchanged code, and an empty profile is a reference-preserving passthrough (overrides a field only when the profile actually specifies it).
Design decisions (plan deviations and why)
038, not the next-free037.037is claimed by the open PR feat(db): keynote master table + project-filtered keynote query (ADR-016) #315 (feat/issue-98);038avoids a collision when that merges.POST /numbering-profiles/snapshot(multipart DOCX upload), not the plannedGET /specs/:id/numbering-profile/snapshot. SpecR discards raw OOXML after ingest (ADR-021) — the AST is the source of truth, so there is no stored source DOCX to re-parse for an existing spec, and the lossy AST cannot reconstruct numId/lvlText/pStyle ladders faithfully. An upload endpoint is the faithful realization of the spec's ingress-snapshot intent ("emit a draft from the real source," flow step 1, before the spec is saved). A thin parser-barrelextractNumberingProfileFromDocx(buffer)keeps the API off docx internals.NULL(not an FK to the built-in), so theRESTRICTFK never fires for it. Without a guard, deleting the built-in would 500 every unassigned spec's resolution. Guarded at the DB layer (AND library_id IS NOT NULL) and the handler (clear 409), with a regression test.numbering[]is filtered to spec-shaped numIds in the snapshot, sospecShapedNumIdsreconstructs exactly on round-trip (generic list numIds don't get a spuriousparttier).Non-blocking follow-ups (future issues)
src/mcp/tool-result.tsto retire per-handlerToolResultredefinitions.meta.conflicts(today pinned at themergeProfileConflictsunit boundary).meta.conflicts(pre-existing); a profile that demotes a structural node to a continuation drops that conflict.parse.ts.Testing
vitest --project unit), incl. byte-for-byte INV1/INV2, override INV3, conflict INV4, and round-trip symmetry.vitest --project integration); 115 skipped are the copyrighted, gitignored ARCAT/CPI fixtures (absent locally; they run in CI) plus other fixture-gated suites.src/api/contract.integration.test.ts) — bidirectional route↔spec coverage for all 8 new routes;openapi.yamlupdated in the same PR.pnpm lint: eslint + tsc --noEmit + prettier) and build clean (pnpm build).🤖 Co-authored by Claude Opus 4.8. Closes #299.
Summary by CodeRabbit
#299) with library-scoped CRUD, DOCX snapshot extraction, and per-spec assignment/clearing, including an immutable built-in “CSI Default”.POST /parseto accept optionalnumberingProfileIdfor deterministic DOCX parse-time overrides (returns404when missing) and to record resolved conflicts.