Skip to content

Salvage the Nightingale governance layer from #92, and stop identity collisions recurring - #111

Merged
sudoshi merged 4 commits into
mainfrom
feat/nightingale-salvage-from-92
Jul 28, 2026
Merged

sudoshi merged 4 commits into
mainfrom
feat/nightingale-salvage-from-92

Conversation

@sudoshi

@sudoshi sudoshi commented Jul 27, 2026

Copy link
Copy Markdown
Owner

#92 built an independent Nightingale product foundation. It was superseded by
#101, which renamed the existing patient app in place — that app was already
feature-complete and is what ships to TestFlight today. #92's app scaffolds are
therefore not adopted: 19 iOS files with no features and no networking, so
taking them would have replaced a working patient application with a launch
screen.

Its backend governance layer, documentation, and privacy work are a different
matter, and are salvaged here.

Activation and disclosure gates

20 pure classes plus tests. The activation gate evaluates five independent,
conjunctive
preconditions — clinical approval, content release, feature
activation, pilot enrollment, source connector — any one of which holds the
whole evaluation. No route, container binding, config reader, source query,
audit writer, or native caller.

This models exactly the separation that was missing when tracing why no
projection reaches a patient: clinical approval and content release as separate
conditions rather than a single flag.

A config collision this avoids

#92 adds a 44-line config/nightingale.php declaring a foundation namespace.
main already has a 271-line config/nightingale.php — the live file the
deployed backend reads for 48 environment variables. Merging #92 as-is would
have had one silently replace the other. The foundation keys are merged into
the existing file under a marked section instead; config:show confirms every
serving value is unchanged.

Privacy manifest, corrected

Neither shipped iOS app had a PrivacyInfo.xcprivacy. Apple requires one for
App Store submission. #92's declared an empty NSPrivacyCollectedDataTypes —
true of a foundation with no networking, false for an app that authenticates
and exchanges clinical content. Copying it verbatim would have filed an
inaccurate declaration, which is worse than filing none.

Verified against source: UserDefaults is the only required-reason API
category in the target, so CA92.1 alone is correct, and NSPrivacyTracking
false is already enforced by check-no-tracking-technologies.sh. The
collected-data entries are deliberately conservative and carry an in-file note
that they need product and privacy sign-off before submission.

63 governance documents

Threat and hazard model, privacy release controls, accessibility layout and
language-readiness matrices, dependency inventory, contract ownership matrix,
identity recovery and protected-state decisions, background rights review,
controlled pilot manifest, and the product identity checklist.

Nine cite nightingale/iosApp paths that were never adopted. Rather than edit
sixty-three documents or drop the nine, the README carries a substitution table
and states which parts of #92 were salvaged and which were not.

The guard that should have existed

scripts/ci/verify-app-identity-uniqueness.sh fails CI when two applications
declare the same identifier within a platform. Per-platform, not global — one
product legitimately declares the same identifier once for iOS and once for
Android.

Verified both directions: passes on the current tree (7 iOS, 2 Android), and
exits 1 naming all four collisions against a reconstruction of the
#92-plus-#101 merged tree. That is the failure that should have happened in
July, before either shipped.

Verification

  • 23 salvaged unit tests pass (149 assertions)
  • Patient suite still passes 301 tests (4,290 assertions)
  • xcodegen bundles the privacy manifest; xcode-project, patient-boundary, and
    tracking-technology guards pass
  • config:show nightingale confirms live values unchanged

Not carried across

nightingale/iosApp, nightingale/androidApp, and every deletion of
hummingbird/…PatientApp.

sudoshi and others added 4 commits July 27, 2026 21:03
PR #92 built an independent Nightingale product foundation, then fell behind
main when the patient product was renamed in place instead (#101). The app tree
it added is a scaffold with no features or networking, so it is superseded — but
its backend governance layer is not, and is worth keeping.

Ports 20 pure classes plus their test:

  App\Nightingale\Activation    a conjunctive gate over five independent
                                preconditions — clinical approval, content
                                release, feature activation, pilot enrollment,
                                source connector — any one of which holds the
                                whole evaluation
  App\Nightingale\Disclosure    a generic non-disclosure gate over relationship,
                                encounter binding, and resource state
  App\Nightingale\EncounterAccess, Identity, Inpatient
                                precondition gates and unconfigured-by-default
                                boundaries

These are pure: no route, container binding, config reader, source query, audit
writer, or native caller. They model exactly the separation that was missing
when tracing why no projection reaches a patient — clinical approval and
content release as independent conditions rather than one flag.

Note the config collision this avoids. #92 adds a 44-line config/nightingale.php
declaring a foundation namespace; main already has a 271-line
config/nightingale.php, the live file the deployed backend reads for 48
environment variables. Merging #92 as-is would have had one silently replace the
other. The foundation keys are merged into the existing file under a clearly
marked section instead, leaving every serving value untouched.

Verified: 23 salvaged tests pass (149 assertions); the patient suite still
passes 301 tests (4,290 assertions); config:show confirms the live values are
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neither shipped iOS app carries a PrivacyInfo.xcprivacy. Apple requires a
required-reason API manifest for App Store submission; TestFlight tolerates its
absence, the store does not. PR #92 had written one, so this salvages it — with
a correction.

That version declared an empty NSPrivacyCollectedDataTypes. True of its
foundation build, which had no networking at all; false here, where the app
authenticates and exchanges clinical content. Copying it verbatim would have
filed an inaccurate declaration with Apple, which is worse than filing none.

Verified against the source rather than assumed:
  - UserDefaults is the only required-reason API category used
    (Networking/PatientSecureStore.swift). No FileManager timestamp, disk
    space, boot time, or active-keyboard API appears anywhere in the target,
    so CA92.1 alone is correct.
  - NSPrivacyTracking false is accurate; scripts/check-no-tracking-technologies.sh
    already enforces the absence of tracking SDKs in CI.

The collected-data entries reflect what the client demonstrably moves: a
patient context identifier and bearer credentials, released clinical
projections, and patient-authored message content. Whether each counts as
"collected" also depends on backend retention, so they are deliberately
conservative and carry an in-file note that they need product and privacy
sign-off before submission. Over-declaring is safe; under-declaring is a
misrepresentation.

xcodegen bundles it into the target (4 references in the generated project);
the xcode-project, patient-boundary, and tracking-technology guards pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 documents: threat and hazard model, privacy release controls, accessibility
layout and language-readiness matrices, foundation dependency inventory,
contract ownership and authorization matrix, identity recovery and
protected-state decisions, background asset governance and rights review, the
controlled pilot manifest, and the product identity and support naming
checklist — plus the release evidence under docs/evidence.

These are the decision records that make a patient-facing clinical product
defensible under review, and they survive the architecture change that
superseded the PR they came from.

Nine of them cite nightingale/iosApp and nightingale/androidApp paths that were
never adopted. Rather than edit sixty-three documents or drop the nine, the
README carries a substitution table and states plainly which parts of #92 were
salvaged and which were not, so a reader is not misled about where the code
actually lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On 2026-07-27 two independent efforts each declared net.acumenus.nightingale:
#101 renamed the existing patient app in place, #92 built a new foundation
tree. Both were internally consistent. Nothing compared them, and the merge
produced zero conflicts — git cannot see that two project.yml files claim the
same App Store record. The collision was found by reading diffs by hand, after
one of them had already shipped to TestFlight.

Enforces per-platform uniqueness rather than global uniqueness: one product
legitimately declares the same identifier once for iOS and once for Android.
What must never happen is the same identifier twice within a platform, because
that is two applications competing for one store listing.

Verified both directions. Against the current tree it passes, reporting 7 iOS
bundle identifiers and 2 Android applicationIds. Against a reconstruction of
the #92-plus-#101 merged tree it exits 1 and names all four collisions with the
declaring files, which is the failure that should have happened in July.

Runs alongside the existing transport-security and tracking-technology guards
in the Backend quality job.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@sudoshi
sudoshi force-pushed the feat/nightingale-salvage-from-92 branch from 4fbecbd to 9baa372 Compare July 28, 2026 01:03
@sudoshi
sudoshi merged commit e6124b2 into main Jul 28, 2026
19 checks passed
@sudoshi
sudoshi deleted the feat/nightingale-salvage-from-92 branch July 28, 2026 01:13
sudoshi added a commit that referenced this pull request Jul 28, 2026
…ted) (#119)

* feat(flow4d): bounded pathway-serving machinery (build; activation gated)

The reusable machinery to serve REAL (non-overlay) governed-pathway progress in
the 4D Navigator for the Nightingale investor-demo cohort (PR #118). Build only —
no prod writes; catalog activation + assignment remain gated behind [SU] signoff.

- Flow4dPathwayAssignmentService: layers a pathway_instance + LOS-driven milestone
  statuses onto an existing #118 cohort grant (source_encounter_id + pathway:read);
  requires the catalog already activated (instantiate enforces approved+active +
  release signed-off), so it fails loudly otherwise.
- care-pathways:promote-executable-layer: the missing draft→approved milestone/stage
  promotion (scope REQUIRED, --confirm REQUIRED; never promotes the whole catalog).
  review_state is not a protected content column, so the immutability trigger allows it.
- flow4d:assign-demo-pathways: reads the cohort SSOT (NightingaleDemoCohort::MEMBERS
  demo_username→pathway_key) + its grants and assigns each patient their pathway.
- Flow4dPathwayServingTest: proves the full loop end-to-end in the isolated test DB —
  adopt → author(draft) → promote → activate → cohort grant → assign → serve real
  progress (PathwayInstanceReadService, demo=false). This IS the prod runbook, proven.

Scoped to the 5 cohort pathways (all evidence-verified). 5 tests green; Pint clean.
Note: PatientJourneyEndpointTest event-count assertion is a pre-existing LOCAL-only
failure from #111's identity changes (main CI is green) — unrelated to this branch.

* fix(test): anchor PatientJourneyEndpointTest events to now (calendar bit-rot)

The journey seeds hardcoded 2026-07-25 events, but the journey window is now-72h;
once the calendar advanced >72h past those dates the events aged out of range and
the ordered-story assertion dropped from 3 to 1. Anchors the seed to now (gaps
preserved: 210 + 525 min) so it's stable. Unrelated to the serving machinery;
unblocks feature-1 CI (and fixes the same latent failure on main).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant