Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Business requirements, user stories, data model, and workflows for each feature
| [Membership Status Partition](../src/Sections/Humans.Governance/Docs/features/membership-status.md) | Six-bucket mutually exclusive status model computed by `PartitionUsersAsync` and used by the Admin dashboard — the Admin /Humans list and Volunteers team sync each compute their own status buckets independently now |
| [Membership Tiers](../src/Sections/Humans.Governance/Docs/features/membership-tiers.md) | Four-tier membership model (Volunteer / Colaborador / Asociado / Board) with three tiers managed in-system |
| [Assembly Votes](../src/Sections/Humans.Governance/Docs/features/assembly-votes.md) | Binding recorded votes of the Asociados on Assembly motions — frozen roster at open, embargoed tally, append-only ballot history, IRV counting |
| [Grants](../src/Sections/Humans.Grants/Docs/features/grants.md) | Program-based entitlements (cheaper ticket, early entry, staying late) issued by hand or by a source section such as Low Income, with revoke, resend and holder notices |
| [In-App Guide](../src/Sections/Humans.Guide/Docs/features/in-app-guide.md) | Embedded `/Guide` rendering of the `docs/guide/` markdown with role-aware filtering and in-app navigation |
| [Issues System](../src/Sections/Humans.Issues/Docs/features/issues-system.md) | In-app issue tracker routing bugs/features/questions by section to the right role-holders, with reporter↔handler threads |
| [Legal Documents & Consent Management](../src/Sections/Humans.Consent/Docs/features/legal-documents-consent.md) | GDPR-compliant document version tracking with immutable consent audit trail, team-scoped, multi-language, configurable through admin GUI |
Expand Down Expand Up @@ -124,11 +125,13 @@ Terse, authoritative invariant docs for each major section: concepts, data model
| [Gdpr](../src/Sections/Humans.Gdpr/Docs/Gdpr.md) | GDPR Article 15 export orchestrator — fans out to every section that holds personal data and merges the slices into the JSON the human downloads; owns no tables and no pages |
| [Google Integration](../src/Sections/Humans.GoogleIntegration/Docs/GoogleIntegration.md) | Shared-Drive-only sync for Drive folders, Groups, and Workspace accounts with reconciliation and Drive-activity monitoring |
| [Governance](../src/Sections/Humans.Governance/Docs/Governance.md) | Colaborador and Asociado tier applications, Board voting workflow, and term lifecycle (not volunteer onboarding) |
| [Grants](../src/Sections/Humans.Grants/Docs/Grants.md) | Entitlements to a person (cheaper ticket, early entry, staying late): issuer rules, one live grant per person, expiry, source write contract, and notices |
| [Guide](../src/Sections/Humans.Guide/Docs/Guide.md) | The in-app `/Guide` renderer for `docs/guide/` markdown with role-scoped block filtering |
| [Holded Connector](../src/Sections/Humans.Holded/Docs/Holded-connector.md) | Thin typed-`HttpClient` surface to the Holded accounting API, shared by Expenses (purchase documents), Finance (provisioning, document sync, SEPA booking), Store (sales documents) and Workgroups (expense accounts). Owned by the Holded section; owns no tables |
| [Holded](../src/Sections/Humans.Holded/Docs/Holded.md) | The ledger mirror: a local, re-derivable copy of Holded's daybook and chart of accounts, plus the sync that maintains it and the `/Holded` admin screen |
| [Issues](../src/Sections/Humans.Issues/Docs/Issues.md) | In-app issue tracker (bugs, features, questions) with screenshots, role-routed triage, and a reporter↔handler conversation thread |
| [Consent](../src/Sections/Humans.Consent/Docs/Consent.md) | GitHub-synced legal documents, per-version append-only consent records, and the Consent Coordinator audit/review queue |
| [Low Income](../src/Sections/Humans.LowIncome/Docs/LowIncome.md) | Stub for the future Low Income process: one Admin-only page that gives or removes a person's Low Income grant, which lives in Grants |
| [MailerLite](../src/Sections/Humans.MailerLite/Docs/MailerLite.md) | Humans ↔ MailerLite synchronisation: inbound import and outbound audience management |
| [Monitor](../src/Sections/Humans.Monitor/Docs/Monitor.md) | Operator-facing monitoring of the Google Workspace estate: detect unrequested permission changes, and show the Google-sync audit trail for one resource or one human |
| [Notifications](../src/Sections/Humans.Notifications/Docs/Notifications.md) | In-app notification fan-out (stored events plus per-user inbox) and live meter counts (computed) |
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/code-review-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Rules are ordered by historical frequency — the patterns that have caused the

## Authorization Gaps *(8+ historical fixes)*

- **Every controller action that modifies data must have `[Authorize]` with explicit roles.** Missing `[Authorize]` on a POST/PUT/DELETE action is a security hole. Don't rely on controller-level `[Authorize]` alone if individual actions need different role requirements.
- **Every controller action that modifies data must carry an explicit policy (`[Authorize(Policy = …)]`) or a resource-based check.** Every endpoint is authenticated by default (fallback policy; `HumansControllerBase` and `ApiControllerBase` carry `[Authorize]`), so a POST/PUT/DELETE action with no policy is open to every signed-in account — a security hole unless that is the stated intent. `[AllowAnonymous]` on a mutating action is a reject without a stated reason. Don't rely on a controller-level policy alone if individual actions need different requirements.
- **Every POST action must have `[ValidateAntiForgeryToken]`.** No exceptions.
- **Admin actions must check admin roles, not just `[Authorize]`.** Bare `[Authorize]` only checks authentication, not authorization. Actions in Admin/TeamAdmin/ShiftAdmin/CampAdmin controllers need role-based authorization.
- **Manual role checks in method bodies should be attributes — policy attributes.** Replace `if (!User.IsInRole("Admin"))` with `[Authorize(Policy = PolicyNames.AdminOnly)]` on the action. **Never `[Authorize(Roles = "...")]`**: raw role strings are forbidden by `design-rules.md` §11 and there are zero of them in `src/` — flagging a reviewer toward one would reintroduce the pattern. For a check that depends on the *resource* (this team, this camp, this expense report), the attribute is not enough: use the section's resource-based handler via `IAuthorizationService.AuthorizeAsync(User, resource, requirement)`.
Expand Down
48 changes: 29 additions & 19 deletions docs/architecture/dependency-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ graph LR
CommPref[CommunicationPreferenceService]:::email
EmailProblems[EmailProblemsService]:::users
Unsub[UnsubscribeService]:::email
UserNameSync[UserNameSyncService]:::users

AdminAuth[AdminAuthorizationService]:::auth
MagicLink[MagicLinkService]:::auth
Expand All @@ -109,6 +108,8 @@ graph LR
Camp[CampService]:::camps
CampContact[CampContactService]:::camps
CampRole[CampRoleService]:::camps
CampGallery[CampImageGallery]:::camps
CampNotices[CampMembershipNotices]:::camps

Cantina[CantinaRosterService]:::cantina
Dietary[DietaryProfileService]:::cantina
Expand All @@ -134,12 +135,12 @@ graph LR
LegalSync[LegalDocumentSyncService]:::legal
Consent[ConsentService]:::consent
ConsentReview[ConsentReviewService]:::consent
ConsentReminder[ConsentReminderService]:::consent
LegalSyncRunner[LegalDocumentSyncRunner]:::consent

TicketQ[TicketQueryService]:::tickets
TicketSync[TicketSyncService]:::tickets
TicketTransfer[TicketTransferService]:::tickets
TicketPrefLock[TicketingPreferenceLock]:::tickets
AttendeeImport[AttendeeContactImportService]:::tickets
OnsiteRoster[OnsiteRosterService]:::tickets
EventPart[EventParticipationService]:::tickets
Expand Down Expand Up @@ -173,11 +174,13 @@ graph LR
OnboardWidget[OnboardingWidgetState]:::onboarding

Feedback[FeedbackService]:::feedback
FeedbackReply[FeedbackReplyDelivery]:::feedback

Budget[BudgetService]:::budget
TicketBudget[TicketingBudgetService]:::budget

Finance[HoldedFinanceManagement]:::finance
CreditorMatch[CreditorMatchService]:::finance
HoldedDocSync[HoldedDocumentSync]:::finance
SepaBooking[SepaBankBooking]:::finance
PayDetails[PaymentDetailsService]:::finance
Expand Down Expand Up @@ -207,6 +210,7 @@ graph LR
Store[StoreAccountingRead]:::store
StoreInvoice[StoreInvoiceIssuer]:::store
StoreOrders[StoreOrderReader]:::store
StoreStripe[StoreStripePayments]:::store

