From 2d113f9f6b84269712c3476f14c8b0a1a093be6b Mon Sep 17 00:00:00 2001 From: govin Date: Fri, 18 Sep 2026 17:29:19 +0800 Subject: [PATCH] docs: describe the paper authoring model, API, and views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- docs/OVERVIEW.md | 156 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 141 insertions(+), 15 deletions(-) diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index 2e16786..d568298 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -9,6 +9,12 @@ The first feature to land is the **template configuration**: a reusable library of section fields plus named templates built from them, so that a paper is written by filling in a known structure instead of deciding one. +The second is **paper authoring**: a paper is a document written against a +template, sentence by sentence, paragraph by paragraph. The structure is never +copied into the paper — it is read from the template on every render — so +switching a paper's template re-shapes the whole document in one write without +touching a single sentence. + ## Tech Stack **Backend** @@ -51,7 +57,7 @@ Browser ──HTTP/JSON──▶ FastAPI ──SQLAlchemy──▶ TiDB (k3s) ## Domain Model -Three tables, and one rule that everything else follows from. +Six tables, and one rule that the last three follow from. ``` section_field the reusable heading library @@ -62,6 +68,16 @@ paper_template a named outline template_field the join, and the only home of display order id, template_id → paper_template, field_id → section_field, sort + +paper the document + id, title, template_id, abstract, author, status, keywords, + target_journal + +paper_sentence one sentence, at one position, in one paper + id, paper_id → paper, template_id, paper_template_filed_sort, sort, content + +paper_sentence_reference the citations of one sentence + id, sentence_id → paper_sentence, reference_id, quote, sort ``` ### The field library is flat, not a tree @@ -103,17 +119,64 @@ typography. Renaming or restyling a field therefore updates every template that places it, which is what makes "fix the template and the section names follow" work. +### A sentence is addressed by position, not by field id + +`paper_sentence.paper_template_filed_sort` holds the `sort` of the template +placement the sentence belongs to — **not** `template_field.id`, and not +`section_field.id`. Together with `sort`, the sentence's own position inside +that paragraph, it is everything needed to place a line of text. + +The indirection is the feature. Two templates have no rows in common, but they +can share a position, so a sentence that remembers "I sit at position 7" lands +on whatever the new template puts at position 7 when the paper is switched. The +alternative — pointing at a placement row — would make every sentence belong to +one template and turn a switch into a re-mapping of every line. + +Three behaviours fall out of that one choice: + +- **The structure exists before the content.** A paragraph is any position the + template defines *or* any position a sentence occupies, so an unwritten + paragraph still renders, empty, and a paper created a second ago already has + its full shape. +- **A switch destroys nothing.** Positions the new template does not define are + still rendered, in their place in the order, under the heading 未设定. Switch + back and the old headings return, because the sentences never moved. +- **Ties and gaps are fine.** Sentences may share a `sort` (broken by `id`) and + may be numbered `10, 20, 30` to leave room, exactly as `template_field.sort` + allows. + +The name `paper_template_filed_sort` keeps the spelling the feature was +specified with; it is the template field's `sort` (the `sort` column of +`template_field`). `paper_sentence.template_id` is the template the sentence was +written against. It is kept in step with the paper's current template and is +provenance rather than a lookup key: rendering never filters on it, which is +precisely why content survives a switch. + +### Citations are a table, not a column + +One sentence may quote several references, so +`paper_sentence_reference` holds one row per citation. `quote` — 引用内容 — is +required and must not be blank: a citation that does not say what it quotes is +rejected with `422` rather than stored as a half-record. `reference_id` is a +plain nullable integer with **no** foreign key, because the reference library +does not exist yet; a citation may therefore be written now and linked later. + ### TiDB does not enforce foreign keys + TiDB parses `FOREIGN KEY` for compatibility and then ignores it. The constraints are declared to document the relationships, and the integrity they would provide is enforced in the application layer instead: - deleting a field still placed in a template is refused with `409`, naming the field and how many templates use it; -- creating a template that references a missing field is refused with `400`; -- `PaperTemplate.items` uses `cascade="all, delete-orphan"`, so deleting a - template removes its rows from the join table. +- deleting a template that a paper is written against is refused with `409`, + naming the template and how many papers use it — the template is that paper's + structure, so removing it would empty the paper rather than tidy up; +- creating a template that references a missing field is refused with `400`, and + creating or patching a paper that references a missing template likewise; +- `PaperTemplate.items`, `Paper.sentences` and `PaperSentence.citations` all use + `cascade="all, delete-orphan"`, so deleting a row removes its dependents. ## API @@ -130,6 +193,15 @@ All routes are mounted under `/api`. Interactive docs at `/docs`. | `POST` | `/templates` | name + abstract + ordered `fields` | | `GET` `PATCH` `DELETE` | `/templates/{id}` | `PATCH` with `fields` replaces the selection | | `POST` | `/templates/batch-delete` | body `{ "ids": [...] }` | +| `GET` | `/papers` | `keyword` matches title/author/keywords, plus `status`, `template_id` | +| `POST` | `/papers` | title + template + author/status/keywords/journal; no sentence rows are created | +| `GET` `PATCH` `DELETE` | `/papers/{id}` | `PATCH` with `template_id` **is** the template switch | +| `POST` | `/papers/batch-delete` | body `{ "ids": [...] }` | +| `GET` | `/papers/{id}/document` | the whole paper: paragraphs in order, citations, warnings | +| `GET` | `/papers/{id}/paragraphs/{sort}` | one paragraph, assembled as the document renders it | +| `PUT` | `/papers/{id}/paragraphs/{sort}` | full replacement; optional `target_sort` moves it | +| `POST` | `/papers/{id}/sentences` | append one sentence | +| `PATCH` `DELETE` | `/papers/{id}/sentences/{id}` | edit or remove one sentence | Conventions worth knowing: @@ -139,6 +211,17 @@ Conventions worth knowing: it before using it in a CSS rule. - List endpoints return `{ items, total, page, page_size, pages }`. - A template read returns `fields` already ordered by `sort`; clients never sort. +- A paper document returns its `paragraphs` in ascending position order, + already carrying their sentences and citation numbering. The client sorts + nothing and merges nothing: two implementations of the same ordering rule + would eventually disagree. +- Sentence `content` is folded to a single line (leading, trailing and inner + whitespace runs collapse to one space), so an all-whitespace sentence becomes + the empty string — legal, and rendered as a blank line. A citation with a + blank `quote` is refused with `422`. +- `keywords` is stored as a canonical ``, ``-joined string; `,`、`;` and `;` + are accepted on write and de-duplicated. +- Paper `status` is one of `draft`, `writing`, `done`. ## Frontend @@ -148,17 +231,26 @@ Conventions worth knowing: ┌───────────────────┬──────────────────────────────────────┐ │ [mark] paper-doc │ 论文 模板 [mk] │ header ├───────────────────┼──────────────────────────────────────┤ -│ 模板配置 │ │ second-level -│ 模板列表 │ │ menu, left -│ 字段管理 │ │ +│ 论文 │ │ second-level +│ 论文列表 │ │ menu, left +│ 新建论文 │ │ +│ ── 我的论文 ── │ │ +│ ● 论文标题 A │ │ +│ ● 论文标题 B │ │ └───────────────────┴──────────────────────────────────────┘ <- 208px -> ``` The mark appears at both ends of the header. The second-level menu sits on the -**left** and is driven entirely by `route.meta.section`, so a route declares -which menu it belongs to and deep links render correctly on first paint. Routes -without a `section` (the welcome page) show no menu. +**left** and is driven by `route.meta.section`, so a route declares which menu +it belongs to and deep links render correctly on first paint. Routes without a +`section` (the welcome page) show no menu. + +The 论文 menu is the one that is not a static list: it carries the papers +themselves, read from the `papers` store, so a paper can be opened straight from +the rail. Every page that creates, renames or deletes a paper reloads that +store, which is what keeps the rail and the table showing the same thing. A +filter box appears once there are more than six papers. Two things about this layout are deliberate and easy to break by accident: @@ -188,18 +280,38 @@ stylesheet. | Path | View | Section | |---|---|---| | `/` | welcome | — | -| `/papers` | 论文 (content TBD) | `papers` | +| `/papers` | paper table + CRUD (`?new=1` opens the create dialog) | `papers` | +| `/papers/:id` | one paper, read as a document | `papers` | | `/templates` | redirects to `/templates/list` | — | | `/templates/list` | template table + CRUD | `templates` | | `/templates/fields` | field library + CRUD | `templates` | +### The paper view + +`PapersView` is the library; `papers/PaperDetailView` is the writing surface. +The detail view renders the server's document verbatim and adds three things it +alone knows how to do: + +- **an edit button beside every paragraph**, written or not — content only ever + enters a paper through the paragraph editor, so it may never be the thing that + is missing; +- **numbered citation markers** in the text, matching the 参考文献 list, which + is numbered in reading order rather than per paragraph; +- **a 切换模板 dialog that previews the impact** — how much content lands under + a new heading, how much keeps its place under 未设定, and how many empty + paragraphs the new template adds — because a switch re-shapes the whole + document in one write. + +Changing a paper's template from the metadata form is deliberately disabled: a +silent re-shape of the document is exactly what the preview exists to prevent. + ## Directory Structure ``` paper-doc/ ├── backend/ # FastAPI application │ ├── app/ -│ │ ├── api/routes/ # route handlers (health, section_fields, templates) +│ │ ├── api/routes/ # route handlers (health, papers, section_fields, templates) │ │ ├── core/ # settings and configuration │ │ ├── crud/ # data-access helpers │ │ ├── db/ # engine, session, declarative base @@ -207,18 +319,19 @@ paper-doc/ │ │ └── schemas/ # Pydantic request/response models │ ├── alembic/ # migration environment and revisions │ ├── scripts/seed.py # idempotent seed for the field library + templates +│ ├── scripts/smoke_papers.py # end-to-end check of the writing loop │ ├── alembic.ini │ ├── requirements.txt │ └── .env.example ├── frontend/ # Vue 3 SPA │ ├── src/ │ │ ├── api/ # axios instance and endpoint modules -│ │ ├── components/ # shell, field and template components +│ │ ├── components/ # shell, field, template and paper components │ │ ├── router/ # vue-router configuration -│ │ ├── stores/ # pinia stores (persisted) +│ │ ├── stores/ # pinia stores (UI prefs persisted, the paper list not) │ │ ├── styles/ # global reset and the shell viewport contract │ │ ├── utils/ # formatting helpers -│ │ └── views/ # route-level components +│ │ └── views/ # route-level components (papers/, templates/) │ ├── package.json │ └── vite.config.ts └── docs/ # project documentation @@ -278,3 +391,16 @@ cd backend .venv/bin/python scripts/seed.py # add anything missing .venv/bin/python scripts/seed.py --reset # empty the tables first ``` + +**Checking the paper feature end to end** + +`scripts/smoke_papers.py` walks the whole writing loop against a running API — +create a paper on a template, verify the empty structure, fill a paragraph with +sentences and citations, switch the template, check that unmatched positions +survive under an unset heading, move a paragraph, and delete it all again. It +exits non-zero on the first failed expectation and cleans up after itself: + +```bash +cd backend +.venv/bin/python scripts/smoke_papers.py +```