Skip to navigation

1040 Fluxx Platform Interop---Requirements-&-Sizing-(v2 Workflow)

Purpose: Translate the new event-driven 1040/Palantir workflow described in 1040_FLUXX_INTEROP_V2.md into a concrete, research-backed requirements list, gap analysis, and sized backlog (Epics → Stories, T-shirt sized Small/Medium/Large) across the fluxx-* microservices.

Method: Every finding below is based on direct inspection of the repos in this workspace (fluxx-classifier, fluxx-document-splitter, fluxx-extractor, fluxx-file-intake, fluxx-postal, fluxx-registration, fluxx-sql-server, fluxx-statuscheck) — READMEs, docs/, config.json / pubsub.config.json topic wiring, and the actual TypeScript source (handlers, orchestrators, repositories, migrations). Where the code disagrees with the informal description in the requirements doc, the code’s actual behavior is called out explicitly.


1. Naming Clarification (Assumption)

There is no repo literally named fluxx-fileupload. The upload endpoint described in requirement #1 (“Palantir calls the fluxx-fileupload endpoint with a PDF”) maps to the existing fluxx-file-intake service’s POST /v1/upload/:applicationId endpoint (uploadController.ts → uploadService.handleMultiFileUpload / handleChildFileUpload). This document proceeds on that assumption — flag if a distinct, new “fileupload” surface is actually intended.

Also note: fluxx-statuscheck is the only other fluxx-* repo not directly named in the requirements, but it consumes almost every topic in the platform for observability and will need updates to stay in sync with the new flow (see Epic 8).


2. Current State — What The Code Actually Does Today

The informal description at the top of 1040_FLUXX_INTEROP_V2.md (upload → extractor calls Databricks → polls job → Fluxx calls Palantir) undersells how much has already been built. The real current-state, confirmed in source, is:

StageServiceBehavior (verified in code)
1. Uploadfluxx-file-intakePOST /v1/upload/:applicationId accepts file(s), also supports child file uploads (parentFileId + clientApplicationClassifierId query params) — this already exists to support split-children re-entering the pipeline. Publishes validated-file → (internal copy) → accepted-file.
2. Classificationfluxx-classifierAlready calls a real classifier (not just a blob-exists placeholder as its README states). AcceptedFileHandler → ClassificationOrchestrator.runClassifications() → resolves a classifier implementation (e.g. tax1040Classifier.ts) → startClassification() calls the configured Databricks endpoint (classificationUrl) and gets a runId. It then polls via ClassificationPollingOrchestrator (DB-backed job/lock model, exponential backoff, pollingUrl) until terminal status, then publishes classified-file (or problem).
3. Splittingfluxx-document-splitterAlready fully implemented (not a placeholder as its README states). ClassifiedFileHandler downloads the classification JSON + source PDF, validates page ranges, splits the PDF (pdfSplitter.ts), uploads each split file back through fluxx-file-intake’s child-upload endpoint (FileIntakeService.uploadChildFile), publishes one split-file event per output file, and a split-file-result manifest event.
4. Extraction triggerfluxx-extractorSubscribes to accepted-file today (extractor-accepted-file subscription; see pubsub.config.json) — i.e., it currently reacts to the original uploaded file, not to split-file. ExtractionTriggerHandler looks up extractor config from fluxx-registration, generates a SAS URL for the blob, and calls ExtractorService.runDocumentExtractions().
5. Extraction executionfluxx-extractorDatabricksAIExtractor.startExtraction() calls the Databricks orchestrator (extractionUrl), gets a runId, and a separate extractionJobPollingWorker.ts polls getRunStatus() until terminal.
6. Delivery to Palantirfluxx-extractorOn success, DatabricksAIExtractor.processExtractionResult() downloads the result blob(s) and calls PalantirApiClient.ingestExtractedFields() directly — i.e., the extractor talks to Palantir directly today; fluxx-postal is not in this loop at all.
Delivery infra (built, unused by this flow)fluxx-postalFully built to consume to-send / to-retry topics, look up deliveryUrl from fluxx-registration (RegistrationService.getDeliveryRoute), and POST the message body via PostalService.postMessage. Nothing in the platform publishes to to-send today — the topic reference doc (fluxx-file-intake/docs/topics-subscriptions-reference.md) literally lists its publisher as TBD. Postal also has no database of its own — every delivery does a live HTTP call to fluxx-registration for the route, and deliveryMethod is hard-coded to 'http' in clientApplicationRepository.ts (not a real stored preference).
Registration storefluxx-registration + fluxx-sql-serverCentral DB (autoextract, schema-per-domain e.g. [registration].[ClientApplicationExtractor]) already stores extractionUrl, pollingUrl, agent, ontologyActionUrl, extractorTypeId per client-application-extractor, and an analogous classifier table. Palantir is already a seeded client application with a DBRICKS extractor row (V1.22.0__createPalintrSeedRegistration.sql).

