Clause taxonomy normalization — implementation plan
Status: Planned
Author: Claude Opus 4.8
Date: 2026-07-20
Repo: north-os
Related: design spec; Cowork-demo remediation
Summary
Introduce a corpus-derived canonical taxonomy beside raw classifier labels, populate it safely, and make both typed-clause handlers retrieve by concept. Preserve exact-label rollout fallback, surface honest playbook neighbours, and expose extractor-recorded party-role context without inferring a protected side.
Problem frame
Exact clause_type equality fragments typed retrieval across 1,714 clause labels and 488 playbook labels. The strongest precedent and the firm's recorded position can exist under an equivalent label while the tool returns a weak subset or a bare absence.
Assumptions
This plan was authored in autonomous pipeline mode. These implementation bets are explicit for downstream review.
- A 135-type vocabulary is sufficiently broad for the current corpus while remaining reviewable.
- Existing clause and rule embeddings are suitable for organization-local centroid assignment of labels not covered by explicit aliases.
- Nullable canonical columns are preferable to a deploy-time default because a false canonical value is worse than exact-label fallback.
- Existing
party_roleis extractor-recorded named-party context only; it does not prove direction, action, protection, or client side.
Requirements
- R1. Store a typed, reviewable, corpus-backed vocabulary of roughly 100–150 canonical clause concepts.
- R2. Add generated Drizzle schema columns without changing table RLS or hand-writing migration SQL.
- R3. Backfill clauses and rules idempotently, preserving raw labels and using aliases before embedding centroids.
- R4. Resolve free-text requests and retrieve all rows for the canonical concept, embedding-ranked within the resulting set.
- R5. Preserve exact raw-label behavior as rollout fallback when canonical data is absent or resolution fails.
- R6. Return exact playbook rules when present and explicitly non-exact nearest canonical positions otherwise.
- R7. Surface recorded clause party roles without claiming client side, protected party, or favorability.
- R8. Measure exact versus canonical candidate coverage read-only on preproduction and pass package tests, root typecheck, and lint. Do not claim precision or recall without adjudicated relevance ground truth.
Scope boundaries
- No raw-label deletion, bulk preproduction write during verification, RLS change, grant change, eval-checker weakening, or silent side-based reranking.
- No runtime LLM dependency for taxonomy resolution.
Deferred to follow-up work
- A directional clause model with actor, burdened party, beneficiary/protected party, evidence, and confidence.
- Persisting richer Clio relationship roles currently discarded by the matter sync layer.
- Taxonomy maintenance UI or human adjudication queue for low-margin centroid assignments.
Context and patterns
packages/knowledge/src/pipeline/clauses.tsalready publishes a smaller preferred-token prompt and recordsparty_role.packages/knowledge/src/handlers/clauses.tsalready enforces source and chunk authorization before embedding order.packages/knowledge/src/handlers/rules.tsalready forces firm-global scope and returns an explicit result contract.packages/db/src/schema/knowledge.tsis the only schema source of truth; migration generation followspackages/db/MIGRATIONS.md.packages/db/scripts/backfill-matter-embeddings.tsestablishes the Bun backfill, environment validation, batching, and idempotent-summary style.- No directly applicable durable learning was found under
docs/solutions/; repository code and the migration playbook are authoritative.
Key technical decisions
- Dual labels: raw
clause_typeremains immutable evidence; canonical type is nullable derived data. - Resolution order: normalized canonical token → reviewed alias → bounded lexical nearest → normalized exact fallback.
- Backfill order: deterministic aliases seed canonical centroids; unresolved label centroids may select the nearest canonical centroid within each organization only when a calibrated similarity and runner-up margin clear the acceptance thresholds. Low-confidence labels stay null and use exact fallback.
- Playbook honesty: exact and nearest results have different contract states; nearest rules retain their actual canonical and raw labels.
- Party evidence: return
party_roleas extractor-recorded named-party context and render it with an explicit non-inference label.
High-level data flow
This is directional guidance for review, not implementation specification.
classifier raw label ── normalize/alias ──► canonical column
│ unresolved label centroids
└── preserved verbatim ◄──────────────┘
request free text ── resolve concept ──► canonical candidate set
├─► clause embedding ranking
└─► exact playbook rules
└─ empty: nearest rule-type centroids, labelled non-exact
Implementation units
U1. Canonical vocabulary and resolver
Goal: Add the single typed taxonomy source used by ingestion, backfill, measurement, and retrieval.
Requirements: R1, R4
Dependencies: None
Files:
- Create:
packages/knowledge/src/taxonomy.ts - Modify:
packages/knowledge/src/index.ts,packages/knowledge/src/pipeline/clauses.ts,packages/knowledge/src/pipeline/rules.ts - Test:
packages/knowledge/test/taxonomy.test.ts, existing pipeline clause/rule tests
Approach: Store the exact ordered 135-token vocabulary, reviewed alias map, normalization rules, and corpus-derivation date as the reproducible source artifact. Export a const tuple and union within @workspace/knowledge; wire metadata remains validated strings in the lower-level contracts package to avoid reversing package dependencies. Constrain new extraction to the canonical list while retaining raw output validation and a null result for unrelated low-confidence free text.
Execution note: Implement resolver behavior test-first.
Test scenarios:
- Happy path: canonical token remains unchanged.
- Happy path: “Limitation on Liability” resolves to
limitation_of_liability. - Edge: punctuation, case, spaces, plural aliases, and reordered terms resolve deterministically.
- Error boundary: unrelated input does not force an arbitrary canonical type.
- Integration: extraction prompts contain the controlled list and no longer invite new tokens.
Verification: All canonical outputs are members of the exported union and the list has 100–150 entries derived from observed labels.
U2. Expand schema and ingestion writes
Goal: Persist canonical values for clauses and playbook rules without breaking historical rows.
Requirements: R2, R5
Dependencies: U1
Files:
- Modify:
packages/db/src/schema/knowledge.ts,packages/knowledge/src/pipeline/commit.ts - Generate: next files under
packages/db/src/migrations/andpackages/db/src/migrations/meta/ - Test:
packages/knowledge/test/pipeline/commit.test.ts
Approach: Add nullable text columns without a new canonical index in this PR; the current corpus is bounded and the widened query plan will be observed before a separate online index rollout is justified. Populate canonical values on new inserts through the resolver. Generate and review the Drizzle migration; do not add SQL data updates or alter policies.
Test scenarios:
- Integration: committed canonical/alias labels write canonical values.
- Edge: an unresolved raw label remains persisted with a null canonical value.
- Regression: clause and rule raw labels and embeddings are unchanged.
Verification: Generated SQL contains only the two additive columns; RLS and GRANT statements are absent. The generated migration passes migration lint, journal monotonicity, and immutability checks, then applies successfully to the local database before roles are reprovisioned.
U3. Idempotent backfill and read-only measurement
Goal: Canonicalize historical data and provide repeatable candidate-coverage evidence.
Requirements: R3, R8
Dependencies: U1, U2
Files:
- Create:
packages/knowledge/src/taxonomy-backfill.ts,packages/knowledge/scripts/backfill-clause-taxonomy.ts,packages/knowledge/scripts/measure-clause-taxonomy.ts - Modify:
packages/knowledge/package.json - Test:
packages/knowledge/test/taxonomy-backfill.test.ts,packages/knowledge/test/taxonomy-pgvector.integration.test.ts
Approach: Expose separate dotenv-wrapped package commands for SELECT-only measurement and writes. The writable command requires OWNER_DATABASE_URL, an explicit organization allowlist, an owner/BYPASSRLS role probe, bounded per-organization transactions, and dry-run by default. Update only null canonical columns. Seed explicit alias assignments, then use existing vector averages for unresolved raw-label centroids against canonical centroids per organization. Persist a centroid result only when independently calibrated clause/rule minimum-similarity and runner-up-margin thresholds both pass; leave low-confidence rows null and report them as unresolved. This command is a safe no-op retry of the initial backfill, not a taxonomy-remapping mechanism.
Test scenarios:
- Happy path: aliases seed both clause and rule canonical values while raw labels remain intact.
- Happy path: a high-confidence unresolved label receives the nearest seeded canonical centroid.
- Edge: low-similarity, low-margin, tied, or seedless labels remain unresolved and are reported.
- Boundary: clause and rule acceptance thresholds are calibrated separately and values equal to or below a threshold do not write.
- Idempotency: a second run changes zero rows.
- Failure: missing owner DSN, missing organization allowlist, or a non-owner role exits before mutation.
- Database integration: real pgvector averages and cosine ordering work across two organizations inside a rolled-back fixture transaction, and only active firm-global sources feed playbook neighbours.
Verification: Two consecutive local runs produce a zero-update second summary. The PR runs preproduction in SELECT-only projection mode; after deployment, an authorized operator supplies the owner DSN to persist the backfill. A vocabulary revision requires a separate dry-run remap workflow rather than overwriting prior non-null classifications.
U4. Concept-complete precedent retrieval and party context
Goal: Retrieve all canonical synonyms, retain exact rollout fallback, and surface extractor-recorded named-party context.
Requirements: R4, R5, R7
Dependencies: U1, U2
Files:
- Modify:
packages/knowledge/src/handlers/clauses.ts,packages/knowledge-contracts/src/retrieval.ts,packages/agent-runtime/src/tools/find-precedent-clauses.ts - Test:
packages/knowledge/test/handlers/clauses.test.ts,packages/agent-runtime/src/tools/find-precedent-clauses.test.ts
Approach: Match canonical type plus the resolved concept's reviewed raw aliases for null/unmapped rollout rows under the existing authorization joins, rank by the existing clause embedding, and return raw/canonical/party-role metadata. Include full candidate-set size separately from the limited result rows. Tool rendering identifies party_role as extractor-recorded named-party context, never direction, protection, favorability, or client side.
Execution note: Start with failing handler contract tests.
Test scenarios:
- Happy path: a free-text synonym reaches a row whose raw label differs but canonical type matches.
- Edge: a pre-backfill row remains reachable through raw exact fallback.
- Authorization: source, chunk, scope, date, document, and matter predicates remain present.
- Integration: agent output includes raw type, canonical type, and nullable recorded party role without favorability language.
Verification: A count independent of LIMIT k reflects the full canonical candidate set, while embedding order still determines the returned rows.
U5. Honest playbook neighbour fallback
Goal: Return a firm position for the resolved concept or the closest recorded canonical positions with explicit absence framing through a discriminated exact/nearest/absent result contract.
Requirements: R4, R5, R6
Dependencies: U1, U2
Files:
- Modify:
packages/knowledge/src/handlers/rules.ts,packages/agent-runtime/src/tools/get-playbook-rules.ts - Test:
packages/knowledge/test/handlers/rules.test.ts,packages/agent-runtime/src/tools/get-playbook-rules.test.ts
Approach: Under forced firm-global scope, return canonical-exact rules together with null/unmapped rows whose raw labels are reviewed aliases of that concept. If resolution fails, try normalized raw exact matching. Only when neither canonical nor raw exact matches exist, compare the requested clause centroid to canonical rule-type centroids and return a bounded nearest set with similarity and actual type. Keep exact and nearest states distinct in the result and audit payload.
Execution note: Implement exact-precedence and honest-absence tests before changing the handler.
Test scenarios:
- Happy path: canonical-exact rules return without neighbour lookup.
- Rollout: a pre-backfill null-canonical rule matching a reviewed raw alias is returned as exact.
- Fallback: an unresolvable request tries normalized raw exact matching before neighbour lookup.
- Fallback: no canonical or raw exact rules returns nearest types labelled “no exact position on file.”
- Edge: missing requested centroid returns honest empty, not invented neighbours.
- Authorization: only active firm-global sources contribute exact or nearest rules.
- Integration: agent rendering never labels a neighbour as the requested rule.
Verification: limitation_of_liability yields an exact canonical position if backfilled rules exist, otherwise measured neighbours such as indemnification/recourse remain explicitly non-exact.
U6. End-to-end verification and rollout state
Goal: Prove contracts, migration safety, candidate-coverage improvement, and documentation status before shipping.
Requirements: R2, R8
Dependencies: U1–U5
Files: all files changed by U1–U5; generated superpowers status/index views.
Test scenarios:
- Migration safety: generated migration passes journal monotonicity, immutability, and migration lint, applies locally, and roles are reprovisioned.
- Package: knowledge, knowledge-contracts, and affected agent-runtime tests pass.
- Database integration: pgvector centroid aggregation, abstention, organization isolation, active-source filtering, and exact-before-nearest behavior run against local Postgres.
- Repository: root typecheck and lint pass.
- Measurement: read-only script reproduces old exact counts and projected canonical candidate expansion counts.
Verification: Smoke the resolver, handler SQL contracts, measurement script, and rendered agent messages; set spec/plan status through the status tool and rebuild the site.
System-wide impact
- Interaction graph: ingestion → canonical storage → typed handlers → agent-runtime/MCP rendering.
- Error propagation: resolver null uses exact fallback; database/embedding failures preserve existing recoverable tool errors.
- State lifecycle: additive nullable migration precedes idempotent owner-run backfill; raw evidence is never mutated.
- API parity: exported knowledge contracts, native agent tools, knowledge HTTP service, and MCP wrappers continue to consume the same top-level inputs.
- Unchanged invariants: RLS, matter wall, firm-global playbook scope, source status, citation handles, and eval checkers remain intact.
Risks and mitigations
| Risk | Mitigation |
|---|---|
| Centroid assigns an adjacent but not synonymous subtype | Explicit aliases win; similarity and runner-up margin must clear calibrated thresholds; low-confidence rows stay null; raw labels remain visible. Initial-backfill retries are idempotent, while future taxonomy revisions require an explicit remap workflow. |
| Canonical columns are null during rolling deploy | Exact normalized raw fallback remains in both handlers. |
| Nearest playbook rule is mistaken for firm policy | Distinct result status and explicit non-exact rendering with actual matched type. |
| Party role is mistaken for protected/client side | Name it extractor-recorded named-party context; do not infer direction, action, protection, filtering, or ranking. |
| Backfill bypasses RLS incorrectly | Require owner DSN, make scope explicit, use updates only in the dedicated script, and never run writes against preproduction during verification. |
Outcome measurement
Observed by taxonomy:measure in a read-only transaction against preproduction on 2026-07-20. Canonical columns are not deployed there yet, so “canonical” is the deterministic post-migration alias fallback lower bound; high-confidence centroid backfill may add long-tail labels.
| Requested concept | Clauses: old exact → canonical | Playbook rules: old exact → canonical | Change |
|---|---|---|---|
limitation_of_liability | 25 → 88 | 0 → 0 | 3.52× clause recall |
indemnification | 765 → 775 | 26 → 26 | +10 clauses |
assignment | 818 → 833 | 29 → 29 | +15 clauses |
jury_trial_waiver | 9 → 23 | 0 → 1 | 2.56× clauses; exact canonical playbook position recovered |
limitation_of_liability has no exact canonical playbook rule. The measurement returns explicitly non-exact neighbours: indemnification (26 rules, cosine similarity 0.6701), recourse_carve_out (4, 0.5976), and insurance (27, 0.5619). Persisted post-backfill counts remain a post-deploy operational check after authorized owner-run execution.
Sources and references
- Clause taxonomy normalization — design spec
packages/db/MIGRATIONS.mdpackages/knowledge/src/handlers/clauses.tspackages/knowledge/src/handlers/rules.tspackages/knowledge/src/pipeline/clauses.tspackages/db/src/schema/knowledge.ts