Skip to content

feat: doc-mode backend — single-file portable idea format + SQLite→doc export#88

Merged
velvetmonkey merged 13 commits into
mainfrom
doc-mode-wedge
May 13, 2026
Merged

feat: doc-mode backend — single-file portable idea format + SQLite→doc export#88
velvetmonkey merged 13 commits into
mainfrom
doc-mode-wedge

Conversation

@velvetmonkey

Copy link
Copy Markdown
Owner

Summary

Introduces a single-file portable doc-mode backend for ideas, alongside the existing SQLite ledger.

  • Format spec (docs/single-doc-format.md) — one idea per .md with structured sections (claim, assumptions, evidence log, verdict, lesson) and frontmatter (id, title, state, timestamps).
  • Core handlers — create / read / list / transition for doc-mode, with conformance tests covering round-trip, state/verdict invariants, and transition order.
  • MCP boundarybackend: \"sqlite\" | \"doc\" flag on idea.* actions with a gate that rejects mode-incompatible calls.
  • Doctordoctor.consistency supports mode: \"sqlite\" | \"doc\" | \"both\" and validates doc files against format invariants.
  • idea.export_doc — one-way SQLite → doc-mode migration so existing ideas can move into the portable format.

Wedge scope (export-only, by design)

This branch ships export but deliberately does not ship import_doc. You can move an idea from SQLite to a doc file; you cannot move it back. If someone edits an exported .md and expects it to sync into the SQLite ledger, that will not work — there is no read-back path.

import_doc is its own follow-on ticket. The wedge is intentionally export-only so the format and conformance tests can be reviewed and adopted without committing to a bidirectional sync surface (which has its own conflict-resolution / lineage-merge / outcome-reconciliation questions).

Field-by-field exclusions in idea.export_doc

What the SQLite ledger tracks but doc-mode does not carry over:

Field Status Rationale
Council sessions / dispatches Permanently excluded Doc-mode is an idea-format, not an interaction log. Sessions belong with the orchestration layer that produced them.
Lineage (ancestry / descendants / supersedes) Deferred Lineage is a graph across many ideas; representing it inside a single .md would either denormalize the graph or require a sidecar index. Worth its own design pass.
Cross-idea citations Deferred Same shape problem as lineage — pointers from one doc into another need a resolver, which doc-mode does not yet have.
Propagation flags Permanently excluded These are SQLite-mode bookkeeping for the propagation engine; they have no meaning in a portable file format.
Private memo context (decision-journal blob) Permanently excluded by default Doc-mode files are portable / shareable; the private memo is opaque, not always shareable, and the structured lesson + verdict rationale already surface the publishable substance.
Most-recent outcome → verdict Included (lossy) Latest non-undone outcome → verdict block (pass/fail/parked) + rationale + lesson. Historical outcomes are not retained in doc-mode.

Test plan

  • CI green on packages/mcp-server suite (locally: 11 files / 221 passed / 1 skipped / 26.31s with --pool=forks)
  • Round-trip test (doc-mode-export.test.ts) covers: assumption decision-clause preference, verdict derivation from outcome verdicts, overwrite guard rejects on existing file, default path under ideas-doc/<slug>-<id>.md
  • doctor.consistency({ mode: \"doc\" }) validates an exported file without errors
  • Manual check: export an SQLite idea with refuted assumptions, confirm status: refuted lands in the doc; export one with no outcomes, confirm verdict: null
  • No regressions on existing doctor.consistency({ mode: \"sqlite\" }) path

Follow-ups (separate tickets)

  • idea.import_doc — read a doc-mode .md back into the SQLite ledger (round-trip viability)
  • Lineage representation in doc-mode (sidecar vs in-frontmatter graph references)
  • Cross-doc citation resolver

🤖 Generated with Claude Code

velvetmonkey and others added 13 commits May 13, 2026 00:05
Match the header.png banner convention used by flywheel-memory,
flywheel-concept, and flywheel-geometry. flywheel-ideas was the
only sibling without one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
idea.ts was 1,340 lines with 12 action handlers in a single switch.
Split each handler into its own module under tools/idea/:

  register.ts  — tool registration + zod schema + dispatch + error boundary
  create.ts    — handleCreate
  read.ts      — handleRead + buildReadNextSteps
  list.ts      — handleList
  transition.ts — handleTransition + buildTransitionNextSteps
  forget.ts    — handleForget
  freeze.ts    — handleFreeze + handleListFreezes
  lineage.ts   — handleAncestry/Descendants/SharedAssumptions
  export.ts    — handleExport
  report.ts    — handleReport

tools/idea.ts kept as a one-line re-export shim so existing imports
(src/index.ts) are unchanged. Zero behavior or response-shape change;
the full test suite (763 core + 175 mcp-server) passes unmodified.

Closes the active hardening item from the roadmap: lower review cost
before bimodal SQLite/doc-mode handler work lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
schema.ts has carried SCHEMA_SQL_V13/V14/V15 for several releases without
being re-exported from the core barrel — the published package was at
SCHEMA_VERSION=15 but only re-exposed the SQL constants through V12.

Fix:
- Extend the barrel to cover SCHEMA_SQL_V13/V14/V15.
- Add packages/core/test/api-surface.test.ts:
  * Snapshots the sorted list of every exported binding so future
    drift (added or removed exports) fails loud in code review.
  * Asserts SCHEMA_VERSION equals the highest re-exported
    SCHEMA_SQL_V* constant — catches the inverse "bumped version,
    forgot to re-export the constant" failure mode too.
- Refresh stale module comments: council-parsers.ts no longer
  describes codex/gemini as stubs throwing NotYetImplemented (the
  parsers shipped in M9); dispatches.ts no longer describes
  council.run as a stub milestone.

No runtime behavior change. Test counts: core 763 passed (+2 for new
snapshots), mcp-server 174 passed unchanged.

Closes the API/barrel drift item from the active hardening sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migrations.ts claimed full idempotence against dirty dev databases, but
V13 + V14 ran bare `ALTER TABLE ADD COLUMN` statements that would fail
with "duplicate column name" when a dev DB had the column applied
out-of-band (no recorded schema_version row).

V13 (two ALTERs) and V14 (two ALTERs + a CREATE TABLE/INDEX block) now
apply each ALTER through a hasColumn guard, with the CREATE statements
inline. SCHEMA_SQL_V13 + SCHEMA_SQL_V14 remain exported from schema.ts
as the canonical schema text and are still exercised by the snapshot
tests. V15 is CREATE-only and stays as a plain db.exec.

The module header now states both idempotence mechanisms explicitly
(CREATE TABLE/INDEX IF NOT EXISTS for object creation, hasColumn for
column-addition) so the claim matches the implementation.

Closes the migration idempotence item from the active hardening sprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add docs/consistency.md as the single authoritative reference for how
flywheel-ideas keeps the SQLite ledger and Markdown notes in sync.

Audits every state-changing MCP operation (idea/assumption/outcome/
council/company/import) against one of four mechanisms — strict
compensating write, transactional DB + best-effort fs mirror, DB-only,
or strict-staging — and notes which doctor issue kinds surface each
class of drift.

The doc-mode section is normative even though no code implements doc
mode yet: it locks the file-format contract (frontmatter + Claim / Test
condition / Assumptions / Evidence log / Verdict / Lesson) and the
intended doctor invariants (round-trip, state/verdict, monotonic
transitions, malformed-section) so the planned single-document wedge
ships against an already-agreed-upon policy instead of forcing a
rewrite of this doc.

Also documents the decision to ship doc mode as a parallel codepath
rather than a RepositoryBackend abstraction, with reasoning.

Closes the explicit consistency policy item from the active hardening
sprint (P2 in roadmap-active).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
outcome.test.ts asserted that ideas_outcome_verdicts rows came back in
ORDER BY assumption_id order by sorting the expected array with
String.prototype.localeCompare. SQLite's default ORDER BY uses byte
(BINARY) collation, while localeCompare is locale- and ICU-version-
dependent. On Node 24's ICU build the two diverge for mixed-case nanoid
ids (lowercase letters sorted before uppercase under locale-aware
compare, but after under SQLite's byte sort), so the test failed
intermittently on CI Node 24 ubuntu and passed everywhere else.

Replace localeCompare with a plain < / > comparison so the expected
order tracks SQLite's collation deterministically across Node versions
and runners.

Pre-existing flake on main (latest main run failed for the same
reason); fixing here so this hardening PR's CI is green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Normative contract for the upcoming doc-mode wedge: one portable .md
per idea, no SQLite. File location, frontmatter keys, stable section
order (Claim / Test condition / Assumptions / Evidence log / Verdict /
Lesson), assumption sub-block shape, evidence-log monotonicity, the
verdict block, and the byte-identical round-trip rule that gates every
doc-mode handler via packages/core/test/single-doc-format.test.ts.

Includes a canonical worked example (NVDA inventory-risk refutation)
so the format is concrete from first read.

The spec calls out the backend boundary explicitly: doc mode covers
the embeddable single-idea lifecycle (create / read / list / transition /
assumption.declare / outcome.log verdict). Council, outcome propagation,
lineage, SEC tracking — anything that needs cross-idea state — remains
SQLite-only and returns not_supported_in_doc_mode. Cross-links to
docs/consistency.md for the per-action matrix and reasoning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lands the dispatch surface for the upcoming portable single-file
backend without touching any sqlite-mode behavior. Every tool now
accepts an optional `backend: 'sqlite' | 'doc'` field; the default
remains 'sqlite' so existing callers and tests are unchanged.

Per-tool backend matrix (per docs/single-doc-format.md +
docs/consistency.md):

  idea          create/read/list/transition supported in doc mode;
                forget/freeze/list_freezes/ancestry/descendants/
                shared_assumptions/export/report reject as
                not_supported_in_doc_mode
  assumption    declare supported; list/lock/unlock/signposts_due/
                forget/radar/extension_set/extension_get reject
  outcome       log supported (verdict-only, no propagation);
                undo/list/read/memo_upsert reject
  council       all actions reject
  company       all actions reject

Supported doc-mode actions currently return a stable
`doc_mode_in_flight` placeholder with a next_steps hint back to
`backend: "sqlite"`. The B2 commit on this branch replaces the
placeholders with real read/write handlers against the file format
contract in docs/single-doc-format.md.

Unsupported actions return `not_supported_in_doc_mode: <tool>.<action>`
with a next_steps hint to switch to sqlite. The error code is stable
and documented so MCP clients can match on it.

Adds packages/mcp-server/src/next_steps.ts::mcpNotSupportedInDocMode
helper to keep the unsupported-action response shape uniform across
all five tools.

Test coverage: new packages/mcp-server/test/doc-mode-boundary.test.ts
exercises every (tool, action) pair across the boundary — 37 cases.
Confirms placeholders for supported actions, not_supported errors for
unsupported actions, and that the sqlite default path is unchanged.
Full mcp-server suite: 211 passed (was 174).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ce tests

Replaces the B1 placeholders with real read/write handlers for the
portable single-file backend.

Core (packages/core/src/doc-mode/format.ts):
- Pure parser + serializer for the doc-mode .md format defined in
  docs/single-doc-format.md. No DB, no fs, no MCP coupling — these are
  the primitives every other doc-mode caller orchestrates around.
- DocModeIdea struct + DocAssumption / DocEvidenceEntry / DocVerdict.
- buildDocIdeaPath / slugifyDocTitle / DOC_MODE_FOLDER for filesystem
  layout helpers.
- DocFormatError carries a `kind` discriminator matching the doctor
  invariants planned for B3: doc_malformed_section,
  doc_transition_out_of_order, etc.

Format conformance (packages/core/test/single-doc-format.test.ts):
- Golden fixtures under test/fixtures/single-doc/ (minimal +
  full-lifecycle) parse-then-re-render byte-identical to source.
- fast-check property over 100 generated DocModeIdea structs asserts
  the same invariant for arbitrary well-formed inputs.
- Negative cases for every DocFormatError kind: missing closing fence,
  wrong type/backend, invalid state, out-of-order evidence log,
  malformed assumption block, mismatched H1, missing required section.

MCP handlers (packages/mcp-server/src/tools/idea/doc-mode.ts):
- docCreate / docRead / docList / docTransition for the `idea` tool.
- docDeclareAssumption appends an ### Assumption block to the idea
  file's ## Assumptions section.
- docLogVerdict sets the ## Verdict block + moves frontmatter state
  to validated/refuted/parked. No cross-idea propagation — doc mode
  is the embeddable single-file lifecycle, per docs/consistency.md.
- locateDocIdea is an O(n) scan of ideas-doc/; an index would be a
  different product (probably SQLite-backed) so the linear scan is
  honest about doc mode's scope.

Wiring:
- idea/register.ts dispatches the four supported actions to the doc
  handlers in B1's gate; the B1 placeholder is gone.
- assumption.ts dispatches `declare` to docDeclareAssumption.
- outcome.ts dispatches `log` to docLogVerdict, inferring the verdict
  state from refutes/validates lists (or accepting an explicit
  verdict). Doc mode does not store assumption-id references in
  outcomes — the verdict block stands alone.

Test coverage (packages/mcp-server/test/):
- doc-mode-boundary.test.ts: 41 cases. Supported actions now route to
  real handlers (asserts success or validation error); unsupported
  actions still return not_supported_in_doc_mode.
- doc-mode-e2e.test.ts: 5 cases covering the full lifecycle through
  the MCP surface — create → declare → transition → log → read, list
  ordering, invalid-transition rejection, missing-verdict rejection.
  Every mutation asserts the on-disk file remains parse-clean.

API surface snapshot refreshed (packages/core/test/__snapshots__/)
to include the new doc-mode exports. SCHEMA_VERSION invariant still
holds at v15.

Test counts: core 775 passed (was 763 before doc-mode tests); mcp-server
216 passed (was 174 before doc-mode tests).

Closes B2 + B4 from the wedge plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends the read-only doctor to inspect doc-mode files in addition to
the existing SQLite/markdown mirror checks.

Core (packages/core/src/consistency-doctor.ts):
- Add 4 new ConsistencyIssueKind values:
  - doc_round_trip_failed (warning) — file is not byte-identical to
    renderer output; re-saving normalizes
  - doc_state_verdict_mismatch (error) — frontmatter state and the
    ## Verdict block disagree, or required verdict is missing
  - doc_transition_out_of_order (error) — created_at > updated_at, or
    latest evidence-log timestamp > updated_at, or evidence-log
    bullets non-monotonic (caught at parse)
  - doc_malformed_section (error) — file fails parse (missing fence,
    missing required section, malformed assumption block, etc.)
- buildConsistencyDoctorReport now accepts ConsistencyDoctorOptions
  with mode: 'sqlite' | 'doc' | 'both' (default 'sqlite' for v0.4
  backward compat).
- New inspectDocMode walks <vault>/ideas-doc/ and runs round-trip,
  state/verdict, monotonic-transition, and parse-shape checks per
  file. Files that fail to parse surface the DocFormatError.kind
  directly when meaningful.
- Reuses parseDocIdea/renderDocIdea from doc-mode/format.ts — no
  reimplementation of format semantics here.

MCP (packages/mcp-server/src/tools/doctor.ts):
- doctor.consistency tool gains optional `mode` arg, forwarded
  through buildConsistencyDoctorReport.
- Result includes the resolved mode for client clarity.

Test coverage (packages/core/test/consistency-doctor-doc-mode.test.ts):
- 11 cases: clean vault (empty + canonical), mode default = sqlite
  (backward compat), every issue kind hand-fixtured, mode: 'both'
  aggregates counts from both inspectors.
- Uses the format-spec golden fixture (minimal.md) as the
  round-trip-safe baseline so this test exercises the same bytes the
  format conformance test already proves.

Test counts: core 786 (was 775); mcp-server 216 unchanged.

Closes B3 from the wedge plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves the seven `protobufjs <=7.5.5` advisories that block the
Security Audit CI gate:

- GHSA-q6x5-8v7m-xcrf — overlong UTF-8 decoding
- GHSA-2pr8-phx7-x9h3 — DoS from crafted field names
- GHSA-66ff-xgx4-vchm — code injection via bytes-field defaults
- GHSA-fx83-v9x8-x52w — prototype injection in message constructors
- GHSA-75px-5xx7-5xc7 — code-gen gadget after prototype pollution
- GHSA-jvwf-75h9-cwgg — process-wide DoS via unsafe option paths
- GHSA-685m-2w69-288q — DoS via unbounded protobuf recursion

protobufjs is four levels deep:
  flywheel-ideas (mcp-server)
    → @velvetmonkey/flywheel-memory
      → @huggingface/transformers
        → onnxruntime-web
          → protobufjs

`npm audit fix` resolved cleanly via package-lock.json only — no
package.json or breaking-API changes. Full suite stays green:
core 786 passed, mcp-server 216 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-way export from the SQLite ledger to a portable single-file
doc-mode `.md`. Carries id/title/state/timestamps, claim body,
assumptions (decision-clause preferred), and a verdict block derived
from the latest non-undone outcome. Council sessions, lineage,
cross-idea citations, propagation flags, and private memo context do
not carry over by design. Defaults to `ideas-doc/<slug>-<id>.md` with
an `overwrite` guard so re-exports cannot silently clobber an edited
doc file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Windows CI failed 10 doc-mode tests because Git checkout converted .md
fixtures to CRLF and the parser's `startsWith('---\n')` fence check
rejected them immediately. Two-layer fix:

- parseDocIdea normalizes CRLF→LF at entry so doc-mode (a portable
  single-file format) accepts files authored on Windows. Renderer
  continues to emit canonical LF, so CRLF round-trips to LF by design.
- .gitattributes pins *.md to eol=lf so the byte-identical golden
  fixture round-trip stays platform-independent.

Adds CRLF round-trip regression coverage for both golden fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@velvetmonkey velvetmonkey merged commit be79bdc into main May 13, 2026
9 checks passed
@velvetmonkey velvetmonkey deleted the doc-mode-wedge branch May 13, 2026 12:48
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.

1 participant