Key architectural gaps this exposes (beyond the literal 7 bullet points)

  1. Two separate poll-and-lock engines exist (classifier’s ClassificationPollingOrchestrator, extractor’s extractionJobPollingWorker) that both need to be replaced/augmented with event-driven completion — this is a bigger lift than “stop polling,” it’s swapping the completion trigger for two independently-built subsystems.
  2. Extractor talks to Palantir directly, hard-coded to a single client (Palantir-specific PalantirApiClient, ontologyActionUrl). Moving delivery to fluxx-postal means removing this direct dependency from fluxx-extractor and generalizing delivery.
  3. to-send has no publisher today. Wiring the extractor’s completion event into to-send (or a new topic postal also subscribes to) is the actual “last mile” connecting the new flow to the already-built postal delivery pipeline.
  4. deliveryMethod/content-shape (payload vs. SAS URL vs. blob URL) does not exist as a real, configurable concept anywhere in the schema. This is wholly new (requirement #7’s “one of three things, whichever is registered”).
  5. fluxx-extractor subscribes to accepted-file, not split-file, even though fluxx-document-splitter’s own config.json already declares an extractor-split-file subscription on split-file — i.e., the SB topology partially anticipates this move already, but the extractor app code and its own pubsub.config.json haven’t been updated to match.
  6. No component currently owns “what to do when there’s no classifier/splitter path” — i.e., a backward-compatibility/parallel-run story is needed if both the old (accepted-file → extractor direct) and new (accepted-file → classifier → splitter → split-file → extractor) paths must coexist during rollout.

3. Target State (New Workflow)

generate fileId (GUID) POST /v1/upload (PDF + fileId) accepted-file (fileId) call classify endpoint (fileId, fire-and-forget) classify, write result to blob publish classification-complete (fileId, topic) classified-file (fileId) upload each split child (new child fileId, parentFileId=original fileId) split-file (child fileId, one per child) call extraction endpoint (child fileId, fire-and-forget) extract, write result to blob publish extraction-complete (fileId, topic) resolve delivery preference (payload/sasUrl/blobUrl) to-send (fileId, parentFileId) resolve delivery route (own DB, cached from registration) POST registered endpoint (payload | sasUrl | blobUrl, fileId) Palantir fluxx-file-intake fluxx-classifier Databricks fluxx-document-splitter fluxx-extractor fluxx-postal fluxx-registration

Key differences from current state, in requirement order:

  1. Upload endpoint unchanged (fluxx-file-intake), except that the FileID is now generated by Palantir and supplied on upload rather than generated server-side (see §3a).
  2. Classifier calls Databricks classify endpoint — already true today; the new part is how it learns of completion, and that the call must now include the Palantir-issued fileId.
  3. No polling — Databricks publishes to a topic on completion; classifier subscribes instead of polling.
  4. Splitter behavior already matches the requirement (splits + publishes one event per file) — needs verification/regression testing against classifier’s new completion path, not new build.
  5. Extractor is triggered by split-file (not accepted-file as today) and calls the existing Databricks extraction endpoint, including the relevant fileId in the call (see §3a for which fileId — parent vs. split child).
  6. No polling — Databricks publishes extraction completion to a topic; extractor subscribes instead of polling.
  7. fluxx-postal — not the extractor — contacts the registered Palantir endpoint, with the payload shape chosen from 3 registered options. This requires postal’s own DB table for registrations/delivery preference and a producer that actually publishes to to-send.

3a. FileID Ownership Change: Palantir-Generated GUID

New design direction: Palantir generates the FileID (a GUID/UUID) client-side at upload time and submits it along with the PDF. This becomes the canonical FileID for that document across the entire platform, and must be explicitly included in both the classification call and the extraction call made to Databricks — making fileId (not just the internal jobId/runId) a first-class cross-system correlation key.

What changes vs. today (confirmed in source):

AreaTodayNew Design
FileID originfluxx-file-intake’s UploadService.handleMultiFileUpload/handleChildFileUpload generates the ID server-side via FileUtils.generateFileId() (crypto.randomUUID()), before the caller ever sees it. The caller cannot supply one today.Palantir generates the GUID and passes it in the upload request; fluxx-file-intake must accept and use the caller-supplied ID instead of generating its own for this client.
Format compatibilityFileUtils.isValidGuid() already validates a standard 8-4-4-4-12 GUID — the same format crypto.randomUUID() produces.No format change needed — a Palantir-issued GUID passes the existing validation as-is.
Propagation through existing eventsfileId is already threaded, unchanged, through accepted-file → classified-file → split-file → split-file-result message contracts.No message-contract changes needed for these existing topics — they already carry fileId; only its origin changes.
Classification call to DatabricksTax1040Classifier.startClassification() is currently a mock — the commented-out real call only sends { jobId, fileUrl }. No fileId field exists in the classification request today.The real (non-mock) classification call must be built to include fileId explicitly in the request body, so Databricks can echo it back on completion.
Extraction call to DatabricksDatabricksApiClient.triggerJobRun() / DatabricksRunNowRequest sends { name, client_id, created_by, metadata: { output_path }, files: [{ file_path, agent }] } — no fileId field exists in this request today, confirmed in databricks.apiClient.ts.Add fileId to DatabricksRunNowRequest (and the corresponding completion/status response) so it is passed on every extraction call and can be used to correlate the eventual completion event (Epic B).
Split-child filesWhen fluxx-document-splitter uploads each split PDF back through fluxx-file-intake’s child-upload endpoint, each split child gets its own new, server-generated fileId (tracked via parentFileId back to the original).Open question (Q6 below): does extraction on a split child pass the child’s own fileId, or the original Palantir-issued parent fileId? See Q6 for the recommended default.

4. Open Questions / Assumptions Requiring Decision Before Sizing Is Final

These materially affect sizing and should be confirmed with the Databricks/infra team and product owner before work starts. Each is flagged inline in the epics below as well.

#QuestionWhy it mattersRecommended default (used for sizing)
Q1Can Databricks publish directly to an Azure Service Bus topic on job completion, or does it only support an HTTP callback/webhook?Determines whether classifier/extractor need a new inbound Service Bus subscription (cheaper, consistent with existing patterns) or a new inbound HTTP webhook route (more work: auth, idempotency, exposing a public endpoint).Assume Service Bus topic publish from Databricks is available (mirrors the existing malware-scan Event Grid → Service Bus pattern already in the platform). Webhook fallback sized separately as a swap-in alternative.
Q2Who generates the SAS URL for delivery option 2?Postal has no blob storage client today. Extractor already has BlobStorageService.generateSignedUrl.Extractor generates the SAS URL (and the plain blob URL) at extraction-completion time and includes both, plus the raw payload, in the message it hands to to-send; postal picks the field it needs based on registered preference. Avoids giving postal blob credentials.
Q3Does “standalone DB table for postal registrations” mean postal gets its own copy/cache of registration+delivery-preference data (denormalized, refreshed via updated-registration events which already exist and are unused), or does it replace the live HTTP call to fluxx-registration entirely?Changes whether postal needs to subscribe to updated-registration (already published, currently has zero consumers) and reconcile a cache, vs. simple pass-through storage.Postal owns a local read-optimized table populated by subscribing to the existing (currently unused) updated-registration topic, with a one-time backfill from fluxx-registration, and falls back to the live HTTP call on cache miss.
Q4Should the old accepted-file → extractor-direct path be removed immediately, or run in parallel during rollout (feature flag by client application)?Affects whether extractor needs to support two trigger topics simultaneously for a transition period.Assume a feature-flagged parallel run per client application (Palantir cut over first), removed once validated.
Q5Which three delivery content values map to requirement #7 exactly — payload, sasUrl, blobUrl — and is this a single enum column or three booleans?Schema design in fluxx-registration/fluxx-sql-server and postal.Single enum column DeliveryContentType (Payload | SasUrl | BlobUrl) on the client application (or a new delivery-preferences table), defaulting to Payload for backward compatibility.
Q6When a split child is extracted, does the extraction call to Databricks (and the resulting delivery to Palantir) use the split child’s own fileId or the original Palantir-issued parent fileId? Also: should file-intake reject/collide-check a duplicate Palantir-supplied fileId, or treat resubmission as idempotent?Determines correlation-key design in Epic B (extraction) and downstream delivery in Epic C, plus validation rules in Epic G.Use the split child’s own fileId for the extraction call and for to-send/Palantir delivery (since extraction and delivery are per-split-document), while carrying the original Palantir fileId alongside as parentFileId/rootFileId for lineage — Palantir can correlate results back to its original submission via that field. File-intake treats a duplicate Palantir-supplied fileId as a 409 Conflict (reject), not silent idempotent replay, unless product says otherwise.

5. Requirements List (Traceable to Source Bullets)

Req #RequirementPrimary Service(s)Status Today
R1Palantir uploads a PDF to the file intake endpointfluxx-file-intake✅ Exists
R2Classifier calls a configured Databricks endpoint to classify the documentfluxx-classifier✅ Exists (via ClassificationOrchestrator + classifier resolver)
R3No polling — Databricks classifies, writes to blob, publishes to a topic; classifier reacts to thatfluxx-classifier❌ Currently polls (ClassificationPollingOrchestrator)
R4Splitter picks up the published classification message, splits the PDF, publishes one event per split filefluxx-document-splitter✅ Exists, but trigger source (message shape from classifier) must remain compatible with R3’s rework
R5Extractor picks up the published split-file message and calls the existing Databricks extraction endpointfluxx-extractor⚠️ Partially — extraction call exists, but trigger is accepted-file, not split-file
R6No polling — Databricks extracts, writes to blob, publishes to a topic; extractor reacts to thatfluxx-extractor❌ Currently polls (extractionJobPollingWorker)
R7Postal picks up the published extraction message and contacts the registered Palantir endpoint with payload, sasUrl, or blobUrl (per registration)fluxx-postal, fluxx-registration❌ Postal not in the loop at all today (extractor calls Palantir directly); only one hard-coded delivery mode exists
R8Postal needs a standalone DB table for postal registrationsfluxx-postal, fluxx-sql-server❌ Does not exist
R9End-to-end workflow (upload → Palantir notified) must work with all of the above changesAll❌ Net-new integration path
R10Palantir generates the FileID (GUID) at upload time and passes it with the file; this becomes the canonical FileID used everywhere across the platform, and is passed to Databricks on both classification and extraction callsfluxx-file-intake, fluxx-classifier, fluxx-extractor❌ Net new — file-intake currently always generates fileId server-side, and neither the classification nor extraction request bodies sent to Databricks include fileId today

6. Epics & Sized Stories

Sizing scale used throughout:

  • S (Small): ~1–3 days, single service, low risk, no schema change or an additive/backward-compatible schema change.
  • M (Medium): ~3–5 days (up to ~1 week), may span 2 services or require a schema migration, moderate coordination/testing.
  • L (Large): ~2+ weeks, cross-service, new infrastructure/data model, higher risk, may depend on external teams (Databricks/infra) or require a rollout/feature-flag strategy.

Epic A — Classifier: Event-Driven Classification Completion (Remove Polling)

Replaces ClassificationPollingOrchestrator with a reactive completion path.

StorySizeDetails
A1. Spike: confirm Databricks completion notification mechanism (Service Bus publish vs. webhook) and payload contract (jobId/runId/fileId correlation, blob location)SResolves Q1. Output: a short design note + topic/webhook contract to unblock A2+.
A2. Define and register new inbound topic/subscription (e.g. classifier-job-complete) in config.json/pubsub.config.json, and add app-config keysSMirrors existing topic-registration patterns already in the repo.
A3. Build the real (non-mock) Tax1040Classifier.startClassification() call and include fileId explicitly in the outbound request body sent to the Databricks classify endpointMCurrently a hard-coded mock (// TODO: Implement the actual call...) that only sends { jobId, fileUrl } — no real HTTP call exists yet, and fileId is not part of the request at all today.
A4. New ClassificationCompleteHandler (or webhook controller) that correlates the incoming completion event to the existing job by fileId (primary) and runId/jobId (secondary) in jobRepository, replacing the terminal-status branch currently in pollJobStatus()MReuses updateJobSucceeded/updateJobFailed logic already in ClassificationOrchestrator; just changes the trigger and adds fileId as a correlation key.
A5. Remove/retire ClassificationPollingOrchestrator, its setTimeout loop, getJobsToPoll lock-based DB queries, and related polling config (MAX_POLLING_ATTEMPTS, POLL_INTERVAL_MS, etc.), behind a feature flag until A4 is proven in an environmentMMust keep DB columns for now (see Epic F) for audit/history; only the loop goes away.
A6. Update/extend contract + unit tests for the new handler and the real classify call; remove or repurpose polling-specific testsMExisting test/contract, test/unit structure reused.
A7. Update fluxx-classifier README/docs to reflect real (event-driven) architectureSAlso corrects the current README, which already understates existing behavior.

Epic A total: ~1 L equivalent (6 stories, 1 spike)


Epic B — Extractor: Trigger from split-file + Event-Driven Extraction Completion (Remove Polling)

StorySizeDetails
B1. Spike: confirm Databricks extraction completion notification mechanism (same as A1, likely shared design)SCan be combined with A1 if same team/contract.
B2. Change extractor’s inbound subscription from accepted-file to split-file; update pubsub.config.json/config.json, ExtractionTriggerMessageBody type to match the split-file event contract (clientApplicationId, internalUrl, fileLocationUrl, fileId)Msplit-file already exists and is published by the splitter today; this is primarily a rewire + type/shape mapping in ExtractionTriggerHandler.
B3. Feature-flag / dual-subscription support so accepted-file-triggered extraction can run in parallel with split-file-triggered extraction during rollout (Q4)MNeeded only if a phased cutover per client application is required; otherwise can be dropped in favor of a hard cutover for Palantir only.
B4. Add fileId to DatabricksRunNowRequest and thread it through DatabricksAIExtractor.startExtraction() — no fileId field exists in the extraction request body today (confirmed: only name, client_id, created_by, metadata.output_path, files[])MResolves Q6 for which fileId is sent (split child’s own fileId, with parent/root fileId carried as auxiliary metadata).
B5. New inbound topic/subscription + handler for extraction completion (mirrors A2/A4), correlating to jobRepository/jobStatusRepository by fileId (primary) and runId (secondary)MReuses updateJobStarted/status update plumbing already in ExtractorService.
B6. Remove/retire extractionJobPollingWorker.ts and related polling config, behind a feature flagMSame caution as A5 — keep schema, remove the loop.
B7. Update/extend tests (unit + contract) for new trigger, fileId-bearing extraction request, and completion handlersM
B8. Update fluxx-extractor README to reflect the new trigger source and event-driven completionS

Epic B total: ~1 L equivalent (8 stories)


Epic C — Decouple Palantir Delivery From Extractor → Route via Postal

This is the biggest behavioral change: today DatabricksAIExtractor.processExtractionResult() calls PalantirApiClient.ingestExtractedFields() directly. That needs to become “publish a delivery-ready message”; fluxx-postal becomes responsible for the actual outbound call.

StorySizeDetails
C1. Define the to-send (or new topic) message contract to carry: clientApplicationId, fileId, jobId, extracted payload (JSON), sasUrl, blobUrl, and enough metadata for postal to pick the right shapeMThis is the concrete fix for the “no publisher for to-send” gap found in research.
C2. In fluxx-extractor, replace the direct PalantirApiClient.ingestExtractedFields() call in the (now event-driven, per Epic B) completion handler with: download/locate result blob → generate SAS URL (BlobStorageService.generateSignedUrl, already exists) → build the C1 message → publish to to-sendMRemoves Palantir-specific coupling from the extractor entirely.
C3. Remove PalantirApiClient and its Palantir-specific config (ontologyRid, ontology action URL calls) from fluxx-extractor once C2 is validated; note ontologyActionUrl remains a registration concept, just consumed by postal instead (see Epic E)SCleans up now-dead code/config in the extractor.
C4. In fluxx-postal, extend DeliveryService/PostalService to build the outbound HTTP body according to the registered DeliveryContentType (payload passthrough vs. { sasUrl } vs. { blobUrl }) instead of forwarding the message body verbatimMBuilds on Epic D/E’s schema work.
C5. Contract/integration test: end-to-end simulated flow from a fake “extraction complete” event through to-send to a mock Palantir receiver, for all 3 delivery content typesMHigh value regression test given this is the new “last mile.”
C6. Update fluxx-postal and fluxx-extractor docs/README to describe the new delivery ownershipS

Epic C total: ~1 L equivalent (6 stories)


Epic D — Postal: Standalone Database for Registrations/Delivery Preferences

Directly implements requirement R8.

StorySizeDetails
D1. Design schema for a postal-owned table (e.g. [postal].[ClientApplicationDeliveryRoute]): clientApplicationId, deliveryUrl, deliveryContentType, isActive, audit columns, sync metadata (lastSyncedAt, sourceVersion)SSchema-only design story; feeds D2.
D2. fluxx-sql-server migration(s) to create the new schema/table (new V1.x.x script), consistent with existing Flyway conventionsSStraightforward additive migration, no cross-service coordination needed to create it.
D3. Add DbConnector/repository layer to fluxx-postal (net-new — postal currently has no DB dependency at all): connection config, postalRegistrationRepository.ts, DI wiring in index.tsMPostal has none of the existing DB plumbing that other services have (framework-nodejs-sql usage, migrations task wiring, docker-compose db service) — this is new infrastructure for this repo, not just a table.
D4. Subscribe fluxx-postal to the existing (currently unconsumed) updated-registration topic to keep the local table in sync; add subscription to config.json/pubsub.config.jsonMResolves Q3. Reuses an existing, already-defined but unused topic.
D5. One-time backfill job/script to populate postal’s table from fluxx-registration’s /v1/clientApplication/delivery-route (bulk) endpoint (already exists: getDeliveryRoutes())SCan reuse the existing bulk endpoint as-is.
D6. Update DeliveryService.selectDeliverer to read from the new local repository first, falling back to the live RegistrationService HTTP call on cache miss (keeps current behavior as a safety net)MDirectly modifies the existing, well-isolated deliveryService.ts.
D7. Docker-compose / dev-container updates for postal to bring up its own DB dependency locally (mirrors the pattern already used by fluxx-registration, fluxx-file-intake, etc.)SCopy-paste-adapt of an existing pattern already used by 4+ sibling repos.
D8. Unit + contract tests for the new repository, sync handler, and fallback behaviorM

Epic D total: ~1 L equivalent (8 stories, includes net-new infra for the repo)


Epic E — Registration: Delivery Content-Type Preference & Cleanup

StorySizeDetails
E1. fluxx-sql-server migration: add DeliveryContentType column (enum-like VARCHAR/lookup table: Payload/SasUrl/BlobUrl) to [registration].[ClientApplication] (or a new delivery-preferences table if 1:many is anticipated), default PayloadSAdditive, backward-compatible.
E2. Update clientApplicationRepository.ts (getDeliveryRoute, getDeliveryRoutes) to return the real stored deliveryContentType instead of the hard-coded deliveryMethod: 'http' string found in researchSSmall, isolated fix — the hook already exists (ClientDeliveryRouteDto.deliveryMethod), it just needs to stop being hard-coded.
E3. Add create/update API + schema validation for setting deliveryContentType on a client application (admin/onboarding flow)MExtends existing clientApplicationController.ts create/update paths.
E4. Confirm/relocate ontologyActionUrl semantics now that delivery is postal-owned — decide whether it moves from [registration].[ClientApplicationExtractor] to a more general delivery-route concept, or stays extractor-specific and is passed through in the C1 messageMDirectly affects Epic C’s message contract; should be resolved alongside C1.
E5. Publish an updated-registration event whenever delivery preference changes (verify this already happens broadly — topic exists — and specifically covers the new field)STopic/publish mechanism already exists; just needs to cover the new field.
E6. Update fluxx-registration OpenAPI docs/README for the new fieldS

Epic E total: ~1 M-to-L equivalent (6 mostly-small stories)


Epic F — Shared Data/Schema Cleanup & Migration Hygiene (fluxx-sql-server)

StorySizeDetails
F1. Consolidated migration review: confirm no breaking changes are needed to existing ExtractorJob/ClassifierJob/SplitterJob tables — new event-driven flow reuses runId-based correlation already in placeSResearch/validation story, likely no code change.
F2. Add new migration(s) for Epic D (postal schema) and Epic E (delivery content type) — bundle release ordering so fluxx-sql-server ships ahead of the services that depend on the new columns/tablesSCoordination/sequencing story.
F3. Decide fate of now-partially-unused polling columns (pollingUrl, pollAttemptCount, nextPollAt, lock columns) — keep for manual/backfill retry tooling vs. deprecate; document decision, no immediate schema change recommendedSAvoid premature deletion; these remain useful for ops/manual re-trigger even after auto-polling is removed.
F4. Add seed/test data for a second (non-Palantir) client application with each of the 3 deliveryContentType values, to support cross-service integration testingSMirrors existing seed pattern (V1.5.0__docai_client_app_seed.sql, V1.22.0__createPalintrSeedRegistration.sql).

Epic F total: ~1 S-to-M equivalent (4 small stories)


Epic G — File Intake: Confirm Fit for Palantir Entry Point (Low Risk)

StorySizeDetails
G1. Confirm POST /v1/upload/:applicationId meets Palantir’s per-form single-PDF upload pattern as-is (auth, file-count limits, response shape) — likely no code change, verification onlySResearch indicates existing endpoint already fits; treat as a verification spike, not a build story.
G2. If Palantir isn’t yet a registered/onboarded client application for file-intake (distinct from its extractor registration), complete onboarding via fluxx-registrationSAdministrative/config story, not necessarily code.
G3. Confirm validated-file → accepted-file path requires no changes to support the higher/parallel volume implied by per-form uploads (vs. previously batching)SVerification story; flag if load/perf testing is warranted.
G4. Accept a caller-supplied fileId on POST /v1/upload/:applicationId (new optional field/part in the multipart request), validate it with the existing FileUtils.isValidGuid(), and use it in fileService.insert() in place of FileUtils.generateFileId() when presentMCore code change for R10. Today handleMultiFileUpload/handleChildFileUpload always call FileUtils.generateFileId() unconditionally — this becomes conditional on caller input.
G5. Enforce uniqueness/collision handling for a caller-supplied fileId: reject with 409 Conflict (per Q6 default) if the ID already exists in the File table before insertSFileID is already the table’s primary key; this adds an explicit pre-check + clear error response instead of relying on a raw DB constraint violation.
G6. Preserve backward compatibility: client applications that do not supply a fileId continue to get one server-generated via FileUtils.generateFileId(), unchangedSEnsures non-Palantir clients (and existing split-child uploads via uploadChildFile, which should keep generating their own fileId per Q6) are unaffected.
G7. Update fluxx-file-intake OpenAPI schema/docs and README to document the new optional caller-supplied fileId fieldS

Epic G total: ~1 M equivalent (7 stories) — was the lowest-risk epic; the FileID change adds real (but still modest) build work


Epic H — Status Check, Observability, Testing & Rollout

StorySizeDetails
H1. Add fluxx-statuscheck subscriptions for any new topics introduced (classifier/extractor completion topics, if Q1 resolves to Service Bus) so file status tracking stays complete end-to-endMStatuscheck already subscribes broadly (status-accepted-file, status-classified-file, status-classifier-job-status, status-problem); this extends the same pattern.
H2. Add to-send/delivery outcome visibility to status tracking (currently no consumer of postal’s delivery success/failure)MNew for the platform — today nothing surfaces “was Palantir actually notified” outside postal’s own logs.
H3. End-to-end integration test environment/script: upload → classify → split → extract → deliver, using local Docker Compose emulators already present in every repoLCross-repo test harness; highest-value regression protection for this whole initiative given 4+ services change trigger/completion behavior simultaneously.
H4. Feature-flag / config-driven rollout plan per client application (start with Palantir only), with rollback path to the current poll-based flowMDirectly supports Q4’s phased-cutover assumption; coordinate with Epics A–C’s feature flags.
H5. Runbook/update to physical_architecture.md and topic reference docs (topics-subscriptions-reference.md) across affected repos to reflect final, shipped topologySDocs currently describe some topics (to-send, outbox) inconsistently with what’s implemented; needs a cleanup pass once the above ships.

Epic H total: ~1 L equivalent (5 stories, includes one explicit L for the E2E harness)


7. Summary Rollup

EpicFocusRough Size
AClassifier — event-driven completionL
BExtractor — split-file trigger + event-driven completionL
CDecouple Palantir delivery → route via PostalL
DPostal — standalone DB for registrations/delivery preferenceL
ERegistration — delivery content-type preferenceM–L
FSQL Server — schema/migration hygieneS–M
GFile Intake — entry-point confirmation + Palantir-supplied FileIDM
HStatuscheck, observability, E2E testing, rolloutL

Overall: 5 Large epics, 1 Medium epic, 1 Medium/Small epic, 1 Small-to-Medium epic — consistent with this being a cross-cutting architectural shift (poll → event-driven; extractor-direct → postal-mediated delivery; server-generated → Palantir-generated FileID) touching 7 of the 8 fluxx-* services plus the shared SQL schema. The three biggest unknowns that could swing sizing up further are Q1 (Databricks’ actual completion-notification capability — webhook support would add new inbound HTTP surface + auth work to both classifier and extractor), Q4 (whether a parallel/flagged rollout is required per client application, which roughly doubles the trigger-handling work in Epics A and B), and Q6 (whether split-child extraction/delivery correlates on the child’s own fileId or the original Palantir fileId, which affects the message contracts touched in Epics B and C).

8. Suggested Sequencing

  1. Epic G (G4–G6, FileID acceptance) — land Palantir-supplied FileID support in fluxx-file-intake first; every other epic’s correlation-key design (A3/A4, B4/B5, C1) depends on fileId being reliably caller-supplied and validated at the point of entry.
  2. Epic F (partial) + Epic E — schema groundwork (delivery content type, seed data) so downstream services have something to build against.
  3. Epic D — stand up postal’s DB and sync mechanism (can proceed in parallel with #2; no hard dependency).
  4. Epic C — wire extractor → to-send → postal → Palantir using the existing poll-based completion as the trigger for now (de-risks the biggest behavioral change — delivery ownership — independently of the polling removal).
  5. Epic A and Epic B (completion-handling + Databricks request stories, including fileId propagation) — once Q1 and Q6 are resolved, swap each service’s completion trigger from polling to event-driven and add fileId to the classify/extract request payloads, reusing the delivery path already proven in step 4.
  6. Epic B (split-file trigger rewire) — switch extractor’s inbound trigger from accepted-file to split-file, feature-flagged per Q4.
  7. Epic G (G1–G3, remaining verification/onboarding stories) — confirm/onboard file-intake fit beyond the FileID work already landed in step 1 (can happen any time, independent of the above).
  8. Epic H — observability and E2E testing threaded throughout, with the full harness (H3) finalized once steps 1–6 are functionally complete, then rollout (H4) client-application-by-client-application starting with Palantir.