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 changeset0054. Blocked: decide whether its 25 historicalactivity_email_thread_company_eventrows 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.mdonly when implemented code changes alter the architecture baseline, then runscript/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
0054dropsactivity_email_thread_company_event, which currently retains 25 historicallinkedaudit 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
0069has removedparty_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. Changeset0068previously deleted the measured 318 legacyparty_identifier.TAX_IDENTIFIERrows; 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_threadrows and 542party_person_name_legacyaudit rows;activity_email_linkis actively written, whileactivity_callandactivity_meetingremain active read-model inputs despite being empty.pg_stat_statementsis 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_TOKENmust 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-bestill 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
@Componentjobs 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.
--oncecreates 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,
--onceand SIGTERM (0.05 s, exit 143). Architecture check, strict MkDocs build andgit diff --checkpassed; 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
ScheduledExecutorServicepreserves 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
--onceand--healthcheck, atomically writes the heartbeat, and shuts down on SIGTERM. Docker keeps non-rootHOST_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,
--onceand 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.shproduces 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 withbaseline_unavailable; both legacy availability flags are false because this local database had already applied contract changeset0054.- 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
0054as 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 --strictpassed. -
[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.reviewindependently gates per-thread review. Validation is inline withnovalidate,is-invalid,aria-invalid, andaria-describedby; focused permission/client/UI tests passed. -
[x] 35.4 Add the unassigned-address read model and per-thread review state.
- Changeset
0072adds the provider/thread review table and backend APIs list unresolved addresses grouped with thread count, last occurrence, and thread links. Changeset0073addsemail.inbox.reviewtoadminandsales_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
0071adds 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
0070reversibly snapshots and removes only the known invalidatlas.cz,googlemail.com, andvolny.czdomain 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.gzis 18,069,571 bytes, mode0600, and has SHA-256c270e205d0ad561c7302f9345c5bc2eb2297176f7be6a79d6ff93ddafedd1a42. 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; targeted0069update/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_IDENTIFIERrows, zero registrations on merged parties, zero active scalar queries, and0069still unapplied. The standalone changelog exposed exactly one pending changeset, so only0069ran; the root contract status left unrelated0053and0054pending. Parties, organizations, and VAT registrations remained4929 / 3895 / 1068, the scalar column is absent, and0069has one changelog row. - Production company
3096list/detail reads and a data-preserving update retain IČO23921382and VATCZ23921382with 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-recorderror_count=227remains visible. Requirement AI/evaluation and Gmail jobs also completedok. 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 JiraUnauthorizedjobs 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
0069rejects 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
0069unapplied. 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_identifiernames are CME/source snapshots or wholesale source-input fields;party_cme_source.tax_identifierremains 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 isagent:agent, encrypted production envs decrypted successfully, and all local plaintext.env.serverfiles were removed after use. - During the backend-first compatibility window the old app stayed healthy.
Company
3096returned IČO23921382, 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; apptax_identifierfields 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
3096by VAT, all nine Polish registrations byNumer 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 company3096IČ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_IDENTIFIERrows, and zero multi-registration parties. A scheduled post-deploy CME run completedok, synchronized 2,656 links, and created zero companies; its existingerror_count=227remains unexplained but has no observed VAT-cutover failure. The independently known JiraUnauthorizedjobs 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_statementsis 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_identifierfields are source snapshots. Full suites pass with 339 app, 19 search, and 12 wholesale tests. Architecture, strict MkDocs, andgit diff --checkpass. -
[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_registrationas 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 --checkpass. -
[x] 34.6 Deploy the legacy identifier cutoff and execute its production data cleanup.
- The standard DB deploy staged
0068but 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
3096showed IČO23921382and VATCZ23921382; Solr and backend search returned it for the current IČO and returned zero results for stale legacy valueCZ7207283666. The wholesale query returned 3,361 active organizations and sourced both scalar and registration VAT for company3096asCZ23921382. - Fresh backup
reply-pilot-20260821T105254Z.sql.gzis 18,061,390 bytes with SHA-2561dcc47ed17a68fdcb31eba0b606c3a7d0052b985b63bc9179979b2f9a40347af. 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 unapplied0068, 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 only0068; its update applied exactly one changeset. The audit records318 -> 0and snapshots all 318 rows. Scalars remain 1,138, registrations remain 1,067, the scalar column remains present, and0053/0054remain unapplied. - An authenticated no-op edit of company
3096and subsequent scheduled CME runs, including one that linked 2,656 sources and created no companies, did not recreate a legacy row. That run reportederror_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 JiraUnauthorizedworker failures continue, but no VAT/identifier SQL or application failure was found. -
[x] 34.5 Remove authoritative VAT use of
party_identifier.TAX_IDENTIFIERand 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 reportsdic:vat_registration. No wholesale code or query references the legacy identifier type. - Contract changeset
0068records one audit row with an exact JSONB snapshot and pre/post counts before deleting legacy rows. Standard!contractupdate skipped it. Targeted local update archived 4 rows and left 0, rollback restored all 4 with checksum9675613e1a5d6f50d69d46f5f6ca43a2, and the final targeted update again recorded4 -> 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_sourceandparty_supplier_identifier_factwere 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 --checkpass. -
[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, and0067while filtering all contract changesets, then the compatible backend was deployed and passed its Docker healthcheck. The VAT catalog contains 28 rows, includingEL -> GR,XI -> GB, and CzechDIČ; all target PK/FK/check/unique constraints are present and the normalized-value index is non-unique. - Fresh backup
reply-pilot-20260821T100732Z.sql.gzis 18,036,706 bytes with SHA-2565ccc599553ec98b56f44b70ab5e07d2a307adb3ade1c3d031bd16709e20fc4fa. Its checksum passed and it restored into an isolated database with 67 public tables, latest changeset0064, 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, and7269. The old broad duplicate count included the two invalidSkupinove_DPHplaceholders, 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
3096passed an authenticated no-op edit/read-back with HTTP 200 while preserving IČO23921382, scalar VATCZ23921382, 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 uniqueerror was caused by an acceptance query typo and its corrected query passed. The brief worker connection failures during container restart and the pre-existing JiraUnauthorizedfailures are unrelated and remain documented separately. -
[x] 34.3 Build the deterministic scalar-only VAT inventory and backfill operation.
vat-registration-backfilldefaults to a read-only repeatable-read dry-run;--applyfirst 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 -nfor the operator script, andgit diff --checkpass. -
[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_IDENTIFIERis 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, andgit diff --checkpass. -
[x] 34.1 Add the expand-only VAT registration model from RP-4122.
- Changeset
0066creates and seeds all 28 supported VIES jurisdictions and createsparty_vat_registrationwith the approved PK/FKs, checks, timestamps, one-row-per-party/jurisdiction constraint, and deliberately non-unique normalized-value index. Catalog checks confirmEL -> GR,XI -> GB, andCZ.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-jurisdictionsand additivevat_registrations[]fields are available on company list/detail payloads. The scalartax_identifierremains 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 the0066rollback despite concurrent changeset0067; 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, andgit diff --checkpass. -
[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!contractLiquibase update applied no destructive changeset. - Recovery evidence: created
reply-pilot-20260821T081752Z.sql.gz(18,034,429 bytes, SHA-256458386dda2b54322b51871a42be93446f6c1e0664e471621943af1b29eff4db4), verified its checksum and gzip stream, restored it into an isolated database, checked representative counts (party4,926,party_contact_mech4,587,activity_email_attachment6,325, andtask_jira_organize_meeting129), and removed the verification database. - Repeated all embedded production data assertions with zero violations, then
applied exactly changesets
0060through0064. Changesets0053and0054remain 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
0060through0064:party_person.middle_name, bothparty_relationshiprole columns,party_contact_mech.from_date, the surrogateparty_contact_mech_purpose.id,party_requirement_state.requested_at,activity_email_attachment.storage_backend_code,ai_prompt.sort_order, and the duplicatetask_jira_organize_meetingtimestamps. 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_datevalues equalcreated_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_atdistinct 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 --checkpassed. -
[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
0059changelog ran exactly one changeset. The broader@contractcommand was not used because unrelated contract migrations0053and0054are still pending; both remained unapplied. - Evidence: the verified pre-contract backup is
reply-pilot-20260820T131432Z.sql.gz(18,026,423 bytes, SHA-256dafdf792b6c478baff45bee4df7b46dc5dbc81212897c87b32a67779cf60c206); backup and checksum are owned byagent:agent, mode0600. - 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 --checkpassed. -
[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
0058executed in production after the scheduled pre-deploy backupreply-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
0058from 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_identifierrows while the publiccompany_registration_numberproperty remains stable. Quarantine rows are excluded from ownership, matching, search, and normal IČO display. - Evidence: expand changeset
0058safely backfills valid unambiguous legacy IČO, preserves all unresolved raw values underLEGACY_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 changeset0059; 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, andgit diff --checkpassed. -
[x] 32.1 Lock the canonical company-identifier target and migration invariants.
- Evidence:
party_identifieris the final identity store while the publiccompany_registration_numberfield remains stable during compatibility;value_rawpreserves entered/source text andvalue_normalizedis the comparison and uniqueness value. - Evidence:
COMPANY_REGISTRATION_NUMBERis reserved for Czech IČO normalized to eight ASCII digits. Verified foreign commercial-register entries useCOMMERCIAL_REGISTER_NUMBERwith country, issuing register, section, and number encoded invalue_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/rundefaulted to a read-only dry-run and accepted apply only as the explicit JSON booleantrue. 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 invalue_raw, and recomputed after-counts in the same transaction. It did not update unresolved rows or useON CONFLICT DO NOTHING; an unexpected unique conflict rolled back all inserts. - Boundary: no schema or production data changed. The operation deliberately
reused
COMPANY_REGISTRATION_NUMBERbefore 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, andgit diff --checkpassed. -
[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_NUMBERtarget withoutON 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_jsonunchanged, and are stored inlast_errorby the existing service flow. CME preserves the raw source value inparty_cme_source, recordslast_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, andmkdocs build --strictpassed. -
[x] 32.2 Establish one backend IČO validation and ownership operation while preserving current UI/API behavior.
- Evidence:
CompanyRegistrationNumberPolicyis 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_NUMBERtarget identifier and maintains the temporaryparty_organization.company_registration_numbervalue. A plain unique insert detects concurrent ownership races; authoritative IČO writes do not useON 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_numbersurvives 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 plusscript/check-architecture.sh, andmkdocs build --strictpassed. -
[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 --strictandgit diff --checkpassed. -
[x] 31.2 Rename the Gmail source/runtime module to
reply-pilot-googlewithout 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 usereply-pilot-google. - Evidence: Gmail endpoint paths,
GMAIL_*behavior, cache layout, watch behavior, and test fixtures remain intact. Compose publishes canonicalreply-pilot-googleand temporaryreply-pilot-gmailaliases; 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_DIRenv values match; bothREMOTE_DIRvalues point to the new module path. Local materialized token and Pub/Sub files match their encrypted sources and are mode0600. - Evidence: the preserved local data tree remains 930 MB / 9,665 files. The
renamed container is healthy as non-root UID:GID
501:20, usesreply-pilot-google:latest, keeps/app/confread-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 verifypassed 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 plusscript/check-architecture.sh,mkdocs build --strict, andgit diff --checkpassed. -
[x] 31.3 Switch repository consumers and architecture truth to the canonical
reply-pilot-googleendpoint. - 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_URLand Jira ReportsJIRA_REPORTS_EMAIL_BASE_URLwere set to the canonical endpoint and verified withsops 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-gmailCompose 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/confread-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 mode0600. -
Validation: BE Maven
clean verifypassed 339 tests, Google Mavenclean verifypassed 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-googlewith 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
cbc301ad44815df7e6943950200b7d781bb9dd65c6ae7cfae2b1b26e950a6cd0and the conf digest wasc5c6ae4b7e8170e53e887cfc527e47fcc5c374e57521aabccdc3f5bada758639. The canonical tree contains 375 MB / 4,921 files and is owned byagent:agent; the technical token and Pub/Sub credential source hashes matched their deployed files, which are both mode0600. - Evidence: production Google, BE, and Jira Reports containers are healthy as
UID:GID
1001:1001, userestart: unless-stopped, and mount/app/confread-only. BEGOOGLE_API_BASE_URLand Jira ReportsJIRA_REPORTS_EMAIL_BASE_URLboth resolve tohttp://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
- Fresh Google, BE, and Jira Reports logs contained no ERROR, FATAL, or traceback lines.
- Evidence: the original
reply-pilot-gmailcontainer is stopped rather than deleted, its original data/conf paths remain intact, and it has bothreply-pilot-gmailandreply-pilot-googlealiases. 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 verifypassed 66 tests, BE Mavenclean verifypassed 338 tests, Jira Reports pytest passed 21 tests, shell syntax and production Compose rendering passed, and the changed production GoogleHOST_DATA_DIRandHOST_CONF_DIRSOPS values were verified withsops decrypt --extractwithout disclosing credentials. -
[x] 31.5 Move per-user Google Calendar OAuth and Calendar API ownership from BE to
reply-pilot-google. - Evidence:
reply-pilot-googleexclusively 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 existingapp_user_google_calendar_profile, and calls the narrow internal API withGOOGLE_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 verifysuites 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=trueandconnected=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. Remainingreply-pilot-gmailstrings 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-gmailcontainer, its sole-use imagesha256: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-gmailwere 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, usesrestart: unless-stopped, mounts/app/confread-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 plusscript/check-architecture.sh,mkdocs build --strict, repository reference audit, andgit diff --checkpassed. -
[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, andoffline_access. Create preserves an already returned Jira key if its follow-up enrichment read requires a new consent, preventing false428responses and duplicate retries. - Evidence: after re-consent, live status and
/myselfsucceeded.RP-3509was 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 inDone. Earlier invalid-format live testing returned428 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 plusscript/check-architecture.sh, strict MkDocs build, regenerated PlantUML PNG, andgit diff --checkpassed. -
[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
JiraProxyServiceto the delegated Jira client. Missing, malformed, or unusable OAuth credentials use the shared HTTP428 jira_oauth_requiredcontract 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, andgit diff --checkpassed.script/check-architecture.shstill reports the pre-existing stalereports/architecture-report.mdbaseline 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-bodyoperator_user_idas identity, and passes the authenticatedappUserIdas 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 shared428 jira_oauth_requiredhandler 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,JiraProxyControllerTestpassed with 13 tests; the controller test wires the realJiraProxyServicebecause no standaloneJiraProxyServiceTestexists. Focused app OAuth/client/lead-import coverage passed with 11 tests; fullmvn -q verify, the full Flask suite with 286 tests,mkdocs build --strict, andgit diff --checkpassed.script/check-architecture.shstill reports the pre-existing stalereports/architecture-report.mdbaseline 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_requiredcontract 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,HttpJiraIssueClientTestpassed with 17 tests; focused app attachment/client/OAuth coverage passed with 11 tests; fullmvn -q verify, full Flask suite with 284 tests,mkdocs build --strict, andgit diff --checkpassed.script/check-architecture.shstill reports the pre-existing stalereports/architecture-report.mdbaseline 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_requirederror, 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,TaskMutationControllerTestpassed with 40 tests; focused app client/workflow coverage passed with 11 tests; fullmvn -q verify, full Flask suite with 283 tests,mkdocs build --strict, andgit diff --checkpassed.script/check-architecture.shstill reports the pre-existing stalereports/architecture-report.mdbaseline 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,EmailSendControllerTestpassed with 51 tests; seven corresponding Flask workflow tests passed; fullmvn -q verify, full Flask suite with 281 tests,mkdocs build --strict, andgit diff --checkpassed.script/check-architecture.shstill reports the pre-existing stalereports/architecture-report.mdbaseline 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:
JiraOAuthRequiredExceptionHandleris the single backend mapping to HTTP428withstatus=error, codejira_oauth_required, and the original reason; controller-local copies were removed. - Evidence: missing, malformed, missing-refresh, rejected-refresh, and
delegated Jira
401paths converge onJiraOAuthRequiredException; no delegated request falls back to the technical account. - Evidence: task mutation, Jira backend, attachment, and lead-import HTTP
clients require both HTTP
428and the contract code before exposing the sharedJiraOAuthRequiredError. - 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 testpassed; focused app client/workflow/template selection passed with 11 tests; fullmvn -q verify, full app suite with 281 tests, andmkdocs build --strictpassed. -
[x] 30.1 Add an explicit delegated Jira request path for every supported API operation.
- Evidence:
JiraIssueClientnow 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:
HttpJiraIssueClientresolves oneJiraDelegatedAccessthrough the existingJiraOAuthAccessProvider, reuses it across every request in the operation, and sends Bearer authentication toapi.atlassian.com/ex/jira/{cloudId}/rest/api/3; the same request access object also covers multipart upload and binary download. - Evidence: delegated HTTP
401responses becomeJiraOAuthRequiredException; 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 testpassed, covering Bearer/cloud URL selection, token refresh, delegated transition metadata, delegated attachment operations, delegated401, and the technical Basic-auth path. - Validation: full
mvn -q testpassed inreply-pilot-be. -
Validation:
mkdocs build --strictandgit diff --checkpassed. -
[x] 29. Add sent-only Gmail threads to the single Gmail-owned cache.
- Evidence:
POST /api/mailbox/snapshotaccepts optionalskip_cached_threads; when true,reply-pilot-gmailreads cached thread IDs from its index, lists Gmail thread IDs for the requested query, skips cached IDs beforethreads.get, and merges new sent-only threads into the same account-scoped cache. - Evidence:
reply-pilot-besupportssync_mode=sent_backfillandPOST /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:sentwith page size 20 andskip_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;ensureWatchre-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
INBOXorSENT. - Validation:
mvn -f reply-pilot-gmail/pom.xml clean verify,mvn -f reply-pilot-be/pom.xml clean verify, andpython3 -m pytest reply-pilot-worker/testspassed during implementation. -
Validation:
script/check-architecture.sh,mkdocs build --strict, andgit diff --checkpassed;GMAIL_WATCH_LABEL_IDSwas 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-gmailowns the account-scoped physical cache underdata/accounts/<GMAIL_CACHE_ACCOUNT_ID>/emails/, including index, thread snapshots, attachment metadata, attachment bytes, remote page token, and history cursor. - Evidence: Gmail attachment
relative_pathvalues remain logical paths such asemails/attachments/<thread>/<file>, so DB/debug-context behavior does not need a migration. - Evidence:
reply-pilot-gmailexposes internal cache read endpoints for BE inbox/detail/attachment reads and the Java Spring BootGET /api/reports/email-weekly-countsendpoint computes from Gmail-owned cache/runtime data. - Evidence:
reply-pilot-jira-reportsdefaultsJIRA_REPORTS_EMAIL_BASE_URLtohttp://reply-pilot-gmail:5000in config, Compose, scripts, docs, and env template. - Evidence:
reply-pilot-beuses an HTTP-backed cache adapter inEMAIL_SYNC_BACKEND=gmail_servicemode, 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-searchindexesreply_pilot_taskJira issues, includes delegatedtask_type_valuein task search text/subtitles, and storesdelegated_task_assignee_app_user_idonly 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:
Donedelegated tasks remain indexed for explicit search through normal company scope orcompany.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, anddocs/refactoring-context.mddocument the final search/visibility behavior. - Evidence:
reports/architecture-report.mdwas regenerated after the search implementation change, andscript/check-architecture.shpassed 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 validationpython3 -m pytest reply-pilot-search/testspassed with 19 tests;python3 script/architecture-report.py --output reports/architecture-report.md,script/check-architecture.sh,mkdocs build --strict, andgit diff --checkpassed. -
[x] 27.6 Add delegated task UI workflow.
- Evidence:
reply-pilot-appkeepsreply_pilot_taskas its own Jira work type, reads backend-providedtask_type_valueinto 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.htmlcomponent 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 callsPOST /api/tasks/{taskId}/reply-pilot-task/close. - Evidence: app routes remain thin
views.pyroute-map entries delegated toreply_pilot_app.workflows.tasks, and mutation controls follow the UI rule that data-changing actions are submitted with buttons. - Evidence:
docs/web-app.mdanddocs/refactoring-context.mddescribe 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 validationpython3 -m pytest reply-pilot-app/testspassed with 249 tests;mkdocs build --strictandgit diff --checkpassed. -
[x] 27.2 Add delegated task cache schema.
- Evidence: migration
reply-pilot-db/migrations/0041_add_reply_pilot_task_subtype.sqladdsreply_pilot_tasktotask_jira_work_typeand createstask_jira_reply_pilot_taskwithtask_type_value,requester_app_user_id, timestamps, FK rollback, and no Jira dropdown check constraint. - Evidence:
reply-pilot-db/migrations/db.changelog.xmlincludes the new changeset, anddocs/database.md,docs/backend.md,docs/permissions.md, anddocs/refactoring-context.mddescribe 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 updateall passed fromreply-pilot-db/;mvn -f reply-pilot-be/pom.xml verifypassed with 212 tests;mkdocs build --strictandgit diff --checkpassed. -
[x] 27.5 Implement delegated task mutations.
- Evidence:
TaskMutationControllerexposesPOST /api/tasks/{taskId}/reply-pilot-task/resolveandPOST /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:
TaskMutationServiceresolves 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 preservesIn Progress. - Evidence: requester close transitions Jira/local cache to
Donewithout changing assignee, and wrong-assignee/wrong-requester paths return 403. - Evidence:
JdbcTaskMutationRepositoryloads delegated mutation targets and checks active assignee ownership throughtask_jira.user_idor the app user's Jira account profile; tests cover service, controller, and JDBC behavior. - Evidence:
docs/backend.md,docs/permissions.md,docs/database.md, anddocs/refactoring-context.mddocument 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 testpassed with 24 tests;mvn -f reply-pilot-be/pom.xml verifypassed with 212 tests;mkdocs build --strictandgit diff --checkpassed. -
[x] 27.4 Implement task-derived company visibility.
- Evidence:
JdbcReadModelRepositorynow 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 activereply_pilot_tasklinked to that company. - Evidence: the delegated exception does not change mutation authorization;
it excludes unrelated companies, unassigned delegated tasks, and
Donedelegated 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, anddocs/refactoring-context.mdrecord the read-only delegated visibility rule and the remaining search alignment boundary. -
Validation:
mvn -f reply-pilot-be/pom.xml -Dtest=JdbcReadModelRepositoryTest testpassed with 6 tests;mvn -f reply-pilot-be/pom.xml verifypassed with 208 tests;mkdocs build --strictandgit diff --checkpassed. -
[x] 27.3 Extend backend Jira sync and read models for delegated tasks.
- Evidence:
JiraTaskSyncServicenow includes configuredReply Pilot Taskissues in task-sync JQL, maps them to local work typereply_pilot_task, copies assignee/status/summary/description as before, and normalizes blank delegated task type values toGeneral. - Evidence:
HttpJiraIssueClientrequests configuredJIRA_REPLY_PILOT_TASK_TYPE_CUSTOM_FIELD_ID(defaultcustomfield_10269) in search/detail reads and parses Jira dropdown objects/lists intoJiraIssue.replyPilotTaskTypeValue. - Evidence:
JdbcJiraTaskSyncRepositoryupsertstask_jira_reply_pilot_task.task_type_valueforreply_pilot_taskrows 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 hidesDonedelegated 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, anddocs/backend.mddocument 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' testpassed with 23 tests;mvn -f reply-pilot-be/pom.xml verifypassed with 207 tests;mkdocs build --strictandgit diff --checkpassed. -
[x] 27.1 Finalize delegated Jira task contract.
- Evidence: user confirmed Jira issue type
Reply Pilot Taskwith admin edit URLhttps://internet-handel.atlassian.net/secure/admin/EditIssueType!default.jspa?id=10206. - Evidence: user confirmed Jira taskType field
customfield_10269and optionsGeneral,Meeting organization, andRegistration (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;
Doneis hidden only from default task view. -
Validation:
mkdocs build --strictpassed. -
[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-besuccessfully built the Java 21 Spring Boot runtime image fromreply-pilot-be/Dockerfile. - Evidence: the previously running
reply-pilot-becontainer was still the old Gunicorn/Python runtime, so Compose was rebuilt and force-recreated from the current Java Dockerfile withHOST_UID=$(id -u) HOST_GID=$(id -g) docker compose up --build -d --force-recreateinreply-pilot-be. - Evidence: the recreated container runs
java -jar /app/reply-pilot-be.jaras non-root host user501:20, Docker reports it healthy, Spring Boot logs show Java 21.0.11 on Tomcat port 5000, andcurl -fsS http://127.0.0.1:9091/healthzreturned{"status":"ok"}. -
Validation:
mvn -f reply-pilot-be/pom.xml clean verifypassed with 169 Java tests;python3 -m pytest reply-pilot-app/testspassed with 213 tests;script/check-architecture.sh,mkdocs build --strict, andgit diff --checkpassed. -
[x] 26.2 Remove legacy Python backend implementation and test harness.
- Evidence: removed the legacy backend package
reply-pilot-be/reply_pilot_be, deletedreply-pilot-be/tests, and removed Python-only backend runtime leftovers:gunicorn.conf.py,requirements.txt,requirements-dev.txt,scripts/be-generate-gmail-token.py, andscripts/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.mdnow listsreply-pilot-bewith 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 verifypassed with 169 Java tests;python3 -m pytest reply-pilot-app/testspassed with 213 tests;script/check-architecture.sh,mkdocs build --strict, andgit diff --checkpassed. A working-tree scan for.pyfiles underreply-pilot-beoutside build/data/log paths returned no files. -
[x] 25.10 Resolve Java lead-import outbound email transaction boundary.
- Evidence:
LeadImportServicenow sends the optional outbound Gmail message and passes the resultingCachedEmailItemtoLeadImportRepository.markImportedinstead of importing activity in a separate repository transaction. - Evidence:
JdbcLeadImportRepository.markImportedlinks the sent recipient email contact to the imported party, callsActivityEmailImportRepository.importEmails(Connection, ...), creates the lead-import activity note, marks the lead item imported, and refreshes batch progress in one caller-owned DB transaction. - Evidence:
JdbcActivityEmailImportRepositorynow 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 testpassed with 10 tests. Full Java validationmvn -f reply-pilot-be/pom.xml clean verifypassed with 169 tests. Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests; app validationpython3 -m pytest reply-pilot-app/testspassed with 213 tests. Architecture and documentation validationscript/check-architecture.shandmkdocs build --strictpassed. Whitespace validationgit diff --checkand 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
JdbcRequirementBackfillWorkeras the concreteRequirementBackfillWorkerbean behindPOST /api/requirements/backfill/run. It mirrors the Python party selection modes forparty_id,after_party_id, and service-levelparty_idsfan-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 testpassed with 18 tests. Full Java validationmvn -f reply-pilot-be/pom.xml clean verifypassed with 167 tests; Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests; app-side validationpython3 -m pytest reply-pilot-app/testspassed with 213 tests. Architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs 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
JdbcRequirementMonitoringWorkeras the concreteRequirementMonitoringWorkerbean behindGET /api/requirements/report. It loads requirement state counts, manual-review quality metrics, AI evidence metrics, and aggregation queue backlog directly from the backendDataSource. - 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:
RequirementMonitoringSettingsmirrorsWORKER_STATUS_BASE_URLandWORKER_STATUS_TIMEOUT_SECONDS, whileOpenAIClientSettingssupplies the AI configured/model fields. - Evidence: direct JDBC tests cover DB-derived report metrics with a local
/statuszHTTP server and the not-configured worker-status fallback. -
Validation: full Java validation
mvn -f reply-pilot-be/pom.xml clean verifypassed with 163 tests; Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests. Architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.7 Port concrete requirement aggregation worker bean to Java.
- Evidence: Java now provides
JdbcRequirementAggregationWorkeras the concreteRequirementAggregationWorkerbean behindPOST /api/requirements/evaluate/run. It claims dueparty_requirement_eval_queuerows, uses PostgreSQLFOR UPDATE SKIP LOCKEDwhere available, increments attempts, and keeps H2-compatible SQL for focused JDBC tests. - Evidence: the worker ports the Python aggregation rules for
ENLISTMENT_TABLE,FEED, andSUPPLIER_IDENTIFIER, including source bonuses, confidence thresholds, competing candidate review states, final feed URL/delivery-mode projection, supplier identifier projection, fulfilled timestamp handling, andparty_requirement_stateupsert without overwriting manual overrides. - Evidence: per-item failures roll back to a savepoint, defer only the failed
queue row, and continue the batch.
RequirementAggregationSettingsmirrors the existingAPP_DATA_DIR,REQUIREMENT_EVAL_LOCK_TTL_SECONDS,REQUIREMENT_AGGREGATION_RULESET_VERSION, andREQUIREMENT_RULESET_STATE_FILEbehavior, including one-time full-company requeue when the aggregation ruleset version changes. - Evidence: direct JDBC tests cover successful
FEEDaggregation, 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 verifypassed with 161 tests; Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests. Architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.6 Port concrete AI requirement classification worker bean to Java.
- Evidence: Java now provides
JdbcRequirementAiClassificationWorkeras the concreteRequirementAiClassificationWorkerbean. It scans deterministic candidates forENLISTMENT_TABLE,FEED, andSUPPLIER_IDENTIFIER, skips candidates already materialized for the currentREQUIREMENT_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 enqueuesparty_requirement_eval_queuework after successful writes. - Evidence:
RequirementAiClassificationSettingsmirrors the existingAPP_DATA_DIR,REQUIREMENT_AI_SCHEMA_DIR, andREQUIREMENT_AI_PROMPT_VERSIONconfiguration.HttpOpenAIJsonClientnow 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
FEEDclassification with an attachment input file,SUPPLIER_IDENTIFIERfact 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 verifypassed with 157 tests; Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests. Architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.5 Replace the historical CME matching worker with Java full sync.
- Evidence:
JdbcCmeCompanySyncWorkerimports onlydodavatelandosloveni_dodavateleintoparty_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 verifypassed with 153 tests; Python backend compatibility validationpython3 -m pytest reply-pilot-be/testspassed with 227 tests. Architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.4 Port automatic incoming supplier reply task creation to Java.
- Evidence:
TaskMutationServicenow implements the domainEmailThreadReplyTaskAutomationport. For incoming messages without an existing thread task, it createsEmail Thread ReplyJira 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 toDrafting Reply, storestask_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_idare transitioned toDrafting Replythrough the same automation port instead of creating a duplicate task. - Evidence:
JdbcActivityEmailImportRepositorynow 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.mdnow 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' testpassed; clean full Java validationmvn -f reply-pilot-be/pom.xml clean verifypassed; full Python backend validationpython3 -m pytest reply-pilot-be/testspassed; architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.3 Port linked incoming-reply Jira task transition to Java activity importer.
- Evidence:
JdbcActivityEmailImportRepositorynow receivesJiraIssueClientand, when a newly imported incoming external email belongs to an existingtask_jira_email_thread_replylink, transitions each linkedemail_thread_replyJira issue toDrafting Replyand syncstask_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' testpassed; clean full Java validationmvn -f reply-pilot-be/pom.xml clean verifypassed; full Python backend validationpython3 -m pytest reply-pilot-be/testspassed; architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.2 Port deterministic requirement/fact extraction to Java activity importer.
- Evidence:
JdbcActivityEmailImportRepositorynow mirrors the deterministic incoming-email extraction layer for non-internal senders. It replaces its owndeterministic-email-parserrows inparty_feed_factandparty_supplier_identifier_fact, replaces its owndeterministic-enlistment-detectorrows inparty_requirement_evidence, and queuesENLISTMENT_TABLE,FEED, andSUPPLIER_IDENTIFIERaggregation work inparty_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
\scan 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' testpassed; full Java validationmvn -f reply-pilot-be/pom.xml verifypassed; full Python backend validationpython3 -m pytest reply-pilot-be/testspassed; architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 25.1 Add production Java JDBC activity-email import repository.
- Evidence: Java now provides
JdbcActivityEmailImportRepositoryas the production SpringActivityEmailImportRepositoryimplementation. It transactionally imports cached email thread messages intoactivity,activity_email,activity_participant, contact mechanism and party identity rows, and synchronizesactivity_email_attachmentandactivity_email_linkrows from the backend email cache. - Evidence: the adapter supports idempotent message updates by
provider = 'gmail'andexternal_message_id, replaces participants and artifacts on re-import, and implementssyncEmailArtifactsfor 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
ActivityImportStoreremains in the unfinished audit. -
Validation: focused Java validation
mvn -f reply-pilot-be/pom.xml -Dtest='JdbcActivityEmailImportRepositoryTest,ArchitectureRulesTest' testpassed; full Java validationmvn -f reply-pilot-be/pom.xml verifypassed; full Python backend validationpython3 -m pytest reply-pilot-be/testspassed; architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[x] 24.21 Final Java endpoint parity audit.
- Evidence: Java now includes the previously missing
POST /api/emails/{emailId}/debug/generateroute throughDebugReplyController; it preserves instruction validation, missing email handling, selected context normalization, attachment file inclusion, OpenAI text generation, response envelope, andreply_subjectbehavior. - Evidence:
reply-pilot-be/tests/test_java_route_parity.pyparses Flaskapi_bpdecorators 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.mdnow 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' testpassed; focused route/compatibility validationpython3 -m pytest reply-pilot-be/tests/test_java_route_parity.py reply-pilot-be/tests/test_api_compatibility.py -qpassed; full Java validationmvn -f reply-pilot-be/pom.xml verifypassed; full Python backend validationpython3 -m pytest reply-pilot-be/testspassed; architecture validationpython3 script/architecture-report.py --output reports/architecture-report.mdandscript/check-architecture.shpassed; documentation and whitespace validationmkdocs build --strictandgit diff --checkpassed. -
[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, andGET /api/requirements/reportthroughWorkerJobController,WorkerJobService, and typed domain worker ports. - Evidence: the Java endpoint behavior preserves compatible limit parsing,
party_id/after_party_id/party_idsparsing, multi-party backfill result merging, success envelopes, not-configured503responses, expected worker400responses, and unexpected worker500error 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' testpassed; full Java validationmvn -f reply-pilot-be/pom.xml verifypassed; Flask compatibility validationpython3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -qpassed; docs validationmkdocs build --strictandgit diff --checkpassed. -
[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, andPOST /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
ActivityEmailImportRepositoryhook 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,LeadImportServiceTestpassed; full Java validationmvn -f reply-pilot-be/pom.xml verifypassed; Flask compatibility validationpython3 -m pytest reply-pilot-be/tests/test_api_compatibility.py -qpassed. -
[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, andPOST /api/emails/{emailId}/send/replywith 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, resolvesreply_to_message_idfrom 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
ActivityEmailImportRepositorywhen 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, andPOST /api/mailbox/import/stepwith 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 throughActivityEmailImportRepository. 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, andGET /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 legacyemails.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/draftandPOST /api/email-thread-task/draftwith 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
/responsesJSON schema calls with stricttext.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, andPOST /api/jira/task-sync/runwith 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, andPOST /api/email-threads/{emailId}/reply-taskswith 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 totask,task_jira,task_jira_supplier_onboarding, andtask_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.mddocuments 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_URLis configured. -
[x] 05. Migrate reply draft storage.
-
Evidence: backend reply-draft API exists and app uses
HttpReplyDraftStorein 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.pyand 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-appno longer containsgmail_store.pyoropenai_client.py; app factory requiresBACKEND_API_BASE_URLoutside tests, builds backend-backed draft/debug clients in standard runtime, and no longer wires anopenai_clientextension. 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/testspassed;python3 -m pytest reply-pilot-be/testspassed;script/check-architecture.shpassed after regeneratingreports/architecture-report.md;mkdocs build --strictpassed. -
[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/checkandPOST /api/simple-auth/nonces/mark-used. -
[x] 17. Move search behind backend.
- Evidence: backend exposes app-facing
GET /api/searchand delegates toreply-pilot-searchthrough a backend search client. App search client now usesBACKEND_API_BASE_URL; app runtime config, Compose, and scripts no longer exposeSEARCH_API_BASE_URLorSEARCH_TIMEOUT_SECONDS. Boundary docs and PlantUML diagrams now showreply-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 testspython3 -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, andtask_storeno longer contain PostgreSQL-backed production store classes orpsycopgimports. Standard app runtime requires backend HTTP read/task APIs;task_storewiring is test-only.reply-pilot-appno longer declarespsycopg, exposesREPLY_PILOT_DB_*runtime config, or documents app-to-DB runtime access. -
Validation:
python3 -m pytest reply-pilot-app/tests -qpassed;python3 -m pytest reply-pilot-be/tests -qpassed;python3 script/architecture-report.py --output reports/architecture-report.mdregenerated the baseline;script/check-architecture.shpassed;mkdocs build --strictpassed. -
[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.pyimplementation logic is intentionally left in place until later Python backend removal. -
Validation: tracker/context-only change;
mkdocs build --strictandgit diff --checkpassed. -
[x] 20. Align refactoring ledger with the
refactoring-planskill. - Evidence: active ledger moved to
docs/refactoring.md; rootrefactoring.mdis 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, andgit diff --check -- refactoring.md docs/refactoring.md mkdocs.ymlpassed. -
[x] 21. Apply updated prompt-free
refactoring-planledger 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, andgit diff --check -- docs/refactoring.md refactoring.md mkdocs.ymlpassed. -
[x] 22. Split refactoring plan into context and tracker documents.
- Evidence:
docs/refactoring-context.mdnow owns target boundary, source-of-truth docs, guardrails, and validation gates;docs/refactoring-tracker.mdnow owns open items, maintenance items, unfinished audit, and done archive. Legacy refactoring files are pointers. -
Validation:
mkdocs build --strictpassed;script/check-architecture.shpassed; diff whitespace check for the refactoring docs andmkdocs.ymlpassed. -
[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}, andDELETE /api/ai-prompts/{promptId}with DTOs, service validation, not-found handling, and JDBC persistence against the existingai_prompt_typeandai_prompttables. - Evidence: Python compatibility harness now covers AI prompt JSON payloads,
status codes, validation messages, legacy
prompt_textinput, 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 andscript/check-architecture.shpassed. -
[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 inreply-pilot-be/reply_pilot_be/views.py, current compatibility harness coverage, and existing Java code underreply-pilot-be/src. - Evidence: the unfinished audit now states the actual migration state:
/healthzand AI prompt endpoints are implemented in Java; all other endpoint families require focused ports with compatibility contracts. -
Validation:
mkdocs build --strictpassed; diff whitespace check fordocs/refactoring-context.mdanddocs/refactoring-tracker.mdpassed. -
[x] 24.3 Port backend status and metadata endpoints to Java.
- Evidence: Java Spring Boot now implements
/api/metaand/api/jira/metafromBackendMetadataSettingswith compatible status, email, OpenAI, Jira, CME, lead-import, andemail_import_status: nullfields./healthzremains compatible through the same controller. - Evidence: compatibility harness now checks
/api/jira/metaagainst 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.shpassed. -
[x] 24.4 Port reply draft storage endpoints to Java.
- Evidence: Java Spring Boot now implements
GET/POST /api/reply-draftsandGET/PATCH/DELETE /api/reply-drafts/{draftId}with service validation, not-found handling, HTTP status mapping, and JDBC persistence againstpublic.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.shpassed. -
[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, andPUT /api/jira/assignees/{appUserId}with compatible JSON payloads, validation messages, not-found handling, DB-connectivity error mapping, and JDBC persistence againstpublic.app_user,public.app_configuration, andpublic.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.shpassed. -
[x] 24.6 Port simple-auth nonce endpoints to Java.
- Evidence: Java Spring Boot now implements
POST /api/simple-auth/nonces/checkandPOST /api/simple-auth/nonces/mark-usedwith compatibleused/markedJSON payloads, validation messages, DB-connectivity error mapping, duplicate-mark handling, and expiry purge semantics againstpublic.simple_auth_nonce. - Evidence: compatibility harness now covers initial check, mark-used,
duplicate mark, expiry boundary behavior, missing nonce, invalid
now_ts, missingexpires_at, and non-positiveexpires_atagainst 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.shpassed. -
[x] 24.7 Port backend search proxy endpoint to Java.
- Evidence: Java Spring Boot now implements
GET /api/search, normalizesq,type, andlimit, caps limit at 50, forwards requests toreply-pilot-searchthroughHttpSearchClientusingSEARCH_API_BASE_URLandSEARCH_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.shpassed. -
[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}, andGET /api/parties/{partyId}/activitieswith 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.shpassed. -
[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, andPOST /api/company-mentionswith 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.shpassed;mkdocs build --strictpassed. -
[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, andGET /api/email-threads/taskswith compatible pagination, status-code filter normalization including the legacyDrafting Replaytypo, assigned-user filtering through Jira account profile, task detailemail_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.shpassed;mkdocs build --strictpassed. -
[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, andPOST /api/companies/{partyId}/requirements/{requirementCode}/reviewwith 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.shpassed;mkdocs build --strictpassed. -
[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}, andDELETE /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-personCONTACT_FORunlink 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.shpassed;mkdocs build --strictpassed.
Latest broad validation recorded before this tracker split:
python3 -m pytest reply-pilot-app/testspassed.python3 -m pytest reply-pilot-be/testspassed.script/check-architecture.shpassed.