Skip to content

feat(fto): product descriptions split into features, confirmed by a person (ADR 0010) - #21

Merged
corlin merged 1 commit into
feat/claim-document-storefrom
feat/product-features
Oct 4, 2026
Merged

corlin merged 1 commit into
feat/claim-document-storefrom
feat/product-features

Conversation

@corlin

@corlin corlin commented Oct 4, 2026

Copy link
Copy Markdown
Owner

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

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

…erson (ADR 0010)

Implements the product owner's choice: users describe the product in free
text, the system proposes features, and a person confirms them before
comparison.

- Splitter (modules/fto/product.py): deterministic rules, with no LLM, because
  product text is customer-confidential and sensitivity routing does not
  exist yet. It splits on lines, list markers (1. 1、 (1) (1) 一、 - •),
  。;;!? and English sentence ends; keeps decimals, common
  abbreviations, Chinese commas and leading minus signs ("-5 °C"); keeps
  exact spans into the original text; flags candidates over 120
  characters.
- Storage (0021): product_descriptions (append-only versions with
  SHA-256), product_feature_sets (draft/confirmed, splitter version,
  confirmer, parent revision) and product_features (code, text, spans,
  origin split/manual/edited). These are tenant tables with FORCE RLS: product
  data is confidential, unlike the global public-patent tables.
- Approval boundary enforced by database triggers: a confirmed set and its
  features cannot be updated, deleted or added to, even bypassing the
  service; changes go through a new revision. organization_id is
  immutable (existing 0001 trigger function).
- Not reusing feature_set_versions (documented in the ADR): the frozen
  comparison, report and search services read "the latest confirmed set",
  so product features there would leak into patentability flows; the
  table also requires a parsed document.
- ProductFeatureService: add description, create draft, edit, add, delete,
  split (exact spans on both halves), merge, confirm (non-empty), revise.

Tests use synthetic texts only: 6 splitter unit tests and 5 Postgres
tests (exact spans, tenant isolation, full edit/confirm/revise flow, the
trigger-enforced boundary, empty set cannot be confirmed).

Stacked on #20 (migration 0021 follows 0020).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@corlin
corlin merged commit a0bc464 into feat/claim-document-store Oct 4, 2026
1 check passed
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)
corlin added a commit that referenced this pull request Oct 4, 2026
)

> **Stacked on #22** (which now targets `main` and includes #21). Merge
#22 first.

## What

The comparison itself (ADR 0011). **One chart = one patent's claims ×
one confirmed product feature set.**
- A person **confirms claim features** and **records per-feature
findings**.
- Claim-level conclusions are derived on read with the exact
all-elements algorithm (ADR 0008).
- As decided, findings are **manual only**: no automated suggestions.
- Backend only; the screen is the next PR.

## Design points

- **Snapshot at creation:** creating a chart freezes the latest fetched
claim document as parsed at that moment (claim numbers, dependencies,
feature texts and spans, parser ID, claims-text SHA-256, what the text
represents, warnings). Recorded findings therefore can't drift onto
different features after a re-fetch or a parser change.
- **Guards:** the product set must be **confirmed** and from the **same
case**. Claims not yet fetched return **409**; no external calls are
made in the request path.
- **Input rules,** enforced in both the service and DB `CHECK`
constraints:
- `literally_present` / `present_by_equivalent` **must cite product
feature codes** from the chart's own product set
  - `present_by_equivalent` / `absent` **need a rationale**
- equivalence is a professional judgment that is supplied, **never
computed**
- **Caveats on every response:**
- US text is **as granted** (reexamination, IPR and corrections aren't
reflected)
  - EP text **may be a translation**
  - conclusions are **not a legal opinion**

## Storage (tenant data, FORCE RLS)

| Table | Mutability |
|---|---|
| `fto_charts` | append-only |
| `fto_chart_claim_features` | text frozen. Roles may update **only**
the confirmation columns, and a trigger forbids revoking a confirmation
|
| `fto_feature_findings` | append-only: **history kept**, latest wins |

## API — `/cases/{case}/fto/charts`

Create · list · get (with conclusions and the features found absent) ·
confirm a claim's features · record a finding · finding history.

Audit summaries contain IDs, claim numbers and finding values only: **no
claim text, product text or rationale**.

## Tests

2 Postgres integration tests through the real app:
- the guards
- **undetermined → `reads_literally` after confirmation →
`does_not_read` once a feature is absent, inherited by the dependent
claim**
- input rules, history, caveats (US and EP)
- wrong case and other tenant both get 404
- audit privacy (checked with a marker string)
- DB immutability (snapshot text, confirmation revocation, findings,
charts)

**Mutation check:** treating unconfirmed features as confirmed fails the
flow test.

## Validation
- `./scripts/test-postgres.sh`: **121 passed** (+2)
- `.venv/bin/pytest apps/api/tests/unit/`: 423 passed
- `pnpm test:web`: 66 passed; `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