10 Commits

Author SHA1 Message Date
govin 5a6461eeec docs: describe the MCP surface and how to connect a client
`docs/MCP.md` is the manual a person needs to put this in front of an agent:
the 29 tools with their arguments, the workflow ("generate, then write it in"
in one call, versus paragraph by paragraph), the error contract, and the exact
configuration for the three clients in play — the Harness's
`cordis.patch.yml`, Claude Code's `claude mcp add` and `.mcp.json`, and Codex's
`config.toml` — including the note that a new Harness entry has to sit inside
the `insert:` list or it is silently treated as an override of an entry that
does not exist.

`overview.md` gains the section that belongs in a design document rather than a
manual: why the MCP surface is shaped differently from the REST one, and the
two rules a new tool has to follow — keep the docstring to a line or two,
because it is sent with every request, and register with
`structured_output=False`, because the inferred envelope sends the same JSON
twice and clients disagree about which copy counts.
2026-09-19 00:01:47 +08:00
govin e582198cc8 backend: decide the seam between sentences instead of gluing them
A paragraph is its sentences concatenated. Nothing was put between them, which
is right for Chinese — 「。」 already separates — and wrong for English, where
`Adaptive capacity rises.` followed by `Relocation follows.` printed as
`...rises.Relocation...`: a sentence boundary the reader cannot see. CJK output
hid it, so it would have surfaced as an English bug later rather than now.

`sentence_separator` now returns "" or a single space per seam, and the document
carries it as `SentenceRead.separator_before`. The rule is about the seam, not
the language: a space goes in unless both sides are CJK. Mixed seams take the
space. Empty sentences take none. It is derived on every read and never stored,
so it cannot drift from the text, and the client prints `separator_before +
content` and adds no spacing of its own.

No splitting was added anywhere, and none exists: a sentence is one line in the
editor and nothing parses it. The single split this project has ever performed
was the one-time move of the old abstract column, which cut after 「。」 only —
conservative on purpose, since an English abstract is better left in one row
than cut at the first `et al.`.

Five smoke checks cover the seam (Chinese, English, the assembled paragraph,
and that no separator is stored inside the content). 45 checks pass.
2026-09-18 19:01:31 +08:00
govin 45926af966 refactor: make the abstract a paragraph instead of a paper field
A template's abstract paragraph (`0 Abstract`) *is* the paper's abstract: it has
a position in the document, the heading and typography the template gives it,
and the same sentence-by-sentence editing as everything else. `paper.abstract`
was a second home for that text — one the document never reads, so a paper
could show two different abstracts and the column could drift from the body.

The column is gone, from the table, the model, the schemas, the API payloads,
the paper form, the list subtitle and the paper page. Nothing else changed.

The text already written into it is not gone. Revision a83f5c21d7b6 writes each
stored abstract into the paper's body first — one sentence per 。 at the
paragraph carrying the abstract heading, appended after anything already there
rather than replacing it. A template without such a heading keeps the text too,
one position above its first paragraph, where the document renders it under
未设定. Verified against the one paper that had an abstract: 450 characters in,
450 out, identical including `|J| ≤ α · I⁻ᵝ`, split into six sentences in the
`0 Abstract` paragraph, still with its own edit button.

The smoke test no longer assumes an empty database: it records the paper total
before it starts and compares against that, so it can run on a real one.
2026-09-18 18:55:10 +08:00
govin 61857c3ce1 frontend: let the list tables fill the window instead of their rows
A list page is a table with controls around it, and the table was as tall as
its rows: on a large screen most of the window sat empty below the last row
while the page stayed the thing that scrolled. The three list pages now stretch
the table to the height the window offers — the toolbar and the pagination stay
put and the rows scroll inside the table — and give the height back on a small
screen.

Pure CSS, no resize listener and no measured pixel height, so nothing goes
stale when the rail collapses or the window is resized. Four links carry it,
each load-bearing: `.page--fill` is `min-height: 100%` (a floor, not a cap),
`.card--fill` makes the card a column, its body gets `min-height: 0` so a flex
child may shrink below its content, and `.table-fill` is `flex: 1 1 0` so the
table takes the leftover height rather than claiming its content height as its
basis. Element Plus then pins the header and scrolls the rows.

Verified by rendering the pages in a headless browser at 1920x1080, 1440x900
and 1280x620: the table measures 781/601/321px, the page never scrolls, the
rows scroll inside it, header and body columns stay aligned to 0px through a
400px horizontal scroll, the pinned action column stays pinned, and the empty
state is centred in the full-height body rather than stranded at the top. A
1280x420 window hits the 240px floor and falls back to the page scrolling, and
the paper document page is untouched.
2026-09-18 18:26:52 +08:00
govin a5f884f440 refactor: prefix every table with its module
Two tables were named after the concept they came from rather than the module
they belong to, so the schema read as if the template tables were part of the
paper module. Renamed (data preserved, `RENAME TABLE` moves rows in place):

    paper_template  -> template                 the 模板 module
    section_field   -> template_field_library   the 字段库 the 模板 module owns

The paper tables and `template_field` already followed the rule. The rename
carries through everything that named a module:

  models   Template, TemplateField, TemplateFieldLibrary
  schemas  Template*, TemplateFieldLibrary*
  crud     app/crud/template.py, app/crud/template_field_library.py
  API      /template-field-library (was /section-fields); handlers are now
           named after library entries, which removes the ambiguity with
           TemplateField — a placement, a different thing entirely
  client   src/api/templateFieldLibrary.ts

`paper_template_filed_sort` is deliberately untouched: it is a column of the
paper module, spelled as the feature was specified.

TiDB v8.5 with tidb_enable_foreign_key on — as this cluster runs — enforces
foreign keys rather than ignoring them, so the docs' "TiDB does not enforce
foreign keys" was wrong. Corrected, with what actually follows from it: the
rename was rehearsed (RENAME TABLE carries a referencing constraint along), the
API keeps checking first so a violation names the row instead of surfacing a
driver error, and the ORM cascades stay so behaviour does not depend on a
cluster setting.

Revision f27a1c6d9e04 verified both ways; 40 smoke checks, type-check and build
all pass.
2026-09-18 17:48:11 +08:00
govin 2d113f9f6b docs: describe the paper authoring model, API, and views
Records the two decisions the paper feature rests on: a sentence is addressed
by the template placement's `sort` rather than by a row id, and citations are
a table rather than a column. Both explain behaviour that otherwise looks
arbitrary — why an empty paper already has its structure, why a template switch
destroys nothing, and why content with no matching heading is still rendered.

Also documents the new endpoints, the /papers/:id route, the paper view's three
jobs, and the smoke test.
2026-09-18 17:29:19 +08:00
govin 0d0aa20be2 frontend: make the logo block exactly as wide as the second-level menu
The logo block stopped wherever its text happened to end (~140px) while the
rail below it is 208px, so the left edge was a step rather than a column.

Both now read `asideWidth()` from src/layout.ts, so they cannot drift apart,
and collapsing the rail narrows the logo block with it — the wordmark is
dropped and the mark centres in the 56px strip. The block also carries the
rail's separator up through the header, so the column reads as one piece.

The header's own 20px padding had to go for this to work: the logo block has
to start at x=0 to be flush with the rail, so the nav and the right-hand mark
now own their insets. The mark's inset comes from the same `ASIDE_INSET` as
the rail's section title, so the two line up.

A side effect worth keeping: the rail is 208px and the top nav's first item
and .el-main are each inset a further 20px by their own Element Plus defaults,
so the nav text, the page body and the page header all start on one vertical
line.

The widths live in a module rather than CSS custom properties because el-aside
takes its width through a prop and would otherwise fight an inline style.
2026-09-18 16:43:30 +08:00
govin 4b826567b8 frontend: move the second-level menu to the left of the content
The rail was on the right. It now leads the row instead, so the section menu
reads as a conventional left-hand navigation column.

Which side it lands on was never a stylesheet decision: `.app-body` is a flex
row and the aside is simply its first child now, with `.app-main` after it.
The only style change that had to follow is the rail's separator, which moves
from its left edge to its right, plus the collapse tooltip's placement.

The global shell contract in base.css now says this explicitly, so the next
person to move the rail knows the side comes from DOM order rather than from a
rule in that file.
2026-09-18 16:35:24 +08:00
govin 9ded3bd574 docs: document the template domain model, API, and shell layout
Records the decisions that are not visible from the code alone:

  - why section_field has no parent_id, and what reuse that buys;
  - that template_field.sort is the single source of display order, and
    that repeats and ties are deliberately legal;
  - that templates reference library fields rather than copying them, which
    is what makes a rename propagate;
  - that TiDB parses FOREIGN KEY and ignores it, so the integrity lives in
    the application layer.

Also adds the API table, the frontend route table, and the seed script.
2026-09-18 16:10:48 +08:00
govin 4438b04ff1 docs: add docs/OVERVIEW.md and CLAUDE.md (init_project steps 2-4) 2026-09-17 12:44:54 +08:00