diff --git a/tools/v1/individual/follow-up-reminder/docs/DATA_OWNERSHIP_CONTRACT.md b/tools/v1/individual/follow-up-reminder/docs/DATA_OWNERSHIP_CONTRACT.md new file mode 100644 index 000000000..75c2167fa --- /dev/null +++ b/tools/v1/individual/follow-up-reminder/docs/DATA_OWNERSHIP_CONTRACT.md @@ -0,0 +1,57 @@ +# Follow-up Reminder Data Ownership Contract + +## Owned Data + +The isolated tool owns only local, caller-supplied or synthetic data shapes: + +- normalized email snapshots passed into the engine. +- reminder review models returned to a future UI. +- signals and warnings derived from bounded local text scans. +- synthetic fixtures used for local tests and review. +- local UI state for loading, empty, error, and review views. + +The tool does not own mailbox records, message labels, calendar events, +notification jobs, user accounts, wallet accounts, Stellar transactions, +database rows, provider credentials, or app route state. + +## Input Boundary + +All caller-provided email data is untrusted. Guards should validate the shape, +sanitize text, cap sizes, and bound existing reminder lists before the engine +runs. The engine scans only bounded text and returns a review model. It does not +store or transmit input data. + +Required safe input properties: + +- `messageId` is a non-empty synthetic or caller-provided message key. +- `subject` and `body` are bounded strings. +- `senderAddress` is caller-supplied display data, not an identity guarantee. +- `receivedAt` is an ISO timestamp used only for local relative-date resolution. +- `existingReminders` is a bounded list of local duplicate keys. + +## Output Boundary + +Reminder outputs are advisory: + +- `draft` means a user may review and edit a suggestion. +- `no_action` means no reminder should be presented by default. +- `dueAt` can be null when the due date is missing or ambiguous. +- `warnings` explain uncertainty and must be visible in a future UI. + +The engine cannot confirm a reminder, create a calendar event, send a +notification, mutate an inbox, change labels, mark messages read, archive, +delete, or reply to email. + +## Integration Handoff + +A future integration issue must document: + +1. which product component supplies normalized email snapshots. +2. where user confirmation is captured. +3. how reminders are persisted after explicit confirmation. +4. which system owns notifications or calendar writes. +5. how duplicate keys are derived across sessions. +6. how user data is protected in logs, analytics, and tests. + +Until that handoff exists, this folder remains a self-contained mini-product +with no production side effects. diff --git a/tools/v1/individual/follow-up-reminder/docs/MODULE_BOUNDARIES.md b/tools/v1/individual/follow-up-reminder/docs/MODULE_BOUNDARIES.md new file mode 100644 index 000000000..3f4da914d --- /dev/null +++ b/tools/v1/individual/follow-up-reminder/docs/MODULE_BOUNDARIES.md @@ -0,0 +1,92 @@ +# Follow-up Reminder Module Boundaries + +## Folder Scope + +All Follow-up Reminder work for this issue stays inside: + +```text +tools/v1/individual/follow-up-reminder/ +``` + +This V1 individual tool is isolated from the main app until a future integration +issue explicitly connects it. This architecture contract does not modify app +shells, routes, inbox internals, mail rendering, auth, wallet, Stellar, +database, notification, calendar, provider, analytics, or shared design-system +code. + +## Module Ownership + +| Layer | Current or planned path | Responsibility | +| ---------- | -------------------------------------- | ---------------------------------------------------------------------------- | +| Public API | `index.ts` | Re-export stable engine, guard, fixture, and type contracts. | +| Engine | `services/followUpReminder.ts` | Convert normalized email input into an advisory reminder review model. | +| Guards | `services/guards.ts` | Validate, sanitize, and bound untrusted caller input before engine use. | +| Fixtures | `services/fixtures.ts` and `fixtures/` | Provide deterministic synthetic email and review cases. | +| Components | `components/` | Render isolated loading, empty, error, card, and review states. | +| Tests | `tests/` | Verify engine, guard, component, fixture, and architecture behavior locally. | +| Docs | `docs/` and `REVIEW_NOTES.md` | Document usage, threats, fixtures, review flow, and contribution boundaries. | + +## Dependency Direction + +Allowed dependency direction: + +```text +index.ts + -> services/followUpReminder.ts + -> services/guards.ts + -> services/fixtures.ts +components/ + -> index.ts or services/* +tests/ + -> services/*, components/*, docs/*, fixtures/* +docs/ + -> describes behavior only +``` + +Rules: + +- services stay pure and never import React, routing, app stores, browser-only + APIs, network clients, calendar clients, notification senders, wallet code, or + database clients. +- components may import local services and fixtures, but must not mount + themselves into the app shell or routes. +- tests may inspect local docs and fixtures, but must not depend on app-wide + integration state. +- docs and fixtures must use synthetic `.test` or `example.*` data only. + +## State Contract + +The engine returns advisory review models only: + +- `draft` when a reminder suggestion should be shown for explicit user review. +- `no_action` when the email is low confidence or has no actionable signal. + +A future UI owns loading and error presentation while gathering email input. The +engine remains synchronous and read-only. It never schedules, confirms, sends, +snoozes, completes, dismisses, saves, or mutates product state. + +## Future Contributor Rules + +Contributors may: + +- add folder-local docs, fixtures, tests, guards, engine helpers, hooks, and + components. +- extend signal detection when tests and fixture notes document the behavior. +- refine isolated components without changing shared design-system primitives. + +Contributors may not: + +- write outside `tools/v1/individual/follow-up-reminder/` for this tool issue. +- add live network calls, secrets, paid services, production data, mailbox + mutation, calendar writes, notification sends, wallet calls, Stellar calls, or + database persistence. +- import from app routes, inbox internals, auth, wallet, Stellar, database, + notification, analytics, or provider modules. + +## Review Checklist + +- Changed files are limited to this tool folder. +- Engine and guards remain folder-local and deterministic. +- Public exports continue to represent advisory reminder models only. +- Docs explain what future contributors may and may not change. +- No integration code, production data, or external services are introduced. diff --git a/tools/v1/individual/follow-up-reminder/fixtures/architecture-boundary-map.json b/tools/v1/individual/follow-up-reminder/fixtures/architecture-boundary-map.json new file mode 100644 index 000000000..9b7e72556 --- /dev/null +++ b/tools/v1/individual/follow-up-reminder/fixtures/architecture-boundary-map.json @@ -0,0 +1,74 @@ +{ + "tool": "Follow-up Reminder", + "root": "tools/v1/individual/follow-up-reminder/", + "releaseTier": "V1", + "audience": "individual", + "modules": [ + { + "name": "public-api", + "path": "index.ts", + "owns": "stable exports", + "mayImport": ["services/followUpReminder", "services/guards", "services/fixtures"], + "mustNotImport": ["src/routes", "src/features", "wallet", "stellar", "database"] + }, + { + "name": "engine", + "path": "services/followUpReminder.ts", + "owns": "reminder review model derivation", + "mayImport": [], + "mustNotImport": ["react", "network", "calendar", "notifications", "database"] + }, + { + "name": "guards", + "path": "services/guards.ts", + "owns": "input validation and sanitization", + "mayImport": ["services/followUpReminder"], + "mustNotImport": ["src/", "network", "secrets", "provider clients"] + }, + { + "name": "fixtures", + "path": "services/fixtures.ts", + "owns": "synthetic review data", + "mayImport": [], + "mustNotImport": ["production data", "network", "secrets"] + }, + { + "name": "components", + "path": "components/", + "owns": "isolated visual states", + "mayImport": ["index.ts", "services/followUpReminder", "services/fixtures"], + "mustNotImport": ["src/routes", "inbox internals", "wallet", "database"] + }, + { + "name": "tests", + "path": "tests/", + "owns": "contract verification", + "mayImport": ["services", "components", "docs", "fixtures"], + "mustNotImport": ["app shell", "production data", "provider clients"] + }, + { + "name": "docs", + "path": "docs/", + "owns": "architecture, threat, fixture, and review notes", + "mayImport": [], + "mustNotImport": ["credentials", "external setup secrets"] + } + ], + "forbiddenIntegrationAreas": [ + "main app shell", + "dashboard layout", + "navigation", + "routing", + "inbox architecture", + "mail rendering", + "authentication", + "wallet core", + "Stellar core", + "database schema", + "notifications", + "calendar writes", + "shared design system" + ], + "allowedStates": ["draft", "no_action"], + "uiOwnedStates": ["loading", "empty", "error", "review"] +} diff --git a/tools/v1/individual/follow-up-reminder/tests/architecture-boundaries.test.mjs b/tools/v1/individual/follow-up-reminder/tests/architecture-boundaries.test.mjs new file mode 100644 index 000000000..54badf6ce --- /dev/null +++ b/tools/v1/individual/follow-up-reminder/tests/architecture-boundaries.test.mjs @@ -0,0 +1,134 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { describe, it } from "node:test"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const root = join(__dirname, ".."); +const docs = join(root, "docs"); +const moduleMap = JSON.parse( + readFileSync(join(root, "fixtures", "architecture-boundary-map.json"), "utf8"), +); +const moduleBoundaries = readFileSync(join(docs, "MODULE_BOUNDARIES.md"), "utf8"); +const dataOwnership = readFileSync(join(docs, "DATA_OWNERSHIP_CONTRACT.md"), "utf8"); + +describe("module boundary documentation", () => { + it("states the folder-local scope and forbidden integration areas", () => { + assert.match(moduleBoundaries, /tools\/v1\/individual\/follow-up-reminder\//); + + for (const forbidden of [ + "app shell", + "routes", + "inbox internals", + "wallet", + "Stellar", + "database", + "notification", + "calendar", + "shared design-system", + ]) { + assert.ok(moduleBoundaries.includes(forbidden), `missing forbidden area: ${forbidden}`); + } + }); + + it("documents required layers for the self-contained mini-product", () => { + for (const layer of [ + "Public API", + "Engine", + "Guards", + "Fixtures", + "Components", + "Tests", + "Docs", + ]) { + assert.ok(moduleBoundaries.includes(layer), `missing layer: ${layer}`); + } + }); + + it("keeps engine state advisory and read-only", () => { + assert.match(moduleBoundaries, /draft/); + assert.match(moduleBoundaries, /no_action/); + assert.match(moduleBoundaries, /never schedules/); + assert.match(moduleBoundaries, /read-only/); + }); +}); + +describe("data ownership contract", () => { + it("defines owned local data and product data that remains out of scope", () => { + for (const owned of [ + "normalized email snapshots", + "reminder review models", + "signals and warnings", + ]) { + assert.ok(dataOwnership.includes(owned), `missing owned data: ${owned}`); + } + + for (const external of [ + "mailbox records", + "calendar events", + "notification jobs", + "wallet accounts", + ]) { + assert.ok(dataOwnership.includes(external), `missing external data: ${external}`); + } + }); + + it("requires future integration handoff before product side effects", () => { + assert.match(dataOwnership, /Integration Handoff/); + assert.match(dataOwnership, /explicit confirmation/); + assert.match(dataOwnership, /no production side effects/); + }); +}); + +describe("architecture boundary map fixture", () => { + it("is scoped to the Follow-up Reminder tool", () => { + assert.equal(moduleMap.tool, "Follow-up Reminder"); + assert.equal(moduleMap.root, "tools/v1/individual/follow-up-reminder/"); + assert.equal(moduleMap.releaseTier, "V1"); + assert.equal(moduleMap.audience, "individual"); + }); + + it("covers the expected module owners", () => { + const moduleNames = new Set(moduleMap.modules.map((entry) => entry.name)); + for (const expected of [ + "public-api", + "engine", + "guards", + "fixtures", + "components", + "tests", + "docs", + ]) { + assert.ok(moduleNames.has(expected), `missing module: ${expected}`); + } + }); + + it("gives every module import constraints", () => { + for (const entry of moduleMap.modules) { + assert.equal(typeof entry.path, "string"); + assert.equal(typeof entry.owns, "string"); + assert.ok(Array.isArray(entry.mayImport)); + assert.ok(Array.isArray(entry.mustNotImport)); + assert.ok(entry.mustNotImport.length > 0); + } + }); + + it("keeps integration areas and state ownership explicit", () => { + for (const area of [ + "routing", + "inbox architecture", + "wallet core", + "database schema", + "calendar writes", + ]) { + assert.ok( + moduleMap.forbiddenIntegrationAreas.includes(area), + `missing forbidden area: ${area}`, + ); + } + + assert.deepEqual(moduleMap.allowedStates, ["draft", "no_action"]); + assert.deepEqual(moduleMap.uiOwnedStates, ["loading", "empty", "error", "review"]); + }); +});