This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DevPlace: gap analysis against the Apple App Store requirement register
Author: retoor <retoor@molodetz.nl>
Stage two of `apple.md`. Input is the requirement register in [`applecomp.md`](applecomp.md) §8. Output is the exhaustive list of changes DevPlace needs to make an iOS client of this platform publishable. The implementation design is [`appleimpl.md`](appleimpl.md).
Every verdict below is backed by a file reference read during the traversal. No verdict is inferred from documentation; documentation was only used to locate code.
---
## 1. Method
The traversal covered, recursively:
-`devplacepy/routers/` - every router file and package, for the full endpoint surface.
-`devplacepy/models.py`, `devplacepy/schemas/` - every input form and output schema.
-`devplacepy/content.py`, `devplacepy/responses.py`, `devplacepy/templating.py` - the shared predicates and response choke points.
-`devplacepy/main.py` - middleware stack and router mounts.
---
## 2. Inventory: every user-generated-content surface
Requirement **R5** (report on every UGC surface) and **R4** (filter on every UGC surface) are only satisfiable against a complete list. This is that list, derived from `SOFT_DELETE_TABLES` in `devplacepy/database/soft_delete.py:7` cross-checked against the routers that write each table.
**Twenty distinct surfaces.** Sixteen of them (S1-S14, S18, and S15's published output) are visible to at least one other person and therefore fall inside guideline 1.2's scope. This breadth is the single defining constraint of the implementation: any design that requires per-surface bespoke code will be incomplete on the day it ships and will decay afterwards.
---
## 3. Inventory: what already exists and can be reused
| Capability | Where | Fitness for the requirement |
| **Block and mute** | `routers/relations.py` (`/block/{username}`, `/mute/{username}`, and the `unblock`/`unmute` inverses), `user_relations` table, `_drop_blocked` in `database/comments.py` | Satisfies **R9** functionally. Reachability from content is a gap (see G9). |
| **Soft delete across the board** | `database/soft_delete.py`, `SOFT_DELETE_TABLES` (44 tables) | Every content removal is already reversible and auditable, which is exactly what **P2** and DSA statements of reasons need. |
| **Admin Trash** | `routers/admin/trash.py`, `/admin/trash`, restore/purge by event | Moderator undo path already exists. |
| **Append-only audit log** | `services/audit/`, 288 keys in `events.md`, `/admin/audit-log` | The evidence substrate for **P1**, **P2** and the 24-hour SLA proof. |
| **Account deactivation** | `users.is_active`, admin toggle at `routers/admin/users.py:179`, devrant `DELETE /api/users/me` at `routers/devrant/auth.py:189` | **Not** account deletion. Apple explicitly rejects deactivation-only. See G12. |
| **Admin seniority guard** | `_is_senior_admin` in `routers/admin/users.py` | Reusable for moderator-action authorization. |
| **Workspace moderation flags** | `services/containers/workspace/flags.py` - `raise_flag`, `clear_flag`, `set_status`, `list_flags`, statuses `open/acknowledged/resolved/dismissed`, severities `info/warn/critical`, soft-deletable `workspace_flags` table | **The closest existing analogue to a report queue.** It is instance-scoped, machine-raised and admin-resolved. Its state machine, severity ladder and audit shape are the correct precedent to generalise from. |
| **Per-user AI opt-in** | `users.ai_correction_enabled` (default `0`) and `users.ai_modifier_enabled` (default `1`), `routers/profile/ai_correction.py`, `routers/profile/ai_modifier.py` | Establishes the pattern for a consent flag on the user row. Partially serves **R15** but is feature-scoped, not consent-scoped, and one of the two defaults to on. |
| **Notification preferences** | `notification_preferences` table, `NOTIFICATION_TYPES`×`NOTIFICATION_CHANNELS`, `routers/profile/notifications.py` | Push is already per-type, per-channel and user-controlled - **R17** is close to satisfied. |
| **Polymorphic target pattern** | `(target_type, target_uid)` on `comments`, `votes`, `reactions`, `bookmarks`; `resolve_target_redirect()` in `comments.py`; `database/ranking.py``VOTABLE_TARGETS`/`STAR_TARGETS`; `database/content.py``resolve_object_url` | **The load-bearing reuse.** A report is structurally identical to a vote: one row keyed on `(target_type, target_uid)` plus an actor. Reporting must be built on this exact pattern, not beside it. |
| **Devii action catalog** | `services/devii/actions/catalog/`, `CONFIRM_REQUIRED` in `dispatcher.py` | Every new route gets its agent face here, per the root `CLAUDE.md` four-faces rule. |
| **Docs prose registry** | `routers/docs/pages.py``DOCS_PAGES`, e.g. the existing `block-and-mute` and admin-only `media-moderation` pages | The publication channel for terms, community guidelines and privacy policy, with role gating already implemented. |
| **Site settings** | `site_settings`, `get_setting`/`get_int_setting`, `/admin/settings` | Where the moderation SLA, minimum age and filter aggressiveness belong - live-editable, no restart. |
| **AI gateway** | `services/openai_gateway/`, `/openai/v1/*`, per-user cost attribution | Single choke point through which **every** third-party AI call passes. **R15**'s consent gate has exactly one correct insertion point because of this. |
---
## 4. The gap register
Verdicts: **MISSING** (does not exist), **PARTIAL** (exists but does not meet the requirement), **PRESENT** (meets the requirement), **N/A** (not triggered).
| **R1** | Terms of service / EULA stating zero tolerance for objectionable content and abusive users | **MISSING** | No terms, EULA, or legal page anywhere. Grep for `terms`/`eula`/`privacy polic` across `templates/` and `routers/` returns only four unrelated docs pages (bots and Code Farm prose). `_footer_links.html` links Docs, Swagger, OpenAPI, Issue Report only. | Author the document; publish it as a first-class page; link it from the footer, the signup form and account settings. |
| **R2** | Recorded affirmative acceptance at account creation, re-acceptance on material change | **MISSING** | `routers/auth/signup.py` collects username, email, password, confirm only. `models.py:51``SignupForm` has four fields. No acceptance column on `users` (`database/schema.py:1823`ff enumerates every ensured column; none is terms-related). | Add a required acceptance control to signup; persist the accepted document version and timestamp; force re-acceptance when the version changes. |
| **R3** | Community guidelines enumerating prohibited content per 1.1.1-1.1.7 | **MISSING** | No such document. | Author and publish; reference from the terms and from every report dialog. |
| **R4** | Automated filtering of objectionable material at post time on every surface | **MISSING** | No content filter exists. The only `blocklist` occurrences in the codebase are the bot **quality** gate (`TRIVIAL_GIST_TERMS`, `GENERIC_COMMENT_PHRASES`) documented in `templates/docs/bots-content.html:38` - these judge whether generated content is *interesting*, not whether user content is *objectionable*, and they run only on bot output. | Introduce a filter that runs on every user-authored text at the single creation choke point, with an admin-tunable severity, that can block, hold for review, or flag. |
| **R5** | Report mechanism on every UGC surface | **MISSING** | No report route, table, template, schema, or Devii action exists. `routers/relations.py` provides block/mute only. `services/containers/workspace/flags.py` flags *workspaces*, machine-raised, and is not reachable by a member for content. | Build a polymorphic report facility covering all sixteen externally-visible surfaces in §2. |
| **R6** | Moderation queue with triage, decision and enforcement | **MISSING** | `/admin` sidebar (`templates/admin_base.html:11`-`59`) has Users, News, Media, Trash, Services, Gateway, Containers, Workspaces, Devii tasks, Bots, Game, AI usage, Statistics, Audit log, Backups, Notifications, Settings. There is no moderation section. `/admin/media` handles only *already soft-deleted* media. | Add a moderation queue as a first-class admin section, in the established `admin_section` pattern. |
| **R7** | Published 24-hour response commitment, and a mechanism that evidences it | **MISSING** | No SLA is published or measured. | Publish the commitment in the terms and the report confirmation; measure age-of-oldest-open-report; surface it to admins and alert on breach. |
| **R8** | Ejection of offending users as a first-class enforcement action | **PARTIAL** | `users.is_active` toggled at `routers/admin/users.py:179`. It is a bare on/off with no reason, no duration, no linkage to a report, and no notice to the user. `routers/devrant/auth.py:189` sets the same flag as "delete account". | Promote to a suspension/ban action carrying reason, scope, duration and a link to the report that caused it, and generating a statement of reasons (**P3**). |
| **R9** | Block abusive users | **PARTIAL** | Fully implemented at `routers/relations.py:87`-`104` with enforcement in `database/comments.py``_drop_blocked`. The gap is discoverability: the action is only reachable from a profile page. `templates/_post_card.html:32`ff and `templates/_comment.html:27`ff action bars offer Reply/Edit/Delete/React/Share and no Block. | Surface block from the content action bar alongside report; verify DM enforcement. |
| **R10** | Published contact information reachable inside the app | **PARTIAL** | `_footer_links.html` links `/issues` ("Issue Report"), which is a Gitea-backed bug tracker requiring an account, not a contact route. No postal address, no email, no phone. | Publish a contact page carrying the DSA-mandated address, email and phone, linked from the footer and from settings. |
| **R11** | Privacy policy meeting 5.1.1(i)'s three content requirements, in-app | **MISSING** | No privacy policy exists. | Author to the three-point spec; publish; link in-app and supply the URL to App Store Connect. |
| **R12** | In-app account deletion of the account record and associated personal data | **MISSING** | The only account-removal path in the product is `DELETE /api/users/me` (`routers/devrant/auth.py:189`) which sets `is_active = False` and revokes tokens - **deactivation**, which Apple's account-deletion support page names as explicitly insufficient. There is no route under `/profile` or `/auth` for deletion. | Build a real, self-service, reauthenticated deletion that removes the account record and the associated personal data, discoverable in account settings. |
| **R13** | Declared-age gate at account creation, plus age-based access restriction | **MISSING** | No birthdate, age or date-of-birth field exists anywhere: grep across `models.py` and `database/` returns nothing. `SignupForm` has no age field. | Collect a declared age at signup, store the derived age band (not the raw birthdate, per 5.1.4 data minimization), enforce a minimum age, and gate age-exceeding content on it. |
| **R14** | Content age labelling; mature content hidden by default | **MISSING** | No maturity flag on any content table. | Add a maturity classification produced by the filter and settable by the author, and hide flagged content behind an explicit, age-gated opt-in. |
| **R15** | Explicit consent before user content reaches third-party AI, with disclosure | **PARTIAL** | Two per-feature toggles exist: `users.ai_correction_enabled` defaults to `0` (opt-in, compliant in shape) and `users.ai_modifier_enabled` defaults to `1` (**opt-out - non-compliant**), both at `database/schema.py:1832`-`1841`. Neither is framed as consent to third-party processing, neither names the provider, and neither covers the other AI paths: Devii (`services/devii/`), DeepSearch, SEO metadata generation, the AI usage analyzer, issue enhancement (`services/gitea/enhance.py`), news import, and bots. All of these route through `/openai/v1/*` (`services/openai_gateway/`). | Introduce one explicit, named, versioned third-party-AI consent, defaulting to off, enforced at the gateway choke point, with the per-feature toggles kept as preferences subordinate to it. |
| **R16** | Easily accessible consent withdrawal | **MISSING** | No consent record exists, therefore nothing to withdraw. | Consent record with a withdraw action in account settings, and a downstream effect that is real (processing stops). |
| **R17** | Push optional, marketing push opt-in, in-app opt-out | **PRESENT** | `notification_preferences` per type per channel (`database/notifications.py`), user-editable at `routers/profile/notifications.py:17`. Push registration is explicit at `routers/push.py:32`. Nothing in the app requires push to function. | Verify no notification type is marketing-by-default; document the position for review notes. |
| **R18** | DMCA / IP notice-and-takedown channel | **MISSING** | None. | Add an intellectual-property report reason to the report facility and a public notice-and-takedown page describing the counter-notice path. |
| **R19** | Demo account with pre-seeded content and complete review notes | **MISSING** | No provisioning path for a review account exists; `registration_open` (`site_settings`) can close signup entirely, which would leave a reviewer unable to create an account. | Provide a stable demo account with visible content from other authors, so report and block can both be exercised. Write the review notes. |
| **R20** | Age-rating questionnaire answered from the real feature set | **BLOCKED BY R4/R5/R6/R13** | The questionnaire asks whether the app has moderation systems, content filtering, reporting tools, blocking functionality and parental controls. Today four of five answers are "no". | Answers become truthful only once R4, R5, R6 and R13 ship. |
| **R21** | App privacy details declared, including third-party AI processing | **BLOCKED BY R15** | Nothing to declare against until the AI data flow is disclosed and consented. | Declare Contact Info, User Content, Identifiers, Usage Data, Diagnostics, all Linked to You, none Used to Track You. |
| **R22** | EU trader status with address, phone, email | **MISSING (metadata)** | The same contact data R10 needs. | Declare in App Store Connect; keep identical to the in-app contact page. |
| **R23** | IPv6-only reachability | **UNVERIFIED** | `docker-compose.yml` and `nginx/nginx.conf.template` were not confirmed to bind IPv6; uvicorn defaults are IPv4. | Verify and, if needed, fix listen directives for the app, nginx, the WebSocket routes and the container ingress. |
| **R24** | Remote code execution positioned under the 2.5.2 educational exception | **PARTIAL** | Substantively compliant already: containers execute **remotely** (`services/containers/`), the browser IDE makes source completely viewable and editable (`routers/projects/files/`), and nothing alters the client binary. What is missing is the **positioning**: no documentation states this, and the review notes do not exist. | Document the architecture for App Review; make the "code runs on our servers, never on your device" statement explicit in the product and the docs. |
| **R25** | Native client materially beyond a web wrapper | **OUT OF SCOPE (client)** | The iOS binary is not in this repository. | The backend obligation is to expose every safety control as a JSON API so the native client can implement them natively rather than embedding web views. Covered by the four-faces rule. |
### 4.2 Conditional requirements
| Req | Trigger present? | Verdict | Evidence |
|-----|------------------|---------|----------|
| **C1** Sign in with Apple or equivalent | **No** | **N/A - must stay N/A** | Auth is exclusively DevPlace's own system: session cookie, `X-API-KEY`, Bearer, HTTP Basic, all resolved in `get_current_user`. `routers/auth/` has no OAuth provider. Guideline 4.8 exempts apps that exclusively use their own account system. **Adding any social login later immediately creates the Sign in with Apple obligation.** |
| **C2** IAP for digital goods | **No** | **N/A - must stay N/A** | No payment processor anywhere: no Stripe, PayPal or checkout integration in the codebase. The Code Farm economy (`services/game/`) is earn-only; Stars and Era awards are not purchasable. AI quota is administered, not sold (`devplace gateway quota set`). **Any future sale of coins, credits, quota or boosts inside the app triggers mandatory IAP.** |
| **C3** Loot-box odds disclosure | **No** | **N/A** | Randomized game rewards are not purchasable with real money. |
| **C4** Contest rules stating Apple is not a sponsor | **Borderline** | **PARTIAL** | Code Farm Eras (`devplace game era start/end`) rank players and award Stars. As long as awards are cosmetic/status only and nothing of monetary value is given, 5.3 is not engaged. Any real prize engages it. Document the position. |
| **C5** Index of offered software with universal links | **Yes** | **MISSING** | Users can publish workspaces reachable via the ingress proxy `/p/{slug}` (`routers/proxy.py`) and other users can open them. Guideline 4.7.4 requires an index of that software with universal links. No such index exists. |
| **C6** Ad reporting control | **No** | **N/A** | No advertising anywhere in the codebase. |
| **C7** App Tracking Transparency | **No** | **N/A** | No cross-app or cross-site tracking; no third-party analytics SDK. |
| **C8** Recording indicator and consent | **Yes** | **MISSING** | Presence tracking (`services/presence.py`, `last_seen`), the live view relay (`services/live_view_relay.py`), Devii terminal sessions and the audit log all make a record of user activity. Guideline 2.5.14 requires explicit consent **and** a clear indication. Presence is currently silent and unconditional. |
| **C9** Per-instance consent before sharing data with user software | **Yes** | **MISSING** | Container workspaces and Devii virtual tools can receive platform data. 4.7.3 requires explicit user consent **in each instance**. |
### 4.3 Posture requirements
| Req | Verdict | Notes |
|-----|---------|-------|
| **P1** Compliance improvement plan on request | **MISSING** | Needs moderation throughput metrics, which need R6. |
| **P2** Moderation decisions retained as an audit trail | **PARTIAL** | The audit log already records every state change and never raises into the caller (`services/audit/`). Moderation event keys do not yet exist in `events.md`. |
| **P3** Statement of reasons to the actioned user | **MISSING** | Content is soft-deleted silently. The notification system (`utils/notifications.py`, `create_notification`) is the right delivery channel and already exists. |
| **P4** Privacy labels kept in step with features | **MISSING** | Process obligation; needs a documented owner and a checklist entry in the feature workflow. |
| **P5** Accurate "What's New" | **MISSING** | Process obligation on the client release. |
---
## 5. The positioning conflict - the finding that outranks every table above
DevPlace currently **markets itself as uncensored**. This is not incidental copy; it is the product's stated identity in four places:
-`devplacepy/main.py:744` - the site description: *"Share what you're building in an open, uncensored environment."*
-`devplacepy/templates/base.html:9` - the default `meta description`, on every page.
-`devplacepy/templates/landing.html:120` - the landing hero paragraph, and at `landing.html:134` a feature card headed **"No Censorship"**.
-`devplacepy/database/schema.py:280` - the default `site_tagline` site setting, echoed in `templates/admin_settings.html:24`.
Guideline 1.2 requires a **method for filtering objectionable material** and makes removal of violating content the developer's explicit responsibility. An App Review reviewer who opens the landing page - which they will, because it is the Support/Marketing URL - reads a promise that the platform does not moderate. That single sentence is sufficient grounds for a 1.2 rejection **regardless of how good the implementation is**, because it is a public statement that the required controls are not exercised.
There is no technical fix for this. The positioning must change to something that is both true and compatible: the platform is **open and uncensored in the sense that it does not editorialise developer opinion**, while enforcing a floor of prohibited categories. The four sites above must be reworded in step, and the wording must match the terms of service and community guidelines exactly, because a mismatch between marketing and policy is itself a 2.3.1 problem.
This is flagged as a decision for the lord, not an assumption: it changes the product's public voice.
---
## 6. Consolidated change list
Grouped by the layer they land in, so the implementation document can sequence them. Nothing here is designed yet; this is scope, not solution.
### 6.1 Data layer
1. A polymorphic **reports** store keyed on `(target_type, target_uid)`, soft-deletable, with a state machine.
2.**Moderation decision** records linked to reports, retained for the audit trail.
3.**Enforcement** records: suspension/ban with reason, scope, duration, originating report.
4.`users` columns: terms-acceptance version and timestamp; declared age band; third-party-AI consent version, timestamp and state; activity-recording consent.
5. A **maturity** classification on content, produced by the filter and adjustable by the author.
6. New `site_settings` keys: moderation SLA hours, minimum age, filter mode and thresholds, contact details, current policy document versions.
7. New soft-delete table registrations and indexes for all of the above.
9. Report listing and decision endpoints for moderators, with the seniority guard.
10. Enforcement endpoints (suspend, ban, lift) replacing the bare `is_active` toggle.
11. Account **deletion** endpoint with reauthentication and a real data-removal cascade.
12. Terms acceptance endpoint plus a gate that forces re-acceptance on version change.
13. AI consent endpoints, and enforcement at the `/openai/v1/*` gateway choke point.
14. Age declaration at signup, and an age predicate applied at every read of maturity-flagged content.
15. The content filter, invoked at the single creation choke point that already exists in `content.py`.
16. Public legal pages: terms, community guidelines, privacy policy, contact, notice-and-takedown.
17. A published index of user-offered software with universal links (4.7.4).
18. A presence/activity-recording consent and indicator (2.5.14).
### 6.3 View layer
19. Report and Block controls in **every** content action bar - `_post_card.html`, `_comment.html`, and the detail templates for gists, projects, news, quizzes, media, messages and profiles.
20. A report dialog reusing the existing modal system, with reasons mapped to the 1.1.x categories.
21. Signup form: terms acceptance and age declaration.
| **Per-surface duplication** | Twenty surfaces × bespoke report code guarantees an incomplete rollout and permanent drift. | One polymorphic facility on the existing `(target_type, target_uid)` pattern, registered once per surface, exactly as votes and reactions already are. |
| **Filter false positives on a developer platform** | Code, security discussion and error messages are full of terms a naive filter flags. Blocking legitimate posts destroys the product. | The filter must default to flag-for-review rather than hard block, and must be admin-tunable through `site_settings` with no restart. |
| **Silent failure** | The root `CLAUDE.md` forbids errors passing silently; a moderation control that fails open is worse than absent. | Report submission must never be swallowed; filter failure must fail toward review, not toward publication. |
| **Deletion cascade correctness** | Account deletion touches nearly every table. A partial cascade leaves orphaned personal data and breaks the 5.1.1(v) promise. | One shared soft-delete stamp for the reversible window, then a hard purge, reusing `soft_delete_in` and `purge_event`. |
| **Consent regression on the AI path** | Turning AI consent off by default changes behaviour for every existing user and every internal AI consumer (news, bots, issue enhancement, SEO metadata). | Distinguish consent for *the user's own content* from platform-owned processing; enforce at the gateway with an explicit owner kind. |
| **Test suite scale** | ~2882 tests run serially. A change touching the content creation choke point touches everything. | Land the data and server layers first, run the full suite at each stage. |
| **Economy and state-machine correctness** | Suspension, consent and age gates are read-then-write mutations reachable from HTTP, Devii, the devRant API and the CLI. | The root `CLAUDE.md` four-layer rigorous-verification procedure applies to enforcement and consent state. |
---
## 8. Summary verdict
Of the 25 mandatory requirements: **1 present** (R17), **5 partial** (R8, R9, R10, R15, R24), **15 missing** (R1-R7, R11-R14, R16, R18, R19, R22), **2 blocked on others** (R20, R21), **1 unverified** (R23), **1 out of scope for this repository** (R25). The six categories partition all 25.
Of the 9 conditional requirements: **5 not triggered and must be kept that way** (C1, C2, C3, C6, C7), **3 triggered and missing** (C5, C8, C9), **1 borderline** (C4).
Of the 5 posture requirements: **1 partial** (P2), **4 missing**.
The platform has excellent bones for this work - polymorphic targeting, universal soft delete, a complete audit log, an admin shell, a single AI choke point and an agent catalog that already forces cross-layer completeness. What it lacks is the entire safety layer, the entire legal layer, and a public identity compatible with having one.