Harden Docs spaces to a fixed {Technical, Product} vocabulary - #33
Conversation
Design spec: make doc 'space' a hard two-value enum enforced on every surface (contract, MCP tools, gate, UI tabs, triage assign), default 'technical', and migrate the single stray 'sapling' doc. Removes the data-derived Sapling tab. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Docs tabs were data-derived (any doc 'space' value became a tab) while
the write side spoke an unmatched sapling|canopy vocab defaulting to canopy —
so a stray 'sapling' doc surfaced a Sapling tab, and new agent docs would have
spawned a Canopy tab.
- space is now a hard enum {technical, product} on the write contract, the
query/propose_doc_update MCP tools, the gate, and writes.ts; new docs default
to 'technical'. An off-vocab space is rejected at the tool boundary.
- The Docs UI renders a FIXED two-tab set (DOC_SPACES) instead of deriving tabs
from data, so a stray/foreign space can never add or change a tab.
- The triage 'assign' surface and the /search space filter move in lockstep.
- Migration 0020 folds any pre-existing off-vocab space (the one 'sapling' doc,
an engineering reference) into 'technical'.
Tests: default→technical, explicit product persists, off-vocab rejected, and a
render test asserting exactly the two fixed tabs. typecheck + web build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe PR standardizes Docs spaces to ChangesDocs space vocabulary
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant MCP tool
participant Contract validation
participant Write tool
participant Docs database
MCP tool->>Contract validation: submit space
Contract validation->>Write tool: accept technical/product
Write tool->>Docs database: persist normalized space
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
canopy | 6e0ca1e | Commit Preview URL Branch Preview URL |
Jul 10 2026, 06:08 AM |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@src/tools/writes.ts`:
- Line 463: In the triage item construction near the `space` assignment,
normalize `raw.space` before passing it to `DocProposal.parse()`: preserve
`target.space` when provided, otherwise accept only `"technical"` or `"product"`
from `raw.space`, and use `undefined` for values such as `"sapling"` or any
other legacy value.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 264e8404-cdde-4ac7-9996-0a858adc662e
📒 Files selected for processing (20)
docs/superpowers/specs/2026-07-10-harden-docs-spaces-design.mdmigrations/0020_docs_space_vocab.sqlshared/contract.tsshared/rows.tssrc/consumer.tssrc/mcp.tssrc/routes.tssrc/tools/writes.tstest/consumer.reconcile.test.tstest/ingest.route.test.tstest/mcp.propose_doc.test.tstest/render.docs.test.tstest/render.review.test.tstest/triage-map.test.tstsconfig.web.jsontsconfig.worker.jsonweb/src/api.tsweb/src/main.tsweb/src/render.tsweb/src/triage-map.ts
| section, | ||
| confidence: "high", // human-vouched on assign | ||
| space: target.space ?? (raw.space as "sapling" | "canopy" | undefined), | ||
| space: target.space ?? (raw.space as "technical" | "product" | undefined), |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- locate relevant files ---'
git ls-files | rg '^(src/tools/writes\.ts|src/.*|migrations/.*|.*0020.*)$' || true
echo '--- find DocProposal and raw.space usages ---'
rg -n 'DocProposal|raw\.space|space: target\.space' src migrations -S
echo '--- outline candidate files if available ---'
for f in src/tools/writes.ts; do
if [ -f "$f" ]; then
echo "## $f"
ast-grep outline "$f" --view expanded || true
fi
doneRepository: SaplingLearn/canopy
Length of output: 3783
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- writes.ts around the proposal parse ---'
sed -n '440,470p' src/tools/writes.ts | cat -n
echo '--- consumer.ts DocProposal handling ---'
sed -n '100,170p' src/consumer.ts | cat -n
echo '--- shared contract definition for DocProposal ---'
rg -n 'export (type|interface) DocProposal|space' src shared -S
echo '--- migration 0020 ---'
sed -n '1,220p' migrations/0020_docs_space_vocab.sql | cat -n
echo '--- earlier triage/migration touching raw JSON ---'
rg -n 'needs_triage|raw' migrations src -SRepository: SaplingLearn/canopy
Length of output: 21030
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,60p' shared/contract.ts | cat -nRepository: SaplingLearn/canopy
Length of output: 2809
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- search historical docs-space changes ---'
rg -n 'sapling|canopy|space.*technical|space.*product|doc_space|docs_space|vocabulary' migrations src shared -S
echo '--- inspect the docs space migration ---'
sed -n '1,200p' migrations/0005_doc_space.sql | cat -n
echo '--- inspect triage table creation ---'
sed -n '40,70p' migrations/0001_init.sql | cat -nRepository: SaplingLearn/canopy
Length of output: 7161
Filter raw.space before parsing triage items
DocProposal.parse() only accepts "technical" or "product", so a legacy triage row with raw.space = "sapling" will fail assignment unless target.space is set. Normalize the raw value to one of the allowed enums and fall back to undefined for old items.
🤖 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/tools/writes.ts` at line 463, In the triage item construction near the
`space` assignment, normalize `raw.space` before passing it to
`DocProposal.parse()`: preserve `target.space` when provided, otherwise accept
only `"technical"` or `"product"` from `raw.space`, and use `undefined` for
values such as `"sapling"` or any other legacy value.
Why
The Docs page showed a stray Sapling tab alongside Technical and Product. Root cause: two disconnected vocabularies.
render.tstook whatever distinctspacevalues existed and made a tab per value. Any value became a tab.spacetosapling | canopyand defaulted new docs tocanopy— a vocabulary that never matched the Technical | Product model the UI was rebuilt around. (Side effect: the next agent-written doc would have spawned a Canopy tab.)Nothing pinned the tab set. The Sapling tab was simply the one doc still carrying
space='sapling'(sapling-frontend-local-dev, an engineering reference).What
Make
spacea real controlled vocabulary of exactly{technical, product}, enforced on every surface:{technical, product}on the write contract (DocProposal,QueryRequest), thequery+propose_doc_updateMCP tools, the gate, andtools/writes.ts. Omitted → defaultstechnical; an off-vocab value is rejected at the tool boundary (an error, not silent triage) so an agent can't widen the tab set.DOC_SPACES = ["technical","product"]directly instead of deriving tabs from data, so a stray/foreignspacecan never add or change a tab.GET /searchspace filter move in lockstep totechnical|product.0020_docs_space_vocab.sql— idempotent, self-defending: folds any pre-existing off-vocab space (the onesaplingdoc → correctly Technical) into the default. No-op on fresh local/test DBs; no FTS rebuild needed.Scope is spaces/tabs only — the parallel section-vocab disconnect is intentionally left alone (noted in the spec).
Testing
npm run typecheck— green (also addedtest/render.docs.test.tsto the tsconfig include/exclude split, matching the existing web-importing-test convention).npm test— the doc-space suite passes: default→technical, explicitproductpersists, off-vocab rejected at the boundary, and a render test asserting exactly the two fixed tabs. Updated the prior sapling/canopy assertions inconsumer.reconcile,mcp.propose_doc,ingest.route,triage-map,render.review.npm run build:web— succeeds.summarize.test.ts(environmental: a realGEMINI_API_KEYin.dev.varsmakes the "key-unset → excerpt" case return a real Gemini result) fails identically onmain.Deploy notes
Two prod steps after merge:
npm run db:migrate:remote— applies0020(removes the Sapling tab from live data; the currently-deployed data-derived frontend drops the tab on this alone).npm run deploy— activates the fixed-tabs UI + the write-side enum.Design spec:
docs/superpowers/specs/2026-07-10-harden-docs-spaces-design.md.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes