diff --git a/.claude/rules/grill-with-docs-for-new-features.md b/.claude/rules/grill-with-docs-for-new-features.md
new file mode 100644
index 00000000..8b9775c9
--- /dev/null
+++ b/.claude/rules/grill-with-docs-for-new-features.md
@@ -0,0 +1,10 @@
+# Use /grill-with-docs For Development
+
+When a user asks to develop this codebase, use the `/grill-with-docs` skill before implementation.
+
+## Required behavior
+
+- Start a grilling session to resolve terminology, scope, and trade-offs.
+- Ask one question at a time and wait for user feedback before continuing.
+- If a question can be answered from the codebase or docs, check there first.
+- Proceed to implementation only after the development intent is clear.
diff --git a/.claude/rules/keep-system-architecture-up-to-date.md b/.claude/rules/keep-system-architecture-up-to-date.md
new file mode 100644
index 00000000..4244659f
--- /dev/null
+++ b/.claude/rules/keep-system-architecture-up-to-date.md
@@ -0,0 +1,18 @@
+# Keep System Architecture Doc Up To Date
+
+## Policy
+
+`system-architecture.md` is the source of truth for the repository architecture.
+
+## Required behavior
+
+When a task changes the system architecture (components, boundaries, data flow, key abstractions, integrations, or deployment topology), update `system-architecture.md` in the same task.
+
+If no architecture impact exists, explicitly confirm that no update is needed.
+
+## Verification checklist
+
+- Architecture-impacting code changes include corresponding `system-architecture.md` updates.
+- New or changed architectural terms are reflected consistently.
+- Mermaid diagrams remain accurate when affected.
+- No secrets are introduced in documentation.
diff --git a/.claude/rules/mirror-providers.md b/.claude/rules/mirror-providers.md
new file mode 100644
index 00000000..a289117e
--- /dev/null
+++ b/.claude/rules/mirror-providers.md
@@ -0,0 +1,23 @@
+# Mirror Providers Rule
+
+## Policy
+
+Keep both provider folders aligned:
+
+- skills are mirrored between `.claude/skills/` and `.cursor/skills/`
+- rules are mirrored between `.claude/rules/` and `.cursor/rules/`
+
+## Required behavior
+
+When creating, updating, renaming, or deleting a skill or rule in one provider folder, apply the equivalent change to the other provider folder in the same task.
+
+## Allowed differences
+
+Only vendor-required differences are allowed (format or loader specifics). Core guidance and intent must remain equivalent.
+
+## Verification checklist
+
+- Both provider folders contain equivalent skill directories and rule files.
+- Names map consistently across providers.
+- Referenced local docs/scripts resolve in each provider folder.
+- No secrets are introduced.
diff --git a/.claude/skills/grill-with-docs/ADR-FORMAT.md b/.claude/skills/grill-with-docs/ADR-FORMAT.md
new file mode 100644
index 00000000..da7e78ec
--- /dev/null
+++ b/.claude/skills/grill-with-docs/ADR-FORMAT.md
@@ -0,0 +1,47 @@
+# ADR Format
+
+ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
+
+Create the `docs/adr/` directory lazily — only when the first ADR is needed.
+
+## Template
+
+```md
+# {Short title of the decision}
+
+{1-3 sentences: what's the context, what did we decide, and why.}
+```
+
+That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
+
+## Optional sections
+
+Only include these when they add genuine value. Most ADRs won't need them.
+
+- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
+- **Considered Options** — only when the rejected alternatives are worth remembering
+- **Consequences** — only when non-obvious downstream effects need to be called out
+
+## Numbering
+
+Scan `docs/adr/` for the highest existing number and increment by one.
+
+## When to offer an ADR
+
+All three of these must be true:
+
+1. **Hard to reverse** — the cost of changing your mind later is meaningful
+2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
+3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
+
+If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
+
+### What qualifies
+
+- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
+- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
+- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
+- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
+- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
+- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
+- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
diff --git a/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md b/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md
new file mode 100644
index 00000000..ddfa247c
--- /dev/null
+++ b/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md
@@ -0,0 +1,77 @@
+# CONTEXT.md Format
+
+## Structure
+
+```md
+# {Context Name}
+
+{One or two sentence description of what this context is and why it exists.}
+
+## Language
+
+**Order**:
+{A concise description of the term}
+_Avoid_: Purchase, transaction
+
+**Invoice**:
+A request for payment sent to a customer after delivery.
+_Avoid_: Bill, payment request
+
+**Customer**:
+A person or organization that places orders.
+_Avoid_: Client, buyer, account
+
+## Relationships
+
+- An **Order** produces one or more **Invoices**
+- An **Invoice** belongs to exactly one **Customer**
+
+## Example dialogue
+
+> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?"
+> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed."
+
+## Flagged ambiguities
+
+- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts.
+```
+
+## Rules
+
+- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
+- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution.
+- **Keep definitions tight.** One sentence max. Define what it IS, not what it does.
+- **Show relationships.** Use bold term names and express cardinality where obvious.
+- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
+- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
+- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.
+
+## Single vs multi-context repos
+
+**Single context (most repos):** One `CONTEXT.md` at the repo root.
+
+**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
+
+```md
+# Context Map
+
+## Contexts
+
+- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
+- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
+- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
+
+## Relationships
+
+- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
+- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
+- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
+```
+
+The skill infers which structure applies:
+
+- If `CONTEXT-MAP.md` exists, read it to find contexts
+- If only a root `CONTEXT.md` exists, single context
+- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
+
+When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
diff --git a/.claude/skills/grill-with-docs/SKILL.md b/.claude/skills/grill-with-docs/SKILL.md
new file mode 100644
index 00000000..5ea0aa91
--- /dev/null
+++ b/.claude/skills/grill-with-docs/SKILL.md
@@ -0,0 +1,88 @@
+---
+name: grill-with-docs
+description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
+---
+
+
+
+Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
+
+Ask the questions one at a time, waiting for feedback on each question before continuing.
+
+If a question can be answered by exploring the codebase, explore the codebase instead.
+
+
+
+
+
+## Domain awareness
+
+During codebase exploration, also look for existing documentation:
+
+### File structure
+
+Most repos have a single context:
+
+```
+/
+├── CONTEXT.md
+├── docs/
+│ └── adr/
+│ ├── 0001-event-sourced-orders.md
+│ └── 0002-postgres-for-write-model.md
+└── src/
+```
+
+If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
+
+```
+/
+├── CONTEXT-MAP.md
+├── docs/
+│ └── adr/ ← system-wide decisions
+├── src/
+│ ├── ordering/
+│ │ ├── CONTEXT.md
+│ │ └── docs/adr/ ← context-specific decisions
+│ └── billing/
+│ ├── CONTEXT.md
+│ └── docs/adr/
+```
+
+Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
+
+## During the session
+
+### Challenge against the glossary
+
+When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
+
+### Sharpen fuzzy language
+
+When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
+
+### Discuss concrete scenarios
+
+When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
+
+### Cross-reference with code
+
+When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
+
+### Update CONTEXT.md inline
+
+When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
+
+`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
+
+### Offer ADRs sparingly
+
+Only offer to create an ADR when all three are true:
+
+1. **Hard to reverse** — the cost of changing your mind later is meaningful
+2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
+3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
+
+If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
+
+
diff --git a/.cursor/rules/grill-with-docs-for-new-features.mdc b/.cursor/rules/grill-with-docs-for-new-features.mdc
new file mode 100644
index 00000000..c6d92e36
--- /dev/null
+++ b/.cursor/rules/grill-with-docs-for-new-features.mdc
@@ -0,0 +1,15 @@
+---
+description: Use grill-with-docs for development work
+alwaysApply: true
+---
+
+# Use /grill-with-docs For Development
+
+When a user asks to develop this codebase, use the `/grill-with-docs` skill before implementation.
+
+## Required behavior
+
+- Start a grilling session to resolve terminology, scope, and trade-offs.
+- Ask one question at a time and wait for user feedback before continuing.
+- If a question can be answered from the codebase or docs, check there first.
+- Proceed to implementation only after the development intent is clear.
diff --git a/.cursor/rules/keep-system-architecture-up-to-date.mdc b/.cursor/rules/keep-system-architecture-up-to-date.mdc
new file mode 100644
index 00000000..4244659f
--- /dev/null
+++ b/.cursor/rules/keep-system-architecture-up-to-date.mdc
@@ -0,0 +1,18 @@
+# Keep System Architecture Doc Up To Date
+
+## Policy
+
+`system-architecture.md` is the source of truth for the repository architecture.
+
+## Required behavior
+
+When a task changes the system architecture (components, boundaries, data flow, key abstractions, integrations, or deployment topology), update `system-architecture.md` in the same task.
+
+If no architecture impact exists, explicitly confirm that no update is needed.
+
+## Verification checklist
+
+- Architecture-impacting code changes include corresponding `system-architecture.md` updates.
+- New or changed architectural terms are reflected consistently.
+- Mermaid diagrams remain accurate when affected.
+- No secrets are introduced in documentation.
diff --git a/.cursor/rules/mirror-providers.mdc b/.cursor/rules/mirror-providers.mdc
new file mode 100644
index 00000000..3b67f533
--- /dev/null
+++ b/.cursor/rules/mirror-providers.mdc
@@ -0,0 +1,23 @@
+# Mirror Providers Rule
+
+## Policy
+
+Keep both provider folders aligned:
+
+- skills are mirrored between `.cursor/skills/` and `.claude/skills/`
+- rules are mirrored between `.cursor/rules/` and `.claude/rules/`
+
+## Required behavior
+
+When creating, updating, renaming, or deleting a skill or rule in one provider folder, apply the equivalent change to the other provider folder in the same task.
+
+## Allowed differences
+
+Only vendor-required differences are allowed (format or loader specifics). Core guidance and intent must remain equivalent.
+
+## Verification checklist
+
+- Both provider folders contain equivalent skill directories and rule files.
+- Names map consistently across providers.
+- Referenced local docs/scripts resolve in each provider folder.
+- No secrets are introduced.
diff --git a/.cursor/skills/grill-with-docs/ADR-FORMAT.md b/.cursor/skills/grill-with-docs/ADR-FORMAT.md
new file mode 100644
index 00000000..da7e78ec
--- /dev/null
+++ b/.cursor/skills/grill-with-docs/ADR-FORMAT.md
@@ -0,0 +1,47 @@
+# ADR Format
+
+ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc.
+
+Create the `docs/adr/` directory lazily — only when the first ADR is needed.
+
+## Template
+
+```md
+# {Short title of the decision}
+
+{1-3 sentences: what's the context, what did we decide, and why.}
+```
+
+That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections.
+
+## Optional sections
+
+Only include these when they add genuine value. Most ADRs won't need them.
+
+- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited
+- **Considered Options** — only when the rejected alternatives are worth remembering
+- **Consequences** — only when non-obvious downstream effects need to be called out
+
+## Numbering
+
+Scan `docs/adr/` for the highest existing number and increment by one.
+
+## When to offer an ADR
+
+All three of these must be true:
+
+1. **Hard to reverse** — the cost of changing your mind later is meaningful
+2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?"
+3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
+
+If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing."
+
+### What qualifies
+
+- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres."
+- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP."
+- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out.
+- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s.
+- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate.
+- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract."
+- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months.
diff --git a/.cursor/skills/grill-with-docs/CONTEXT-FORMAT.md b/.cursor/skills/grill-with-docs/CONTEXT-FORMAT.md
new file mode 100644
index 00000000..ddfa247c
--- /dev/null
+++ b/.cursor/skills/grill-with-docs/CONTEXT-FORMAT.md
@@ -0,0 +1,77 @@
+# CONTEXT.md Format
+
+## Structure
+
+```md
+# {Context Name}
+
+{One or two sentence description of what this context is and why it exists.}
+
+## Language
+
+**Order**:
+{A concise description of the term}
+_Avoid_: Purchase, transaction
+
+**Invoice**:
+A request for payment sent to a customer after delivery.
+_Avoid_: Bill, payment request
+
+**Customer**:
+A person or organization that places orders.
+_Avoid_: Client, buyer, account
+
+## Relationships
+
+- An **Order** produces one or more **Invoices**
+- An **Invoice** belongs to exactly one **Customer**
+
+## Example dialogue
+
+> **Dev:** "When a **Customer** places an **Order**, do we create the **Invoice** immediately?"
+> **Domain expert:** "No — an **Invoice** is only generated once a **Fulfillment** is confirmed."
+
+## Flagged ambiguities
+
+- "account" was used to mean both **Customer** and **User** — resolved: these are distinct concepts.
+```
+
+## Rules
+
+- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others as aliases to avoid.
+- **Flag conflicts explicitly.** If a term is used ambiguously, call it out in "Flagged ambiguities" with a clear resolution.
+- **Keep definitions tight.** One sentence max. Define what it IS, not what it does.
+- **Show relationships.** Use bold term names and express cardinality where obvious.
+- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs.
+- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine.
+- **Write an example dialogue.** A conversation between a dev and a domain expert that demonstrates how the terms interact naturally and clarifies boundaries between related concepts.
+
+## Single vs multi-context repos
+
+**Single context (most repos):** One `CONTEXT.md` at the repo root.
+
+**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other:
+
+```md
+# Context Map
+
+## Contexts
+
+- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders
+- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments
+- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping
+
+## Relationships
+
+- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking
+- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices
+- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money`
+```
+
+The skill infers which structure applies:
+
+- If `CONTEXT-MAP.md` exists, read it to find contexts
+- If only a root `CONTEXT.md` exists, single context
+- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved
+
+When multiple contexts exist, infer which one the current topic relates to. If unclear, ask.
diff --git a/.cursor/skills/grill-with-docs/SKILL.md b/.cursor/skills/grill-with-docs/SKILL.md
new file mode 100644
index 00000000..5ea0aa91
--- /dev/null
+++ b/.cursor/skills/grill-with-docs/SKILL.md
@@ -0,0 +1,88 @@
+---
+name: grill-with-docs
+description: Grilling session that challenges your plan against the existing domain model, sharpens terminology, and updates documentation (CONTEXT.md, ADRs) inline as decisions crystallise. Use when user wants to stress-test a plan against their project's language and documented decisions.
+---
+
+
+
+Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
+
+Ask the questions one at a time, waiting for feedback on each question before continuing.
+
+If a question can be answered by exploring the codebase, explore the codebase instead.
+
+
+
+
+
+## Domain awareness
+
+During codebase exploration, also look for existing documentation:
+
+### File structure
+
+Most repos have a single context:
+
+```
+/
+├── CONTEXT.md
+├── docs/
+│ └── adr/
+│ ├── 0001-event-sourced-orders.md
+│ └── 0002-postgres-for-write-model.md
+└── src/
+```
+
+If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives:
+
+```
+/
+├── CONTEXT-MAP.md
+├── docs/
+│ └── adr/ ← system-wide decisions
+├── src/
+│ ├── ordering/
+│ │ ├── CONTEXT.md
+│ │ └── docs/adr/ ← context-specific decisions
+│ └── billing/
+│ ├── CONTEXT.md
+│ └── docs/adr/
+```
+
+Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed.
+
+## During the session
+
+### Challenge against the glossary
+
+When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?"
+
+### Sharpen fuzzy language
+
+When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things."
+
+### Discuss concrete scenarios
+
+When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts.
+
+### Cross-reference with code
+
+When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?"
+
+### Update CONTEXT.md inline
+
+When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md).
+
+`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else.
+
+### Offer ADRs sparingly
+
+Only offer to create an ADR when all three are true:
+
+1. **Hard to reverse** — the cost of changing your mind later is meaningful
+2. **Surprising without context** — a future reader will wonder "why did they do it this way?"
+3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons
+
+If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md).
+
+
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000..fba70d90
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,83 @@
+# 1. Think Before Coding
+
+**Don't assume. Don't hide confusion. Surface tradeoffs.**
+
+Before implementing:
+- State your assumptions explicitly. If uncertain, ask.
+- If multiple interpretations exist, present them - don't pick silently.
+- If a simpler approach exists, say so. Push back when warranted.
+- If something is unclear, stop. Name what's confusing. Ask.
+- Read `system-architecture.md` first to understand repository structure and boundaries before broad grepping/searching across the entire codebase.
+- If the user's request is too vague or short or unclear, use the "grill-with-docs" skill to clarify the request.
+- Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
+
+# 2. Simplicity First
+
+**Minimum code that solves the problem. Nothing speculative.**
+
+- No features beyond what was asked.
+- No abstractions for single-use code.
+- No "flexibility" or "configurability" that wasn't requested.
+- No error handling for impossible scenarios.
+- If you write 200 lines and it could be 50, rewrite it.
+
+Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
+
+# 3. Surgical Changes
+
+**Touch only what you must. Clean up only your own mess.**
+
+When editing existing code:
+- Don't "improve" adjacent code, comments, or formatting.
+- Don't refactor things that aren't broken.
+- Match existing style, even if you'd do it differently.
+- If you notice unrelated dead code, mention it - don't delete it.
+
+When your changes create orphans:
+- Remove imports/variables/functions that YOUR changes made unused.
+- Don't remove pre-existing dead code unless asked.
+
+The test: Every changed line should trace directly to the user's request.
+
+# 4. Plans Must Be Executable By Small Models
+
+**Every plan must be clear, exhaustive, imperative, and verifiable.**
+
+When writing a plan:
+- Use numbered imperative steps (`Do X`, `Then do Y`), never vague goals.
+- Make the plan exhaustive for the requested scope: no hidden steps, no implicit work.
+- State assumptions and prerequisites explicitly before execution steps.
+- For each step, define an objective verification check (test, command, observable output, or diff condition).
+- Define concrete completion criteria so an agent can truthfully decide "done" vs "not done."
+- Use language that a small model can follow literally without guessing intent.
+
+Hard rule:
+- If a small-capability agent could not implement the plan truthfully from the plan text alone, the plan is invalid and must be rewritten.
+
+# 5. Agent Entry Point (Shared Across Providers)
+
+Provider-specific rule files are the source of truth for cross-provider mirroring:
+
+- `.cursor/rules/mirror-providers.mdc`
+- `.claude/rules/mirror-providers.md`
+
+# 6. Be safe, be consistent
+
+**Security is a hard gate. A task is not done until this section is satisfied.**
+
+Always perform a security pass while reading and writing code:
+- Treat secrets exposure as critical (API keys, tokens, passwords, credentials, private keys, webhook URLs, `.env` contents).
+- Never hardcode secrets in code, tests, docs, examples, commits, PR text, logs, or terminal snippets.
+- Do not move secrets into "safe-looking" files (fixtures, sample configs, scripts). If a value is sensitive, keep it out.
+- If you detect a leak or high-risk pattern, stop and warn clearly. Do this even when it is outside the original request.
+
+Before declaring completion, run a critical consistency review of your own work:
+- Verify the result is consistent with sections 1-4: explicit assumptions, minimal scope, and surgical edits only.
+- Confirm every changed line traces directly to the user's request (no speculative features, no side-quest refactors).
+- Check that your naming, structure, and behavior align with existing project language and conventions.
+- Check consistency with documentation produced by the `/grill-with-docs` process (`CONTEXT.md`, `CONTEXT-MAP.md`, and relevant ADRs). If code and docs disagree, flag it explicitly.
+- Challenge your own decisions: "Where did I overreach?" and "What part is inconsistent with the stated principles?"
+- If inconsistencies remain, report them explicitly and propose the smallest concrete fix.
+
+Completion rule:
+- Do not present the task as complete until all checks are done: (1) secret/leak/security scan, (2) critical consistency self-review, (3) consistency check against `/grill-with-docs` artifacts (`CONTEXT.md`, `CONTEXT-MAP.md`, relevant ADRs).
\ No newline at end of file
diff --git a/contributing.md b/contributing.md
index d43650a6..9921818d 100644
--- a/contributing.md
+++ b/contributing.md
@@ -8,6 +8,10 @@ The only external deps are [`tom-select`](https://tom-select.js.org/) (tag input
The project uses modern ES modules and [WXT](https://wxt.dev/) for bundling, dev server, manifest generation, and cross-browser packaging.
+### Related documentation
+
+**This guide covers how to set up, build, and contribute** (commands, conventions, step-by-step how-tos, tests, release). For **how the system works** — the mental model, data flow, core abstractions, component responsibilities, and design rationale — see [`system-architecture.md`](system-architecture.md) (e.g. its [Technical Stack](system-architecture.md#technical-stack) for the full dependency and tooling list).
+
## Set-up
1. [Install `npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) (Node.js 22+ recommended)
@@ -25,7 +29,7 @@ The project uses modern ES modules and [WXT](https://wxt.dev/) for bundling, dev
- **Dev server**: Hot module reloading with `npm run dev`
- **Zip packaging**: `npm run zip` produces ready-to-submit archives for both browsers
-You do **not** need to know WXT internals to contribute. The key thing to know is that WXT discovers entry points from `src/entrypoints/` and builds them into `dist/`.
+You do **not** need to know WXT internals to contribute. The key thing to know is that WXT discovers entry points from `src/entrypoints/` and builds them into `dist/`. For how WXT (and these entry points) fit the overall architecture, see [`system-architecture.md` → System Architecture](system-architecture.md#system-architecture).
## Build commands
@@ -217,26 +221,17 @@ Files in `public/` are copied as-is to the build output. Use this for:
### Key source files for contributors
-**Core Logic** (`src/shared/js/utils/`):
-
-- `config.js` — Global state and settings. Start here to understand data structures. Re-exports `knownPaperPages` / `preprintSources` from `sources/index.js` for backward compatibility.
-- `functions.js` — Utility functions used everywhere.
-- `data.js` — How papers are stored, validated, and migrated between versions.
-- `sources/` — **Add new paper sources here** (`sources/.js` + register in `sources/index.js`). See below.
-- `parsers.js` — Shared helpers (fetchDom, fetchText, BibTeX glue, DC meta extraction, …) used by source modules.
-- `preprintMatching.js` — Cross-source enrichment in `addOrUpdatePaper` (CrossRef, DBLP, Semantic Scholar, …).
-- `paper.js` — Paper object operations: `makePaper`, updates, linking, `paperToAbs` / `paperToPDF`.
-
-**User Interface** (`src/popup/js/`):
+The tree above shows **where** each file lives. For **what each component does and
+how they relate** (responsibilities, data flow, the parse → enrich → store
+pipeline), see [`system-architecture.md` → Component Deep-Dive](system-architecture.md#component-deep-dive).
+A few starting points for common contributions:
-- `popup.js` — Main popup logic. How the extension popup initializes and displays papers.
-- `memory.js` — Memory table functionality. Search, sorting, and display of saved papers.
-- `handlers.js` — User interactions. Button clicks, keyboard shortcuts, form handling.
-
-**Background Processing**:
-
-- `src/background/background.js` — Handles sync, notifications, and browser API calls.
-- `src/content_scripts/content_script.js` — Automatically detects papers when browsing websites.
+- **Understand the data structures** → `src/shared/js/utils/config.js` (the `state`
+ shape) and `data.js` (paper schema in `validatePaper`).
+- **Add a paper source** → `src/shared/js/utils/sources/` ([Adding a paper source](#adding-a-paper-source)).
+- **Add a paper attribute** → `src/shared/js/utils/data.js` ([Creating a new paper attribute](#creating-a-new-paper-attribute)).
+- **Popup / UI work** → `src/popup/js/` (`popup.js`, `memory.js`, `handlers.js`, `templates.js`).
+- **Background / content-script behavior** → `src/background/background.js`, `src/content_scripts/content_script.js`.
### Prettier
@@ -256,6 +251,11 @@ Most editors can auto-format on save with the Prettier extension. For VS Code, i
## Adding a paper source
+> For the conceptual model behind sources (the registry, the `is` map dispatch, and
+> the **load-order/TDZ constraint** you must respect — never call `functions.js`
+> exports at a source module's top level), see
+> [`system-architecture.md` → Core Concepts & Abstractions](system-architecture.md#core-concepts--abstractions).
+
1. **Create** `src/shared/js/utils/sources/.js` exporting a class that extends `BasePaperSource` from `sources/base.js`. Implement at minimum:
- `name` (lowercase key, e.g. `"arxiv"`)
- `displayName` (human label)
@@ -270,7 +270,7 @@ Most editors can auto-format on save with the Prettier extension. For VS Code, i
### Why stateless handlers, not class instances
-Saved papers are **plain objects** (JSON in `chrome.storage`, messages, Gist sync). MV3 service workers also terminate often, so there is no long-lived "paper with methods" to hydrate. Each source module exports a **stateless** subclass of `BasePaperSource`: the registry does a name lookup and calls static-ish methods on the class (`ArxivSource.parse`, …). That matches one hash lookup per dispatch, same cost order as the old large `switch` statements, without prototype loss across storage boundaries.
+Sources are **stateless** `BasePaperSource` subclasses (the registry does a name lookup and calls static methods like `ArxivSource.parse`), not instances. For the rationale — MV3 worker lifetime, plain-object serialization across storage/messages/Gist, and dispatch cost — see [`system-architecture.md` → Key Design Decisions](system-architecture.md#key-design-decisions). The load-order/TDZ constraint that sources must respect is documented in the same file under [Core Concepts & Abstractions](system-architecture.md#core-concepts--abstractions).
## Creating a new paper attribute
diff --git a/system-architecture.md b/system-architecture.md
new file mode 100644
index 00000000..e9ae501c
--- /dev/null
+++ b/system-architecture.md
@@ -0,0 +1,366 @@
+# PaperMemory — System Architecture
+
+> Source of truth for the repository's architecture. Read this before broad
+> grepping. Keep it current when components, boundaries, data flow, key
+> abstractions, or integrations change (see `.claude/rules/keep-system-architecture-up-to-date.md`).
+>
+> **This doc covers how the system works** — mental model, data flow,
+> abstractions, design rationale, invariants. For how to set up, build, and
+> contribute (commands, conventions, step-by-step how-tos, tests, release), see
+> [`contributing.md`](contributing.md).
+>
+> **This doc covers how the system works** — mental model, data flow,
+> abstractions, design rationale, invariants. For how to set up, build, and
+> contribute (commands, conventions, step-by-step how-tos, tests, release), see
+> [`contributing.md`](contributing.md).
+
+## Overview
+
+**PaperMemory** is a cross-browser (Chrome/Brave/Edge + Firefox) WebExtension that
+acts as an automated, minimalist reference manager for research papers. As the user
+browses, it silently detects when a tab is a known paper page (arXiv, OpenReview,
+NeurIPS, Nature, IEEE, and ~30 other academic sources), parses the paper's metadata,
+and records it in a local library ("the Memory"). On top of recording, it enriches
+papers: it discovers code repositories, matches preprints to their published
+versions across several bibliographic databases, generates BibTeX and Markdown
+links, rewrites PDF tab titles to human-readable names, and can sync the whole
+library to a private GitHub Gist.
+
+The system is a **client-side-only** application: there is no backend owned by the
+project. All persistence is in `chrome.storage.local`; all enrichment happens by
+calling third-party public APIs directly from the extension. The "server" role is
+played entirely by the **background service worker**, which brokers
+cross-origin fetches and privileged browser APIs on behalf of UI surfaces and
+content scripts.
+
+The target reader of this document is an engineer who needs to reason about how a
+URL becomes a stored, enriched paper — and how to add a new paper source or UI
+surface without breaking the (deliberately load-order-sensitive) module graph.
+
+## System Architecture
+
+PaperMemory is built with **[WXT](https://wxt.dev/)** (`wxt.config.js`), which
+compiles the `src/` tree into a Manifest V3 extension. WXT's role is purely
+build-time: it wires `src/entrypoints/*` into manifest-declared contexts (background
+worker, content script, and four HTML pages), applies the `@pm` / `@pmu` path
+aliases, and produces per-browser bundles. There is no runtime framework — the code
+is plain ES modules plus jQuery and Tom Select for DOM work.
+
+```mermaid
+graph TB
+ subgraph Browser["Browser (per-tab + extension contexts)"]
+ subgraph CS["Content Script — every page (<all_urls>)"]
+ CScript["content_script.js
detect paper page,
inject feedback UI"]
+ end
+ subgraph UI["UI Surfaces (HTML pages)"]
+ Popup["popup
memory browser + actions"]
+ FullMem["fullMemory
full-tab library view"]
+ Options["options
preferences + import/export"]
+ BibM["bibMatcher
batch BibTeX enrichment"]
+ end
+ subgraph BG["Background Service Worker"]
+ BGScript["background.js
message router,
privileged-API broker"]
+ end
+ end
+
+ subgraph Shared["src/shared/js/utils — shared core (imported by every context)"]
+ Paper["paper.js
addOrUpdatePaper orchestrator"]
+ State["state.js + config.js
in-memory state, prefs"]
+ Data["data.js
storage, migration, validation"]
+ Sources["sources/*
plugin registry (BasePaperSource)"]
+ Sync["sync.js
GitHub Gist sync"]
+ Preprint["preprintMatching.js
CrossRef / DBLP / S2 / Unpaywall"]
+ Urls["urls.js · parsers.js · files.js · functions.js"]
+ end
+
+ subgraph External["Third-party APIs (called directly)"]
+ Arxiv[(arXiv API)]
+ OR[(OpenReview API)]
+ Gist[(GitHub Gist)]
+ Match[(CrossRef / DBLP /
Semantic Scholar /
Unpaywall)]
+ end
+
+ CScript -->|addOrUpdatePaper| Paper
+ Popup --> Paper
+ BibM --> Preprint
+ Paper --> Sources
+ Paper --> Data
+ Paper --> Preprint
+ Paper --> State
+ Paper -->|pushToRemote| Sync
+ CScript -.->|chrome.runtime message| BGScript
+ Popup -.->|chrome.runtime message| BGScript
+ BGScript -->|brokered fetch| Arxiv
+ BGScript -->|brokered fetch| OR
+ BGScript --> Match
+ Sync --> Gist
+ Data --> Storage[("chrome.storage.local")]
+ State --> Storage
+```
+
+### Communication patterns
+
+- **Shared-core imports.** The bulk of logic lives in `src/shared/js/utils/` and is
+ imported directly into whichever context needs it (popup, content script, worker).
+ There is one logical codebase executed in several JS realms; `state`/`config.js`
+ is a per-realm singleton, **not** shared memory across contexts.
+- **Message passing (`chrome.runtime`).** Used for two things: (1) the content
+ script and popup ask the background worker to perform privileged or
+ cross-origin operations they cannot do themselves; (2) the worker pushes
+ commands (keyboard shortcuts, tab-URL changes) into content scripts. The router is
+ the single `chrome.runtime.onMessage` listener in `background.js` (a `type`-keyed
+ `if/else` chain). It rejects messages whose `sender.id !== chrome.runtime.id`.
+- **Storage as the integration bus.** All contexts read/write the same
+ `chrome.storage.local` `papers` object. `addOrUpdatePaper` re-reads `papers`
+ immediately before every write and merges, because multiple tabs may parse the
+ same paper concurrently (see *Concurrency* below).
+
+### Why the background worker brokers fetches
+
+Several source parsers (`arxiv.js` via `fetch-arxiv-xml`, `openreview` via
+`OpenReview*JSON`) and all preprint-matching calls are routed through the worker
+even though `fetch` exists in content scripts. The worker holds `host_permissions:
+["*://*/*"]`, so it can issue cross-origin requests without being subject to the
+visited page's CSP or CORS posture. Content scripts delegate via
+`sendMessageToBackground({ type: ... })`.
+
+## Core Concepts & Abstractions
+
+### Paper sources — the plugin registry
+
+The defining abstraction is the **paper source**: a class extending
+`BasePaperSource` ([`sources/base.js`](src/shared/js/utils/sources/base.js)) that
+knows how to recognise and parse one family of URLs. Sources are **stateless** —
+the class is never instantiated; only static members are used, and papers stay
+plain objects. Each source declares:
+
+- `name` — internal key, matches `paper.source` and the `is.{name}` flag.
+- `displayName`, `isPreprint`, and `patterns` (string substrings or
+ `(url) => boolean` predicates).
+- `matches(url)`, `urlToId(url, ctx)`, `parse(url, tab, ctx)`, plus URL
+ transforms `toAbs` / `toPDF` and venue helpers.
+
+All ~32 concrete sources are registered **once** in
+[`sources/index.js`](src/shared/js/utils/sources/index.js)'s `ALL_SOURCES` array.
+From that single list the module derives every lookup the rest of the code needs:
+`getSource(name)`, `knownPaperPages` (consumed by `isPaper`), `sourceFromIs`,
+`matchUrl`, `preprintSources`, and `SOURCE_DISPATCH_ORDER`. **Adding a source = create
+the module + add one import/array entry.** Patterns must be mutually exclusive — this
+is enforced by the "Source Pattern Mutual Exclusion" test in `test/test-meta.js`.
+For the step-by-step procedure, see
+[`contributing.md` → Adding a paper source](contributing.md#adding-a-paper-source).
+
+`WebsiteSource` is special: it is the universal fallback for arbitrary pages and is
+**excluded** from `DISPATCH_SOURCES`. It is invoked explicitly in `paper.js`
+(`getSource("website")`) when the user manually parses a non-academic page, never via
+pattern dispatch.
+
+> **Load-order constraint (documented in `base.js`):** `functions.js` imports
+> `getSource` from `sources/index.js`, which imports every concrete source — some of
+> which import back from `functions.js`. This cycle is safe **only** because source
+> modules consume `functions.js` bindings inside method bodies (runtime), never at
+> module top level. Evaluating a `functions.js` export inside a static initialiser
+> (e.g. `static patterns = [...]`) hits a Temporal Dead Zone and crashes the
+> extension at load.
+
+### The "is" map and source dispatch
+
+[`isPaper(url)`](src/shared/js/utils/paper.js) returns an object `is` mapping every
+source name → boolean (plus `localFile`, `stored`, `parsedWebsite`). This map is the
+currency of dispatch: `sourceFromIs(is)` walks `DISPATCH_SOURCES` and returns the
+first source whose flag is set, while `parseIdFromUrl` walks `SOURCE_DISPATCH_ORDER`
+to resolve a stable id. Because patterns are mutually exclusive, dispatch order is
+not semantically meaningful.
+
+### Stable IDs and de-duplication
+
+A paper's identity is its `id` (e.g. `Arxiv-2103.12345`, `Website_`). Two
+fuzzy de-dup mechanisms keep the library clean:
+
+- **`urlHashToId`** — `miniHash(url) → id`, a fast cache so a revisited URL
+ resolves without re-parsing.
+- **`titleHashToIds`** — `miniHash(title) → [ids]`, used by `findFuzzyPaperMatch` to
+ detect that a freshly parsed paper is the same work as an existing one under a
+ different source (e.g. an arXiv preprint and its NeurIPS published version).
+ `preprintSources` is consulted so a non-preprint match is preferred as the
+ canonical record.
+
+## Data Models
+
+### The `paper` object
+
+The single domain entity. It is a plain object validated (and defaulted) by
+[`validatePaper`](src/shared/js/utils/data.js) against a schema of expected keys:
+
+| Field | Type | Notes |
+|---|---|---|
+| `id` | string | Unique PaperMemory id; primary key in `papers` |
+| `source` | string | A registered source `name` |
+| `title`, `author`, `year`, `venue` | string | Core metadata (`author` is ` and `-separated) |
+| `bibtex`, `key`, `doi` | string | Citation data; `key` is the BibTeX key |
+| `pdfLink`, `md` | string | PDF URL and a pre-rendered Markdown link |
+| `codeLink`, `code` | string/object | Repo URL + raw PapersWithCode payload |
+| `note` | string | Free-text + auto-generated "Accepted @ …" venue notes |
+| `tags` | string[] | User + auto-tags (`autoTagPaper`) |
+| `favorite`, `favoriteDate` | bool/string | Starring |
+| `count`, `addDate`, `lastOpenDate` | number/string | Visit tracking + sort keys |
+| `extras` | object | Optional per-source overflow |
+
+### Storage layout (`chrome.storage.local`)
+
+The `papers` key holds an `{ id → paper }` map plus a `__dataVersion` sentinel.
+Other top-level keys: `prefs`/menu checks, `autoTags`, `ignoreSources`,
+`urlHashToId`, sync state, and periodic backups. `unlimitedStorage` is requested so
+the library can grow without quota errors.
+
+### Migration
+
+[`migrateData`](src/shared/js/utils/data.js) runs on every `initState`. It compares
+the stored `__dataVersion` against a hardcoded `latestDataVersion` and applies an
+ordered chain of `if (currentVersion < N)` blocks that mutate papers in place
+(backfilling defaults, renaming sources, fixing links), recording a per-paper
+`migrationSummaries` audit trail. A backup is taken before migrating.
+
+### In-memory state
+
+`config.js` exports a per-realm `state` singleton (current papers, sorted lists,
+tags, prefs, the title-rendering function, hash caches, UI flags). `state.js`
+`initState` is the canonical hydrator: load papers → backup → migrate → load prefs &
+caches → (UI contexts only) match local files, sort, build tag set. It is invoked in
+the worker, popup, content script, and full-memory page.
+
+## Component Deep-Dive
+
+### `paper.js` — the parse/enrich/store orchestrator
+**Location:** [`src/shared/js/utils/paper.js`](src/shared/js/utils/paper.js)
+The heart of the system. `addOrUpdatePaper({url, is, prefs, tab, ...})` is the single
+funnel through which papers enter the Memory. Its pipeline: resolve id
+(`parseIdFromUrl`) → update-visit-or-`makePaper` → fuzzy-dedup merge
+(`mergePapers`) → PapersWithCode code discovery (`tryPWCMatch`) → write to storage →
+`pushToRemote` → asynchronous preprint matching (`tryPreprintMatch`) to backfill
+venue/bibtex → second write. Caller-supplied `contentScriptCallbacks`
+(`update`/`preprints`/`done`/`feedback`) let the content script render progressive
+feedback. `makePaper` dispatches to `src.parse()` and stamps `paper.source`.
+
+### `sources/*` — source plugins
+**Location:** [`src/shared/js/utils/sources/`](src/shared/js/utils/sources/)
+~32 `BasePaperSource` subclasses + the `index.js` registry + `WebsiteSource`
+fallback. See *Core Concepts*. Source-specific scraping helpers were factored out of
+`functions.js` into per-source modules (recent refactor, commits `1b18d72`/`ebf02e1`).
+
+### `background.js` — service worker
+**Location:** [`src/background/background.js`](src/background/background.js)
+(entrypoint wrapper at `src/entrypoints/background.js`). Hosts the message router,
+brokered fetches (arXiv XML, OpenReview notes/forums, Google Scholar, the
+preprint-matching APIs), the toolbar badge state machine
+(`badgeOk/Wait/Error/Clear`), keyboard-command handlers (`manualParsing`,
+`downloadPdf`, `defaultAction`), the PDF-title rewriter (`chrome.tabs.onUpdated` +
+`chrome.scripting.executeScript`), and Gist push/pull. On popup disconnect it pushes
+sync.
+
+### `content_script.js` — page integration
+**Location:** [`src/content_scripts/content_script.js`](src/content_scripts/content_script.js)
+Injected at `document_start` on ``. On load it runs `initSyncAndState`,
+pings the worker (`hello`), then calls `isPaper(url)`; on a hit it invokes
+`addOrUpdatePaper` and renders the floating feedback notification. Also listens for
+worker-pushed commands and provides in-page actions (copy BibTeX/MD link, open
+abstract/PDF/ar5iv/HuggingFace, etc., shared with the popup's `handlers.js`).
+
+### UI surfaces — `popup`, `fullMemory`, `options`, `bibMatcher`
+**Location:** `src/popup/`, `src/fullMemory/`, `src/options/`, `src/bibMatcher/`
+(thin `src/entrypoints/*/main.js` wrappers import the real logic + `debug.js`).
+- **popup** ([`popup/js/popup.js`](src/popup/js/popup.js), `memory.js`, `handlers.js`,
+ `templates.js`) — primary surface: searchable/sortable memory list, per-paper
+ actions, the preferences menu, and on-open parsing of the active tab. HTML is
+ assembled from `popup/html/**` fragments via a custom `` Vite
+ plugin (`htmlIncludePlugin` in `wxt.config.js`).
+- **fullMemory** — the popup's memory view in a full browser tab.
+- **options** — preferences, custom PDF-title function, auto-tag rules, JSON/BibTeX
+ import & export, sync configuration.
+- **bibMatcher** — a standalone tool: paste a `.bib` file, batch-match each entry
+ against DBLP/Semantic Scholar/CrossRef/Unpaywall/Google Scholar to fill in
+ published venues. Built directly on `preprintMatching.js`.
+
+### `sync.js` — GitHub Gist synchronisation
+**Location:** [`src/shared/js/utils/sync.js`](src/shared/js/utils/sync.js)
+Optional cross-device sync via a private GitHub Gist using a user-supplied Personal
+Access Token (`@octokit/request`). `getIdentifier` stamps writes with a per-device
+`__syncId` so a device ignores its own echoes on pull. `pushToRemote` is fired
+after every storage write; pull/merge happens on init and popup connect.
+
+### Supporting utilities
+`data.js` (storage/migration/validation/prefs), `urls.js` (id resolution),
+`parsers.js` (DOM/JSON/BibTeX fetch helpers + DC meta-tag extraction),
+`bibtexParser.js`, `files.js` (matching downloaded PDFs in `PaperMemoryStore/` via
+the `downloads` API), `functions.js` (49 misc helpers incl. `miniHash`,
+`sendMessageToBackground`), `miniquery.js` (a tiny jQuery-ish DOM helper).
+
+## Key Workflow — automatic capture of a paper
+
+1. User opens e.g. `https://arxiv.org/abs/2103.12345`.
+2. WXT-injected **content script** runs `initSyncAndState`, pings the worker,
+ computes `is = isPaper(url)`.
+3. `is.arxiv` is true → `addOrUpdatePaper({url, is, prefs})`.
+4. `parseIdFromUrl` → `ArxivSource.urlToId` → `Arxiv-2103.12345` (de-dup against
+ `titleHashToIds` for an existing published version).
+5. New id → `makePaper` → `ArxivSource.parse`, which asks the **worker** to fetch
+ `export.arxiv.org/api/query` (`fetch-arxiv-xml`), parses the Atom XML into a
+ `paper`, builds a BibTeX entry.
+6. `initPaper` defaults fields, runs `autoTagPaper`, `validatePaper`.
+7. PapersWithCode code discovery → write `papers` to storage → `pushToRemote`.
+8. Async `tryPreprintMatch` queries CrossRef/DBLP/S2/Unpaywall to backfill the
+ published venue/bibtex; a second storage write persists it.
+9. Content-script callbacks render "Saved ✓ (+ repo …)" feedback; the worker may
+ rewrite the PDF tab title.
+
+## Concurrency & Invariants
+
+- **Concurrent writers.** Multiple tabs can parse the same paper simultaneously.
+ `addOrUpdatePaper` re-reads `papers` from storage immediately before each
+ `chrome.storage.local.set` and merges (`mergePapers`, `incrementCount`) rather
+ than blindly overwriting. Treat `state.papers` as a stale snapshot, never the
+ source of truth at write time.
+- **Pattern mutual exclusion** across sources is a hard invariant (test-enforced).
+- **No top-level use of `functions.js` exports inside source modules** (TDZ — see
+ the `base.js` constraint above).
+- **`state` is per-realm.** Do not assume the popup's `state` reflects the content
+ script's; reconcile through storage.
+
+## Technical Stack
+
+- **Build:** WXT 0.20 (Vite under the hood), Manifest V3, dual Chrome/Firefox output.
+- **Runtime libs:** jQuery 4, Tom Select 2 (tag inputs), `@octokit/request` (Gist).
+- **Language:** plain ES modules (`"type": "module"`), JSDoc-typed, no TypeScript;
+ `jsconfig.json` + `@pm`/`@pmu` path aliases.
+- **Tooling:** Prettier; Mocha + jsdom + Puppeteer (stealth) for the
+ `test/test-*.js` suite (unit + headless-browser integration). `npm test` builds
+ Chrome first (`pretest`) then runs Mocha.
+- **External APIs (no project backend):** arXiv, OpenReview, GitHub Gist, CrossRef,
+ DBLP, Semantic Scholar, Unpaywall, Google Scholar, (historically PapersWithCode —
+ its API is currently stubbed out in `background.js`).
+- **Permissions:** `activeTab`, `storage`, `unlimitedStorage`, `downloads`(+`.open`),
+ `scripting`, and `host_permissions: *://*/*`.
+- **Docs site:** MkDocs Material (`mkdocs.yml`, `docs/`) published to papermemory.org.
+
+## Key Design Decisions
+
+- **Stateless source classes over instances (Option A, per `base.js`).** Saved
+ papers are plain objects (JSON in `chrome.storage`, runtime messages, and Gist
+ sync), and MV3 service workers terminate frequently — so there is no long-lived
+ "paper with methods" to hydrate, and prototype identity would be lost across
+ every storage/message boundary anyway. Each source is therefore a stateless
+ `BasePaperSource` subclass: dispatch is a name lookup in the registry followed by
+ a static method call (`ArxivSource.parse`, …). That is one hash lookup per
+ dispatch — the same cost order as the large `switch` statements it replaced —
+ without the serialisation hazards of class instances.
+- **Single registry array as the only edit point** for sources, with all lookups
+ derived from it — minimises the surface area and footguns of adding a source.
+- **Background worker as a fetch/permission broker** rather than a stateful server —
+ keeps the architecture backend-free while sidestepping page CSP/CORS.
+- **Storage-as-bus with read-before-write merges** instead of a lock — pragmatic
+ concurrency control for an extension with no shared memory across realms.
+- **Gist-based sync with device `__syncId` tagging** — zero project infrastructure,
+ user owns their data and credentials.
+- **Two-phase enrichment** (fast parse + store, then async venue/code backfill) so
+ the user gets immediate feedback while slow third-party lookups complete in the
+ background.