Skip to content
Open
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
1 change: 1 addition & 0 deletions DEBT.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Adjust this list in `scripts/check-todos.mjs` (`CRITICAL_PATHS`) and here togeth
|-------|----------|----------|-------|-------------|-------|
| #496 | `client/app/api/csp-report/route.ts` | high | _unassigned_ | Replace stubbed PayPal integration with real client | 2026-05-29 |
| #494 | `backend/...` | med | _unassigned_ | Price changes / consolidation suggestions fetched from DB | 2026-05-29 |
| #698 | `backend/src/services/api-latency-service.ts:68` | med | _unassigned_ | Redis-based API latency storage (currently in-memory fallback) | 2026-05-31 |

> The rows above are seeded from existing issue summaries (`ISSUE_496_*`,
> `ISSUE_494_*`). Verify the file paths and owners, then keep this table in sync
Expand Down
2 changes: 1 addition & 1 deletion backend/src/services/api-latency-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class ApiLatencyService {
try {
const redisStore = RateLimitRedisStore.getInstance();
if (redisStore.isAvailable()) {
// TODO: Implement Redis-based storage once we have access to raw Redis client
// TODO(#698): Implement Redis-based storage once we have access to raw Redis client
this.recordToMemory(family, latencyMs);
} else {
this.recordToMemory(family, latencyMs);
Expand Down
89 changes: 89 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# SYNCRO MVP Roadmap

**Backlog ID:** [#120](https://github.com/Calebux/SYNCRO/issues/120)
**Last updated:** 2026-05-31
**Source backlog:** [repo-issue-backlog-2026-05.md](./repo-issue-backlog-2026-05.md)

This document maps open work to MVP completion. Use it in sprint planning alongside [issue-triage-policy.md](./issue-triage-policy.md) and the [technical debt registry](../DEBT.md).

---

## MVP exit criteria

MVP is **done** when every criterion below is met. Each is verifiable in CI, ops dashboards, or a short manual checklist.

| # | Criterion | How to verify |
|---|-----------|---------------|
| E1 | **Security baseline** — no client-side service-role keys; unified Next.js config; CSP and auth middleware enforced on all API routes | CI `validate-dependencies` job; [backlog #35](./repo-issue-backlog-2026-05.md#35--p0-eliminate-accidental-service-role-usage-from-client-server-code), [#598](./repo-issue-backlog-2026-05.md#598--p0-unify-conflicting-nextjs-configuration-files) closed |
| E2 | **Core product flows** — register, login, MFA, subscription CRUD, reminders, and analytics work end-to-end against production Supabase | Smoke suite in [SMOKE_TESTS.md](./SMOKE_TESTS.md) green; manual checklist in [CurrentState.md](./archive/CurrentState.md#success-metrics) |
| E3 | **Payments** — Stripe and Paystack webhooks process live events; no stubbed payment paths in `client/app/api/` or `client/lib/` | Webhook integration tests pass; zero `severity:high` `tech-debt` issues in payments area |
| E4 | **Ops quality** — per-route bundle budgets enforced; no budget violations on `main` | CI [bundle-size workflow](../.github/workflows/bundle-size.yml); [backlog #697](./repo-issue-backlog-2026-05.md#697--p2-tighten-bundle-size-budgets-for-the-client-app) |
| E5 | **Technical debt gate** — every TODO/FIXME in critical paths references a tracked GitHub issue | `node scripts/check-todos.mjs` exits 0; [DEBT.md](../DEBT.md) registry matches code |
| E6 | **Blockchain (testnet)** — subscription registry, renewal, escrow, and virtual-card contracts deployed and exercised on Stellar testnet | Contract test suite green; deployment runbook signed off |
| E7 | **Production readiness** — Sentry alerting, correlation IDs, and rollback playbooks documented and exercised once | [SENTRY_ALERT_ROUTING.md](./SENTRY_ALERT_ROUTING.md), [MIGRATION_ROLLBACK_PLAYBOOKS.md](./MIGRATION_ROLLBACK_PLAYBOOKS.md) drill completed |

**Current MVP status:** ~90% — see [CurrentState.md](./archive/CurrentState.md). Primary gaps: live payment finalization (E3), mainnet contract audit (post-MVP), and remaining integration hardening (E7).

---

## Milestones & issue clusters

### M1 — Security & architecture foundation ✅

Foundation work that unblocks safe iteration. **Cluster complete.**

| Issue | Priority | Epic | Status |
|-------|----------|------|--------|
| [#35](https://github.com/Calebux/SYNCRO/issues/35) — Remove service-role from client | P0 | Client API security | ✅ Done — [backlog notes](./repo-issue-backlog-2026-05.md#35--p0-eliminate-accidental-service-role-usage-from-client-server-code) |
| [#598](https://github.com/Calebux/SYNCRO/issues/598) — Unify Next.js config | P0 | Architecture | ✅ Done — [backlog notes](./repo-issue-backlog-2026-05.md#598--p0-unify-conflicting-nextjs-configuration-files) |
| ADR-001 | — | Frontend/backend split | ✅ Accepted — [adr/ADR-001-frontend-backend-split.md](./adr/ADR-001-frontend-backend-split.md) |

### M2 — Production hardening 🔄

Hardening threads required before public MVP launch.

| Cluster | Representative issues | Area labels | Target |
|---------|----------------------|-------------|--------|
| **Payments & webhooks** | #496 (PayPal stub), Stripe/Paystack webhook hardening | `area/client`, `area/backend` | E3 |
| **Bundle & performance** | [#697](https://github.com/Calebux/SYNCRO/issues/697) bundle budgets, #698 Redis API latency storage | `area/client`, `area/backend` | E4 |
| **Integrations** | #494 (DB-backed suggestions), Telegram bot enrichment, email parsing | `area/backend`, `area/client` | E2 |
| **Tech debt governance** | [#120](https://github.com/Calebux/SYNCRO/issues/120) roadmap, DEBT.md policy | `area/governance` | E5 |

### M3 — MVP launch 🎯

Final gate before marking MVP complete.

| Cluster | Scope | Exit criteria |
|---------|-------|---------------|
| **Live payments** | Production webhook secrets, idempotency, DLQ handling | E3 — 7-day production webhook success rate ≥ 99% |
| **Smoke & E2E** | Full user journey in CI/staging | E2 — all smoke tests green on release branch |
| **Incident readiness** | On-call runbooks, CSP/payment drills | E7 — one documented drill per quarter |
| **Contracts (testnet)** | Soroban renewal + escrow flows | E6 — contract integration tests on testnet |

### M4 — Post-MVP (out of scope for exit)

Tracked but not blocking MVP: mainnet contract deployment, advanced email parsing ML, multi-region payment expansion.

---

## Completed issue clusters (historical)

Use these for context when grooming new work — patterns and resolutions are documented in the backlog archive.

| Cluster | Issues | Resolution doc |
|---------|--------|----------------|
| Client privilege escalation | #35 | [repo-issue-backlog-2026-05.md § #35](./repo-issue-backlog-2026-05.md#35--p0-eliminate-accidental-service-role-usage-from-client-server-code) |
| Next.js config drift | #598 | [repo-issue-backlog-2026-05.md § #598](./repo-issue-backlog-2026-05.md#598--p0-unify-conflicting-nextjs-configuration-files) |
| Bundle size enforcement | #697 | [repo-issue-backlog-2026-05.md § #697](./repo-issue-backlog-2026-05.md#697--p2-tighten-bundle-size-budgets-for-the-client-app) |
| Issue taxonomy & triage | #773 (merged) | [issue-triage-policy.md](./issue-triage-policy.md) |
| Tech debt CI policy | (this effort) | [DEBT.md](../DEBT.md), [INSTALL.md](../INSTALL.md) |

---

## How to use this roadmap

1. **Weekly grooming** — Pull the next open item from M2/M3 into sprint; confirm it maps to an exit criterion (E1–E7).
2. **New issues** — Tag with `area/`, `priority/`, and link the milestone (M2/M3) in the issue body.
3. **Closing MVP** — When E1–E7 are all green, open a release issue, run smoke tests, and update [CurrentState.md](./archive/CurrentState.md).

**Related docs:** [DEBT.md](../DEBT.md) · [issue-triage-policy.md](./issue-triage-policy.md) · [branch-protection.md](./branch-protection.md)
2 changes: 2 additions & 0 deletions docs/repo-issue-backlog-2026-05.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Repository Issue Backlog — May 2026

> **Roadmap:** Issue clusters and MVP exit criteria live in [ROADMAP.md](./ROADMAP.md).

## #35 — [P0] Eliminate accidental service-role usage from client server code

**Scope:** client-api
Expand Down
9 changes: 9 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ const eslintConfig = defineConfig([
"build/**",
"next-env.d.ts",
]),
{
rules: {
// Soft in-editor signal; authoritative enforcement is scripts/check-todos.mjs
"no-warning-comments": [
"warn",
{ terms: ["todo", "fixme"], location: "anywhere" },
],
},
},
]);

export default eslintConfig;
124 changes: 74 additions & 50 deletions scripts/check-todos.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import { readFileSync, statSync } from "node:fs";
import { execSync } from "node:child_process";
import { pathToFileURL } from "node:url";

// --- Configuration ---------------------------------------------------------

Expand Down Expand Up @@ -78,39 +79,39 @@ function isCritical(path) {
);
}

// --- Scan -------------------------------------------------------------------

const warnOnly = process.argv.includes("--warn-only");

const blocking = [];
const warnings = [];

for (const file of getTrackedFiles()) {
if (isIgnored(file)) continue;
if (file.endsWith("check-todos.mjs")) continue; // don't scan the scanner
if (!SOURCE_EXTENSIONS.has(extOf(file))) continue;

let content;
try {
if (statSync(file).isDirectory()) continue;
content = readFileSync(file, "utf8");
} catch {
continue; // deleted / unreadable
function runScan({ warnOnly = false } = {}) {
const blocking = [];
const warnings = [];

for (const file of getTrackedFiles()) {
if (isIgnored(file)) continue;
if (file.endsWith("check-todos.mjs")) continue; // don't scan the scanner
if (!SOURCE_EXTENSIONS.has(extOf(file))) continue;

let content;
try {
if (statSync(file).isDirectory()) continue;
content = readFileSync(file, "utf8");
} catch {
continue; // deleted / unreadable
}

const lines = content.split("\n");
lines.forEach((line, i) => {
if (!TODO_TOKEN.test(line)) return;
if (TRACKED_TODO.test(line)) return; // properly tracked, OK

const entry = {
file,
line: i + 1,
text: line.trim().slice(0, 120),
};
if (isCritical(file)) blocking.push(entry);
else warnings.push(entry);
});
}

const lines = content.split("\n");
lines.forEach((line, i) => {
if (!TODO_TOKEN.test(line)) return;
if (TRACKED_TODO.test(line)) return; // properly tracked, OK

const entry = {
file,
line: i + 1,
text: line.trim().slice(0, 120),
};
if (isCritical(file)) blocking.push(entry);
else warnings.push(entry);
});
return { blocking, warnings };
}

// --- Report -----------------------------------------------------------------
Expand All @@ -122,27 +123,50 @@ function printGroup(label, entries) {
}
}

if (warnings.length) {
printGroup("⚠️ Untracked TODO/FIXME (non-critical, warning)", warnings);
}
function main() {
const warnOnly = process.argv.includes("--warn-only");
const { blocking, warnings } = runScan({ warnOnly });

if (blocking.length) {
printGroup("❌ Untracked TODO/FIXME in CRITICAL paths", blocking);
console.log(
"\nEvery TODO/FIXME in critical paths must reference a GitHub issue:\n" +
" // TODO(#123): short description\n" +
" // FIXME(#123): short description\n\n" +
"1. Open an issue, label it `tech-debt` + a severity.\n" +
"2. Reference its number in the comment.\n" +
"3. Add a row to DEBT.md.\n"
);
if (!warnOnly) {
process.exit(1);
if (warnings.length) {
printGroup("⚠️ Untracked TODO/FIXME (non-critical, warning)", warnings);
}

if (blocking.length) {
printGroup("❌ Untracked TODO/FIXME in CRITICAL paths", blocking);
console.log(
"\nEvery TODO/FIXME in critical paths must reference a GitHub issue:\n" +
" // TODO(#123): short description\n" +
" // FIXME(#123): short description\n\n" +
"1. Open an issue, label it `tech-debt` + a severity.\n" +
"2. Reference its number in the comment.\n" +
"3. Add a row to DEBT.md.\n"
);
if (!warnOnly) {
process.exit(1);
}
}

if (!blocking.length && !warnings.length) {
console.log("✅ No untracked TODO/FIXME found.");
} else if (!blocking.length) {
console.log("\n✅ No blocking issues (critical paths are clean).");
}
}

if (!blocking.length && !warnings.length) {
console.log("✅ No untracked TODO/FIXME found.");
} else if (!blocking.length) {
console.log("\n✅ No blocking issues (critical paths are clean).");
const isMain =
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href;

if (isMain) {
main();
}

export {
CRITICAL_PATHS,
SOURCE_EXTENSIONS,
TODO_TOKEN,
TRACKED_TODO,
isCritical,
extOf,
isIgnored,
};
41 changes: 41 additions & 0 deletions scripts/check-todos.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
TRACKED_TODO,
TODO_TOKEN,
isCritical,
extOf,
isIgnored,
} from "./check-todos.mjs";

describe("check-todos policy", () => {
it("detects TODO/FIXME tokens", () => {
assert.equal(TODO_TOKEN.test("// TODO: bare"), true);
assert.equal(TODO_TOKEN.test("// FIXME(#496): tracked"), true);
assert.equal(TODO_TOKEN.test("const autodocs = 1"), false);
});

it("accepts GitHub issue references in tracked format", () => {
assert.equal(TRACKED_TODO.test("// TODO(#491): migrate SDK"), true);
assert.equal(TRACKED_TODO.test("// FIXME(#496): stub"), true);
assert.equal(TRACKED_TODO.test("// TODO: no ref"), false);
assert.equal(TRACKED_TODO.test("// TODO(#abc): bad ref"), false);
});

it("classifies critical paths for auth, payments, and integrations", () => {
assert.equal(isCritical("client/lib/payment-service.ts"), true);
assert.equal(isCritical("client/app/api/auth/route.ts"), true);
assert.equal(isCritical("backend/src/services/telegram-bot-service.ts"), true);
assert.equal(isCritical("client/app/page.tsx"), false);
assert.equal(isCritical("docs/ROADMAP.md"), false);
});

it("ignores build artifacts and scans source extensions only", () => {
assert.equal(isIgnored("client/node_modules/foo.ts"), true);
assert.equal(isIgnored("client/lib/foo.ts"), false);
assert.equal(extOf("backend/src/x.ts"), ".ts");
assert.equal(extOf("README"), "");
});
});