ExpenseReport[ExpenseReportService]:::expenses
ExpensePublisher[ExpenseHoldedPublisher]:::expenses
Expand Down Expand Up @@ -289,20 +293,16 @@ graph LR
AcctDel --> Audit
AcctDel --> Email
Merge --> Audit
Merge --> Team
Merge --> Notif
DupAcct --> Team
DupAcct --> Role
DupAcct --> Audit
DupAcct --> AppDec
DupAcct --> Consent
UsersAudience --> TicketQ
HumanLifecycle --> NotifEmitter
HumanLifecycle --> NotifInbox
HumanLifecycle --> Audit
HumanLifecycle --> Metrics
TeamMsgOpts --> TRes
UserNameSync --> Audit

%% Auth
Role --> User
Expand Down Expand Up @@ -337,6 +337,9 @@ graph LR
CampRole --> User
CampRole --> Audit
CampRole --> NotifEmitter
CampGallery --> Audit
CampNotices --> NotifEmitter
CampNotices --> User

%% Cantina
Cantina --> ShiftMgmt
Expand All @@ -348,6 +351,8 @@ graph LR
MemTier --> User
%% Consent
ConsentReview --> User
ConsentReview --> Audit
ConsentReminder --> User
%% Finance
PayDetails --> User
PayDetails --> Audit
Expand All @@ -357,6 +362,7 @@ graph LR
CityPlan --> Team
CityPlan --> User
CityPlan --> Audit
CityPlan --> SettingsSvc

%% Shifts
ShiftMgmt --> Audit
Expand Down Expand Up @@ -405,7 +411,6 @@ graph LR
LegalSync --> Team
LegalSync --> User
LegalSync --> Audit
Consent --> NotifInbox
Consent --> HumanLifecycle
Consent --> User
Consent --> Metrics
Expand All @@ -432,7 +437,6 @@ graph LR
TicketTransfer --> UEmail
TicketTransfer --> Email
TicketTransfer --> Audit
TicketPrefLock --> TicketQ
AttendeeImport --> UEmail
AttendeeImport --> AcctProv
AttendeeImport --> User
Expand Down Expand Up @@ -516,11 +520,13 @@ graph LR

%% Feedback
Feedback --> User
Feedback --> UEmail
Feedback --> Team
Feedback --> Email
Feedback --> NotifEmitter
Feedback --> Audit
FeedbackReply --> User
FeedbackReply --> UEmail
FeedbackReply --> Email
FeedbackReply --> NotifEmitter
FeedbackReply --> Audit

%% Budget
Budget --> Team
Expand All @@ -535,6 +541,8 @@ graph LR
Finance --> UEmail
Finance --> Email
Finance --> ExpenseBookings
CreditorMatch --> Holded
CreditorMatch --> ExpenseReport
CashFlowSvc --> Budget
CashFlowSvc --> TicketQ
%% Finance -> Expenses is the section project (IExpenseHoldedBookingService, read-only);
Expand Down Expand Up @@ -601,6 +609,7 @@ graph LR
Store --> Holded
StoreInvoice --> Audit
StoreInvoice --> Holded
StoreStripe --> Audit
StoreOrders --> Camp
StoreOrders --> Team
StoreOrders --> SettingsSvc
Expand All @@ -625,6 +634,7 @@ graph LR
ExpenseBgProc --> Finance
ExpenseBgProc --> Audit
ExpenseBookings --> User
ExpenseBookings --> Holded

%% Containers
Container --> Camp
Expand Down Expand Up @@ -719,6 +729,7 @@ graph LR
Grant --> Audit
Grant --> SettingsSvc
LowIncome --> Grant
LowIncome --> User

%% ═══════════════════════════════════
%% Lazy-resolved (IServiceProvider/Lazy<T>) — break DI cycles
Expand All @@ -727,7 +738,6 @@ graph LR
UEmail -. "lazy" .-> TicketQ
UEmail -. "lazy" .-> GIdentity
Team -. "lazy" .-> Role
Team -. "lazy" .-> Email
Team -. "lazy" .-> GSyncOutbox
Team -. "lazy" .-> GSyncSvc
Team -. "lazy" .-> GIdentity
Expand All @@ -739,17 +749,17 @@ graph LR
ShiftMgmt -. "lazy" .-> Team
ShiftMgmt -. "lazy" .-> TicketQ
ShiftMgmt -. "lazy" .-> User
ShiftMgmt -. "lazy" .-> SettingsSvc
ShiftSign -. "lazy" .-> Team
MembershipCalc -. "lazy" .-> Consent
Consent -. "lazy" .-> MembershipCalc
TRes -. "lazy" .-> Role
Cal -. "lazy" .-> Team
Metrics -. "lazy" .-> User

