MVP-1 ① Personas & Authorization — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the status-aware owner/role/kind capability matrix (enforced in SDK service AND Postgres RLS — the dual gate), the append-only consent substrate (adult ToS + child VPC with the child-PII write gate, online + offline), the supervised-action path over the built approval policy, and the authz observability events.
Architecture: A pure Authorizer resolves capabilitiesFor(member) (union over kind/roles/owner, then filtered by status + expiry + parental-kind gate); every SDK service mutation opens with authorizer.require(cap, target), and each capability is mirrored by an RLS SECURITY DEFINER helper + policy. Consent gates child-PII persistence in service and schema, online and offline. Supervised action reuses the existing single token-moving path.
Tech Stack: Dart 3.9 / Flutter 3.44 (FVM: fvm flutter / fvm dart), pub workspace; packages/client_sdk (pure Dart, package:supabase core), Drift (local), Supabase Postgres + RLS (cloud, project bgedvvmihygwxhjxlvfu); app blocs + auto_route.
Design source of truth: app/test-gallery/authored/developer/architecture/authorization.md (capability matrix §2, consent §3, supervised §4, events §5, coverage §7). Spec: docs/superpowers/specs/2026-06-25-mvp1-personas-authz-foundation-design.md.
Global Constraints
client_sdkstays pure Dart —package:supabasecore only, noflutter_*dep.- No service-role key — anon/publishable key only (
sb_publishable_…), RLS-enforced; reuse the SP2SupabaseClient; privileged ops via Edge Functions only. - Dual gate — every capability + the consent gate enforced in service AND RLS, in the same task, with an RLS-parity test.
- Household-scoped — zero cross-household access; helpers keyed to
auth.uid(). - No child PII in events — opaque household-scoped IDs only; tag internal-BI; pass the pre-emit scrub.
- Append-only
consents;Member.consentStateis a projection; consent event survives child-PII erasure (opaque ref). - Offline parity — consent gate holds against the local Drift store.
- Tests: async-throw uses
await expectLater(fut, throwsA(isA<X>())); TRUE exit codes (cmd > /tmp/t.txt 2>&1; echo EXIT=$?), never pipe test runs totail;build_runnerscoped frompackages/client_sdk(--build-filter) + restore clobbered.g.dart/.gr.dartsiblings from HEAD; suite count ≥ baseline; verify new columns via Supabase MCPinformation_schema. - Git: no destructive ops, explicit
git add(never bare-A), never delete*_test.dart. - graphify-first before touching code (
graphify query "<q>");graphify update .after. - Leverage already-built (do NOT rebuild):
MemberKind/isParental,MemberRole,MemberStatus, ledger + zero-floor,ApprovalPolicy/autoApproveCompletion/resolvedBy. - Review routing per phase: flutter-reviewer (Dart), database-reviewer (RLS/migration), security-reviewer (consent gate/secrets/dual-gate).
Baseline first (do before Task 1)
Record the green baseline so "suite ≥ baseline" is checkable:
cd packages/client_sdk && fvm dart test > /tmp/baseline_sdk.txt 2>&1; echo EXIT=$? and cd app && fvm flutter test > /tmp/baseline_app.txt 2>&1; echo EXIT=$?. Note both pass-counts in the progress ledger.
File structure
| File | Responsibility |
|---|---|
packages/client_sdk/lib/src/models/capability.dart | Capability enum (the authz verbs) |
packages/client_sdk/lib/src/models/consent_state.dart · consent_method.dart | consent enums |
packages/client_sdk/lib/src/models/consent_record.dart · terms_acceptance.dart · member_access.dart | consent + access value types |
packages/client_sdk/lib/src/services/authorizer.dart | pure capability resolution + require |
packages/client_sdk/lib/src/services/consent_service.dart | consent capture/gate/lifecycle |
…/models/household_member.dart (modify) | + owner, + consentState, + consentRef |
…/models/exceptions.dart (modify) | new domain exceptions |
…/services/household_service.dart (modify) | owner/role mutations, guards, removeMember, bootstrap |
…/services/approval_service.dart (modify) | actingMemberId supervised extension |
…/adapters/local/* · …/adapters/cloud/* (modify) | persist the new columns/tables |
infra/supabase/migrations/2026…*.sql | columns, tables, helpers, RLS, triggers |
app/lib/** (repos, blocs, pages, guards) | role/owner, consent, supervised UX, route guards |
Phase 1 — Pure Authorizer (no I/O)
Task 1: Capability enum + consent enums
Files:
- Create:
packages/client_sdk/lib/src/models/capability.dart,consent_state.dart,consent_method.dart - Test:
packages/client_sdk/test/models/capability_test.dart
Interfaces — Produces:
-
enum Capability { manageBilling, deleteHousehold, manageOwners, removeMember, manageRoles, manageHousehold, manageCatalog, manageRoutines, manageBudget, inviteMember, manageMemberProfile, manageNotificationPrefs, printList, captureConsent, revokeConsent, eraseChildData, approveCompletion, approveSpendRequest, approveGoalRequest, superviseAction, viewHouseholdAll, createFamilyGoal, completeGoal, editOwnProfile, editOwnPreferences, submitChore, viewOwnWallet, viewSharedFamilyGoals, createOwnGoal, editOwnGoal, archiveGoal, requestGoal, redeemReward, moveOwnFunds, manageOwnEnvelopes, adjustOwnRunEstimate } -
enum ConsentState { none, pending, granted, revoked }withfromWireName/wireName(mirror the pattern inmodels/member_status.dart). -
enum ConsentMethod { emailPlus, card }with wire namesemail_plus/card. -
Step 1 — failing test
capability_test.dart:
import 'package:client_sdk/src/models/consent_state.dart';
import 'package:test/test.dart';
void main() {
test('ConsentState round-trips wire names', () {
expect(ConsentState.granted.wireName, 'granted');
expect(ConsentState.fromWireName('revoked'), ConsentState.revoked);
});
test('fromWireName rejects unknown', () {
expect(() => ConsentState.fromWireName('bogus'), throwsArgumentError);
});
}
- Step 2 — run, expect FAIL (
fvm dart test test/models/capability_test.dart): missing file. - Step 3 — implement the three enums. Mirror the existing wire-name pattern in
member_status.dart(read it first via graphify:graphify explain "MemberStatus").Capabilityneeds no wire name (never persisted). - Step 4 — run, expect PASS.
- Step 5 — commit:
git add packages/client_sdk/lib/src/models/capability.dart packages/client_sdk/lib/src/models/consent_state.dart packages/client_sdk/lib/src/models/consent_method.dart packages/client_sdk/test/models/capability_test.dart && git commit -m "feat(sdk): Capability + consent enums"
Task 2: MemberAccess + the Authorizer
Files:
- Create:
packages/client_sdk/lib/src/models/member_access.dart,packages/client_sdk/lib/src/services/authorizer.dart - Test:
packages/client_sdk/test/services/authorizer_test.dart
Interfaces — Consumes: Capability (T1); existing HouseholdMember (fields kind, roles, status; isParental on kind), MemberStatus. Produces:
class MemberAccess { final String memberId; final DateTime? expiresAt; const MemberAccess({required this.memberId, this.expiresAt}); }class Authorizer { Set<Capability> capabilitiesFor(HouseholdMember m, {MemberAccess? access, DateTime? now}); bool can(HouseholdMember m, Capability c, {MemberAccess? access, DateTime? now}); void require(HouseholdMember m, Capability c, {MemberAccess? access, DateTime? now}); }—requirethrowsAuthorizationFailure(T3).
Resolution rules (from design §2): union over kind+roles+owner; then if status != active → empty; if access?.expiresAt is before now → empty. Parental-kind gate is intrinsic: captureConsent/revokeConsent/eraseChildData/superviseAction/approveSpendRequest/approveGoalRequest require m.kind.isParental. owner flag grants {manageBilling, deleteHousehold, manageOwners}. admin grants the config set; helper grants only {approveCompletion, viewHouseholdAll}; every member self-row grants {submitChore, viewOwnWallet, viewSharedFamilyGoals, editOwnPreferences, adjustOwnRunEstimate, editOwnProfile(adult)}; consented-member self grants {createOwnGoal, editOwnGoal, archiveGoal, requestGoal, redeemReward, moveOwnFunds, manageOwnEnvelopes}.
- Step 1 — failing tests (real matrix coverage):
// helper may approve completions but NOT spend/goal
test('helper is approve-only, completed-jobs-only', () {
final m = member(roles: {MemberRole.helper}, status: MemberStatus.active);
expect(auth.can(m, Capability.approveCompletion), isTrue);
expect(auth.can(m, Capability.approveSpendRequest), isFalse);
expect(auth.can(m, Capability.approveGoalRequest), isFalse);
expect(auth.can(m, Capability.manageCatalog), isFalse);
});
test('non-active member holds nothing', () {
final m = member(roles: {MemberRole.admin}, status: MemberStatus.invited);
expect(auth.capabilitiesFor(m), isEmpty);
});
test('expired access revokes all capabilities', () {
final m = member(roles: {MemberRole.helper}, status: MemberStatus.active);
final past = DateTime.utc(2020); final now = DateTime.utc(2026);
expect(auth.capabilitiesFor(m, access: MemberAccess(memberId: m.id, expiresAt: past), now: now), isEmpty);
});
test('owner caps require parental and owner flag', () { /* owner+parental → manageBilling true; child → false */ });
test('require throws AuthorizationFailure when denied', () {
expect(() => auth.require(member(status: MemberStatus.active), Capability.manageBilling), throwsA(isA<AuthorizationFailure>()));
});
(Provide a member({roles, kind, owner, status}) test factory at the top — mirror test/support/ factories; read graphify explain "HouseholdMember" for the exact constructor.)
- Step 2 — run, expect FAIL.
- Step 3 — implement
MemberAccess+Authorizerper the rules above. Pure: noawait, no I/O. - Step 4 — run, expect PASS — all matrix rows.
- Step 5 — commit.
Task 3: domain exceptions
Files: Modify packages/client_sdk/lib/src/models/exceptions.dart; Test packages/client_sdk/test/models/authz_exceptions_test.dart.
Produces: AuthorizationFailure, LastOwnerCannotDeleteWithMembers, LastAdminCannotDemote, OwnerMustBeParental, ConsentRequired, TermsAcceptanceRequired, MemberAccessExpired — each extending the existing DomainRuleException base (read graphify explain "DomainRuleException" for the base shape; mirror InsufficientBalanceException). Each carries a non-PII message.
- Step 1 — failing test: assert each is a
DomainRuleExceptionand itsmessagecontains no member name/email (only ids/role words). - Step 2 — FAIL. Step 3 — implement (mirror existing exception classes). Step 4 — PASS. Step 5 — commit.
Phase 1 review: flutter-reviewer. Gate: matrix coverage complete, pure (no I/O), exceptions PII-free.
Phase 2 — Schema + RLS (the schema half of the dual gate)
Migrations are additive-only (offline-first window). Mirror the existing style in infra/supabase/migrations/20260612000003_auto_approve_policy.sql (header comment explaining the invariant + the SQL-twin note) and the RLS helper parental_household_ids() (find it: graphify query "parental_household_ids RLS household policy" or grep the migrations dir). Use a timestamp prefix after the latest existing migration. After each migration: apply via Supabase MCP apply_migration to bgedvvmihygwxhjxlvfu, then verify columns via execute_sql against information_schema.columns.
Task 4: household_members columns + owner⇒parental trigger
Files: Create infra/supabase/migrations/<ts>_authz_owner_consent_columns.sql; Test (parity) packages/client_sdk/test/cloud/authz_schema_test.dart (guarded by the live-creds flag, mirror test/cloud/supabase_chores_test.dart).
SQL:
alter table public.household_members
add column owner boolean not null default false,
add column consent_state text not null default 'none'
check (consent_state in ('none','pending','granted','revoked')),
add column consent_ref uuid; -- FK added in Task 5 once consents exists
-- owner must be a parental member (parent | co_parent). SQL twin of Authorizer.
create or replace function public.assert_owner_is_parental() returns trigger
language plpgsql as $$
begin
if new.owner and new.kind not in ('parent','co_parent') then
raise exception 'OwnerMustBeParental';
end if;
return new;
end $$;
create trigger trg_owner_is_parental before insert or update on public.household_members
for each row execute function public.assert_owner_is_parental();
- Steps: write the migration →
apply_migration→ verify both columns present + the check viainformation_schema(MCPexecute_sql) → parity test asserts a non-parental owner insert is rejected → commit (git addthe migration + test).
Task 5: consents, terms_acceptances, member_access tables + the consent_ref FK
SQL (append-only consents; member_ref opaque so it survives erasure):
create table public.consents (
id uuid primary key default gen_random_uuid(),
household_id uuid not null references public.households(id),
member_ref uuid, -- nullable after erasure (tombstoned)
state text not null check (state in ('none','pending','granted','revoked')),
method text check (method in ('email_plus','card')),
tos_version text not null, privacy_version text not null,
verified_at timestamptz, deadline_at timestamptz,
created_at timestamptz not null default now(),
actor_account_id uuid not null
);
create table public.terms_acceptances (
account_id uuid not null, tos_version text not null, privacy_version text not null,
accepted_at timestamptz not null default now(),
primary key (account_id, tos_version, privacy_version)
);
create table public.member_access (
member_id uuid primary key references public.household_members(id),
expires_at timestamptz
);
alter table public.household_members
add constraint household_members_consent_ref_fkey
foreign key (consent_ref) references public.consents(id);
- Append-only enforced via RLS (no UPDATE/DELETE policy) in Task 7. Steps: migration → apply → verify tables/FK via MCP → parity test (insert consent, link member) → commit.
Task 6: role helper functions
SQL (mirror parental_household_ids() exactly — same security definer, set search_path, auth.uid() keying):
create or replace function public.admin_household_ids() returns setof uuid
language sql security definer set search_path = public stable as $$
select household_id from public.household_members
where auth_user_id = auth.uid() and status = 'active'
and 'admin' = any(roles) and kind in ('parent','co_parent') $$;
-- helper_household_ids(): same but 'helper' = any(roles); owner_household_ids(): owner = true and kind parental.
- Steps: write all three → apply → verify via MCP (
selectreturns the caller's households) → commit.
Task 7: RLS policies per surface + guard triggers
- Policies: owner-only surfaces keyed to
owner_household_ids(); admin config surfaces toadmin_household_ids(); approval surfaces toadmin ∪ helper; spend/goal approval toadminonly;consents/terms_acceptancesinsert-only (no update/delete → append-only). - Triggers (SQL twins of the service guards): zero-last-owner + zero-last-admin (raise on delete/demote of the last one in a household with other members); child-PII write gate (reject insert/update of a
kind='child'member's profile/wallet/goal rows unlessconsent_state='granted'with a non-nullconsent_ref); status/expiry (deny if member notactiveormember_access.expires_at < now()). - Test (RLS-parity): with two seeded households + an anon-key client, assert cross-household read returns 0 rows and a helper cannot insert a spend approval; assert a child-PII write without consent raises. Steps: migration → apply → MCP verify policies exist (
pg_policies) → parity test → commit.
Phase 2 review: database-reviewer + security-reviewer. Gate: additive-only, no service-role anywhere, helpers mirror
parental_household_ids(), append-only holds, every column verified viainformation_schema.
Phase 3 — Service guards + HouseholdService
Task 8: HouseholdMember + adapters carry the new fields
Files: Modify household_member.dart (+ bool owner, + ConsentState consentState, + String? consentRef; update ctor, copyWith, props, JSON), the local Drift mapper, the cloud row-mapper. Test test/models_round_trip_test.dart (extend) + test/local_storage_adapter_test.dart.
Interfaces — Produces: the extended HouseholdMember. Consumes: ConsentState (T1).
- Step 1 — failing test: round-trip a member with
owner: true, consentState: granted, consentRef: <uuid>through camelCase↔snake_case mappers and Drift. Useawait expectLateronly where async. Run codegen scoped:cd packages/client_sdk && fvm dart run build_runner build --build-filter 'lib/src/models/household_member.g.dart' --build-filter 'lib/src/adapters/local/local_database.g.dart'then restore any clobbered sibling.g.dartfrom HEAD. - Steps 2–5 as standard. Commit includes regenerated
.g.dart.
Task 9: owner/role mutations + zero-last guards + removeMember
Files: Modify household_service.dart; Test test/services/household_service_authz_test.dart.
Interfaces — Produces: Future<void> setRole(...), Future<void> grantOwner(memberId), Future<void> revokeOwner(memberId), Future<void> transferOwnership(from, to), Future<void> removeMember(memberId) — each calls authorizer.require(...) first and enforces the zero-last invariants (throws LastOwnerCannotDeleteWithMembers / LastAdminCannotDemote).
- Step 1 — failing tests: revoking the last owner of a multi-member household throws
LastOwnerCannotDeleteWithMembers; a non-owner callinggrantOwnerthrowsAuthorizationFailure;transferOwnershipleaves ≥1 owner.await expectLater(svc.revokeOwner(last), throwsA(isA<LastOwnerCannotDeleteWithMembers>())). - Steps 2–5. Mirror the guard style of the existing ledger zero-floor service check.
Task 10: createHousehold bootstrap grant
Files: Modify household_service.dart; Test same file.
Produces: createHousehold grants the creator owner=true + role=admin atomically with the household row (the one path that precedes a member record — an explicit, audited exception to require).
- Step 1 — failing test: after
createHousehold, the creator member isowner && roles.contains(admin) && status==active. Steps 2–5.
Task 11: wire require(...) across existing mutating methods
Files: Modify the mutating methods in household_service.dart, chore_service.dart, economy_service.dart, approval_service.dart (and the facade) to open with authorizer.require(actor, <capability>). Map each method → capability per §2.
- Step 1 — failing tests: a
member-kind actor callingcreateChorethrowsAuthorizationFailure; ahelpercallingapproveSpendRequestthrows. (One representative test per capability class.) Steps 2–5. RLS-parity: confirm the matching policy from Phase 2 denies the same op.
Phase 3 review: flutter-reviewer + security-reviewer. Gate: every mutation guarded, zero-last holds in service AND (Phase 2) schema, bootstrap audited.
Phase 4 — ConsentService
Task 12: ConsentRecord + TermsAcceptance models + adapters
Files: Create consent_record.dart, terms_acceptance.dart; modify adapters + Drift schema for the consents/terms_acceptances/member_access tables. Test round-trip.
- Standard model+mapper TDD (mirror Task 8). Commit regenerated
.g.dart(scoped + restore siblings).
Task 13: capture + child-PII write gate (online)
Files: Create consent_service.dart; modify the facade to expose it. Test test/services/consent_service_test.dart.
Produces: Future<ConsentRecord> captureConsent({memberId, method, tosVersion, privacyVersion}) (writes pending→granted appropriately, requires captureConsent capability), Future<void> revokeConsent(memberId) (→ revoked + eraseChildData cascade), and the gate: any child-PII write path asserts consentState==granted else throws ConsentRequired.
- Step 1 — failing tests: writing a child goal with
consentState==nonethrowsConsentRequired; aftercaptureConsent→granted it succeeds;revokeConsentflips torevokedand re-blocks.await expectLater(...). - Steps 2–5. RLS-parity: the Phase 2 child-PII trigger rejects the same direct write.
Task 14: offline Drift gate + revocation tombstone
Files: Modify consent_service.dart + the local adapter. Test test/services/consent_offline_test.dart.
Produces: the gate reads local Drift consent_state before any local child-PII write; a revoke while offline tombstones queued child writes (marks them dropped) and reconciles on sync.
- Step 1 — failing tests: with the in-memory/Drift local store and no network, a child-PII write under
nonethrowsConsentRequired; revoke-offline tombstones a pending child write. Steps 2–5.
Task 15: adult ToS + consent lifecycle
Files: Modify consent_service.dart. Test test/services/consent_lifecycle_test.dart.
Produces: Future<void> acceptTerms({tosVersion, privacyVersion}), bool termsAcceptanceRequired(account), Future<void> checkConsentCurrency(member, currentVersions) (→ granted→pending on a material change; sets deadlineAt — the mechanism; the 14-day/85% numbers are ⚖️ deferred), post-erasure retention (on member delete, null member_ref on the consent row but keep the event), and shadow-member ToS ordering (admin accepts at creation; shadow accepts own on activation).
- Step 1 — failing tests:
termsAcceptanceRequiredtrue untilacceptTermswith the current version;checkConsentCurrencyflips a stalegranted→pending; deleting a child nullsmember_refbut the consent row remains. Steps 2–5.
Phase 4 review: security-reviewer + database-reviewer. Gate: gate holds online AND offline, append-only retained, no child PII leaked on erasure.
Phase 5 — Supervised action
Task 16: actingMemberId on the approval path
Files: Modify approval_service.dart (+ approval.dart/chore_submission.dart if they need actingMemberId/principalAccountId). Test test/services/supervised_action_test.dart.
Produces: submit/approve carry actingMemberId (child) ≠ principalAccountId (parent); superviseAction required of the approving parent; reuses the built autoApproveCompletion (auto) and the manual queue — no new earn path. Earn routes by consent state (own wallet if granted, else household envelope — the household-envelope credit itself is ②; here assert the routing decision/exception, not the envelope write).
- Step 1 — failing tests: a supervised submit on a manual chore needs a parent approve carrying
approvedByMemberId; on an auto chore it resolves viaautoApproveCompletionwithresolvedBy=auto_policyand no per-action tap; a non-parental actor callingsuperviseActionthrows. Steps 2–5.
Phase 5 review: flutter-reviewer. Gate: single token-moving path preserved (no bypass); manual + auto both covered.
Phase 6 — Authz observability
Task 17: the five authz events
Files: Create packages/client_sdk/lib/src/services/authz_events.dart (emit through the existing analytics seam — find it: graphify query "analytics telemetry event emit bucket"); wire emit points in Authorizer.require, ConsentService, ApprovalService. Test test/services/authz_events_test.dart.
Produces: authz_decision, authz_escalation_blocked, consent_state_changed, child_pii_write_rejected, supervised_action_committed — payloads carry opaque household-scoped IDs only, tagged internal-BI.
- Step 1 — failing tests: a denied
requireemitsauthz_escalation_blockedwhose payload contains no member name/email (only an opaque id) and abucket=internalBItag; a child-PII rejection emitschild_pii_write_rejected. Assert the pre-emit scrub drops any child identifier. Steps 2–5.
Phase 6 review: security-reviewer. Gate: zero child identifier in any payload; correct bucket tag.
Phase 7 — App-UI surfaces
Mirror the existing inside/outside + bloc + auto_route patterns (read graphify query "bloc repository page auto_route guard" and an existing editor like the chore editor). Each task: repository → bloc → page → flow test (mock repositories via the flow-test harness; see app/test/README.md).
Task 18: authz + consent repositories
Files: Create app/lib/outside/repositories/authz_repository.dart, consent_repository.dart (thin delegates to the SDK facade — no domain logic). Test: mock at this seam in later flow tests.
- Standard repository TDD. Commit.
Task 19: role & owner management (Admin → Members)
Files: Create the bloc + page; wire the route. Test app/test/flows/role_owner_management_test.dart.
Produces: assign role; grant/revoke owner (≥1 guard surfaced as a blocked action with LastOwnerCannotDeleteWithMembers message); transfer ownership.
- Step 1 — failing flow test: tapping "remove owner" on the last owner shows the guard message; assigning helper updates the row. Steps 2–5.
Task 20: consent lifecycle + adult ToS gate
Files: Create the consent bloc + page + the session-start ToS gate. Test app/test/flows/consent_lifecycle_test.dart.
Produces: start VPC (email-plus/card), show none→pending→granted→revoked, revoke; block the session until acceptTerms.
- Step 1 — failing flow test: session blocked until ToS accepted; capturing consent moves the chip to granted. Steps 2–5.
Task 21: supervised-action UX
Files: Create the supervised flow widget(s). Test app/test/flows/supervised_action_test.dart.
Produces: child picks a bounty / signs off on a shared device; manual → parent approve prompt; auto → silent resolve.
- Step 1 — failing flow test: manual chore shows the parent-approval prompt; auto chore resolves with no prompt. Steps 2–5.
Task 22: capability-aware route guards + badges
Files: Modify the auto_route guards + the More/Settings badges to call authorizer.can(...) (not raw isParental). Test app/test/flows/route_guard_capability_test.dart.
Produces: Admin hub gated by capability; a non-parental helper still sees the approval badge (the bug §2 fixes).
- Step 1 — failing flow test: a helper sees the approval-queue badge; a plain member is denied the Admin route with the "no permission" surface. Steps 2–5.
Phase 7 review: flutter-reviewer + a11y check on the new surfaces. Gate: guards use
can(...), helper badge visible, no rawisParentalin guard files.
Phase 8 — Integration, parity, docs
Task 23: full sweep + flip status + graphify
- Run the whole suite (
packages/client_sdk+app); assert ≥ baseline (compare to/tmp/baseline_*); greenfvm dart analyze. - Run the guarded live RLS-parity smoke against
bgedvvmihygwxhjxlvfu(creds flag), extending the SP3 smoke: cross-household isolation + helper-cannot-approve-spend + child-PII-gate. - Edit
authorization.md: flip the caution banner + theproposed → builtlines for the now-built pieces; redeploy the dev site (scripts/test_gallery_serve.sh+scripts/deploy-developer-gallery.shfrom repo root). -
graphify update .; commitgraph.json+GRAPH_REPORT.md. - Commit. Final whole-branch review (flutter-reviewer + database-reviewer + security-reviewer).
Self-review (run after writing — completed)
- Spec coverage: P1↔Authorizer/capabilities; P2↔schema/RLS/helpers/triggers; P3↔guards/owner/role/zero-last/bootstrap; P4↔consent online+offline+ToS+lifecycle; P5↔supervised; P6↔events; P7↔UI surfaces+guards; P8↔integration/parity/docs. Every spec phase + the §7 coverage items map to a task. ✓
- Placeholders: the ⚖️ SLA numbers are explicitly deferred (spec out-of-scope), not a plan gap; no TBD/"handle edge cases". ✓
- Type consistency:
Authorizer.require/can/capabilitiesFor,Capability,ConsentState,MemberAccess, the new exceptions are named identically across Tasks 1–22. ✓