Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions MAINTAINING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ config, account, AI via `requestCompletion`) only when PaperBell is present.
`paperbell-shared-config.ts` (zero-dependency by design).
- It is pinned to `PPB_SCHEMA_VERSION`. When PaperBell bumps its schema, **re-vendor** the file and
update the compatibility check.
- ⚠️ **Re-vendoring overwrites our proposal block.** A straight copy from upstream — for *any*
reason, not just a projects-related one — deletes the `projects` additions below and breaks
`src/paperbell/client.ts` and the new-paper modal. After every re-vendor, either re-apply that
block or, if upstream has adopted it, reconcile the two and drop the proposal marker. `npm run
lint` catches the breakage, but only if you run it.
- One block of that file is **not** vendored from upstream: the proposed `projects` scope, flagged
as such in the file header and written up in
[docs/PROPOSAL_PROJECTS_SCOPE.md](./docs/PROPOSAL_PROJECTS_SCOPE.md). Its client methods are
declared **optional** and every caller checks `capabilities` *and* `typeof method === "function"`,
so it stays inert against every host that exists today. `PPB_SCHEMA_VERSION` stays at `1` while it
is a proposal — bumping it unilaterally would silence the newer-schema warning for a real upstream
v2. When the host ships it, re-vendor as usual and delete the proposal marker.

### Contract conformance (verified against PaperBell 0.4.4)

Expand Down
47 changes: 47 additions & 0 deletions docs/PAPERBELL_INTEGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ the user the first time it touches a scope; approval is remembered, denial retur
| `llm-credentials` | full LLM credentials **including the API key** (for streaming) | Wired, not yet used by a feature |
| `activation` | license / activation status | Wired, not yet used by a feature |
| `download-ticket` | a ticket for a protected download | Wired, not yet used by a feature |
| `projects` | the host's project list, for linking an output to its project | **Proposed** — consumed by the new-paper modal, but no shipped host implements it |

We deliberately request **no** scopes at startup — that would trigger a consent prompt on
every launch. Only `plugin-info` (which needs no consent) is read eagerly to learn the
Expand Down Expand Up @@ -74,6 +75,46 @@ an "AI available" hint gated on `capabilities.includes("llm-invoke")`.
> any feature — they are the seams for the roadmap in
> [PAPERBELL_SUITE.md](./PAPERBELL_SUITE.md).

### Link a new paper to its project

The **New PaperBell paper project…** modal asks which PaperBell project the paper is a
deliverable of, and writes the answer as a top-level `project:` key in the frontmatter of
**every** draft index note it creates:

```yaml
---
longform:
format: scenes
title: Sea Level Memory
draftTitle: Main Manuscript
...
project: ColMemo
---
```

That key is the hook Project Manager uses to count a project's outputs. `metadata.json` is
deliberately left alone — it stays pure publication metadata. Note that `project` (the
*project's* acronym) and `_longform.acronym` (this *paper's*, used for PDF filenames) are
different values; the modal labels them distinctly for the same reason.

Where the dropdown's contents come from, in order:

1. **Host list** — `fetchProjects()` calls the proposed `requestProjects` (scope:
`projects`) and the field becomes a dropdown of real projects. Gated on
`capabilities.includes("projects")` **and** `typeof client.requestProjects === "function"`:
capabilities can be stale, and an older host's handle simply has no such method.
2. **Free text** — every other case. Host absent, host too old, consent denied, host-side
error, or an empty list all return `null` from `fetchProjects`, and the field stays the
plain text box it was built as. The fetch is fire-and-forget, so creating a paper never
waits on — or fails because of — PaperBell.

Leaving the field empty omits the key entirely rather than writing an empty `project:`,
which a sibling querying frontmatter would read as a null association.

The contract for `projects` is a **proposal**, not something any host ships today; it is
vendored (and marked as such) in `src/paperbell/shared-config.ts` and written up for the
host team in [PROPOSAL_PROJECTS_SCOPE.md](./PROPOSAL_PROJECTS_SCOPE.md).

## Failing safe (standalone mode)

- No host → client stays disconnected; `connected` is `false`, `config` is `null`,
Expand All @@ -90,3 +131,9 @@ PaperBell's contract, pinned to `PPB_SCHEMA_VERSION`. If the host advertises a *
schema version than we vendored, the client logs a warning (it does not break). When the
host bumps its schema, re-vendor this file and reconcile the check — the procedure and a
decoupled conformance test are described in [MAINTAINING.md](../MAINTAINING.md).

One block of that file is **ours, not upstream's**: the proposed `projects` scope, flagged
in the file header. `PPB_SCHEMA_VERSION` stays at `1` while it is a proposal — raising it
unilaterally would silence the "host schema is newer than vendored" warning for a real
upstream v2. Feature detection never reads the schema version anyway; it reads
capabilities and checks the method exists.
25 changes: 18 additions & 7 deletions docs/PAPERBELL_SUITE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,14 @@ The host plugin (`paperbell`) sits across all of them, dispatching LLM calls cen
- A paper project is a folder of drafts sharing one `metadata.json`
([PAPER_PROJECT.md](./PAPER_PROJECT.md)). Any sibling that reads the vault can discover a
paper's parts from the project index frontmatter and `metadata.json`.
- Outputs are meant to link back to their project (`project: <acronym>`) and to select
`concepts:` — the hooks by which Project Manager counts deliverables and Cards Wrangler
reverse-queries "outputs around this concept". PaperOut writes manuscripts; these
conventions live in the notes.
- Outputs link back to their project via a top-level `project: <acronym>` key, **written
by the new-paper scaffold** onto every draft index note (the modal asks for it; the
dropdown is populated from the host when it supports the proposed `projects` scope, and
is a plain text box otherwise). This is the hook by which Project Manager counts
deliverables — see [PROPOSAL_PROJECTS_SCOPE.md](./PROPOSAL_PROJECTS_SCOPE.md) for the
contract and the open questions we have put to the host team.
- `concepts:` — by which Cards Wrangler reverse-queries "outputs around this concept" —
is still a convention that lives in the notes; PaperOut does not write it.
- Compile writes stable JSON sidecars (`manuscript-lines.json`, `figure-numbers.json`, …)
and a PDF at a predictable path ([MANUSCRIPT_REFS.md](./MANUSCRIPT_REFS.md)).
- The Pandoc toolchain is pulled on demand from **paperout-assets-market**
Expand All @@ -45,6 +49,8 @@ The host plugin (`paperbell`) sits across all of them, dispatching LLM calls cen

- PaperOut registers with the host and **follows its UI language**; it reads account status
and host capabilities ([PAPERBELL_INTEGRATION.md](./PAPERBELL_INTEGRATION.md)).
- The new-paper modal offers the host's **project list** to link the paper to, via the
proposed `projects` scope — degrading to manual entry on any host that lacks it.

**The gap:** PaperOut currently sits somewhat isolated at the Output end. It does **not** yet
consume the concept network, the scholar/publication ledger, or a shared citation source; and
Expand All @@ -66,9 +72,12 @@ Closing that gap is the roadmap below.
4. **Citations via Zotero / Cards.** Resolve `[@citekey]` and produce `references.bib` from
Zotero (Better BibTeX) or from Cards Wrangler's citekey footnotes, instead of a
hand-maintained `.bib`.
5. **Deliverables to Project Manager.** PaperOut publishes a read API / events so Project
Manager can count a paper's drafts and compile status as a project deliverable, and a
review-tracking sibling can read the harvested sidecars.
5. **Deliverables to Project Manager.** *Partly done:* outputs now carry `project:` in
their frontmatter, so Project Manager can attribute a paper by scanning the vault.
Still open: PaperOut publishes a read API / events so Project Manager can count a
paper's drafts and compile status without scanning, and a review-tracking sibling can
read the harvested sidecars. The contract is one-way today — see
[PROPOSAL_PROJECTS_SCOPE.md §6](./PROPOSAL_PROJECTS_SCOPE.md).
6. **Compile-finished hooks.** A "compile finished" event triggers downstream packaging /
submission, or a `results.json` refresh from an analysis plugin.

Expand Down Expand Up @@ -129,6 +138,8 @@ ready to pick up.
## See also

- [PAPERBELL_INTEGRATION.md](./PAPERBELL_INTEGRATION.md) — the host handshake and scopes.
- [PROPOSAL_PROJECTS_SCOPE.md](./PROPOSAL_PROJECTS_SCOPE.md) — the `projects` scope we have
proposed to the host, and the frontmatter questions that go with it.
- [PAPER_PROJECT.md](./PAPER_PROJECT.md) — the paper project scaffold and layout.
- [MANUSCRIPT_REFS.md](./MANUSCRIPT_REFS.md) — sidecars and response-letter sync.
- [METADATA_AND_PLACEHOLDERS.md](./METADATA_AND_PLACEHOLDERS.md) — `metadata.json` and `{{ }}`.
11 changes: 11 additions & 0 deletions docs/PAPER_PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ Create one via the folder right-click menu **New PaperBell paper project…**, o
of the same name (`newPaperProject`). Enter a title; the acronym is auto-derived from the
initials (editable).

## Linking the paper to a project

The modal also asks which **PaperBell project** the paper is a deliverable of. The answer
is written as a top-level `project:` key in every draft index note's frontmatter — the hook
sibling plugins use to count a project's outputs. Leave it empty and the key is omitted.

This is the *project's* acronym (`ColMemo`), not the paper's own acronym above (`SLM`) —
they are separate values with separate jobs. When the PaperBell host is installed and new
enough, the field is a dropdown of your real projects; otherwise it is a text box you fill
in yourself. See [PAPERBELL_INTEGRATION.md](./PAPERBELL_INTEGRATION.md#link-a-new-paper-to-its-project).

## Choosing the parts

The modal asks which parts the paper needs. **Only the Main Manuscript is created by
Expand Down
184 changes: 184 additions & 0 deletions docs/PROPOSAL_PROJECTS_SCOPE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# Proposal to the PaperBell host: a `projects` scope

**Status:** proposal. Nothing here is implemented by any shipped host. PaperOut has
vendored the types (`src/paperbell/shared-config.ts`, marked as a proposal) and gates
every call on capability detection, so this document can be reviewed, changed, or
rejected without breaking anything on our side.

**Why:** PaperOut writes an academic paper into `50 - Outputs`, but nothing in the note
said which project the paper belongs to, so Project Manager could not recognize it as a
project deliverable. As of this change, the **New PaperBell paper project…** modal asks
for the project and writes it into every draft's frontmatter. What it offers in that
dropdown is what this proposal is about.

---

## 1. The frontmatter convention matters more than the API

This is the request we most want an answer to, and it does not depend on any code you
ship.

Today PaperOut writes, at the top level of each draft index note:

```yaml
---
longform:
format: scenes
title: Sea Level Memory
draftTitle: Main Manuscript
workflow: PaperBell Manuscript
sceneFolder: manuscript
scenes:
- introduction
- methods
- results
ignoredFiles: []
project: ColMemo
---
```

`project: <acronym>`, a plain string — following the convention already written down in
[PAPERBELL_SUITE.md](./PAPERBELL_SUITE.md). **If Project Manager actually queries
something else, tell us before this reaches users.**

The main alternative is a wikilink (`project: "[[40 - Projects/ColMemo]]"`), which buys
native backlinks and survives renaming the project note — genuinely better properties.
Changing our writer is one line; migrating notes already on users' disks is not. So the
cost of getting this wrong grows with every release.

**A way to not have to decide centrally:** return a `frontmatterValue` field on each
project and PaperOut will write it verbatim. Then the authority over the interop format
lives in Project Manager, where it belongs, and we never have to re-agree on it.

## 2. One paper is up to four notes — dedupe by `longform.title`

A PaperBell paper project can contain a Main Manuscript, a Supplementary, a Response
Letter, and a Cover Letter. Each is its own note with its own frontmatter, and **each
carries the same `project:` value**. We chose that deliberately: a supplementary or a
response letter is genuinely part of the project's output, and opening any one of them
should show what it belongs to.

The consequence for you: counting notes counts one paper up to four times. Two ready-made
dedupe keys:

- `longform.title` — identical across all drafts of one paper (that is precisely what
groups them into a project in our model);
- `_longform.acronym` in the paper folder's `metadata.json` — the paper's own acronym.

Note that `_longform.acronym` (e.g. `SLM`, this *paper's* code, used for PDF filenames)
and `project` (e.g. `ColMemo`, the *project's* code) are different things. We keep them
visibly separate in our UI; worth doing the same in yours.

## 3. The `projects` scope

Vendored verbatim in `src/paperbell/shared-config.ts`.

```ts
export const PPB_PROJECTS_CHANGED_EVENT = "paperbell:projects-changed";

export interface PPBProject {
id: string; // 稳定 id,重命名 / 移动后不变
name: string; // 展示名
acronym?: string; // 写入 `project:` 的值;同 vault 内须唯一
notePath?: string; // 项目笔记路径(可选)
status?: "active" | "planned" | "paused" | "done" | "archived";
folder?: string; // 项目根文件夹(可选)
concepts?: string[]; // 关联的 featured concepts(可选)
}

export interface PPBProjectsQuery {
status?: NonNullable<PPBProject["status"]>[]; // 缺省 ["active", "planned"]
query?: string;
}

export interface PPBProjectsResult {
ok: boolean;
projects: PPBProject[];
error?: string;
}

// on PPBClient:
requestProjects?(params?: PPBProjectsQuery): Promise<PPBProjectsResult | null>;
onProjectsChange?(cb: () => void): () => void;
```

Three requests about the data:

1. **`id` must be stable across renames and moves.** A vault path does not satisfy this.
We display `name`, write `acronym`, and would use `id` for the reverse reporting in
§6. If the only stable handle you have is the path, say so and we will not build
anything on `id`.
2. **`acronym` must be unique within a vault.** Two projects sharing one acronym write
the same `project:` value, and deliverable attribution silently becomes wrong. If you
cannot guarantee uniqueness, drop `acronym` from the contract and give us
`frontmatterValue` (§1) instead — one authoritative string per project.
3. **Please make consent cheap for this scope.** Nothing here is sensitive: project names
and acronyms are visible to the user in their own vault. As a consent-gated scope, the
user gets a permission dialog the first time they create a paper — friction with no
security benefit. Either mark it low-friction, or fold it into the existing `config`
scope.

**One concrete consequence of the consent gate.** We call `requestProjects()` when the
new-paper modal opens, and the contract gives us no way to cancel a pending request. If the
user closes the modal while your permission dialog is up, that dialog **outlives the modal**
— it appears orphaned, asking about a field that is no longer on screen. Two ways out, either
is fine:

- a consent-free probe (e.g. `hasProjects(): boolean`, or simply advertising a count in
`getPluginInfo()`) so we only trigger the real prompt when there is something to show; or
- an `AbortSignal` parameter on `requestProjects`, so a closing modal can withdraw the ask.

Making the scope low-friction (above) also dissolves this, since there would be no dialog to
strand.

## 4. `paperbell:projects-changed`

Same semantics as the existing `paperbell:config-changed`. Without it we re-fetch every
time the modal opens; with it we can hold a list and refresh on change. Lower priority
than §3 — the feature works without it.

## 5. Version discipline

`MAINTAINING.md` already records that two different host builds both reported `0.4.4`.
When you add this API surface, please also bump `PaperBellPluginInfo.version` **and** add
`"projects"` to `capabilities`.

Our detection deliberately ignores the version string and checks two things:

```ts
capabilities.includes("projects") && typeof client.requestProjects === "function"
```

So **`capabilities` has to be honest** — advertising a scope you do not implement is the
one failure mode that reaches a user (we handle it without throwing, but the dropdown
silently stays a text field). Bumping `schemaVersion` is optional for this change since
it is backward-compatible; if you do bump it, we will re-vendor and realign
`PPB_SCHEMA_VERSION` on our side.

## 6. The reverse direction (next round — not blocking this one)

The contract is currently one-way: sub-plugins consume, and there is no publish path.
`PPBRequestSource` has no field through which a sub-plugin can expose its own API, so
today there is no way for Project Manager to ask us "what deliverables does this project
have, and how far along is each one?" other than reaching into
`app.plugins.plugins["longform-paperbell"].api` directly.

Two ways to close it — **we are happy with either, please pick one**:

- **Registry**: let `registerPPBplugin` accept an `api` field, and let the host hand a
sibling's API to another sub-plugin on request. Generic, and solves this for every
pair of plugins at once.
- **Bus**: give the host an `emit(event, payload)` and let us broadcast
`paperout:deliverable-changed` when a project is scaffolded and when a compile
finishes. Simpler, push-based, no query surface to design.

Until one exists, Project Manager can only scan frontmatter — which is exactly why §1
and §2 are the parts of this document that need an answer first.

## See also

- [PAPERBELL_INTEGRATION.md](./PAPERBELL_INTEGRATION.md) — the handshake and scopes as
they exist today.
- [PAPERBELL_SUITE.md](./PAPERBELL_SUITE.md) — where PaperOut sits in CIMPO and the
wider collaboration roadmap.
- [PAPER_PROJECT.md](./PAPER_PROJECT.md) — what a paper project is made of.
2 changes: 1 addition & 1 deletion src/commands/scaffold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export const newPaperProject: CommandBuilder = (plugin) => ({
active?.parent instanceof TFolder
? active.parent
: plugin.app.vault.getRoot();
new NewPaperModal(plugin.app, parent).open();
new NewPaperModal(plugin, parent).open();
},
});
7 changes: 7 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ export const en = {
"scaffold.acronymLabel": "Acronym",
"scaffold.acronymDesc":
"Short code used for the PDF name and labels. Defaults to the title’s initials; editable later in metadata.json.",
"scaffold.projectLabel": "PaperBell project",
"scaffold.projectDesc":
"The research project this paper is a deliverable of — not the paper’s own acronym above. Written as a project: key in each draft’s frontmatter, which is how Project Manager counts a project’s outputs. Leave empty for none.",
"scaffold.projectPlaceholder": "e.g. ColMemo",
"scaffold.projectNone": "— No project —",
"scaffold.projectManual": "Enter manually…",
"scaffold.projectBackToList": "Choose from the project list instead",
"scaffold.create": "Create project",
"scaffold.invalidName": "Enter a project title without : \\ or / characters.",
"scaffold.created": "Created PaperBell project “{title}”.",
Expand Down
Loading