%% ── Edge styling ──
%% Eager edges come first, then the lazy block; linkStyle indexes the lazy block by position.
%% 395 eager edges (indices 0-394) come first, then the 20 lazy edges (395-414); linkStyle indexes the lazy block by position.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove hand-maintained counts from the freshness docs

This new comment duplicates totals already derivable from the Mermaid edge list, so the next edge change can silently make the documentation false. The same diff adds further derived counts in docs/freshness/last-report.md (161 docs, 6 nodes, and the repeated edge totals), AuditLog.md (four members), and CityPlanning.md (Three entities); the repository's triggered no-derived-aggregates-in-docs rule requires deleting rather than refreshing these counts, so please sweep all added documentation for this class.

AGENTS.md reference: AGENTS.md:L46-L48

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in af07cf2. I removed the linkStyle comment counts and the report counts, plus the other derived counts in this diff: AuditLog.md, both CityPlanning docs, admin-shell.md, workload-dashboard.md, and a pre-existing "one of two ways" in the Tickets guide. The dependency-graph prompt in the catalog asked for those counts, so it now says the comment states no counts.


Generated by Claude Code

%% Recompute the indices whenever edges change.
linkStyle 389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409 stroke:#f97316,stroke-width:2.5px
linkStyle 395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414 stroke:#f97316,stroke-width:2.5px
```


Expand Down Expand Up @@ -779,9 +789,9 @@ Other notable one-way lazy edges:

- **Team → User** — user-slice stitching; User no longer reaches back into Team.
- **Team → GoogleSyncOutbox** — enqueues transactional-outbox Google-sync events on membership/role changes; one-way.
- **ShiftManagement → Role / User / Camp / Tickets**, **Team → Role / Email**, **TeamResource → Role** — one-way lazy where eager injection would still close a cycle through other paths (notably `ISystemTeamSync`, a job interface outside this graph).
- **ShiftManagement → Role / User / Camp / Tickets / Settings**, **Team → Role**, **TeamResource → Role** — one-way lazy where eager injection would still close a cycle through other paths (notably `ISystemTeamSync`, a job interface outside this graph).
- **Team → WorkspaceIdentity**, **UserEmail → WorkspaceIdentity** — one-way lazy resolution of `IGoogleIntegrationService` / `IGoogleIntegrationWrite`.
- **Calendar → Team**, **HumansMetrics → User** — Singleton hosts (the `CachingCalendarService` decorator, the polled metrics gauge) resolving a scoped read service per call, not cycle breaks.
- **HumansMetrics → User** — a Singleton host (the polled metrics gauge) resolving a scoped read service per call, not a cycle break.

When adding a new cross-service call, default to ctor injection. Reach for the lazy pattern only when ctor injection produces a circular DI error, and document why at the call site.

Expand All @@ -794,9 +804,9 @@ The most depended-on cross-section surfaces (read the counts off the diagram):
- **`TeamService`** — second-largest section fan-in; read consumers go through `ITeamService`, cross-section writes through `ITeamWrite` (internal `ITeamManagement` joins them); batch methods exist to avoid N+1 at call sites.
- **`UserEmailService`** — email-identity lookups across the system. (The Google Workspace address is `WorkspaceIdentityService`'s since the Users decomposition.)
- **`OutboxEmailService`** (`IEmailService`) / **`CommunicationPreferenceService`** — outbound mail and its consent/unsubscribe gating.
- **`NotificationEmitter`** — the enqueue surface almost all notifiers inject; only `AccountMergeService` and `WorkgroupService` take the full `INotificationService`.
- **`NotificationEmitter`** — the enqueue surface almost all notifiers inject; only `WorkgroupService` takes the full `INotificationService`.
- **`SettingsWriteService`** (`ISettingsService`) — event/app settings read by most event-facing sections.
- **`ShiftManagementService`** — shift hub; itself lazy-resolves Team/Role/Tickets/User/Camp to break cycles.
- **`ShiftManagementService`** — shift hub; itself lazy-resolves Team/Role/Tickets/User/Camp/Settings to break cycles.
- **`AdminAuthorizationService`**, **`ShiftViewService`** — repo-only adapters with zero outbound cross-section edges.

## Pending follow-ups
Expand Down
2 changes: 2 additions & 0 deletions docs/architecture/design-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ The fanouts:
| any holder of bare Guids | `IEntityNameContributor` (`Humans.Base.Interfaces`) | `CachingTeamService` (Teams), `CachingUserService` (Users) | `Guid → EntityName(type, display name, slug?)` for the ids on one page |
| the `/Email/EmailPreview` gallery (`EmailController`) | `IEmailPreviewContributor` (`Humans.Email.Contracts`) | every section that sends email | one rendered sample per template a section sends |
| the preference toggles (`CommunicationPreferenceService`) | `ICommunicationPreferenceLock` (`Humans.Email.Contracts`) | `TicketingPreferenceLock` (Tickets) | the categories a human may not change on the member and guest pages |
| `AuditActionRegistry` (AuditLog) | `IAuditActionContributor` (`Humans.AuditLog`, `Contracts/` folder) | every section that writes an audit row, via its `<Section>AuditActions` | stored action name → `AuditActionDefinition` (verb, alert flag, quick filter) for rendering and filtering |
| `NotificationSourceRegistry` (Notifications) | `INotificationSourceContributor` (`Humans.Notifications.Contracts`) | every section that emits a notification, via its `<Section>NotificationSources` | stored source name → `NotificationSourceDefinition` (category, inbox group, retired flag) |

`IEntityNameContributor` is the one that lives in Base rather than in a consumer's contracts leaf, because it has more than one consumer and they must not reference each other (nobodies-collective/Humans#1059). It inverts the arrow that used to make AuditLog reference Teams and GoogleIntegration just to print names: sections point at the Base contract, and the consumer names no section. Wide-fan-in sections that must not gain outbound links — Audit, Auth, Search — are exactly its intended callers.

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/maintenance-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ what's overdue.
| Process | Last Run | Next Due | Cadence | Est. Cost | Notes |
|---------|----------|----------|---------|-----------|-------|
| NuGet vulnerability check | 2026-10-21 | 2026-10-28 | Weekly | — | `dotnet list package --vulnerable`. 2026-10-21: 0 vulnerable (incl. transitive). |
| Freshness sweep (diff) | 2026-10-21 | 2026-10-22 | Daily | — | `/freshness-sweep` — report at `docs/freshness/last-report.md`; narrative lives in each sweep's PR body. 2026-10-21: mostly Users-decomposition drift; pruned the per-section DbContext and G5 design specs. |
| Freshness sweep (diff) | 2026-10-30 | 2026-10-31 | Daily | — | `/freshness-sweep` — report at `docs/freshness/last-report.md`; narrative lives in each sweep's PR body. 2026-10-30: Board read-only access, section-owned roles and authenticated-by-default drift; pruned the workgroup-budget plan. |
| Section doctor | 2026-08-17 | — | 1–2×/day | — | `/section-doctor` — frozen row (nobodies-collective/Humans#1069); each run writes `docs/health/runs/<date>-<Section>.md`. |
| Debt sweep | 2026-10-04 | 2026-10-05 | Daily | — | Nightly [debt workflow](../../.codex/prompts/daily-debt.md); central and section debt ledgers. 2026-10-06 ledger review with Peter — central 40→18 and section 94→49 rows, 10 issues filed (#1919–#1928), 53 rows unblocked under standing policy. |
| Freshness sweep (full) | — | — | Weekly | — | `/freshness-sweep --full` — full regeneration of every catalog entry. First run pending. |
Expand Down
6 changes: 4 additions & 2 deletions docs/architecture/roslyn-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,10 @@ have the supporting machinery. Each notes the missing piece.
- **`No new `[Authorize]`-less POST/PUT/DELETE actions in controllers`**
(`code-review-rules.md` "Authorization Gaps, 8+ historical fixes"). Each
method is a `MethodDeclarationSyntax` decorated with
`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`; the rule is "must also carry
`[Authorize]` or have it inherited from the class". **Needs:** baseline
`[HttpPost]`/`[HttpPut]`/`[HttpDelete]`. Every endpoint is now authenticated
by default (fallback policy; `[Authorize]` on `HumansControllerBase` and
`ApiControllerBase`), so "carries `[Authorize]`" always holds; the live form
is "carries a policy or makes a resource-based check". **Needs:** baseline
framework — there are existing controller actions that legitimately use
alternate auth attributes (`[AllowAnonymous]`, custom policies, the
attribute on a base class). Without a baseline-aware analyzer the
Expand Down
Loading
Loading