Reply Pilot Refactoring Tracker

This is the active status tracker for the app/backend boundary refactor. Durable target context, guardrails, and validation gates live in docs/refactoring-context.md.

Legacy note: the old active tracker lived at docs/refactoring.md. That file is now only a compatibility pointer.

Open Items

To Solve

  • [ ] 35.7 Deploy and accept the RP-4121 expand cutover in production. Owner: reply-pilot-db, reply-pilot-be, reply-pilot-app. Outcome: apply only the additive/data-cleanup migrations, deploy backend before app, run the reconciliation report, smoke all three existing surfaces plus the new queue, and confirm no automatic company/domain or legacy link is recreated. Guardrails: fresh recovery-tested backup, normal SOPS-backed deploy flow, no contract migrations, and preserve unrelated production state. Validation: service health, migration history, row-count/precondition evidence, UI/API smoke checks, and post-deploy logs.
  • [ ] 35.8 Retire the legacy email-thread/company tables in a later contract window. Owner: reply-pilot-db. Outcome: after an observation window and the audit-retention decision, recovery-test a fresh backup and apply only existing contract changeset 0054. Blocked: decide whether its 25 historical activity_email_thread_company_event rows are intentionally deleted or archived elsewhere. Validation: expand-only exclusion, targeted contract update/rollback/update on a disposable restore, production preconditions, migration history, and post-contract backend/app smoke checks.

Regular Maintenance

  • [ ] M1. Keep the architecture report honest after boundary changes. Run python3 script/architecture-report.py --output reports/architecture-report.md only when implemented code changes alter the architecture baseline, then run script/check-architecture.sh.
  • [ ] M2. Keep this tracker current after each refactor item. Move completed non-maintenance items to ## Done (Archive), keep evidence, and update ## Unfinished Audit.
  • [ ] M3. Keep documentation validation green after docs changes: mkdocs build --strict.

Unfinished Audit

  • Contract changeset 0054 drops activity_email_thread_company_event, which currently retains 25 historical linked audit rows. Item 35.8 remains blocked until the owner chooses intentional deletion or archive outside the legacy table.
  • No unfinished RP-4122 work remains. Items 34.1 through 34.11 are complete, production changeset 0069 has removed party_organization.tax_identifier, and standard deploys still exclude contract changesets. The current production dataset has zero parties with multiple VAT registrations because no business-data target was approved for an artificial acceptance row. Changeset 0068 previously deleted the measured 318 legacy party_identifier.TAX_IDENTIFIER rows; its audit snapshot remains and the post-write/background-job count is zero.
  • Production Jira authentication is failing independently of the schema contract. Jira task sync and organize-meeting resume both return Unauthorized; worker logs contain the same failure before and after item 33.2. Organize-meeting database reads remain healthy.
  • No table retirement is approved. Production has 11 historical task_jira_email_thread rows and 542 party_person_name_legacy audit rows; activity_email_link is actively written, while activity_call and activity_meeting remain active read-model inputs despite being empty. pg_stat_statements is unavailable and an interactive DBeaver connection was observed, so absence of ad-hoc/manual SQL cannot honestly be proved. Catalog metadata, party_cme_source.first_seen_at, party_contact_mech_purpose.created_at, email-link metadata, and dormant activity subtype timestamps therefore remain.
  • No unfinished item 32 work remains. The 573 quarantined registration values are retained audit history, not canonical ownership or matching candidates; later business corrections may still replace individual rows.
  • Jira task sync, automatic incoming-email Jira operations, and Jira reports intentionally remain on technical credentials because they have no authenticated human actor. JIRA_EMAIL/JIRA_API_TOKEN must remain configured while this allowlist exists.
  • No unfinished item 28 work remains after the follow-up Gmail mailbox cache boundary move. The earlier weekly-report-only move was partial because reply-pilot-be still owned the durable mailbox cache.
  • No separate sent-only Gmail cache or worker remains planned. Sent-only threads are handled as a second pass into the same Gmail-owned account cache.

Done (Archive)

  • [x] 36.1 Move the Java worker to Spring scheduling and separate job classes. Owner: reply-pilot-worker.
  • Nine @Component jobs replace the monolithic worker. Seven use @Scheduled(fixedDelay) measured from completion; Gmail watch and import use Spring triggers for dynamic retry deadlines. Spring owns the shared scheduler pool and cancellation. Status/metrics and status HTTP serving are separate components.
  • HTTP/JSON contracts, startup flags, Gmail retry/import state, env defaults, single-instance non-overlap, heartbeat and health contracts remain. --once creates no scheduler or HTTP status server, even when an external Spring profile property requests scheduling.
  • Validation: 29 tests and Maven packaging passed locally; all 29 tests also passed in the Java 21 build container. Tests cover actual annotations, both Gmail retry triggers, independent heartbeat during a blocked job, recovery after exceptions, non-overlap and shutdown interruption. Isolated Java 21 runtime smoke passed UID 501, read-only conf, heartbeat ownership, healthcheck, --once and SIGTERM (0.05 s, exit 143). Architecture check, strict MkDocs build and git diff --check passed; the knowledge graph was refreshed.
  • No deployment or secret changes were made.

  • [x] 36. Replace the Python worker with Java 21. Owner: reply-pilot-worker.

  • JDK ScheduledExecutorService preserves all nine HTTP job flows, per-job non-overlap, Gmail retry deadlines and active import continuation. JDK HTTP client/server and Jackson replace the Python implementation and tests.
  • Existing env defaults, status JSON and service boundaries remain; the executable JAR supplies --once and --healthcheck, atomically writes the heartbeat, and shuts down on SIGTERM. Docker keeps non-root HOST_UID:HOST_GID, read-only conf and the existing internal-only network contract.
  • Validation: Maven package and 27 tests passed locally and in the Java 21 build container. Isolated runtime smoke passed healthcheck, UID 501, read-only conf, heartbeat ownership, --once and SIGTERM without forced kill. Compose rendering, shell syntax, architecture check and strict MkDocs build passed. The knowledge graph and architecture baseline were refreshed.
  • Production was not deployed; no secret values were changed.

  • [x] 35.6 Prove local end-to-end parity and reconciliation.

  • db-email-company-reconciliation.sh produces a read-only CSV with historical, active-legacy, and canonical party IDs, zero/one/many cardinality, unresolved addresses, review state, and clickable thread and company URLs. The local run produced 2,309 thread rows with baseline_unavailable; both legacy availability flags are false because this local database had already applied contract changeset 0054.
  • The dated 2026-08-21 production baseline remains 25 historical links: 22 reproduced by canonical matching and three intentionally discarded by the owner. The report must be rerun against production before 0054 as part of item 35.7; no production report was run in this implementation slice.
  • Full backend tests passed (438 tests), full app tests passed (341 tests), architecture validation passed after regenerating its deterministic report, and mkdocs build --strict passed.

  • [x] 35.5 Add the „Nepřiřazené e-maily“ app workflow.

  • The paginated/filterable page uses backend read and mutation clients only, offers attach/create-person/create-company/review actions, and creates a company with the exact email contact but no inferred domain.
  • Company write and write-assigned scopes gate resolution actions; email.inbox.review independently gates per-thread review. Validation is inline with novalidate, is-invalid, aria-invalid, and aria-describedby; focused permission/client/UI tests passed.

  • [x] 35.4 Add the unassigned-address read model and per-thread review state.

  • Changeset 0072 adds the provider/thread review table and backend APIs list unresolved addresses grouped with thread count, last occurrence, and thread links. Changeset 0073 adds email.inbox.review to admin and sales_int.
  • Repository/service/controller authorization tests passed; Liquibase validate/update/rollback-count 1/update and rollback-count 2/update passed; the activity model and generated PlantUML diagram are current.

  • [x] 35.3 Cut all existing app surfaces over to the canonical backend result.

  • Inbox, email detail/thread detail, task company options, and company email history now consume the thread/company backend contracts. Tests use explicit canonical fixtures; text-only company mentions intentionally resolve to nothing.
  • The full 341-test app suite and architecture check passed.

  • [x] 35.2 Implement the canonical backend email-thread/company resolver.

  • Changeset 0071 adds the dynamic address resolver view for exact company email, exact linked-person email, and exact manual domain matches, including zero/one/many outcomes and unresolved rows. Backend reverse company/thread reads use the same view and visibility scope.
  • Focused JDBC/service/controller tests and the full 438-test Maven suite passed.

  • [x] 35.1 Stop inferred company/domain creation and clean invalid shared domains.

  • Email ingest and task creation no longer create a company, person, or domain from an address. Changeset 0070 reversibly snapshots and removes only the known invalid atlas.cz, googlemail.com, and volny.cz domain assertions; there is no runtime blacklist.
  • Focused import/task tests, Liquibase verification, and strict docs build passed.

  • [x] 34.11 Apply and accept the VAT scalar contract in production.

  • The standard database deploy validated 64 applied changesets, ran zero, and filtered all 12 contracts. The collection-only backend was then deployed through the normal SOPS-backed module flow and became healthy as 1001:1001; the deployed app, backend, and search source audit found no organization-scalar reader, writer, compatibility adapter, or retired backfill command. Local plaintext production env files were removed.
  • Fresh backup reply-pilot-20260821T130020Z.sql.gz is 18,069,571 bytes, mode 0600, and has SHA-256 c270e205d0ad561c7302f9345c5bc2eb2297176f7be6a79d6ff93ddafedd1a42. Its checksum and restore passed. The disposable restored database preserved 4,929 parties, 3,895 organizations, 1,138 non-empty scalars, and 1,068 VAT registrations; targeted 0069 update/rollback/update passed, rollback reconstructed the 1,068 single-registration scalars, and the restore database was dropped.
  • The final production gate repeated the exact contract preconditions with zero legacy TAX_IDENTIFIER rows, zero registrations on merged parties, zero active scalar queries, and 0069 still unapplied. The standalone changelog exposed exactly one pending changeset, so only 0069 ran; the root contract status left unrelated 0053 and 0054 pending. Parties, organizations, and VAT registrations remained 4929 / 3895 / 1068, the scalar column is absent, and 0069 has one changelog row.
  • Production company 3096 list/detail reads and a data-preserving update retain IČO 23921382 and VAT CZ23921382 with no scalar JSON field. Create and update duplicate-jurisdiction probes both returned indexed HTTP 400 validation without creating or changing a row. The real Simple Auth flow over HTTPS returned the authenticated edit/detail pages with the same values and no scalar input. A post-contract search sync indexed 16,689 documents with an empty error field and finds the company by either IČO or VAT.
  • All six production services are healthy. Scheduled post-contract CME sync completed ok, synchronized 2,656 links, and created zero companies; its existing per-record error_count=227 remains visible. Requirement AI/evaluation and Gmail jobs also completed ok. No lead, merge, or AI business record was manufactured without an approved target, so those paths retain the full green item-34.10 deterministic suites. The only database errors were two operator acceptance queries using wrong column names; corrected queries passed, and the final observation window contains zero SQL/schema or scalar-access errors. The pre-existing Jira Unauthorized jobs remain unrelated and unresolved.

  • [x] 34.10 Remove scalar compatibility and stage the organization-column contract.

  • Company create/update commands, controllers, read models, JSON payloads, forms, clients, and AI context now use only vat_registrations[]. Backend SQL has no organization-scalar reader or writer. The completed one-off VAT backfill operation, tests, CLI wiring, and shell entry point were deleted.
  • Contract changeset 0069 rejects remaining legacy TAX identifiers, registrations on merged parties, and every syntactically valid historical scalar not represented on its effective party, including cyclic merge chains, before dropping the organization column. Invalid historical values are intentionally discarded. Rollback restores the column and repopulates only parties with exactly one registration.
  • Standard Liquibase validate/update ran zero changes, filtered all 12 contracts, and left 0069 unapplied. On a disposable database, missing PL reconciliation and a valid scalar on a cyclic merge both stopped the changeset. After reconciliation, update/rollback/update each passed; the rollback restored CZ, PL, and SK single-registration raw values, left the invalid and merged-source scalars blank, and the final update removed the column. The disposable database was dropped.
  • The refreshed graph and SQL/string audit finds no removed backfill or scalar adapter and no organization-scalar runtime access. Remaining tax_identifier names are CME/source snapshots or wholesale source-input fields; party_cme_source.tax_identifier remains intact.
  • Validation passes with clean backend Maven verify (432 tests), full app (339), search (19), and wholesale (12) suites, regenerated activity-model diagram, refreshed architecture report/check, strict MkDocs, and git diff --check. Nothing was deployed or committed in this item.

  • [x] 34.9 Deploy and accept the canonical VAT collection cutover.

  • Production deployed backend first, then search, then app. Each service is healthy and runs as remote UID:GID 1001:1001; module data ownership is agent:agent, encrypted production envs decrypted successfully, and all local plaintext .env.server files were removed after use.
  • During the backend-first compatibility window the old app stayed healthy. Company 3096 returned IČO 23921382, one canonical CZ registration, and the matching derived scalar. The 28-row catalog returned the Czech and Polish local labels. The deployed organization-scalar source audit found only the explicitly retained one-off backfill reader; app tax_identifier fields are CME source snapshots.
  • Search first refreshed the 1,067 VAT-bearing company documents, then a targeted fingerprint invalidation reindexed all 3,310 visible company documents. Solr contains 3,310 company documents and 16,689 total indexed documents. Search found company 3096 by VAT, all nine Polish registrations by Numer identyfikacyjny VAT, and both ACTIVE/show-by-default parties that already share one normalized VAT value; it did not select one owner.
  • No interactive browser was connected. The real production Simple Auth flow was instead exercised over HTTPS: the authenticated edit form returned 200, rendered repeatable CZ/PL catalog labels, novalidate, add/remove controls, and no scalar input. A form save returned 302 then detail 200 and preserved company 3096 IČO and VAT exactly. A duplicate-jurisdiction API request returned indexed HTTP 400 validation and made no data change.
  • Counts remained 1,138 non-empty organization scalars, 1,067 canonical VAT registrations, zero legacy TAX_IDENTIFIER rows, and zero multi-registration parties. A scheduled post-deploy CME run completed ok, synchronized 2,656 links, and created zero companies; its existing error_count=227 remains unexplained but has no observed VAT-cutover failure. The independently known Jira Unauthorized jobs remain unchanged.
  • Backend/search/app logs contain zero post-deploy error lines and no active scalar statement was observed. One DB error was caused by an acceptance SQL query using the wrong timestamp owner; the corrected query and all later DB checks passed with zero further DB errors. Historical/ad-hoc access still cannot be proved because pg_stat_statements is unavailable.
  • No production company was given a second registration and no lead, AI, or merge business workflow was manufactured without an approved target. Deterministic acceptance therefore remains in the already-green full suites plus a fresh focused run: 43 backend tests for multi-VAT mutation, lead/CME, AI context, and merge behavior; 3 app form tests; and 2 wholesale VAT-signal tests all pass.

  • [x] 34.8 Cut browser, search, and batch consumers to the collection.

  • App company clients, DTOs, create/edit forms, detail, and closure views now use vat_registrations[]. The repeatable form loads the authenticated catalog, displays local/country/jurisdiction labels, preserves submitted row order and raw values, maps indexed errors to their rows, and rejects a duplicate jurisdiction before mutation. Existing routes and page hierarchy are unchanged.
  • Search joins and indexes every registration's raw and normalized value plus jurisdiction, country, and local name; representative documents and subtitles cover Czech and Polish rows. It does not read the organization scalar.
  • Wholesale lookup reads all collection rows and treats VAT only as corroboration for an existing name, domain, or IČO match. A VAT-only match, including the same value on multiple parties, neither reports a duplicate nor chooses an owner.
  • The final graph/string audit found no organization-scalar consumer in app, search, or wholesale; retained party_cme_source.tax_identifier fields are source snapshots. Full suites pass with 339 app, 19 search, and 12 wholesale tests. Architecture, strict MkDocs, and git diff --check pass.

  • [x] 34.7 Cut backend company behavior to the VAT registration collection.

  • Company create/edit/list/detail/search, lead and CME promotion, merge, and AI draft context now use party_vat_registration as the authoritative VAT source. Runtime code no longer reads or writes the organization scalar.
  • Collection mutations preserve indexed field errors. The legacy scalar JSON adapter represents zero or one registration, is derived from the collection, and rejects mutation when a party already has multiple rows.
  • Merge validates both collections before moving related data, deduplicates equal same-jurisdiction values, rejects unequal values, and preserves other jurisdictions. Equal normalized VAT values on different parties remain legal and never select an import or CME owner; source snapshots and immutable evidence remain non-authoritative.
  • The reference audit found only the explicitly retained one-off backfill scalar reader and source snapshot fields outside compatibility JSON names. Focused backend tests pass with 66 tests and the full Maven suite passes with 437 tests. Architecture, strict MkDocs, and git diff --check pass.

  • [x] 34.6 Deploy the legacy identifier cutoff and execute its production data cleanup.

  • The standard DB deploy staged 0068 but filtered it with !contract; the backend, search, and app consumers were then deployed in that order and all passed health checks. The persistent search sync rebuilt 278 changed company documents. A graph and deployed-source audit found no positive authoritative legacy reader or writer; remaining literals are negative guards or immutable evidence/classification labels.
  • Production smoke evidence passed for backend company detail/update, the authenticated server-rendered company page, search, and the wholesale SQL. Company 3096 showed IČO 23921382 and VAT CZ23921382; Solr and backend search returned it for the current IČO and returned zero results for stale legacy value CZ7207283666. The wholesale query returned 3,361 active organizations and sourced both scalar and registration VAT for company 3096 as CZ23921382.
  • Fresh backup reply-pilot-20260821T105254Z.sql.gz is 18,061,390 bytes with SHA-256 1dcc47ed17a68fdcb31eba0b606c3a7d0052b985b63bc9179979b2f9a40347af. Its checksum and gzip checks passed, and an isolated restore reproduced 318 legacy identifiers, 1,138 non-empty organization scalars, 1,067 VAT registrations, an unapplied 0068, and the scalar column. The verification database was then removed.
  • The immediate cleanup gate remeasured exactly 318 legacy rows and confirmed unchanged scalar/registration counts, the scalar column, and pending older contract changesets 0053/0054. Targeted Liquibase status exposed only 0068; its update applied exactly one changeset. The audit records 318 -> 0 and snapshots all 318 rows. Scalars remain 1,138, registrations remain 1,067, the scalar column remains present, and 0053/0054 remain unapplied.
  • An authenticated no-op edit of company 3096 and subsequent scheduled CME runs, including one that linked 2,656 sources and created no companies, did not recreate a legacy row. That run reported error_count=227; no evidence ties those errors to the VAT cutoff. Company identity, VAT registration, API/UI, and search checks remained correct afterward.
  • DB, backup, Google, backend, search, app, worker, docs, and Jira-report containers are healthy; backend, search, and app health endpoints return ok. Search/app logs have no cutoff errors. Two DB errors were caused by acceptance-harness mistakes and their corrected read-only checks passed; the brief backend warning was an intentionally overlapping CME request. The independently tracked Jira Unauthorized worker failures continue, but no VAT/identifier SQL or application failure was found.

  • [x] 34.5 Remove authoritative VAT use of party_identifier.TAX_IDENTIFIER and stage its cleanup.

  • Backend company details, app rendering, and search aggregation explicitly filter legacy rows; the backfill operation no longer reads their count. Manual supplier-identifier review no longer offers or accepts the type. Merge keeps a negative guard until cleanup, and immutable supplier evidence plus AI extraction classifications remain intentionally non-authoritative.
  • Wholesale lookup now aggregates VAT from party_vat_registration; the SQL ran successfully against the local schema and its focused test reports dic:vat_registration. No wholesale code or query references the legacy identifier type.
  • Contract changeset 0068 records one audit row with an exact JSONB snapshot and pre/post counts before deleting legacy rows. Standard !contract update skipped it. Targeted local update archived 4 rows and left 0, rollback restored all 4 with checksum 9675613e1a5d6f50d69d46f5f6ca43a2, and the final targeted update again recorded 4 -> 0. Production execution and its fresh recovery-tested backup were completed in item 34.6.
  • The refreshed graph/reference audit has zero positive authoritative readers/writers: remaining runtime literals are negative backend/app/search guards or supplier-evidence/AI labels and schemas. party_cme_source and party_supplier_identifier_fact were not changed.
  • Validation: focused suites pass; full backend (433), app (337), search (19), and wholesale (10) suites pass. Liquibase validate, standard skip, explicit update/rollback/update, the live wholesale SQL, architecture check, strict MkDocs, and git diff --check pass.

  • [x] 34.4 Deploy and accept the VAT expand/compatibility/backfill release in production.

  • Production deployment used the required order: the standard expand-only DB deploy applied changesets 0065, 0066, and 0067 while filtering all contract changesets, then the compatible backend was deployed and passed its Docker healthcheck. The VAT catalog contains 28 rows, including EL -> GR, XI -> GB, and Czech DIČ; all target PK/FK/check/unique constraints are present and the normalized-value index is non-unique.
  • Fresh backup reply-pilot-20260821T100732Z.sql.gz is 18,036,706 bytes with SHA-256 5ccc599553ec98b56f44b70ab5e07d2a307adb3ade1c3d031bd16709e20fc4fa. Its checksum passed and it restored into an isolated database with 67 public tables, latest changeset 0064, 3,895 organizations, 1,125 non-empty VAT scalars, and 318 legacy TAX identifiers; the verification database was then removed.
  • The first production dry-run reported 3,895 organizations, 1,125 non-empty scalars, 1,086 valid candidates, 27 permitted merged-source deduplications, 1,059 planned inserts, 39 invalid discards, 40 scalars on merged parties, and 318 legacy TAX identifiers. The baseline organization delta is exactly new blank-scalar parties 7267, 7268, and 7269. The old broad duplicate count included the two invalid Skupinove_DPH placeholders, and its prefix distribution included those two invalid SK-shaped values plus two malformed CZ values; excluding those four explains the final valid-candidate duplicate and jurisdiction counts.
  • A scheduled CME sync legitimately advanced the compatibility model between dry-run and apply: it linked 2,656 existing source rows, created no company, added 13 valid scalars, and wrote 577 already-consistent registrations. The apply command replanned under locks, accepted that explainable state, and inserted the remaining 490 registrations. The stable post-apply dry-run is exact and idempotent: 1,138 non-empty scalars = 1,067 already consistent + 32 permitted deduplications + 39 invalid discards, with zero planned inserts; the 1,099 valid candidates are distributed as CZ 1,057, SK 24, PL 9, DE 4, FR 2, and EE/IT/NL 1 each.
  • Production smoke evidence: company 3096 passed an authenticated no-op edit/read-back with HTTP 200 while preserving IČO 23921382, scalar VAT CZ23921382, and its single registration; company list/detail and the 28-row VAT catalog returned HTTP 200, and the public app health endpoint returned healthy. CME has 2,656 linked rows and zero SQL-like errors; all 769 linked parties with a non-empty scalar have a target registration. The target deliberately retains one duplicate normalized-value group across two parties, while all 40 merged-party scalars reconcile with 32 permitted deduplications and no target row remains on a merged party. Historical lead imports remain globally reconciled by the zero-insert inventory.
  • Safety boundary: no fake production company, lead mutation, or destructive merge was manufactured without an approved business target. Those writer paths remain covered by the focused and full local suites from items 34.2 and 34.3; live CME, manual edit, duplicate-value, and merged-party data exercised the production compatibility and reconciliation paths.
  • Final health checks found DB, backup, Google, backend, search, app, and worker containers running and healthy. Missing-column/SQL signature scans returned zero matches in DB/backend/app/worker logs; backend and app emitted no post-health errors. One DB operator is not unique error was caused by an acceptance query typo and its corrected query passed. The brief worker connection failures during container restart and the pre-existing Jira Unauthorized failures are unrelated and remain documented separately.

  • [x] 34.3 Build the deterministic scalar-only VAT inventory and backfill operation.

  • vat-registration-backfill defaults to a read-only repeatable-read dry-run; --apply first emits that report, then replans under organization/target row locks and performs plain inserts in one serializable transaction.
  • Every non-empty organization scalar is reconciled as planned/applied insert, already consistent, permitted deduplication, or discard with a party/value/ reason record. The report also exposes the production-baseline counters and jurisdiction distribution without hard-coding expected counts.
  • The refined merged-party rule prefers the valid active-target scalar, accepts one distinct merged-source value otherwise, permits equal values on different effective parties, and rejects unexplained target drift before the first insert. Legacy TAX identifiers are count-only audit metadata; their values, CME snapshots, and supplier facts are not backfill sources.
  • Validation: focused classification/merged-rule, dry-run, exact reconciliation, idempotency, conflict rollback, clear-and-replay, and CLI apply-flag tests pass; the full backend suite passes with 433 tests; script/check-architecture.sh, mkdocs build --strict, bash -n for the operator script, and git diff --check pass.

  • [x] 34.2 Introduce the single-registration compatibility write path.

  • One JDBC write path now validates and normalizes VAT, locks the organization row, and atomically mirrors the submitted raw scalar plus the normalized jurisdiction row for manual create/edit, lead promotion, and CME promotion. An unchanged invalid legacy scalar does not block unrelated edits.
  • VAT equality no longer blocks company creation or selects a lead match. party_identifier.TAX_IDENTIFIER is rejected by generic and country-specific mutation flows, is not created by imports, and is left on a merged legacy source for the separately gated cleanup.
  • Merge deduplicates equal same-jurisdiction registrations, rejects unequal same-jurisdiction values before moving related data, and keeps registrations from different jurisdictions. CME source snapshots and supplier evidence remain unchanged and non-authoritative.
  • Validation: focused company/party/lead/CME/merge/controller/client/form, duplicate-value, concurrency, and rollback tests pass; full backend suite passes with 428 tests and full app suite passes with 337 tests; script/check-architecture.sh, mkdocs build --strict, and git diff --check pass.

  • [x] 34.1 Add the expand-only VAT registration model from RP-4122.

  • Changeset 0066 creates and seeds all 28 supported VIES jurisdictions and creates party_vat_registration with the approved PK/FKs, checks, timestamps, one-row-per-party/jurisdiction constraint, and deliberately non-unique normalized-value index. Catalog checks confirm EL -> GR, XI -> GB, and CZ.local_name = DIČ.
  • Backend code has one syntax policy that uppercases before removing only whitespace, -, ., and /; rejects unsupported characters, unknown prefixes, and jurisdiction-invalid formats; and never performs live VIES validation. Raw and normalized values are both exposed by the read model.
  • Authenticated GET /api/vat-jurisdictions and additive vat_registrations[] fields are available on company list/detail payloads. The scalar tax_identifier remains intact. No mutation, backfill, global VAT ownership, matching, or merge behavior was added.
  • Validation: focused policy/repository/controller tests and the full backend suite pass (418 tests); Liquibase validate/update, standard rollback-count 1/update, and a targeted two-changeset rollback/update proved the 0066 rollback despite concurrent changeset 0067; the final local catalog has 28 rows and the expected constraints/indexes. The activity/contact diagram was regenerated and inspected; strict MkDocs, regenerated architecture report/check, and git diff --check pass.

  • [x] 33.2 Apply the proven-unused-column contract migrations in production.

  • Deployed the compatible database and backend on 2026-08-21. Both became healthy, the backend health endpoint returned ok, and the standard !contract Liquibase update applied no destructive changeset.
  • Recovery evidence: created reply-pilot-20260821T081752Z.sql.gz (18,034,429 bytes, SHA-256 458386dda2b54322b51871a42be93446f6c1e0664e471621943af1b29eff4db4), verified its checksum and gzip stream, restored it into an isolated database, checked representative counts (party 4,926, party_contact_mech 4,587, activity_email_attachment 6,325, and task_jira_organize_meeting 129), and removed the verification database.
  • Repeated all embedded production data assertions with zero violations, then applied exactly changesets 0060 through 0064. Changesets 0053 and 0054 remain unapplied. The retired columns are absent, the purpose-link primary key is (party_contact_mech_id, purpose_code), representative row counts are unchanged, and main-changelog validation plus a repeated standard update passed.
  • Production smoke evidence: company list/detail, email attachment, AI prompt, and organize-meeting task reads returned HTTP 200; a normal incremental mailbox import completed; and requirement evaluation, AI classification, and CME company synchronization completed after the contract. Backend and worker logs contain no missing-column or SQL errors.
  • Boundary: no synthetic company merge was introduced into production; the merge writers remain covered by the 380-test local backend verification. The scheduled organize-meeting resume reached Jira but Jira returned Unauthorized, a separately tracked operational condition already present before this deployment.

  • [x] 33.1 Decide and stage retirement of unused database objects.

  • Removed from application consumers and staged behind contract changesets 0060 through 0064: party_person.middle_name, both party_relationship role columns, party_contact_mech.from_date, the surrogate party_contact_mech_purpose.id, party_requirement_state.requested_at, activity_email_attachment.storage_backend_code, ai_prompt.sort_order, and the duplicate task_jira_organize_meeting timestamps. Purpose links now use their existing natural (party_contact_mech_id, purpose_code) key.
  • Production evidence on 2026-08-21 found zero meaningful middle names, role values, or requested timestamps; all 4,587 contact from_date values equal created_at; all 6,325 attachments use the only supported backend; the five prompt rows have the same configured and ID order; and all 126 organize-meeting timestamp pairs duplicate the root task timestamp.
  • Kept every proposed table and every column whose semantics or external use could not be disproved. In particular, all 2,880 CME source rows carry a meaningful first-seen timestamp, and 15 of 101 purpose links have a created_at distinct from their contact link.
  • Validation: the standard Liquibase context skipped all five contract changesets; explicit contract update, rollback, and repeated update passed with schema/value assertions. Full backend Maven verify passed 380 tests, Liquibase validate, PlantUML regeneration, architecture check, strict MkDocs build, and git diff --check passed.

  • [x] 32.9 Remove legacy IČO compatibility and drop the organization column in a later contract release.

  • Evidence: after explicit production acceptance, the targeted 0059 changelog ran exactly one changeset. The broader @contract command was not used because unrelated contract migrations 0053 and 0054 are still pending; both remained unapplied.
  • Evidence: the verified pre-contract backup is reply-pilot-20260820T131432Z.sql.gz (18,026,423 bytes, SHA-256 dafdf792b6c478baff45bee4df7b46dc5dbc81212897c87b32a67779cf60c206); backup and checksum are owned by agent:agent, mode 0600.
  • Evidence: the pre-drop assertion observed zero drift. Afterwards the legacy column, sync trigger, and trigger function are absent; the canonical format constraint and one-IČO-per-party index remain. Counts stayed at 2,964 canonical and 573 quarantined rows, with zero invalid or duplicate canonical values and zero parties with multiple canonical values.
  • Cleanup: the one-off inventory/backfill endpoint, worker, implementation, response model, and focused migration tests are removed. Ignored production JSON/CSV inventory exports were deleted after reconciliation; durable aggregate evidence remains here. Applied Liquibase changesets and quarantine-aware runtime filtering remain by design.
  • Validation: Liquibase validate, production health/read/search smoke checks, post-contract log inspection, local full module suites, architecture check, regenerated PlantUML, strict MkDocs build, and git diff --check passed.

  • [x] 32.8 Deploy and accept the identifier-read cutover while retaining a rollback-safe compatibility window.

  • Evidence: the operator reported the deployed application working and explicitly requested completion on 2026-08-20. Before contract execution, the legacy mirror and canonical identifiers had zero drift.
  • Evidence: backend company list, detail, and search smoke requests returned HTTP 200; backend, app, search, DB, backup, and worker containers were healthy. Managed application containers ran non-root; the DB retained its documented vendor-image runtime exception.
  • Evidence: post-cutover CME synchronization completed repeatedly with 703 supplier and 2,176 reservation source rows, 2,669 linked companies, and no newly created company. No missing-column, SQL grammar, or traceback errors appeared after the contract migration.
  • Boundary: no synthetic company/import/merge mutations were added to production for acceptance. Those writers were covered by the full local suites; live CME exercised the production identifier writer.

  • [x] 32.6 Accept the production quarantine of unresolved legacy registration values.

  • Evidence: production retains 573 non-empty raw values under LEGACY_UNRESOLVED_COMPANY_REGISTRATION_NUMBER: 475 belong to merged parties and 98 to active parties. Of the active rows, 4 parties also have a canonical IČO and 94 do not.
  • Evidence: active quarantine shapes are 1 eight-digit collision, 19 seven-digit values, 65 other numeric values, and 13 non-numeric values. They remain audit-only and are excluded from ownership, CME matching, search, and normal IČO display; no automatic owner was selected and no raw value was discarded.
  • Validation: production has zero invalid canonical IČO, zero duplicate canonical value groups, zero parties with multiple canonical values, and zero quarantine rows with an empty raw value.

  • [x] 32.5 Run the production preflight and unambiguous backfill with recovery evidence.

  • Evidence: expand changeset 0058 executed in production after the scheduled pre-deploy backup reply-pilot-20260820T111631Z.sql.gz; its checksum and an isolated restore were verified, and the temporary verification database was removed.
  • Evidence: the backup contained 3,530 non-empty organization values. Of those, 2,958 match the same party's current canonical identifier and 571 match its exact raw-preserved quarantine row. The remaining value was moved after the backup but before 0058 from party 3884 to newly created party 7263, where it is the sole canonical owner. There is zero unexplained loss.
  • Evidence: after expand migration production contained 2,964 canonical and 573 quarantined rows. Canonical format, global uniqueness, and one value per party checks all returned zero violations; the rollback mirror had zero drift. The later verified pre-contract backup is recorded in item 32.9.

  • [x] 32.7 Enforce the target IČO invariants and switch application reads.

  • Evidence: backend company reads/writes, manual forms, lead import, CME sync, merge, search indexing, and the wholesale export query now use canonical party_identifier rows while the public company_registration_number property remains stable. Quarantine rows are excluded from ownership, matching, search, and normal IČO display.
  • Evidence: expand changeset 0058 safely backfills valid unambiguous legacy IČO, preserves all unresolved raw values under LEGACY_UNRESOLVED_COMPANY_REGISTRATION_NUMBER, enforces eight-digit canonical values and at most one canonical IČO per party, and installs the rollback-window compatibility trigger. Standard deploy skipped contract changeset 0059; its later explicit production execution is item 32.9.
  • Evidence: the contract pre-drop assertion rejects drift in either direction; an injected local blank-legacy/canonical mismatch stopped the migration. After reconciliation, explicit contract apply, rollback, and repeated apply passed. This was implementation-stage validation; the later production change is recorded in items 32.5 and 32.9.
  • Evidence: canonical IČO replacement deletes the old row before claiming the new value inside the same transaction, so the one-IČO-per-party index does not block edits and an ownership conflict still restores the old row.
  • Validation: full backend Maven verify passed 380 tests, app pytest passed 333 tests, search pytest passed 19 tests, and wholesale-scout pytest passed 9 tests. Liquibase validate plus expand and contract update/rollback/update lifecycles, PlantUML regeneration, script/check-architecture.sh, mkdocs build --strict, and git diff --check passed.

  • [x] 32.1 Lock the canonical company-identifier target and migration invariants.

  • Evidence: party_identifier is the final identity store while the public company_registration_number field remains stable during compatibility; value_raw preserves entered/source text and value_normalized is the comparison and uniqueness value.
  • Evidence: COMPANY_REGISTRATION_NUMBER is reserved for Czech IČO normalized to eight ASCII digits. Verified foreign commercial-register entries use COMMERCIAL_REGISTER_NUMBER with country, issuing register, section, and number encoded in value_normalized.
  • Boundary: DIČ and other identifier families remain separate. No malformed foreign-looking value is promoted without verification of its type and issuer.
  • Validation: code/data-flow inventory is recorded in the refactoring context and database/activity-model documentation.

  • [x] 32.4 Build a dry-runnable IČO inventory and safe backfill operation.

  • Historical evidence: POST /api/company-registration-numbers/backfill/run defaulted to a read-only dry-run and accepted apply only as the explicit JSON boolean true. Its ordered row payload contained only manual-correction classes with raw/normalized values plus target owner, current target values and duplicate party IDs. The endpoint and implementation were removed after the migration completed in item 32.9.
  • Historical evidence: every non-empty legacy value received exactly one deterministic classification: already consistent, safe to backfill, duplicated across active parties, owned by another party, mismatched, malformed, or tied to a merged party. Valid active duplicates were classified together before any ownership winner could be inferred.
  • Historical evidence: apply locked legacy rows in party-ID order, inserted only the safe class with plain INSERT, preserved the original legacy string in value_raw, and recomputed after-counts in the same transaction. It did not update unresolved rows or use ON CONFLICT DO NOTHING; an unexpected unique conflict rolled back all inserts.
  • Boundary: no schema or production data changed. The operation deliberately reused COMPANY_REGISTRATION_NUMBER before item 32.1 fixed the final scheme boundary; production backup, preflight, dry-run, and apply remain item 32.5.
  • Validation: focused H2 fixtures cover every class, dry-run zero mutation, exact raw-value preservation, before/after reconciliation, repeat-run idempotency, and full rollback after a deterministic injected uniqueness race. The focused backend suite passed 9 tests; full backend Maven verify passed 377 tests, all 331 app tests passed, and script/check-architecture.sh, mkdocs build --strict, and git diff --check passed.

  • [x] 32.3 Route non-interactive writers and company merge through the same IČO contract.

  • Evidence: lead import and CME synchronization normalize optional source IČO values through the shared eight-ASCII-digit policy and claim the existing COMPANY_REGISTRATION_NUMBER target without ON CONFLICT DO NOTHING. Missing values remain allowed; an existing company is enriched only when its authoritative and temporary compatibility values are empty or equal.
  • Evidence: malformed or incompatible lead values roll back the company mutation, leave lead_import_item.source_record_json unchanged, and are stored in last_error by the existing service flow. CME preserves the raw source value in party_cme_source, records last_error, and uses a row savepoint so another valid source row can still synchronize.
  • Evidence: CME matching and lead-import IČO matching now use the target identifier. Existing ownership is followed rather than transferred to a fallback company, and a fallback company with another authoritative IČO is rejected without creating a second owner.
  • Evidence: company merge locks both companies, permits zero or one distinct authoritative IČO and keeps the temporary target compatibility value in sync, and rolls back before moving any related data when more than one non-empty authoritative IČO is present.
  • Boundary: no schema migration, existing-data backfill, read-model cutover, scheme/issuer decision, or automatic party merge was included. The scheme decision was completed later in 32.1; the remaining work stays in 32.5-32.9.
  • Validation: focused JDBC lead-import, CME-sync, and company-merge tests cover missing, normalized/equal, malformed, different, and already-owned values; mvn -q -f reply-pilot-be/pom.xml verify, python3 -m pytest -q reply-pilot-app/tests (331 passed), script/check-architecture.sh, and mkdocs build --strict passed.

  • [x] 32.2 Establish one backend IČO validation and ownership operation while preserving current UI/API behavior.

  • Evidence: CompanyRegistrationNumberPolicy is the shared backend trust boundary for whitespace removal and the existing eight-ASCII-digit Czech IČO shape. Manual create and changed edit values use it before persistence.
  • Evidence: the JDBC company mutation repository transactionally claims the existing COMPANY_REGISTRATION_NUMBER target identifier and maintains the temporary party_organization.company_registration_number value. A plain unique insert detects concurrent ownership races; authoritative IČO writes do not use ON CONFLICT DO NOTHING, and a conflict rolls back unrelated company-field updates.
  • Evidence: an unchanged valid and unique legacy IČO with no target conflict is safely claimed on save. Unchanged malformed, duplicated, mismatched, or already-conflicting legacy IČO still does not block an unrelated edit and is not silently claimed. Backend field_errors.company_registration_number survives the app HTTP clients and renders on the existing create/edit form field.
  • Boundary: no schema migration was needed. This slice reused the existing identifier type before the scheme/issuer scope was completed in 32.1. Non-interactive writers and merge remained 32.3; inventory, backfill, read cutover, constraints, and compatibility removal remained 32.4-32.9.
  • Validation: focused backend service/repository/controller and app form/client tests cover valid, malformed, duplicate, unchanged-dirty, rollback, and concurrent create/update cases. mvn -q -f reply-pilot-be/pom.xml verify, python3 -m pytest -q reply-pilot-app/tests (331 passed), regenerated architecture report plus script/check-architecture.sh, and mkdocs build --strict passed.

  • [x] 31.1 Lock the Google integration ownership target and staged migration plan.

  • Evidence: refactoring context now distinguishes the Google Cloud project, OAuth branding/client configuration, technical Gmail credential, per-user grant, runtime module, DB ownership, and consumer API boundary.
  • Evidence: tracker slices separate compatibility-preserving source rename, consumer switch, production Gmail cutover, Calendar ownership move, production Calendar acceptance, and destructive compatibility cleanup.
  • Evidence: repository inventory found 167 current Gmail module/name/base-URL references across 67 code/config/doc files and three SOPS artifacts per environment (.env, technical Gmail token, Pub/Sub service account).
  • Evidence: the read-only production audit recorded the running container, consumer endpoints, old/new module-path state, token ownership/mode, and mailbox data size/file count without printing credential values.
  • Validation: documentation-only slice; mkdocs build --strict and git diff --check passed.

  • [x] 31.2 Rename the Gmail source/runtime module to reply-pilot-google without changing Gmail behavior.

  • Evidence: the directory, Maven artifact, Java package root cz.replypilot.google.gmail, application class, image/container, Compose project/service, operational scripts, root orchestration, SOPS source names, and documented runtime paths now use reply-pilot-google.
  • Evidence: Gmail endpoint paths, GMAIL_* behavior, cache layout, watch behavior, and test fixtures remain intact. Compose publishes canonical reply-pilot-google and temporary reply-pilot-gmail aliases; the running BE and Jira Reports containers reached the renamed service through the old alias, including a read-only weekly-count request.
  • Evidence: decrypted local/prod technical token and Pub/Sub payload hashes match their pre-rename Git versions. All non-REMOTE_DIR env values match; both REMOTE_DIR values point to the new module path. Local materialized token and Pub/Sub files match their encrypted sources and are mode 0600.
  • Evidence: the preserved local data tree remains 930 MB / 9,665 files. The renamed container is healthy as non-root UID:GID 501:20, uses reply-pilot-google:latest, keeps /app/conf read-only, and resolves both DNS aliases to the same address. Production was not mutated; its old data path remains reserved for the later rollback-safe cutover.
  • Validation: Google Maven clean verify passed all 60 tests; Docker build, Compose config, shell syntax, health/cache/watch/weekly-count smokes, SOPS decrypt and payload-preservation checks, regenerated PlantUML PNGs, regenerated architecture report plus script/check-architecture.sh, mkdocs build --strict, and git diff --check passed.

  • [x] 31.3 Switch repository consumers and architecture truth to the canonical reply-pilot-google endpoint.

  • Evidence: BE runtime defaults, Compose, scripts, env template, HTTP adapter diagnostics, and focused tests now use http://reply-pilot-google:5000. Jira Reports config, Compose, scripts, env template, tests, and docs use the same canonical endpoint. Worker has no direct Google endpoint and remains a BE consumer, so no speculative worker configuration was added.
  • Evidence: local and production SOPS source values for BE GMAIL_API_BASE_URL and Jira Reports JIRA_REPORTS_EMAIL_BASE_URL were set to the canonical endpoint and verified with sops decrypt --extract. Decrypted hashes excluding the changed key prove that no other value in those four encrypted env files changed. Production runtime was not deployed; its observed old consumer values remain recorded for item 31.4.
  • Evidence: module/runtime/current architecture documentation now uses the canonical endpoint. The reply-pilot-gmail Compose alias remains only for rollback. Existing Google Cloud Pub/Sub topic/subscription fixture names were intentionally preserved because they identify external resources, not Docker consumers.
  • Evidence: rebuilt local BE and Jira Reports containers are healthy as UID:GID 501:20, expose the canonical URL in their runtime environments, keep /app/conf read-only, and reached the unchanged healthy Google container. Canonical health, BE watch read, BE Gmail-backed inbox read, and Jira Reports weekly-count read all returned HTTP 200. The old alias health also returned 200 as explicit rollback evidence; the Gmail token remains mode 0600.
  • Validation: BE Maven clean verify passed 339 tests, Google Maven clean verify passed 60 tests, worker pytest passed 30 tests, and Jira Reports pytest passed 21 tests. Shell syntax and all three Compose configs passed; PlantUML PNGs and the architecture report were regenerated; script/check-architecture.sh, mkdocs build --strict, and tracked plus untracked whitespace checks passed.

  • [x] 31.4 Cut production Gmail capability over to reply-pilot-google with a rollback-safe data migration.

  • Evidence: the stopped-source copy from the old production data/conf paths to the canonical Google paths matched exact tree digests. The Gmail data digest was cbc301ad44815df7e6943950200b7d781bb9dd65c6ae7cfae2b1b26e950a6cd0 and the conf digest was c5c6ae4b7e8170e53e887cfc527e47fcc5c374e57521aabccdc3f5bada758639. The canonical tree contains 375 MB / 4,921 files and is owned by agent:agent; the technical token and Pub/Sub credential source hashes matched their deployed files, which are both mode 0600.
  • Evidence: production Google, BE, and Jira Reports containers are healthy as UID:GID 1001:1001, use restart: unless-stopped, and mount /app/conf read-only. BE GOOGLE_API_BASE_URL and Jira Reports JIRA_REPORTS_EMAIL_BASE_URL both resolve to http://reply-pilot-google:5000.
  • Evidence: Google health, cache, watch, weekly-count, and live Gmail read smokes returned HTTP 200. BE health, watch, and Gmail-backed inbox returned HTTP 200. Jira Reports reached the Google weekly-count endpoint internally, and its health, loopback index, and public HTTPS index all returned HTTP
    1. Fresh Google, BE, and Jira Reports logs contained no ERROR, FATAL, or traceback lines.
  • Evidence: the original reply-pilot-gmail container is stopped rather than deleted, its original data/conf paths remain intact, and it has both reply-pilot-gmail and reply-pilot-google aliases. The active canonical container also retains the old alias. Rollback can therefore stop Google and restart the preserved Gmail container without a consumer or HAProxy change; no cleanup is authorized before item 31.7.
  • Scope: deploying current HEAD also placed the already implemented Calendar ownership code in the Google and BE images. Both production Calendar OAuth client values were verified empty in SOPS and in the running Google container, so Calendar stayed inactive and item 31.6 was not accepted.
  • Validation: Google Maven clean verify passed 66 tests, BE Maven clean verify passed 338 tests, Jira Reports pytest passed 21 tests, shell syntax and production Compose rendering passed, and the changed production Google HOST_DATA_DIR and HOST_CONF_DIR SOPS values were verified with sops decrypt --extract without disclosing credentials.

  • [x] 31.5 Move per-user Google Calendar OAuth and Calendar API ownership from BE to reply-pilot-google.

  • Evidence: reply-pilot-google exclusively performs OAuth exchange, credential encryption/decryption, refresh, revoke, and Google Calendar API calls. BE retains app-user authorization and the app-facing contract, stores only the opaque encrypted envelope in the existing app_user_google_calendar_profile, and calls the narrow internal API with GOOGLE_API_TOKEN; no schema migration was needed.
  • Evidence: Calendar OAuth client settings, token-encryption key, and shared internal token are owned by local and production secrets/<environment>/reply-pilot-google.env. The corresponding Calendar client and encryption secrets are absent from BE configuration.
  • Validation: full Google and BE Maven clean verify suites passed, all 327 app tests passed, and focused OAuth/status/CRUD/ACL/recurrence, refresh/revoke, failure-contract, and app form/profile tests passed.

  • [x] 31.6 Deploy the Calendar ownership cutover and accept the production flow.

  • Evidence: production Google and BE run the ownership-cutover code with the Calendar OAuth and encryption configuration present only in the Google runtime. The profile table contains four v1. encrypted grants; all four are on the current envelope format and the latest profile was updated on 2026-08-18.
  • Evidence: a live Backend status request for the most recently updated profile returned HTTP 200 with configured=true and connected=true, exercising BE's profile read and authenticated call into the Google module. Existing production use therefore supplies stronger consent and refresh evidence than a new local consent ceremony.
  • Scope: this closure did not create, edit, disconnect, or delete a real production calendar event or grant. Those mutating, read-only-calendar, recurring-event, disconnect, and reconnect contracts are covered by the passing Google/BE/app suites; repeating them against a user's live calendar was unnecessary and would add production side effects without closing an observed gap.

  • [x] 31.7 Remove temporary Gmail naming compatibility after production stability confirmation.

  • Evidence: the repository's only live compatibility alias was removed from Google Compose, active runtime/module documentation now names only reply-pilot-google, and the canonical module was redeployed. Remaining reply-pilot-gmail strings are historical archive evidence, the explicit cleanup record, or Google Cloud Pub/Sub fixture identifiers rather than Docker consumers.
  • Evidence: before deletion, the target was re-resolved as the stopped reply-pilot-gmail container, its sole-use image sha256:3d54958aaec92cde843430b804ac71cd32d8b1e93cbfc5b2f1ee2bee1d6bcd13, and the separate 437 MB / 5,040-file legacy module path. The canonical path was independently present with 444 MB / 5,283 files. The stopped container, image, and /home/agent/docker_deployments/reply-pilot/reply-pilot-gmail were then deleted and verified absent; canonical data and encrypted SOPS sources remain the recovery sources.
  • Evidence: the redeployed canonical container is healthy as UID:GID 1001:1001, uses restart: unless-stopped, mounts /app/conf read-only, and has no legacy Docker alias. Google health, cache status, watch, weekly email counts, and inbox smokes returned HTTP 200 before and after cleanup. Backend Calendar remained HTTP 200/connected, Jira Reports reached the canonical weekly-count endpoint with HTTP 200, and recent Google logs had zero ERROR, FATAL, or traceback lines.
  • Validation: Google and BE Maven clean verify, all 327 app tests, Google Compose rendering, module shell syntax, regenerated architecture report plus script/check-architecture.sh, mkdocs build --strict, repository reference audit, and git diff --check passed.

  • [x] 30.8 Close the delegated Jira migration with an exhaustive call-site audit, documentation, and live Jira acceptance.

  • Evidence: the production call-site audit classifies every browser-triggered Jira read/write as delegated with the backend-authenticated app user. The only technical-account allowlist is Jira task sync, automatic incoming-email Jira operations, and Jira reports.
  • Evidence: the OAuth authorization request includes read:jira-user, read:jira-work, write:jira-work, and offline_access. Create preserves an already returned Jira key if its follow-up enrichment read requires a new consent, preventing false 428 responses and duplicate retries.
  • Evidence: after re-consent, live status and /myself succeeded. RP-3509 was created through Reply Pilot and Jira recorded the same human user as creator, reporter, comment author, attachment author, unassign and reassignment author, summary/description update author, and transition author. The task finished in Done. Earlier invalid-format live testing returned 428 jira_oauth_required; a no-header background task-sync smoke succeeded through the technical no-actor path.
  • Evidence: Jira OAuth, backend, web-app, integration, module README, profile microcopy, refactoring context, and the PlantUML/PNG flow all describe the final boundary.
  • Validation: backend mvn -q verify, all 289 app tests, all 21 Jira reports tests, regenerated architecture report plus script/check-architecture.sh, strict MkDocs build, regenerated PlantUML PNG, and git diff --check passed.

  • [x] 30.7 Delegate all remaining user-facing Jira reads while preserving the background allowlist.

  • Evidence: Jira proxy myself, user lookup, issue/search/assigned/stale/ detail/transition/comment read routes now require the backend-authenticated app user and pass that actor through JiraProxyService to the delegated Jira client. Missing, malformed, or unusable OAuth credentials use the shared HTTP 428 jira_oauth_required contract and never retry with the technical account.
  • Evidence: explicit task refresh passes the actor returned by the existing task authorization check into the Jira issue read. Direct and delegated task access therefore use the authenticated caller's OAuth token, not a task owner or request-body identity.
  • Evidence: task overview/detail, related-task, comment, transition, and Supplier Onboarding read workflows preserve available local data and offer the existing Jira connection action when Jira returns 428. One request emits at most one connection prompt, and a missing token suppresses the global remote task badge instead of turning unrelated pages into errors.
  • Evidence: Jira task synchronization, automatic incoming-email Jira operations, and Jira reports remain the explicit no-human-actor technical allowlist. Regression coverage asserts that task sync and incoming-email automation do not acquire an arbitrary app-user actor.
  • Validation: focused backend Jira proxy/task refresh/service tests and focused app OAuth/read UI tests passed; the full Flask suite passed with 289 tests, the Jira reports suite passed with 21 tests, full backend mvn -q verify, mkdocs build --strict, and git diff --check passed. script/check-architecture.sh still reports the pre-existing stale reports/architecture-report.md baseline documented in the unfinished audit; this slice did not regenerate the broader dirty-tree report.

  • [x] 30.6 Secure and delegate lead-import and generic Jira mutation APIs.

  • Evidence: the lead-import action controller requires authenticated UserAuthorization, ignores request-body operator_user_id as identity, and passes the authenticated appUserId as operator and Jira actor. Lead-import Jira user lookup, issue creation, optional transition, and final description update use that same actor. A delegated OAuth failure is rethrown to the shared 428 jira_oauth_required handler after preserving the existing item-error recording and external-side-effect ordering.
  • Evidence: generic Jira create, update, transition, comment, and unassign routes now require the existing trusted app-user context and use its actor for every Jira write. A repository-wide production string-literal audit found no consumers outside reply-pilot-app; consumers outside this repository cannot be proven absent and must now send the trusted authentication context. The existing app HTTP clients already attach that header.
  • Evidence: the lead-import app workflow handles the shared OAuth exception, renders HTTP 428, keeps the submitted operator note and email fields, and offers the existing Jira connection flow. No new client, credential store, or technical-account fallback was added.
  • Validation: focused LeadImportServiceTest,LeadImportControllerTest,JiraProxyControllerTest passed with 13 tests; the controller test wires the real JiraProxyService because no standalone JiraProxyServiceTest exists. Focused app OAuth/client/lead-import coverage passed with 11 tests; full mvn -q verify, the full Flask suite with 286 tests, mkdocs build --strict, and git diff --check passed. script/check-architecture.sh still reports the pre-existing stale reports/architecture-report.md baseline documented in the unfinished audit; this slice did not regenerate the broader dirty-tree report.

  • [x] 30.5 Delegate Jira attachment operations.

  • Evidence: each existing attachment controller action retains its task authorization check and passes the resulting UserAuthorization.appUserId() to the attachment service. Settings, listing, streamed upload, same-filename replacement deletion, rollback deletion, download metadata verification, and binary content download use that actor; no delegated failure retries with technical credentials.
  • Evidence: replacement failures keep the previous filename behavior and attempt to remove the newly uploaded attachment with the same actor. OAuth failure during replacement preserves the shared HTTP 428 jira_oauth_required contract after the delegated rollback attempt. Existing size/type validation, task authorization, response headers, and multipart streaming remain unchanged.
  • Evidence: the Flask attachment client sends the authenticated actor only in the trusted backend header, including its streamed multipart request. Attachment list, upload, and download workflows handle the shared OAuth exception and offer the existing Jira connection flow instead of returning a 500.
  • Validation: focused backend TaskAttachmentServiceTest,TaskAttachmentControllerTest,HttpJiraIssueClientTest passed with 17 tests; focused app attachment/client/OAuth coverage passed with 11 tests; full mvn -q verify, full Flask suite with 284 tests, mkdocs build --strict, and git diff --check passed. script/check-architecture.sh still reports the pre-existing stale reports/architecture-report.md baseline documented in the unfinished audit; this slice did not regenerate the broader dirty-tree report.

  • [x] 30.4 Propagate the authenticated actor through the remaining user-triggered task/company mutations.

  • Evidence: the six existing task mutation controller actions retain their authorization checks and pass the resulting UserAuthorization.appUserId() separately from requester and assignee business data. Company reassignment, Supplier Onboarding creation/edit, Reply Pilot Task creation, task reassignment, and manual Email Thread Reply task creation use that actor for Jira user lookup, issue reads, creation, updates, and assignment.
  • Evidence: the Flask task mutation and email clients expose the shared jira_oauth_required error, and affected company, reassignment, task, and email workflows offer the existing Jira connection flow without changing their browser or backend routes. Supplier Onboarding and Reply Pilot Task post-create link failures preserve the existing partial-success warning instead of inviting duplicate issue creation.
  • Validation: focused backend TaskMutationServiceTest,TaskMutationControllerTest passed with 40 tests; focused app client/workflow coverage passed with 11 tests; full mvn -q verify, full Flask suite with 283 tests, mkdocs build --strict, and git diff --check passed. script/check-architecture.sh still reports the pre-existing stale reports/architecture-report.md baseline documented in the unfinished audit; this slice did not regenerate the broader dirty-tree report.

  • [x] 30.3 Delegate task workflows that already carry an authenticated actor.

  • Evidence: Supplier Onboarding reply uses one authenticated app user for issue/status/comment reads, comment creation, assignment, and transition; Reply Pilot resolve uses that actor for assignee lookup, comment, and issue update; Email Thread Reply update uses it for lookup, update, and every requested transition, not only report-attributed statuses.
  • Evidence: Reply Pilot close, move-to-waiting, Email Thread Reply close, and the post-forward move-to-waiting path retain their actor-aware transition metadata. Assignee/requester ids remain business data and never select the OAuth credential. Supplier reply comment identity is loaded from the authenticated app user rather than trusted request payload fields.
  • Evidence: an OAuth reconnect requirement after a Supplier comment or after an email send returns the existing partial-success warning instead of inviting a retry of an already completed external side effect. Existing authorization, status checks, and local cache synchronization remain unchanged.
  • Validation: focused backend TaskMutationServiceTest,TaskMutationControllerTest,EmailSendControllerTest passed with 51 tests; seven corresponding Flask workflow tests passed; full mvn -q verify, full Flask suite with 281 tests, mkdocs build --strict, and git diff --check passed. script/check-architecture.sh still reports the pre-existing stale reports/architecture-report.md baseline documented in the unfinished audit; this slice did not regenerate the broader dirty-tree report.

  • [x] 30.2 Make OAuth-required handling one consistent backend/app contract.

  • Evidence: JiraOAuthRequiredExceptionHandler is the single backend mapping to HTTP 428 with status=error, code jira_oauth_required, and the original reason; controller-local copies were removed.
  • Evidence: missing, malformed, missing-refresh, rejected-refresh, and delegated Jira 401 paths converge on JiraOAuthRequiredException; no delegated request falls back to the technical account.
  • Evidence: task mutation, Jira backend, attachment, and lead-import HTTP clients require both HTTP 428 and the contract code before exposing the shared JiraOAuthRequiredError.
  • Evidence: Reply Pilot resolve, Supplier Onboarding reply, and Email Thread Reply edit preserve submitted values and render the shared Jira connection prompt; existing actor-aware transition actions offer the same connection flow without changing their success routes or payloads.
  • Validation: mvn -q -Dtest=JiraOAuthServiceTest,JiraProxyControllerTest,TaskMutationControllerTest test passed; focused app client/workflow/template selection passed with 11 tests; full mvn -q verify, full app suite with 281 tests, and mkdocs build --strict passed.

  • [x] 30.1 Add an explicit delegated Jira request path for every supported API operation.

  • Evidence: JiraIssueClient now exposes required actor-aware variants for issue reads/searches, user lookup, create/update, assign/unassign, transition, comments, and attachment settings/list/upload/download/delete; the existing no-actor variants remain the explicit technical-account path.
  • Evidence: actor-aware methods are not allowed to default to technical methods, so an implementation cannot compile while silently omitting the delegated path.
  • Evidence: HttpJiraIssueClient resolves one JiraDelegatedAccess through the existing JiraOAuthAccessProvider, reuses it across every request in the operation, and sends Bearer authentication to api.atlassian.com/ex/jira/{cloudId}/rest/api/3; the same request access object also covers multipart upload and binary download.
  • Evidence: delegated HTTP 401 responses become JiraOAuthRequiredException; no delegated method retries with Basic credentials. Existing technical methods still use the configured Basic authentication and Jira base URL for background callers.
  • Validation: mvn -q -Dtest=HttpJiraIssueClientTest,JiraOAuthServiceTest test passed, covering Bearer/cloud URL selection, token refresh, delegated transition metadata, delegated attachment operations, delegated 401, and the technical Basic-auth path.
  • Validation: full mvn -q test passed in reply-pilot-be.
  • Validation: mkdocs build --strict and git diff --check passed.

  • [x] 29. Add sent-only Gmail threads to the single Gmail-owned cache.

  • Evidence: POST /api/mailbox/snapshot accepts optional skip_cached_threads; when true, reply-pilot-gmail reads cached thread IDs from its index, lists Gmail thread IDs for the requested query, skips cached IDs before threads.get, and merges new sent-only threads into the same account-scoped cache.
  • Evidence: reply-pilot-be supports sync_mode=sent_backfill and POST /api/mailbox/sync/sent; both route through the existing mailbox import state machine and worker step endpoint.
  • Evidence: full mailbox sync is now sequential: inbox snapshot with reset, then in:sent with page size 20 and skip_cached_threads=true, then the activity import runs against the unified cache.
  • Evidence: Gmail watch defaults and encrypted local/prod Gmail env now use GMAIL_WATCH_LABEL_IDS=INBOX,SENT; ensureWatch re-registers when saved labels, filter behavior, or topic differ from current configuration.
  • Evidence: incremental history keeps cache-relevant threads when any message in the thread has INBOX or SENT.
  • Validation: mvn -f reply-pilot-gmail/pom.xml clean verify, mvn -f reply-pilot-be/pom.xml clean verify, and python3 -m pytest reply-pilot-worker/tests passed during implementation.
  • Validation: script/check-architecture.sh, mkdocs build --strict, and git diff --check passed; GMAIL_WATCH_LABEL_IDS was verified through SOPS for local and prod Gmail env files.

  • [x] 28. Move Gmail mailbox cache ownership back to the Gmail module.

  • Evidence: the old weekly-report-only state was partial; the completed boundary is now Google Gmail API/mailbox -> reply-pilot-gmail -> reply-pilot-be.
  • Evidence: reply-pilot-gmail owns the account-scoped physical cache under data/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/, including index, thread snapshots, attachment metadata, attachment bytes, remote page token, and history cursor.
  • Evidence: Gmail attachment relative_path values remain logical paths such as emails/attachments/<thread>/<file>, so DB/debug-context behavior does not need a migration.
  • Evidence: reply-pilot-gmail exposes internal cache read endpoints for BE inbox/detail/attachment reads and the Java Spring Boot GET /api/reports/email-weekly-counts endpoint computes from Gmail-owned cache/runtime data.
  • Evidence: reply-pilot-jira-reports defaults JIRA_REPORTS_EMAIL_BASE_URL to http://reply-pilot-gmail:5000 in config, Compose, scripts, docs, and env template.
  • Evidence: reply-pilot-be uses an HTTP-backed cache adapter in EMAIL_SYNC_BACKEND=gmail_service mode, does not create a Gmail cache file repository bean in that mode, and proxies attachment bytes from the Gmail module instead of reading local BE files.
  • Validation: focused Gmail/BE cache tests passed during implementation; full validation is tracked by the implementation task outcome.

  • [x] 27.7 Align search, docs, and architecture checks after delegated task workflow changes.

  • Evidence: reply-pilot-search indexes reply_pilot_task Jira issues, includes delegated task_type_value in task search text/subtitles, and stores delegated_task_assignee_app_user_id only for active delegated tasks.
  • Evidence: assigned-scope task search now allows active delegated tasks for the cached assignee in addition to unscoped tasks and tasks owned through normal company assignment.
  • Evidence: Done delegated tasks remain indexed for explicit search through normal company scope or company.view_all, but they do not get the active delegated-assignee search exception.
  • Evidence: docs/backend.md, docs/permissions.md, docs/web-app.md, reply-pilot-search/README.md, and docs/refactoring-context.md document the final search/visibility behavior.
  • Evidence: reports/architecture-report.md was regenerated after the search implementation change, and script/check-architecture.sh passed against the updated baseline.
  • Validation: focused search validation python3 -m pytest reply-pilot-search/tests/test_runtime.py -k 'task_scope or task_documents' passed with 2 tests; full search validation python3 -m pytest reply-pilot-search/tests passed with 19 tests; python3 script/architecture-report.py --output reports/architecture-report.md, script/check-architecture.sh, mkdocs build --strict, and git diff --check passed.

  • [x] 27.6 Add delegated task UI workflow.

  • Evidence: reply-pilot-app keeps reply_pilot_task as its own Jira work type, reads backend-provided task_type_value into task list/detail models, and shows delegated task context in the existing task overview.
  • Evidence: delegated task detail renders the linked company, Jira description, and read-only Jira comments through the extracted _jira_comments.html component shared with the current supplier task UI.
  • Evidence: assignees can open a one-textarea resolve page that calls backend POST /api/tasks/{taskId}/reply-pilot-task/resolve; requesters can close returned tasks through a POST action that calls POST /api/tasks/{taskId}/reply-pilot-task/close.
  • Evidence: app routes remain thin views.py route-map entries delegated to reply_pilot_app.workflows.tasks, and mutation controls follow the UI rule that data-changing actions are submitted with buttons.
  • Evidence: docs/web-app.md and docs/refactoring-context.md describe the app delegated task UI and backend-owned mutation boundary.
  • Validation: focused app validation python3 -m pytest reply-pilot-app/tests/test_task_mutation_client.py reply-pilot-app/tests/test_app.py -k 'reply_pilot_task or task_mutation_client_maps_task_mutation_payloads or supplier_onboarding_reply_page_renders_summary_and_comments_newest_first' passed with 6 tests; full app validation python3 -m pytest reply-pilot-app/tests passed with 249 tests; mkdocs build --strict and git diff --check passed.

  • [x] 27.2 Add delegated task cache schema.

  • Evidence: migration reply-pilot-db/migrations/0041_add_reply_pilot_task_subtype.sql adds reply_pilot_task to task_jira_work_type and creates task_jira_reply_pilot_task with task_type_value, requester_app_user_id, timestamps, FK rollback, and no Jira dropdown check constraint.
  • Evidence: reply-pilot-db/migrations/db.changelog.xml includes the new changeset, and docs/database.md, docs/backend.md, docs/permissions.md, and docs/refactoring-context.md describe the delegated task cache, requester ownership, and narrow authorization rules.
  • Evidence: Docker was running with healthy reply-pilot-db, and the local DB ended in the migrated state after update, rollback, and re-update.
  • Validation: ./scripts/db-liquibase.sh validate, ./scripts/db-liquibase.sh update, ./scripts/db-liquibase.sh rollback-count 1, and ./scripts/db-liquibase.sh update all passed from reply-pilot-db/; mvn -f reply-pilot-be/pom.xml verify passed with 212 tests; mkdocs build --strict and git diff --check passed.

  • [x] 27.5 Implement delegated task mutations.

  • Evidence: TaskMutationController exposes POST /api/tasks/{taskId}/reply-pilot-task/resolve and POST /api/tasks/{taskId}/reply-pilot-task/close; both require an authenticated app user and delegate ownership checks to the backend service instead of adding a new RBAC permission.
  • Evidence: TaskMutationService resolves only for the cached Jira assignee of an active delegated task, appends the submitted text to Jira/plain-text description, reassigns Jira/local cache to the stored requester app user, and preserves In Progress.
  • Evidence: requester close transitions Jira/local cache to Done without changing assignee, and wrong-assignee/wrong-requester paths return 403.
  • Evidence: JdbcTaskMutationRepository loads delegated mutation targets and checks active assignee ownership through task_jira.user_id or the app user's Jira account profile; tests cover service, controller, and JDBC behavior.
  • Evidence: docs/backend.md, docs/permissions.md, docs/database.md, and docs/refactoring-context.md document the delegated task mutation contract. DB migration validation for the subtype table remains tracked separately by blocked item 27.2.
  • Validation: mvn -f reply-pilot-be/pom.xml -Dtest=TaskMutationServiceTest,TaskMutationControllerTest,JdbcTaskMutationRepositoryTest test passed with 24 tests; mvn -f reply-pilot-be/pom.xml verify passed with 212 tests; mkdocs build --strict and git diff --check passed.

  • [x] 27.4 Implement task-derived company visibility.

  • Evidence: JdbcReadModelRepository now grants a read-only delegated task exception inside company/task read-model scope checks when the current app user is the cached Jira assignee of an active reply_pilot_task linked to that company.
  • Evidence: the delegated exception does not change mutation authorization; it excludes unrelated companies, unassigned delegated tasks, and Done delegated tasks.
  • Evidence: task read paths use task-level delegated scope, so company access from one active assigned delegated task does not expose other delegated tasks on the same company.
  • Evidence: docs/permissions.md, docs/backend.md, docs/database.md, and docs/refactoring-context.md record the read-only delegated visibility rule and the remaining search alignment boundary.
  • Validation: mvn -f reply-pilot-be/pom.xml -Dtest=JdbcReadModelRepositoryTest test passed with 6 tests; mvn -f reply-pilot-be/pom.xml verify passed with 208 tests; mkdocs build --strict and git diff --check passed.

  • [x] 27.3 Extend backend Jira sync and read models for delegated tasks.

  • Evidence: JiraTaskSyncService now includes configured Reply Pilot Task issues in task-sync JQL, maps them to local work type reply_pilot_task, copies assignee/status/summary/description as before, and normalizes blank delegated task type values to General.
  • Evidence: HttpJiraIssueClient requests configured JIRA_REPLY_PILOT_TASK_TYPE_CUSTOM_FIELD_ID (default customfield_10269) in search/detail reads and parses Jira dropdown objects/lists into JiraIssue.replyPilotTaskTypeValue.
  • Evidence: JdbcJiraTaskSyncRepository upserts task_jira_reply_pilot_task.task_type_value for reply_pilot_task rows and removes stale subtype rows if an issue is no longer a delegated task.
  • Evidence: backend task list/detail/company-task read models expose task_type_value; the default task list hides Done delegated tasks unless the request explicitly filters by status or Jira key, while explicit lookup and company task lists remain discoverable.
  • Evidence: /api/meta//api/jira/meta, .env.example, reply-pilot-be/README.md, and docs/backend.md document the delegated task issue type and custom-field configuration. Existing read-only Jira comments remain available through the existing Jira comments API for the UI component slice.
  • Validation: mvn -f reply-pilot-be/pom.xml -Dtest='JiraTaskSyncServiceTest,JdbcJiraTaskSyncRepositoryTest,JdbcReadModelRepositoryTest,JiraProxyControllerTest,HttpJiraIssueClientTest,HealthControllerTest' test passed with 23 tests; mvn -f reply-pilot-be/pom.xml verify passed with 207 tests; mkdocs build --strict and git diff --check passed.

  • [x] 27.1 Finalize delegated Jira task contract.

  • Evidence: user confirmed Jira issue type Reply Pilot Task with admin edit URL https://internet-handel.atlassian.net/secure/admin/EditIssueType!default.jspa?id=10206.
  • Evidence: user confirmed Jira taskType field customfield_10269 and options General, Meeting organization, and Registration (B2B).
  • Evidence: user confirmed company link appears in Jira description while Reply Pilot DB company id is workflow/authorization source of truth; requester is the Reply Pilot app user who created the task; resolve text is stored in Jira description; Jira comments remain read-only through the existing comment component; Done is hidden only from default task view.
  • Validation: mkdocs build --strict passed.

  • [x] 26.1 Finish Java backend runtime cutover validation.

  • Evidence: Docker Desktop was started locally and docker build -t reply-pilot-be:runtime-cutover-check reply-pilot-be successfully built the Java 21 Spring Boot runtime image from reply-pilot-be/Dockerfile.
  • Evidence: the previously running reply-pilot-be container was still the old Gunicorn/Python runtime, so Compose was rebuilt and force-recreated from the current Java Dockerfile with HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build -d --force-recreate in reply-pilot-be.
  • Evidence: the recreated container runs java -jar /app/reply-pilot-be.jar as non-root host user 501:20, Docker reports it healthy, Spring Boot logs show Java 21.0.11 on Tomcat port 5000, and curl -fsS http://127.0.0.1:9091/healthz returned {"status":"ok"}.
  • Validation: mvn -f reply-pilot-be/pom.xml clean verify passed with 169 Java tests; python3 -m pytest reply-pilot-app/tests passed with 213 tests; script/check-architecture.sh, mkdocs build --strict, and git diff --check passed.

  • [x] 26.2 Remove legacy Python backend implementation and test harness.

  • Evidence: removed the legacy backend package reply-pilot-be/reply_pilot_be, deleted reply-pilot-be/tests, and removed Python-only backend runtime leftovers: gunicorn.conf.py, requirements.txt, requirements-dev.txt, scripts/be-generate-gmail-token.py, and scripts/repair-contaminated-lead-import-party.py.
  • Evidence: backend documentation now records Java 21 Spring Boot/Maven/JUnit as the maintained backend implementation and validation surface. The app README points Gmail OAuth token generation to reply-pilot-gmail/scripts/gmail-generate-token.sh.
  • Evidence: reports/architecture-report.md now lists reply-pilot-be with no Python package, no Python production files, no Python test files, no Python script files, and no Python runtime/dev dependencies.
  • Validation: mvn -f reply-pilot-be/pom.xml clean verify passed with 169 Java tests; python3 -m pytest reply-pilot-app/tests passed with 213 tests; script/check-architecture.sh, mkdocs build --strict, and git diff --check passed. A working-tree scan for .py files under reply-pilot-be outside build/data/log paths returned no files.

  • [x] 25.10 Resolve Java lead-import outbound email transaction boundary.

  • Evidence: LeadImportService now sends the optional outbound Gmail message and passes the resulting CachedEmailItem to LeadImportRepository.markImported instead of importing activity in a separate repository transaction.
  • Evidence: JdbcLeadImportRepository.markImported links the sent recipient email contact to the imported party, calls ActivityEmailImportRepository.importEmails(Connection, ...), creates the lead-import activity note, marks the lead item imported, and refreshes batch progress in one caller-owned DB transaction.
  • Evidence: JdbcActivityEmailImportRepository now supports transactional email import on a supplied JDBC connection and rejects automatic reply-task side effects in that mode, because those task side effects are post-commit behavior for incoming supplier replies.
  • Evidence: direct JDBC tests cover the successful transaction, recipient contact linking, no non-transactional importer call, and rollback of the imported sent email marker/contact link/item/batch updates when the transactional importer fails.
  • Evidence: the touched outreach/contact upsert paths are JDBC-portable and savepoint-guarded so duplicate-key races do not leave a PostgreSQL transaction aborted before fallback update.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest=JdbcLeadImportRepositoryTest,LeadImportServiceTest,JdbcActivityEmailImportRepositoryTest test passed with 10 tests. Full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 169 tests. Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests; app validation python3 -m pytest reply-pilot-app/tests passed with 213 tests. Architecture and documentation validation script/check-architecture.sh and mkdocs build --strict passed. Whitespace validation git diff --check and a direct trailing-whitespace scan of Java/doc files passed.

  • [x] 25.9 Port concrete historical requirement backfill worker bean to Java.

  • Evidence: Java now provides JdbcRequirementBackfillWorker as the concrete RequirementBackfillWorker bean behind POST /api/requirements/backfill/run. It mirrors the Python party selection modes for party_id, after_party_id, and service-level party_ids fan-out, selects incoming email threads for active organization parties, and preserves the existing backfill result payload keys.
  • Evidence: the worker first synchronizes cached email thread artifacts through ActivityEmailImportRepository.syncEmailArtifacts; when cache lookup misses or cached artifact sync fails, it falls back to DB-derived thread messages and DB-derived attachments without inserting duplicate base email activity rows.
  • Evidence: Java AI classification and requirement aggregation workers now expose party-scoped processing methods for backfill. The scoped aggregation path claims only selected-party queue rows and intentionally skips the global ruleset-version full rebuild.
  • Evidence: direct JDBC tests cover paged historical backfill with one cached thread and one DB fallback thread, empty target selection, explicit DB fallback attachments in the activity importer, party-scoped AI candidate processing, and party-scoped aggregation queue claiming.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest=JdbcActivityEmailImportRepositoryTest,JdbcRequirementBackfillWorkerTest,JdbcRequirementAiClassificationWorkerTest,JdbcRequirementAggregationWorkerTest test passed with 18 tests. Full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 167 tests; Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests; app-side validation python3 -m pytest reply-pilot-app/tests passed with 213 tests. Architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict, git diff --check, and direct trailing-whitespace scan of touched Java/doc files passed.

  • [x] 25.8 Port concrete requirement monitoring/report worker bean to Java.

  • Evidence: Java now provides JdbcRequirementMonitoringWorker as the concrete RequirementMonitoringWorker bean behind GET /api/requirements/report. It loads requirement state counts, manual-review quality metrics, AI evidence metrics, and aggregation queue backlog directly from the backend DataSource.
  • Evidence: the worker mirrors the Python report payload shape, including state-count totals/rows, precision/recall/accuracy/value-match metrics, OpenAI configured/model fields, AI evidence counts, worker status payload, worker error totals, last AI status/detail/update time, and unavailable worker-status reasons.
  • Evidence: RequirementMonitoringSettings mirrors WORKER_STATUS_BASE_URL and WORKER_STATUS_TIMEOUT_SECONDS, while OpenAIClientSettings supplies the AI configured/model fields.
  • Evidence: direct JDBC tests cover DB-derived report metrics with a local /statusz HTTP server and the not-configured worker-status fallback.
  • Validation: full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 163 tests; Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests. Architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.7 Port concrete requirement aggregation worker bean to Java.

  • Evidence: Java now provides JdbcRequirementAggregationWorker as the concrete RequirementAggregationWorker bean behind POST /api/requirements/evaluate/run. It claims due party_requirement_eval_queue rows, uses PostgreSQL FOR UPDATE SKIP LOCKED where available, increments attempts, and keeps H2-compatible SQL for focused JDBC tests.
  • Evidence: the worker ports the Python aggregation rules for ENLISTMENT_TABLE, FEED, and SUPPLIER_IDENTIFIER, including source bonuses, confidence thresholds, competing candidate review states, final feed URL/delivery-mode projection, supplier identifier projection, fulfilled timestamp handling, and party_requirement_state upsert without overwriting manual overrides.
  • Evidence: per-item failures roll back to a savepoint, defer only the failed queue row, and continue the batch. RequirementAggregationSettings mirrors the existing APP_DATA_DIR, REQUIREMENT_EVAL_LOCK_TTL_SECONDS, REQUIREMENT_AGGREGATION_RULESET_VERSION, and REQUIREMENT_RULESET_STATE_FILE behavior, including one-time full-company requeue when the aggregation ruleset version changes.
  • Evidence: direct JDBC tests cover successful FEED aggregation, manual override preservation, unsupported requirement deferral with continuation, and ruleset-version rebuild queueing/state-file persistence.
  • Validation: full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 161 tests; Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests. Architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.6 Port concrete AI requirement classification worker bean to Java.

  • Evidence: Java now provides JdbcRequirementAiClassificationWorker as the concrete RequirementAiClassificationWorker bean. It scans deterministic candidates for ENLISTMENT_TABLE, FEED, and SUPPLIER_IDENTIFIER, skips candidates already materialized for the current REQUIREMENT_AI_PROMPT_VERSION, loads email/source context, calls the structured OpenAI JSON client with the committed schema payload, validates the fixed AI payload shape, replaces AI-created evidence/fact rows, and enqueues party_requirement_eval_queue work after successful writes.
  • Evidence: RequirementAiClassificationSettings mirrors the existing APP_DATA_DIR, REQUIREMENT_AI_SCHEMA_DIR, and REQUIREMENT_AI_PROMPT_VERSION configuration. HttpOpenAIJsonClient now supports input files for JSON-schema calls so attachment-backed classifications can upload cached files like the Python reference path.
  • Evidence: direct JDBC tests cover successful FEED classification with an attachment input file, SUPPLIER_IDENTIFIER fact materialization without writing requirement evidence rows, and invalid AI payload handling without partial DB writes.
  • Validation: full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 157 tests; Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests. Architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.5 Replace the historical CME matching worker with Java full sync.

  • Evidence: JdbcCmeCompanySyncWorker imports only dodavatel and osloveni_dodavatele into party_cme_source; the old queue, match read model, endpoint, worker job, and configuration were removed.
  • Validation: full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed with 153 tests; Python backend compatibility validation python3 -m pytest reply-pilot-be/tests passed with 227 tests. Architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.4 Port automatic incoming supplier reply task creation to Java.

  • Evidence: TaskMutationService now implements the domain EmailThreadReplyTaskAutomation port. For incoming messages without an existing thread task, it creates Email Thread Reply Jira issues, uses the Python source priority for default assignee resolution (party_organization, app_configuration, configured Jira account, configured assignee email, otherwise unassigned), transitions new issues to Drafting Reply, stores task_jira_email_thread_reply, and backfills the app task link into Jira.
  • Evidence: existing thread tasks found by task_jira_email_thread_reply.external_thread_id are transitioned to Drafting Reply through the same automation port instead of creating a duplicate task.
  • Evidence: JdbcActivityEmailImportRepository now collects automatic reply task requests only when a newly imported incoming external message did not update any linked reply task and resolved to exactly one supplier company. It runs the automation after the email-import transaction commits, so the Java task service can read company rows created by the import while failed Jira automation still does not roll back the email import.
  • Evidence: docs/backend.md now records the actual Python/Java behavior and removes the stale claim that automatic reply-task creation depends on a Supplier Onboarding task.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest='JdbcActivityEmailImportRepositoryTest,TaskMutationServiceTest,ArchitectureRulesTest' test passed; clean full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed; full Python backend validation python3 -m pytest reply-pilot-be/tests passed; architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.3 Port linked incoming-reply Jira task transition to Java activity importer.

  • Evidence: JdbcActivityEmailImportRepository now receives JiraIssueClient and, when a newly imported incoming external email belongs to an existing task_jira_email_thread_reply link, transitions each linked email_thread_reply Jira issue to Drafting Reply and syncs task_jira.status.
  • Evidence: the Java importer preserves the Python fallback where a failed transition still updates local status if a subsequent Jira issue load shows the issue is already in Drafting Reply.
  • Evidence: this item intentionally covers linked-task status automation only. Automatic creation of a new Email Thread Reply task for an unlinked incoming supplier reply remains in the unfinished audit.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest='JdbcActivityEmailImportRepositoryTest,TaskMutationServiceTest,ArchitectureRulesTest' test passed; clean full Java validation mvn -f reply-pilot-be/pom.xml clean verify passed; full Python backend validation python3 -m pytest reply-pilot-be/tests passed; architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.2 Port deterministic requirement/fact extraction to Java activity importer.

  • Evidence: JdbcActivityEmailImportRepository now mirrors the deterministic incoming-email extraction layer for non-internal senders. It replaces its own deterministic-email-parser rows in party_feed_fact and party_supplier_identifier_fact, replaces its own deterministic-enlistment-detector rows in party_requirement_evidence, and queues ENLISTMENT_TABLE, FEED, and SUPPLIER_IDENTIFIER aggregation work in party_requirement_eval_queue.
  • Evidence: the Java detector writes explicit feed URL facts, labeled IČO/DIČ/GLN/vendor-code identifier facts, and deterministic enlistment-table evidence from matching spreadsheet attachment filenames or body markers. While porting the DIČ detector, Java intentionally avoids the Python regex bug where \s can consume the next email line into the VAT value.
  • Evidence: H2 repository coverage verifies deterministic fact/evidence insertion, aggregation queueing, and idempotent replacement on reimport of the same Gmail message.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest='JdbcActivityEmailImportRepositoryTest,ArchitectureRulesTest' test passed; full Java validation mvn -f reply-pilot-be/pom.xml verify passed; full Python backend validation python3 -m pytest reply-pilot-be/tests passed; architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 25.1 Add production Java JDBC activity-email import repository.

  • Evidence: Java now provides JdbcActivityEmailImportRepository as the production Spring ActivityEmailImportRepository implementation. It transactionally imports cached email thread messages into activity, activity_email, activity_participant, contact mechanism and party identity rows, and synchronizes activity_email_attachment and activity_email_link rows from the backend email cache.
  • Evidence: the adapter supports idempotent message updates by provider = 'gmail' and external_message_id, replaces participants and artifacts on re-import, and implements syncEmailArtifacts for the mailbox attachment phase.
  • Evidence: this item intentionally covered base activity-email/artifact persistence only. Deterministic requirement/fact extraction is covered by later item 25.2; incoming-reply Jira automation from Python ActivityImportStore remains in the unfinished audit.
  • Validation: focused Java validation mvn -f reply-pilot-be/pom.xml -Dtest='JdbcActivityEmailImportRepositoryTest,ArchitectureRulesTest' test passed; full Java validation mvn -f reply-pilot-be/pom.xml verify passed; full Python backend validation python3 -m pytest reply-pilot-be/tests passed; architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 24.21 Final Java endpoint parity audit.

  • Evidence: Java now includes the previously missing POST /api/emails/{emailId}/debug/generate route through DebugReplyController; it preserves instruction validation, missing email handling, selected context normalization, attachment file inclusion, OpenAI text generation, response envelope, and reply_subject behavior.
  • Evidence: reply-pilot-be/tests/test_java_route_parity.py parses Flask api_bp decorators and Java Spring controller mappings and verifies all 112 current Flask backend JSON/health routes are represented by Java controllers. The test expands Java's email-thread catch-all dispatch route and records the requirement-review path-shape equivalence for current single-segment requirement codes.
  • Evidence: docs/backend.md now records the Java endpoint parity audit, family-level route inventory, route-shape notes, and remaining non-endpoint migration caveats. No endpoint-level deferrals remain in the audit.
  • Validation: focused Java tests mvn -f reply-pilot-be/pom.xml -Dtest='DebugReplyControllerTest,AiDraftControllerTest,HttpOpenAIJsonClientTest' test passed; focused route/compatibility validation python3 -m pytest reply-pilot-be/tests/test_java_route_parity.py reply-pilot-be/tests/test_api_compatibility.py -q passed; full Java validation mvn -f reply-pilot-be/pom.xml verify passed; full Python backend validation python3 -m pytest reply-pilot-be/tests passed; architecture validation python3 script/architecture-report.py --output reports/architecture-report.md and script/check-architecture.sh passed; documentation and whitespace validation mkdocs build --strict and git diff --check passed.

  • [x] 24.20 Port worker job endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/cme/company-sync/run, POST /api/requirements/ai-classify/run, POST /api/requirements/evaluate/run, POST /api/requirements/backfill/run, and GET /api/requirements/report through WorkerJobController, WorkerJobService, and typed domain worker ports.
  • Evidence: the Java endpoint behavior preserves compatible limit parsing, party_id/after_party_id/party_ids parsing, multi-party backfill result merging, success envelopes, not-configured 503 responses, expected worker 400 responses, and unexpected worker 500 error prefixes.
  • Evidence: CME sync, requirement AI, requirement aggregation, or historical backfill worker bean is claimed by this item. Without a configured port bean, the new endpoints intentionally return the same not-configured errors as the Flask reference.
  • Validation: focused Java tests mvn -f reply-pilot-be/pom.xml -Dtest='WorkerJobControllerTest' test passed; full Java validation mvn -f reply-pilot-be/pom.xml verify passed; Flask compatibility validation python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q passed; docs validation mkdocs build --strict and git diff --check passed.

  • [x] 24.19 Port lead import endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/lead-imports/files, POST /api/lead-imports/batches, GET /api/lead-imports/batches/{batchId}, GET /api/lead-imports/batches/{batchId}/items/{itemId}, POST /api/lead-imports/batches/{batchId}/items/{itemId}/actions, and POST /api/lead-imports/batches/{batchId}/items/{itemId}/draft.
  • Evidence: the Java lead import service handles server-side JSONL inbox to processing/done/failed file movement, batch/item persistence, pending-item draft generation through OPENAI_MODEL_LEAD_IMPORT, prompt id/snapshot storage, item errors, skip/do-not-contact/import actions, party/contact upsert, outreach policy, activity notes, Jira Supplier Onboarding task creation with lead-import labels, optional outbound Gmail send, and compatible validation/error envelopes.
  • Evidence: outbound email activity recording uses the existing Java ActivityEmailImportRepository hook when configured; it is not a single DB transaction with lead item update, and the production JDBC activity importer remains tracked in the unfinished audit.
  • Validation: focused Java tests mvn -f reply-pilot-be/pom.xml test -Dtest=LeadImportControllerTest,LeadImportServiceTest passed; full Java validation mvn -f reply-pilot-be/pom.xml verify passed; Flask compatibility validation python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q passed.

  • [x] 24.18 Port email draft/send endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/emails/drafts/outbound, POST /api/emails/{emailId}/drafts/reply, POST /api/emails/send/outbound, and POST /api/emails/{emailId}/send/reply with compatible JSON payloads, multipart reply payloads, validation messages, missing-email handling, response envelopes, and attachment count/size limits.
  • Evidence: the Gmail HTTP adapter now forwards outbound/reply draft and send calls to reply-pilot-gmail, resolves reply_to_message_id from thread history, and emits multipart form-data for reply attachments.
  • Evidence: successful reply sends update the local email cache immediately; sent replies and outbound sends call the pluggable ActivityEmailImportRepository when present. This slice does not add the production JDBC activity-email importer, so DB contact-history persistence remains tracked in the unfinished audit.
  • Evidence: compatibility harness now covers email draft/send endpoint contracts, including multipart attachment limit behavior, against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='EmailSendControllerTest,HttpGmailServiceEmailSyncClientTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 24.17 Port mailbox watch/import endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/mailbox/watch, POST /api/mailbox/watch/ensure, POST /api/mailbox/watch/stop, POST /api/mailbox/watch/notify, POST /api/mailbox/watch/pull, POST /api/mailbox/sync/full, POST /api/mailbox/import, GET /api/mailbox/import/status, and POST /api/mailbox/import/step with compatible envelopes, status codes, notification history comparison, full-sync alias behavior, and rate-limit JSON fields.
  • Evidence: the Gmail HTTP adapter now forwards watch status/ensure/stop, notification record/pull/ack calls, maps 429 retry metadata, and preserves the existing email-cache sync bridge.
  • Evidence: mailbox import status/state is persisted through mailbox-import-status.json, mailbox-import-state.json, and a process lock; import orchestration is pluggable through ActivityEmailImportRepository. This slice does not add the production JDBC activity-email importer, so database artifact import remains an explicit unfinished audit item instead of a hidden success.
  • Evidence: compatibility harness now covers mailbox watch/import endpoint contracts against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='MailboxControllerTest,HttpGmailServiceEmailSyncClientTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 24.16 Port inbox, email, and attachment read endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/inbox, GET /api/emails, GET /api/emails/{emailId}, GET /api/emails/{emailId}/attachments, and GET /api/emails/{emailId}/attachments/{filename} with compatible inbox pagination, email list filtering, detail payloads, attachment metadata, binary attachment serving, JSON error envelopes, and file-cache path safety.
  • Evidence: Java now reads the existing email cache layout under data/emails, preserves legacy emails.json, computes attachment content hashes, preserves remote page cursors during single-thread refreshes, and adds a Gmail-service HTTP sync adapter for read-side detail fetch, snapshot page fetch, and attachment refresh behavior.
  • Evidence: compatibility harness now covers inbox/list/detail/attachment metadata/binary download behavior plus missing email and missing attachment errors against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='EmailReadControllerTest,HttpGmailServiceEmailSyncClientTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 24.15 Port AI draft generation endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/supplier-onboarding/draft and POST /api/email-thread-task/draft with compatible success envelopes, OpenAI JSON schema requests, supplier lead-import model selection, validation messages, empty-AI-response handling, and OpenAI client error mapping.
  • Evidence: the Java OpenAI JSON client now supports /responses JSON schema calls with strict text.format, parsed-output extraction, output-text JSON fallback, model/response-id propagation, and missing-key/upstream error handling.
  • Evidence: compatibility harness now covers supplier onboarding and email-thread task draft success payloads plus missing company/thread context validation against the current Flask reference backend.
  • Validation: full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 24.14 Port Jira proxy and task-sync endpoints to Java.

  • Evidence: Java Spring Boot now implements Jira proxy endpoints for GET /api/jira/myself, GET /api/jira/users/by-email, GET /api/jira/issues/{issueKey}, PUT /api/jira/issues/{issueKey}, GET /api/jira/issues/{issueKey}/transitions, POST /api/jira/issues/{issueKey}/transition, GET /api/jira/issues/{issueKey}/comments, POST /api/jira/issues/{issueKey}/comments, POST /api/jira/issues/{issueKey}/unassign, GET /api/jira/issues/search, GET /api/jira/issues/assigned, GET /api/jira/issues/stale, POST /api/jira/issues, and POST /api/jira/task-sync/run with compatible request parsing, response envelopes, Jira upstream error mapping, validation messages, pagination token handling, and environment-label behavior.
  • Evidence: Java task-sync service now builds full/incremental Jira JQL, honors safety overlap, maps Jira assignees to app users, writes Jira task cache fields and sync state through JDBC, and records skipped/failed runs.
  • Evidence: compatibility harness now covers Jira user lookup, issue detail, transitions, comments, search/assigned/stale lists, create/update, transition/unassign, task-sync run, and validation behavior against the current Flask reference backend.
  • Validation: full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 24.13 Port task and email-thread reply mutation endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/companies/{partyId}/supplier-onboarding-tasks, PATCH /api/tasks/{taskId}/company, POST /api/tasks/{taskId}/email-threads, PATCH /api/tasks/{taskId}/supplier-onboarding, PATCH /api/tasks/{taskId}/email-thread-reply, POST /api/tasks/{taskId}/refresh-from-jira, POST /api/tasks/{taskId}/supplier-onboarding/reply, POST /api/tasks/{taskId}/move-reply-to-waiting, and POST /api/email-threads/{emailId}/reply-tasks with compatible response envelopes, duplicate reply-task conflicts, validation/error mapping, Jira issue/user/transition/comment calls, app task links, assignee profile sync, and JDBC writes to task, task_jira, task_jira_supplier_onboarding, and task_jira_email_thread_reply.
  • Evidence: compatibility harness now covers supplier-onboarding task creation, task company/thread links, supplier-onboarding updates/replies, email-thread reply updates, Jira refresh, move-to-waiting, manual email-thread reply task creation, and duplicate-thread conflict behavior against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='TaskMutationServiceTest,TaskMutationControllerTest,JdbcTaskMutationRepositoryTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q.

  • [x] 01. Lock intended boundary.

  • Evidence: docs/module-boundaries.md documents target boundary, allowed transitional calls, and app DB/Gmail/OpenAI debt.

  • [x] 02. Inventory AI prompt usage.

  • Evidence: current prompt routes, workflows, templates, prompt types, payload fields, tests, and backend endpoint needs were captured before implementation.

  • [x] 03. Add backend AI prompt API.

  • Evidence: backend prompt service/workflow/routes and API tests were added.

  • [x] 04. Switch app AI prompt UI to backend.

  • Evidence: app prompt CRUD uses backend-backed prompt store when BACKEND_API_BASE_URL is configured.

  • [x] 05. Migrate reply draft storage.

  • Evidence: backend reply-draft API exists and app uses HttpReplyDraftStore in standard runtime.

  • [x] 06. Migrate user profile storage.

  • Evidence: user profile reads/writes go through backend app-state endpoints via HttpUserProfileStore.

  • [x] 07. Migrate app configuration and Jira assignee mapping.

  • Evidence: app configuration and Jira assignable-user/profile mapping go through backend app-state endpoints via HTTP adapters.

  • [x] 08. Inventory read models before moving them.

  • Evidence: route/workflow reads, template fields, backend endpoint gaps, and safe migration order were captured before implementation.

  • [x] 09. Move company and person read pages.

  • Evidence: company/person list and detail read paths are backend-backed in standard runtime.

  • [x] 10. Move activity and email detail read pages.

  • Evidence: activity/email detail and related company-by-thread reads are backend-backed in standard runtime.

  • [x] 11. Move task read pages.

  • Evidence: task list/detail and related task lookup reads are backend-backed in standard runtime.

  • [x] 12. Inventory mutations.

  • Evidence: mutation inventory classified existing backend-backed flows, remaining task-store debt, backend endpoint gaps, and safe migration order.

  • [x] 13. Move company/person/contact mutations.

  • Evidence: company update, company merge, visibility, and requirement review use record_mutation_client.py and backend /api/companies/... mutation endpoints. Person/contact/identifier/link mutations use backend clients.

  • [x] 14. Move task and Jira mutations.

  • Evidence: supplier onboarding task creation/editing, task company selection, email-thread reply task creation/editing, email-thread linking, supplier onboarding replies, Jira refresh, and reply-task status movement delegate to backend email/task mutation APIs.

  • [x] 15. Remove app email/Gmail/OpenAI fallback production code.

  • Evidence: reply-pilot-app no longer contains gmail_store.py or openai_client.py; app factory requires BACKEND_API_BASE_URL outside tests, builds backend-backed draft/debug clients in standard runtime, and no longer wires an openai_client extension. Reply debug generation delegates to backend email service APIs. Test-only local email cache behavior remains isolated to generic cache-service fixtures.
  • Validation: python3 -m pytest reply-pilot-app/tests passed; python3 -m pytest reply-pilot-be/tests passed; script/check-architecture.sh passed after regenerating reports/architecture-report.md; mkdocs build --strict passed.

  • [x] 16. Move simple-auth nonce persistence.

  • Evidence: app owns browser auth/session flow, but used nonce persistence is backend-backed through POST /api/simple-auth/nonces/check and POST /api/simple-auth/nonces/mark-used.

  • [x] 17. Move search behind backend.

  • Evidence: backend exposes app-facing GET /api/search and delegates to reply-pilot-search through a backend search client. App search client now uses BACKEND_API_BASE_URL; app runtime config, Compose, and scripts no longer expose SEARCH_API_BASE_URL or SEARCH_TIMEOUT_SECONDS. Boundary docs and PlantUML diagrams now show reply-pilot-app -> reply-pilot-be -> reply-pilot-search.
  • Validation: focused backend proxy tests python3 -m pytest reply-pilot-be/tests/test_api.py -q -k "api_search" passed; focused app search/create-app tests python3 -m pytest reply-pilot-app/tests/test_app.py -q -k "search_page or create_app_uses_backend_email_thread_task_draft_client_when_backend_configured" passed. Full validation also passed: python3 -m pytest reply-pilot-app/tests; python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh; mkdocs build --strict.

  • [x] 18. Remove remaining app DB/runtime dependency debt.

  • Evidence: app directory_store, record_store, and task_store no longer contain PostgreSQL-backed production store classes or psycopg imports. Standard app runtime requires backend HTTP read/task APIs; task_store wiring is test-only. reply-pilot-app no longer declares psycopg, exposes REPLY_PILOT_DB_* runtime config, or documents app-to-DB runtime access.
  • Validation: python3 -m pytest reply-pilot-app/tests -q passed; python3 -m pytest reply-pilot-be/tests -q passed; python3 script/architecture-report.py --output reports/architecture-report.md regenerated the baseline; script/check-architecture.sh passed; mkdocs build --strict passed.

  • [x] 19.1-19.7 Close Python backend route-map cleanup as obsolete.

  • Evidence: on 2026-06-12, project direction changed: further Python Flask backend route-map refactoring is no longer needed because Java backend endpoint parity is the active migration path and Python backend code will be removed later. This closes slices 19.1-19.7 for lead import, inbox/email/attachment, mailbox, CME/requirement worker, email draft/send, debug reply generation, and final Flask route-map audit.
  • Evidence: this is not an implementation-complete claim for the Flask route cleanup. Remaining Python views.py implementation logic is intentionally left in place until later Python backend removal.
  • Validation: tracker/context-only change; mkdocs build --strict and git diff --check passed.

  • [x] 20. Align refactoring ledger with the refactoring-plan skill.

  • Evidence: active ledger moved to docs/refactoring.md; root refactoring.md is only a compatibility pointer; active tasks use checkbox status, stable IDs, concise prompt-free entries, unfinished audit, maintenance items, and done archive.
  • Validation: mkdocs build --strict, script/check-architecture.sh, and git diff --check -- refactoring.md docs/refactoring.md mkdocs.yml passed.

  • [x] 21. Apply updated prompt-free refactoring-plan ledger rules and re-audit item statuses.

  • Evidence: open items 15, 17, 18, and 19 were re-audited against current code and documentation; full prompt blocks were removed from active ledger entries; current statuses remain open as documented.
  • Validation: mkdocs build --strict, script/check-architecture.sh, and git diff --check -- docs/refactoring.md refactoring.md mkdocs.yml passed.

  • [x] 22. Split refactoring plan into context and tracker documents.

  • Evidence: docs/refactoring-context.md now owns target boundary, source-of-truth docs, guardrails, and validation gates; docs/refactoring-tracker.md now owns open items, maintenance items, unfinished audit, and done archive. Legacy refactoring files are pointers.
  • Validation: mkdocs build --strict passed; script/check-architecture.sh passed; diff whitespace check for the refactoring docs and mkdocs.yml passed.

  • [x] 24.1 Port AI prompt endpoints to Java.

  • Evidence: Java Spring Boot implements GET /api/ai-prompt-types, GET /api/ai-prompts, GET /api/ai-prompts/{promptId}, POST /api/ai-prompts, PATCH /api/ai-prompts/{promptId}, and DELETE /api/ai-prompts/{promptId} with DTOs, service validation, not-found handling, and JDBC persistence against the existing ai_prompt_type and ai_prompt tables.
  • Evidence: Python compatibility harness now covers AI prompt JSON payloads, status codes, validation messages, legacy prompt_text input, filtering, update, delete, and missing-resource behavior against the Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='AiPromptControllerTest,AiPromptServiceTest,JdbcAiPromptRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; architecture report was regenerated and script/check-architecture.sh passed.

  • [x] 24.2 Restore Java migration tracker before the next endpoint slice.

  • Evidence: active tracker now lists remaining Java Spring Boot endpoint migration slices 24.3-24.21 based on docs/backend.md, Flask route decorators in reply-pilot-be/reply_pilot_be/views.py, current compatibility harness coverage, and existing Java code under reply-pilot-be/src.
  • Evidence: the unfinished audit now states the actual migration state: /healthz and AI prompt endpoints are implemented in Java; all other endpoint families require focused ports with compatibility contracts.
  • Validation: mkdocs build --strict passed; diff whitespace check for docs/refactoring-context.md and docs/refactoring-tracker.md passed.

  • [x] 24.3 Port backend status and metadata endpoints to Java.

  • Evidence: Java Spring Boot now implements /api/meta and /api/jira/meta from BackendMetadataSettings with compatible status, email, OpenAI, Jira, CME, lead-import, and email_import_status: null fields. /healthz remains compatible through the same controller.
  • Evidence: compatibility harness now checks /api/jira/meta against the current Flask reference, and Java tests cover default metadata plus configured Jira/OpenAI/email-import metadata.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='HealthControllerTest,BackendMetadataSettingsTest,BackendRuntimeSettingsTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.4 Port reply draft storage endpoints to Java.

  • Evidence: Java Spring Boot now implements GET/POST /api/reply-drafts and GET/PATCH/DELETE /api/reply-drafts/{draftId} with service validation, not-found handling, HTTP status mapping, and JDBC persistence against public.email_replay_draft.
  • Evidence: compatibility harness now covers reply draft create, list, detail, update, validation errors, missing update/delete, delete, and missing detail against the current Flask reference.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='ReplyDraftControllerTest,ReplyDraftServiceTest,JdbcReplyDraftRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.5 Port app-state and Jira assignee storage endpoints to Java.

  • Evidence: Java Spring Boot now implements GET/POST /api/user-profiles, GET/PATCH /api/user-profiles/{loginKey}, GET/PATCH /api/app-configuration, GET /api/jira/assignees, GET /api/jira/assignees/{appUserId}/account-id, and PUT /api/jira/assignees/{appUserId} with compatible JSON payloads, validation messages, not-found handling, DB-connectivity error mapping, and JDBC persistence against public.app_user, public.app_configuration, and public.app_user_jira_profile.
  • Evidence: compatibility harness now covers user profile create/list/detail and Gmal box link update, app configuration load/update, Jira assignable list, Jira account-id lookup, Jira profile upsert, missing profile, and app configuration validation against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='AppStateControllerTest,AppStateServiceTest,JdbcAppStateRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.6 Port simple-auth nonce endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/simple-auth/nonces/check and POST /api/simple-auth/nonces/mark-used with compatible used/marked JSON payloads, validation messages, DB-connectivity error mapping, duplicate-mark handling, and expiry purge semantics against public.simple_auth_nonce.
  • Evidence: compatibility harness now covers initial check, mark-used, duplicate mark, expiry boundary behavior, missing nonce, invalid now_ts, missing expires_at, and non-positive expires_at against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='SimpleAuthNonceControllerTest,SimpleAuthNonceServiceTest,JdbcSimpleAuthNonceRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.7 Port backend search proxy endpoint to Java.

  • Evidence: Java Spring Boot now implements GET /api/search, normalizes q, type, and limit, caps limit at 50, forwards requests to reply-pilot-search through HttpSearchClient using SEARCH_API_BASE_URL and SEARCH_TIMEOUT_SECONDS, preserves successful search JSON payloads, and maps search-client failures to HTTP 503 error payloads.
  • Evidence: compatibility harness now covers search success and search-client failure against the current Flask reference backend with a fake search client.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='SearchControllerTest,SearchServiceTest,HttpSearchClientTest,SearchClientSettingsTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.8 Port company and person read-model endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/companies, GET /api/companies/{partyId}, GET /api/companies/{partyId}/tasks, GET /api/people, GET /api/people/{partyId}, and GET /api/parties/{partyId}/activities with compatible pagination, filtering, nested company/person detail payloads, not-found messages, company tasks, party activity lists, and JDBC persistence against the existing party/activity/task tables.
  • Evidence: compatibility harness now covers company/person list and detail, company tasks, party activities, and missing company/person responses against the current Flask reference backend with in-memory read-model stores.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='ReadModelControllerTest,ReadModelServiceTest,JdbcReadModelRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed.

  • [x] 24.9 Port activity, email-thread, and contact read-model endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/activity-emails, GET /api/activity-emails/{activityId}, GET /api/contact-methods/{contactMechId}, GET /api/activities/{activityId}, GET /api/email-threads/{externalThreadId}/companies, GET /api/email-threads/{externalThreadId}/requirements, GET /api/contact-emails/companies, and POST /api/company-mentions with compatible JSON payloads, not-found messages, contact-email parsing, slash-capable email-thread id routing, company mention matching, and JDBC persistence against the existing contact/activity/requirement tables.
  • Evidence: compatibility harness now covers activity email list/detail, contact detail, activity detail, email-thread company and requirement lookups, contact-email company lookup, company mentions, and missing contact/email/activity responses against the current Flask reference backend with in-memory read-model stores.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='ReadModelControllerTest,ReadModelServiceTest,JdbcReadModelRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed; mkdocs build --strict passed.

  • [x] 24.10 Port task read-model endpoints to Java.

  • Evidence: Java Spring Boot now implements GET /api/tasks, GET /api/tasks/{taskId}, GET /api/email-threads/{externalThreadId}/task, and GET /api/email-threads/tasks with compatible pagination, status-code filter normalization including the legacy Drafting Replay typo, assigned-user filtering through Jira account profile, task detail email_threads, optional company filter for email-thread task lookup, sorted thread-id-with-task responses, and missing task errors.
  • Evidence: compatibility harness now covers task list, task detail, email-thread task lookup with and without company filters, unmatched thread task lookup, thread-id batch lookup, and missing task responses against the current Flask reference backend with in-memory read-model stores.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='ReadModelControllerTest,ReadModelServiceTest,JdbcReadModelRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed; mkdocs build --strict passed.

  • [x] 24.11 Port company mutation endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/companies, PATCH /api/companies/{partyId}, POST /api/companies/{targetPartyId}/merge, POST /api/companies/{partyId}/visibility, and POST /api/companies/{partyId}/requirements/{requirementCode}/review with compatible response envelopes, validation messages, duplicate conflict payloads, not-found handling, DB-connectivity error mapping, and JDBC transaction behavior for company create/update/merge/visibility plus manual requirement review facts/state updates.
  • Evidence: compatibility harness now covers company create success, validation errors, duplicate conflicts, update, missing update, visibility, requirement review, invalid review, merge validation, and merge success against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='CompanyMutationControllerTest,CompanyMutationServiceTest,JdbcCompanyMutationRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed; mkdocs build --strict passed.

  • [x] 24.12 Port person/contact/identifier mutation endpoints to Java.

  • Evidence: Java Spring Boot now implements POST /api/people, PATCH /api/people/{partyId}, POST /api/parties/{partyId}/contacts, PATCH /api/parties/{partyId}/contacts/{contactMechId}, DELETE /api/parties/{partyId}/contacts/{contactMechId}, POST /api/companies/{partyId}/identifiers, DELETE /api/companies/{partyId}/identifiers/{identifierId}, and DELETE /api/companies/{companyId}/people/{personId} with compatible response envelopes, validation messages, duplicate/conflict candidate payloads, not-found handling, DB-connectivity error mapping, JDBC transactions, contact soft unlink, identifier delete, and company-person CONTACT_FOR unlink behavior.
  • Evidence: compatibility harness now covers person create/update, validation errors, duplicate person email conflicts, party contact add/update/delete and duplicate-contact conflicts, company identifier add/delete and duplicate conflicts, and company-person unlink success and missing-link errors against the current Flask reference backend.
  • Validation: focused Java tests passed: mvn -f reply-pilot-be/pom.xml -Dtest='PartyMutationServiceTest,PartyMutationControllerTest,JdbcPartyMutationRepositoryTest,ArchitectureRulesTest' test; full Java validation passed: mvn -f reply-pilot-be/pom.xml verify; focused compatibility passed: python3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -q; full backend Python validation passed: python3 -m pytest reply-pilot-be/tests; script/check-architecture.sh passed; mkdocs build --strict passed.

Latest broad validation recorded before this tracker split:

  • python3 -m pytest reply-pilot-app/tests passed.
  • python3 -m pytest reply-pilot-be/tests passed.
  • script/check-architecture.sh passed.