Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -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.
92 changes: 92 additions & 0 deletions tools/v1/individual/follow-up-reminder/docs/MODULE_BOUNDARIES.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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"]
}
Original file line number Diff line number Diff line change
@@ -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"]);
});
});