Skip to content

Eligibility-context as a declarative composite view #363

Description

@mryhmln

Summary

The eligibility domain's rules engine adapter needs a structured view of household member data to evaluate SNAP and Medicaid eligibility. No such endpoint exists today. Issue #354 proposed a bespoke hand-crafted endpoint (EligibilitySnapshot with PUT upsert semantics); that PR was closed without merging. This issue replaces that approach: the eligibility context should be expressed as a plain declarative composition (using the infrastructure from #357) that assembles member-level data from multiple intake sub-resources into a single GET endpoint. This issue produces updates to the intake architecture doc and the composition cross-cutting doc.

Reference branch for baseline design thinking: origin/feat/eligibility-snapshot (the unmerged #354 branch). The docs on that branch (intake.md, eligibility.md, inter-domain-communication.md) represent the latest thinking even though the implementation was not merged. The decisions below record where we have departed from that baseline and why.

Settled decisions

GET-based live assembly, not PUT-based materialization

Decision: The composition endpoint is a standard GET. The Eligibility state machine calls GET /intake/applications/{id}/eligibility-context before each evaluate call; the composition runtime assembles current data from Intake's sub-resources and returns it. No stored EligibilitySnapshot resource in Intake; no PUT upsert.

Rationale: The #354 design used PUT upsert so that PII would stay in Intake's storage boundary (the snapshot is assembled and returned but not persisted in Eligibility). That constraint does not hold: the "determination context" that the rules engine uses must be stored with the Determination for audit and appeal purposes — you cannot satisfy federal audit requirements without a durable record of the inputs used to reach an outcome. The PUT approach also didn't eliminate the runtime dependency on Intake (the state machine still calls PUT synchronously before evaluate); it just shifted when the call happens. GET-based live assembly is architecturally equivalent at evaluate time and fits the composition infrastructure directly without extension.

PII flows to Eligibility's Decision records

Decision: The GET response is stored as Decision.memberContext and Determination.householdContext on Eligibility's records. This is the "determination context" pattern: the domain that owns the outcome owns the inputs that produced it.

Rationale: IBM Cúram's determination context, Salesforce Government Cloud's evaluation objects, and CalSAWS all store input snapshots with the determination record — not in the source domain. The #354 approach (audit trail via event log replay of EligibilitySnapshot.updated events) is operationally fragile: it requires the event log to be queryable, replayable, and long-lived, which varies significantly across state deployments. Stored fields on Determination/Decision are durable, queryable, and portable. States with strict FTI controls (IRS Pub. 1075) may need to overlay the eligibility context to limit which income fields are persisted on Decision.memberContext — that is an overlay concern, not a baseline design constraint.

Assembly logic stays in Intake

Decision: The composition config lives in Intake (the owning domain). Eligibility calls one endpoint and receives a pre-built payload without knowing which sub-resources contribute or how they are joined.

Rationale: Intake holds the authoritative application data and knows its own internal resource structure. If assembly logic lived in Eligibility's state machine or in the adapter, every Intake model change would require an Eligibility change. The composition config encodes the assembly contract in a single place, overlay-extensible by states.

Availability dependency is accepted

Decision: The GET call to Intake at evaluate time is an accepted synchronous dependency on a co-deployed service. No fallback to cached data is built into the baseline.

Rationale: The PUT upsert approach did not eliminate this dependency — the state machine still called PUT synchronously before evaluate. The only way to eliminate the dependency is event-driven push (Intake publishes updates when data changes; Eligibility maintains a continuously-synchronized projection) — that is significantly more complex and no major platform does it for eligibility evaluation. In practice, Intake and Eligibility are co-deployed; downtime is correlated. Post-evaluation access (audit, appeal, supervisor review) uses the stored Decision.memberContext and does not require Intake to be available.

Application-scoped endpoint with query param filtering

Decision: One composition endpoint per application (GET /intake/applications/{id}/eligibility-context). Callers that need a single member's data pass ?memberId=abc.

Rationale: SNAP evaluates the household as a unit — it needs all members in one call. Medicaid ex parte evaluates per person — callers can filter to a single member via query param. A separate per-member endpoint would require the rules engine adapter to know which endpoint to call per program type; that routing logic belongs in the composition config, not spread across callers. The plain composition filtering gap (see Required fixes) enables ?memberId=abc to work once fixed.

Resource structure mirrors the eligibility input data model

Each logical grouping of eligibility input data maps to exactly one Intake resource. The structure follows the rules engine's input model directly: root-level properties extend existing resources; named sub-objects become new singleton resources; multi-valued arrays are existing or new collection resources. Resource names match the corresponding input model category names.

New member-scoped resources (one record per member, bound by applicationId + memberId):

Resource Maps to Key fields Programs
member-pregnancy members[].pregnancy isPregnant; expectedChildren, endDate (Medicaid only) SNAP, Medicaid
member-veteran-status members[].veteranStatus isVeteran, hasDisability, needsAidAndAttendance, survivorNeedsAidAndAttendance, survivorHasDisability SNAP
member-student-status members[].studentStatus enrollment, inK12, inWorkStudy, assignedByEmploymentTrainingProgram, unfitForEmployment SNAP, Medicaid
member-disability-details members[].disabilityDetails hasPhysicalDisability, hasMentalDisability, isIncapacitated, unableToPrepareMeals, receivesDisabilityBenefits, receivesPublicDisabilityPension, receivesRailroadRetirementDisability, receivesInterimAssistance, ssiApplicationPending, inVocationalRehabilitation, hasDisabledChild (Medicaid only) SNAP, Medicaid
member-living-situation members[].livingSituation isExperiencingHomelessness, isBoarder, isRoomer, isLiveInAttendant, isSeparateAndApart, preparesFoodWithHousehold, recentlyReleasedFromInstitution, participatesInAnotherHousehold, livesInBoardingHouse, livesInInstitution, livesInFederallySubsidizedElderlyHousing, residesAtSubstanceAbuseTreatmentFacility, livesInHomelessShelter, livesInGroupLivingArrangement, isResidentOfBatteredWomensShelter SNAP
member-work-requirements members[].workRequirements registeredForWork, providedEmploymentInfo, reportedToReferredSuitableEmployer, abawdWorkProgramHoursPerWeek, abawdCountableMonthsUsed, abawdWaiverExempt, abawdStateExemption, impactedByDomesticViolence, isMigrantFarmWorker, isRegularParticipantInDrugOrAlcoholTreatment, hasSelfDeclaredTemporaryConditionPreventingWorkActivities, isUnableToMaintainEmployment, hasOtherValidReasonForWorkRequirementsUnfitness, isEnrolledInEmploymentTrainingProgram, striker sub-record (isStriker, eligibleDayBeforeStrike, exemptFromRegistrationDayBeforeStrike) SNAP
member-immigration-details members[].immigrationDetails citizenshipImmigrationStatus, qualifiedYearsInUs, qualifyingWorkQuarters, lawfullyResidedSince1996Senior, qualifyingMilitaryConnection; sponsor sub-record (memberId, isOrganization, dependentCount, otherSponsoredNonCitizens) SNAP
member-findings members[].findings ipvDisqualification, snapDrugFelonyConviction, ssnNonCompliance, workRequirementsNonCooperation, voluntaryQuit, voluntaryQuitReason, qualityAssuranceNonCooperation, felonyNonCompliance, paroleOrProbationViolation, isFleeingFelon, sanctionImposed SNAP
member-tax-info members[].taxInfo taxFilingStatus (single, mfj, mfs, hoh, not-filing), filingJointlyWithMemberId, claimedAsDependentByMemberId, claimsDependentMemberIds, isNonCustodialParentClaimingChild Medicaid
member-health-coverage members[].healthCoverage hasMedicarePartA, hasMedicarePartB, hasMedicarePartD, hasEmployerCoverage, employerCoverageAffordable, employerCoverageMonthlyPremium, hasTricare, hasCobra Medicaid
member-tribal-info members[].tribalInfo isEnrolledTribalMember, tribeName, hasTribalIncome SNAP, Medicaid

Existing member-scoped resources extended with root-level fields:

Resource Added fields
application-members receivesTanf, isEmancipated, isFosterChild, wasInFosterCareOn18thBirthday, receivesFamilyPreservationServices, spouseId, isHeadOfHousehold

Existing member-scoped collection resources (no changes needed):

Resource Maps to
member-incomes members[].income[]
member-expenses members[].expenses[]
member-assets members[].assets[]
member-employment-records members[].employment[]

New household-scoped resource (multi-valued — one record per caregiver/dependent pair):

Resource Maps to Key fields Programs
household-caregiver-relationships household.caregiverRelationships[] caregiverId, dependentId, isParent, isNonparentParentalControl, providesMostOfCare, adequateChildcareUnavailable, claimedForWorkExemption SNAP

Existing household-scoped resource extended with root-level fields:

Resource Added fields
household-info isMigrantOrSeasonalFarmWorker, expectsShelterCosts, previousSubstantialLotteryWinnings, participatesInCommodityFoodProgram, receivesEnergyAssistance, receivedEmergencyBenefits

Existing application-scoped resource extended with root-level fields:

Resource Added fields
applications benefitMonth, certificationPeriodStartDate, isRecertification, receivedSnapInLast30Days

Sponsorship belongs with immigration details, not as a standalone resource

Decision: Sponsor data is a sub-record on member-immigration-details, not a separate resource.

Rationale: Sponsorship is only relevant for non-citizen members and is inherently tied to immigration status. The rules engine treats sponsor data as a nested object within immigration details. A standalone sponsorship resource would require a join back to the member's immigration record to be meaningful — unnecessary indirection.

No backing state resource needed

Decision: The eligibility context does not need a backing state resource at the baseline.

Rationale: Post-evaluation access uses the stored Decision.memberContext. Whether data has changed since the last rules engine run is tracked via the Determination's evaluation timestamps on the Eligibility side, not via a state resource in Intake. States that need change-detection can overlay a state resource.

Eligibility context fields are opaque objects, not schema mirrors

Decision: Decision.memberContext and Determination.householdContext are typed as type: object with additionalProperties: true — opaque blobs. They are not given schemas that mirror the Intake composition response shape.

Rationale: These fields are audit snapshots — they record whatever the rules engine received at evaluation time, not a live view of Intake data. The exact shape is defined by the rules engine's input model, which is external to the blueprint and varies by state. Duplicating the Intake composition shape in Eligibility's OpenAPI spec would create a maintenance coupling: every change to the composition would require an Eligibility schema update. Salesforce Government Cloud's evaluation objects and IBM Cúram's determination context both store the input payload as a flexible structure, not a typed mirror of the source domain's schema.

Update events suffice for progressive evaluation audit

Decision: Storing memberContext as a single overwritable field on Decision is sufficient for progressive determination. No versioned history or array of snapshots is needed on the record itself.

Rationale: The platform emits ResourceUpdatedEvent automatically on every PATCH, carrying before/after values for all changed fields. Each evaluate pass that writes a new value to memberContext produces a decision.updated event with {field: "memberContext", before: {...}, after: {...}}. The event log preserves the complete evaluation history. The current field value reflects the latest evaluation state; prior states are recoverable from the log. This is equivalent in durability to Cúram's determination context pattern and does not require any additional audit infrastructure.

New resource schemas defined in schemas/common/, not inline in OpenAPI

Decision: New Intake resource schemas introduced by this issue (MemberPregnancy, MemberVeteranStatus, etc.) are defined as JSON Schema objects in schemas/common/intake.yaml. intake-openapi.yaml and state machine files reference them via $ref: ./schemas/common/intake.yaml#/$defs/SchemaName. They are not defined inline in the OpenAPI spec.

Rationale: Resource schemas must be usable by both the OpenAPI spec and state machine transition request/response validation. Inline OpenAPI schemas are not accessible to state machines. The schemas/common/ pattern is already established for shared enums and types (enums.yaml, member.yaml, household.yaml, etc.), and the meta-schema already uses external $ref to schemas/common/enums.yaml. Extending this pattern to resource schemas avoids duplication between the API contract and the state machine validation layer.

Two distinct field path formats: schema key and JSONPath

Decision: The blueprint uses two distinct field path formats for different purposes. They must not be mixed.

Schema key (SchemaName.fieldName, e.g., MemberPregnancy.isPregnant) — identifies a field by its location in a schema. Used for annotation keys, sort/search nested field params, and any context where a field is named by schema location rather than navigated to through data. No array traversal notation.

JSONPath (e.g., members[].pregnancy.isPregnant, members[].income[].amount) — navigates a data structure at runtime. [] marks an array segment being traversed. Used in Decision.missingFields output (stored as returned by the rules engine), the json-path annotation field (see next decision), and x-enum-source values (build-time data path, e.g., slaTypes[].id). Follows JSONPath conventions (RFC 9535).

Rationale: These are two different addressing systems. Schema keys identify where a field lives in a type definition. JSONPath navigates through assembled data. The distinction is not about [] preference — it is about what the path is for. Mixing formats creates ambiguity and breaks lookups. The x-enum-source extension already uses JSONPath format in the codebase; this decision formalizes the convention. Canonical documentation belongs in docs/architecture/cross-cutting/contract-metadata.md.

Decision.missingFields stores raw composition-shape paths

Decision: Decision.missingFields stores the field paths exactly as returned by the rules engine — paths in the composition shape (e.g., members[].pregnancy.isPregnant) using [] notation for array segments. No translation to Intake resource paths happens in the adapter or Eligibility service.

Rationale: The rules engine returns missing field paths relative to the input it received (the eligibility context composition response). Translating those paths to Intake resource + field pointers would require the Eligibility adapter to know Intake's resource structure — a cross-domain coupling that would need updating whenever the composition config changes. Storing raw paths keeps the adapter simple. Human-friendly display (for caseworker UIs) is handled by the field-level annotation system (see next decision), not by a translation layer.

Field-level annotations for eligibility context schemas include a json-path field

Decision: New Intake resource schemas introduced by this issue get corresponding entries in intake-annotations.yaml following the existing annotation format: SchemaName.fieldName: {programs: [...], policies: [...], dataClassification: [...]}. Each entry also includes a json-path field containing the JSONPath address of that field in the assembled eligibility context response (e.g., json-path: members[].pregnancy.isPregnant). The annotation system supports lookup by either the schema key or the json-path value. The json-path field is formally typed as an optional property in annotations-schema.yaml — not left as an unvalidated extension via additionalProperties: true. Annotations remain in the annotations file (under schema:), not inline in the composition config. Authoring json-path values requires knowing the composition's assembled output shape in advance; this is acceptable because the composition has a designed, fixed output structure. Human-friendly labels are NOT part of the baseline — they are a state overlay extension documented in #131.

Rationale: Decision.missingFields stores raw JSONPath strings as returned by the rules engine. Consumers (caseworker UIs, adapters) need to look up annotations for those paths to display program context, policies, and state-provided labels. Because the annotation system is keyed by schema key (MemberPregnancy.isPregnant) and missing fields arrive as JSONPath (members[].pregnancy.isPregnant), a direct lookup would fail without a bridge. The json-path field on each annotation entry provides that bridge: consumers build a reverse index from JSONPath values to schema keys and look up annotations using whichever format they have. The json-path field uses the JSONPath format defined in the previous decision. Supporting both lookup formats keeps the annotation system usable from any context without requiring callers to translate between formats. Keeping annotations in the annotations file (not the composition config) preserves the annotations file as the single source of truth for field metadata across all consumers — the composition config defines structure, the annotations file describes fields.

Required fixes (in scope)

Plain composition must be DRY with the sectionView panel path

The plain composition path (assemblePlainComposition + fetchNodeItems in composition-assembler.js) is missing three capabilities that the sectionView panel path already has. These are not design differences — they are gaps. Both paths must behave identically for include node results except where the response shape genuinely differs (sections/panels vs. flat record).

Capability sectionView panel plain composition
Query param filtering (filterItems) Yes Missing
Sorting (sortItems) Yes Missing
Pagination (paginateItems) Yes MissingfindAll called with limit: null, returns all records

Fix: Thread queryParams and paginationDefaults through assemblePlainComposition from the route handler (req.query). For each include node result set, call filterItemssortItemspaginateItems in that order, mirroring lines 466–469 of the sectionView panel path. findAll inside fetchNodeItems should respect limit from queryParams, not always use limit: null.

Relevant files:

  • packages/mock-server/src/composition-assembler.jsfetchNodeItems and assemblePlainComposition
  • packages/mock-server/src/route-generator.jsassemblePlainComposition call site (~line 614)

#358 — State-machine FK expansion bug

The resolved-spec pipeline expands FK references before generating OpenAPI endpoints and schemas. Transition body schemas containing $ref to components/schemas/* fail AJV compilation because the surrounding namespace is not registered. The eligibility context surfaces the same mismatch for any transition endpoint referencing eligibility-context-related schemas. Must be fixed before the composition endpoint is usable end-to-end.

#359 — Missing expeditedFlagged on Determination

expeditedFlagged is absent from the Determination schema. Any field the rules engine writes back to Determination must be present in the schema. Must be fixed before the composition endpoint is usable end-to-end.

#360collectionToSchemaPrefix non-plural word bug

The route generator's collection-to-schema-prefix conversion fails for non-plural resource names. The new resources introduced by this issue will trigger this bug. Must be fixed before the new resources can be generated correctly.

Expected outputs

Intake contracts and architecture:

  • New Intake resources (each with OpenAPI schema and example data): member-pregnancy, member-veteran-status, member-student-status, member-disability-details, member-living-situation, member-work-requirements, member-immigration-details, member-findings, household-caregiver-relationships, member-tax-info, member-health-coverage, member-tribal-info
  • Extended existing resources: application-members, household-info, applications
  • New resource schemas defined in schemas/common/intake.yaml (not inline in intake-openapi.yaml); intake-openapi.yaml references them via $ref
  • Eligibility context plain composition config in intake-compositions.yaml
  • Field-level annotation entries for all new schemas in intake-annotations.yaml (programs, policies, dataClassification, and json-path per field)
  • annotations-schema.yaml updated to include json-path as a formally-typed optional property on Annotation objects
  • Updated docs/architecture/domains/intake.md — all new and extended resource entities with regulatory context, eligibility context composition, updated decisions

Eligibility contracts:

  • Decision.memberContext field added to eligibility-openapi.yaml — nullable opaque object (type: object, additionalProperties: true) storing the per-member eligibility context at evaluation time
  • Decision.missingFields field added to eligibility-openapi.yaml — array of composition-shape field paths (using [] notation for array segments) returned by the rules engine when it cannot produce a complete evaluation; field structure and Decision status behavior for the incomplete response path are design questions to be settled during Phase 4
  • Determination.householdContext field added to eligibility-openapi.yaml — nullable opaque object storing the household-level eligibility context at evaluation time
  • Eligibility state machine (eligibility-state-machine.yaml) — review whether the evaluate transition needs a step or context binding to call GET /intake/applications/{id}/eligibility-context; design and implement the incomplete rules engine response path (when the rules engine returns missing fields rather than a determination outcome); document changes or explicit "no changes needed" with rationale

Cross-cutting:

  • Updated docs/architecture/cross-cutting/resource-composition.md — any needed additions for the eligibility context pattern
  • Updated docs/architecture/cross-cutting/contract-metadata.md — document the two distinct field path formats (schema key vs. JSONPath), when each applies, the json-path annotation field, and the x-enum-source JSONPath usage

Constraints

  • Intake is the system of record for application data. The composition endpoint lives in Intake; Eligibility stores the response on its own records for audit and post-evaluation access.
  • States must be able to extend the eligibility context with program-specific inputs via overlay.
  • States with IRS Pub. 1075 FTI obligations may overlay the eligibility context to limit which income fields are persisted on Decision.memberContext — document this as a known customization point.
  • Branches off feat/declarative-resource-composition (Declarative resource composition #357), not main.

Definition of done

  • Architecture doc covers all new and extended resource entities with regulatory context, the eligibility context composition config, and the adapter integration pattern.
  • Decision.memberContext, Decision.missingFields, and Determination.householdContext schema shapes are defined in the architecture doc and reflected in eligibility-openapi.yaml.
  • Eligibility state machine review is documented — either changes are included or "no changes needed" is explicitly stated with rationale. The incomplete rules engine response path (missing fields) is designed and implemented.
  • Field-level annotations are defined for all new schemas in intake-annotations.yaml.
  • Two distinct field path formats (schema key and JSONPath) are documented in contract-metadata.md.
  • The cross-cutting doc reflects any needed additions for the eligibility context pattern.
  • Plain composition include node results support filtering, sorting, and pagination — parity with the sectionView panel path, covered by unit tests.
  • The eligibility context composition endpoint is covered by an integration test.

Implementation phases

Phase 0 — Required fixes (unblocking)

Fix the four prerequisite bugs before any new contract work can be validated end-to-end:

  1. Plain composition DRY gap — thread queryParams + paginationDefaults through assemblePlainComposition; add filterItemssortItemspaginateItems on include node results; fix findAll limit: null.
  2. state-machine: Decision/Determination create POSTs use flat FK names; resolved spec expands them (style: expand) #358 — State-machine FK expansion — register components/schemas/* namespace with AJV before compiling individual transition body schemas.
  3. state-machine: Determination create omits required expeditedFlagged; Zod rejects every list/get response #359 — Missing expeditedFlagged — add field to Determination schema.
  4. Fix collectionToSchemaPrefix stripping trailing 's' from non-plural words #360collectionToSchemaPrefix non-plural bug — fix route generator so non-plural resource names are handled correctly.

Phase 1 — New Intake resources

For each new resource listed in the settled decisions:

  • Define the JSON Schema object in schemas/common/intake.yaml with all required fields, types, and nullable: true on optional fields per Optional fields in response schemas should be nullable #369.
  • Add the corresponding resource entry in intake-openapi.yaml referencing the schema via $ref: ./schemas/common/intake.yaml#/$defs/SchemaName.
  • Add annotation entries for the new schema's fields in intake-annotations.yaml (programs, policies, dataClassification, and json-path for the JSONPath address in the eligibility context response).
  • Add example data in intake-openapi-examples.yaml covering at least one record per new resource keyed to the seeded application.
  • Verify npm run validate passes after each resource addition.

Phase 2 — Extend existing resources

Add the new fields to application-members, household-info, and applications schemas:

  • Mark all new fields nullable: true (optional at intake time).
  • Add annotation entries for the new fields.
  • Add example values to existing example records.
  • Verify npm run validate passes.

Phase 3 — Eligibility context composition

  • Author the eligibility-context plain composition config in intake-compositions.yaml. The config assembles: all new member-scoped resources (joined by memberId), extended application-members fields, member-incomes, member-expenses, member-assets, member-employment-records, household-info (extended fields), household-caregiver-relationships, and application-level fields.
  • Verify the mock server generates GET /intake/applications/{id}/eligibility-context and returns a well-formed response.
  • Verify ?memberId=abc filters the members[] array to a single member (requires Phase 0 plain composition fix).

Phase 4 — Eligibility contracts

  • Add Decision.memberContext (nullable opaque object), Decision.missingFields (array of composition-shape field paths), and Determination.householdContext (nullable opaque object) to eligibility-openapi.yaml.
  • Design the missingFields field structure: confirm string[] of []-notation field paths is sufficient, or determine whether a richer object (with field path, program, and message) better serves caseworker workflows. Settle the Decision status behavior when the rules engine returns missing fields rather than a determination outcome (new status vs. remain in pending).
  • Review eligibility-state-machine.yaml evaluate transition: determine whether a callAdapter step or context binding is needed to fetch GET /intake/applications/{id}/eligibility-context and bind the result before the rules engine call. Design and implement the incomplete response path. Document outcome (change or explicit "no changes needed" with rationale).

Phase 5 — Documentation

  • Update docs/architecture/domains/intake.md: add entity descriptions with regulatory context for each new and extended resource; document the eligibility context composition config; record any updated decisions.
  • Update docs/architecture/cross-cutting/resource-composition.md: add the eligibility context as an example of the plain composition pattern; document IRS Pub. 1075 FTI overlay as a known customization point for income fields on Decision.memberContext.
  • Update docs/architecture/cross-cutting/contract-metadata.md: document the two distinct field path formats (schema key vs. JSONPath), when each applies, the json-path annotation bridge field, and the x-enum-source JSONPath usage.

Validation steps

Start the mock server (npm run mock:start) after completing Phase 3+. All steps use the seeded application id from intake-openapi-examples.yaml.

  1. Eligibility context — full household: GET /intake/applications/{id}/eligibility-context returns 200 with members[] containing one entry per seeded member, each with pregnancy, veteranStatus, studentStatus, disabilityDetails, livingSituation, workRequirements, immigrationDetails, findings, taxInfo, healthCoverage, tribalInfo, income[], expenses[], assets[], employment[] sub-objects. household contains caregiverRelationships[], extended household-info fields, and application-level fields.

  2. Eligibility context — single member filter: GET /intake/applications/{id}/eligibility-context?memberId={memberId} returns 200 with members[] containing exactly one entry.

  3. Eligibility context — no member match: GET /intake/applications/{id}/eligibility-context?memberId=nonexistent returns 200 with members[] empty.

  4. New resource CRUD: POST /intake/applications/{id}/members/{memberId}/member-pregnancy creates a record and GET returns it. Repeat for at least two other new resources.

  5. Schema validation: npm run validate passes with all new schemas, extended fields, and annotation entries in place.

  6. Integration test: The eligibility-context composition endpoint integration test passes (npm run test:integration).

  7. Unit tests: Plain composition filtering/sorting/pagination unit tests pass (npm test).

  8. Eligibility state machine review: Either the evaluate transition has been updated to fetch and bind the eligibility context, or the architecture doc explicitly states "no changes needed" with rationale. The incomplete rules engine response path is implemented and tested.

Dependencies / Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    cross-cuttingConcerns that span multiple domains or the platform layerdesignArchitectural design and documentationintakeIntake domain — application submission, eligibility screening, and household data

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions