Skip to content

feat(fto): fetch and store claim texts for US and EP (ADR 0009) - #20

Merged
corlin merged 1 commit into
mainfrom
feat/claim-document-store
Oct 4, 2026
Merged

corlin merged 1 commit into
mainfrom
feat/claim-document-store

Conversation

@corlin

@corlin corlin commented Oct 4, 2026

Copy link
Copy Markdown
Owner

What

Claim text for the comparison model (#19), stored with the ADR 0006 pattern. The design is in ADR 0009.

  • Global, append-only table patent_claim_documents (worker SELECT+INSERT, app SELECT, no tenant columns).
  • ClaimDocumentFetcher (worker): fetch → archive the raw bytes → record. A source failure writes nothing; "not found" is recorded.

US: two-step fetch with safety checks (all verified live)

  1. ODP lookup by patent number → grantDocumentMetaData.fileLocationURI.
  2. That URL answers with one 302 to a signed, time-limited data.uspto.gov URL.

The client follows only USPTO dataset URLs, and only a redirect to data.uspto.gov/files/ (SSRF guard). It sends the API key only to api.uspto.gov, and records the redirect without its signature, which is a temporary credential.

Only the <claims> element goes into the DB. The full XML names inventors, so it's archived in object storage under the open ADR 0006 retention question.

EP

OPS claims for the exact publication. A 404 (an A1 publication, or an unknown number) is recorded as not_found.

What the stored text represents (important for FTO)

  • US = claims as granted. Reexamination, IPR and correction certificates can later amend or cancel claims, and they're not in the grant XML. Stored as text_represents = "as_granted".
  • EP = that specific publication. A B2/B3 replaces the B1 text. Stored as "as_published".
  • The US fileCreateDateTime is a file time, not a data date (a 2007 grant shows 2024-09-29).
  • Fetching post-grant changes (PTAB/reexamination; automatic selection of B2/B3) is follow-up work.

Verification

  • Live: all 16 coverage-set patents fetched and parsed, with no parse failures. The only warnings are informational: "preceding claims" ×31 (expanded into alternatives) and long features ×9.
  • Unit tests: allowlist, redirect host, no leakage of the key or signature, EP 404.
  • Postgres tests: grants; US and EP end to end with the stored text parsed back by the ADR 0008 parsers; three not-found shapes; failures write nothing.

Validation

  • ./scripts/test-postgres.sh: 111 passed (+8)
  • .venv/bin/pytest apps/api/tests/unit/: 417 passed (+4)
  • pnpm test:web: 62 passed; pnpm build:web: ok
  • verify-source-lock, verify-scaffold, verify-p0-02, verify-p0-e2e, docker compose config: ok

🤖 Generated with Claude Code

Claim text for the ADR 0008 comparison model, stored with the ADR 0006
global append-only pattern (patent_claim_documents; worker SELECT+INSERT,
app SELECT, no tenant columns).

US (two-step, verified live): ODP lookup by patent number gives
grantDocumentMetaData.fileLocationURI; that URL answers with one 302 to a
signed, time-limited data.uspto.gov URL. The client:
- follows only USPTO dataset URLs and only a redirect to
  data.uspto.gov/files/ (SSRF guard)
- sends the API key only to api.uspto.gov
- records the redirect without its signature (a temporary credential)
Only the <claims> element is stored in the DB; the full XML, which names
inventors, is archived in object storage under the ADR 0006 retention
question.

EP: OPS claims for the exact publication; 404 (A1, unknown number) is
recorded as not_found.

What the text represents is recorded: US claims are "as_granted"
(reexamination, IPR and correction certificates are not in the grant XML).
EP text is "as_published" for that publication (a B2/B3 replaces B1).
fileCreateDateTime is stored as the file time, not a data date. A source
failure, or XML without <claims>, writes nothing.

Tests: 4 client unit tests (allowlist, redirect host, no key or signature
leakage, EP 404) and 8 Postgres tests (grants; US/EP end to end with the
stored text parsed back by the ADR 0008 parsers; three not-found shapes;
failures write nothing). Live check: all 16 coverage-set patents fetched
and parsed, with no parse failures; the only warnings are informational
("preceding claims" x31, long features x9).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@corlin
corlin merged commit 4df92b3 into main Oct 4, 2026
1 check passed
corlin added a commit that referenced this pull request Oct 4, 2026
…erson (ADR 0010) (#21)

> **Stacked on #20**: migration `0021` follows `0020`. Merge #20 first;
this diff is product features only.

## What

Your choice: **free-text product description → proposed features →
confirmed by a person** (ADR 0010).

## Splitter: deterministic, no LLM

Product descriptions are **customer-confidential**, and the sensitivity
routing from ADR 0005 doesn't exist yet, so **nothing here calls a
model**. The rules:
- Split on lines, list markers (`1.` `1、` `(1)` `(1)` `一、` `-` `•`),
`。;;!?` and English sentence ends.
- Keep together: decimals (`2.5 mm`), common abbreviations (`e.g.`,
`Fig.`), Chinese commas, and **leading minus signs** (`-5 °C` isn't
treated as a bullet).
- Every candidate keeps **exact spans** into the original text.
Candidates over 120 characters are flagged for splitting.

## Storage: tenant data (FORCE RLS)

| Table | Mutability |
|---|---|
| `product_descriptions` | append-only versions (with SHA-256) |
| `product_feature_sets` | `draft` is editable; **`confirmed` is
immutable** |
| `product_features` | immutable once their set is confirmed |

The **approval boundary is enforced by database triggers**, not only by
the service. Even direct SQL can't update, delete or add to a confirmed
set; changes go through a **new revision** (`parent_set_id`).

**Note:** deleting a *case* cascades to its sets, so a case holding a
confirmed set can't be deleted. That's deliberate: confirmed data can't
be removed by a cascade.

## Why not reuse `feature_set_versions`

ADR 0004 planned to reuse it, but the code says otherwise. The
**frozen** comparison, report and search services read "the case's
latest confirmed set", so product features stored there would **leak
into the patentability flows**. The table also requires a parsed
document. This PR reuses only the workflow pattern.

## Service

`ProductFeatureService` operations:
- add a description version
- create a draft from the splitter
- edit, add, delete
- split (exact spans on both halves)
- merge
- confirm (rejected if empty)
- revise a confirmed set

Feature codes (`P1`…) stay stable, so comparisons can reference them.

## Tests (synthetic texts only, no customer data)

- **6 splitter unit tests:** Chinese lists, English sentences with
decimals and abbreviations, bullets vs minus signs, Chinese commas,
long-candidate warnings.
- **5 Postgres tests:** exact spans, **tenant isolation**, the full edit
→ confirm → revise flow, the **trigger-enforced boundary** (direct SQL
rejected), and an empty set can't be confirmed.

## Validation
- `./scripts/test-postgres.sh`: **116 passed** (111 on #20 + 5)
- `.venv/bin/pytest apps/api/tests/unit/`: 423 passed (+6)
- `pnpm test:web`: 62 passed; `pnpm build:web`: ok
- `verify-source-lock`, `verify-scaffold`, `verify-p0-02`,
`verify-p0-e2e`, `docker compose config`: ok

## Next
API endpoints and UI for entering descriptions and the confirmation
workflow, then wiring confirmed product features into the claim chart.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
corlin added a commit that referenced this pull request Oct 4, 2026
…firmation (#22)

> **Now targets `main` and includes #21.** #21 was merged into its
stacked base branch (`feat/claim-document-store`) after #20 had already
landed, so its product-feature storage never reached `main`. This PR's
diff against `main` is exactly #21 (ADR 0010, migration `0021`, service,
splitter) plus the API and workbench below.

## What

Step 3 of ADR 0010: the **HTTP API and screen** for entering a product
description and confirming its features.

## API — `/api/v1/organizations/{org}/cases/{case}/product/…`

| Method & path | Action |
|---|---|
| `POST/GET descriptions` | add / list description versions |
| `POST descriptions/{id}/feature-sets` | create a draft from the rule
splitter |
| `GET feature-sets`, `GET feature-sets/{id}` | list / get feature sets
|
| `PATCH`/`DELETE …/features/{code}`, `POST …/features` | edit / delete
/ add |
| `POST …/features/{code}/split`, `POST …/merge` | split / merge |
| `POST …/confirm`, `POST …/revisions` | confirm (locks) / revise |

- Writes go through `access.mutation` (authorization, tenant
transaction, audit).
- **Audit summaries contain IDs and feature codes only, never product
text.** The audit log must not become a second copy of confidential
descriptions; this is asserted with a marker string.
- Every set endpoint **checks that the set belongs to the case in the
URL**, so a set from another case in the same organization returns 404.
- Bodies are length-bounded. Edits after confirmation return **409**,
and the database triggers from #21 block them independently.

## Workbench — new step "产品技术特征" (`/cases/:caseId/product`)

- A free-text description box. **"保存描述并生成候选特征"** saves a new version and
creates a draft; splitter warnings are shown.
- Per feature: edit, split at a character position, delete (confirm
dialog), select-to-merge, add a manual feature.
- **Confirming goes through a dialog stating the set becomes
immutable.** A confirmed set is read-only, with "创建修订".
- The page states that splitting uses rules only and **sends nothing to
a model**.
- The new stepper step never shows as "done", because case status
doesn't track it yet; I didn't fake that.

## Tests
- **3 API integration tests** through the real app (login with TOTP):
the full flow; audit contains no product text; another tenant and a
wrong case get 404; 409 after confirm; invalid bodies rejected.
- **4 web tests:** generating a draft, merge enabled only with 2
selected, confirm only through the dialog, a confirmed set is read-only
with a revision option.
- ⚠️ **Not checked visually in a browser:** no full stack (API + DB +
login) runs locally. Behaviour is covered by the tests and the
TypeScript build.

## Validation
- `./scripts/test-postgres.sh`: **119 passed** (+3)
- `.venv/bin/pytest apps/api/tests/unit/`: 423 passed
- `pnpm test:web`: **66 passed** (+4); `pnpm build:web`: ok
- `verify-source-lock`, `verify-scaffold`, `verify-p0-02`,
`verify-p0-e2e`, `docker compose config`: ok

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant