Skip to content
Merged
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
2 changes: 1 addition & 1 deletion docs/revamp/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Roadmap, status checklist, prompt pack, and vision docs committed to the repo.

### Milestone 6 — Extensions & connectors
- **6.1 Extension registry + MT providers as built-in addons — M — deps: 0.1.** `core/extensions/types.ts` (`ExtensionManifest {id, name, version, kinds, permissions, builtIn}`; kinds: `mt-provider|qa-rule|panel|import-format|export-format|storage-connector`), `registry.ts` (enablement persisted in settings), `builtins.ts`; `src/extensions/mt/*` manifest wrappers delegating to existing `src/core/mt/*` (no logic moves); `mt/index.ts` resolves via registry. Existing MT tests untouched prove behavior preservation. **DoD**: four MT providers run through registry; disabling removes from panel. *Shipped: `core/extensions/{types,registry,builtins}.ts` (dependency-free singleton, enablement as a disabled set, `subscribe`) + `src/extensions/mt` wrappers; `core/mt` `enabledProviders` + `useMTSettings` AND the user setting with the registry (reactive via `useSyncExternalStore`); disabled set persisted under the device-local `extensions.disabled` key, registered from `main.tsx`. No Add-ons page yet (6.2).*
- **6.2 QA rules + formats as addons + Add-ons page — M — deps: 6.1.** Wrap `QA_CODES` rules and XLIFF/TMX/TBX/CSV/DOCX-export as contributions; `features/addons/AddonsPage.tsx` (catalogue, enable/disable, permission display, "suggested/built-in" UX); `/addons` route + nav. QA falls back to all-on when registry empty (keeps tests). **DoD**: Add-ons page lists everything pluggable; toggling a QA addon changes QA output.
- **6.2 QA rules + formats as addons + Add-ons page — M — deps: 6.1.** Wrap `QA_CODES` rules and XLIFF/TMX/TBX/CSV/DOCX-export as contributions; `features/addons/AddonsPage.tsx` (catalogue, enable/disable, permission display, "suggested/built-in" UX); `/addons` route + nav. QA falls back to all-on when registry empty (keeps tests). **DoD**: Add-ons page lists everything pluggable; toggling a QA addon changes QA output. *Shipped: QA (`qa.<code>`) + format (`format.*`) manifests in `core/extensions/builtins`; `core/qa/registryRules` (`effectiveQaRules` ANDs toggles with the registry, defaults on when unregistered) wired into `QAPanel` (reactive); `features/addons/AddonsPage` at `/addons` (+ nav) grouping addons by kind with permission chips + built-in badges — MT + QA toggle (enforced), formats shown Always-on (format-level gating deferred).*
- **6.3 Google Drive connector — M — deps: 6.1, 2.4.** `src/extensions/connectors/gdrive/` (Google Identity Services token client, `drive.file` scope — avoids verification burden; REST list/download/upload; token in memory/session only), generic `ConnectorFilePicker`; "From cloud" in ImportDialog + "Save to cloud" on DOCX export. Pure client OAuth — works for local-only users too. **DoD**: import DOCX from Drive; export back to Drive.
- **6.4 OneDrive connector — S — deps: 6.3.** `src/extensions/connectors/onedrive/` via dynamic `@azure/msal-browser` (`loginPopup` — avoids redirect/hash-router interplay), Graph `Files.ReadWrite`. **DoD**: same loop on OneDrive; both connectors appear as addons with permission labels. *Milestone 6 = extensions + connectors shipped.*

Expand Down
5 changes: 3 additions & 2 deletions docs/revamp/STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ Rules for autonomous sessions:
| 5.1 | Members & roles management | M | 4.1 | done (PR #60) |
| 5.2 | Role-gated editing workflow | M | 5.1, 1.6 | done (PR #61) |
| 5.3 | Approval workflow + attribution in versioning | S | 5.2 | done (PR #62) |
| 6.1 | Extension registry + MT providers as built-in addons | M | 0.1 | in-review (PR #63) |
| 6.2 | QA rules + formats as addons + Add-ons page | M | 6.1 | pending |
| 6.1 | Extension registry + MT providers as built-in addons | M | 0.1 | done (PR #63) |
| 6.2 | QA rules + formats as addons + Add-ons page | M | 6.1 | in-review (PR #64) |
| 6.3 | Google Drive connector | M | 6.1, 2.4 | pending |
| 6.4 | OneDrive connector | S | 6.3 | pending |

Expand Down Expand Up @@ -79,3 +79,4 @@ Rules for autonomous sessions:
- 2026-07-21 — Phase 5.1 merged in PR #60. Phase 5.2 built: role-gated editing workflow. `src/core/workflow/rules.ts` — the pure heart: `workflowCapabilities(role, stage, status) → {canEditDirect, mustSuggest, canResolveChanges, canReview, canManage}` (PM manages + edits through translation/review and is the only role that touches a `final` project; translator edits directly in translation but is **forced to suggest** in review, and must suggest on an already-`reviewed` segment; revisor waits during translation then edits + accepts/rejects in review; accept/reject is revisor/PM-only), plus `effectiveRoleStage(cloud)` (local-only → **PM-of-self**: project_manager/translation, every capability granted → zero regression) and `forcesSuggesting(role, stage)`. `supabase/migrations/0009_workflow.sql` — a `project_stage` enum + `projects.stage` (default `translation`) + `deadline`; writes are project_manager-only through the **existing** `projects_update` policy (no new policy). Numbering deviation: ROADMAP §5.2 sketched `0007_workflow.sql`, but `0007`/`0008` were taken (compaction + members), so it ships as `0009`. `WorkflowStage` added to `core/types`; `Project.cloud` gains `stage?`/`deadline?` (read on open). `projectCloud.ts` — `fetchCloudProject` now returns `stage`/`deadline`, `openCloudProject` stores them, and `setCloudProjectStage` + the `changeProjectStage(projectId, stage)` orchestration (writes Postgres — RLS rejects a non-PM — then patches the local `project.cloud.stage`). Wiring: a tiny `features/editor/workflow/useWorkflowStore` (default permissive PM/translation) is published by `EditorPage` from `effectiveRoleStage(project.cloud)`; `EditModeToggle` gains a `forced` prop (locked + disabled) driven by `forcesSuggesting`, and `EditorPage` pins `editMode='suggesting'` when forced; `ChangesPanel` (accept-all/reject-all + per-suggestion buttons) and `ChangeHoverCard` hide their resolve affordances when `!canResolveChanges`; a PM stage `<Select>` (+ read-only badge for non-PMs) lives in `ManageMembersDialog`. Deviations (noted): built on the designated session branch, not `claude/revamp-phase-5-2`; content-level rules are client-enforced per D8 (the append-only authored log is the audit trail), stated honestly. `docs/cloud.md` (§3 migration list + §12 manual role-matrix). Tests: `workflow.rules` (full role × stage × status matrix incl. reviewed-segment suggest, final lock, local-only PM-of-self, `forcesSuggesting`), `cloud.projectCloud` extended (stage read defaults to translation + `setCloudProjectStage` update). Verified: flag-OFF `dist` has **zero** `GoTrueClient`; the tracked-changes e2e still green (local-only = PM keeps accept/reject). Green: 671 unit (+10) / 8 tracked-changes e2e / build. No new e2e (ROADMAP §5.2 asks none; the full role matrix needs three real accounts, per the §12 manual matrix).
- 2026-07-21 — Phase 5.2 merged in PR #61. Phase 5.3 built: approval workflow + unified attribution in versioning (**Milestone 5 complete → web v2 launch candidate**). `ProjectVersion` gains `authorId?` (the stable `profile.identity` author id — the *same* identity tracked-change marks, comments, and presence use, so attribution is unified across the app, D8) and `approval?: { authorId, authorName, at }`. `versionRepo.capture` now stamps `authorId` on **every** version (via `ensureLocalAuthor()`), and a new `versionRepo.signOff(projectId, liveDoc?)` records a `named` version labeled `Approved by {name}` with the `approval` metadata set (its presence marks an approval milestone). Wiring: `useWorkflowStore` gains a `useCanReview()` selector (revisor/PM; local-only → PM-of-self → true); `VersionHistoryPanel` shows a **Sign off** button beside Save version only when `canReview`, and renders approval versions with an accent `BadgeCheck` + their `Approved by …` label. No migration + no schema bump (versions are local Dexie; `authorId`/`approval` are additive unindexed fields; pre-5.3 rows simply lack them). Deviations (noted): built on the designated session branch, not `claude/revamp-phase-5-3`; sign-off gating is client-enforced via the workflow rules at the call site (D8) — the append-only authored update log remains the tamper-evident audit trail. Tests: `versionRepo` extended (every capture stamps a stable authorId consistent across captures; `signOff` records a labeled approval version whose `approval.authorId`/`at` match the version's `authorId`/`createdAt`, while a plain `saveNamed` has no `approval`). Verified: flag-OFF `dist` has **zero** `GoTrueClient`; the version-history e2e still green. Green: 673 unit (+2) / 1 version e2e / build. No new e2e (ROADMAP §5.3 asks none). **Milestone 5 (roles & workflow) complete — the web v2 launch candidate.**
- 2026-07-21 — Phase 5.3 merged in PR #62. Phase 6.1 built: the typed extension registry + the four MT providers re-registered as built-in addons (start of **Milestone 6**, extensions & connectors; D9 — in-process registry now, sandboxed hosting later, but the manifest contract is **final**). `src/core/extensions/types.ts` — `ExtensionManifest {id, name, version, kinds, permissions, builtIn, description?}`; `ExtensionKind = mt-provider|qa-rule|panel|import-format|export-format|storage-connector`; `ExtensionPermission = network|credentials|storage|clipboard|filesystem`. `src/core/extensions/registry.ts` — a dependency-free (no storage/React imports) singleton `extensionRegistry`: `register`/`unregister`/`get`/`has`/`list(kind?)` (id-sorted), enablement as a **disabled set** so a registered extension is enabled by default and an unknown id is treated as enabled (behaviour unchanged until something is explicitly turned off), `isEnabled`/`setEnabled`/`getDisabledIds`/`setDisabledIds`, and `subscribe`. `src/core/extensions/builtins.ts` — the four MT manifests (`mt.mymemory`/`mt.libretranslate`/`mt.ollama`/`mt.claude`; permissions `network` + `credentials` for the keyed ones) + idempotent `registerBuiltinExtensions()`. `src/extensions/mt/index.ts` — thin **wrappers** pairing each manifest to its existing `core/mt` provider (**no provider logic moved** — the existing MT unit tests are untouched and keep proving behaviour) + `registryEnabledMtProviderIds()`. `core/mt/index.ts` gains `MT_EXTENSION_ID` + `isProviderExtensionEnabled(id)` (defaults enabled when the manifest isn't registered), and `enabledProviders` now ANDs the user setting with the registry — so **disabling the addon removes the provider**; `useMTSettings.enabledIds` does the same and re-derives via `useSyncExternalStore(extensionRegistry.subscribe)` so the MT panel updates live. Wiring: `features/addons/registryPersistence.ts` (`registerExtensions()` sync + `startExtensionRegistry()` hydrating the disabled set from the new device-local `extensions.disabled` settings key and persisting subsequent changes), started from `main.tsx` before render. Deviations (noted): built on the designated session branch, not `claude/revamp-phase-6-1`; no Add-ons **page** yet (that's 6.2) — 6.1 ships the registry + reactive resolution and proves the disable→panel path via unit tests. `docs/cloud.md` unchanged (no migration). Tests: `extensions.registry` (register/list-by-kind/default-enabled/toggle/unknown-id/hydrate+notify/unregister), `extensions.mt` (the four register as `mt-provider`; wrappers point at the real `MT_PROVIDERS`; **disabling `mt.claude` removes it from `enabledProviders` even with the user setting on** — the DoD — and re-enabling restores it). Verified: flag-OFF `dist` Supabase-free; the untouched MT unit tests + `mt-flow` e2e stay green. Green: 682 unit (+9) / 3 mt-flow e2e / build. No new e2e (ROADMAP §6.1 asks none).
- 2026-07-21 — Phase 6.1 merged in PR #63. Phase 6.2 built: QA rules + formats as addons + the Add-ons page. `core/extensions/builtins.ts` now also registers `QA_EXTENSION_MANIFESTS` (one `qa.<code>` per QA rule, kind `qa-rule`, no permissions — labels/descriptions from `QA_RULE_LABELS`) and `FORMAT_EXTENSION_MANIFESTS` (`format.xliff`/`format.docx`/`format.tmx`/`format.tbx`/`format.csv`, kinds `import-format`/`export-format`, `filesystem` permission); `BUILTIN_MANIFESTS` = MT + QA + formats, all registered by `registerBuiltinExtensions()`. `core/qa/registryRules.ts` — `isQaRuleEnabled(code)` (registry, **defaults on when the manifest isn't registered** so `runQA` stays pure and its tests are untouched) + `effectiveQaRules(toggles)` which ANDs the per-project rule toggles with the registry; `QAPanel` runs `runQA` with `effectiveQaRules(settings.qaRules)` and re-derives via `useSyncExternalStore(extensionRegistry.subscribe)` so toggling a QA addon changes QA output live. `features/addons/AddonsPage.tsx` — the `/addons` catalogue: sections per kind (Machine translation, Quality assurance, Import & export formats), each row showing the name, description, **permission chips**, and a Built-in badge; MT + QA rows get a working enable/disable toggle (enforced — MT panel + QA output react), formats are shown **Always on** (their round-trip logic stays in `core/*`; format-level gating is a follow-up, kept out to avoid touching every import/export path). New `/addons` route (lazy `AddonsPage`) + a Puzzle nav item. Deviations (noted): built on the designated session branch, not `claude/revamp-phase-6-2`; format addons are catalogue/display-only in v1 (no toggle) — only the DoD's QA + the 6.1 MT toggles are enforced. No migration. Tests: `qa.registryRules` (falls back all-on when unregistered; disabling `qa.double_space` forces it off + **removes the finding from `runQA` output** — the DoD; others unaffected), `AddonsPage` (lists MT + QA + format sections/rows; toggling a QA addon flips registry enablement; formats have no toggle). Verified: flag-OFF `dist` Supabase-free (AddonsPage its own lazy chunk); full e2e green after the nav/route addition. Green: 688 unit (+6) / 29 e2e / build. No new e2e (ROADMAP §6.2 asks none).
2 changes: 2 additions & 0 deletions src/app/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const TerminologyPage = lazyWithRetry(() => import('@/features/terminology'), 't
const CorporaPage = lazyWithRetry(() => import('@/features/corpora'), 'corpora')
const GuidePage = lazyWithRetry(() => import('@/features/guide'), 'guide')
const SettingsPage = lazyWithRetry(() => import('@/features/settings'), 'settings')
const AddonsPage = lazyWithRetry(() => import('@/features/addons/AddonsPage'), 'addons')
const AboutPage = lazyWithRetry(() => import('@/features/about'), 'about')

function RouteFallback() {
Expand All @@ -39,6 +40,7 @@ export function AppRoutes() {
<Route path="/corpora" element={<CorporaPage />} />
<Route path="/guide" element={<GuidePage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/addons" element={<AddonsPage />} />
<Route path="/about" element={<AboutPage />} />
</Routes>
</Suspense>
Expand Down
3 changes: 2 additions & 1 deletion src/components/layout/navItems.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FolderOpen, Database, BookA, Languages, Library, Settings } from 'lucide-react'
import { FolderOpen, Database, BookA, Languages, Library, Settings, Puzzle } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'

export interface NavItem {
Expand All @@ -14,5 +14,6 @@ export const NAV_ITEMS: NavItem[] = [
{ to: '/tm', icon: Database, label: 'TM', end: false },
{ to: '/terminology', icon: BookA, label: 'Glossary', end: false },
{ to: '/corpora', icon: Library, label: 'Corpora', end: false },
{ to: '/addons', icon: Puzzle, label: 'Add-ons', end: false },
{ to: '/settings', icon: Settings, label: 'Settings', end: false },
]
72 changes: 71 additions & 1 deletion src/core/extensions/builtins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { ExtensionManifest } from './types'
import { extensionRegistry } from './registry'
import { QA_CODES, QA_RULE_LABELS } from '@/core/qa/types'

/**
* Built-in extension manifests (ROADMAP §6.1). The four MT providers are the
Expand Down Expand Up @@ -49,13 +50,82 @@ export const MT_EXTENSION_MANIFESTS: ExtensionManifest[] = [
},
]

/** QA rules as addons (ROADMAP §6.2) — one manifest per rule so it can be
* toggled from the Add-ons page; disabling one drops it from QA output. */
export const QA_EXTENSION_MANIFESTS: ExtensionManifest[] = QA_CODES.map((code) => ({
id: `qa.${code}`,
name: QA_RULE_LABELS[code],
version: '1.0.0',
kinds: ['qa-rule'],
permissions: [],
builtIn: true,
description: 'Quality-assurance check run over the project’s segments.',
}))

/** Import/export formats as addons (ROADMAP §6.2). Listed in the catalogue with
* their `filesystem` permission; the round-trip logic stays in core/*. */
export const FORMAT_EXTENSION_MANIFESTS: ExtensionManifest[] = [
{
id: 'format.xliff',
name: 'XLIFF 1.2 (bilingual)',
version: '1.0.0',
kinds: ['import-format', 'export-format'],
permissions: ['filesystem'],
builtIn: true,
description: 'Round-trip bilingual XLIFF from other CAT tools.',
},
{
id: 'format.docx',
name: 'Word (.docx)',
version: '1.0.0',
kinds: ['import-format', 'export-format'],
permissions: ['filesystem'],
builtIn: true,
description: 'Import a Word document and export the clean translation.',
},
{
id: 'format.tmx',
name: 'TMX (translation memory)',
version: '1.0.0',
kinds: ['import-format', 'export-format'],
permissions: ['filesystem'],
builtIn: true,
description: 'Exchange translation memory with other tools.',
},
{
id: 'format.tbx',
name: 'TBX (term base)',
version: '1.0.0',
kinds: ['import-format'],
permissions: ['filesystem'],
builtIn: true,
description: 'Import terminology from a TBX term base.',
},
{
id: 'format.csv',
name: 'CSV (glossary)',
version: '1.0.0',
kinds: ['import-format', 'export-format'],
permissions: ['filesystem'],
builtIn: true,
description: 'Import/export glossary terms as CSV.',
},
]

/** Every built-in manifest, in catalogue order (MT, then QA, then formats). */
export const BUILTIN_MANIFESTS: ExtensionManifest[] = [
...MT_EXTENSION_MANIFESTS,
...QA_EXTENSION_MANIFESTS,
...FORMAT_EXTENSION_MANIFESTS,
]

let registered = false

/** Register every built-in manifest. Idempotent — safe to call on each startup. */
export function registerBuiltinExtensions(): void {
if (registered) return
registered = true
for (const manifest of MT_EXTENSION_MANIFESTS) extensionRegistry.register(manifest)
for (const manifest of BUILTIN_MANIFESTS) extensionRegistry.register(manifest)
}

/** Test helper: allow re-registration after a registry reset. */
Expand Down
36 changes: 36 additions & 0 deletions src/core/qa/registryRules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { extensionRegistry } from '@/core/extensions/registry'
import { QA_CODES, type QACode, type QARuleToggles } from './types'

/**
* Fold the extension registry's QA-rule enablement into the per-project rule
* toggles (ROADMAP §6.2). A rule runs only when its user toggle is on AND its
* `qa.<code>` addon is enabled — so disabling the addon on the Add-ons page
* removes it from QA output. Falls back to **all-on** when the manifest isn't
* registered (e.g. a unit test that never registers built-ins), so `runQA` stays
* pure and its existing tests are unaffected.
*/

/** The registry manifest id for a QA rule. */
export function qaExtensionId(code: QACode): string {
return `qa.${code}`
}

/** Whether a QA rule's addon is enabled (default true when not registered). */
export function isQaRuleEnabled(code: QACode): boolean {
const id = qaExtensionId(code)
return !extensionRegistry.has(id) || extensionRegistry.isEnabled(id)
}

/**
* AND the given rule toggles with the registry: any rule whose addon is disabled
* is forced off; the rest keep their toggle value.
*/
export function effectiveQaRules(
toggles: Partial<QARuleToggles>,
): Partial<QARuleToggles> {
const out: Partial<QARuleToggles> = { ...toggles }
for (const code of QA_CODES) {
if (!isQaRuleEnabled(code)) out[code] = false
}
return out
}
Loading
Loading