diff --git a/.env.example b/.env.example index 9b4f21fa..7931e53c 100644 --- a/.env.example +++ b/.env.example @@ -5,5 +5,6 @@ # Browser-assisted imports are disabled by default. Enable only for local # development with a random token that is also configured in the extension. +# Generate one with `openssl rand -hex 32`; weak tokens are rejected. TRAUMA_BROWSER_IMPORT_ENABLED=false TRAUMA_BROWSER_IMPORT_TOKEN= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a017ddb4..96f21d19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,7 @@ on: - "src/**" - "tests/**" - "e2e/**" + - "extensions/**" - "public/**" - "drizzle/**" - "scripts/**" @@ -20,6 +21,7 @@ on: - "package.json" - "bun.lock" - "mise.toml" + - ".env.example" - "trauma.config.example.json" pull_request: branches: @@ -29,6 +31,7 @@ on: - "src/**" - "tests/**" - "e2e/**" + - "extensions/**" - "public/**" - "drizzle/**" - "scripts/**" @@ -40,6 +43,7 @@ on: - "package.json" - "bun.lock" - "mise.toml" + - ".env.example" - "trauma.config.example.json" workflow_dispatch: @@ -58,21 +62,37 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false - name: Set up Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.13 - name: Install dependencies run: bun install --frozen-lockfile + - name: Audit dependencies + run: bun run audit + - name: Run baseline verification run: bun run verify - name: Install Playwright browser run: bunx playwright install --with-deps chromium - - name: Run E2E smoke tests + - name: Run E2E tests run: bun run test:e2e + + - name: Upload Playwright failure artifacts + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + if-no-files-found: ignore + name: playwright-failures-${{ github.run_id }}-${{ github.run_attempt }} + path: | + playwright-report/ + test-results/ + retention-days: 7 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..a4dd4008 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,55 @@ +name: Documentation + +on: + push: + branches: + - main + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "AGENTS.md" + - "Backlog.md" + - "CLAUDE.md" + - "README.md" + - "scripts/check-docs.ts" + - "package.json" + pull_request: + branches: + - main + paths: + - ".github/workflows/docs.yml" + - "docs/**" + - "AGENTS.md" + - "Backlog.md" + - "CLAUDE.md" + - "README.md" + - "scripts/check-docs.ts" + - "package.json" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docs-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Check documentation + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out repository + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + persist-credentials: false + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.13 + + - name: Check documentation + run: bun run docs:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 00fdbd19..8de94654 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,87 +7,136 @@ on: - "v*.*.*" permissions: - contents: write + contents: read concurrency: group: release-${{ github.ref }} cancel-in-progress: false jobs: - release: - name: Create GitHub Release + validate: + name: Validate release tag runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 5 + outputs: + tag_name: ${{ steps.release_tag.outputs.tag_name }} + valid: ${{ steps.release_tag.outputs.valid }} + version: ${{ steps.release_tag.outputs.version }} steps: + - name: Check out tagged commit + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 1 + persist-credentials: false + ref: ${{ github.sha }} + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.3.13 + - name: Validate release tag id: release_tag - shell: bash - run: | - TAG_NAME="${GITHUB_REF_NAME}" + run: bun run scripts/validate-release.ts - if [[ ! "$TAG_NAME" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "::notice title=Skipped release::Tag '$TAG_NAME' is not a three-part semantic version." - echo "valid=false" >> "$GITHUB_OUTPUT" - exit 0 - fi + verify: + name: Verify release + needs: validate + if: needs.validate.outputs.valid == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read - echo "valid=true" >> "$GITHUB_OUTPUT" - echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT" - echo "version=${TAG_NAME#v}" >> "$GITHUB_OUTPUT" + steps: - name: Check out repository - if: steps.release_tag.outputs.valid == 'true' - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: fetch-depth: 0 + persist-credentials: false + ref: ${{ github.sha }} - name: Set up Bun - if: steps.release_tag.outputs.valid == 'true' - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.3.13 - name: Install dependencies - if: steps.release_tag.outputs.valid == 'true' run: bun install --frozen-lockfile + - name: Audit dependencies + run: bun run audit + - name: Run baseline verification - if: steps.release_tag.outputs.valid == 'true' run: bun run verify - name: Install Playwright browser - if: steps.release_tag.outputs.valid == 'true' run: bunx playwright install --with-deps chromium - - name: Run E2E smoke tests - if: steps.release_tag.outputs.valid == 'true' + - name: Run E2E tests run: bun run test:e2e + - name: Upload Playwright failure artifacts + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + if-no-files-found: ignore + name: playwright-failures-${{ github.run_id }}-${{ github.run_attempt }} + path: | + playwright-report/ + test-results/ + retention-days: 7 + + release: + name: Create GitHub Release + needs: [validate, verify] + if: needs.validate.outputs.valid == 'true' && needs.verify.result == 'success' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + + steps: + - name: Check out tagged commit + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 1 + persist-credentials: false + ref: ${{ github.sha }} + - name: Create release - if: steps.release_tag.outputs.valid == 'true' env: GH_TOKEN: ${{ github.token }} - TAG_NAME: ${{ steps.release_tag.outputs.tag_name }} - RELEASE_VERSION: ${{ steps.release_tag.outputs.version }} + TAG_NAME: ${{ needs.validate.outputs.tag_name }} + RELEASE_VERSION: ${{ needs.validate.outputs.version }} run: | if gh release view "$TAG_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then echo "Release $TAG_NAME already exists." exit 0 fi - RELEASE_NOTES_FILE="changelog/v${RELEASE_VERSION}.md" + RELEASE_NOTES_PATH="changelog/v${RELEASE_VERSION}.md" + + if [[ -f "$RELEASE_NOTES_PATH" ]]; then + if [[ ! -r "$RELEASE_NOTES_PATH" ]]; then + echo "::error title=Invalid release notes path::$RELEASE_NOTES_PATH is not readable." + exit 1 + fi - if [[ -f "$RELEASE_NOTES_FILE" ]]; then gh release create "$TAG_NAME" \ --repo "$GITHUB_REPOSITORY" \ --title "TRAUMA $RELEASE_VERSION" \ - --notes-file "$RELEASE_NOTES_FILE" \ + --notes-file "$RELEASE_NOTES_PATH" \ --verify-tag - else + elif [[ ! -e "$RELEASE_NOTES_PATH" && ! -L "$RELEASE_NOTES_PATH" ]]; then gh release create "$TAG_NAME" \ --repo "$GITHUB_REPOSITORY" \ --title "TRAUMA $RELEASE_VERSION" \ --generate-notes \ --verify-tag + else + echo "::error title=Invalid release notes path::$RELEASE_NOTES_PATH is not a regular file." + exit 1 fi diff --git a/.gitignore b/.gitignore index 79a5be61..3f3e99f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .superpowers/ .eda/ +.tmp/ +app.config.timestamp_*.js +.claude/settings.local.json node_modules/ .vinxi/ .output/ diff --git a/AGENTS.md b/AGENTS.md index 902ff35d..6a143902 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,26 +3,13 @@ Trauma is a TypeScript bookmark management app. The project domain term is `memory` for one bookmark and `memories` for the collection. -## Documentation Map +Start with [docs/INDEX.md](docs/INDEX.md). It is the authoritative map for +current architecture, contracts, operations, and verification. Historical +records are context only and must not override current semantic docs, code, or +tests. -Start with [docs/INDEX.md](docs/INDEX.md). - -Key project decisions are split by purpose: - -- Architecture: [docs/architecture/overview.md](docs/architecture/overview.md) -- Data and storage: [docs/architecture/data-and-storage.md](docs/architecture/data-and-storage.md) -- Runtime flows: [docs/architecture/flows.md](docs/architecture/flows.md) -- UI and routing: [docs/architecture/ui-and-routing.md](docs/architecture/ui-and-routing.md) -- Technology stack: [docs/references/technology-stack.md](docs/references/technology-stack.md) -- Configuration: [docs/references/configuration.md](docs/references/configuration.md) -- Coding standards: [docs/references/coding-standards/INDEX.md](docs/references/coding-standards/INDEX.md) -- Glossary: [docs/references/glossary.md](docs/references/glossary.md) -- Operations: [docs/operations/local-self-hosting.md](docs/operations/local-self-hosting.md) -- Verification: [docs/quality/verification.md](docs/quality/verification.md) -- Execution workflows: [docs/workflows/README.md](docs/workflows/README.md) - -The approved foundation design record is -[docs/superpowers/specs/2026-05-09-trauma-foundation-design.md](docs/superpowers/specs/2026-05-09-trauma-foundation-design.md). +Open durable work belongs in [Backlog.md](Backlog.md). Keep task-specific plans +temporary and move lasting requirements into the owning semantic document. Commit finalization exclusions for Sawyer live in [.sawyer/exclude-whitelist.txt](.sawyer/exclude-whitelist.txt). diff --git a/Backlog.md b/Backlog.md new file mode 100644 index 00000000..0eb5ab53 --- /dev/null +++ b/Backlog.md @@ -0,0 +1,7 @@ +# Backlog + +This file contains durable open outcomes only. Keep it under 20 items, remove +completed items, and put current behavior in semantic docs rather than expanding +this file into implementation plans. + +No durable open outcomes are currently recorded. diff --git a/CLAUDE.md b/CLAUDE.md index 8cc77f72..4e90b2cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,45 +1,22 @@ -# Trauma +# TRAUMA -Personal bookmark management app. Domain term: `memory` (one bookmark), `memories` (collection). +Personal bookmark management app. Domain term: `memory` for one bookmark and +`memories` for the collection. -Stack: TypeScript / SolidStart / Bun / Drizzle / SQLite / Vitest / Playwright. - -## Documentation - -All project detail lives under `docs/`. Start at [docs/INDEX.md](docs/INDEX.md). - -### Architecture -- [Overview](docs/architecture/overview.md) -- [Data and storage](docs/architecture/data-and-storage.md) -- [Runtime flows](docs/architecture/flows.md) -- [UI and routing](docs/architecture/ui-and-routing.md) - -### References -- [Technology stack](docs/references/technology-stack.md) -- [Configuration](docs/references/configuration.md) -- [Coding standards](docs/references/coding-standards/INDEX.md) -- [Glossary](docs/references/glossary.md) - -### Operations / Quality -- [Local self-hosting](docs/operations/local-self-hosting.md) -- [Verification](docs/quality/verification.md) - -### Workflows / Spec -- [Execution workflows](docs/workflows/README.md) -- [Foundation design](docs/superpowers/specs/2026-05-09-trauma-foundation-design.md) +All project detail lives under `docs/`. Start at [docs/INDEX.md](docs/INDEX.md) +and read only the owning documents for the change. Open durable work is tracked +in [Backlog.md](Backlog.md). ## Common Commands | Command | Purpose | -|---------|---------| -| `bun install` | Install deps | -| `bun run dev` | Dev server | -| `bun run verify` | typecheck + test + build | -| `bun run test:e2e` | Playwright smoke | -| `bun run db:generate` / `db:migrate` | Drizzle schema | - -## Rules - -- CLAUDE.md = index only. Detail belongs in `docs/`. -- Keep `AGENTS.md` and this file aligned as short maps, not design docs. -- Update foundation spec only when an approved foundation decision changes. +| --- | --- | +| `bun install` | Install dependencies | +| `bun run dev` | Start the development server | +| `bun run dev:smoke` | Verify development-server startup | +| `bun run verify` | Run typecheck, tests, and build | +| `bun run test:e2e` | Run the Playwright E2E suite | +| `bun run db:generate` / `bun run db:migrate` | Generate Drizzle files or apply checked runtime migrations | + +Keep this file as an index. Current behavior belongs in semantic docs; completed +execution history belongs in Git. diff --git a/README.md b/README.md index 9e32b466..fea4d16f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- image + Trauma Logo

TRAUMA


@@ -13,23 +13,20 @@ > project that I work on in the margins of another project, so please do not > expect particularly eager maintenance. -The project is designed as a lightweight local/self-hosted web app: one -SolidStart app, one Bun runtime, SQLite for metadata, markdown files for saved -content, and git backup for the markdown store. +The project is a lightweight local/self-hosted web app: one SolidStart app, one +Bun runtime, SQLite for relational state, a file-backed memory store for reader +content and Psychiatrist threads, and built-in git backup for selected store +artifacts. ## Status -The foundation implementation is now more than scaffold. The current baseline -includes SolidStart/Bun runtime commands, Drizzle/SQLite persistence, markdown -content storage, add-memory import, memory browsing, reader routes, flashbacks, -git backup, backup failsafe recovery, Defuddle-based extraction, Tailwind -styling, and the local browser-assisted import extension. +The current baseline includes URL and browser-assisted import, memory browsing +and read state, source and translated readers, Flashbacks, Moments, Brilliant +translation, the memory-scoped Psychiatrist assistant, settings, responsive +shells, and backup failsafe recovery. See [docs/INDEX.md](docs/INDEX.md) for the +current implementation contracts. -Some workflow documents remain as implementation records or future hardening -plans. Treat [docs/workflows/README.md](docs/workflows/README.md) as the -current map before starting new work. - -## Proves +## Previews | Sun Light | Sun Paper | | --- | --- | @@ -60,13 +57,16 @@ Install dependencies: bun install ``` -Create the local environment file from the example: +Create the local configuration files from the examples: ```bash cp .env.example .env +cp trauma.config.example.json trauma.config.json ``` -`.env` is gitignored. Keep it for local TRAUMA settings such as browser import. +Both local files are gitignored. Keep `.env` for settings such as browser +import, and edit `trauma.config.json` for local storage and backup paths. See +[Configuration](docs/references/configuration.md) for the supported fields. The `dev`, `start`, and `preview` scripts default `HOST` to `127.0.0.1` unless you set another host in the shell. @@ -82,7 +82,7 @@ Run baseline verification: bun run verify ``` -Run E2E smoke tests: +Run the Playwright E2E suite: ```bash bun run test:e2e @@ -90,21 +90,15 @@ bun run test:e2e ## Documentation -Start with [docs/INDEX.md](docs/INDEX.md). - -Key references: - -- [Foundation design](docs/superpowers/specs/2026-05-09-trauma-foundation-design.md) -- [Task execution workflows](docs/workflows/README.md) -- [Architecture overview](docs/architecture/overview.md) -- [Data and storage](docs/architecture/data-and-storage.md) -- [Runtime flows](docs/architecture/flows.md) -- [UI and routing](docs/architecture/ui-and-routing.md) -- [Configuration](docs/references/configuration.md) -- [Verification strategy](docs/quality/verification.md) +Start with [docs/INDEX.md](docs/INDEX.md). Open durable work is listed in +[Backlog.md](Backlog.md); completed execution history is retained by Git rather +than duplicated in agent-facing documentation. -## Initial Scope +## Operating Scope -TRAUMA is initially single-user and local/self-hosted. Auth, public signup, -managed databases, external queues, serverless deployment, and full offline -archival are out of scope for the foundation. +TRAUMA is single-user and local/self-hosted. TRAUMA user accounts, sessions, +multi-user ownership, public signup, managed databases, external queues, +serverless deployment, and full offline archival are out of scope. Optional +Codex app-server authentication for Brilliant and Psychiatrist is a separate +backend integration documented in the +[configuration reference](docs/references/configuration.md#codex-app-server-environment). diff --git a/app.config.ts b/app.config.ts index 87eee979..6266a7df 100644 --- a/app.config.ts +++ b/app.config.ts @@ -28,6 +28,7 @@ const HMR_PORTS: Record = { }; export default defineConfig({ + middleware: "./src/middleware.ts", server: { externals: { traceInclude: [ diff --git a/bun.lock b/bun.lock index e92f2644..589e21b5 100644 --- a/bun.lock +++ b/bun.lock @@ -9,24 +9,25 @@ "@solidjs/router": "^0.15.0", "@solidjs/start": "1.3.2", "defuddle": "^0.18.0", - "drizzle-orm": "^0.44.7", + "drizzle-orm": "^0.45.2", "entities": "^7.0.1", "highlight.js": "^11.11.1", "ipaddr.js": "^2.4.0", - "linkedom": "^0.18.12", - "markdown-it": "^14.1.1", - "markdown-it-anchor": "^9.2.0", + "linkedom": "^0.18.13", + "markdown-it": "^14.3.0", + "markdown-it-anchor": "^9.2.1", "markdown-it-footnote": "^4.0.0", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "remark-parse": "^11.0.0", - "sanitize-html": "^2.17.3", + "sanitize-html": "^2.17.6", "solid-js": "^1.9.5", "unified": "^11.0.5", "unist-util-visit-parents": "^6.0.2", "vinxi": "^0.5.7", }, "devDependencies": { + "@babel/core": "7.29.7", "@playwright/test": "^1.56.1", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.3.0", @@ -40,48 +41,55 @@ "drizzle-kit": "^0.31.8", "tailwindcss": "^4.3.0", "typescript": "^5.9.3", + "vite-plugin-solid": "^2.11.12", "vitest": "^4.0.15", }, }, }, + "overrides": { + "@babel/core": "7.29.7", + "h3": "1.15.11", + "tar": "7.5.20", + "vite": "6.4.3", + }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="], + "@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="], - "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="], + "@babel/core": ["@babel/core@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-compilation-targets": "^7.29.7", "@babel/helper-module-transforms": "^7.29.7", "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA=="], - "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + "@babel/generator": ["@babel/generator@7.29.7", "", { "dependencies": { "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="], - "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], - "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="], - "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], - "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="], - "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + "@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="], - "@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@babel/parser": ["@babel/parser@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" }, "bin": "./bin/babel-parser.js" }, "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], - "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + "@babel/traverse": ["@babel/traverse@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/template": "^7.29.7", "@babel/types": "^7.29.7", "debug": "^4.3.1" } }, "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw=="], - "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@babel/types": ["@babel/types@7.29.7", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA=="], "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], @@ -93,6 +101,12 @@ "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.11.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], @@ -171,12 +185,16 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.6", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@oxc-project/types": ["@oxc-project/types@0.139.0", "", {}, "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw=="], + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], @@ -217,6 +235,38 @@ "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.1.5", "", { "os": "android", "cpu": "arm64" }, "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.1.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.1.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.1.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.1.5", "", { "os": "linux", "cpu": "arm" }, "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.1.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.1.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.1.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.1.5", "", { "os": "linux", "cpu": "x64" }, "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.1.5", "", { "os": "none", "cpu": "arm64" }, "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.1.5", "", { "dependencies": { "@emnapi/core": "1.11.1", "@emnapi/runtime": "1.11.1", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.1.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.1.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + "@rollup/plugin-alias": ["@rollup/plugin-alias@6.0.0", "", { "peerDependencies": { "rollup": ">=4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g=="], "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg=="], @@ -349,6 +399,8 @@ "@tanstack/server-functions-plugin": ["@tanstack/server-functions-plugin@1.121.21", "", { "dependencies": { "@babel/code-frame": "7.26.2", "@babel/core": "^7.26.8", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9", "@babel/template": "^7.26.8", "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@tanstack/directive-functions-plugin": "1.121.21", "babel-dead-code-elimination": "^1.0.9", "tiny-invariant": "^1.3.3" } }, "sha512-a05fzK+jBGacsSAc1vE8an7lpBh4H0PyIEcivtEyHLomgSeElAJxm9E2It/0nYRZ5Lh23m0okbhzJNaYWZpAOg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], + "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -497,7 +549,7 @@ "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "boolbase": ["boolbase@2.0.0", "", {}, "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA=="], "boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="], @@ -583,9 +635,9 @@ "crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="], - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + "css-select": ["css-select@7.0.0", "", { "dependencies": { "boolbase": "^2.0.0", "css-what": "^8.0.0", "domhandler": "^6.0.1", "domutils": "^4.0.2", "nth-check": "^3.0.1" } }, "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g=="], - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + "css-what": ["css-what@8.0.0", "", {}, "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw=="], "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], @@ -595,6 +647,8 @@ "dax-sh": ["dax-sh@0.43.2", "", { "dependencies": { "@deno/shim-deno": "~0.19.0", "undici-types": "^5.26" } }, "sha512-uULa1sSIHgXKGCqJ/pA0zsnzbHlVnuq7g8O2fkHokWFNwEGIhh5lAJlxZa1POG5En5ba7AU4KcBAvGQWMMf8rg=="], + "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], + "db0": ["db0@0.3.4", "", { "peerDependencies": { "@electric-sql/pglite": "*", "@libsql/client": "*", "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", "sqlite3": "*" }, "optionalPeers": ["@electric-sql/pglite", "@libsql/client", "better-sqlite3", "drizzle-orm", "mysql2", "sqlite3"] }, "sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -643,7 +697,7 @@ "drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="], - "drizzle-orm": ["drizzle-orm@0.44.7", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="], + "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], "duplexer": ["duplexer@0.1.2", "", {}, "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="], @@ -747,7 +801,7 @@ "gzip-size": ["gzip-size@7.0.0", "", { "dependencies": { "duplexer": "^0.1.2" } }, "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA=="], - "h3": ["h3@1.15.3", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.4", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.0", "radix3": "^1.1.2", "ufo": "^1.6.1", "uncrypto": "^0.1.3" } }, "sha512-z6GknHqyX0h9aQaTx22VZDf6QyZn+0Nh+Ym8O/u0SGSkyF5cuTJYKlc8MkzW3Nzf9LE1ivcpmYC3FUGpywhuUQ=="], + "h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -843,7 +897,7 @@ "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], @@ -857,6 +911,8 @@ "knitwork": ["knitwork@1.3.0", "", {}, "sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw=="], + "launder": ["launder@1.7.1", "", { "dependencies": { "dayjs": "^1.11.7" } }, "sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw=="], + "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], @@ -883,9 +939,9 @@ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], - "linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], + "linkedom": ["linkedom@0.18.13", "", { "dependencies": { "css-select": "^7.0.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.1.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-ES/o9qotMpzpN2MHs+Iq/JcVoOj8Fa5wiQYrTdFpvAnwXL0g66XHHUc9WUMk6nAlBtGsFQ24ne+SYnvnaQ2FSw=="], - "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], + "linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="], "listhen": ["listhen@1.10.0", "", { "dependencies": { "@parcel/watcher": "^2.5.6", "@parcel/watcher-wasm": "^2.5.6", "citty": "^0.2.2", "consola": "^3.4.2", "crossws": ">=0.2.0 <0.5.0", "defu": "^6.1.7", "get-port-please": "^3.2.0", "h3": "^1.15.11", "http-shutdown": "^1.2.2", "jiti": "^2.6.1", "mlly": "^1.8.2", "node-forge": "^1.4.0", "pathe": "^2.0.3", "std-env": "^4.1.0", "tinyclip": "^0.1.12", "ufo": "^1.6.4", "untun": "^0.1.3", "uqr": "^0.1.3" }, "bin": { "listen": "bin/listhen.mjs", "listhen": "bin/listhen.mjs" } }, "sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ=="], @@ -899,7 +955,7 @@ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -907,9 +963,9 @@ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], - "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + "markdown-it": ["markdown-it@14.3.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw=="], - "markdown-it-anchor": ["markdown-it-anchor@9.2.0", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg=="], + "markdown-it-anchor": ["markdown-it-anchor@9.2.1", "", { "peerDependencies": { "@types/markdown-it": "*", "markdown-it": "*" } }, "sha512-p6APiLJDFAW2GEvaavDvhIBn7jrX2jLv77NkBGgNacFTurbORYc4pyYySg/mI6mpR6cHQuAtzKtmqgQr4K8dsQ=="], "markdown-it-footnote": ["markdown-it-footnote@4.0.0", "", {}, "sha512-WYJ7urf+khJYl3DqofQpYfEYkZKbmXmwxQV8c8mO/hGIhgZ1wOe7R4HLFNwqx7TjILbnC98fuyeSsin19JdFcQ=="], @@ -1053,7 +1109,7 @@ "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "nth-check": ["nth-check@3.0.1", "", { "dependencies": { "boolbase": "^2.0.0" } }, "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ=="], "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], @@ -1161,6 +1217,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rolldown": ["rolldown@1.1.5", "", { "dependencies": { "@oxc-project/types": "=0.139.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.1.5", "@rolldown/binding-darwin-arm64": "1.1.5", "@rolldown/binding-darwin-x64": "1.1.5", "@rolldown/binding-freebsd-x64": "1.1.5", "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", "@rolldown/binding-linux-arm64-gnu": "1.1.5", "@rolldown/binding-linux-arm64-musl": "1.1.5", "@rolldown/binding-linux-ppc64-gnu": "1.1.5", "@rolldown/binding-linux-s390x-gnu": "1.1.5", "@rolldown/binding-linux-x64-gnu": "1.1.5", "@rolldown/binding-linux-x64-musl": "1.1.5", "@rolldown/binding-openharmony-arm64": "1.1.5", "@rolldown/binding-wasm32-wasi": "1.1.5", "@rolldown/binding-win32-arm64-msvc": "1.1.5", "@rolldown/binding-win32-x64-msvc": "1.1.5" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA=="], + "rollup": ["rollup@4.60.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.3", "@rollup/rollup-android-arm64": "4.60.3", "@rollup/rollup-darwin-arm64": "4.60.3", "@rollup/rollup-darwin-x64": "4.60.3", "@rollup/rollup-freebsd-arm64": "4.60.3", "@rollup/rollup-freebsd-x64": "4.60.3", "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", "@rollup/rollup-linux-arm-musleabihf": "4.60.3", "@rollup/rollup-linux-arm64-gnu": "4.60.3", "@rollup/rollup-linux-arm64-musl": "4.60.3", "@rollup/rollup-linux-loong64-gnu": "4.60.3", "@rollup/rollup-linux-loong64-musl": "4.60.3", "@rollup/rollup-linux-ppc64-gnu": "4.60.3", "@rollup/rollup-linux-ppc64-musl": "4.60.3", "@rollup/rollup-linux-riscv64-gnu": "4.60.3", "@rollup/rollup-linux-riscv64-musl": "4.60.3", "@rollup/rollup-linux-s390x-gnu": "4.60.3", "@rollup/rollup-linux-x64-gnu": "4.60.3", "@rollup/rollup-linux-x64-musl": "4.60.3", "@rollup/rollup-openbsd-x64": "4.60.3", "@rollup/rollup-openharmony-arm64": "4.60.3", "@rollup/rollup-win32-arm64-msvc": "4.60.3", "@rollup/rollup-win32-ia32-msvc": "4.60.3", "@rollup/rollup-win32-x64-gnu": "4.60.3", "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A=="], "rollup-plugin-visualizer": ["rollup-plugin-visualizer@7.0.1", "", { "dependencies": { "open": "^11.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^18.0.0" }, "peerDependencies": { "rolldown": "1.x || ^1.0.0-beta || ^1.0.0-rc", "rollup": "2.x || 3.x || 4.x" }, "optionalPeers": ["rolldown", "rollup"], "bin": { "rollup-plugin-visualizer": "dist/bin/cli.js" } }, "sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg=="], @@ -1171,7 +1229,7 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "sanitize-html": ["sanitize-html@2.17.3", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^10.1.0", "is-plain-object": "^5.0.0", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-Kn4srCAo2+wZyvCNKCSyB2g8RQ8IkX/gQs2uqoSRNu5t9I2qvUyAVvRDiFUVAiX3N3PNuwStY0eNr+ooBHVWEg=="], + "sanitize-html": ["sanitize-html@2.17.6", "", { "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", "htmlparser2": "^12.0.0", "is-plain-object": "^5.0.0", "launder": "^1.7.1", "parse-srcset": "^1.0.2", "postcss": "^8.3.11" } }, "sha512-M4bo9tfv1yfhQZZKkc6dL07ALrGJtfvNOuhX3hU9AVPR/uPQ+nKOJBqTYc7LfMQblTW04mtSWDJWEyLvygJsLA=="], "scule": ["scule@1.3.0", "", {}, "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g=="], @@ -1259,7 +1317,7 @@ "tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="], - "tar": ["tar@7.5.15", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ=="], + "tar": ["tar@7.5.20", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-9FcyK4PA6+WbzlTM9WhQm6vB5W7cP7dUiPsv1g7YDwEQnQ1CGpK3MGlKk/ITVWMk05kHZuBhmVhiv8LZoy/PFQ=="], "tar-stream": ["tar-stream@3.2.0", "", { "dependencies": { "b4a": "^1.6.4", "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg=="], @@ -1363,7 +1421,7 @@ "vinxi": ["vinxi@0.5.11", "", { "dependencies": { "@babel/core": "^7.22.11", "@babel/plugin-syntax-jsx": "^7.22.5", "@babel/plugin-syntax-typescript": "^7.22.5", "@types/micromatch": "^4.0.2", "@vinxi/listhen": "^1.5.6", "boxen": "^8.0.1", "chokidar": "^4.0.3", "citty": "^0.1.6", "consola": "^3.4.2", "crossws": "^0.3.4", "dax-sh": "^0.43.0", "defu": "^6.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "get-port-please": "^3.1.2", "h3": "1.15.3", "hookable": "^5.5.3", "http-proxy": "^1.18.1", "micromatch": "^4.0.8", "nitropack": "^2.11.10", "node-fetch-native": "^1.6.6", "path-to-regexp": "^6.2.1", "pathe": "^1.1.1", "radix3": "^1.1.2", "resolve": "^1.22.10", "serve-placeholder": "^2.0.1", "serve-static": "^1.15.0", "tinyglobby": "^0.2.14", "ufo": "^1.6.1", "unctx": "^2.4.1", "unenv": "^1.10.0", "unstorage": "^1.16.0", "vite": "^6.4.1", "zod": "^4.0.0" }, "bin": { "vinxi": "bin/cli.mjs" } }, "sha512-82Qm+EG/b2PRFBvXBbz1lgWBGcd9totIL6SJhnrZYfakjloTVG9+5l6gfO6dbCCtztm5pqWFzLY0qpZ3H3ww/w=="], - "vite": ["vite@6.4.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ=="], + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], "vite-plugin-solid": ["vite-plugin-solid@2.11.12", "", { "dependencies": { "@babel/core": "^7.23.3", "@types/babel__core": "^7.20.4", "babel-preset-solid": "^1.8.4", "merge-anything": "^5.1.7", "solid-refresh": "^0.6.3", "vitefu": "^1.0.4" }, "peerDependencies": { "@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*", "solid-js": "^1.7.2", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["@testing-library/jest-dom"] }, "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA=="], @@ -1407,16 +1465,6 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "@babel/core/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - - "@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -1449,9 +1497,39 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@tanstack/directive-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@tanstack/directive-functions-plugin/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@tanstack/directive-functions-plugin/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@tanstack/router-utils/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], - "@vinxi/listhen/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + "@tanstack/router-utils/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@tanstack/server-functions-plugin/@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + + "@tanstack/server-functions-plugin/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@tanstack/server-functions-plugin/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@tanstack/server-functions-plugin/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@types/babel__core/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@types/babel__core/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@types/babel__generator/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@types/babel__template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@types/babel__template/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@types/babel__traverse/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@vercel/nft/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@vinxi/listhen/jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], @@ -1459,6 +1537,8 @@ "@vinxi/listhen/std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "@vinxi/plugin-directives/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + "@vinxi/plugin-directives/magicast": ["magicast@0.2.11", "", { "dependencies": { "@babel/parser": "^7.22.16", "@babel/types": "^7.22.17", "recast": "^0.23.4" } }, "sha512-6saXbRDA1HMkqbsvHOU6HBjCVgZT460qheRkLhJQHWAbhXoWESI3Kn/dGGXyKs15FFKR85jsUqFx2sMK0wy/5g=="], "@vinxi/server-components/magicast": ["magicast@0.2.11", "", { "dependencies": { "@babel/parser": "^7.22.16", "@babel/types": "^7.22.17", "recast": "^0.23.4" } }, "sha512-6saXbRDA1HMkqbsvHOU6HBjCVgZT460qheRkLhJQHWAbhXoWESI3Kn/dGGXyKs15FFKR85jsUqFx2sMK0wy/5g=="], @@ -1471,16 +1551,32 @@ "archiver-utils/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], + + "babel-dead-code-elimination/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "babel-dead-code-elimination/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "babel-dead-code-elimination/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + "babel-plugin-jsx-dom-expressions/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "c12/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "compress-commons/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "css-select/domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="], + + "css-select/domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="], + "dax-sh/undici-types": ["undici-types@5.28.4", "", {}, "sha512-3OeMF5Lyowe8VW0skf5qaIE7Or3yS9LS7fvMUI0gg4YxpIBVg0L8BxCmROw2CcYhSkpR68Epz7CGc8MPj94Uww=="], + "defuddle/linkedom": ["linkedom@0.18.12", "", { "dependencies": { "css-select": "^5.1.0", "cssom": "^0.5.0", "html-escaper": "^3.0.3", "htmlparser2": "^10.0.0", "uhyphen": "^0.2.0" }, "peerDependencies": { "canvas": ">= 2" }, "optionalPeers": ["canvas"] }, "sha512-jalJsOwIKuQJSeTvsgzPe9iJzyfVaEJiEXl+25EkKevsULHvMJzpNqwvj1jOESWdmgKDiXObyjOYwlUqG7wo1Q=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "dot-prop/type-fest": ["type-fest@5.6.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA=="], @@ -1501,7 +1597,9 @@ "listhen/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "listhen/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + "magicast/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "magicast/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "make-dir/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], @@ -1517,12 +1615,10 @@ "nitropack/citty": ["citty@0.2.2", "", {}, "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w=="], - "nitropack/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], + "nitropack/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], "nitropack/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - "nitropack/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], - "nitropack/semver": ["semver@7.8.0", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="], "nitropack/serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -1533,16 +1629,26 @@ "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-scurry/lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], + "readdir-glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "sanitize-html/htmlparser2": ["htmlparser2@12.0.0", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "domutils": "^4.0.2", "entities": "^8.0.0" } }, "sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "solid-refresh/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "solid-refresh/@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "solid-refresh/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1571,7 +1677,7 @@ "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], - "unstorage/h3": ["h3@1.15.11", "", { "dependencies": { "cookie-es": "^1.2.3", "crossws": "^0.3.5", "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg=="], + "unstorage/lru-cache": ["lru-cache@11.3.6", "", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="], "untun/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], @@ -1581,6 +1687,12 @@ "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "vite/picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + + "vite/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="], + + "vite/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1589,12 +1701,6 @@ "youch/cookie-es": ["cookie-es@3.1.1", "", {}, "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg=="], - "@babel/core/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "@babel/template/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1641,7 +1747,67 @@ "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "@vinxi/listhen/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + "@tanstack/directive-functions-plugin/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@tanstack/directive-functions-plugin/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@tanstack/directive-functions-plugin/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/router-utils/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@tanstack/router-utils/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/server-functions-plugin/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/server-functions-plugin/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@tanstack/server-functions-plugin/@babel/template/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@tanstack/server-functions-plugin/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@tanstack/server-functions-plugin/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@tanstack/server-functions-plugin/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@tanstack/server-functions-plugin/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@tanstack/server-functions-plugin/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@tanstack/server-functions-plugin/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@types/babel__core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@types/babel__core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@types/babel__generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@types/babel__generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@types/babel__template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@types/babel__template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@types/babel__traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@types/babel__traverse/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@vinxi/plugin-directives/@babel/parser/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@vinxi/plugin-directives/magicast/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@vinxi/server-components/magicast/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "@vinxi/server-components/magicast/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "ansi-align/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1651,82 +1817,122 @@ "archiver-utils/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + "babel-dead-code-elimination/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "babel-dead-code-elimination/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "babel-dead-code-elimination/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "babel-dead-code-elimination/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "babel-dead-code-elimination/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "babel-dead-code-elimination/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "babel-plugin-jsx-dom-expressions/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "babel-plugin-jsx-dom-expressions/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "c12/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "css-select/domhandler/domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], + + "css-select/domutils/dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], + + "css-select/domutils/domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], + + "defuddle/linkedom/css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], + "lazystream/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "listhen/@parcel/watcher-wasm/napi-wasm": ["napi-wasm@1.1.3", "", { "bundled": true }, "sha512-h/4nMGsHjZDCYmQVNODIrYACVJ+I9KItbG+0si6W/jSjdA9JbWDoU4LLeMXVcEQGHjttI2tuXqDrbGF7qkUHHg=="], - "listhen/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + "magicast/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="], "nitropack/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "nitropack/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], + "nitropack/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], - "nitropack/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="], + "nitropack/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], - "nitropack/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="], + "nitropack/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], - "nitropack/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="], + "nitropack/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], - "nitropack/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="], + "nitropack/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], - "nitropack/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="], + "nitropack/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], - "nitropack/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="], + "nitropack/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], - "nitropack/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="], + "nitropack/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], - "nitropack/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="], + "nitropack/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], - "nitropack/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="], + "nitropack/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - "nitropack/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="], + "nitropack/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - "nitropack/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="], + "nitropack/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - "nitropack/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="], + "nitropack/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - "nitropack/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="], + "nitropack/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - "nitropack/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="], + "nitropack/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - "nitropack/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="], + "nitropack/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - "nitropack/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="], + "nitropack/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - "nitropack/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="], + "nitropack/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - "nitropack/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="], + "nitropack/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - "nitropack/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="], + "nitropack/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - "nitropack/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="], + "nitropack/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - "nitropack/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="], + "nitropack/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - "nitropack/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="], + "nitropack/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - "nitropack/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="], + "nitropack/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - "nitropack/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="], + "nitropack/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - "nitropack/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - - "nitropack/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], + "nitropack/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], "nitropack/serve-static/send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "readdir-glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + "sanitize-html/htmlparser2/domelementtype": ["domelementtype@3.0.0", "", {}, "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg=="], + + "sanitize-html/htmlparser2/domhandler": ["domhandler@6.0.1", "", { "dependencies": { "domelementtype": "^3.0.0" } }, "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg=="], + + "sanitize-html/htmlparser2/domutils": ["domutils@4.0.2", "", { "dependencies": { "dom-serializer": "^3.0.0", "domelementtype": "^3.0.0", "domhandler": "^6.0.0" } }, "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA=="], + + "sanitize-html/htmlparser2/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "solid-refresh/@babel/generator/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "solid-refresh/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "solid-refresh/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], @@ -1783,22 +1989,60 @@ "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "unstorage/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@tanstack/directive-functions-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/server-functions-plugin/@babel/template/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@tanstack/server-functions-plugin/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@vinxi/plugin-directives/@babel/parser/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@vinxi/plugin-directives/@babel/parser/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@vinxi/plugin-directives/magicast/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@vinxi/plugin-directives/magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@vinxi/server-components/magicast/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@vinxi/server-components/magicast/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "ansi-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "archiver-utils/glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], "archiver-utils/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "babel-dead-code-elimination/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "css-select/domutils/dom-serializer/entities": ["entities@8.0.0", "", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="], + + "defuddle/linkedom/css-select/boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + + "defuddle/linkedom/css-select/css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], + + "defuddle/linkedom/css-select/nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], + "nitropack/serve-static/send/fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "readdir-glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "sanitize-html/htmlparser2/domutils/dom-serializer": ["dom-serializer@3.1.1", "", { "dependencies": { "domelementtype": "^3.0.0", "domhandler": "^6.0.0", "entities": "^8.0.0" } }, "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + "archiver-utils/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "solid-refresh/@babel/helper-module-imports/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], } } diff --git a/docs/INDEX.md b/docs/INDEX.md index 438b81b1..9cc0170e 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -1,59 +1,62 @@ # TRAUMA Documentation Index -This directory is the working documentation set for TRAUMA. - -The approved foundation spec remains the design record: - -- [Foundation design](superpowers/specs/2026-05-09-trauma-foundation-design.md) -- [Execution workflows](workflows/README.md) - -Use the documents below for day-to-day implementation context. They are derived -from the foundation design and should stay aligned with it. +This is the authoritative map for current TRAUMA implementation context. Read +the smallest owning document for the work. When prose and executable behavior +disagree, verify the code and tests, then correct the owning semantic document. ## Architecture - [Overview](architecture/overview.md): runtime shape, module boundaries, and - dependency rules. -- [Data and storage](architecture/data-and-storage.md): SQLite metadata, - markdown store, flashback persistence, and ownership of canonical state. -- [Flows](architecture/flows.md): add memory, extraction fallback, flashback, - and git backup flows. -- [UI and routing](architecture/ui-and-routing.md): canonical routes, shell - layout, filters, flashback browse, composer, and reader behavior. + dependency direction. +- [Data and storage](architecture/data-and-storage.md): canonical ownership + across SQLite, memory-store artifacts, translations, and Psychiatrist. +- [Runtime flows](architecture/flows.md): add memory, Flashbacks, Moments, + translation, Psychiatrist, and git backup. +- [UI and routing](architecture/ui-and-routing.md): canonical routes, shell, + browse/filter state, reader behavior, and responsive navigation. ## References - [Technology stack](references/technology-stack.md): selected stack, - exclusions, and rationale. -- [Design system](references/design-system/INDEX.md): front-end tokens, - themes, shell layout, route surfaces, icons, interactions, and verification. -- [Configuration](references/configuration.md): `trauma.config.json` shape, - validation rules, and operational meaning. -- [Coding standards](references/coding-standards/INDEX.md): map of - TypeScript, SolidStart, Bun, Drizzle, security, testing, and anti-pattern - rules for implementation work, including review feedback triage. -- [Glossary](references/glossary.md): domain language and status terms. + deployment target, exclusions, and rationale. +- [Design system](references/design-system/INDEX.md): tokens, themes, layout, + route surfaces, icons, interaction, accessibility, and visual verification. +- [Configuration](references/configuration.md): `trauma.config.json`, path and + backup rules, browser import, and Codex app-server environment. +- [Coding standards](references/coding-standards/INDEX.md): TypeScript, + SolidStart, Bun, Drizzle, security, testing, and review-feedback rules. +- [Glossary](references/glossary.md): product terms and persisted status fields. + +## Operations And Quality -## Operations +- [Local/self-hosting](operations/local-self-hosting.md): persistent-disk + operation, backup recovery, access control, and Psychiatrist isolation. +- [Verification](quality/verification.md): verification commands, current risk + coverage, and the completion bar. -- [Local/self-hosting model](operations/local-self-hosting.md): expected - deployment shape, persistent disk assumptions, and git backup behavior. +## Work -## Quality +- [Backlog](../Backlog.md): concise durable open work. +- [Execution workflow policy](workflows/README.md): how temporary task plans and + completed history are handled. +- [Historical workflow index](workflows/archive/README.md): completed task + families and their current semantic owners. -- [Verification](quality/verification.md): E2E-first strategy and focused - unit/integration coverage. +## Historical Records -## Workflows +- [Foundation design, 2026-05-09](superpowers/specs/2026-05-09-trauma-foundation-design.md): + superseded pre-implementation record retained for historical context only. -- [Task execution workflows](workflows/README.md): task-scoped worker handoff - files for implementation PRs. +Historical records are not implementation specifications. Current architecture, +reference, operations, quality docs, code, and tests take precedence. ## Documentation Rules -- Keep `AGENTS.md` as a short map, not a design document. -- Put system boundaries and dependency rules under `docs/architecture/`. -- Put exact contracts, config shapes, and terminology under `docs/references/`. -- Put runtime/deployment procedures under `docs/operations/`. -- Put testing and verification expectations under `docs/quality/`. -- Update the foundation spec only when changing an approved foundation decision. +- Keep `AGENTS.md`, `CLAUDE.md`, and `README.md` as short entry points. +- Put durable system behavior in the owning architecture, reference, + operations, or quality document. +- Keep one owner for a contract and link to it instead of copying it. +- Track open outcomes in `Backlog.md`; delete completed task plans after moving + durable requirements to semantic docs. +- Use Git history for execution chronology, review transcripts, commit lists, + and superseded implementation plans. diff --git a/docs/architecture/data-and-storage.md b/docs/architecture/data-and-storage.md index e2d6fd5b..6550719e 100644 --- a/docs/architecture/data-and-storage.md +++ b/docs/architecture/data-and-storage.md @@ -1,24 +1,71 @@ # Data And Storage Architecture -TRAUMA separates metadata from readable content. +TRAUMA uses SQLite and a file-backed memory store. Ownership is domain-specific; +neither storage system is a universal source of truth. -SQLite is the canonical runtime metadata store. Markdown files are readable -content artifacts and git-backup artifacts. The SQLite database file itself is -not backed up through TRAUMA's git backup feature. +## Ownership Matrix -## Memory Content Store +| Domain | Runtime owner | Store artifact and backup role | +| --- | --- | --- | +| Memory metadata, read state, taxonomy, backup state | SQLite | `CONTENT.md` frontmatter carries only the portable content identity and extraction snapshot. | +| Source reader content | `CONTENT.md` | Canonical readable body and built-in git backup artifact. | +| Translation attempts and current-output identity | SQLite `translation_*` rows | Language-scoped `CONTENT.md` is the canonical translated body; `TRANSLATION_MAP.json` exports projection data. | +| Flashbacks | SQLite `flashbacks` | `FLASHBACKS.json` is a deterministic backup/export projection, never the runtime authority. | +| Moments | SQLite `moments` | No store sidecar currently exists. | +| Psychiatrist threads and pairs | Memory-local files under `threads/` | Canonical thread state, replay data, and built-in git backup artifacts; no SQLite thread rows. | +| Application preferences and external auth references | SQLite | Not part of built-in git backup. | +| Theme selection and open UI state | Browser/UI state | Never persisted to SQLite or the memory store. | -Memory content lives under: +The SQLite database file is outside `storePath` and is not committed by +TRAUMA's built-in git backup. Operators who need database recovery must back it +up at the host level. + +## Memory Store Layout + +The current store tree is: ```text -{storePath}/memories/{memoryId}/CONTENT.md +{storePath}/memories/{memoryId}/ + CONTENT.md + FLASHBACKS.json + {langCode}/ + CONTENT.md + FLASHBACKS.json + TRANSLATION_MAP.json + threads/{threadId}/ + THREAD.json + THREAD.md + PAIRS.jsonl + pairs/{pairId}/ + PROMPT.md + CONTEXT.json + RESPONSE.md + turns/{turnId}.json + streams/{turnId}.jsonl ``` -`memoryId` is UUID v7. The ID is stable and not derived from title, URL, tags, -or category names. +Some files are created only when their feature is used. All resolved paths must +remain inside the configured `storePath` and the owning memory subtree. + +`{storePath}/.operations/` contains short-lived create/delete journals and +variant-specific Flashback export reconciliation intents, while +`{storePath}/.delete-staging/` contains directories moved during deletion. +They are internal crash-recovery state, not memory artifacts or backup inputs. +A deletion journal is cleared only after content, backup, and SQLite state have +been reconciled. A creation row is published only after canonical `CONTENT.md` +and its directory entries have been synced; recovery fails closed when a +surviving creation journal instead finds a row without that canonical file. -`CONTENT.md` contains minimal frontmatter plus markdown body. Initial -frontmatter keys: +## Source Content + +`memoryId` is UUID v7 and is not derived from title, URL, tags, or category +names. Source content lives at: + +```text +{storePath}/memories/{memoryId}/CONTENT.md +``` + +`CONTENT.md` contains Markdown plus these frontmatter keys: - `id` - `url` @@ -26,161 +73,234 @@ frontmatter keys: - `captured_at` - `extraction_status` -Tags and categories are not written to frontmatter. SQLite is their source of -truth. +Tags, categories, read state, backup state, and other runtime metadata remain in +SQLite. `extraction_status` values are defined by +`src/server/memory-status.ts`; frontmatter validation, the SQLite constraint, +and tests must share that contract or have explicit drift coverage. -`extraction_status` values are defined by `src/server/memory-status.ts`. The -markdown frontmatter parser, writer, SQLite schema constraint, and tests must -derive from that shared contract or include explicit drift coverage. - -Remote images stay remote in the initial design. TRAUMA is not a full offline -archive. +Remote images stay remote. TRAUMA is not a full offline archive. ## SQLite Model Runtime tables: - `memories` +- `memory_creation_idempotency` - `tags` - `categories` - `memory_tags` - `memory_categories` - `flashbacks` -- `translation_jobs` -- `translation_chunks` -- `translation_projection_spans` +- `moments` - `backup_environment_stamps` - `backup_failsafe_alerts` - `app_settings` +- `openai_auth_credentials` +- `translation_jobs` +- `translation_chunks` +- `translation_projection_spans` -`memories` stores URL metadata, content path, extraction status, backup status, -and timestamps. - -Runtime initialization applies bundled migrations before repositories are -returned. Application code must not observe a partially initialized SQLite -schema. - -`tags` and `categories` are both many-to-many with memories. Category means a -curated grouping. Tag means ad-hoc labeling. URL import does not auto-assign -either. - -`backup_environment_stamps` stores the validated backup identity for the local -markdown backup repository: resolved paths, configured remote, remote URL when -available, branch, and timestamps. Startup and backup writes compare the current -config against this stamp before accepting new writes. - -`backup_failsafe_alerts` stores the single active critical backup alert, when -one exists. Alert kinds distinguish path drift, missing backup repository, -remote push failure, and backup content inconsistency so the UI can offer only -the recovery actions that are safe for that condition. - -`app_settings` stores singleton local application preferences. Translation -defaults such as `translation_target_language`, `codex_translation_model`, and -`codex_translation_reasoning_effort` are current UI/settings state used to seed -future translation forms. They are not historical job records. - -## Flashback Model - -Flashbacks are SQLite metadata rendered into reader HTML at read time. - -`flashbacks` stores: - -- `id` -- `memory_id` -- `variant_kind` -- `lang_code` -- `translation_output_hash` -- `text` -- `prefix` -- `suffix` -- `start_offset` -- `end_offset` -- `content_hash` -- timestamps - -`flashbacks.memory_id` is the canonical memory relation. `variant_kind`, -`lang_code`, and `translation_output_hash` scope a row to the reader content -variant where it was created. Source Flashbacks use `variant_kind = 'source'` -with null language and output hash. Translated Flashbacks use -`variant_kind = 'translation'`, a supported BCP 47 `lang_code`, and the -completed translation `output_hash`. API responses may shape this as -`memory.flashbacks: Flashback[]`, but memories should not store a separate -flashback ID array as source-of-truth state. - -Flashbacks are local to the reader content variant where they are created. -Source Flashbacks use source reader offsets. Translated Flashbacks use -translated reader offsets and are scoped to the completed translation output -hash. Global Flashback browse and memory search surfaces include renderable -Flashbacks from both source and translated variants. - -New flashback rows use active-variant reader-text offsets. `content_hash` uses -the `sha256:` format and hashes the same canonical reader text used for -offset calculation after line endings are normalized to `\n`. If the current -reader text hash does not match the row, the reader must not render that -flashback at a guessed location. For translated rows, a mismatched -`translation_output_hash` also makes the Flashback stale and non-renderable. - -Flashback browse and search views use `text`, `prefix`, `suffix`, and the -related memory title. The flashback table remains the canonical source for -flashback snippets; no separate denormalized flashback feed is introduced in -the initial design. - -`CONTENT.md` is not mutated for normal flashback persistence. The reader applies -records as transient inline marks when rendering: +Runtime initialization applies and validates bundled migrations before +repositories are exposed. Application code must not observe a partially +initialized schema. Every runtime connection enables foreign keys and WAL and +uses a bounded five-second busy timeout so short write contention is serialized +instead of failing immediately. + +Global Moment and Flashback browse reads use deterministic `created_at`/`id` +keyset pages ordered by `created_at DESC, id DESC`. Interactive pages default to +30 rows and reject limits outside `1..100`. Their opaque versioned cursor binds +the collection kind, timestamp, and ID; malformed or cross-collection tokens +never reach a repository query. + +Paged Flashback projection scans at most four SQL batches per request and +returns the last scanned cursor even when every candidate is stale, so callers +can advance without an unbounded loop. Each batch reads only its distinct +memory/variant content keys. Paged Moment projection resolves only the SQL page +and reads one table of contents per distinct memory on that page. Legacy full +collection reads remain available only for API compatibility and are not used +by the collection routes or Reader All rail. + +`memories` owns URL metadata, content path, extraction/read/backup status, and +timestamps. Tags and categories are many-to-many relations; URL import does not +assign either automatically. + +`memory_creation_idempotency` durably binds an optional add-memory UUID v7 +request identity to its preflight-normalized request URL before import begins. +The identity is also the intended memory ID. Completed reservations and +recoverable interrupted creations survive process restart and later memory +deletion. A replay can therefore return the existing/recovered row, or fail with +a stable conflict after deletion, but can never silently import and recreate the +memory. A newly inserted reservation is released only when its initial attempt +fails before leaving recoverable durable state, which lets the shell retry that +same failed attempt. The table has no foreign key to `memories` because the +reservation must exist before the memory row and may outlive it. + +`backup_environment_stamps` records the validated backup identity: resolved +project/store paths, configured remote and its URL when available, branch, and +timestamps. Startup and writes compare paths, remote, configured branch, and +the checked-out branch with this stamp. + +`backup_failsafe_alerts` stores the one active critical backup alert. Its kind +distinguishes path drift, missing repository, push failure, and content +inconsistency so only safe recovery actions are offered. Recovery confirmation +uses an opaque generation derived from the complete persisted alert; replacing +or consuming the alert invalidates an older confirmation. + +`app_settings` stores singleton local preferences such as translation language, +Codex model, and reasoning effort. These defaults seed future translation jobs; +they do not rewrite historical job records. The canonical combined translation +defaults mutation validates language, model, and reasoning effort before one +SQLite `UPDATE` writes all three columns. Invalid input writes none of them. +Language-only and Codex-only mutations remain compatibility surfaces, but must +preserve the same singleton row and return the canonical settings projection. + +`openai_auth_credentials` is the retained singleton compatibility table for +an external credential reference. Current Codex login state belongs to Codex +app-server; TRAUMA must not copy Codex tokens into SQLite. + +## Flashbacks + +Flashbacks are variant-local SQLite text ranges rendered into reader HTML. +Their rows include memory and variant identity, selected text and context, +reader offsets, content hash, and timestamps. + +- Source rows use `variant_kind = 'source'` with null language and output hash. +- Translated rows use `variant_kind = 'translation'`, a supported BCP 47 + `lang_code`, and the current translation `output_hash`. +- Offsets are measured against the active variant's canonical reader text. +- `content_hash` uses `sha256:` after line endings are normalized to `\n`. +- A content/output hash mismatch makes the row stale and non-renderable; the + reader must not guess a new location. + +The reader applies current rows as transient marks: ```html selected text ``` -The reader pipeline must allow `mark` and `data-flashback-id` while still -sanitizing unsafe HTML. +The sanitizer allows that element and attribute while removing unsafe HTML. +Normal Flashback writes never mutate `CONTENT.md`. -Flashback removal uses the same text-range model as flashback creation. When a -user selects text that is already flashbacked, only the selected range is -unflashbacked. Exact matches delete the corresponding `flashbacks` row and -remove the rendered mark. Partial matches shrink the existing range or split it -into multiple remaining flashback ranges in SQLite. This prevents a nested or -wider flashback from being removed when the user intended to toggle off only a -sentence or phrase. - -Because the built-in git backup does not back up SQLite directly, flashback -changes write a deterministic metadata export at: +Toggling off a selection deletes an exact range or shrinks/splits overlapping +rows so unselected text stays Flashbacked. SQLite remains authoritative. After +each successful mutation, TRAUMA writes the active variant's deterministic +export and enqueues it for backup: ```text {storePath}/memories/{memoryId}/FLASHBACKS.json {storePath}/memories/{memoryId}/{langCode}/FLASHBACKS.json ``` -The source path stores source Flashbacks. The language-scoped path stores -translated Flashbacks for that completed translation output. Those files are -backup/export artifacts, not the runtime source of truth. - -## Translation Projection Storage - -Translated content is stored beside the source memory: +Before mutating SQLite, the server durably records a variant-specific export +reconciliation intent. Failure before export rename restores the previous +authoritative ranges where necessary; the retained intent makes that rollback +recoverable too. Once both SQLite and the export are durably published, the +intent is cleared. Backup enqueue or backup-status failure does not undo the +toggle. The API returns the normal success fields plus an optional `backup` +warning and `pending` or `failed` status. + +Export publication syncs newly created directory entries, temporary-file +bytes, and the final export directory. If final directory sync fails after the +atomic rename, TRAUMA verifies the exact deterministic target bytes and retries +that sync without rewriting. Any remaining post-rename uncertainty keeps the +next SQLite rows authoritative and returns HTTP success with the public +`flashback_export_durability_unconfirmed` warning; internal paths and +confirmation diagnostics are not exposed. Reader and collection callers keep +their committed optimistic state and revalidate from SQLite instead of retrying +or restoring stale UI. Startup reconciliation runs with git backup enabled or +disabled, rereads the active variant rows, and republishes the deterministic +export, including an empty row list after unflashback. Toggle and recovery hold +the same store/memory/artifact-variant lock across SQLite read and publication, +so an older recovery snapshot cannot overwrite a newer toggle. Translation +completion joins that same per-language lock, records an export intent before +making the new output hash current, publishes the new hash's projection +(including an empty list), and includes `FLASHBACKS.json` in translation backup. +An older retained intent therefore cannot republish the previous translation +after a newer translation becomes authoritative. + +## Moments + +Moments are source-canonical section bookmarks in SQLite. A row owns the memory, +section anchor/title/level/path, optional reader offsets and content hash, and +timestamps. The unique memory/section-anchor relation prevents duplicate +bookmarks for the same source section. + +When a Moment is created from a translated reader, the server validates the +translated table of contents and maps the selected section to the matching +source section path and level before persisting it. The translated file is not +a separate Moment authority. + +No Moment export file exists in the current contract, so built-in store git +backup does not preserve Moment rows. + +## Translation Storage + +Translated reader artifacts live beside the source memory: ```text {storePath}/memories/{memoryId}/{langCode}/CONTENT.md {storePath}/memories/{memoryId}/{langCode}/TRANSLATION_MAP.json ``` -`translation_jobs` is the current/history table for translation attempts. -Each row stores the resolved `model` and `reasoning_effort` used for that -attempt, so later settings changes do not rewrite the model or effort that a -queued, running, completed, or failed job actually used. -`translation_chunks` may temporarily hold translated chunk Markdown while a job -is running, but completed chunk bodies and temporary projection JSON are purged -after final commit. - -`translation_projection_spans` stores durable runtime alignment from source -reader offsets to translated reader offsets. Rows are keyed by `memory_id`, -`lang_code`, `source_hash`, and `output_hash`, so translated annotations are -used only when both the source file and translated file still match the -completed translation. `TRANSLATION_MAP.json` is the git-backup/export artifact -for the same projection data; SQLite remains the runtime source of truth. - -`translation_projection_spans` remain alignment metadata for translation output -inspection and future projection features. Flashbacks no longer depend on these -rows for translated reader behavior: source and translated Flashbacks are stored -as variant-local rows. Moment rows remain source canonical unless a later -workflow explicitly changes that contract. +`translation_jobs` owns attempt history and the resolved model/reasoning effort +for each attempt. `translation_chunks` holds durable work state and may hold +translated chunk Markdown while a job is active; completed chunk bodies and +temporary projection JSON are purged after final commit. + +Active-job recovery reuses completed chunks only when the job's persisted +prompt-policy/chunker versions and its complete source chunk manifest still +match the current runtime. The manifest includes count, indexes, source chunk +hashes, and ordered block IDs. A mismatch terminalizes that attempt before any +new translation, output commit, or backup work and leaves a fresh attempt +available. + +`translation_projection_spans` owns runtime source-to-translated alignment. +Rows are scoped by memory, language, source hash, and output hash so stale files +cannot reuse current projections. `TRANSLATION_MAP.json` is the portable +git-backup/export representation of that data; SQLite remains the runtime +authority for spans. + +The language-scoped `CONTENT.md` is committed with a same-directory temporary +file, file sync, and atomic rename. A translation is current only when its +completed job identity, source hash, output hash, and file agree. A durable +`committing` job keeps its chunks so an interrupted output/projection commit can +be replayed before the artifact becomes current. + +## Psychiatrist Thread Store + +Psychiatrist thread state is file-backed and memory-local. It has no SQLite +thread, pair, turn, or stream rows. + +- `THREAD.json` is the machine-readable thread manifest and active-turn state. +- `PAIRS.jsonl` is the append-only pair revision log. +- `PROMPT.md` and `CONTEXT.json` preserve the exact user prompt and context + provenance for a pair. +- `RESPONSE.md` stores the pair's current completed assistant response. +- `THREAD.md` is the readable transcript projection. +- `turns/{turnId}.json` stores durable turn identity and terminal state. +- `streams/{turnId}.jsonl` stores safe replayable events before SSE fan-out. + +Manifests, response files, turn records, context snapshots, prompts, and +transcript projections use a same-directory temporary file, file sync, atomic +rename, and owning-directory sync when they are published or replaced. +`PAIRS.jsonl` and turn streams repair only a torn trailing fragment, append +through an open file handle, and file-sync before the append becomes visible to +callers or stream subscribers. Creating a JSONL file also syncs its owning +directory. JSONL replay is read incrementally behind byte and row limits rather +than loading an unchecked legacy file. + +A completed first answer or Regenerate enqueues the manifest, transcript, pair +files, turn record, and stream for built-in backup. Backup enqueue failure is +reported as a warning and does not erase the saved answer. + +For a pair, the latest valid `PAIRS.jsonl` revision is authoritative over +`RESPONSE.md`. A new response projection is removed, or a replaced response is +restored, if its completed revision cannot be appended. A crash that leaves the +two out of agreement is reconciled from the latest durable pair revision before +backup retry. The `THREAD.json` manifest and `THREAD.md` transcript are +recoverable projections after a completed pair revision, so a post-save +finalization failure is surfaced as a warning instead of erasing the answer. + +Psychiatrist may write only inside the active memory's `threads/` subtree. It +must not mutate source or translated `CONTENT.md`, taxonomy, Flashbacks, +Moments, settings, translation state, or unrelated memories. diff --git a/docs/architecture/flows.md b/docs/architecture/flows.md index 07e77e99..7f8b835a 100644 --- a/docs/architecture/flows.md +++ b/docs/architecture/flows.md @@ -1,206 +1,469 @@ # Runtime Flows -This document describes the core runtime flows that implementation should -preserve. +These are the durable runtime boundaries implementation must preserve. Storage +ownership is defined in [Data and storage](data-and-storage.md). ## Add Memory -The global add memory composer accepts only a URL. - -Flow: - -1. Generate a UUID v7 memory ID. -2. Fetch the URL server-side. -3. Run the Defuddle-backed extraction pipeline. -4. Create SQLite metadata. -5. Write `{storePath}/memories/{memoryId}/CONTENT.md`. -6. Enqueue markdown backup work. - -If extraction succeeds, save extracted title, description, favicon URL, and the -markdown body produced by Defuddle. - -If extraction fails or returns empty markdown, still create a link-only memory. -Record extraction status and error details in SQLite. - -Raw HTML is not stored in the initial design. - -Default extraction runs behind an interruptible runtime boundary. The import -timeout budget covers fetch, validation, Defuddle parser work, and Defuddle -markdown generation; if the budget is exhausted, the importer returns link-only -fallback instead of persisting late extraction output. +The global composer accepts one URL. + +The shell owns one submission controller shared by the rail and phone popovers. +It assigns a cryptographically random UUID v7 identity to each normalized URL +attempt. Closing a popover does not abandon the pending request; retrying a +failed or response-lost attempt reuses its identity, while changing the URL +rotates it. + +1. Validate the optional `Idempotency-Key` header as a canonical UUID v7 before + configuration, database, or store work. Requests without the header remain + supported and receive a server-generated UUID v7 memory ID. +2. Validate the public URL, then durably reserve an idempotency key for that + preflight-normalized request URL. Reusing a key for a different URL fails + with `409`; URL equality alone never deduplicates memories. +3. Coalesce concurrent work for the same key and URL. A retry returns its + existing or creation-journal-recovered row before importing again. An old + reservation whose row and recoverable creation journal are both absent + returns a stable `409` without importing; this includes replays after later + deletion. A clean initial failure before recoverable state releases only the + newly inserted reservation so the same failed attempt can retry. +4. Validate that the configured backup environment is ready for a new write. +5. Acquire one of four process-wide URL-import slots without queueing. If all + slots are occupied, return `429 import_busy` with `Retry-After: 1` before + fetch or extraction begins; every terminal path releases its slot. +6. Fetch the public URL, then run Defuddle extraction inside the + interruptible import timeout boundary. +7. Use extracted Markdown on success or a safe Markdown link on link-only + fallback. Raw HTML is never persisted. +8. Persist a creation journal containing the intended SQLite row. +9. Durably publish `{storePath}/memories/{memoryId}/CONTENT.md` with + `overwrite: false`: write a same-directory temporary file, sync its bytes, + publish it without replacing an existing file, and sync the owning directory + hierarchy before SQLite success is possible. +10. Insert the SQLite `memories` row with the new content path and initial backup + status. +11. Remove the creation journal. If the SQLite insert fails, delete the newly + written memory directory before returning the error. +12. If backup is enabled, enqueue the written content path and persist the best + available backup status. + +Extraction failure or empty output still creates a link-only memory and records +the extraction detail. Import timeout covers fetch, host/redirect validation, +Defuddle parsing, and Markdown generation; late extraction output is discarded. + +Once both `CONTENT.md` and the memory row are durable, backup enqueue or backup +status-update failure must not turn the successful create into an ambiguous +failed request. Return the created memory with the best persisted status. +On startup, a surviving creation journal reconstructs a missing SQLite row only +when its owning `CONTENT.md` exists; otherwise the unused journal is removed. A +surviving journal with an existing row but missing canonical content is an +integrity failure and remains available for diagnosis rather than being cleared. + +## Delete Memory + +Deletion accepts only the canonical +`memories/{memoryId}/CONTENT.md` path owned by the requested row. + +Before inspecting or moving artifacts, deletion reserves the memory against +process-local artifact publication. It waits for an already-admitted short +publication to finish and rejects new translation, Flashback, and Psychiatrist +writes until deletion either completes or restores the memory after failure. + +1. Back up the current memory artifacts locally when git backup is enabled. +2. Persist a deletion journal, then atomically move the owning memory directory + into delete staging. +3. Commit the staged deletion before removing the SQLite row. +4. Remove staged content and the journal after the row is gone. + +Startup recovery restores staged content and marks backup pending while the row +still exists. If both canonical and staged content are already absent, recovery +marks the row pending, revalidates the full backup environment, commits the +deletion, and only then removes the row and journal. Any backup or validation +failure keeps the pending row and journal for retry. If the row is already gone, +recovery finishes staging cleanup. + +Operation-journal recovery is exclusive per resolved `storePath`: it waits for +active journaled mutations, and a queued recovery prevents a new journaled +mutation from starting. Add and delete acquire a shared lease before writing a +journal and retain it through terminal journal removal or rollback. Different +mutations may still run concurrently; the barrier exists only to prevent +recovery from consuming or restoring an operation that is still active. ## Browser-Assisted Import -Browser-assisted import exists as an optional local Chrome MV3 extension path -for pages the server cannot fetch or extract reliably. - -Flow: - -1. The operator enables browser import with local environment settings. -2. The user opens a page in the browser and clicks the TRAUMA extension. -3. The extension captures bounded content from the current user-visible tab. -4. The extension sends JSON to `/api/browser-import` on the local TRAUMA server - with a bearer token. -5. The server validates enablement, token, origin, content type, payload size, - URL shape, timestamp, and captured snapshot shape. -6. The server creates the memory through the same add-memory persistence path: - SQLite metadata, `CONTENT.md`, and backup enqueue. -7. The extension opens the created memory route or reports the server error. - -The extension is a privileged local client, not a trusted persistence layer. It -may provide browser-only access to the visible DOM, but final validation, -server-side Defuddle extraction, SQLite writes, and backup state remain -server-owned. Raw extension HTML is untrusted and must not bypass the server -sanitization and markdown-store contracts. - -## Flashback - -Reader content is not generally editable. Flashback creation changes SQLite -metadata and transient reader rendering; it does not rewrite `CONTENT.md`. The -same selection gesture also toggles off existing flashbacks. - -Flow: - -1. User selects text in `/memories/:id` or `/memories/:lang_code/:id`. -2. Frontend determines whether the selected range is already fully flashbacked. -3. If the range is not already flashbacked, the frontend renders an optimistic - flashback immediately. -4. If the range is already flashbacked, the frontend optimistically removes - flashback styling only from the selected range. -5. Frontend sends selected `text`, `prefix`, `suffix`, `start_offset`, and - `end_offset` to the server with the intended toggle operation. -6. Server resolves the selection against the active reader variant text, stores - `start_offset`, `end_offset`, and `content_hash`, then creates, deletes, - shrinks, or splits `flashbacks` rows so SQLite represents exactly the - flashbacked ranges that remain. -7. Server writes the active variant's `FLASHBACKS.json` as a deterministic - metadata export for git backup. -8. Server enqueues backup work for the metadata export. - -Flashback toggle rules: - -- Selecting unflashbacked text creates a flashback for the selected range. -- Selecting an already-flashbacked range unflashbacks the selected range only. -- Selecting a subset of a larger flashback preserves the unselected flashbacked - text by shrinking or splitting metadata. -- Selecting across multiple existing flashbacks removes only the selected - overlap from each affected flashback. - -Selection payload: - -1. `text` -2. `prefix` -3. `suffix` -4. `start_offset` -5. `end_offset` - -The server stores offsets in active-variant reader text and guards them with -`content_hash` in `sha256:` format. Hash-mismatched flashbacks are treated -as stale and are not rendered at a guessed location. - -Translated reader variants include `langCode` in Flashback toggle requests. -The server validates the current translation, reads translated `CONTENT.md`, -and uses translated reader offsets with a variant scope of `(memory_id, -lang_code, translation_output_hash)`. It does not write translated Flashback -changes into source rows. If the translated output is missing, stale, or -hash-mismatched, the Flashback toggle fails closed instead of guessing -translated text. - -Moment creation on translated reader variants also includes `langCode`. The -server validates the posted section against translated ToC data, then stores -the source ToC section with the same `sectionPath` and level. Moment rows remain -source canonical. - -If persistence fails, the optimistic UI state is rolled back or surfaced as -failed. - -If flashback persistence returns backup failsafe metadata, the frontend must -refresh the global backup failsafe alert before showing the local flashback -failure state. +The optional local Chrome MV3 extension handles pages the server cannot fetch +or extract reliably. + +1. The operator enables browser import and configures a cryptographically random + bearer token that satisfies the configuration contract. +2. The extension captures a bounded snapshot from the current user-visible tab. +3. It POSTs JSON to `/api/browser-import` on the local TRAUMA server. +4. The server validates enablement, token, extension origin, content type, + payload size, URL, timestamp, and captured snapshot shape. +5. Before reading the request body, the route acquires one of two process-wide + browser-import slots without queueing. Overflow returns + `429 browser_import_busy` with `Retry-After: 1`; every response and failure + releases its slot. +6. Server-side Defuddle extraction and the normal add-memory persistence flow + create the memory. +7. The extension opens the created reader or reports the server error. + +Extension HTML is untrusted input. The extension never writes the store or +SQLite directly and cannot bypass server sanitization, URL policy, or backup +readiness. + +## Flashback Toggle + +Reader content is not generally editable. A selection toggles variant-local +Flashback ranges without rewriting `CONTENT.md`. + +1. The user selects text on a source or translated reader. +2. The frontend optimistically adds or removes styling only for the selected + range. +3. It sends text, prefix, suffix, start/end offsets, intended operation, and the + active language when translated. +4. The server resolves the selection against current active-variant reader text + and fails closed if the content or translation hash is stale. +5. It durably persists backup intent and a variant-specific export + reconciliation intent before changing SQLite rows. +6. Under the shared artifact-variant lock, it creates, deletes, shrinks, or + splits only the intended SQLite ranges and rewrites deterministic + `FLASHBACKS.json`. +7. It clears the reconciliation intent only after confirmed export publication, + then enqueues that export for backup. + +Offsets use canonical reader text and a `sha256:` content hash. Translated +rows are additionally scoped by language and translation output hash. A stale +row is not rendered at a guessed location. Failure before authoritative SQLite +and export publication rolls back or clearly fails the optimistic UI. Once +both are durable, backup enqueue or status failure keeps the toggle successful, +returns an explicit backup warning with `pending` or `failed` status, and does +not restore the old rows or export. Backup failsafe metadata also refreshes the +global alert. Post-rename durability uncertainty is a committed success with an +explicit warning: Reader and collection clients preserve the new interaction +state and revalidate authoritative SQLite projections. Startup reconciliation +runs independently of git backup state, regenerates non-empty or empty exports, +and rereads rows while holding the same artifact-variant lock used by toggles. + +## Moment Toggle + +Moments bookmark source-canonical reader sections. + +1. The reader submits the selected table-of-contents section. +2. On a translated route, the server validates the translated section and maps + it to the source section with the same path and level. +3. The server creates or removes the corresponding SQLite `moments` row. +4. Reader and `/moments` projections are revalidated. + +Moment persistence does not mutate reader Markdown. The current contract has no +Moment store export; see the ownership matrix before changing backup behavior. + +## Collection Browse Pagination + +Flashback and Moment interactive reads use bounded keyset pages. + +1. The client submits no cursor for the first page or returns the opaque cursor + from the preceding page. +2. The server validates the version, collection kind, timestamp, ID, and limit + before opening collection storage. +3. The repository applies `(created_at, id)` descending keyset predicates and a + SQL limit. It never loads the full collection for a paged request. +4. Flashbacks validate only bounded raw batches and advance past stale rows; + Moments resolve targets only for the current raw page. +5. `/flashbacks`, `/moments`, and Reader All replace their current page rather + than accumulating rows. + +No-query `GET /api/moments` retains its full-list envelope. The Flashback +mutation route remains POST-only. Paged clients explicitly use `page=1` on the +Moments API or the separate `/api/flashbacks/page` route; mutation behavior is +unchanged. + +## Brilliant Translation + +Translation runs through the separately operated Codex app-server and uses +durable SQLite job/chunk state. Before new or recoverable Codex work is reserved, +the shared runtime-isolation assertion must confirm that an external boundary +makes host data unreadable to the app-server. + +Before the Reader starts a translation, it sends the selected language, model, +and reasoning effort together to `PATCH /api/settings/translation-defaults`. +The route applies the normal mutation Host/origin/body guards, validates every +field and the Codex catalog selection, then persists all three defaults with one +settings update. The Reader uses the canonical language/model/effort returned in +`SettingsState` for the translation `POST`; it must not reuse stale form values. +The language-mismatch rejection in the runner remains a required defense for +direct or stale callers. Existing language-only and Codex-only settings routes +remain compatible for their current clients. + +1. `POST /api/memories/:memoryId/translations` validates the request, resolves + the configured target language and Codex model/effort, then opens source + `CONTENT.md` and reads at most the 20-MiB limit plus one byte into + demand-sized, fixed-capacity chunks. Source admission does not trust a prior + file size: it continues positional reads through short reads, closes the + file handle on success or failure, and rejects overflow before streaming + UTF-8 decoding, Markdown/frontmatter parsing, document-type inference, or + incremental raw-byte hashing. +2. If a completed translation and file are current, the route returns + `status: current`. If the same source/language already has active work, it + returns `status: active` and reschedules that job when recoverable. +3. Otherwise it parses the source into translatable blocks, creates one pending + job plus its chunk records, emits queued events, closes the short-lived + probe/model-selection Codex client, and schedules only durable job identity + and runtime dependencies on the in-process sequential runner. The runner + opens a fresh Codex client only when that job reaches execution. Before the + probe or durable job creation, the chunker deterministically splits oversized + paragraphs and lists only at Markdown-safe ordered boundaries. Every source + chunk is at most 2,500 rough tokens and its complete initial prompt is at most + 64 KiB by serialized UTF-8 bytes. An oversized fenced or other structurally + indivisible block fails with `validation_failed` before scheduling. Aggregate + admission also runs before client creation and durable rows: the complete + source is at most 20 MiB, with at most 16,384 translation segments and 4,096 + chunks. These limits retain the supported import ceiling and ordinary long + articles while bounding short-sentence expansion and SQLite job fan-out. +4. The runner claims `pending` work or resumes `running`, `stitching`, or + `committing` work. It re-reads the source through the same bounded admission + before creating a client and marks the job stale when the source hash + changed. The final commit reload applies that same source-byte limit again, + so growth between resume admission and publication cannot reach decoding, + parsing, hashing, output publication, or backup. Before reusing any chunk, + it requires the persisted + prompt-policy and chunker versions, chunk count, chunk indexes, source chunk + hashes, and ordered block IDs to match the current runtime manifest exactly. + An incompatible job fails terminally and permits a fresh attempt; it cannot + write output or enter backup. +5. Each chunk is sent through the Brilliant prompt/validation boundary and its + validated Markdown and projection data are persisted before the next chunk. + The complete outbound prompt, including retry diagnostics, is checked again + against the fixed 64-KiB UTF-8 limit immediately before the translation + client is called. Prompt overflow cannot reach the app-server WebSocket and + terminally fails the chunk without automatic retry. + Translated text is admitted by UTF-8 bytes before projection or payload + persistence: at most 1 MiB per segment, 4 MiB across one chunk, and 56 MiB + for the complete translated `CONTENT.md`, including preserved frontmatter. + The runner counts resumed chunks and rejects aggregate overflow before the + next chunk is persisted; final stitching rechecks the same bound before + joining or publishing output. Overflow is a terminal, non-auto-retried + `validation_failed` attempt. + Codex events pass fixed serialized UTF-8 admission before any delta reaches + replay or SSE. The 4,096-event/4-MiB chunk-attempt budget resets for each + attempt; the 262,144-event/32-MiB job budget accumulates across every chunk + and retry. A 64-KiB event or any cumulative overflow stops callbacks, + interrupts the active turn best-effort, and fails without retry through the + existing safe unknown error contract. + Chunk partition passes reuse already-built segment-manifest payloads for + unchanged groups. Source and translated projection maps retain the canonical + reader text and protected-offset semantics. A translation-local compact + boundary map only remaps normalized LF source offsets back to raw CRLF + positions; hidden paragraph separators remain collapsed exactly as they are + in canonical reader text. The legacy Flashback reader projection contract is + unchanged. +6. Cancellation is checked before and after chunk work. Pending jobs cancel + immediately; running jobs move to `cancel_requested` and interrupt the active + Codex turn. Stitching, committing, and terminal jobs reject cancellation to + avoid ambiguous final state. +7. The runner transitions through `running -> stitching -> committing` with + compare-and-set guards, then rechecks the source hash. +8. Completed chunks are stitched in order and the final Markdown/frontmatter + structure is validated. +9. Backup intent for `CONTENT.md`, `TRANSLATION_MAP.json`, and the translated + `FLASHBACKS.json` projection is persisted before terminal artifacts are + written. +10. The terminal artifact/SQLite publication holds the memory mutation + reservation and the same per-language Flashback projection lock used by + toggle/recovery. It rechecks the reservation immediately before each write. + The translated `CONTENT.md` is file-synced and atomically renamed into its + language directory. TRAUMA hashes the written bytes, writes + `TRANSLATION_MAP.json`, replaces SQLite projection spans, and records a + Flashback reconciliation intent before marking the job complete with output + path/hash. +11. Completed chunk payloads are purged best-effort. Translation content and + both projections are published and enqueued for backup. The Flashback + projection is written for the new output hash even when it has no rows, and + its intent is cleared only after confirmed publication. Export or enqueue + failure does not undo a completed translation; a retained intent is replayed + on startup. + +A crash after the atomic output rename but before projection or SQLite +completion leaves the durable job in `committing` with its chunks intact. The +next start for the same source and language reschedules that job; replay rewrites +the output and projections, completes SQLite state, and repeats backup intent +and enqueue. + +In-process replay retains at most 500 events and 4 MiB, evicting the oldest +events by both limits while preserving order. The SSE endpoint pulls the durable +job snapshot and replay one event at a time, then follows live events through a +per-subscriber queue capped at 128 events and 3 MiB. Heartbeats are sent only +when the stream has desired capacity. A slow subscriber overflow unsubscribes +and errors only that connection; reconnect reads the bounded replay. Terminal +events or refreshed terminal snapshots are sent before exact cleanup and close. +Reconnecting after a process restart still relies on the durable snapshot, not +on replay history that lived only in the previous process. + +Codex device-login polling owns an `AbortController` per polling generation and +passes its signal into each in-flight auth-status `GET`. Canceling setup or +unmounting Settings aborts both delay and fetch work. An `AbortError` is normal +cancellation: it must not publish failure feedback or a stale auth state, and an +older poll completion must not clear or re-enable controls owned by a newer +action generation. ## Psychiatrist -Psychiatrist is a reader-only, memory-scoped assistant. It appears on source and -translated reader routes and talks to TRAUMA API routes only; browser code never -connects to Codex app-server directly. - -Flow: +Psychiatrist is a reader-only, memory-scoped assistant. Browser code talks only +to TRAUMA API routes; it never connects directly to Codex app-server. -1. The reader creates or resumes a thread for the active memory variant through - `/api/memories/:memoryId/psychiatrist/threads`. -2. The server loads the active source or translated memory context, records the - active content hash, and stores thread metadata under +1. A source or translated reader creates or resumes a thread for the active + memory variant. +2. The server loads current memory context and stores thread metadata under `{storePath}/memories/{memoryId}/threads/{threadId}/`. -3. A user prompt creates one pending pair in `PAIRS.jsonl` before Codex - execution starts. Prompts and answers are pair records under the thread - subtree, not SQLite rows. -4. The server builds the deterministic Psychiatrist prompt from the repo-local - `psychiatrist` policy, active memory context, visible pair history, and - current user prompt. -5. Codex app-server turns run backend-only with shell access, file editing, - local filesystem browsing, project/store roots, and network access denied by - default. Network may be enabled only for a user-approved web-source turn. -6. Safe process and answer events are written to - `streams/{turnId}.jsonl` before SSE fan-out, so navigation and reload can - replay already-visible output. -7. A completed first answer writes `pairs/{pairId}/RESPONSE.md`, rewrites - `THREAD.md`, appends a completed pair revision, and enqueues built-in git - backup with reason `psychiatrist_thread_update`. -8. Regenerate reuses the same stored prompt and context provenance for the same - pair, overwrites the existing `RESPONSE.md`, rewrites `THREAD.md`, and - enqueues backup with reason `psychiatrist_response_regenerate`. - -Every durable assistant answer belongs to exactly one stored user prompt in the -same pair. Failed, canceled, stale, and permission-required turns must not append -orphan assistant responses. Psychiatrist writes are limited to the memory-local -`threads/` subtree; canonical `CONTENT.md`, translated `CONTENT.md`, taxonomy, -Flashbacks, Moments, settings, and other SQLite state are not modified by chat. +3. A user message writes the pending pair revision, `PROMPT.md`, context + snapshot, turn record, and stream path before Codex execution begins. +4. The deterministic prompt combines the repo-local Psychiatrist policy, active + memory context, visible pair history, and current user prompt. +5. Codex turns run backend-only from an ephemeral empty working directory with + `approvalPolicy: never` and a read-only sandbox. Prompt policy forbids shell + and filesystem use; the required external process/container boundary makes + the home directory, application project, and memory store unreadable. Network + is disabled unless the user approved public web sources for that turn. +6. Safe process and answer events enter a bounded serialized persistence queue. + Each accepted event is appended and file-synced in the turn stream before + SSE fan-out, so navigation and reload can replay only durable visible output. +7. A completed first answer writes `RESPONSE.md`, updates pair/thread/turn state, + refreshes `THREAD.md`, and enqueues all completed artifacts for backup. +8. Regenerate reuses the stored prompt/context provenance for the same pair, + replaces that pair's response, refreshes the transcript, and enqueues the + updated artifacts. + +Every durable answer belongs to one stored user prompt in the same pair. Failed, +canceled, stale, and permission-required turns cannot append orphan assistant +responses. Closing the panel or navigating away disconnects browser SSE only; +Stop is the explicit cancellation action. + +Process and answer-delta writes are serialized and fully drained after Codex +settles but before a terminal state or terminal event is written. A stream +persistence failure fails an otherwise successful turn; when Codex also fails, +its safe failure remains authoritative. Closed turn queues reject late Codex +callbacks, so no non-terminal event can be persisted after terminal state. + +The fixed turn admission policy permits at most four active-or-reserved turns +across all threads. A second turn for the same thread remains a `409` conflict; +different-thread overflow returns `429 turn_capacity_exceeded` with +`Retry-After: 1` before a Codex client is created. Capacity is released on +startup failure, cancellation, and every detached terminal path. + +The fixed event admission policy permits at most 64 KiB per serialized Codex event, +128 events or 1 MiB pending, and 4,096 events or 4 MiB over one turn. Final +answer text is capped at 2 MiB. The durable stream is capped at 4,100 rows and +8 MiB. Exceeding any boundary stops admission and persists a safe failed outcome +when the existing stream still has room for its terminal event; no partial +answer is published as `RESPONSE.md`. + +On success, the queue drains and the final answer passes its byte check before +backup intent or answer publication. `RESPONSE.md` plus the completed +`PAIRS.jsonl` revision become durable before the completed turn record and +completed stream event. A manifest or `THREAD.md` finalization fault after that +canonical pair save is a completed answer with a warning and is repaired from +`PAIRS.jsonl`; it is not rewritten as a failed answer. On failure, the failed +pair/turn state is durable before the failed stream event is appended. + +SSE replay is delivered one encoded event per pull. A live connection buffers +at most 128 not-yet-delivered events and 3 MiB while replay or a slow consumer +blocks delivery; exceeding that budget unsubscribes and errors only that +connection. Reconnect still reads the bounded durable stream. + +The browser closes a named terminal `EventSource` before validating that frame, +so a malformed terminal followed by server EOF cannot enter native EventSource +reconnect indefinitely. It performs one canonical thread reload for the same +reader, thread, and turn. The canonical snapshot either publishes the terminal +pair and returns the dock to idle, reconnects the still-active turn with that +single automatic recovery already consumed, or leaves the existing manual Retry +path on reload failure or a second malformed terminal. This reconciliation does +not call the cancel route and preserves a same-thread transcript page and scroll +position. + +The latest durable pair revision is authoritative for `RESPONSE.md`. Startup +recovery rewrites a missing or torn completed response and removes a response +that has no completed revision. Detached turn failures are contained even when +their best-effort failure-state write also fails. + +Psychiatrist writes are limited to the memory-local `threads/` subtree. Source +and translated content, taxonomy, Flashbacks, Moments, settings, and SQLite +domain state remain unchanged. Each short thread, pair, response, turn, and +stream publication holds the same memory mutation reservation as deletion. ## Git Backup -Backup is built-in git backup, not a generic hook system. - -Flow: - -1. Markdown write succeeds. -2. Backup work is placed on the in-process sequential queue. -3. The backup worker uses `projectPath` as the working directory. -4. The worker stages only changes under `storePath`. -5. The worker commits with the configured message template, including the - backup action when `{action}` is present. -6. The worker pushes only when configured. -7. SQLite backup status fields are updated. - -Backup failures do not roll back memory creation or flashback creation. - -On startup, TRAUMA should find pending, queued, or failed backup states that are -eligible for retry and re-enqueue them. `queued` is process-local, so queued rows -from a previous process are eligible after restart. - -Backup failsafe recovery actions must be retry-safe. If migration already -copied a file before a later git step failed, rerunning migration may accept the -existing target only when its bytes match the source. Different target content -remains a hard conflict. - -Backup readiness is tied to the full backup identity, not just filesystem -paths. The persisted stamp must match project path, store path, git remote, -remote URL, branch, and already-successful tracked content before new writes are -accepted. If the repository is recreated at the same path, or the configured -remote/branch changes while successful backup rows already exist, TRAUMA must -force an explicit recovery path instead of silently treating the new repository -as complete. - -When already-successful backup rows point at missing, out-of-scope, or -untracked content, the alert is a content-integrity failure rather than a path -drift. The UI and logs must not describe this as a backup location change or -offer path migration as a remedy. - -Only `missing_file` content-integrity alerts may offer deletion of the orphan -SQLite memory record. If the content still exists but is untracked or outside -the configured paths, recovery must preserve the record and require backup -repository/path repair instead. - -If migration commits local backup content but the configured push fails, the -operator must be able to retry that recovered push after repairing the remote. -A push-failure alert must not turn a completed local migration into an -unrecoverable banner state. +Backup is built-in git backup, not a generic hook system. Every built-in git +command uses a command-scoped null `core.hooksPath`, so repository and global +hooks do not run during normal backup, retry, startup recovery, or failsafe +repair. + +1. The owning domain persists its durable store artifact or backup intent. +2. Explicit relative paths enter the in-process sequential queue. +3. The worker uses `projectPath` as the git working directory and stages only + those paths after confirming they remain under `storePath`. +4. It commits with the configured template and pushes only when configured. +5. SQLite backup status and failsafe state are updated where the domain owns + such state. + +Backup failure does not roll back an already durable memory, Flashback export, +translation, or Psychiatrist answer. Startup retries eligible pending, queued, +or failed work; process-local `queued` state from a prior process is eligible. +Preparation and enqueue failures are isolated per memory so one corrupt retry +candidate cannot prevent later eligible memories from running. + +Backup readiness is tied to the full persisted identity: project/store paths, +remote name and URL, configured and checked-out branch, and already-successful +tracked content. A recreated repository or changed identity requires explicit +recovery rather than silent acceptance. Successful content paths are compared +with one tracked-index snapshot per readiness check rather than starting a git +process per memory. + +Recovery is retry-safe. Existing migration targets are accepted only when their +bytes match the source. Each migrated file is copied into an owned +same-directory temporary file, synced, and published atomically without +overwriting an existing destination; the directory entry is synced before the +action continues when the filesystem supports directory sync. A retry removes +only the exact temporary path owned by the approved alert generation, accepts +an already-published destination only when its bytes match the synced snapshot, +and rejects a true conflict. Migration requires disjoint previous/current trees, +does not traverse `.git`, and rejects symlinked or non-directory destination +components before rechecking canonical containment at publication. + +Failsafe dry runs and applied actions capture the active alert generation inside +a fair process-local exclusive lease keyed by the persisted config database. +Applied actions revalidate that generation before side effects and clear it with a +compare-and-delete, so concurrent or stale confirmations cannot consume a +replacement alert. Ordinary Git backup writers and environment alert/stamp +writers participate in the same lease, preventing local `HEAD` or alert changes +during recovery. A stale web confirmation returns `409`; the CLI fails and +leaves the current alert available for a fresh dry run. + +Git writers acquire that action lease before a runtime storage borrow. A writer +queued behind root-changing recovery therefore fails before Git side effects +once storage admission is suspended. Long-lived Psychiatrist turns keep a +dedicated borrow through terminal persistence and backup enqueue; live SSE keeps +one only while loading its initial replay. Translation workers hold their +database borrow for the complete active run. + +Recovery also reserves current and previous project/store roots through the +cross-process runtime lease before reading old content. Config revert returns +its own request and database borrows, then synchronously suspends admission only +when no other borrower remains. A busy runtime returns `409` without rewriting +config. Successful suspension permits only the atomic config write and requires +a process restart; no database or store access follows in that process. Content +migration does not change configured roots and does not suspend admission. + +A push failure after local migration preserves the local commit and previous +environment stamp, leaves a durable push-failure alert, and remains retryable +after remote repair. Recovery validates the exact repository root, remote +fingerprint, checked-out branch, and `HEAD` before Git mutation and after the +idempotent push. Only then does it update the environment stamp and clear the +approved alert in one SQLite transaction. When push is configured, Git push +cannot share a transaction with SQLite: a process failure +after the remote accepts the push but before the SQLite transaction leaves the +alert active, and retrying the already-pushed commit safely completes the stamp +update. The runtime root-set lease coordinates separately running servers and +maintenance CLIs; the action lease orders work inside one process. + +Missing, out-of-scope, or untracked content recorded as successfully backed up +is a content-integrity failure, not path drift. Only a rechecked `missing_file` +case may offer deletion of the orphan SQLite memory row; other cases require +repository/path repair without discarding content. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d4bfe56f..f56c5580 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -1,59 +1,85 @@ # Architecture Overview -TRAUMA is a single-user, local-first bookmark management app that can also run -on a single VPS or home server with persistent local disk. +TRAUMA is a single-user, local-first bookmark manager that can also run as one +self-hosted instance on a VPS or home server with persistent local disk. -The app uses a SolidStart monolith: UI, route handlers, server functions, and -server modules live in one TypeScript project. The monolith is intentional. The -project is a sub-project and should stay light to operate. +The app is an intentional SolidStart monolith. UI, route handlers, server +functions, and server modules live in one TypeScript project to keep operation +and maintenance light. ## Runtime Shape -- One Bun process. +- One Bun application process. - One SolidStart app. -- One SQLite metadata database. -- One markdown store rooted at `storePath`. -- One built-in git backup queue for markdown content. +- One SQLite database for relational runtime state. +- One file-backed memory store rooted at `storePath`. +- One in-process sequential git backup queue for selected store artifacts. +- An optional, separately operated Codex app-server for Brilliant translation + and Psychiatrist turns. -Avoid introducing separate API services, external queues, managed databases, or -serverless-first assumptions unless a later design explicitly changes the -foundation. +The one-process rule is fail-closed. A process-lifetime lease protects the +effective `databasePath`, `storePath`, and `projectPath` before storage opens. +Overlapping paths and filesystem aliases contend; disjoint sibling trees can +run independently. Maintenance commands use the same ownership boundary. + +Those three storage-root paths are restart-scoped. Manual path changes are +rejected before the new storage opens. A root-changing backup recovery reserves +both the current and previous roots, returns its own request and database +borrows, then suspends storage admission only when no other admitted work is +active. A busy runtime rejects the recovery without changing config. After +successful suspension, or an ownership-integrity failure during suspension, +restart the TRAUMA process before any further storage work. + +Direct database initialization holds the same database-family ownership for the +returned connection lifetime. This also serializes migration and WAL setup when +`initializeDatabase` is used outside the server runtime. Lease refresh may add +inode identities only for the originally admitted primary and sidecar paths; +path retargeting and newly introduced hardlink aliases fail before ownership +can expand. + +Avoid separate API services, external queues, managed databases, or +serverless-first assumptions unless a current design explicitly changes this +shape. ## Module Boundaries -Server-side code should be organized around these responsibilities: +Server-side code is organized around these responsibilities: -- `config`: load and validate `trauma.config.json`. -- `db`: Drizzle schema and repository functions over SQLite. -- `importer`: fetch URLs and extract readable content. -- `store`: create and read memory markdown files. -- `backup`: enqueue and run built-in git backup work. -- `reader`: render markdown through the curated reader pipeline. -- `ui shell`: route-level layouts, navigation, filters, composer, and reader UI. +- `config`: load and validate `trauma.config.json` and operator environment. +- `db`: Drizzle schema, migrations, and repositories over SQLite. +- `importer`: fetch public URLs and extract readable content. +- `store`: resolve, create, read, and remove memory-store artifacts. +- `backup`: validate backup identity and enqueue explicit store paths for git. +- `reader`: render untrusted Markdown through the curated reader pipeline. +- `translation`: run durable Brilliant jobs through Codex app-server. +- `psychiatrist`: run memory-scoped turns and persist thread artifacts. +- `ui shell`: shared navigation, responsive chrome, filters, popovers, and + route-owned surfaces. -Each module should expose a narrow API. UI code should not reach into storage, -backup, or importer internals directly; it should call route loaders, server -functions, or repository-level interfaces. +Each module exposes a narrow API. UI code does not reach into storage, backup, +importer, translation, Psychiatrist, or database internals; it uses route +loaders, actions, server functions, or domain interfaces. ## Dependency Direction -Preferred dependency direction: - ```text -UI routes/actions - -> application/server functions - -> importer | store | backup | reader | db repositories - -> config | filesystem | SQLite | git +UI routes and actions + -> application and server functions + -> domain services and repositories + -> config | filesystem | SQLite | git | Codex app-server ``` -Keep filesystem writes inside `store` and git operations inside `backup`. -Keep SQL details inside `db`. +Keep SQL inside `db`, store-path writes inside owning server modules, git +operations inside `backup`, and Codex protocol details behind their adapters. ## Explicit Non-Goals -- Next.js. -- PostgreSQL in the initial implementation. +- Next.js or React-specific routing assumptions. +- PostgreSQL or managed database services. - Serverless or edge runtime compatibility. -- Authentication or user ownership. +- TRAUMA user accounts, sessions, public signup, or multi-user ownership. - External queue infrastructure. - Generic lifecycle hooks. + +Codex app-server authentication used by optional backend features is not a +TRAUMA user-authentication system. diff --git a/docs/architecture/ui-and-routing.md b/docs/architecture/ui-and-routing.md index b7931f97..1d12b018 100644 --- a/docs/architecture/ui-and-routing.md +++ b/docs/architecture/ui-and-routing.md @@ -1,148 +1,140 @@ # UI And Routing Architecture -TRAUMA's UI direction follows the current X-style layout while staying focused -on bookmark/memory management. +This document owns the canonical route and responsive-shell contract. The +[design system](../references/design-system/INDEX.md) owns visual and +accessibility details without redefining route availability. ## Canonical Routes -Initial canonical routes: +- `/memories`: browse, search, filter, read state, and memory actions. +- `/memories/:id`: source reader. +- `/memories/:langCode/:id`: current translated reader. +- `/flashbacks`: renderable source and translated Flashback excerpts. +- `/moments`: source-canonical section bookmarks. +- `/settings`: translation defaults, Codex model/effort, and Codex auth state. -- `/` -- `/memories` -- `/memories/:id` -- `/flashbacks` +`/` redirects to `/memories`. `/highlights` redirects to `/flashbacks` and +`/flashback` redirects to `/moments` only for legacy compatibility; do not link +to them or use their retired terminology in new contracts. -`/` redirects to `/memories`. +`/category`, `/tags`, `/backup`, and `/memories/new` are not routes. Categories +and tags are managed inline and through the browse right rail. Backup is exposed +through status/failsafe surfaces. Add memory is a global shell popover. -`/memories` is the canonical browse and filter route. Query string state holds -filters and view options: +## Browse Query State + +`/memories` owns filter and view state in the URL: ```text /memories?q=...&category=...&tag=...&flashback=...&view=list|grid ``` -`/flashbacks` is the canonical route for browsing renderable flashbacked -excerpts across memories and reader content variants. +`q` supports free text, `title:`, `url:`, `tag:`, `category:`, `flashback:`, and +standalone `read`/`unread` terms. Search covers memory metadata, taxonomy, and +renderable Flashback text/context, not the full reader body. Explicit taxonomy +and Flashback query keys combine with search terms rather than clearing them. + +Memory results are cursor-paginated. Read-state tabs manipulate the search +terms, and list/grid remains URL-addressable even when no dedicated view toggle +is rendered. -`/category`, `/tags`, and `/memories/new` are not initial routes. Category/tag -management pages are future work. +`/flashbacks` and `/moments` use an opaque `cursor` query value. The URL is the +page source of truth: Next writes the continuation cursor, First removes it, +and browser Back or Reload restores that exact page. These routes render only +the current page; they never append earlier pages to the DOM. ## Shell Layout -Desktop layout: +Desktop (`min-width: 1041px`): -- Left shared navigation. -- Center content area. -- Right category/tag/Flashback panel. +- Persistent `275px` left rail. +- Route-owned main pane up to `840px`. +- `360px` right rail with category/tag filters and contextual shortcuts. +- Ready source and translated readers may replace the top right-rail slot with + their table of contents. -The left navigation is an app-shell component shared by all routes. It should -not be implemented as a page-specific component. +Tablet (`721px` through `1040px`): -The right panel lists categories, tags, and Flashback shortcuts. Category and tag -items update the `/memories` query filter. Flashback shortcuts apply -`/memories?flashback=`. Memory navigation is handled by the -`/flashbacks` row title/link or reader anchors, not the primary right-panel -shortcut. Translated Flashback links route to `/memories/:lang_code/:id` when -the Flashback belongs to a translated variant. +- Compact `80px` icon rail and route main pane. +- No right rail, navigation drawer, filter drawer, or duplicate brand/filter + header. +- Add memory and Theme remain available from rail popovers. -## Search And Filters +Phone (`max-width: 720px`): -`/memories?q=...` searches memory metadata and flashback metadata. The query -must match title, URL, description, category names, tag names, flashbacked text, -and stored flashback prefix/suffix context. This is not full body search; body -FTS remains future work. +- Compact brand header, route main pane, and fixed horizontally scrollable + bottom `Primary tabs` bar. +- Live tabs: Memories, Flashbacks, Moments, and Settings. +- Disabled placeholders: Categories, Tags, and Backup. +- Add memory and Theme are actions that open popovers above the bar. +- No navigation or filter drawer. -`flashback=...` filters memories to a specific flashback ID. This is mainly -used by right-panel flashback shortcuts. +The shell owns global chrome, composer/theme popup state, backup alert, and the +right-rail slot. Routes own headers, controls, loading/empty/error states, and +content. Responsive route/component sizing should use the shared fluid and +container-query utilities rather than device-specific wrappers. ## Flashbacks View -`/flashbacks` is a flashback-first browse view. +`/flashbacks` renders dense excerpt rows. Each row shows muted prefix/suffix +context, selected text as the focal content, and the memory title as subordinate +metadata. Its link opens the matching source or translated reader at the +Flashback anchor. -Each flashback row shows the memory title, muted prefix context, flashbacked -text, and muted suffix context. The visual treatment should feel -close to a GitHub pull request file-review view: dense rows, selected text as -the focal content, muted surrounding context, subordinate source metadata, and -no full article rendering. +Right-rail Flashback shortcuts may filter `/memories` with `flashback=`. +They do not replace direct reader links from the canonical Flashbacks route. +The route uses a bounded server page and exposes First/Next navigation. A page +may be empty while still offering Next when stale stored rows were scanned. -Clicking the row content or memory title opens the source or translated reader -route at the corresponding flashback anchor. +## Moments View -## Responsive Behavior +`/moments` lists saved reader sections. Rows open the source reader at the +stored section anchor and expose Moment deletion. A Moment selected on a +translated reader maps to its source-canonical section before it appears here. +The route uses the same URL-owned First/Next page model as `/flashbacks`. -Responsive behavior is part of the initial design. +## Add Memory -On narrow screens: +Add memory is a shared anchored `Popup` available from desktop/tablet rail and +phone tabs. It accepts one URL and has no dedicated route. Escape, outside +pointer dismissal, and successful completion use the shared popup lifecycle. +Rail and phone forms project one shell-owned URL, pending, error, and +idempotency attempt state. Dismissing or switching popovers cannot start a +duplicate request, and an unmounted form cannot navigate or close a later +popover when its request settles. -- Preserve the same conceptual shell. -- Collapse left navigation into a drawer. -- Collapse right filters into a drawer. -- Keep the add memory composer globally reachable. +## Reader -## Styling System +The source route reads source `CONTENT.md`. A translated route renders only a +completed translation whose source hash, output hash, job row, and language +file are current. -UI styling is implemented with Tailwind CSS v4 through the SolidStart Vite -configuration. +Both reader variants support: -- `src/styles/tailwind.css` owns Tailwind imports, theme tokens, and minimal - document-level base styles. -- Page and component styling belongs on JSX through static Tailwind classes and - Solid `classList`. -- Reader HTML produced from markdown uses Tailwind Typography plus narrow - arbitrary variants for sanitized markup that components cannot author - directly. -- Do not rebuild the removed `src/styles/app.css` semantic selector layer. +- GitHub Flavored Markdown, footnotes, tables, and task lists. +- Syntax highlighting. +- Sanitized HTML and controlled external embeds. +- Stable heading anchors and a live table-of-contents reading range. +- Variant-local Flashback marks and source-canonical Moments. +- Brilliant translation controls and variant tabs. +- The memory-scoped Psychiatrist dock. -## Add Memory Composer +Text selection toggles Flashback ranges. Source and translated Flashbacks never +share offsets: translated writes include language and current output hash, and +only current rows render. Moment creation from translated content is validated +then mapped to the matching source section. -Add memory is a global composer modal or drawer. It accepts only a URL in the -initial design. +Psychiatrist appears only on ready source and translated readers. It creates or +resumes a thread for the active memory variant through TRAUMA APIs; the browser +never connects directly to Codex app-server. -Do not create `/memories/new` for the initial implementation unless a later -design changes the route model. +External embeds may auto-load only after reader sanitization and media URL +policy. This has privacy/network effects and must not be expanded to unsafe or +private targets. -## Reader +## Styling Boundary -`/memories/:id` renders source `CONTENT.md` in read mode. -`/memories/:lang_code/:id` renders a current translated variant from -`memories///CONTENT.md` when the translation row and file -hash are current. - -Psychiatrist is a reader-only surface on `/memories/:id` and -`/memories/:lang_code/:id`. It is not rendered on `/memories`, `/flashbacks`, -settings, or shell-only routes. The reader creates or resumes a memory-local -thread for the active source or translated variant, and all chat traffic goes -through TRAUMA API routes rather than browser-to-Codex connections. - -The initial markdown reader supports: - -- GitHub Flavored Markdown. -- Syntax flashbacking. -- HTML sanitization. -- Footnotes. -- Heading anchors. -- Table of contents that tracks the reader's live position and highlights the - active chapter reading range (see the design-system reader-and-content TOC - reading-progress contract). -- Controlled external embeds. -- Flashback marks. - -Text selection inside reader content is a flashback toggle. Selecting -unflashbacked text creates a flashback. Selecting text that is already -flashbacked removes flashback styling from the selected text only, preserving -any unselected flashbacked text around it. - -Flashbacks are local to the reader content variant where they are created. -Source Flashbacks use source reader offsets. Translated Flashbacks use -translated reader offsets and are scoped to the completed translation output -hash. Global Flashback browse and memory search surfaces include renderable -Flashbacks from both source and translated variants. - -Translated reader routes show translated Flashbacks for the active translated -variant only. Source Flashbacks do not automatically appear in translated -content. Creating a Flashback from a translated route sends the active language -to the backend so the write is stored against the translated `CONTENT.md` and -current translation output hash. - -External embeds auto-load in the initial design. This has privacy and network -side effects; future configuration may allow lazy or disabled embeds. +Tailwind CSS v4 and semantic tokens own styling. Route and component classes +belong in JSX; sanitized reader markup may use narrowly scoped selectors in the +global stylesheet. Do not restore the removed broad `src/styles/app.css` +selector layer. diff --git a/docs/operations/local-self-hosting.md b/docs/operations/local-self-hosting.md index f6d037bd..6cd1c7e6 100644 --- a/docs/operations/local-self-hosting.md +++ b/docs/operations/local-self-hosting.md @@ -10,26 +10,53 @@ Expected runtime: - One Bun process. - One SolidStart app. - One SQLite database file. -- One markdown store on persistent disk. -- Optional git remote for markdown backup. +- One memory artifact store on persistent disk. +- Optional git remote for built-in store backup. -The initial deployment target is a local machine, VPS, or home server. The +The deployment target is a local machine, VPS, or home server. The server must have persistent disk access. -## Data Ownership +TRAUMA enforces the one-process operating model before a request can open +runtime storage. One lease owns the complete effective database, artifact +store, and backup project root set for the Bun process lifetime. Overlapping +paths, symlinks, and hardlinks contend; disjoint sibling trees remain +independent. A conflict reports the recorded owner PID and roots. Stop the +active owner instead of deleting lease state. + +The stable coordinator lives at +`/.local/state/trauma/runtime-leases/coordinator.sqlite`, +outside application data and built-in backup. Do not version, copy, or delete +it while TRAUMA or a maintenance command is running. Crash-stale state is +cleaned automatically by the next owner. If startup reports an unsupported +coordinator schema, stop every TRAUMA process and maintenance command, verify +none remains, move the `runtime-leases` directory aside, and start the current +version again. + +Coordination covers processes under the same OS account on one host filesystem +namespace. Separate hosts, UIDs, or containers do not share it, and unusual +FUSE or bind-mount aliases are outside the guarantee. Run one TRAUMA process or +container, enforce a single replica, and do not overlap maintenance jobs. -SQLite owns runtime metadata. +## Data Ownership -The markdown store owns readable memory content and is the only content area -covered by built-in git backup. +Canonical ownership is defined by the +[data and storage matrix](../architecture/data-and-storage.md#ownership-matrix). +SQLite and the file-backed store each own specific domains. Built-in git backup +covers only explicitly enqueued artifacts under `storePath`. The SQLite database file should be protected by normal host backup strategy if needed. TRAUMA's built-in git backup does not commit the database file. ## Git Backup -Built-in backup commits markdown store changes from `storePath` using -`projectPath` as the git working directory. +Built-in backup commits explicitly enqueued store artifacts from `storePath` +using `projectPath` as the git working directory. + +TRAUMA disables ambient Git hooks for all built-in repository commands by +overriding `core.hooksPath` with the operating system's null device. Repository +or global `pre-commit`, `commit-msg`, `post-commit`, and `pre-push` hooks are not +part of the backup contract and do not run during normal backup, retry, startup +recovery, or failsafe migration. `projectPath` is expected to be the backup repository root. For the default local setup, use `projectPath: "./data"` and `storePath: "./data/storage"`. @@ -37,24 +64,31 @@ TRAUMA treats `./data` as separate from the application repository. On a clean first start with git backup enabled, TRAUMA creates `projectPath`, creates `storePath`, and initializes a git repository under `projectPath` when -one does not already exist. If memory rows or `CONTENT.md` files already exist, -TRAUMA does not auto-initialize a new repository. It creates a critical failsafe +one does not already exist. If memory rows or source `CONTENT.md` files already +exist, TRAUMA does not auto-initialize a new repository. It creates a critical +failsafe alert so the operator can choose `revert` or `migrate` explicitly. -Backup work is asynchronous. A failed backup does not invalidate memory -creation, flashback creation, or markdown writes. Failures are recorded and -surfaced through metadata. +Backup work is asynchronous. A failed backup does not invalidate memory or +Flashback creation, a completed translation, a saved Psychiatrist answer, or +other durable store writes. Failures are recorded and surfaced through metadata. +Flashback export reconciliation is separate from git: startup replays retained +export intents from SQLite even when git backup is disabled or its failsafe is +active. Translation completion also republishes the current language's +`FLASHBACKS.json` under the shared language lock and includes it in the +translation backup set, so an older output-hash projection cannot survive a +newly completed translation. When push is enabled, a missing configured remote name is treated as local-only backup and does not warn. A configured remote that exists but fails to push creates a critical alert while keeping the local commit. -If SQLite records successful backup content but the corresponding `CONTENT.md` +If SQLite records a successful memory backup but its source `CONTENT.md` path is missing, outside the configured paths, or untracked, TRAUMA reports a backup content-integrity alert. This is not a backup location change. The web UI and CLI offer deletion only for the `missing_file` case, and only after re-checking that the file is still absent. Untracked or out-of-scope content must be repaired -as backup repository/path state so existing markdown is not discarded. +as backup repository/path state so existing content is not discarded. ## Local Dev Server Contract @@ -66,34 +100,59 @@ the shell or the command that needs them. Standard commands: - `bun run dev` — start the dev server. -- `bun run dev:smoke` — boot the dev server, probe `/memories`, then exit. +- `bun run dev:smoke` — boot the dev server, probe the canonical + `GET /` redirect to `/memories`, then exit. Fails if the requested port is occupied, the server cannot bind, exits early, falls back to a different port, or does not respond within the timeout. - `bun run start` — serve the production build. -The smoke check sets `TRAUMA_BROWSE_FIXTURES=1` so it does not depend on a -real `trauma.config.json`. Run the smoke check before relying on the dev -server in CI or scripted environments. +Stop the running app before invoking a maintenance CLI that opens the configured +runtime, including `bun run db:migrate` and +`scripts/trauma-backup-failsafe.ts`. Dry-run and status modes also participate +in the lease because database initialization can apply pending migrations. +Start the app again after the command exits. + +The smoke launcher supplies an internal multi-signal fixture context so its +loopback `GET /` probe does not depend on a real `trauma.config.json`. The probe +does not follow the redirect and accepts only `302 Location: /memories`. The +fixture flag alone never bypasses the runtime lease. Other paths, query strings, +non-loopback requests, and all mutating methods still require the configured +runtime lease. Run the smoke check before relying on the dev server in CI or +scripted environments. `bun run test:e2e` boots its own dev server with explicit `HOST=127.0.0.1`, `PORT=4173`, and `TRAUMA_HMR_PORT=24681`, so it can run alongside -`bun run dev` without HMR port collisions and is unaffected by `.env`. +`bun run dev` without HMR port collisions and is unaffected by `.env`. Its +fixed `.trauma/e2e` config acquires ordinary database, project, and store +leases; E2E control is not a runtime-lease bypass. The Playwright harness +preseeds only the config and directories required for server readiness. Named +fixture actions reset data inside that same logical leased root. + +## Access Control -## Auth +TRAUMA has no user accounts, browser sessions, public signup, or multi-user +ownership. Deploy it behind local access controls, private networking, or a +reverse-proxy policy if it is exposed beyond the host. -There is no auth in the initial operating model. The app is single-user and -should be deployed behind local access controls, private networking, or a -reverse proxy policy if exposed. +Requests are accepted for loopback hostnames by default. A reverse proxy that +preserves another hostname must set `TRAUMA_ALLOWED_HOSTS` to the exact +comma-separated hostnames it serves; see the +[configuration reference](../references/configuration.md#trusted-request-hosts). +The host allowlist blocks DNS-rebinding-style boundary confusion but does not +replace proxy authentication or private-network policy. -Future public/team operation requires a separate auth design. +Codex app-server login used by Brilliant and Psychiatrist authenticates that +backend integration only; it does not protect the TRAUMA web application. +Public or team operation requires a separate auth design and threat model. -## Psychiatrist Runtime Isolation +## Codex Runtime Isolation -Do not enable production Psychiatrist turns against an app-server that can read -the host user's home directory, the TRAUMA application project, or the memory -store. Codex `readOnly` sandbox policy blocks writes but still allows host reads, -so an empty working directory and prompt policy are not an isolation boundary. +Do not enable production Brilliant translation or Psychiatrist turns against an +app-server that can read the host user's home directory, the TRAUMA application +project, or the memory store. Codex `readOnly` sandbox policy blocks writes but +still allows host reads, so an empty working directory and prompt policy are not +an isolation boundary. Run the app-server under an independently enforced process or container policy that exposes none of those host roots. Constrain any app-server egress to public @@ -104,11 +163,11 @@ and requests it only for a user-approved web-source turn. Only after that external policy is active, start TRAUMA with: ```bash -TRAUMA_PSYCHIATRIST_RUNTIME_ISOLATION=external_no_host_reads_public_http_https_only \ +TRAUMA_CODEX_RUNTIME_ISOLATION=external_no_host_reads_public_http_https_only \ TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:// bun run start ``` This assertion tells TRAUMA that the operator has supplied the boundary; it does not create or validate the boundary itself. If it is absent or has any -other value, production message and Regenerate requests fail closed with -`runtime_isolation_required` and do not start a turn. +other value, production translation, message, and Regenerate requests fail +closed with `runtime_isolation_required` and do not start Codex work. diff --git a/docs/quality/verification.md b/docs/quality/verification.md index d02df01f..f6411de9 100644 --- a/docs/quality/verification.md +++ b/docs/quality/verification.md @@ -1,109 +1,142 @@ # Verification Strategy -TRAUMA uses an E2E-first verification strategy. +TRAUMA uses layered, risk-based verification. Focused tests prove domain +invariants; Playwright proves cross-boundary user behavior. -## E2E Coverage - -Playwright should cover the main user workflows: - -- Add memory success path. -- Link-only fallback when extraction fails. -- Markdown file creation. -- `/memories` list rendering. -- `/memories/:id` reader rendering. -- `/flashbacks` flashback excerpt rendering. -- Category/tag filtering. -- `/memories?q=...` matching flashback text. -- Flashback selection and persistence. -- Flashback toggle removal when selecting already-flashbacked text. -- Backup status display. - -E2E tests should use controlled URLs or fixtures so extraction behavior is -deterministic. - -## Startup Smoke - -`bun run dev:smoke` boots the dev server with a deterministic host and port, -probes `/memories`, then shuts the server down. The smoke check fails fast -when the server cannot bind, crashes early, or does not respond within the -timeout. - -Use the smoke check before E2E runs and after toolchain or config changes -that could affect dev startup. - -## Release Automation - -`.github/workflows/release.yml` creates a GitHub Release from tag pushes after -running the same baseline verification and E2E smoke suite as CI. - -Release tags must be three-part numeric semantic versions with an optional -leading `v`: - -- `0.1.111` -- `v0.1.111` - -The workflow intentionally ignores non-matching tag names before checkout or -dependency installation. Release titles use the normalized version without the -optional leading `v`. - -## Focused Tests - -Unit or integration tests should cover: - -- Config validation. -- Drizzle repositories. -- Importer success and failure mapping. -- Markdown store writer. -- Flashback marker insertion. -- Flashback marker removal, shrink, and split behavior. -- Backup queue behavior. -- Backup environment failsafe drift, bootstrap, recovery, and push-failure - behavior. -- Reader sanitization and rendering. - -For backup failsafe changes, run the focused suite before broad verification: +## Standard Commands ```bash -mise exec -- bun run test tests/server/db/schema.test.ts -mise exec -- bun run test tests/server/backup/backup-environment.test.ts tests/server/backup/git-backup.test.ts tests/server/routes/api-backup-failsafe.test.ts tests/components/backup-failsafe.test.ts tests/server/backup/backup-failsafe-cli.test.ts +bun run typecheck +bun run test +bun run build +bun run dev:smoke +bun run test:e2e ``` -Use recovery commands in dry-run mode before applying filesystem changes: - -```bash -mise exec -- bun run scripts/trauma-backup-failsafe.ts status --config trauma.config.json -mise exec -- bun run scripts/trauma-backup-failsafe.ts revert --config trauma.config.json -mise exec -- bun run scripts/trauma-backup-failsafe.ts migrate --config trauma.config.json -mise exec -- bun run scripts/trauma-backup-failsafe.ts delete-missing-record --config trauma.config.json -``` +`bun run docs:check` validates documentation links, indexed reachability, +retired active terminology, and chronology boundaries. `bun run verify` runs +that documentation check, typecheck, unit/integration tests, and the production +build. `bun run dev:smoke` is the deterministic startup probe. +`bun run test:e2e` is the full Playwright suite, not the startup smoke check. + +Run `bun run audit` for dependency changes and security reviews. CI keeps the +audit separate from `bun run verify` so the normal typecheck, test, and build +loop remains network-independent. + +The audit ignores `GHSA-67mh-4wv8-2f99` and `GHSA-g7r4-m6w7-qqqr`. Their +affected `esbuild` versions are transitive build-tool dependencies of +`@esbuild-kit/core-utils` and `tsx`; those consumers use transforms, not +esbuild's vulnerable development-server API. A global `esbuild` override is +unsafe because Vinxi, Vite, Nitro, and the database tools require different +release lines. Recheck the reachability and remove each exception when its +upstream dependency advances. + +Run focused tests while iterating, then use the broad commands appropriate to +the changed risk. Tests and E2E must use fixtures and temporary database, +store, and git paths rather than external websites or real application data. + +## Current E2E Risk Coverage + +- `e2e/bootstrap.spec.ts`: basic application boot and shell availability. +- `e2e/add-memory.spec.ts`: public composer submission, successful extraction, + link-only fallback, reader projection, SQLite/store persistence, and local git + backup completion. +- `e2e/browse-shell.spec.ts`: canonical redirects, query/filter/read state, + pagination, memory actions, taxonomy, keyboard navigation, shell popovers, and + desktop/tablet/phone chrome. +- `e2e/cross-device-responsive.spec.ts`: responsive navigation, safe-area and + overflow behavior, theme controls, and primary-action reachability. +- `e2e/collection-pagination.spec.ts`: large SQLite/store archives, stable + Flashback/Moment current-page DOM, URL Reload/Back, and Reader All rail-local + pagination and scroll bounds. +- `e2e/reader.spec.ts`: source reader, translation controls, deletion, + Flashbacks, Moments, table-of-contents behavior, and Psychiatrist + streaming/resume/cancel/regenerate/permission flows. +- `e2e/security-boundaries.spec.ts`: hostile Host rejection, configured + loopback acceptance, and backup reconciliation GET non-exposure. +- `e2e/settings.spec.ts`: model-catalog recovery, saved-default preservation, + retry focus, and cancellation-safe Codex auth polling. + +The Add Memory fixture seam is owned by `src/server/importer/runtime.ts`. It is +enabled only by Playwright's three fixed E2E guards and synthesizes results for +two exact reserved `.invalid` URLs without network I/O. Custom hosts, paths, +query strings, userinfo, and content cannot enter the seam; every non-exact URL +uses the production SSRF validation and pinned-fetch importer. + +Mutable browser fixtures use the loopback-only `/api/e2e-control` route and the +fixed `.trauma/e2e` runtime. Playwright supplies a fresh control token and the +exact literal-loopback bind host, config, browse-fixture, import-fixture, and +control signals; the framework-reported socket peer must also be loopback. Keep +this boundary limited to named fixture actions; never add raw scripts, SQL, +paths, or Git arguments to its request schema. Runtime integration coverage +also proves that fixture reset preserves lease identity, exclusion, and +connection-lifetime release behavior. Before Playwright starts its server, the +bootstrap validates any existing config and requires its resolved database, +project, and store roots to match the fixed fixture layout. Invalid config, +layout symlinks, non-file SQLite family entries, and SQLite hardlink aliases +fail without rewriting the prior config; valid non-path backup settings remain +until the authenticated fixture reset canonicalizes them. + +## Focused Test Ownership + +Use focused tests for the smallest affected boundary: + +- `tests/server/config/**`, `db/**`, and `store/**`: config, migrations, + repositories, and filesystem ownership. +- `tests/server/importer/**`, `browser-import/**`, and `memories/**`: public + URL policy, extraction, add/delete compensation, and browse behavior. + Import admission coverage proves overflow is rejected before body/fetch work + and that timeout, validation, and failure paths release capacity. +- `tests/server/reader/**`, `flashbacks/**`, and route tests: sanitization, + reader hashes, variant-local ranges, Moments, and API validation. +- `tests/server/browse/**` and collection route tests: opaque cursor validation, + stable keyset ordering, bounded stale-row scans, distinct content/TOC reads, + legacy API compatibility, and paged envelopes. +- `tests/server/translation/**`: job state, chunking, Codex protocol, + serialized event admission, retry/job cumulative budgets, replay/SSE + backpressure, cancellation races, stitching, projections, and current-output + resolution. It also covers probe-client closure before queueing and absolute + per-segment, per-chunk, and complete-document UTF-8 output admission. Shared + Codex byte-limit changes also run the adjacent Psychiatrist event-persistence + and SSE suites. +- `tests/server/psychiatrist/**` plus `tests/skills/**`: runtime isolation, + prompt policy, file-backed thread state, events, citations, route semantics, + and shared active-plus-reserved turn capacity. +- `tests/server/backup/**` and backup route/component tests: identity stamps, + content integrity, recovery, queue behavior, and push failure. +- `tests/components/**` and `tests/scripts/frontend-refine-tokens.test.ts`: + UI contracts that are cheaper and more deterministic than browser tests. + +Tests for persisted data must use real Bun SQLite and temporary files where the +runtime behavior depends on them. Pure parsing/transformation tests should stay +deterministic and avoid server startup. + +## Startup And Release + +`bun run dev:smoke` boots a server on an explicit host/port and requires exact +`GET /` to return the canonical `302 Location: /memories` without following the +redirect. It fails on bind fallback, early exit, or timeout. Use it after +runtime, config, build-tool, or startup-script changes. + +CI and tagged releases run `bun run verify` and `bun run test:e2e`. Failed E2E +runs retain their first-attempt Playwright trace and upload the report and test +results for seven days. Release tags are three-part numeric semantic versions +with an optional leading `v`, for example `0.3.0` or `v0.3.0`, and their version +must exactly match the tagged commit's `package.json`. Release notes use the +tagged commit's matching `changelog/vX.Y.Z.md` when present; only a genuinely +absent local changelog falls back to generated notes. ## Completion Bar -A change that affects storage, import, flashbacks, backup, routing, or reader -rendering should include either E2E coverage or a clear reason why focused tests -are sufficient. - -Do not claim an implementation is complete without running the relevant -verification commands and recording their outcomes. - -## Review Follow-Up - -When a valid review finding exposes a reproducible bug, invariant gap, parser -edge case, or implementation anti-pattern, the fix should use the strongest -durable guardrail that fits: - -- One-off bug: add a regression test. -- Repeated style issue: add or update linting, formatting, typecheck, or a - static check. -- Architecture invariant: document the invariant and add a test/static check - where possible. -- Workflow failure: update workflow automation or the workflow checklist. -- Reviewer false positive: update reviewer config, ignore rules, or reply with - evidence. - -Do not treat a prose-only documentation update as sufficient when the finding -is machine-checkable. - -Review follow-up is not complete until thread-aware review state has been -checked after the fix is pushed and the corresponding review thread has a -concrete reply. +- Behavior changes include regression coverage at the owning boundary. +- Storage, security, import, translation, Psychiatrist, backup, routing, or + reader changes run focused tests plus the broad checks justified by risk. +- Browser-visible layout or interaction changes run Playwright. +- Documentation-only changes run `bun run docs:check`; a dedicated lightweight + CI workflow runs for documentation paths. +- Verification commands and exact outcomes are recorded at handoff. +- Tests are never weakened merely to make a change pass. + +For accepted review findings, follow the canonical +[review feedback policy](../references/coding-standards/review-feedback-policy.md) +instead of duplicating review procedure here. diff --git a/docs/references/coding-standards/INDEX.md b/docs/references/coding-standards/INDEX.md index eb5c7e74..6eee6722 100644 --- a/docs/references/coding-standards/INDEX.md +++ b/docs/references/coding-standards/INDEX.md @@ -1,12 +1,8 @@ # Coding Standards Index -This directory defines Trauma's implementation rules. Read the files that match -the code you are changing. - -The project uses ECC common rules and TypeScript extensions as strict defaults. -When a generic ECC rule conflicts with Trauma's stack, these local rules win. -The most important stack-specific override is that Trauma is SolidStart/Solid, -not React. +This directory defines TRAUMA's self-contained implementation rules. Read only +the files that match the code being changed. Generic external guidance is +subordinate to these repository-local rules and the current architecture. ## Rule Levels @@ -46,10 +42,3 @@ The rules are grounded in the official docs for the active stack: behavior. - Drizzle codebase-first migrations, relations, transactions, and `sql` template usage. - -## Review Feedback Loop - -Valid review feedback should first become a test, static check, tool -configuration, or workflow automation when possible. Use -[Review feedback policy](review-feedback-policy.md) to decide when prose docs -are justified. diff --git a/docs/references/coding-standards/drizzle-sqlite.md b/docs/references/coding-standards/drizzle-sqlite.md index 4f535263..14cd01bd 100644 --- a/docs/references/coding-standards/drizzle-sqlite.md +++ b/docs/references/coding-standards/drizzle-sqlite.md @@ -20,24 +20,27 @@ guards for existing local SQLite databases. - MUST define both database-level constraints and Drizzle relations when a table relationship is part of the domain model. -- MUST wrap multi-table or multi-step writes in transactions. Memory creation, - tag/category association, flashback persistence, and backup status updates - must not partially commit. +- MUST wrap related SQLite multi-table or multi-step writes in transactions. + Tag/category association, Flashback range splitting, and other relational + mutations must not partially commit. Cross-store workflows such as memory + creation use explicit compensation because filesystem writes cannot join a + SQLite transaction. - MUST keep Bun SQLite Drizzle transaction callbacks synchronous. Do not pass an `async` callback to `db.transaction(...)`; awaited work can escape the rollback boundary. Add a focused rollback regression test for multi-step writes that must be atomic. -- MUST make post-insert boundary failures recoverable for create workflows. - After a memory row and `CONTENT.md` are durable, backup enqueue/status failures - must return the created memory with the best persisted status or compensate - explicitly instead of surfacing an ambiguous failed create. +- MUST make post-persistence boundary failures recoverable for create workflows. + Memory creation writes `CONTENT.md` before inserting the SQLite row and + removes that new file if the insert fails. After both are durable, backup + enqueue/status failures must return the created memory with the best persisted + status instead of surfacing an ambiguous failed create. - MUST apply bundled migrations before exposing repositories or returning an initialized database handle to application code. - MUST use current Bun SQLite APIs. Use `Database.run`, prepared statements, or Drizzle queries instead of deprecated `Database.exec`. - MUST close the SQLite handle if initialization fails after the handle is opened. -- MUST keep SQLite database files outside the markdown backup store. +- MUST keep SQLite database files outside the configured artifact store. - MUST resolve and validate configured paths before filesystem writes. - MUST prevent path traversal when reading or writing markdown content. - MUST resolve bundled migration paths from module/package location or an @@ -48,10 +51,13 @@ `__drizzle_migrations` table must use a rowid-compatible integer primary key, not PostgreSQL-style `SERIAL`. - MUST validate bundled runtime migration state before exposing the database. - Every applied migration row must correspond to a bundled migration, and every - matching row must have the expected hash. Unknown, newer, or hash-mismatched - rows must fail loudly instead of letting older runtime code operate on a - different schema. + Runtime manifests must have unique, strictly increasing `folderMillis` + values before SQLite is mutated. Applied rows, ordered by `created_at`, must + be a unique, contiguous prefix of the bundled migrations and every row must + have the expected hash. Unknown, newer, duplicate, hash-mismatched, or + gap-bearing histories must fail loudly before any migration body runs instead + of letting runtime code operate on or replay migrations against a different + schema. - MUST treat migration execution and migration recording as one atomic unit. If a migration body succeeds but writing `__drizzle_migrations` fails, the schema changes must roll back with the failed record write. @@ -90,5 +96,6 @@ queries. - SHOULD expose repository methods that match domain use cases rather than generic table access. -- AVOID adding external services, queues, managed databases, or auth/user - ownership unless a later design explicitly adds them. +- AVOID adding external services, queues, managed databases, or TRAUMA + user/session/multi-user ownership unless a current design explicitly adds + them. diff --git a/docs/references/coding-standards/principles.md b/docs/references/coding-standards/principles.md index 1a654b11..0c4d6761 100644 --- a/docs/references/coding-standards/principles.md +++ b/docs/references/coding-standards/principles.md @@ -20,12 +20,10 @@ Trauma favors clean, boring implementation over cleverness. ## State And Data Ownership -- MUST keep canonical ownership clear: - - SQLite owns metadata. - - Markdown files own extracted readable content. - - The UI owns transient presentation state. -- MUST NOT duplicate canonical state across SQLite and markdown unless a - documented sync rule says which side wins. +- MUST follow the canonical + [data ownership matrix](../../architecture/data-and-storage.md#ownership-matrix). +- MUST NOT duplicate canonical state across SQLite and store artifacts unless a + documented projection/sync rule names the runtime authority. - MUST update data immutably in UI and domain transformations. - MUST model status fields as explicit unions and enforce persisted constraints where possible. diff --git a/docs/references/coding-standards/security-boundaries.md b/docs/references/coding-standards/security-boundaries.md index 78731caa..7fad5b62 100644 --- a/docs/references/coding-standards/security-boundaries.md +++ b/docs/references/coding-standards/security-boundaries.md @@ -16,8 +16,8 @@ payloads. For example, frontmatter errors should name `extraction_status`, not the internal `extractionStatus` property. - MUST validate path containment against the actual ownership boundary. A - database path restriction should target the markdown backup store boundary, - not a broader project directory unless that is the designed invariant. + database path restriction should target the configured store boundary, not a + broader project directory unless that is the designed invariant. - SHOULD use schema-based validation when the shape is non-trivial. ## Markdown, HTML, And Reader Safety @@ -28,6 +28,9 @@ custom markdown conversion unless a separate design explains why the extractor boundary is insufficient. - MUST sanitize rendered markdown or HTML before it reaches the browser. +- MUST bound syntax-highlighting work for untrusted code fences. Oversized or + unknown-language blocks render as escaped plain code instead of entering a + high-cost grammar or automatic language scan. - MUST enforce auto-loaded media safety at render time. Images, responsive sources, and iframes must not load local/private/IP/userinfo/non-HTTPS URLs merely because they appear in extracted markdown. @@ -44,8 +47,14 @@ - MUST NOT hardcode secrets, tokens, credentials, or private local paths. - MUST keep `.env*` secrets untracked. +- MUST disable repository and global Git hooks for every built-in git command, + including readiness checks, normal backup, retry, startup recovery, and + failsafe repair. Apply a command-scoped null `core.hooksPath` through the + shared Git execution boundary; trusting `projectPath` never grants ambient + hook execution. +- MUST NOT add a user-configurable shell hook surface to built-in backup. - MUST validate URL protocols before importer fetches. `http:` and `https:` are - the only expected initial protocols. + the only accepted importer protocols. - MUST fetch only public HTTP(S) hosts from importer code. Reject localhost, `*.localhost`, local/private/link-local/non-global IP targets, URL userinfo, unsafe redirects, and DNS answers that resolve outside the public-host policy. @@ -60,6 +69,10 @@ - MUST include article extraction work in the same import timeout budget. Default extractor parsing and conversion must run behind an interruptible worker or process boundary instead of blocking the request event loop. +- MUST reject excess URL and browser-assisted imports through fixed, + process-wide, non-queuing admission before fetch, extraction, or browser body + buffering. Return a stable `429` plus `Retry-After` and release admission on + every terminal path. - MUST request identity encoding or explicitly decode compressed bodies when using low-level HTTP clients that do not automatically decompress responses. - MUST decode HTML entities before URL resolution where TRAUMA itself accepts or @@ -71,8 +84,93 @@ - MUST prevent XSS in markdown and extracted content rendering. - MUST avoid leaking stack traces, filesystem paths, or raw dependency errors to browser-visible responses. -- MUST keep auth assumptions out of the initial implementation. If auth is - introduced later, it needs a separate design and threat model. +- MUST NOT mistake Codex app-server login for TRAUMA access control. User + accounts, browser sessions, public signup, or multi-user ownership require a + separate design and threat model. + +## Codex App-Server Boundaries + +- MUST treat memory content, translated content, source pages, and prior prompts + as untrusted prompt input. Follow the repo-local + [Psychiatrist](../../../.agents/skills/psychiatrist/SKILL.md) and + [translation](../../../.agents/skills/reader-translate/SKILL.md) policies. +- MUST keep browser clients behind TRAUMA routes. They never connect directly + to Codex app-server or receive raw protocol events. +- MUST NOT enable production Brilliant translation or Psychiatrist turns until the independently + enforced boundary in + [local/self-hosting](../../operations/local-self-hosting.md#codex-runtime-isolation) + makes the home directory, application project, and memory store unreadable. + Codex `readOnly` sandbox policy is not that boundary. +- MUST default Psychiatrist network access off. Public web access requires + explicit approval for the current turn and externally constrained egress. +- MUST validate structured translation output and fail closed on source-hash, + output-shape, protocol, or cancellation conflicts. +- MUST close the Brilliant probe/model-selection client before scheduling a + durable job. A queued job must not retain a connected Codex client; the + sequential runner creates and owns one only when execution begins. +- MUST hard-bound Brilliant source chunks to 2,500 rough tokens and complete + outbound prompts to 64 KiB by serialized UTF-8 bytes. Safely splittable + paragraph and list content must retain exact Markdown byte order and stable + source offsets; structurally indivisible overflow must fail before durable + scheduling. Recheck every initial or retry prompt before the app-server client + call, and never automatically retry a prompt-limit failure. +- MUST reject new or resumed Brilliant work above 20 MiB of total source, 16,384 + translation segments, or 4,096 chunks before any Codex client call. New work + must also fail before client creation or durable job insertion. Aggregate + overflow is a non-retryable `validation_failed` result; tests may inject + smaller limits but production limits are fixed code constants. Source loading + must not trust `stat`: open the file, read positionally through short reads, + retain demand-sized fixed-capacity chunks totaling at most the source-byte + limit plus one probe byte, and always close the handle. Reject the probe-byte + overflow before streaming UTF-8 decoding, Markdown or frontmatter parsing, + document-type inference, or incremental raw-byte hashing. Resume and final + commit reloads must receive the same workload source-byte limit. +- MUST reject translated output above 1 MiB per segment, 4 MiB per chunk, or + 56 MiB for the complete translated `CONTENT.md` by serialized UTF-8 bytes. + Count preserved frontmatter and resumed chunks, reject aggregate overflow + before chunk persistence, and recheck before final stitching or publication. + Absolute output overflow is terminal and is not automatically retried. +- MUST admit Brilliant translation Codex events against fixed server-side + serialized UTF-8 budgets: 64 KiB per event, 4,096 events or 4 MiB per chunk + attempt, and 262,144 events or 32 MiB for the whole job. Job admission is + cumulative across every chunk and retry; only the chunk-attempt budget resets. + These limits are test-injectable code constants, not runtime configuration. +- MUST propagate Brilliant translation admission failure through the Codex event + callback. The callback stops accepting events, the active turn is interrupted + best-effort, and the job fails without retry through the existing safe unknown + public error contract. Cancellation remains authoritative when it races the + limit failure. +- MUST bound Brilliant translation in-process replay to 500 events and 4 MiB, + and each live SSE subscriber to 128 pending events and 3 MiB. Snapshot and + replay delivery are pull-driven; a slow subscriber overflow disconnects only + that subscriber and never fails the translation job. +- MUST treat Codex protocol event size and rate as untrusted input. Psychiatrist + enforces fixed server-side byte and count budgets at callback admission, the + pending persistence queue, the complete turn, the durable replay stream, and + each SSE subscriber; these safety limits are not runtime configuration. +- MUST cap Psychiatrist active and reserved turns together across threads. + Capacity overflow is a retryable `429` before client creation, distinct from + the existing same-thread `409`, and all start, cancel, failure, and detached + terminal paths must release their admission. +- MUST propagate Psychiatrist persistence backpressure to the Codex conversation + callback. Once admission fails, the turn stops accepting events and fails with + the safe `event_limit_exceeded` class instead of accumulating more work. +- MUST bound replay reads and slow SSE consumers. Oversized legacy replay files + fail before unbounded parsing, inactive replay is encoded one event at a time, + and a live subscriber that exceeds its pending budget is unsubscribed. +- MUST expose only safe process/status events and final answer text. Never send + hidden reasoning, raw backend payloads, tokens, endpoints, credential paths, + or local absolute paths to the browser. +- MUST treat Psychiatrist citation URLs from request responses, persisted + threads, and SSE terminal events as untrusted browser output. Citation shape + validation remains forward-compatible, but only credential-free public HTTP + and HTTPS URLs may become anchors. Active schemes, malformed URLs, localhost + names, and private, loopback, link-local, or other non-unicast IP literals + must render as inert escaped title text rather than rejecting the whole answer. +- MUST keep the Psychiatrist browser process projection bounded independently of + the durable 4,096-event turn limit. Normalize safe status text, coalesce + adjacent duplicates, and render no more than eight status rows per pair while + retaining the first context status and the latest seven statuses. ## Browser-Assisted Import diff --git a/docs/references/coding-standards/solidstart-ui.md b/docs/references/coding-standards/solidstart-ui.md index 3d2936b1..37909259 100644 --- a/docs/references/coding-standards/solidstart-ui.md +++ b/docs/references/coding-standards/solidstart-ui.md @@ -28,7 +28,8 @@ - SHOULD use route data, server functions, or `createResource` for async data loading depending on whether the data is server-owned or client-only. - SHOULD keep components mostly presentational. Move persistence, extraction, - markdown, and backup behavior into server/domain modules. + reader transforms, translation/Psychiatrist protocols, and backup behavior + into server/domain modules. - AVOID component-level duplication of server state. Derive view state from route data, params, query state, or local UI signals. diff --git a/docs/references/coding-standards/testing-verification.md b/docs/references/coding-standards/testing-verification.md index fae19e88..4a1ba4a7 100644 --- a/docs/references/coding-standards/testing-verification.md +++ b/docs/references/coding-standards/testing-verification.md @@ -3,20 +3,16 @@ ## Tests And Verification - MUST include tests with behavior changes. -- MUST cover storage, importer, markdown, flashback, backup, routing, and reader - changes with either focused tests or Playwright coverage. +- MUST cover storage, importer, reader, Flashback, Moment, translation, + Psychiatrist, backup, routing, and shell changes with focused tests or + Playwright coverage appropriate to the boundary. - MUST run `bun run typecheck` for TypeScript-facing changes. - MUST run relevant verification before handoff and record exact commands and outcomes. - MUST NOT weaken or delete tests to make a change pass unless the test is wrong and the PR explains the correction. -- MUST add or identify regression coverage, static checks, or tool - configuration for each accepted review finding that exposed a reproducible - bug, invariant gap, parsing edge case, or bad practice. -- MUST NOT use a prose-only docs update as the durable fix for a - machine-checkable review finding. -- MUST use thread-aware review inspection for PR follow-up and re-sweep after - pushing fixes. +- MUST follow the [review feedback policy](review-feedback-policy.md) for + regression coverage, durable guardrails, replies, and thread-aware re-sweeps. - SHOULD write deterministic tests with local fixtures, especially for extraction and markdown rendering. - SHOULD prefer focused tests for pure transforms and repositories, then E2E for diff --git a/docs/references/coding-standards/typescript.md b/docs/references/coding-standards/typescript.md index 77fe9137..9b526a90 100644 --- a/docs/references/coding-standards/typescript.md +++ b/docs/references/coding-standards/typescript.md @@ -4,8 +4,10 @@ - MUST keep TypeScript `strict` clean. - MUST keep the strict compiler options used by the project, including - `noFallthroughCasesInSwitch`, `noImplicitOverride`, and - `noUncheckedIndexedAccess`. + `allowUnreachableCode: false`, `allowUnusedLabels: false`, + `noFallthroughCasesInSwitch`, `noImplicitOverride`, + `noImplicitReturns`, `noUncheckedIndexedAccess`, `noUnusedLocals`, and + `noUnusedParameters`. - MUST treat indexed access and map/object lookups as possibly missing. Narrow or guard the value before use instead of adding non-null assertions. - MUST NOT use `any`, `as any`, or implicit `any`. diff --git a/docs/references/configuration.md b/docs/references/configuration.md index a249db8d..af43619d 100644 --- a/docs/references/configuration.md +++ b/docs/references/configuration.md @@ -6,15 +6,15 @@ TRAUMA uses static JSON configuration at the project root: trauma.config.json ``` -The initial design does not allow executable config files or arbitrary -lifecycle hooks. +The current configuration contract does not allow executable config files or +arbitrary lifecycle hooks. Runtime UI preferences are stored in SQLite, not in `trauma.config.json`. Codex translation defaults such as the selected model and reasoning effort are managed through `app_settings` so the reader translation popover can reopen with the user's last saved selections. -## Initial Shape +## Configuration Shape ```json { @@ -40,14 +40,55 @@ as `/Users/name/trauma-data` or a config-relative path such as `./data`. ## Path Rules -- `storePath` contains memory markdown files. +- `storePath` contains source/translated reader files, metadata exports, and + memory-local Psychiatrist thread artifacts. - `projectPath` is the git working directory used by built-in backup. - `storePath` must be inside `projectPath`. - `databasePath` points to the SQLite runtime database. - `databasePath` must be outside `storePath`, which keeps the SQLite database - outside TRAUMA's markdown backup scope. + outside TRAUMA's built-in store-backup scope. -Invalid path relationships are startup errors. +Path relationships are checked against the effective locations of all existing +path components after resolving symbolic links. Missing trailing directories or +files remain valid so a clean first start can create them. Invalid or +unresolvable effective path relationships are startup errors. + +## Database Migrations + +Application startup applies committed migrations through TRAUMA's checked +runtime runner. To apply the same migrations without starting the server, use: + +```bash +bun run db:migrate +``` + +The command loads `trauma.config.json` from the current directory, or the path +set by `TRAUMA_CONFIG_PATH`, and fails if that config is missing or invalid. An +explicit config path can be supplied without changing the process environment: + +```bash +bun run db:migrate --config /path/to/trauma.config.json +``` + +`db:migrate` deliberately uses the same hash, compatibility, foreign-key, and +atomicity checks as application startup. `TRAUMA_DATABASE_PATH` remains a +Drizzle tooling override and does not bypass the runtime config contract. +The command acquires database-family ownership before creating or opening the +SQLite file and holds it until the connection closes. Stop the server and any +other maintenance process first; an active owner makes the command exit without +creating the database. + +`databasePath`, `projectPath`, and `storePath` are restart-scoped. Manual edits +to those fields are rejected before a running server opens the new storage; +restart TRAUMA to adopt them. Other JSON settings follow their owning runtime +loader and are not covered by this storage-root rule. + +A root-changing backup recovery revalidates its alert, reserves both current and +previous roots, and returns its own request and database borrows. It rewrites +config only when no other admitted request or background task remains. A busy +runtime returns `409` with config unchanged; retry after current work finishes. +After storage suspension, restart the TRAUMA process even if the config write +fails. ## Backup Environment Failsafe @@ -66,8 +107,35 @@ mise exec -- bun run scripts/trauma-backup-failsafe.ts revert --config trauma.co mise exec -- bun run scripts/trauma-backup-failsafe.ts migrate --config trauma.config.json ``` -Both commands are dry-run by default. Add `--apply` only after checking the -summary. +Stop the TRAUMA app before running these commands; maintenance CLIs acquire the +same database, store, and project root-set leases as the server and fail closed +when a configured or previous failsafe root is active. + +Both commands are dry-run by default and print the opaque alert `generation` +approved by that summary. Apply only that generation after reviewing it: + +```bash +mise exec -- bun run scripts/trauma-backup-failsafe.ts migrate --config trauma.config.json --apply --generation +``` + +Restart the app when the command exits. In the web UI, a successful config +revert leaves a terminal process-restart notice instead of reloading the same +process. An applied config revert writes and +syncs a same-directory temporary file before atomic replacement. A write, sync, +or rename failure leaves the previous config intact and removes the temporary +file. + +Applied recovery is generation-scoped: if another confirmation or environment +check already consumed or replaced the displayed alert, the stale action fails +and must be reviewed again. Backup content migration also uses synced +same-directory temporary files and no-overwrite atomic publication, so a process +interruption cannot leave a partial final file that blocks a safe retry. Previous +and current migration trees must be disjoint; symlinked or non-directory +destination components and source `.git` internals are never traversed or +published. A failed push leaves the previous stamp unchanged. After remote or +branch repair, retry recovery pushes first, revalidates the repository root, +branch, remote fingerprint, and `HEAD`, then records that identity and clears +the approved alert in one transaction. If the stamp and configured paths still match but SQLite says a memory was successfully backed up while its `CONTENT.md` is missing, outside the configured @@ -75,6 +143,12 @@ backup paths, or not tracked by the backup repository, TRAUMA creates a separate content-integrity alert. This is not a backup location change, so path migration actions must not be offered for that alert. +A legacy `redacted:migration-0016` remote value is an unknown identity, not a +wildcard match. Existing data therefore remains fail-closed until the operator +reviews the current repository and explicitly applies the `migrate` recovery; +that action records the current remote fingerprint without persisting its URL or +credentials. + When the content-integrity reason is `missing_file`, the UI and CLI may offer a delete recovery that removes the orphan SQLite `memories` row. This recovery is not available for untracked or out-of-scope content because those cases may @@ -82,7 +156,8 @@ still have recoverable markdown content. ## Backup Rules -`backup.git.enabled` controls built-in markdown backup. +`backup.git.enabled` controls built-in backup for explicitly enqueued store +artifacts. When enabled, TRAUMA stages only files under `storePath`, commits with `commitMessageTemplate`, and pushes only when `backup.git.push` is true. @@ -104,7 +179,24 @@ When `backup.git.push` is true, a missing remote name skips push without a warning and keeps the local backup commit. If the remote exists but push fails, TRAUMA records a critical push-failure alert. -No generic command hooks are part of the initial design. +No generic command hooks are part of the current contract. + +## Trusted Request Hosts + +TRAUMA accepts `localhost`, `127.0.0.1`, and `::1` request hosts by default. +This prevents a public hostname that resolves to the loopback interface from +crossing the local-only boundary. + +When a reverse proxy preserves a different `Host` value, add its exact hostname +through a comma-separated server environment variable. Entries are hostnames, +not URLs, ports, or wildcard patterns: + +```bash +TRAUMA_ALLOWED_HOSTS=archive.example,reader.example bun run start +``` + +This allowlist is not authentication. Non-local deployments still require the +access controls described in the operations guide. ## Browser-Assisted Import Environment @@ -123,19 +215,28 @@ TRAUMA_BROWSER_IMPORT_TOKEN= - `TRAUMA_BROWSER_IMPORT_ENABLED` must be `true` before the API accepts imports. - `TRAUMA_BROWSER_IMPORT_TOKEN` is a local bearer token shared with the browser - extension settings. Do not commit it. + extension settings. When import is enabled it must contain at least 32 + URL-safe characters. Generate a random value with `openssl rand -hex 32` and + do not commit it. Advanced operator and CI overrides, such as runtime config path, Drizzle CLI database path, dev smoke tuning, fixture mode, or browser import origin/size limits, should be set explicitly in the shell or CI job that needs them. They are intentionally not part of `.env.example`. +Import concurrency is not operator configuration. TRAUMA uses fixed code-level +non-queuing limits of four public URL imports and two browser captures; excess +requests receive `429` with `Retry-After`. + ## Codex App-Server Environment Brilliant translation and Psychiatrist are optional backend-only consumers of a separately running Codex app-server. TRAUMA does not start or supervise that process. +Translation output byte admission and the four-turn Psychiatrist capacity are +fixed server safety constants, not environment or JSON configuration. + Use the Codex app-server Unix listener when enabling these features: ```bash @@ -150,26 +251,27 @@ different socket path. Loopback WebSocket endpoints are not supported. `http://` `https://`, `ws://`, and `stdio://` are rejected because they are not Brilliant wire-protocol transports. -Psychiatrist production turns require a separately enforced runtime boundary. -Codex `sandboxPolicy: readOnly` prevents writes, but it does not remove shell or -file-read capabilities and is not sufficient isolation for untrusted memory and -transcript content. The external boundary must make the user's home directory, -the application project, and the memory store unreadable to the app-server -runtime. If egress is available, it must be constrained to public HTTP(S) -destinations and must still be enabled by TRAUMA only after the user approves -web sources for that turn. +Brilliant translation and Psychiatrist production turns require a separately +enforced runtime boundary. Codex `sandboxPolicy: readOnly` prevents writes, but +it does not remove shell or file-read capabilities and is not sufficient +isolation for untrusted imported content or transcripts. The external boundary +must make the user's home directory, application project, and memory store +unreadable to the app-server runtime. If egress is available, constrain it to +public HTTP(S); Psychiatrist still enables it only for a user-approved web-source +turn. After independently enforcing that boundary, the operator must make this exact assertion in the TRAUMA server environment: ```bash -TRAUMA_PSYCHIATRIST_RUNTIME_ISOLATION=external_no_host_reads_public_http_https_only \ +TRAUMA_CODEX_RUNTIME_ISOLATION=external_no_host_reads_public_http_https_only \ TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:// bun run dev ``` -Without the exact assertion, message and Regenerate routes fail with -`runtime_isolation_required` before reserving a turn or writing thread -artifacts. The variable is only an operator-controlled fail-closed gate. It does -not create, inspect, or verify a sandbox, and it must not be set until the -app-server process or container is actually isolated. Translation does not use -this Psychiatrist-specific gate. +Without the exact assertion, translation and Psychiatrist mutation routes fail +with `runtime_isolation_required` before reserving Codex work. The variable is +only an operator-controlled fail-closed gate. It does not create, inspect, or +verify a sandbox, and it must not be set until the app-server process or +container is actually isolated. The legacy +`TRAUMA_PSYCHIATRIST_RUNTIME_ISOLATION` name remains accepted for compatibility; +new deployments should use the shared name. diff --git a/docs/references/design-system/DESIGN.md b/docs/references/design-system/DESIGN.md index 04b3cbdf..87716a51 100644 --- a/docs/references/design-system/DESIGN.md +++ b/docs/references/design-system/DESIGN.md @@ -35,8 +35,8 @@ TRAUMA adapts that model for local archive work. 6. Keep typography stable. No viewport-scaled font sizes and no negative letter spacing. 7. Prefer semantic Tailwind tokens over raw colours in components. -8. Preserve existing data, import, flashback, and backup behaviour when - refining UI. +8. Preserve existing data, import, Flashback, Moment, translation, + Psychiatrist, and backup behaviour when refining UI. ## Current Design Scope @@ -49,13 +49,16 @@ The design system currently covers: - Tablet icon rail and phone bottom tabs. - Memory browse list and grid. - Flashback browse page. +- Moment browse page. - Reader mode. - Add-memory composer. +- Translation and Psychiatrist reader surfaces. +- Workspace settings. - Backup failsafe banner surface. - TRAUMA brand mark and custom icons. -It does not define authentication screens, team/user management, billing, -settings routes, or public marketing pages. +It does not define TRAUMA user/session authentication, team management, billing, +or public marketing pages. ## Source Of Truth @@ -70,6 +73,8 @@ Route surfaces own route-specific composition: - `src/components/memories/MemoryBrowse.tsx` - `src/routes/flashbacks/index.tsx` +- `src/components/moments/MomentBrowse.tsx` +- `src/components/settings/SettingsPage.tsx` - `src/components/reader/MemoryReader.tsx` - `src/components/reader/reader-styles.ts` diff --git a/docs/references/design-system/INDEX.md b/docs/references/design-system/INDEX.md index 4886abeb..e03d1123 100644 --- a/docs/references/design-system/INDEX.md +++ b/docs/references/design-system/INDEX.md @@ -35,6 +35,8 @@ It is not a separate component package. - `src/components/icons/TraumaIcons.tsx` - `src/components/memories/MemoryBrowse.tsx` - `src/routes/flashbacks/index.tsx` +- `src/components/moments/MomentBrowse.tsx` +- `src/components/settings/SettingsPage.tsx` - `src/components/reader/MemoryReader.tsx` - `src/components/reader/reader-styles.ts` diff --git a/docs/references/design-system/assets-and-icons.md b/docs/references/design-system/assets-and-icons.md index a46687b4..a5404b3f 100644 --- a/docs/references/design-system/assets-and-icons.md +++ b/docs/references/design-system/assets-and-icons.md @@ -51,9 +51,11 @@ Rules: - Memories. - Flashbacks, represented by a lightning mark. +- Moments. - Categories. - Tags. - Backup. +- Theme. - Settings. Active route links use the filled variant. Inactive links and future controls @@ -73,7 +75,6 @@ Current utility icons include: - Check. - Sun. - Moon. -- Paint tool for the Theme tab. - Page. - Paper. - Hermès shopping bag silhouette for night paper surface mode. diff --git a/docs/references/design-system/components-and-surfaces.md b/docs/references/design-system/components-and-surfaces.md index 1e6d8e6c..9172ec9e 100644 --- a/docs/references/design-system/components-and-surfaces.md +++ b/docs/references/design-system/components-and-surfaces.md @@ -108,6 +108,10 @@ drawers: constraints. - Popovers close on Escape, outside pointer interaction, or successful completion of the contained workflow. +- Opening moves focus into the panel. Escape and successful completion restore + the opener; outside-pointer dismissal preserves focus on the outside target. +- Menu popovers expose a roving `menuitem` keyboard model for Arrow Up/Down, + Home, and End, and close when Tab moves focus outside. - Outside pointer dismissal is a cancel action for confirmation popovers. It closes the panel and resets draft form edits without submitting the action. - Add memory keeps the shell-level command globally reachable, but the composer @@ -190,6 +194,13 @@ Do not hand-roll separate Flashback text treatments for each route. `/flashbacks uses dense route rows: shared inline Flashback text first, then the source memory title as small supplemental metadata at the bottom. +## Collection Page Controls + +Flashbacks and Moments expose compact First/Next link navigation below the +current route rows. The controls operate on URL cursor state and replace the +visible rows. Loading uses `aria-busy`; failed page loads use an assertive alert; +an empty continuation page remains distinct from an empty archive. + ## Right Rail Sections Right rail sections are independent islands. @@ -200,7 +211,7 @@ Use them for lightweight filter and shortcut groups: - Tags. - Flashback shortcuts. - Reader table of contents, only as route-specific right rail content on a - concrete memory route. + ready source or translated memory route. They should not become dense forms, search panels, settings pages, or broad route content containers. Contextual route content must stay small enough to @@ -223,6 +234,11 @@ treatment is per Flashback item: selected text remains normal and readable, while the stored prefix/suffix context uses the shared Flashback context blur/mask classes. +Reader All Flashbacks is a bounded page, not an unbounded shortcut group. Its +list body owns `max-height`, vertical overflow, and overscroll containment, and +its First/Previous/Next controls use rail-local cursor history. Switching to +Current and back to All preserves that local page. + ## Add Memory Composer The global composer uses the existing `AddMemoryForm` and existing @@ -253,6 +269,9 @@ Rules: - Keep it visible above route content. - Use state/severity tokens rather than route-specific styling. - Keep recovery actions concrete and aligned with server behaviour. +- When a config revert suspends storage, disable every recovery action and show + a persistent instruction to restart the TRAUMA process/server. Do not reload + the page into the suspended process. ## Empty, Loading, And Error States diff --git a/docs/references/design-system/interaction-and-accessibility.md b/docs/references/design-system/interaction-and-accessibility.md index d2ca8dcd..89633cdf 100644 --- a/docs/references/design-system/interaction-and-accessibility.md +++ b/docs/references/design-system/interaction-and-accessibility.md @@ -2,18 +2,12 @@ ## Route Behaviour -Canonical UI routes: - -- `/memories` -- `/flashbacks` -- `/moments` -- `/memories/:id` -- `/settings` - -The root route redirects to `/memories`. +The canonical route inventory and compatibility redirects are owned by +[UI and routing](../../architecture/ui-and-routing.md#canonical-routes). This +document adds interaction and accessibility requirements only. Do not add live navigation links to missing `/category`, `/tags`, or `/backup` -routes. Future items may be rendered as disabled controls only. +routes. Those items may be rendered as disabled controls only. ## Query State @@ -30,6 +24,12 @@ Supported browse concerns: Filter buttons should toggle their own query key without clearing unrelated query state. +Flashback and Moment collection cursors are opaque URL state. First removes the +cursor and Next writes the server continuation. Pending navigation must remove +the previous page from the rendered row set, expose `aria-busy`, and announce a +load failure with `role="alert"`. Native links preserve Reload and browser +Back behavior. + The `q` search value is preserved as raw input in the URL and may contain fielded filters: @@ -76,6 +76,12 @@ Theme controls: - Close the popover with Escape and outside pointer interaction. - Use the shared translucent `Popup` surface for anchored popovers; domain components must not add their own outside-pointer listeners for those panels. +- Shared outside-pointer dismissal may suppress the click produced by that same + primary pointer sequence so a closing popover does not activate the surface + beneath it. Suppression is bound to the initiating `pointerId` and clears on + its click, matching `pointercancel`, a pointerup that produces no click, the + next pointer/keyboard interruption, window blur, or a short deadline. A later + unrelated click must never be swallowed. ## Focus And Keyboard @@ -89,7 +95,37 @@ Rules: - Reader flashback keyboard toggling must remain explicit and must not trigger during ordinary text navigation. - Confirmation popovers must treat Cancel, Escape, and outside pointer - dismissal as the same cancel/reset path. + dismissal as the same cancel/reset path. Dismissal revokes pending focus + ownership even if focus later falls back to `body`; completion must not + reclaim focus after the user closes the confirmation. +- A focused asynchronous Confirm action keeps focus on the pending dialog + surface while its controls are disabled. If confirmation fails or returns + `false`, focus returns to Confirm unless the user moved focus or closed the + dialog. +- Opening a shared popover moves focus to its first enabled control. Escape and + explicit completion return focus to the opener; outside-pointer dismissal + leaves focus with the newly targeted surface. +- Popup menus use `menuitem` controls with roving focus. Arrow Up/Down wrap, + Home/End move to the bounds, Escape returns to the opener, and Tab closes the + menu after focus leaves it. +- Successful keyboard deletion moves focus from the removed row to the next + row's focusable primary link, then the previous row's. Revalidation resolves + those row IDs again, then checks the row now occupying the removed row's + ordinal before using the focusable results region. Disabled, inert, missing- + href, or negative-tab-index links are skipped. +- Deletion controls explicitly transfer focus ownership before they become + disabled. Only the latest deletion in a results region may restore focus; + focus the helper placed on a surviving row or the region remains owned across + revalidation, while user focus movement is never reclaimed. +- When a successful destructive setting removes its opener, focus moves to its + stable enabled successor only while the action still owns focus. Codex auth + logout uses `Start setup`; unsupported logout keeps the existing trigger. +- Reader selection and section actions use a labelled horizontal toolbar. + Arrow Left/Right wrap, Home/End move to the bounds, and Escape returns focus + to the reader content. +- Reader All Flashbacks uses native First/Previous/Next buttons. Unavailable + directions are disabled, the list is a bounded scroll region, and tab changes + preserve its rail-local cursor history. ## Labels And Landmarks @@ -106,6 +142,8 @@ Route surfaces should use `aria-labelledby` and stable headings: - `memories-title`. - `flashbacks-title`. +- `moment-title`. +- `settings-title`. - Reader fallback states use `reader-state-title`; ready reader content uses a route-local sticky header with only the back control and `Memory` label. The memory URL/action row, title, and taxonomy chips live in the main reader @@ -130,8 +168,8 @@ Disabled future controls: Desktop: - Left rail, main pane, and right rail are visible. -- Reader TOC appears at the top of the right rail only on concrete memory - reader routes. +- Reader TOC appears at the top of the right rail only on ready source or + translated memory reader routes. - TOC and Flashback shortcut lists scroll inside their own bounded list bodies. Tablet: @@ -146,12 +184,12 @@ Tablet: Mobile: -- Bottom `Primary tabs` render Memories, Flashbacks, Categories, Tags, Backup, - Add memory, Theme, and Settings. +- Bottom `Primary tabs` render Memories, Flashbacks, Moments, Categories, Tags, + Backup, Add memory, Theme, and Settings. - The tab list scrolls horizontally when space is constrained. The page itself must not gain horizontal overflow from the tab bar. -- Categories, Tags, Backup, and Settings stay disabled until their route or - action contracts are implemented. +- Categories, Tags, and Backup are disabled. Memories, Flashbacks, Moments, and + Settings are live route tabs. - Phone tab icons use a larger dedicated icon slot so the tab bar remains scannable without enlarging desktop rail icons. - Phone tab text labels are `sr-only`: names must remain available to assistive @@ -174,3 +212,14 @@ final selected and opened states. Do not add decorative motion or animation loops. Any future animation must serve an interaction state and respect readability. + +Async failures use an assertive `role="alert"` region. Successful settings or +save feedback uses a polite `role="status"` region. Do not rely on colour or +visual placement alone to announce completion or failure. Reader model-catalog +failures follow the same alert contract, keep current selections intact, and +expose a Retry action. Only one catalog request may be in flight; Retry and +dependent controls remain disabled while it is pending, and unmounted or +superseded requests must not publish stale results. A successful retry may move +focus from the removed Retry control to Model only when the user has not moved +focus elsewhere. Aborted Codex-auth polling is cancellation rather than failure +and must not announce stale feedback. diff --git a/docs/references/design-system/layout-and-shell.md b/docs/references/design-system/layout-and-shell.md index 4244665a..d69e76b5 100644 --- a/docs/references/design-system/layout-and-shell.md +++ b/docs/references/design-system/layout-and-shell.md @@ -36,8 +36,8 @@ Current desktop contract: - Position: sticky, full viewport height. - Border: right border only. - Internal layout: `flex flex-col gap-1.5`. -- Brand/home mark: use a `36px` mark with `px-2.5` so its centre aligns with - the `40px` navigation icon column. +- Brand/home mark: use the `30px` rail/tablet mark inside the `40px` icon + column. The phone header uses `28px`; Local archive uses `26px`. Navigation item contract: @@ -48,7 +48,7 @@ Navigation item contract: - Text line-height: at least `1.2` so descenders such as `g`, `q`, `p`, and `y` are not clipped. - Shape: rounded full pill for hit area only; active state must not add a - selected pill fill or surrounding primary-colour flashback. + selected pill fill or surrounding primary-colour highlight. - Active route, all themes: use the filled icon variant and bold visible tab label. - Active route, paper themes on desktop rail: draw the hand-written underline @@ -61,8 +61,9 @@ Navigation item contract: - Tablet icon rail and phone bottom tabs must not render the paper underline. - Disabled/future routes: disabled button, low opacity, no live link. -The rail may show future controls such as Backup and Settings, but they must -remain disabled until their routes exist. +Memories, Flashbacks, Moments, and Settings are live rail routes. Categories and +Tags are desktop shortcuts to right-rail filters. Backup remains disabled until +its route or action contract exists. ## Add Memory Action @@ -84,14 +85,16 @@ sufficient. ## Local Archive Surface The local archive row sits at the bottom of the left rail. It is not an auth -profile and must not imply account management. +profile and must not imply account management. It is a static status surface, +not a button or menu trigger. Current contract: -- Transparent background by default. -- Rounded full hover target. +- Transparent background with no hover or pressed treatment. +- Rounded rail layout without interactive semantics or a trailing action icon. - `40px` mark/avatar cell. -- Text shows local archive status and storage location. +- Text shows local archive status and a non-sensitive on-device label. Do not + expose or guess the configured store path in browser-visible shell chrome. ## Theme Controls @@ -175,14 +178,16 @@ Current islands: Route-specific content may be inserted above these browse filters when a route has a strong contextual aid. The current example is the reader table of -contents on `/memories/:id`. This content is registered through the shell right -rail context, appears before browse filters, and is cleared when the route +contents on ready source and translated memory routes. This content is +registered through the shell right rail context, appears before browse filters, +and is cleared when the route unmounts. Do not show reader TOC content on `/memories`, `/flashbacks`, or other non-reader routes. Long right-rail aids must not grow the application layout by item count. TOC and -Flashback shortcut lists render their item bodies as bounded scroll regions. The rail -itself remains viewport-bound, and each long list owns its own scrolling. +Flashback shortcut lists render their item bodies as bounded scroll regions. +The rail itself remains viewport-bound, and each long list owns its own +scrolling. ## Tablet And Mobile @@ -229,11 +234,11 @@ Phone: - Primary navigation is the bottom `Primary tabs` bar, using a native-app-like tab layout. - The phone tab bar renders every primary rail item: Memories, Flashbacks, - Categories, Tags, Backup, Add memory, Theme, and Settings. + Moments, Categories, Tags, Backup, Add memory, Theme, and Settings. - When all tabs do not fit, only the tab bar scrolls horizontally. Do not drop tabs or reintroduce navigation/filter drawers. -- Future or unavailable sections are rendered as disabled tabs rather than live - links to routes that do not exist. +- Categories, Tags, and Backup render as disabled tabs rather than links to + routes that do not exist. Settings is a live route. - Phone tab icons use a dedicated larger icon slot than the compact tablet rail. - Phone tab text labels are visually hidden. Keep accessible names on the tabs, but do not render visible text in the bottom bar. @@ -261,6 +266,6 @@ Reader image contract: Route frame classes belong to the route surface, not the shell. The shell owns columns, global navigation, bottom phone tabs, global composer popover state, -and the right rail slot. Route files own -headers, search controls, read-state tabs, empty states, reader content, and +and the right rail slot. Route files own headers, search controls, read-state +tabs, empty states, reader content, and route-specific loading states. diff --git a/docs/references/design-system/reader-and-content.md b/docs/references/design-system/reader-and-content.md index 8c055d1e..fcce046c 100644 --- a/docs/references/design-system/reader-and-content.md +++ b/docs/references/design-system/reader-and-content.md @@ -11,7 +11,8 @@ selection. Reader route frames use: ```text -min-h-screen w-full bg-trauma-bg-surface +trauma-route-surface trauma-reader-surface trauma-mobile-stable-viewport +w-full bg-trauma-bg-surface ``` The frame must fill the main pane. It must not centre itself independently @@ -26,10 +27,11 @@ Contract: - Back control links to `/memories`. - Header uses the route frame background with backdrop blur. - Header label is exactly `Memory`. -- Do not repeat the memory title in the header; the markdown body already owns - the visible content title. +- Do not repeat the memory title in the sticky header. `MemoryReader` lifts the + content title, or renders the memory title fallback, in the main reader intro. - Do not render `Reader mode` copy. -- Source URL is visible and opens in a new tab only when safe. +- The main reader intro shows the source URL and memory actions. +- A safe source URL opens in a new tab. - Source URL links use `text-trauma-link` and `hover:text-trauma-link-hover`. - Unsafe or non-linkable source values render as text. @@ -42,7 +44,11 @@ Contract: - Registered by `MemoryReader` into the shell right rail while a ready memory route is mounted. -- Rendered as the first right rail island on `/memories/:id`. +- The right-rail subtree is registered once per ready Reader mount. Reader-owned + Flashbacks, Moments, and pending Moment state flow into that stable subtree + through reactive accessors; state changes must not replace the subtree. +- Rendered as the first right rail island on ready source and translated reader + routes. - Hidden from `/memories`, `/flashbacks`, and other non-reader routes. - Removed from the right rail on reader unmount. - Rounded island surface matching right rail section geometry. @@ -54,6 +60,12 @@ Contract: indentation for hierarchy. - Heading links live inside a bounded scroll body so many headings do not expand the right rail or the whole app layout. +- Toggling a Moment preserves the Flashbacks Current/All selection, the bounded + TOC `scrollTop`, and the enclosing right-rail `scrollTop`. +- All Flashbacks loads lazily through the same bounded page loader as the + canonical Flashbacks route. It renders one page at a time in a bounded scroll + body and owns rail-local First/Previous/Next cursor history. Current remains + memory-local and switching tabs does not reset the All page. - When the bounded TOC body can scroll further, show a subtle blur fade only on the available scroll edge. At the top of the list this appears only after the user has scrolled down; at the bottom it disappears when no more content @@ -81,7 +93,9 @@ static aids (anchors, scroll fades, Moment toggles, long-press menus). reader DOM. - The range is painted by a single measured overlay element (`trauma-toc-reading-band`) positioned behind the entries, not per-row - backgrounds. Its top/height are measured from the first and last on-screen + backgrounds. The overlay remains a presentation-only list item so the + ordered list has valid direct-child semantics. Its top/height are measured + from the first and last on-screen rows so the highlight is one seamless region with no visible seams between chapters. The fill is a subtle translucent contrast lift over the TOC surface (`color-mix(in srgb, var(--fg-1) 8%, transparent)`); background-only, it does @@ -110,6 +124,22 @@ static aids (anchors, scroll fades, Moment toggles, long-press menus). Rendered markdown uses Tailwind Typography through `@tailwindcss/typography`. +The Markdown pipeline is deliberately split by responsibility: + +- Reader display uses `markdown-it` for GFM-like rendering, footnotes, heading + anchors/ToC metadata, and syntax highlighting, then sanitizes the generated + HTML before adding TRAUMA-owned Moment controls. +- Auto-loaded image and iframe URLs pass through the reader media policy; the + same-origin media proxy performs public-host, redirect, timeout, byte-limit, + and raster-content checks before a browser can load remote images. +- Translation uses the `unified`/Remark AST only for structural segmentation, + projection, and validation. That AST pipeline does not render browser HTML. + +Keep these boundaries separate. A future parser replacement must preserve the +same heading IDs and paths, code output, tables/tasks/footnotes, sanitizer and +embed policy, ToC metadata, and `data-flashback-id` semantics before removing +either implementation. + Reader prose rules: - `prose max-w-none min-w-0`. @@ -150,7 +180,9 @@ Flashback browse excerpts and right-rail shortcuts use the shared ## Flashback Interaction -Selecting reader text toggles flashback state: +Selecting reader text opens the reader selection-actions menu; selection alone +does not change persisted or rendered flashback state. Activating `Flashback +selection` from that menu toggles flashback state: - Selecting unmarked text flashbacks it. - Selecting a fully marked range unflashbacks it. @@ -168,6 +200,12 @@ content and do not reuse stale DOM. Reader fallback states use the same route frame and should not look like a separate page type. +The translation popover treats Codex model-catalog load failure as actionable +async feedback. Its assertive `role="alert"` keeps the error and a Retry action +visible without relying on color. Retry is disabled while its request is pending; +after recovery started from the focused Retry control, focus moves to the +restored Model control unless the user moved focus elsewhere. + ## Psychiatrist Dock Psychiatrist uses a bottom-centered floating home-bar affordance on ready reader @@ -180,14 +218,26 @@ The dock creates or resumes the active memory variant's latest thread. Stored pair history returned by the thread API is rendered as user prompt/assistant response rows. Safe process/status events are visually subordinate to answer text, and hidden chain-of-thought or raw backend payloads are never rendered. +The browser normalizes and coalesces adjacent duplicate status text, retains the +first context row plus the latest seven rows per pair, and therefore renders at +most eight process rows for each visible pair. Answer deltas remain one answer +projection and are not subject to process redaction or coalescing. Interaction contract: - Submit becomes Stop while a turn is running. +- Plain Enter submits only when the prompt textarea owns the keyboard event. + Shift+Enter remains a newline. Enter during IME composition, including the + legacy `keyCode = 229` signal, must not be prevented or submitted. - Stop is the only UI action that calls the cancel route. - Panel close, Escape, route unmount, memory navigation, and browser reload do not cancel the server turn. - Route lifecycle cleanup closes the browser `EventSource` connection only. +- A named terminal SSE frame closes its `EventSource` before payload parsing. If + that terminal payload is malformed, the browser performs at most one + canonical same-reader/thread/turn reload and never calls cancel. A failed + reload or a second malformed terminal for that turn exposes the existing + manual thread Retry action instead of creating an automatic reconnect loop. - Returning to the same memory resumes the latest matching thread and reconnects to `active_turn.event_url` when present. - Completed responses expose Regenerate. Regenerate streams into the same pair @@ -195,6 +245,35 @@ Interaction contract: - `network_permission_required` is shown as a per-turn permission state: the user must explicitly allow web search/source lookup for that answer before any web-source turn may run. +- Citation wire values remain compatible strings. Only credential-free public + `http:` and `https:` URLs become links; malformed URLs, active-content schemes, + localhost names, and non-public IP literals remain visible as inert source + title text. +- The full thread response remains the canonical in-memory transcript. The DOM + projects fixed 24-pair Older/Newer pages from it, pins an out-of-page active + pair and the latest persisted web-source retry pair, and never renders more + than 26 pair rows. Pins are deduplicated and stay in transcript chronology. + The polite range label reports each out-of-page pin by one-based transcript + number and active or web-source-retry reason; pins already in the page do not + change the ordinary range label. +- Opening the dock, loading a different thread, or appending a new prompt shows + the most recent page. Same-thread reconciliation preserves the current page. + Page controls target the transcript with `aria-controls`, announce the visible + range politely, keep keyboard focus stable, and move the transcript viewport + to the top without a live stream update stealing that reading position. +- Opening an unloaded dock focuses the enabled Close control while the prompt is + disabled. Initial readiness hands focus to the prompt only when the same dock + opening is still active and focus remains on Close. User focus movement, + panel close, and background reconciliation never steal focus; load failure + leaves Retry keyboard-reachable. A retry started from the focused Retry + control first hands focus to stable Close while the error action unmounts, + then hands it to the enabled prompt only after the same retry succeeds and + focus still remains on Close. +- Opening the dock, completing its initial thread load, or appending a new user + prompt scrolls the transcript to its bottom. Streaming output follows only + when the transcript was within 48 px of the bottom before that update; a user + who scrolls upward keeps that reading position. Deferred scroll writes must be + canceled by a newer manual scroll, reader generation, close, or unmount. Motion follows the reader's accessibility contract. Normal expansion may animate from the home bar, but `prefers-reduced-motion: reduce` disables transform-heavy diff --git a/docs/references/design-system/verification.md b/docs/references/design-system/verification.md index afc06a6b..3740223d 100644 --- a/docs/references/design-system/verification.md +++ b/docs/references/design-system/verification.md @@ -99,7 +99,9 @@ tests cannot fully prove: - `/memories` desktop. - `/memories?view=grid` desktop. - `/flashbacks` desktop. -- `/memories/:id` reader. +- `/moments` desktop. +- `/settings` desktop. +- Source and translated reader routes. - `/memories` tablet. - `/memories` mobile. diff --git a/docs/references/glossary.md b/docs/references/glossary.md index a6abb36e..3c2413fb 100644 --- a/docs/references/glossary.md +++ b/docs/references/glossary.md @@ -12,10 +12,12 @@ : The readable markdown file for one memory. `storePath` -: Directory that contains memory markdown files. +: Directory that contains source/translated reader files, portable metadata +exports, and memory-local Psychiatrist thread artifacts. `projectPath` -: Git working directory used by the built-in markdown backup feature. +: Git working directory used by built-in backup for explicitly enqueued +`storePath` artifacts. `category` : Curated grouping. A memory can have multiple categories. diff --git a/docs/references/technology-stack.md b/docs/references/technology-stack.md index 56afe021..ccf42791 100644 --- a/docs/references/technology-stack.md +++ b/docs/references/technology-stack.md @@ -1,6 +1,6 @@ # Technology Stack Reference -This reference records the selected initial stack and explicit exclusions. +This reference records the selected current stack and explicit exclusions. ## Selected Stack @@ -13,6 +13,8 @@ This reference records the selected initial stack and explicit exclusions. - Database: SQLite through Drizzle's Bun SQLite support. - Web content extraction: Defuddle with linkedom for server-side DOM parsing. - Browser-assisted import: local Chrome Manifest V3 extension built with Bun. +- Optional AI backend transport: a separately operated Codex app-server over + its Unix listener. - Unit/integration testing: Vitest. - E2E testing: Playwright. @@ -58,19 +60,23 @@ This is not a serverless-first application. ## Rationale SolidStart keeps routing, SSR, server functions, and UI in one lightweight app. -Bun keeps the runtime/package manager surface small. SQLite and markdown files -keep operational cost low and work naturally with a single persistent disk. +Bun keeps the runtime/package manager surface small. SQLite and the file-backed +memory store keep operational cost low and work naturally with one persistent +disk. Drizzle is used to keep the schema and query layer type-safe while remaining close to SQL. ## Exclusions -Do not introduce these into the initial implementation: +Do not introduce these into the current architecture without an explicit +design change: - Next.js. - PostgreSQL or managed database services. - Redis or external job queues. - Serverless/edge-only deployment assumptions. - React-specific component or routing assumptions. -- Authentication/user ownership. +- TRAUMA user accounts, browser sessions, public signup, or multi-user + ownership. Codex app-server login is an integration credential, not product + user authentication. diff --git a/docs/reviews/2026-07-13-pr-30-psychiatrist-merge-readiness-round-2.md b/docs/reviews/2026-07-13-pr-30-psychiatrist-merge-readiness-round-2.md deleted file mode 100644 index cef6f618..00000000 --- a/docs/reviews/2026-07-13-pr-30-psychiatrist-merge-readiness-round-2.md +++ /dev/null @@ -1,215 +0,0 @@ -# PR #30 Psychiatrist merge-readiness review, round 2 - -Date: 2026-07-13 JST -Pull request: [#30 docs: plan psychiatrist workflow](https://github.com/hauntedfail/Trauma/pull/30) -Code head reviewed: `0b567edfffd793a56c570388d0d8e3841684b9d2` -Head branch: `docs/task-24-psychiatrist-plan` -Dedicated implementation worktree: `/private/tmp/trauma-pr30-review-followup-20260712` - -## Outcome - -The added Codex review was processed end to end with EDA in -`parent-subtasks` mode. All five inline findings were accepted, fixed with -regression coverage, replied to inline, reacted to, and resolved. The generic -review body contained no independent technical claim and was recorded as a -`not_valid` standalone item through a PR trace comment. - -After the GitHub findings were closed, repeated detached local reviews found -and fixed additional backup durability, replay recovery, Regenerate, Stop, -thread-reload, approval-retry, and browser SSE transport defects. Review and -repair continued until independent backend and UI acceptance reviews reported -no actionable finding at the final code head. - -At `0b567ed`: - -- local HEAD and the live PR head matched; -- all commits were GPG-signed and pushed normally without history rewriting; -- the fresh GraphQL sweep returned zero unresolved review threads; -- GitHub reported the PR as mergeable; -- CodeRabbit and GitHub Actions Verify passed; -- full repository verification, Playwright, and development startup smoke passed. - -The ignored EDA execution packet remains at: - -`.eda/n30/002/002_review_comments_and_merge_ready_loop/` - -## Canonical GitHub review inventory - -Planning mode: `parent-subtasks` - -| Canonical item | Adjudication | Outcome | -| --- | --- | --- | -| [Review 4680524779](https://github.com/hauntedfail/Trauma/pull/30#pullrequestreview-4680524779) | `not_valid` as a standalone item | Generic Codex review container; its inline claims were handled separately. Recorded in the [trace and re-review comment](https://github.com/hauntedfail/Trauma/pull/30#issuecomment-4952725751). | -| [3566836234](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566836234) | `must_fix` | Regenerate now rejects old prompt-policy manifests before mode resolution or any turn side effect while preserving stored-context Regenerate. [Inline reply](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3567095591). | -| [3566836238](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566836238) | `must_fix` | A running same-memory backup always receives one merged follow-up, including identical and subset paths. [Inline reply](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3567095589). | -| [3566836239](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566836239) | `must_fix` | Completion persists one canonical terminal after enqueue transition outcome and before worker file snapshot. [Inline reply](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3567095577). | -| [3566836240](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566836240) | `must_fix` | Stop uses typed results and canonical reconciliation before actions become available. [Inline reply](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3567095578). | -| [3566836243](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566836243) | `must_fix` | Directly owned inactive turns reconcile even when replay is absent or empty. [Inline reply](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3567095576). | - -All five original comments received a `+1` reaction. Their GraphQL threads are -resolved. The re-review request was included in the trace comment after the -first pushed remediation and green CI; subsequent sweeps found no new -unresolved Codex thread. - -## Ordered Revy implementation units - -### 001: Regenerate invariants - -- Rejected old policy versions before ID generation, reservation, Codex, - stream, or backup work. -- Removed the live-memory hash preflight that incorrectly rejected historical - Regenerate; stored `PROMPT.md` and `CONTEXT.json` remain authoritative. -- RED: two API failures proved ID generation was reachable and changed current - memory returned 409 instead of the required 202. - -### 002: backup queue boundary - -- Removed the running-path suppression that lost identical/subset writes. -- Added `enqueue(input, finalizer?)`: queued state is durable before the short - finalizer and the worker cannot start until finalization succeeds. -- RED: the second snapshot was missing; old code ran the worker before the - ignored finalizer and ignored its rejection. - -### 003: terminal publication and replay recovery - -- First-answer and Regenerate completion now write exactly one terminal event. -- Empty replay reconciles the directly requested inactive turn. -- Missing canceled replay is repaired without duplicating existing terminals. -- RED: eight failures covered premature and duplicate terminals, three empty - replay recovery paths, and missing canceled replay. - -### 004: Stop, readiness, and accessibility - -- Added runtime-validated cancel results and explicit - `starting | running | stopping | idle` phases. -- Added initial thread readiness/retry, disabled busy Regenerate controls, and - one scoped atomic polite live region. -- RED: cancel returned `undefined`, malformed success was accepted, and partial - output remained after canceled-without-SSE. - -### 005: failed-finalizer intent retention - -- Replaced count-only intent recovery with full per-memory jobs, retaining - paths and newest trigger reason. -- Restored pending status when no runnable job owns queued state. -- RED: standalone failure left ghost queued state; a later different-path job - omitted the retained Psychiatrist path; sequential intent order was reversed. - -### 006: UI terminal and approval races - -- A terminal SSE received during Stop invalidates the pending Stop generation. -- Ambiguous approval mutations reconcile canonical state and preserve the CTA - if reconciliation fails. -- Stop reload adopts a different canonical active turn. -- RED: ghost Stop returned after terminal SSE, successor was not adopted, and - approval CTA disappeared after a failed mutation. - -### 007: real browser SSE transport - -- Added a native Chromium EventSource test using a controllable HTTP SSE - response. -- It verifies scoped URL/query construction, named events, a held-open running - stream, terminal projection, and client-driven close. -- Removed dead Playwright route/state that the in-memory fake never requested. -- RED: the prior implied transport assertion received no event request. - -### 008: changed-thread Stop reconciliation - -- Post-Stop reload validates reader generation and reconciles the newly - installed thread identity instead of requiring the stopped identity. -- Changed idle threads clear old state; changed active threads are adopted. -- Successor adoption clears historical approval errors and CTAs. -- RED: changed thread remained `Stopping`, successor Stop was missing, and a - historical approval CTA leaked into the successor turn. - -### 009: cancel-outcome canonical reconciliation - -- Successful canceled/completed/failed outcomes and ambiguous cancel failures - converge through one canonical reconciliation state. -- Exact old active turns are restored only when confirmed; terminal state goes - idle, successors are adopted, and reload failure remains non-repeatable with - Retry thread load. -- RED: canceled missed successor, rejected response missed server cancellation, - exact-old active was not reloaded, and reload failure had no retry path. - -### 010: persist-intent transition - -- Published an exact full-job persist transition before awaiting the pending - database update. -- Running jobs treat a transition as pending; success merges the durable intent - before removing its token; failure removes only that token. -- RED: the race produced `queued -> pending -> success`, persisted success, - fresh retry count 0, and no restarted job. The fixed path remains pending and - restart discovers both original and new paths. - -## Signed implementation commits - -All pushes were normal non-force pushes to -`refs/heads/docs/task-24-psychiatrist-plan`. - -- `c5a2f22fba4be710950912733d11b1f5627a1a59` — `fix: resolve psychiatrist review races` -- `07cfce016118f6c0a9f2e6c861cd11f2ffdf9b17` — `fix: preserve failed backup intents` -- `dd706c894884a60413c95f2e1ef77a2bcdec3dac` — `fix: reconcile psychiatrist stop races` -- `3998756d74914116d4261e9c79b6171730bcdcfb` — `fix: reconcile changed psychiatrist threads` -- `6939eaf5984189be581704bbd298909081f19e26` — `fix: reconcile psychiatrist cancel outcomes` -- `0b567edfffd793a56c570388d0d8e3841684b9d2` — `fix: guard backup intent persistence` - -Sawyer verified good signatures from configured key -`34DA85F7D6AC9041` for every commit. Local HEAD and the remote target ref were -verified after every push. - -## Verification record - -Final code-head verification: - -- `mise exec -- bun run verify` - - 117 test files passed; - - 1070 tests passed; - - 5 tests remain explicitly todo; - - production build passed. -- `mise exec -- bun run test:e2e` - - 71/71 Playwright tests passed at `0b567ed`. -- `TRAUMA_DEV_PORT=63931 TRAUMA_HMR_PORT=24931 mise exec -- bun run dev:smoke` - - `/memories` responded successfully. -- backup-focused final suite - - 52/52 passed. -- reader-focused final suite - - 34/34 passed. -- component/request suite - - 46/46 passed. -- `git diff --check` - - passed. - -The build continues to emit the existing non-blocking Node `DEP0155` warning -from `defuddle` through `temml`. Playwright also reports the existing -`NO_COLOR`/`FORCE_COLOR` warning. All commands exit successfully. - -## Local review and agent record - -Independent detached worktrees were created at each pushed review baseline. -Read-only lanes covered: - -- backup concurrency, durable intent, finalizer failure, and Regenerate - provenance; -- terminal publication, SSE replay, crash recovery, cancellation repair, and - `Last-Event-ID`; -- UI turn lifecycle, Stop/cancel races, approval retry, thread identity, - accessibility, and real EventSource behavior. - -Confirmed findings were returned to fresh Revy TDD units. The parent reviewed -the resulting diffs and verification evidence. Sawyer alone staged, created -signed commits, pushed, and verified the remote ref. The final detached backend -and UI acceptance reviews at `0b567ed` both reported no actionable finding. - -## Final GitHub state before this report-only commit - -- Code head: `0b567edfffd793a56c570388d0d8e3841684b9d2` -- Mergeable: yes -- Unresolved review threads: 0 -- CodeRabbit: passed -- GitHub Actions Verify: [passed in 3m03s](https://github.com/hauntedfail/Trauma/actions/runs/29211290135/job/86699333960) -- Re-review/trace comment: [4952725751](https://github.com/hauntedfail/Trauma/pull/30#issuecomment-4952725751) - -This report is the only change after the reviewed code head. Its commit and -resulting documentation-only CI run are verified separately during final -handoff. diff --git a/docs/reviews/2026-07-13-pr-30-psychiatrist-review-response-and-local-audit.md b/docs/reviews/2026-07-13-pr-30-psychiatrist-review-response-and-local-audit.md deleted file mode 100644 index 09858605..00000000 --- a/docs/reviews/2026-07-13-pr-30-psychiatrist-review-response-and-local-audit.md +++ /dev/null @@ -1,215 +0,0 @@ -# PR #30 Psychiatrist review response and local implementation audit - -Date: 2026-07-13 JST -Pull request: [#30 docs: plan psychiatrist workflow](https://github.com/hauntedfail/Trauma/pull/30) -Head branch: `docs/task-24-psychiatrist-plan` -Dedicated worktree: `/private/tmp/trauma-pr30-review-followup-20260712` - -## Outcome - -The merged Psychiatrist implementation was reviewed in two passes: - -1. EDA review-thread discovery, adjudication, fixes, inline replies, reactions, - resolution, signed commits, push, and CI follow-up. -2. A separate local security, backend/state/storage, and frontend/UI review, - followed by ordered red-green fixes for every confirmed finding. - -At the final implementation head `41cece3351af1d4ecce7a6a4223138938b3b90ea`: - -- all GitHub review threads are resolved; -- the fresh GraphQL thread sweep returns zero unresolved threads; -- the local branch and live PR head match; -- the implementation and E2E verification suites pass; -- commits were signed and pushed normally without history rewriting. - -The durable EDA execution packet is intentionally ignored by Git and remains at: - -`.eda/n30/001/001_merged_psychiatrist_review/` - -## GitHub review response - -The initial canonical sweep found six unresolved, non-outdated review threads. - -| Review comment | Adjudication | Result | -| --- | --- | --- | -| [3446962188](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962188) | Already fixed | Verified same-pair approved Regenerate retry and its regression coverage; replied, reacted, resolved. | -| [3446962190](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962190) | Already fixed | Verified structured `webSourceRequired` handling without answer-text parsing; replied, reacted, resolved. | -| [3446962191](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962191) | Must fix | Made public thread reads memory-scoped with a direct owning-memory lookup and cross-memory 404 regression; replied, reacted, resolved. | -| [3446962193](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962193) | Must fix | Deferred route assertions until their owning workflow stages create those routes; replied, reacted, resolved. | -| [3446962195](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962195) | Must fix | Clarified that transcript/domain data stays out of SQLite while built-in backup status bookkeeping remains allowed; replied, reacted, resolved. | -| [3446962198](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3446962198) | Already fixed | Verified retry reconstruction enumerates all Psychiatrist thread artifacts; replied, reacted, resolved. | - -The first implementation commit was: - -- `12a932bfb2fa1196126fac560dc4dd86fd11a7d5` — `fix: scope psychiatrist thread reads to memories` - -Its first CI run exposed only E2E fixture-level SQLite read contention after the -Moment POST had succeeded. The fix added a readonly-connection -`PRAGMA busy_timeout = 5000` without changing production database behavior: - -- `19f44845a6d7ca3f950a3bc48ff399f3aa3f51fa` — `test: tolerate sqlite contention in e2e fixture` - -The exact failed E2E repeated 10/10 successfully after the change, the full E2E -suite passed, and the rerun became green. - -## Local implementation and security audit - -Independent read-only lanes reviewed the exact `19f44845` baseline for: - -- Codex runtime isolation and untrusted prompt/transcript boundaries; -- backend state machines, persistence, crash recovery, replay, and backup; -- frontend turn ownership, lifecycle, responsive layout, and accessibility. - -The parent review also traced active-variant identity and public API lookup -boundaries. Confirmed findings and accepted fixes were executed sequentially by -fresh Revy units. - -### 004: fail closed without an externally enforced runtime boundary - -Codex `readOnly` prevents writes but does not remove shell or host-file reads. -Production message and Regenerate handlers now return -`503 runtime_isolation_required` before reservation or storage work unless the -operator makes the exact external-isolation assertion. Injected fake clients -remain testable. - -The configuration and self-hosting docs explicitly state that the assertion -does not create or inspect a sandbox. Operators must independently ensure that -home, project, and store roots are unreadable and egress is constrained to -public HTTP(S) destinations. - -### 005: active variant identity and bounded public APIs - -Source and translated sessions now use active content hash, variant kind, -language, translation output hash, source hash, and prompt-policy version as -their resume identity. - -Message, Regenerate, cancel, read, and replay routes are nested under the -owning memory/thread path. Cross-memory and cross-variant requests fail before -Codex, writes, backup, or cancellation. Pair and replay lookup use direct store -paths; no Psychiatrist `scanSync()` remains. - -### 006: completion, backup intent, and crash recovery - -The backup queue now has a two-phase durability boundary: - -- `persistIntent()` creates retry-eligible durable state without starting a worker; -- completion persists the full answer/turn/replay set; -- final enqueue records queued state before terminal publication and worker execution. - -Startup recovery treats a completed PAIRS revision for the same turn as -authoritative. It repairs first-answer and Regenerate turn records, terminal -replay, and thread projection instead of rewriting a saved answer as an -interrupted failure. - -### 007: JSONL torn-tail recovery - -PAIRS and stream journals now recover only an invalid, unterminated final -fragment. Complete or interior corruption continues to fail hard. The next -stream event ID is derived from valid rows and remains monotonic after repair, -including when the process cache is warm. - -### 008: UI turn lifecycle isolation - -Successful Stop disconnects and invalidates the old stream. Stream callbacks -must match reader, thread, stream, and current-turn generations before they can -change running state. Deferred load/send/Regenerate/Stop continuations ignore -disposed or changed reader generations. Component cleanup disconnects only and -does not cancel server work. - -### 009: phone layout and prompt accessibility - -The fixed dock clears the 4.75rem phone tab bar plus the shared safe-area token -through 720px and preserves the desktop bottom spacing. The prompt textarea now -has a real visually hidden `Message Psychiatrist` label. - -The combined local-audit implementation was signed and pushed as: - -- `4561918fe29542006dedad6c7c0fbaa41e978539` — `fix: harden psychiatrist runtime boundaries` - -## Final post-push review sweep - -A fresh sweep after `4561918` found four additional unresolved threads. - -Two were already fixed by the active-variant/API work and were replied to, -reacted to, and resolved: - -- [3566362003](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566362003) — translated thread identity; -- [3566362005](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566362005) — memory-scoped message sends. - -Two were valid and received new red-green fixes: - -### 010: direct-send prompt-policy freshness - -[3566362007](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566362007) -showed that a browser could directly send to an old-policy manifest. The route -now compares `policyVersion` before reservation, ID generation, context, Codex, -stream, artifact, or backup work. It marks the thread stale and returns the -stable refresh response. The pre-fix regression returned 202 instead of 409. - -### 011: structured process-event allowlist - -[3566362010](https://github.com/hauntedfail/Trauma/pull/30#discussion_r3566362010) -showed that blacklist filtering could still persist arbitrary app-server text. -Only three exact `{kind, status: "started"}` pairs are now accepted and mapped -to fixed TRAUMA-owned strings. Unknown, missing, text-bearing, or extra/raw -payload fields are ignored; arbitrary message, summary, status, source, and -backend text cannot reach `psychiatrist.process.delta` persistence. - -These final review fixes were signed and pushed as: - -- `41cece3351af1d4ecce7a6a4223138938b3b90ea` — `fix: close psychiatrist review gaps` - -Both comments received inline replies and reactions, both threads were -resolved, and the final GraphQL sweep returned zero unresolved threads. - -## Verification record - -Final local verification at `41cece3`: - -- `mise exec -- bun run verify` - - 117 test files passed; - - 1048 tests passed; - - 5 tests remain explicitly todo; - - production build passed. -- `GIT_CONFIG_GLOBAL=/dev/null mise exec -- bun run test:e2e` - - 56/56 Playwright tests passed. -- `tests/server/psychiatrist/api-routes.test.ts` - - 64/64 passed after the policy-freshness change. -- `tests/server/translation/codex-app-server.test.ts` - - 37/37 passed with host permission for its temporary Unix listener. -- `git diff --check` - - passed. - -The build continues to emit the existing non-blocking Node `DEP0155` warning -from `defuddle` through `temml`; all verification commands exit successfully. - -Remote verification and signing: - -- Sawyer verified good GPG signatures using configured key - `34DA85F7D6AC9041`. -- Every push was a normal non-force push to - `refs/heads/docs/task-24-psychiatrist-plan`. -- No remote history was rewritten and no excluded working-tree content was - staged or deleted. - -## Residual operational constraints - -- `TRAUMA_PSYCHIATRIST_RUNTIME_ISOLATION` is an operator assertion, not a - sandbox implementation. Psychiatrist production turns must remain disabled - until the app-server is independently isolated from readable host roots and - non-public egress. -- Legacy arbitrary-text `item/process` notifications and structured process - notifications with unapproved extra metadata are intentionally dropped. - New process states require explicit kind/status approval and a fixed display - mapping. -- Chromium reports a zero safe-area inset in the current Playwright - environment. Nonzero safe-area behavior is protected by the shared CSS token - and static contract rather than device-inset emulation. - -## Final PR state - -- PR: [#30](https://github.com/hauntedfail/Trauma/pull/30) -- Head: `41cece3351af1d4ecce7a6a4223138938b3b90ea` -- Unresolved review threads: `0` -- CodeRabbit: passed -- GitHub Actions Verify: [passed in 2m49s](https://github.com/hauntedfail/Trauma/actions/runs/29202691844/job/86676626232) diff --git a/docs/superpowers/specs/2026-05-09-trauma-foundation-design.md b/docs/superpowers/specs/2026-05-09-trauma-foundation-design.md index bc2d3256..b3864f53 100644 --- a/docs/superpowers/specs/2026-05-09-trauma-foundation-design.md +++ b/docs/superpowers/specs/2026-05-09-trauma-foundation-design.md @@ -1,7 +1,14 @@ -# Trauma Foundation Design +# Historical Trauma Foundation Design Date: 2026-05-09 +> **Superseded historical record.** This document captures assumptions made +> before the current implementation existed. Do not use it as an implementation +> contract or update target. Start at the [documentation index](../../INDEX.md) +> for current architecture, storage, routing, operations, and verification. +> Current semantic docs, code, and tests resolve every contradiction in their +> favor. This file is retained only to explain the project's origin. + ## Purpose Trauma is a personal bookmark management app. The product language uses diff --git a/docs/workflows/README.md b/docs/workflows/README.md index bec2b73f..42c05fcb 100644 --- a/docs/workflows/README.md +++ b/docs/workflows/README.md @@ -1,140 +1,19 @@ -# TRAUMA Execution Workflows +# TRAUMA Execution Workflow Policy -This directory contains task-scoped execution workflows. Each file is intended -to be read independently by a worker that owns one pull request. +Current durable open work lives in [Backlog.md](../../Backlog.md). There are no +long-lived task execution plans in this directory. -Use this directory instead of a single large implementation plan. The goal is to -keep each worker's context focused on its own domain. +For implementation work: -## Task Map +1. Start from [docs/INDEX.md](../INDEX.md) and read the owning semantic docs. +2. Create a temporary task/PR plan only when the change needs coordination. +3. Keep each unit domain-scoped and include tests with behavior changes. +4. Run the verification justified by + [the verification strategy](../quality/verification.md). +5. Move lasting contracts into architecture, reference, operations, or quality + docs, update Backlog state, then delete the temporary plan. -Status values describe the current `main` baseline. This directory is for -active or ready execution plans, not for long-term storage of completed PR -history. Once a workflow lands, keep only durable semantic knowledge in -architecture, reference, quality, or operations docs. - -| Order | Workflow | Domain | Status | -| --- | --- | --- | --- | -| 9 | [E2E integration hardening](task-09-e2e-integration-hardening.md) | Deterministic fixtures and full flow Playwright coverage | Available for further hardening | -| 11 | [Test suite health refactor](task-11-test-suite-health-refactor.md) | Test boundaries, weak assertions, startup coverage, script normalization | TODO after archived Task 10 baseline | -| 12 | [GitHub Actions and docs health](task-12-github-actions-and-docs-health.md) | CI trigger split, docs health checks, scheduled docs maintenance workflow | TODO after Task 11 | -| 13 | [Markdown reader library decision](task-13-markdown-reader-library-decision.md) | Reader library spike, ADR, dependency direction | TODO after archived Task 10 baseline | -| 14 | [Markdown reader refactor](task-14-markdown-reader-refactor.md) | Reader pipeline decomposition and behavior-preserving refactor | TODO after Task 13 | -| 15 | [Refactor wave integration](task-15-refactor-wave-integration.md) | Cross-task verification and workflow/docs synchronization | TODO after Tasks 11-14 | - -## Current Audit Notes - -- Task 10 was stale in the active task map because its startup contract is - already part of the merged baseline through `dev`, `start`, `preview`, and - `dev:smoke`; the execution plan is now archived. -- Task 19 and its repair/follow-up plans are archived as historical Brilliant - execution records. Current translation behaviour is represented by the code, - architecture/reference docs, and tests, not active workflow plans. -- Tasks 20-23 are archived as completed or superseded execution records. Current - lazy loading, popover/translation UI, vim-like browse navigation, and reader - TOC progress behaviour should be read from the code, semantic docs, and tests - rather than from active workflow plans. -- Tasks 11-15 remain active TODO plans after checking the current tree. In - particular, `scripts/check-docs-health.ts`, `docs:check`, - `.github/workflows/docs-health.yml`, `docs/references/reader-pipeline-decision.md`, - and the Task 14 split reader modules are not present on `main`. -- Task 24 is archived. Current Psychiatrist behavior is represented by the - code, `.agents/skills/psychiatrist/SKILL.md`, architecture/reference docs, - and focused server/component/browser verification. - -## Archived Workflows - -`docs/workflows/archive/` contains historical execution plans that are no -longer active work. Do not start implementation from archived files. If an -archived task reveals a durable rule, move that rule into the owning semantic -document instead of reviving the old execution plan. - -| Workflow | Archived Location | Reason | -| --- | --- | --- | -| Task 10: Runtime dev server stabilization | [archive/task-10-runtime-dev-server-stabilization.md](archive/task-10-runtime-dev-server-stabilization.md) | Startup contract is merged into the shared baseline. | -| Task 19: Brilliant Codex translation family | [archive/task-19-codex-translation.md](archive/task-19-codex-translation.md) | Translation implementation and follow-up repair plans have landed or been superseded. | -| Task 20: Lazy loading performance | [archive/task-20-lazy-loading-performance/README.md](archive/task-20-lazy-loading-performance/README.md) | Cursor pagination and lazy browse/reader data work are no longer active workflow plans. | -| Task 21: Popover and translation UI fixes | [archive/task-21-popover-and-translation-ui-fixes/README.md](archive/task-21-popover-and-translation-ui-fixes/README.md) | Popover and translation UI repair work is represented by current code and tests. | -| Task 22: Vim-like memory browse keybindings | [archive/task-22-vim-like-key-bindings.md](archive/task-22-vim-like-key-bindings.md) | Browse keybinding work is no longer an active implementation workflow. | -| Task 23: Reader TOC reading-progress | [archive/task-23-reader-toc-reading-progress/README.md](archive/task-23-reader-toc-reading-progress/README.md) | Reader TOC progress behavior has moved from active workflow to historical record. | -| Task 24: Psychiatrist memory assistant | [archive/task-24-psychiatrist-assistant/README.md](archive/task-24-psychiatrist-assistant/README.md) | Psychiatrist is represented by current code, repo-local skill policy, semantic docs, and tests. | - -## Worker Rules - -- Start at [docs/INDEX.md](../INDEX.md) for orientation before touching files; - all durable project detail lives under `docs/`. -- Own only the files listed in the workflow unless the PR description explains - why a boundary change is required. -- Read the workflow file, its referenced docs, and - [coding standards](../references/coding-standards/INDEX.md) before coding. -- Keep PRs domain-scoped. Do not bundle unrelated UI, storage, importer, and - backup changes. -- Add tests in the same PR as the behavior. -- Run the workflow's verification commands before handoff. -- Record exact commands and outcomes in the PR body. - -## Shared Baseline - -All workflows assume the bootstrap already exists: - -- Bun `1.3.13`, pinned in `mise.toml` and `package.json`. -- SolidStart stable v1 through `@solidjs/start@1.3.2`. -- Drizzle, Vitest, and Playwright are installed. -- `bun run verify` runs typecheck, unit tests, and build. -- `bun run test:e2e` runs the Playwright smoke suite. -- Config and persistence foundations are merged, including path validation, - migrations before repository exposure, and Bun SQLite connection lifecycle. -- Markdown content store APIs are merged, including content path resolution, - writer/reader behavior, and filesystem-isolated fixtures. -- Importer and add-memory APIs are merged, including link-only fallback behavior - and backup enqueue boundaries. -- Browse shell and reader pipeline foundations are merged, including - repository-backed browse rows, shared shell layout, and sanitized markdown - rendering. -- Highlight creation, toggle removal, `/highlights`, and highlight-aware - `/memories` search are merged. -- Git backup queue and backup status tracking are merged. -- Runtime command stabilization is merged: `dev`, `start`, and `preview` run - Vinxi through Bun for server code that depends on Bun APIs. -- Defuddle v0.18+ extraction, browser-assisted import API, and the local Chrome - MV3 browser extension are merged. -- Backup environment failsafe, content-integrity alerts, and missing-file - SQLite record deletion recovery are merged. -- `ExtractionStatus` is shared through `src/server/memory-status.ts` and used - by markdown frontmatter validation and SQLite constraints. -- The refined frontend baseline is merged: design tokens, theme surfaces, - brand assets, shell layout, browse/highlight surfaces, reader right rail, - TOC behaviour, wax controls, and cross-device chrome are documented under - [design system](../references/design-system/INDEX.md). -- Browse and reader performance baselines include cursor-paginated memories, - lazy Flashback loading, scoped browse revalidation, and reader All-tab - deferral. -- Translation UI baselines include shared translucent popover chrome, persisted - Codex model/effort defaults, outside-cancel translation popover behavior, and - existing translation start/progress integration checks. -- `/memories` supports vim-like browse keybindings for row selection, search - focus, and selected-memory opening while preserving text-input behavior. -- Reader TOC behavior includes live reading-progress visualization through - `MemoryReader`-owned reactive state. - -## Branching - -Use concise branch names that match the workflow: - -- `feat/importer-add-memory` -- `feat/browse-shell` -- `feat/reader-pipeline` -- `feat/highlights` -- `feat/git-backup-queue` -- `test/e2e-hardening` -- `fix/dev-server-startup` -- `test/test-suite-health` -- `ci/docs-health` -- `chore/reader-library-decision` -- `refactor/markdown-reader` -- `chore/refactor-wave-integration` -- `feat/brilliant` -- `feat/lazy-loading` -- `fix/anything` -- `feat/vim-like-key-bind` -- `feat/psychiatrist` +Completed task narratives, worker handoffs, commit lists, review transcripts, +and superseded file-by-file plans belong in Git history. The concise +[historical workflow index](archive/README.md) maps completed task families to +their current semantic owners; it is not an implementation source. diff --git a/docs/workflows/archive/.gitkeep b/docs/workflows/archive/.gitkeep deleted file mode 100644 index 476c75b8..00000000 --- a/docs/workflows/archive/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Intentionally kept so Git tracks this otherwise empty directory. diff --git a/docs/workflows/archive/README.md b/docs/workflows/archive/README.md new file mode 100644 index 00000000..b7ba55f7 --- /dev/null +++ b/docs/workflows/archive/README.md @@ -0,0 +1,16 @@ +# Historical Workflow Index + +Detailed completed and superseded execution plans were removed from the live +documentation tree. Git history remains the complete record. Do not implement +from historical workflow text; use the current semantic owners below. + +| Task family | Current semantic owner | +| --- | --- | +| Runtime startup stabilization | [Local/self-hosting](../../operations/local-self-hosting.md) and [Verification](../../quality/verification.md) | +| UI routing, shell, memory actions, read state | [UI and routing](../../architecture/ui-and-routing.md), [Data and storage](../../architecture/data-and-storage.md), and [Design system](../../references/design-system/INDEX.md) | +| Brilliant translation and repairs | [Runtime flows](../../architecture/flows.md#brilliant-translation), [Data and storage](../../architecture/data-and-storage.md#translation-storage), and [Configuration](../../references/configuration.md#codex-app-server-environment) | +| Lazy browse/reader loading | [UI and routing](../../architecture/ui-and-routing.md#browse-query-state) and [Verification](../../quality/verification.md) | +| Popovers, translation controls, responsive shell | [Design system](../../references/design-system/INDEX.md) and [UI and routing](../../architecture/ui-and-routing.md) | +| Browse keyboard navigation | [Interaction and accessibility](../../references/design-system/interaction-and-accessibility.md) | +| Reader ToC progress and Moments | [Reader and content](../../references/design-system/reader-and-content.md) and [Runtime flows](../../architecture/flows.md#moment-toggle) | +| Psychiatrist assistant and hardening | [Runtime flows](../../architecture/flows.md#psychiatrist), [Data and storage](../../architecture/data-and-storage.md#psychiatrist-thread-store), [Security rules](../../references/coding-standards/security-boundaries.md), and the [repo-local Psychiatrist skill](../../../.agents/skills/psychiatrist/SKILL.md) | diff --git a/docs/workflows/archive/task-10-runtime-dev-server-stabilization.md b/docs/workflows/archive/task-10-runtime-dev-server-stabilization.md deleted file mode 100644 index c008238a..00000000 --- a/docs/workflows/archive/task-10-runtime-dev-server-stabilization.md +++ /dev/null @@ -1,108 +0,0 @@ -# Task 10: Runtime Dev Server Stabilization Workflow - -> Archived execution plan. The startup contract described here has landed on -> `main`; use current package scripts, operations docs, and tests for active -> implementation decisions. - -## Goal - -Make local development startup reliable. `bun run dev` must start the app -without exit code 1 on a clean checkout with the pinned toolchain. - -## Required Context - -- [Technology stack](../../references/technology-stack.md) -- [Local/self-hosting operation](../../operations/local-self-hosting.md) -- [Verification strategy](../../quality/verification.md) -- [Coding standards](../../references/coding-standards/INDEX.md) - -## Current Failure To Reproduce - -Run: - -```bash -mise exec -- bun run dev -``` - -Observed failure: - -```text -Unable to find a random port on any host -error: script "dev" exited with code 1 -``` - -## Ownership - -Primary files and directories: - -- `package.json` -- `app.config.ts` -- `mise.toml` -- `playwright.config.ts` -- Dev/server scripts under `scripts/**` if introduced. -- Runtime startup tests or smoke checks under `tests/**` or `e2e/**`. - -Do not refactor reader, importer, database repositories, or UI layout in this -task unless the dev startup failure is directly caused by those surfaces. - -## Implementation Steps - -1. Capture the startup failure. - - Run `mise exec -- bun run dev`. - - Record the exact exit code and stack trace in the PR notes. - - Check whether the failure depends on occupied ports, host binding, sandbox - permissions, or Vinxi random-port behavior. - -2. Define the local dev server contract. - - Choose explicit default host and port behavior. - - Prefer deterministic configuration over random-port discovery. - - Keep the dev contract compatible with Playwright and local browser testing. - -3. Implement the smallest startup fix. - - Update SolidStart/Vinxi config or package scripts as needed. - - If a helper script is added, keep it thin and deterministic. - - Do not hide startup failures by swallowing errors. - -4. Add a startup smoke check. - - Add a command that starts the dev or preview server with explicit host and - port, probes `/memories`, then shuts down cleanly. - - The check must fail if the server cannot bind or exits early. - - Avoid depending on a long-lived background process in CI. - -5. Update docs only for the new contract. - - Document exact local dev commands. - - Document any `HOST` or `PORT` environment variables if they become part of - the contract. - -## Acceptance Criteria - -- `mise exec -- bun run dev` no longer exits with code 1 on a clean checkout. -- The selected host/port behavior is deterministic and documented. -- A smoke check catches early startup crashes. -- Existing verification still passes. - -## Verification - -Run: - -```bash -mise exec -- bun run dev -bun run typecheck -bun run test -bun run build -``` - -Run E2E only after the startup contract is stable: - -```bash -bun run test:e2e -``` - -## PR Handoff - -The PR description must include: - -- Original startup failure output. -- Final startup command and URL. -- Any host/port contract changes. -- Exact verification commands and outcomes. diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh.md deleted file mode 100644 index b3cfada3..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh.md +++ /dev/null @@ -1,115 +0,0 @@ -# Task 18-alpha: UI component and routing refresh - -## Status - -- State: Ready for implementation planning handoff -- Base branch: `workflow18-read-status` -- Implementation branch: `refine/ui-routing-refresh` -- Execution model: implement in the subtask order below. Independent subtasks - may be delegated to separate workers only when their owned files do not - overlap. -- Out of scope: database schema changes, importer/extractor changes, backup - semantics, reader markdown rendering changes, broad route redesign, and any - route that is not explicitly named in a subtask. - -## Planning boundary - -This document and its subtask files may be edited during planning. Source code, -tests, and design-system docs must not be changed from a planning prompt unless -the user explicitly moves the branch into implementation by saying `go`. Until -that `go` instruction is given, only define or refine the workflow exec plan. If -a planning conversation surfaces a concrete fix, record the requirement in the -owning subtask first. Only keep an implementation diff from the planning phase -when the user explicitly says to keep that already-applied fix; otherwise revert -it and leave only the workflow-plan update. - -## Core intent - -Task 18-alpha refreshes shared UI foundations that became fragmented during -Task 18: - -- one shared taxonomy rendering primitive for memory-row chips and right-rail - category/tag filter rows -- one shared popup shell for Add memory, Theme, and general action menus -- a unified menu-popup path for memory and Moment action menus -- composer and Theme popovers using the same popup shell and background surface - contract as the action menus -- route-surface alignment so the refreshed components behave consistently on - `/memories`, `/memories/:id`, `/flashbacks`, `/moments`, and existing shell - navigation surfaces - -This is primarily a UI architecture cleanup. It must preserve the current -desktop design, mobile/tab behaviour, product language, and Task 18 memory -actions. - -## Global invariants - -- Do not change SQLite schema or migration files. -- Do not change memory deletion, backup, import, Flashback, or Moment - persistence semantics. -- Use current product language: `Flashback` is a text marker and `Moment` is a - section bookmark. -- Keep `ThemeBlock`'s segmented toggle buttons as the special-case selector UI. - The popup shell around Theme may be shared; the theme selector itself remains - domain-specific. -- Add memory, Theme, memory action menus, and Moment action menus share the same - popup outside-click, Escape, z-index, animation, and background-surface - contract. -- Popup content remains pluggable. The shared shell owns chrome and interaction; - menu, composer, and Theme content own their internal form/menu layout. -- The common popup background should follow the current action-menu visual - direction: semantic app background/elevated surface tokens, current theme - texture continuity, `rounded-[20px]`, border token, and bounded shadow. -- Do not use global CSS selectors where a focused Solid component contract can - express the behaviour. -- Existing tests that inspect source strings should either be replaced with - behavioural tests or updated to assert the new component boundaries. -- Update design-system docs only for durable component contracts, not for - implementation diary details. - -## Subtask execution order - -1. [18-alpha.1 Taxonomy rendering consolidation](task-18-alpha-ui-routing-refresh/01-taxonomy-rendering-consolidation.md) -2. [18-alpha.2 Shared popup shell foundation](task-18-alpha-ui-routing-refresh/02-shared-popup-shell-foundation.md) -3. [18-alpha.3 General action menu migration](task-18-alpha-ui-routing-refresh/03-general-action-menu-migration.md) -4. [18-alpha.4 Composer and Theme popup migration](task-18-alpha-ui-routing-refresh/04-composer-theme-popup-migration.md) -5. [18-alpha.5 Route surface alignment](task-18-alpha-ui-routing-refresh/05-route-surface-alignment.md) -6. [18-alpha.6 Integration verification and design docs](task-18-alpha-ui-routing-refresh/06-integration-verification-and-design-docs.md) - -## Dependency and parallelisation guidance - -- `18-alpha.1` can run in parallel with `18-alpha.2`; taxonomy rendering and - popup shell foundation do not need to touch the same files except tests. -- `18-alpha.3` depends on `18-alpha.2`. -- `18-alpha.4` depends on `18-alpha.2` and should wait until popup shell API is - stable. -- `18-alpha.5` depends on `18-alpha.1`, `18-alpha.3`, and `18-alpha.4`, because - route surfaces should only be aligned after shared components exist. -- `18-alpha.6` is the final integration gate and must run after all prior - subtasks. - -When using subagents, assign disjoint ownership: - -- Worker A may own `18-alpha.1` taxonomy components and tests. -- Worker B may own `18-alpha.2` popup shell component and tests. -- The main rollout should integrate `18-alpha.3` and `18-alpha.4` after Worker B - lands, because these touch shared shell and menu consumers. - -## Verification baseline - -Every implementation slice must run its targeted tests and `mise exec -- bun run -typecheck`. The final integration slice must run: - -```sh -git diff --check -mise exec -- bun run verify -mise exec -- bun run test:e2e -``` - -If local browser verification is used for screenshots, check at least: - -- `/memories` -- `/memories/:id` with right rail and reader menus -- `/flashbacks` -- `/moments` -- phone-width bottom tabs with Add memory and Theme popovers diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/01-taxonomy-rendering-consolidation.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/01-taxonomy-rendering-consolidation.md deleted file mode 100644 index 9f89fba0..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/01-taxonomy-rendering-consolidation.md +++ /dev/null @@ -1,89 +0,0 @@ -# 18-alpha.1 Taxonomy rendering consolidation - -## Goal - -Create a shared taxonomy rendering primitive so memory-row tags/categories and -right-rail category/tag filters are rendered from one component family instead -of separate local markup. - -## Files likely owned - -- Create: `src/components/taxonomy/TaxonomyList.tsx` -- Create: `tests/components/taxonomy-list.test.tsx` -- Modify: `src/components/memories/MemoryBrowse.tsx` -- Modify: `src/components/reader/MemoryReader.tsx` -- Modify: `src/components/shell/AppShell.tsx` -- Optional modify: `src/components/memories/browse-data.ts` -- Optional modify: `tests/components/memory-reader-actions.test.ts` - -## Component contract - -Create a component family with one data shape and two display modes: - -```ts -export interface TaxonomyListItem { - id: string; - name: string; - memoryCount?: number; -} - -export interface TaxonomyListProps { - activeId?: string; - emptyLabel?: string; - items: readonly TaxonomyListItem[]; - kind: "category" | "tag"; - mode: "chips" | "filters"; - onSelect?: (item: TaxonomyListItem) => void; -} -``` - -Rules: - -- `mode="chips"` is used by memory-row metadata and reader memory taxonomy - chips. Memory page main content must use this same chip mode for the memory's - attached categories and tags below the title. -- `mode="filters"` is used by the right rail category/tag sections. -- Counts render only when `memoryCount` is defined. -- Filter mode uses `aria-pressed` when `onSelect` is present. -- Chip mode is non-interactive unless `onSelect` is provided. -- Empty state text is supplied by the consumer, so category and tag wording can - stay route-specific. -- The component must not know about browse query keys. Parents own routing and - filter state. - -## Implementation steps - -1. Add `TaxonomyList` tests first: - - chip mode renders names without counts when `memoryCount` is absent - - filter mode renders counts when provided - - active filter uses `aria-pressed="true"` - - empty list renders the provided empty label - - click calls `onSelect` with the selected item -2. Implement `TaxonomyList`. -3. Replace memory-row tag/category rendering in `MemoryBrowse.tsx`. -4. Replace memory page reader taxonomy chip rendering in `MemoryReader.tsx` - with `TaxonomyList mode="chips"` so the memory page and memories browse - route share one taxonomy-chip design. -5. Replace `TaxonomyFilterButton` usage in `AppShell.tsx` right rail with - `TaxonomyList`. -6. Remove local taxonomy rendering helpers that become unused. - -## Tests - -```sh -mise exec -- bun --bun x vitest run tests/components/taxonomy-list.test.tsx -mise exec -- bun --bun x vitest run tests/components/app-shell-taxonomy.test.ts tests/components/memory-browse-actions.test.ts -mise exec -- bun --bun x vitest run tests/components/memory-reader-actions.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Memory-row taxonomy and right-rail taxonomy render through the shared - taxonomy component family. -- Right-rail category/tag filtering keeps current query behaviour. -- Memory-row taxonomy keeps current visual density and does not become a route - filter by accident. -- Memory page taxonomy chips below the title match the memories browse chip - design and come from the same shared taxonomy component. -- No data loading or API behaviour changes are introduced. diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/02-shared-popup-shell-foundation.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/02-shared-popup-shell-foundation.md deleted file mode 100644 index d443259a..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/02-shared-popup-shell-foundation.md +++ /dev/null @@ -1,84 +0,0 @@ -# 18-alpha.2 Shared popup shell foundation - -## Goal - -Create one shared popup shell that owns popup chrome, anchoring, close -behaviour, layer, animation, and background surface. It must allow different -content bodies: general menu, Add memory composer, and Theme controls. - -## Files likely owned - -- Create: `src/components/ui/Popup.tsx` -- Create: `tests/components/popup.test.tsx` -- Modify: `src/components/ui/KebabActionMenu.tsx` only if needed to consume the - shared shell in the same slice -- Optional modify: `src/styles/tailwind.css` only for a reusable utility class - that cannot stay inside the component - -## Component contract - -The shared component should expose controlled content without owning domain -state: - -```ts -export interface PopupControls { - close: () => void; - open: boolean; -} - -export interface PopupProps { - children: (controls: PopupControls) => JSX.Element; - class?: string; - disabled?: boolean; - id: string; - initialOpen?: boolean; - label: string; - mode?: "dialog" | "menu"; - panelClass?: string; - phonePanel?: boolean; - placement?: "bottom-start" | "bottom-end" | "top-start" | "top-end"; - trigger: (controls: PopupControls & { toggle: () => void }) => JSX.Element; - onClose?: () => void; - onOpenChange?: (open: boolean) => void; -} -``` - -Rules: - -- The component owns outside pointer close. -- Escape closes the popup. -- It renders `aria-controls`, `aria-expanded`, and `aria-haspopup` through - trigger helpers or documented trigger props. -- The panel uses the common popup visual recipe: - `rounded-[20px]`, `border-trauma-border`, `bg-trauma-bg-elev`, app-theme - surface continuity, `shadow-trauma-2`, and `animate-trauma-pop-bounce`. -- `mode="menu"` renders `role="menu"`; `mode="dialog"` renders - `role="dialog"`. -- The component must not hard-code Kebab, Theme, or Add memory labels. -- It must work when nested inside the left rail, phone bottom tabs, and route - cards without clipping under the main pane. - -## Implementation steps - -1. Add tests for open/close, Escape, outside pointer, role selection, and - trigger state. -2. Implement `Popup`. -3. Confirm the component can render arbitrary children without importing memory, - shell, or theme modules. -4. Keep existing consumers unchanged in this subtask unless a tiny integration - is required to prove the shell. - -## Tests - -```sh -mise exec -- bun --bun x vitest run tests/components/popup.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- There is exactly one popup shell component for future popup migrations. -- Popup chrome and interaction rules are implemented once. -- The component is content-agnostic and can host menu, composer, and Theme - content without domain imports. - diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/03-general-action-menu-migration.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/03-general-action-menu-migration.md deleted file mode 100644 index de6bd61a..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/03-general-action-menu-migration.md +++ /dev/null @@ -1,79 +0,0 @@ -# 18-alpha.3 General action menu migration - -## Goal - -Migrate general action menus to the shared popup shell while preserving their -current behaviour and design. General action menus are the memory, Moment, and -Flashback meatballs menus, not the special Theme selector UI. - -## Files likely owned - -- Modify: `src/components/ui/KebabActionMenu.tsx` -- Modify: `src/components/memories/MemoryActionMenu.tsx` -- Modify: `src/components/moments/MomentActionMenu.tsx` -- Create: `src/components/flashbacks/FlashbackActionMenu.tsx` -- Modify: `tests/components/memory-action-menu.test.ts` -- Modify: `tests/components/moment-action-menu.test.ts` -- Create: `tests/components/flashback-action-menu.test.ts` -- Optional create: `src/components/ui/ActionMenu.tsx` -- Optional create: `tests/components/action-menu.test.ts` - -## Behaviour contract - -- Memory, Moment, and Flashback menus share the same popup shell. -- The trigger remains a meatballs/kebab button with a visible hover/focus state. -- `role="menu"` and item semantics are preserved. -- Menu item styling remains centralized: - - min touch target height - - icon/text grid - - theme-aware hover background - - error message class for failed actions -- Delete actions rendered from a general action menu must use a shared danger - item style: - - label pattern: `Delete memory`, `Delete moment`, or `Delete {domain}` - - text/icon colour: `text-trauma-danger` - - icon: shared trash icon from the app icon set, not a text hyphen - - hover/focus affordance remains visible and theme-aware -- Memory menu keeps: - - delete confirmation - - category popover entry - - backup-failsafe revalidation behaviour from parent handlers -- Moment menu keeps: - - delete action - - route navigation behaviour in `/moments` -- Flashback menu keeps: - - `Delete flashback` as its only first-pass menu item - - the shared danger item style and trash icon - - deletion wired through the existing Flashback removal/toggle mutation path - - no new API route or persistence semantics unless a later workflow update - explicitly permits that change -- Menu content must not gain composer or Theme-specific styling. - -## Implementation steps - -1. Add or update tests that assert both memory and Moment menus render through - the shared popup shell or shared action-menu wrapper. -2. Refactor `KebabActionMenu` to use `Popup`, or replace it with a thin - `ActionMenu` wrapper around `Popup`. -3. Migrate `MemoryActionMenu`. -4. Migrate `MomentActionMenu`. -5. Add or migrate a Flashback action menu for `/flashbacks` rows. -6. Remove duplicate menu panel class definitions that are no longer needed. - -## Tests - -```sh -mise exec -- bun --bun x vitest run tests/components/memory-action-menu.test.ts tests/components/moment-action-menu.test.ts -mise exec -- bun --bun x vitest run tests/components/flashback-action-menu.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- There is one general action-menu popup path. -- Memory, Moment, and Flashback menus still work from their current routes. -- Hover/focus affordance remains visible on browse cards and reader header - menus. -- Delete menu items use the shared danger style and trash icon across memory, - Moment, and Flashback menus. -- No Theme or composer internals leak into general menu components. diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/04-composer-theme-popup-migration.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/04-composer-theme-popup-migration.md deleted file mode 100644 index 8ffd8e47..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/04-composer-theme-popup-migration.md +++ /dev/null @@ -1,63 +0,0 @@ -# 18-alpha.4 Composer and Theme popup migration - -## Goal - -Migrate Add memory composer and Theme popovers to the shared popup shell while -keeping their domain-specific content components intact. - -## Files likely owned - -- Modify: `src/components/shell/AppShell.tsx` -- Modify: `src/components/memories/AddMemoryForm.tsx` only if class props need - a cleaner boundary -- Modify: `tests/components/app-shell.test.ts` -- Modify: `tests/components/add-memory-form.test.ts` only if form class - boundaries change -- Optional create: `src/components/shell/ShellPopovers.tsx` if extracting from - `AppShell.tsx` reduces file weight without changing shell behaviour - -## Behaviour contract - -- `AddMemoryComposerButton` uses the shared popup shell for: - - rail mode - - compact rail mode - - phone bottom-tab mode -- `ThemeNavButton` uses the same popup shell for: - - rail mode - - phone bottom-tab mode -- `ThemeBlock` keeps its segmented toggle buttons and theme-specific icon - rules. It is not converted into a generic menu. -- Composer content keeps `AddMemoryForm` and the existing `POST /api/memories` - flow. -- Both popovers inherit the common action-menu background surface recipe rather - than using a separate bespoke panel background. -- Popovers must render above rail, main pane, and phone bottom bar. -- Opening state must apply the existing selected/pressed visual treatment to - the trigger. - -## Implementation steps - -1. Update shell tests to assert Add memory and Theme use the shared popup shell - contract. -2. Replace duplicated outside-pointer and Escape handling in - `AddMemoryComposerButton` and `ThemeNavButton` with `Popup`. -3. Move only small shell-local constants if required for readability. -4. Keep `ThemeBlock` content unchanged except for panel wrapper removal. -5. Keep `AddMemoryForm` submit behaviour unchanged. - -## Tests - -```sh -mise exec -- bun --bun x vitest run tests/components/app-shell.test.ts tests/components/mobile-responsive-contract.test.ts -mise exec -- bun --bun x vitest run tests/components/add-memory-form.test.ts tests/components/add-memory-submit.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Add memory and Theme popovers share the same popup shell and panel surface. -- Theme selector remains visually and behaviourally distinct inside the shared - shell. -- Phone and compact rail layouts do not clip the popovers. -- Existing theme persistence and add-memory creation behaviour remain intact. - diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/05-route-surface-alignment.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/05-route-surface-alignment.md deleted file mode 100644 index bcfb4203..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/05-route-surface-alignment.md +++ /dev/null @@ -1,269 +0,0 @@ -# 18-alpha.5 Route surface alignment - -## Goal - -Align route surfaces with the refreshed shared components and define the limited -routing scope for this branch. This subtask does not create broad new route -features; it ensures existing route surfaces use the new UI foundations -consistently. - -## Files likely owned - -- Modify: `src/components/shell/AppShell.tsx` -- Modify: `src/components/memories/MemoryBrowse.tsx` -- Modify: `src/components/memories/browse-data.ts` -- Modify: `src/components/memories/MemoryReadStatusControl.tsx` -- Modify: `src/components/reader/MemoryReader.tsx` -- Modify: `src/components/icons/TraumaIcons.tsx` -- Modify: `src/components/flashbacks/FlashbackExcerpt.tsx` -- Modify: `src/components/flashbacks/FlashbackShortcutList.tsx` -- Modify: `src/routes/memories/index.tsx` -- Modify: `src/routes/memories/[id].tsx` -- Modify: `src/routes/flashbacks/index.tsx` -- Modify: `src/routes/moments/index.tsx` -- Modify: `tests/components/app-shell.test.ts` -- Modify: `tests/components/mobile-responsive-contract.test.ts` -- Modify: `tests/components/memory-browse-actions.test.ts` -- Modify: `tests/memories/browse-data.test.ts` -- Modify: `tests/components/memory-read-status.test.ts` -- Modify: `tests/components/memory-reader-actions.test.ts` -- Modify: `tests/components/flashbacks-route-state.test.ts` -- Create: `tests/components/flashback-excerpt.test.ts` -- Modify: `tests/components/trauma-icons.test.ts` -- Optional modify: `docs/references/design-system/interaction-and-accessibility.md` - if route behaviour changes. - -## Routing scope - -Allowed route work in this branch: - -- ensure refreshed popup and taxonomy components behave across existing routes - `/memories`, `/memories/:id`, `/flashbacks`, `/moments`, and `/settings` -- keep `/memories` query filters for search, category, tag, Flashback, and view - mode working after taxonomy component consolidation -- extend the `/memories` search grammar while keeping the search input synced to - the URL `q` query parameter: - - the search bar value is exactly `parseBrowseQuery(location.search).q` - - typing in the search bar writes the raw input back to `?q=...` using the - existing replace-navigation pattern so browser history is not polluted per - keystroke - - do not add new top-level URL query keys for fielded search; field filters - live inside `q` - - unqualified terms remain broad free-text search over title, URL, - categories, tags, and Flashback text/context - - fielded syntax supports `title:{some title}`, `url:{example.com}`, - `tag:{sqlite}`, `category:{research}`, and `flashback:{selected text}` - - braced values may contain spaces and are trimmed before matching - - non-braced field syntax may be accepted for single-token values, such as - `tag:sqlite` or `url:example.com` - - read-state filters are `read` and `unread` as standalone query tokens - - field filters, read-state filters, explicit right-rail filters, and - unqualified free-text terms combine with AND semantics - - if both `read` and `unread` appear, the result is empty because the query - asks for mutually exclusive states -- keep the `/memories` main-pane search field focus indicator aligned with the - rounded search surface. When the search input is focused, the visible - indicator must fit the search bar outline and rounded corners, matching the - focus treatment used by the New tag/New category popup input fields. -- keep product-language labels consistent across surfaces: the memories right - rail title must use `Flashbacks`, matching the memory page right rail and - shell navigation tab label -- align read-status rendering on `/memories` and `/memories/:id`: - - move the read-status control from the lower-right memory-card footer to the - action cluster immediately left of the meatballs menu button - - apply the same action-cluster placement in memory reader mode - - render icon-only visible UI, with no visible `Read`, `Unread`, `Mark read`, - or `Mark unread` label text - - preserve an accessible button name with `aria-label` so icon-only rendering - does not remove screen-reader context - - use an open-eye icon for `read: false` - - use a closed-eye icon for `read: true` - - clicking the icon toggles read status using the existing read-status API and - optimistic/error behaviour -- restructure the memory page main pane: - - the route-local sticky header renders only the previous/back button and the - `Memory` label - - the header must not render title text, URL/source link, read-status button, - taxonomy chips, or meatballs menu actions - - the main pane content begins with the memory title as the first primary - content element - - render the memory's attached category/tag chips directly below the title, - using the shared taxonomy chip component from `18-alpha.1` - - move the URL/source display above the title - - render the URL/source display and button icon group in the same row - - move the button icon group, including read-status and meatballs menu, to the - upper-right of the title block/main content intro rather than the sticky - route header - - keep the title/content hierarchy readable: URL/action row first, title - second, taxonomy chips third, markdown reader body after that -- align Flashback excerpt rendering across `/flashbacks` and the right pane: - - render the persisted Flashback string at normal readable text contrast - - render the stored prefix/suffix context before and after the Flashback - string - - render prefix/suffix context at lower contrast than the Flashback string so - the selected text remains the focal content - - do not render the Flashback text as ordinary full-row body copy without - context distinction - - keep the right pane Flashback component bounded as an independent scroll - region when its content overflows - - do not apply a TOC-style component-level fade to Flashback lists; the - visual effect belongs to the stored prefix/suffix context spans for each - Flashback item - - reuse a shared Flashback excerpt/list primitive where practical so - `/flashbacks` and right pane Flashbacks do not drift visually -- align `/flashbacks` page cards: - - use the same Flashback string plus lower-contrast context rendering - - render the source memory title as supplemental metadata at the bottom-left - of the card - - make the source title smaller and visually subordinate to the Flashback - string - - remove the current `Source memory` label because the title context is - self-evident - - render a meatballs menu button for each Flashback card - - the first-pass menu contains `Delete flashback` -- keep root redirect to `/memories` -- keep disabled shell controls disabled unless their route already exists and - has a working page contract - -Blocked route work in this branch: - -- adding new public `/categories` or `/tags` pages -- changing API routes -- changing browse query key names -- adding a filter drawer or navigation drawer -- turning right-rail filter components into primary route pages - -If the implementation discovers that a new route is required, stop and update -this workflow before coding the route. - -## Implementation steps - -1. Run the focused route/source tests before changes to identify current - assertions. -2. Update route surfaces to import the new shared taxonomy and popup-backed menu - components where needed. -3. Verify category/tag filter query toggles still preserve unrelated query - state. -4. Add browse-data parser/filter tests for the expanded search grammar before - changing implementation: - - `parseBrowseQuery("?q=title%3A%7Breader+mode%7D")` preserves the raw - search string as `q` - - `filterBrowseMemories(..., parseBrowseQuery("?q=title:{...}"))` filters by - title only - - `url:{...}`, `tag:{...}`, `category:{...}`, and `flashback:{...}` target - their own fields without matching unrelated fields - - `read` matches only `read: true`; `unread` matches only `read: false` - - `read unread` yields no results - - free-text terms still perform broad search across title, URL, taxonomy, - and Flashback text/context - - field filters combine with explicit `category=`, `tag=`, and `flashback=` - query parameters using AND semantics -5. Implement a small query-token parser in `browse-data.ts`. Keep it pure and - deterministic; do not parse fielded search in `MemoryBrowse.tsx`. -6. Update `filterBrowseMemories` so all search modes are applied in the shared - browse-data layer, not in UI components. -7. Verify the search input continues to read from `query().q` and write raw - input through `updateQuery({ q: value }, { replace: true })`. -8. Add or update a focused component/source contract test for the memory search - focus surface before changing the implementation. The test should assert that - the rounded search container owns the focus indicator, using an inset ring or - equivalent rounded-corner-safe style rather than relying on the inner - transparent input outline. -9. Update the `/memories` search surface style only after the test fails for the - current implementation. -10. Add or update icon tests for the shared open-eye and closed-eye icons before - wiring them into read status controls. -11. Add or update read-status component tests so the control renders icon-only - visible content while retaining an accessible name and preserving toggle - request behaviour. -12. Update `MemoryReadStatusControl` so it can render the icon-only action - variant required by browse cards and reader headers. Do not duplicate read - status request logic in route components. -13. Update `/memories` memory cards so the read-status toggle sits immediately - left of the meatballs menu button in the header action cluster, and remove - the lower-right footer placement. -14. Update memory reader mode so the same icon-only read-status toggle sits - immediately left of its meatballs menu button. -15. Add or update memory reader tests for the main pane structure before - changing reader markup: - - sticky reader header contains previous/back control and `Memory` - - sticky reader header does not contain the memory title - - URL/source link and reader action icons render in the same intro row above - the title - - title renders as the first primary content heading in the main pane intro - - category/tag chips render below title using the shared taxonomy chip path -16. Update memory reader markup to match the new intro/header split. Keep data - loading, read-status mutation, delete mutation, Flashback, and Moment - behaviour unchanged. -17. Add or update Flashback excerpt/list tests before changing Flashback route - or right pane markup: - - prefix and suffix context render around the selected Flashback text - - selected Flashback text uses normal text contrast - - prefix/suffix context uses lower-contrast text tokens plus the shared - Flashback context blur/mask treatment - - right pane Flashback lists do not expose component-level TOC fade hooks; - the blur belongs to each item context span - - `/flashbacks` row renders source memory title at the bottom-left without - a `Source memory` label - - `/flashbacks` row renders a meatballs menu with `Delete flashback` -18. Use `FlashbackInlineText` as the shared Flashback text primitive so the - route and right pane share context rendering rules. -19. Update `/flashbacks` rows to use the shared inline rendering, subordinate - source title metadata, and Flashback action menu. -20. Wire `Delete flashback` only through the existing Flashback removal/toggle - mutation path. If the current backend cannot delete by Flashback id without - changing API routes or persistence semantics, stop and update this workflow - before implementing an API change. -21. Verify memory-row navigation still ignores nested interactive controls, - including the moved read-status button. -22. Verify `/moments` action menu delete still does not break navigation to a - memory section. - -## Tests - -```sh -mise exec -- bun --bun x vitest run tests/memories/browse-data.test.ts -mise exec -- bun --bun x vitest run tests/components/app-shell.test.ts tests/components/app-shell-taxonomy.test.ts -mise exec -- bun --bun x vitest run tests/components/memory-browse-actions.test.ts tests/components/memory-action-menu.test.ts tests/components/moment-action-menu.test.ts -mise exec -- bun --bun x vitest run tests/components/memory-read-status.test.ts tests/components/memory-reader-actions.test.ts tests/components/trauma-icons.test.ts -mise exec -- bun --bun x vitest run tests/components/flashback-excerpt.test.ts tests/components/flashbacks-route-state.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Existing route behaviour survives the component refresh. -- `/memories` filter state remains URL-query based. -- `/memories` search supports fielded `title:`, `url:`, `tag:`, - `category:`, `flashback:`, and standalone `read`/`unread` filters inside the - existing `q` parameter. -- The search bar input and URL `?q=` stay synchronized with the raw user input. -- Fielded search, free-text search, explicit right-rail filters, and read-state - filters combine predictably with AND semantics. -- The `/memories` search bar focus indicator fits the rounded outer search - surface and no longer renders as a corner-mismatched inner outline. -- Read-status controls render as icon-only buttons immediately left of the - meatballs menu on both `/memories` cards and `/memories/:id` reader mode. -- Read-status icon semantics are consistent: open eye means unread - (`read: false`), closed eye means read (`read: true`). -- Icon-only read-status buttons keep accessible names and continue to toggle - through the existing read-status API. -- Memory page sticky header renders only previous/back navigation and the - `Memory` label. -- Memory page main pane intro renders URL/source and action icons in one row - above the title, then taxonomy chips below the title. -- Memory page action icons no longer live in the sticky route header. -- Memory page taxonomy chips reuse the same shared taxonomy chip design as - memories browse rows. -- Flashback excerpts on `/flashbacks` and in the right pane render selected text - at normal contrast with lower-contrast, subtly blurred prefix/suffix context - around it. -- Flashback right pane lists use per-item context blur/mask styling rather than - component-level scroll-edge overlays. -- `/flashbacks` rows render source memory title as small bottom-left metadata - and no longer render a `Source memory` label. -- `/flashbacks` rows provide a meatballs menu with `Delete flashback` using - the shared action-menu danger style. -- Right-rail Flashback list headings use the plural `Flashbacks` everywhere. -- No new public route appears without an explicit workflow update. -- Disabled future shell controls remain visually and semantically disabled. diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/06-integration-verification-and-design-docs.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/06-integration-verification-and-design-docs.md deleted file mode 100644 index 96e9f3a9..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/06-integration-verification-and-design-docs.md +++ /dev/null @@ -1,62 +0,0 @@ -# 18-alpha.6 Integration verification and design docs - -## Goal - -Run the final integration gate, remove stale design-system wording, and record -the durable component contracts that emerged from implementation. - -## Files likely owned - -- Modify: `docs/references/design-system/components-and-surfaces.md` -- Modify: `docs/references/design-system/interaction-and-accessibility.md` -- Modify: `docs/references/design-system/layout-and-shell.md` only if layer or - shell overflow rules changed. -- Modify: `docs/workflows/task-18-alpha-ui-routing-refresh.md` only if the - completed implementation changes the execution record. - -## Documentation rules - -- Document semantic component rules, not PR history. -- Keep `AGENTS.md` unchanged unless a new navigation pointer is required. -- Do not duplicate exact class strings unless the class is part of a durable - public component contract. -- Remove stale wording that says Theme/Add memory/action menus use separate - popup implementations after the shared popup shell lands. -- Keep route behaviour docs aligned with the actual route list. - -## Verification commands - -Run all commands from the repository root: - -```sh -git diff --check -mise exec -- bun --bun x vitest run tests/components/popup.test.tsx tests/components/taxonomy-list.test.tsx -mise exec -- bun --bun x vitest run tests/components/app-shell.test.ts tests/components/mobile-responsive-contract.test.ts -mise exec -- bun --bun x vitest run tests/components/memory-action-menu.test.ts tests/components/moment-action-menu.test.ts tests/components/flashback-action-menu.test.ts -mise exec -- bun --bun x vitest run tests/components/flashback-excerpt.test.ts tests/components/flashbacks-route-state.test.ts -mise exec -- bun run verify -mise exec -- bun run test:e2e -``` - -If local Playwright cannot run because of browser or macOS sandbox limits, -record the exact failure and run the highest available local verification -instead. Do not claim E2E coverage passed unless the command exits 0. - -## Visual checks - -When a local dev server is available, inspect: - -- `/memories`: memory-row taxonomy chips and right-rail filters -- `/memories/:id`: reader action menu, Theme popup from shell, Add memory popup -- `/flashbacks`: shell, popup layering, Flashback context contrast, source - title metadata, and `Delete flashback` menu -- `/moments`: Moment action menu -- phone width: bottom tab Add memory and Theme popovers - -## Acceptance criteria - -- Final `bun run verify` passes. -- E2E either passes or a concrete environment limitation is recorded. -- Design-system docs describe the shared popup shell and taxonomy rendering - contract. -- Workflow docs contain no stale one-off review diary. diff --git a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/README.md b/docs/workflows/archive/task-18-alpha-ui-routing-refresh/README.md deleted file mode 100644 index 72acb3ef..00000000 --- a/docs/workflows/archive/task-18-alpha-ui-routing-refresh/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Task 18-alpha subtasks - -Implement these subtasks on `refine/ui-routing-refresh`, based on -`workflow18-read-status`. - -## Order - -1. [18-alpha.1 Taxonomy rendering consolidation](01-taxonomy-rendering-consolidation.md) -2. [18-alpha.2 Shared popup shell foundation](02-shared-popup-shell-foundation.md) -3. [18-alpha.3 General action menu migration](03-general-action-menu-migration.md) -4. [18-alpha.4 Composer and Theme popup migration](04-composer-theme-popup-migration.md) -5. [18-alpha.5 Route surface alignment](05-route-surface-alignment.md) -6. [18-alpha.6 Integration verification and design docs](06-integration-verification-and-design-docs.md) - -## Rules for agents - -- Keep UI component contracts reusable and narrow. -- Do not change data persistence or backup behaviour. -- Do not add a new public route unless the subtask explicitly names it. -- Preserve current desktop visual design while cleaning duplicated components. -- During planning, edit only workflow files. Do not change source code, tests, - or design-system docs until the user explicitly says `go`. -- Use shared popup chrome for popup surfaces; keep menu/composer/theme contents - separate. -- Add focused tests in the same subtask that introduces or migrates behaviour. -- Update design-system docs only after implementation proves the final contract. diff --git a/docs/workflows/archive/task-18-memory-actions/01-data-model-and-repository-foundation.md b/docs/workflows/archive/task-18-memory-actions/01-data-model-and-repository-foundation.md deleted file mode 100644 index 6be1e383..00000000 --- a/docs/workflows/archive/task-18-memory-actions/01-data-model-and-repository-foundation.md +++ /dev/null @@ -1,142 +0,0 @@ -# 18.1 Data model and repository foundation - -## Goal - -Create the backend foundation for read status, taxonomy records, taxonomy assignment, taxonomy list ordering, and memory deletion metadata. This subtask does not implement public API routes or UI. - -## Files likely owned - -- `src/server/db/schema.ts` -- `drizzle/.sql` -- `src/server/db/repositories.ts` -- optional `src/server/taxonomy/id.ts` -- `tests/server/db/schema.test.ts` -- `tests/server/db/repositories.test.ts` - -## Data model contract - -Add `read` to `memories`: - -```ts -read: integer("read", { mode: "boolean" }).notNull().default(false) -``` - -Rules: - -- Existing rows migrate to `false`. -- New rows default to `false`. -- No index for `read`. -- Keep the TypeScript property name `read`. -- Keep SQL column name `read` unless actual SQLite/Drizzle evidence proves that unsafe. - -Review existing taxonomy schema before editing: - -- `tags` -- `categories` -- `memory_tags` -- `memory_categories` - -Default design: - -- Keep many-to-many join tables. -- Keep unique names. -- Do not add taxonomy columns unless repository queries cannot satisfy required ordering. -- Use join-table `createdAt` / `updatedAt` as assignment activity timestamps. -- Do not put tags/categories into `CONTENT.md` frontmatter. - -ID rule: - -- Do not derive tag/category IDs from display names. -- Use a generated stable ID so later rename support does not require primary-key migration. -- If adding a generator, keep it taxonomy-specific rather than reusing memory ID semantics accidentally. - -## Repository contract - -Extend repositories with focused methods. - -Memory: - -```ts -setReadStatus(input: { memoryId: string; read: boolean; updatedAt: Date }): Promise -findDeletionTarget(memoryId: string): Promise<{ id: string; contentPath: string } | undefined> -deleteMemoryRecord(memoryId: string): Promise -``` - -Taxonomy: - -```ts -createTag(input: { id: string; name: string; now: Date }): Promise -createCategory(input: { id: string; name: string; now: Date }): Promise -findTagByName(name: string): Promise -findCategoryByName(name: string): Promise -attachTagToMemory(input: { memoryId: string; tagId: string; now: Date }): Promise -attachCategoryToMemory(input: { memoryId: string; categoryId: string; now: Date }): Promise -listTagsForBrowse(): Promise -listCategoriesForBrowse(): Promise -``` - -`TaxonomyBrowseRow`: - -```ts -{ - id: string; - name: string; - memoryCount: number; - lastAssignedAt: string | null; -} -``` - -Repository rules: - -- Attach operations are idempotent. -- Attach operations update assignment activity timestamps when an existing relation is reattached. -- Missing memory/tag/category is reported explicitly, not silently ignored. -- `listTagsForBrowse()` and `listCategoriesForBrowse()` include records with zero attached memories. -- Sorting is `memoryCount desc`, `lastAssignedAt desc`, `name asc`. -- Deleting a memory record relies on SQLite foreign-key cascades for Flashbacks, Moments, and join rows. -- Deleting a memory record does not delete global tag/category rows. - -## Query shape changes - -Update browse and reader DTO sources so later UI subtasks can consume: - -- `read` -- `extractionStatus` -- attached `tags` -- attached `categories` - -Do not implement rendering in this subtask. - -## Tests - -Cover: - -- migration adds `read` defaulting to false for existing rows -- repository-created memories default unread when no explicit value is supplied -- `setReadStatus()` toggles true and false -- tag/category creation succeeds -- duplicate tag/category names return or preserve a single canonical row according to repository contract -- attach tag/category to memory succeeds -- attach is idempotent and updates assignment activity -- taxonomy list includes zero-count records -- taxonomy sorting follows count, recent assignment, name -- memory delete record cascades Flashbacks, Moments, and join rows -- memory delete record keeps global tag/category rows - -## Verification - -Run focused backend tests for this slice: - -```sh -mise exec -- bun run test tests/server/db/schema.test.ts -mise exec -- bun run test tests/server/db/repositories.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Database migration is minimal and reversible through normal project migration flow. -- Repository methods expose all data needed by later API/UI subtasks. -- No UI or route behaviour is introduced here. -- No tags/categories are written to content markdown. - diff --git a/docs/workflows/archive/task-18-memory-actions/02-api-and-mutation-service-layer.md b/docs/workflows/archive/task-18-memory-actions/02-api-and-mutation-service-layer.md deleted file mode 100644 index 3b44230a..00000000 --- a/docs/workflows/archive/task-18-memory-actions/02-api-and-mutation-service-layer.md +++ /dev/null @@ -1,231 +0,0 @@ -# 18.2 API and mutation service layer - -## Goal - -Expose read status, taxonomy creation, taxonomy assignment, and memory deletion through server routes/services. This subtask should not implement visual UI. - -## Files likely owned - -- `src/routes/api/memories/read-status.ts` -- `src/routes/api/memories/[memoryId].ts` -- `src/routes/api/memories/tags.ts` -- `src/routes/api/memories/categories.ts` -- `src/routes/api/tags.ts` -- `src/routes/api/categories.ts` -- optional `src/server/memories/delete-memory.ts` -- optional `src/server/taxonomy/mutations.ts` -- `src/server/store/memory-content.ts` -- `tests/server/routes/api-memory-read-status.test.ts` -- `tests/server/routes/api-memory-delete.test.ts` -- `tests/server/routes/api-taxonomy.test.ts` - -## API contract - -### Read status - -```http -POST /api/memories/read-status -content-type: application/json - -{ - "memoryId": "memory-id", - "read": true -} -``` - -Responses: - -- `200` with `{ "memoryId": string, "read": boolean }` -- `400` malformed payload -- `404` missing memory - -### Tags - -```http -POST /api/tags -content-type: application/json - -{ - "name": "sqlite" -} -``` - -Responses: - -- `201` newly created -- `200` existing record returned -- `400` malformed or empty name - -### Categories - -```http -POST /api/categories -content-type: application/json - -{ - "name": "Research" -} -``` - -Responses mirror tags. - -### Attach tag to memory - -```http -POST /api/memories/tags -content-type: application/json - -{ - "memoryId": "memory-id", - "tagId": "tag-id" -} -``` - -Also support create-or-attach by name for the browse `Add tag` popup: - -```json -{ - "memoryId": "memory-id", - "name": "sqlite" -} -``` - -Rules: - -- exactly one of `tagId` or `name` is accepted -- idempotent existing relation returns `200` -- missing memory/tag returns `404` -- name path creates or resolves the tag, then attaches it - -### Attach category to memory - -```http -POST /api/memories/categories -content-type: application/json - -{ - "memoryId": "memory-id", - "categoryId": "category-id" -} -``` - -Also support create-or-attach by name: - -```json -{ - "memoryId": "memory-id", - "name": "Research" -} -``` - -Rules mirror tag assignment. - -### Delete memory - -```http -DELETE /api/memories/:memoryId -``` - -Responses: - -- `204` deleted -- `404` missing memory -- `500` deletion failed - -## Deletion service contract - -Implement deletion in a service rather than directly inside the route. - -Deletion must remove: - -- SQLite `memories` row -- SQLite cascaded `flashbacks` -- SQLite cascaded `memory_tags` -- SQLite cascaded `memory_categories` -- filesystem directory for the memory under `storePath` -- git backup tracking for the deleted memory content when backup is enabled - -Deletion must not remove: - -- global `tags` -- global `categories` -- unrelated content directories - -Recommended safe flow: - -1. Load config and initialize DB. -2. Read deletion target `{ id, contentPath }`. -3. Resolve the memory directory under `storePath`. -4. Assert the resolved directory is inside `storePath`. -5. Move the directory to a delete-staging path under `storePath` if feasible. -6. Delete the SQLite row. -7. Queue or execute a backup deletion job for the removed content path when git backup is enabled. -8. Remove the staged directory. -9. If SQLite deletion fails after staging, restore the staged directory. - -Backup deletion strategy: - -- A normal memory delete must not leave the deleted `CONTENT.md` tracked in the - backup repository. -- Prefer adding an explicit backup job type for deleted content paths, for - example `{ kind: "delete", contentPaths: [...] }`, so the backup worker can - stage removals and commit them. -- If the current backup queue only supports changed content paths, extend it in - the same subtask or document why deletion backup is deferred and surface the - risk in the PR. -- The route should return success only after the local SQLite/filesystem delete - succeeds. Backup push failure may be reported through the existing backup - warning channel, but it must not silently resurrect or retain deleted content - in the backup contract. - -If implementation chooses a simpler direct delete flow, document why the consistency tradeoff is acceptable and add tests for the failure mode that remains. - -## Validation rules - -- JSON body must be an object. -- Reject unexpected body shapes. -- Trim taxonomy names. -- Reject empty taxonomy names. -- Preserve display case unless a repository-level normalized-name design is added in subtask 18.1. -- Never trust client-provided paths. -- For memory IDs, require non-empty string at the route layer and let repository determine existence. - -## Tests - -Cover: - -- read status true/false success -- read status malformed payload -- read status missing memory -- create tag success -- create category success -- duplicate tag/category returns existing record -- attach tag by ID -- attach tag by name -- attach category by ID -- attach category by name -- attach missing memory -- attach missing tag/category -- delete memory success -- delete missing memory -- delete removes content directory -- delete queues or records backup deletion for the removed content path when backup is enabled -- delete does not remove global tags/categories -- delete does not accept path traversal through content path - -## Verification - -```sh -mise exec -- bun run test tests/server/routes/api-memory-read-status.test.ts -mise exec -- bun run test tests/server/routes/api-memory-delete.test.ts -mise exec -- bun run test tests/server/routes/api-taxonomy.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Public mutation routes exist and match the contracts above. -- Route files delegate persistence to repositories/services. -- Filesystem deletion is constrained to the configured store path. -- Memory deletion has an explicit backup deletion strategy for removed content paths. -- No UI code is introduced in this subtask. diff --git a/docs/workflows/archive/task-18-memory-actions/03-shared-ui-primitives.md b/docs/workflows/archive/task-18-memory-actions/03-shared-ui-primitives.md deleted file mode 100644 index eae8ec82..00000000 --- a/docs/workflows/archive/task-18-memory-actions/03-shared-ui-primitives.md +++ /dev/null @@ -1,146 +0,0 @@ -# 18.3 Shared UI primitives - -## Goal - -Create or extract reusable UI primitives needed by browse and reader pages. This subtask should establish components, not wire all final page behaviour. - -## Files likely owned - -- `src/components/memories/MemoryActionMenu.tsx` -- `src/components/memories/MemoryReadStatusControl.tsx` -- `src/components/memories/TaxonomyCreatePopover.tsx` -- optional `src/components/memories/memory-actions.ts` -- `tests/components/memory-action-menu.test.tsx` -- `tests/components/memory-read-status.test.tsx` -- `tests/components/taxonomy-create-popover.test.tsx` - -## Memory action menu contract - -Create one reusable memory-level meatballs menu component. - -If a browse memory menu already exists on the implementation branch: - -- extract it without changing visual intent -- keep browse using the extracted component -- expose props needed by reader - -If no menu exists: - -- create a new `MemoryActionMenu` component -- use a three-dot/meatballs trigger -- keep it generic enough for browse and reader - -Required props: - -```ts -{ - memoryId: string; - memoryTitle: string; - onDelete?: (memoryId: string) => Promise | void; - onAttachCategoryByName?: (input: { memoryId: string; name: string }) => Promise | void; - disabled?: boolean; - class?: string; -} -``` - -Required menu items: - -- `Delete memory` -- `Add category` - -Rules: - -- `Delete memory` requires confirmation. -- `Add category` opens the shared taxonomy popover in category mode. -- Do not create a separate reader-only menu. -- The trigger must be keyboard accessible. -- Escape closes the menu/popover. -- Clicking outside closes the menu/popover if existing project patterns support it. - -## Read status control contract - -Create `MemoryReadStatusControl`. - -Props: - -```ts -{ - memoryId: string; - initialRead: boolean; - compact?: boolean; - class?: string; - onChange?: (read: boolean) => void; -} -``` - -Behaviour: - -- Render visible label `Read` or `Unread`. -- Render action text `Mark unread` when currently read. -- Render action text `Mark read` when currently unread. -- Use `aria-pressed`. -- Disable while request is in flight. -- Optimistically update local state. -- Revert and show a small inline error when the API fails. - -## Taxonomy create popover contract - -Create one shared popover component for: - -- right-pane `New category` -- right-pane `New tag` -- browse-footer `Add tag` -- memory-menu `Add category` - -Props: - -```ts -{ - title: string; - label: string; - placeholder: string; - submitLabel: string; - onSubmitName: (name: string) => Promise | void; - onClose: () => void; -} -``` - -Behaviour: - -- Focuses the input when opened. -- Trims input. -- Rejects empty input client-side. -- Enter submits. -- Icon button next to input submits. -- Shows a small inline error on failure. -- Does not decide whether the operation is create-only or create-and-attach; parent handlers decide. - -## Tests - -Cover: - -- menu renders trigger and required items -- delete item calls confirmation path before mutation callback -- add category opens taxonomy popover -- read status renders initial state -- read status toggles with optimistic UI -- read status reverts on request failure -- taxonomy popover submits on Enter -- taxonomy popover rejects empty names -- taxonomy popover calls `onSubmitName` with trimmed value - -## Verification - -```sh -mise exec -- bun run test tests/components/memory-action-menu.test.tsx -mise exec -- bun run test tests/components/memory-read-status.test.tsx -mise exec -- bun run test tests/components/taxonomy-create-popover.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Browse and reader can use the same action menu component. -- Taxonomy create/attach UI can use the same popover component. -- Components are behaviour-ready but not over-coupled to a single page. - diff --git a/docs/workflows/archive/task-18-memory-actions/04-browse-memory-item-actions.md b/docs/workflows/archive/task-18-memory-actions/04-browse-memory-item-actions.md deleted file mode 100644 index 65014f23..00000000 --- a/docs/workflows/archive/task-18-memory-actions/04-browse-memory-item-actions.md +++ /dev/null @@ -1,116 +0,0 @@ -# 18.4 Browse memory item actions - -## Goal - -Wire Task 18 behaviours into `/memories` memory items: read status, link-only status, footer tags, add-tag action, per-memory menu, and delete action. - -## Files likely owned - -- `src/components/memories/MemoryBrowse.tsx` -- `src/components/memories/browse-data.ts` -- `src/components/memories/browse-loader.ts` -- `src/components/memories/MemoryActionMenu.tsx` -- `src/components/memories/MemoryReadStatusControl.tsx` -- `src/components/memories/TaxonomyCreatePopover.tsx` -- `tests/memories/browse-data.test.ts` -- `tests/components/memory-browse-actions.test.tsx` - -## Rendering contract - -Each browse memory item must render: - -- title -- URL -- description -- attached category chips if already present -- attached tag chips -- latest/selected Flashback preview -- open link -- shared memory action menu -- read status control -- footer status line - -Footer status line: - -- For `extractionStatus === "link_only"`, render an error icon and text `Link-only`. -- For successful full-content memories, render no `Saved` label. -- Render attached tags on this line. -- Render an `Add tag` placeholder/action on this line. - -Do not remove category chips unless a later design task explicitly moves them. - -## Add tag behaviour - -Clicking `Add tag`: - -1. Opens the shared `TaxonomyCreatePopover`. -2. Uses tag mode labels. -3. Submits by name. -4. Calls the create-or-attach tag API for the current memory. -5. Updates the current memory's visible tag list after success. - -The popup may create a new tag or resolve an existing tag by name. The user should not need to know which path occurred. - -## Delete behaviour - -The memory action menu on browse items includes `Delete memory`. - -After successful delete: - -- remove the memory from the current visible list, or -- revalidate/navigate using the existing route-data pattern. - -Do not leave a deleted item visible until a full page refresh. - -## Read status behaviour - -Render `MemoryReadStatusControl` near the lower-right of the memory item. - -Rules: - -- Initial state comes from browse data. -- Toggle persists through the API. -- Failure leaves the item in its previous state and shows a concise error. - -## Browse data changes - -`BrowseMemory` needs: - -- `read` -- `extractionStatus` -- attached categories -- attached tags - -Search behaviour: - -- Existing title/URL/description/tag/category/Flashback search remains. -- Do not add read-status filtering in this subtask. - -## Tests - -Cover: - -- successful memory does not render `Saved` -- link-only memory renders error icon and `Link-only` -- tags render in footer/status line -- `Add tag` opens popover -- submitting `Add tag` attaches tag and updates visible row -- delete menu item deletes and removes memory from view -- read control renders and toggles -- existing category/tag/Flashback filtering still works - -## Verification - -```sh -mise exec -- bun run test tests/memories/browse-data.test.ts -mise exec -- bun run test tests/components/memory-browse-actions.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- `/memories` exposes all specified per-memory actions. -- Link-only state is visible and successful saved state is not redundantly shown. -- `Add tag` attaches a tag to the selected memory. -- Delete works from the browse item menu. -- Existing browse filtering is not regressed. diff --git a/docs/workflows/archive/task-18-memory-actions/05-right-pane-taxonomy-management.md b/docs/workflows/archive/task-18-memory-actions/05-right-pane-taxonomy-management.md deleted file mode 100644 index 315cfeea..00000000 --- a/docs/workflows/archive/task-18-memory-actions/05-right-pane-taxonomy-management.md +++ /dev/null @@ -1,113 +0,0 @@ -# 18.5 Right-pane taxonomy management - -## Goal - -Change the right-pane category/tag sections from visible-memory-derived filters to full taxonomy lists, and add `New category` / `New tag` creation popovers. - -## Files likely owned - -- `src/components/shell/AppShell.tsx` -- `src/components/memories/browse-data.ts` -- `src/components/memories/browse-loader.ts` -- optional `src/components/memories/taxonomy-loader.ts` -- `src/components/memories/TaxonomyCreatePopover.tsx` -- `tests/components/app-shell-taxonomy.test.tsx` -- `tests/memories/browse-data.test.ts` - -## Data contract - -Right-pane category/tag sections must list all categories/tags in the database. - -They must not be derived from: - -- only currently filtered memories -- only currently visible page rows -- only tags/categories attached to at least one memory - -Each taxonomy item should include: - -- `id` -- `name` -- `memoryCount` -- `lastAssignedAt` - -Sort order: - -1. `memoryCount` descending -2. `lastAssignedAt` descending, with `null` last -3. `name` ascending - -## Category UI - -In the right-pane Categories section: - -- Add a plus icon button. -- Label: `New category`. -- Clicking opens `TaxonomyCreatePopover` anchored to that action. -- Popover receives a name input. -- Enter submits. -- Icon button next to input submits. -- On success, the category appears in the right-pane list. - -Do not auto-select the new category filter unless the user explicitly clicks it after creation. - -## Tag UI - -Same as category, but: - -- Section: Tags -- Label: `New tag` -- Endpoint: tag creation - -Do not auto-select the new tag filter unless the user explicitly clicks it after creation. - -## Existing filter behaviour - -Keep existing category/tag filter buttons: - -- Clicking a category filters `/memories?category=`. -- Clicking the same active category toggles it off. -- Clicking a tag filters `/memories?tag=`. -- Clicking the same active tag toggles it off. - -The right-pane list being global must not break filtering against the current browse memory rows. - -## Empty states - -If no categories exist: - -- render `New category` -- render a small empty-state hint - -If no tags exist: - -- render `New tag` -- render a small empty-state hint - -## Tests - -Cover: - -- right pane shows categories/tags not attached to visible memories -- right pane includes zero-count categories/tags -- sort order is count, recent assignment, name -- `New category` opens popover and creates category -- `New tag` opens popover and creates tag -- creation does not auto-apply filter -- existing filter toggle behaviour remains - -## Verification - -```sh -mise exec -- bun run test tests/components/app-shell-taxonomy.test.tsx -mise exec -- bun run test tests/memories/browse-data.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Right pane is backed by full taxonomy data. -- Category/tag creation works from the right pane. -- Existing filtering remains intact. -- No reader UI is changed in this subtask. - diff --git a/docs/workflows/archive/task-18-memory-actions/06-reader-memory-actions.md b/docs/workflows/archive/task-18-memory-actions/06-reader-memory-actions.md deleted file mode 100644 index 8bef551a..00000000 --- a/docs/workflows/archive/task-18-memory-actions/06-reader-memory-actions.md +++ /dev/null @@ -1,119 +0,0 @@ -# 18.6 Reader memory actions - -## Goal - -Wire Task 18 into memory reader mode: shared action menu, read status, attached taxonomy rendering, and delete behaviour. - -## Files likely owned - -- `src/server/reader/page-data.ts` -- `src/components/reader/MemoryReader.tsx` -- `src/components/reader/route-state.ts` -- `src/components/memories/MemoryActionMenu.tsx` -- `src/components/memories/MemoryReadStatusControl.tsx` -- `tests/server/reader/page-data.test.ts` -- `tests/components/memory-reader-actions.test.tsx` - -## Reader data contract - -Reader page data for the active memory must include: - -- `id` -- `title` -- `url` -- `description` -- `faviconUrl` -- `contentPath` -- `extractionStatus` -- `read` -- attached `tags` -- attached `categories` -- existing rendered content/Flashback data - -Reader mode must not fetch or render global taxonomy lists. - -## Header rendering contract - -Reader header right edge: - -- shared `MemoryActionMenu` -- read status control - -Lower/right header area: - -- read status label/control -- attached tags/categories for the current memory - -Keep the header compact. Do not redesign reader typography or content layout. - -## Action menu contract - -Use the same `MemoryActionMenu` component as browse memory items. - -Menu items available from reader: - -- `Delete memory` -- `Add category` - -Delete from reader: - -1. Confirm deletion. -2. Call delete API. -3. Navigate to `/memories` after success. -4. Show concise error on failure. - -Add category from reader: - -1. Opens shared taxonomy popover. -2. Creates or resolves category by name. -3. Attaches category to the active memory. -4. Updates visible reader category chips after success. - -## Attached taxonomy rendering - -Reader mode renders only categories/tags attached to the current memory. - -Rules: - -- Do not render all tags/categories from the app. -- Do not render empty taxonomy sections if the memory has no attached records, unless needed for the `Add category` action. -- Preserve existing Flashback and markdown rendering behaviour. - -## Read status behaviour - -Render `MemoryReadStatusControl` in the header. - -Rules: - -- Initial state comes from reader page data. -- Toggle persists through API. -- Failure reverts local state and shows a concise error. - -## Tests - -Cover: - -- page data includes read, categories, and tags -- reader header renders shared action menu -- reader header renders read status control -- reader renders only attached taxonomy -- delete from reader navigates to `/memories` on success -- delete failure shows error and does not navigate -- add category attaches and updates visible reader taxonomy -- existing Flashback rendering remains intact - -## Verification - -```sh -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/components/memory-reader-actions.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Reader uses the shared memory action menu. -- Reader does not duplicate browse menu logic. -- Reader only displays taxonomy attached to the current memory. -- Reader deletion and read status work without breaking Flashback behaviour. - diff --git a/docs/workflows/archive/task-18-memory-actions/08-settings-page-and-openai-auth.md b/docs/workflows/archive/task-18-memory-actions/08-settings-page-and-openai-auth.md deleted file mode 100644 index 4d789c82..00000000 --- a/docs/workflows/archive/task-18-memory-actions/08-settings-page-and-openai-auth.md +++ /dev/null @@ -1,344 +0,0 @@ -# 18.8 Settings page and OpenAI auth state - -## Goal - -Add `/settings` as part of Task 18. The page owns translation target language configuration and OpenAI auth state controls. - -This is intentionally not Task 19. It is part of the same user-facing memory workspace expansion as Task 18. - -## Files likely owned - -- `src/routes/settings.tsx` -- `src/components/settings/SettingsPage.tsx` -- `src/components/settings/settings-loader.ts` -- `src/components/settings/settings-submit.ts` -- `src/components/shell/AppShell.tsx` -- `src/server/db/schema.ts` -- `drizzle/.sql` -- `src/server/db/repositories.ts` -- `src/server/settings/settings.ts` -- `src/server/settings/openai-auth.ts` -- `src/routes/api/settings.ts` -- `src/routes/api/settings/translation-language.ts` -- `src/routes/api/settings/openai-auth/enable.ts` -- `src/routes/api/settings/openai-auth.ts` -- `tests/server/settings/settings.test.ts` -- `tests/server/routes/api-settings.test.ts` -- `tests/components/settings-page.test.tsx` - -## Product contract - -Create a new settings page: - -```text -/settings -``` - -The first settings fields are: - -- Translation target language -- OpenAI Auth - -## Translation target language - -Render a select menu for translation target language. - -Rules: - -- The select menu lists supported language kinds. -- The selected value persists through the settings API. -- The selected value reloads correctly after refresh. -- Invalid language values are rejected by the API. -- Server validation is authoritative. - -Recommended initial language options use BCP 47 language codes: - -- `ja-JP`: Japanese -- `en-US`: English -- `ko-KR`: Korean -- `zh-CN`: Chinese -- `fr-FR`: French -- `de-DE`: German -- `es-ES`: Spanish -- `it-IT`: Italian -- `pt-BR`: Portuguese - -If implementation adds a shared language tuple, both UI and API should import from it. - -Canonical API format: - -- Persist and return BCP 47 language codes exactly as listed above. -- Do not return ISO 639-1 short codes such as `ja` from settings APIs. -- If implementation accepts a short code for compatibility, normalize it before - storage and always return the supported BCP 47 value, for example `ja-JP`. - -## OpenAI Auth UI - -Default disabled state: - -- Render a clickable button. -- Button label: `Enable`. -- Clicking calls the OpenAI auth enable API. - -Enabled state: - -- Render the same button disabled. -- Button label: `Enabled`. -- Under the field, render hint text that communicates OpenAI auth is enabled. -- Render a danger-styled `Delete auth` button to the right of the disabled `Enabled` button. - -## OpenAI Auth API validation - -Frontend disabled state is not a security boundary. - -If a direct API request tries to enable OpenAI auth while auth is already enabled: - -- Do not mutate state. -- Do not return an error. -- Return a successful response that explicitly says auth is already enabled. - -Recommended response: - -```json -{ - "status": "enabled", - "alreadyEnabled": true, - "message": "OpenAI auth is already enabled." -} -``` - -## Data model contract - -Add settings persistence if no current equivalent exists. - -Recommended schema: - -```ts -appSettings: { - id: "default", - translationTargetLanguage: SupportedLanguageCode, - createdAt: Date, - updatedAt: Date -} -``` - -OpenAI auth state should not be a cosmetic boolean if real credentials are involved. - -Recommended design: - -- Store non-secret settings in an app settings singleton row. -- Store OpenAI auth credential material separately from non-secret settings. -- API responses expose only status, never credential material. -- `openaiAuthStatus` is derived from credential presence/validity, not trusted from a client-submitted flag. - -If the project does not yet have a real OpenAI auth provider: - -- Implement the settings page and API boundary. -- Keep credential handling behind a small server adapter. -- Do not fake an enabled status without a stored/validated auth record. -- If enable cannot complete because provider configuration is missing, return a clear `not_configured` response. - -## API contract - -### Read settings - -```http -GET /api/settings -``` - -Response: - -```json -{ - "translationTargetLanguage": "ja-JP", - "openaiAuth": { - "status": "disabled" - } -} -``` - -or: - -```json -{ - "translationTargetLanguage": "ja-JP", - "openaiAuth": { - "status": "enabled" - } -} -``` - -### Update translation target language - -```http -PATCH /api/settings/translation-language -content-type: application/json - -{ - "language": "ja-JP" -} -``` - -Responses: - -- `200` with updated settings -- `400` for malformed body -- `400` for unsupported language - -### Enable OpenAI auth - -```http -POST /api/settings/openai-auth/enable -``` - -Responses: - -- `200` with `{ "status": "enabled", "alreadyEnabled": false }` after a successful enable operation -- `200` with `{ "status": "enabled", "alreadyEnabled": true, "message": "OpenAI auth is already enabled." }` if already enabled -- `409` with a clear `not_configured` response when auth cannot be enabled because a real provider/configuration is unavailable - -Security rule: - -- The route must load current auth state server-side before mutation. -- If already enabled, return the idempotent already-enabled response and do not overwrite credential state. -- If no real auth provider exists yet, do not create a cosmetic enabled row. - Return `not_configured` and keep status disabled. - -### Delete OpenAI auth - -```http -DELETE /api/settings/openai-auth -``` - -Responses: - -- `200` with `{ "status": "disabled" }` after deleting auth -- `200` with `{ "status": "disabled", "alreadyDisabled": true }` if no auth existed - -Rules: - -- Require a deliberate UI action from the danger button. -- Do not return deleted credential data. -- Deleting auth should only remove OpenAI auth state, not other settings. - -## UI contract - -Navigation: - -- Add `Settings` to the app shell navigation. -- Route path is `/settings`. - -Page layout: - -- Use the existing app shell and project visual language. -- Do not redesign the global shell as part of this subtask. -- Keep fields grouped and readable. - -Translation field: - -- Label clearly describes translation target language. -- Select menu contains supported language options. -- Persist on change or via an explicit save action. Prefer explicit save if current form patterns expect it; otherwise on-change is acceptable if error handling is clear. -- Show inline failure text if API update fails. - -OpenAI Auth field: - -- Disabled state: - - button label `Enable` - - button clickable - - no `Delete auth` button - -- Enabled state: - - disabled button label `Enabled` - - hint text under field, for example `OpenAI auth is enabled.` - - danger `Delete auth` button immediately to the right of the disabled button - -Delete auth: - -- Use danger styling. -- Confirm before deleting if the project already uses confirmation patterns. -- On success, update UI to disabled state. - -## Backend service contract - -Repository/service methods: - -```ts -getSettings(): Promise -updateTranslationTargetLanguage(language: SupportedLanguageCode): Promise -getOpenAiAuthStatus(): Promise<"disabled" | "enabled"> -enableOpenAiAuth(): Promise<{ status: "enabled"; alreadyEnabled: boolean }> -deleteOpenAiAuth(): Promise<{ status: "disabled"; alreadyDisabled: boolean }> -``` - -If a real auth provider is unavailable, the enable service must return an -explicit non-exception result rather than throwing or faking enabled state: - -```ts -enableOpenAiAuth(): Promise< - | { status: "enabled"; alreadyEnabled: boolean } - | { status: "disabled"; code: "not_configured"; message: string } -> -``` - -The route maps `code: "not_configured"` to HTTP `409`. Do not use `503` for -this provider-missing condition; reserve `503` for a distinct transient service -outage if such a state is implemented later. - -Validation: - -- Shared supported-language guard. -- Request body shape validation. -- Idempotent OpenAI auth enable validation. -- Auth enable route must not overwrite enabled auth state. -- No API response may include secrets. - -## Tests - -Backend tests: - -- settings singleton initializes with default translation target language -- valid translation language update persists -- unsupported translation language is rejected -- malformed translation body is rejected -- OpenAI auth enable succeeds from disabled state only when a real provider or stored auth record is available -- OpenAI auth enable from disabled state returns `409` with `not_configured` without enabling auth when no provider exists -- OpenAI auth enable while already enabled returns `alreadyEnabled: true` -- OpenAI auth enable while already enabled does not overwrite auth state -- OpenAI auth delete succeeds from enabled state -- OpenAI auth delete from disabled state returns `alreadyDisabled: true` -- settings API never returns credential material - -Frontend/component tests: - -- `/settings` renders translation language select -- language select shows all supported language kinds -- disabled OpenAI auth state renders clickable `Enable` -- enabled OpenAI auth state renders disabled `Enabled` -- enabled state renders hint text -- enabled state renders danger `Delete auth` button -- delete auth success returns UI to disabled state -- enable already-enabled response keeps UI enabled without surfacing an error - -## Verification - -```sh -mise exec -- bun run test tests/server/settings/settings.test.ts -mise exec -- bun run test tests/server/routes/api-settings.test.ts -mise exec -- bun run test tests/components/settings-page.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- `/settings` route exists. -- App navigation exposes Settings. -- Translation target language is selectable and persisted. -- Settings APIs persist and return BCP 47 language codes such as `ja-JP`. -- Unsupported languages are rejected server-side. -- OpenAI auth disabled state shows clickable `Enable`. -- OpenAI auth enabled state shows disabled `Enabled`, enabled hint text, and danger `Delete auth`. -- Direct API enable requests while already enabled return a successful already-enabled response and do not mutate credential state. -- If no real OpenAI auth provider exists, enable returns a clear `409` not-configured response and does not fake enabled state. -- Deleting OpenAI auth removes only auth state. -- No secret material is rendered or returned by the settings API. diff --git a/docs/workflows/archive/task-18-memory-actions/09-reader-flashback-marker-selection-and-tabs.md b/docs/workflows/archive/task-18-memory-actions/09-reader-flashback-marker-selection-and-tabs.md deleted file mode 100644 index 67a06fe6..00000000 --- a/docs/workflows/archive/task-18-memory-actions/09-reader-flashback-marker-selection-and-tabs.md +++ /dev/null @@ -1,287 +0,0 @@ -# 18.9 Reader flashback selection and tabs - -## Goal - -Revise reader flashback interaction so text selection does not immediately create a flashback. A normal text selection should open a TRAUMA-owned contextual menu near the selection. Clicking the flashback icon in that menu creates the flashback. - -Flashback persistence must remain record-based. Do not mutate `CONTENT.md` to store flashback marks. - -## Files likely owned - -- `src/components/reader/MemoryReader.tsx` -- `src/components/reader/flashback-events.ts` -- `src/components/reader/flashback-failure.ts` -- `src/components/reader/route-state.ts` -- `src/server/flashbacks/toggle.ts` -- `src/server/flashbacks/ranges.ts` -- `src/server/store/flashback-markers.ts` -- `src/server/db/schema.ts` -- `drizzle/.sql` -- `src/server/db/repositories.ts` -- `src/routes/api/flashbacks.ts` -- `src/components/shell/AppShell.tsx` -- `src/components/flashbacks/flashbacks-loader.ts` -- `tests/server/flashbacks/toggle.test.ts` -- `tests/server/flashbacks/ranges.test.ts` -- `tests/server/flashbacks/flashback-markers.test.ts` -- `tests/server/routes/api-flashbacks-toggle.test.ts` -- `tests/components/memory-reader-flashback-selection.test.tsx` -- `tests/components/reader-flashback-tabs.test.tsx` - -## Selection UX contract - -Current behaviour creates/toggles a flashback directly from selected text. Replace that with explicit user intent: - -1. User selects text normally in reader content. -2. TRAUMA renders a small contextual menu above or below the selected range. -3. The menu belongs to the app, not the browser context menu. -4. The menu contains a flashback icon action. -5. Clicking the flashback icon creates the flashback record for the selected range. -6. Clearing the selection, pressing Escape, scrolling away, or clicking outside closes the menu. - -Menu positioning: - -- Prefer above the selection. -- If there is not enough viewport space above, render below. -- Keep the menu within the viewport horizontally. -- Do not render the menu inside copied text or persisted content. - -Accessibility: - -- The flashback action is a real button. -- It has an accessible label, for example `Flashback selection`. -- Escape closes the menu. -- The menu must not trap focus permanently. - -## Flashback record design - -Selected text alone is not enough. A word or sentence may appear multiple times in the same content. - -Use canonical reader-text offsets as the primary identity: - -```ts -{ - id: string; - memoryId: string; - text: string; - startOffset: number; - endOffset: number; - prefix: string; - suffix: string; - contentHash?: string; - createdAt: Date; - updatedAt: Date; -} -``` - -Rules: - -- `startOffset` and `endOffset` are offsets into canonical reader text, not into raw markdown and not into DOM HTML. -- Offsets are the primary mechanism that distinguishes repeated words. -- `text` stores the exact selected text for display and validation. -- `prefix` and `suffix` are display context for flashback-only rendering. They are not the primary anchor and must not be treated as sufficient disambiguation data. -- `contentHash` is recommended if the current schema does not already detect stale offset mappings. -- If adding `contentHash`, compute it from canonical reader text, not from the raw markdown file. -- Use `sha256:` as the `contentHash` format. -- Hash the UTF-8 bytes of the exact canonical reader text used for offset calculation. -- Normalize line endings to `\n` before both offset calculation and hashing. -- Do not trim leading/trailing text for hashing. -- Do not apply Unicode compatibility normalization for hashing unless the same - normalization is also applied before offset calculation and rendering; if a - Unicode normalization step is later introduced, document it beside the shared - text-walker utility and keep one canonical implementation. -- If content hash mismatches later, do not silently apply a flashback to the wrong occurrence. - -Canonical reader text: - -- Derive it from the same reader content tree that users select from. -- Exclude app chrome, menus, buttons, route shell, and hidden controls. -- Normalize text consistently between selection mapping and flashback rendering. -- Prefer one shared text-walker utility so offset calculation and mark rendering cannot drift. - -Why this disambiguates repeated text: - -- If `foo` appears three times, all three records have `text = "foo"`. -- The selected occurrence is identified by its unique `startOffset` / `endOffset`. -- `contentHash` guards against applying offsets to a different content version. -- `prefix` and `suffix` stay available for recent-flashback rows and flashback-only rendering, but they are not used to choose between repeated identical occurrences. - -Do not use occurrence index as the primary identity. It is fragile when nearby content changes. - -## Ambiguous repeated text policy - -`prefix` and `suffix` cannot disambiguate repeated text when the repeated selection and its surrounding context are identical. - -Example: - -```text -雨降る日のこと。 ... 雨降る日のこと。 - ^ selected 日 ^ identical 日 -``` - -If the selected text is `日`, both occurrences can have the same: - -- `text` -- `prefix` -- `suffix` -- surrounding sentence -- broader surrounding paragraph - -The correct policy is: - -1. When `contentHash` matches, apply the flashback by `startOffset` and `endOffset`. -2. Before rendering, verify that the canonical reader-text slice at the stored offsets equals `text`. -3. If the slice does not match, treat the flashback as stale and do not render it at a guessed location. -4. If `contentHash` changed, fallback re-anchoring may search by text/context only as a recovery mechanism. -5. Fallback re-anchoring may auto-apply only when it finds exactly one valid candidate. -6. If fallback finds multiple candidates, mark the flashback as ambiguous/stale and do not render it automatically. - -This avoids silently rendering a flashback on the wrong occurrence. A missing/stale flashback is safer than a false flashback. - -## Persistence contract - -`CONTENT.md` must not be changed when creating, removing, or rendering flashbacks. - -If existing code currently writes flashback marks back into markdown: - -- Stop doing that for normal flashback persistence. -- Keep flashback rows in SQLite as the source of truth. -- Render flashback marks at read time by applying records to canonical reader text/HTML. -- Keep any markdown-marker utilities only if needed for migration, tests, or legacy compatibility. - -Backup/export requirement: - -- Do not rely on `CONTENT.md` mutation as the backup representation for new flashbacks. -- If the built-in git backup does not back up SQLite, add a metadata backup/export - path for flashback rows before removing markdown marker writes from the normal - flashback flow. -- The backup representation must be deterministic and restorable enough to - preserve flashback records, including `memoryId`, `text`, offsets, guard - context, and `contentHash`. -- Prefer a small metadata export file under the memory's backup scope, or a - documented backup job that serializes flashback metadata with tests. -- If flashback metadata backup is intentionally deferred, the implementation PR - must state the restore-risk explicitly and should not claim full backup - parity for SQLite-only flashbacks. - -## API contract - -The existing flashback API may be reused, but its semantics must match explicit menu action. - -Expected create/toggle payload: - -```json -{ - "memoryId": "memory-id", - "text": "selected text", - "startOffset": 120, - "endOffset": 133, - "prefix": "before ", - "suffix": " after", - "contentHash": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" -} -``` - -Validation: - -- `memoryId` is a non-empty string. -- `text` is non-empty. -- offsets are non-negative integers. -- `endOffset > startOffset`. -- `text.length` matches the canonical slice when available. -- mismatched canonical slice returns a validation response and does not create a row. - -Existing unflashback behaviour: - -- Preserve existing flashback removal/toggle semantics if current tests depend on them. -- If the selected range exactly matches an existing flashback, the menu action may remove it only if that is the current product behaviour. -- Do not remove flashback deletion capability accidentally. - -## Reader flashback tabs - -The `/memories` recent flashback component stays unchanged. - -Reader mode changes: - -- In the reader-mode flashback component, render tabs under the component title. -- Left tab: all flashbacks across all memories. -- Second tab: flashbacks attached to the active memory only. - -Labels: - -- Left tab: `Current` -- Second tab: `All` - -Default active tab: - -- `Current` by default when the memory page opens, even when the current memory - has no Flashbacks. - -Tab behaviour: - -- Switching tabs does not navigate away from the reader page. -- `All` rows can navigate to their owning memory/Flashback anchor. -- `Current` rows navigate within the current reader page. -- Empty state for `Current`: show a concise hint that no Flashbacks exist for this memory. -- Empty state for `All`: show the existing no-Flashbacks empty state. - -## Tests - -Selection/menu tests: - -- selecting text renders the custom flashback menu -- flashback is not created until the icon is clicked -- Escape closes the menu -- clicking outside closes the menu -- repeated selected text creates a record for the selected occurrence by offset -- stale/mismatched offset validation does not create a flashback - -Persistence tests: - -- creating a flashback inserts/updates flashback records only -- creating a flashback does not modify `CONTENT.md` -- removing a flashback does not modify `CONTENT.md` -- existing flashback rendering still works from SQLite rows -- flashback metadata is exported or queued for backup when SQLite is not backed up directly -- restore-risk is documented if metadata backup is deliberately deferred - -Record tests: - -- duplicate selected text with different offsets creates distinct records -- `contentHash` uses `sha256:` from the same canonical reader text used for offsets -- line-ending normalization is consistent between hash creation and validation -- overlapping ranges continue to follow existing range rules -- exact existing flashback selection preserves existing toggle/remove semantics if supported - -Reader tab tests: - -- `/memories` right-rail Flashback shortcut list keeps the shared row design -- reader Flashback component renders `Current` as the left tab -- reader Flashback component renders `All` as the second tab -- `Current` tab only lists Flashbacks for the active memory -- `All` tab lists Flashbacks across memories -- default tab follows the contract above - -## Verification - -```sh -mise exec -- bun run test tests/server/flashbacks/toggle.test.ts -mise exec -- bun run test tests/server/flashbacks/ranges.test.ts -mise exec -- bun run test tests/server/flashbacks/flashback-markers.test.ts -mise exec -- bun run test tests/server/routes/api-flashbacks-toggle.test.ts -mise exec -- bun run test tests/components/memory-reader-flashback-selection.test.tsx -mise exec -- bun run test tests/components/reader-flashback-tabs.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Selecting reader text opens a custom TRAUMA flashback menu. -- Flashback records are created only after clicking the flashback icon. -- Flashback persistence does not mutate `CONTENT.md`. -- Repeated text selections are disambiguated by canonical offsets. -- Existing flashback rendering remains available. -- `/memories` right-rail Flashback shortcut list remains unchanged except for - the island title `Flashback`. -- Reader mode has `Current` left tab and `All` second tab. -- Reader current-memory tab lists only active-memory flashbacks. diff --git a/docs/workflows/archive/task-18-memory-actions/10-moment-section-bookmarks.md b/docs/workflows/archive/task-18-memory-actions/10-moment-section-bookmarks.md deleted file mode 100644 index 07baff1c..00000000 --- a/docs/workflows/archive/task-18-memory-actions/10-moment-section-bookmarks.md +++ /dev/null @@ -1,346 +0,0 @@ -# 18.10 Moment section bookmarks - -## Goal - -Add Moment, TRAUMA's product name for section bookmarks. - -Moment lets a user save a reader section/chapter as a bookmark and later browse all saved Moments at `/moments`. Each Moment item shows the bookmarked section and linked memory metadata. Clicking a Moment navigates to the relevant memory and section anchor. - -This subtask requires database design work. Do not model Moment as browser local state or as markup inside `CONTENT.md`. - -## Files likely owned - -- `src/server/db/schema.ts` -- `drizzle/.sql` -- `src/server/db/repositories.ts` -- `src/server/reader/page-data.ts` -- `src/components/reader/MemoryReader.tsx` -- `src/components/reader/route-state.ts` -- `src/components/reader/flashback-events.ts` -- `src/components/shell/AppShell.tsx` -- `src/routes/moments/index.tsx` -- `src/routes/api/moments.ts` -- `src/routes/api/moments/[momentId].ts` -- optional `src/server/moments/section-anchors.ts` -- optional `src/components/moments/MomentBrowse.tsx` -- optional `src/components/reader/ReaderContextMenu.tsx` -- `tests/server/db/schema.test.ts` -- `tests/server/db/repositories.test.ts` -- `tests/server/routes/api-moments.test.ts` -- `tests/server/reader/page-data.test.ts` -- `tests/components/reader-moment-actions.test.tsx` -- `tests/components/moment-route.test.tsx` - -## Product contract - -Route: - -```text -/moments -``` - -The `/moments` page lists all Moments. - -Each Moment row/card must include: - -- section/chapter title -- source memory title -- source memory URL or display source -- created/saved timestamp -- enough section metadata to clarify where the Moment points - -Clicking a Moment navigates to: - -```text -/memories/:memoryId# -``` - -If the anchor is stale or missing, navigate to the memory reader page and surface a non-blocking notice rather than failing the whole route. - -## Data model contract - -Add a `moments` table. - -Recommended schema: - -```ts -moments: { - id: string; - memoryId: string; - sectionAnchor: string; - sectionTitle: string; - sectionLevel: number; - sectionPath: string; - sectionStartOffset: number | null; - sectionEndOffset: number | null; - contentHash: string | null; - createdAt: Date; - updatedAt: Date; -} -``` - -Rules: - -- `memoryId` references `memories.id` with `onDelete: "cascade"`. -- A memory deletion deletes its Moments. -- `sectionAnchor` is the reader anchor used for navigation. -- `sectionTitle` is the displayed chapter/section title at save time. -- `sectionLevel` is heading level where available. -- `sectionPath` identifies the section in the document outline, for example heading-index path or generated ToC path. -- `sectionStartOffset` and `sectionEndOffset` are canonical reader-text offsets for the section when available. -- `contentHash` is computed from canonical reader text or a section-anchor source hash when available. -- Do not write Moment state to `CONTENT.md`. - -Uniqueness: - -- Prefer one Moment per `(memoryId, sectionAnchor)` initially. -- Re-saving the same section should be idempotent and return the existing Moment. -- If anchor generation can change, repository logic should detect an existing matching `sectionPath` before creating duplicates. - -Indexing: - -- Add an index on `memoryId` if needed for reader page lookups. -- Add an index on `createdAt` if `/moment` lists by newest first. -- Do not add broad speculative indexes. - -## Section identity and stale handling - -Moment attaches to reader sections/chapters, not arbitrary selected text. - -Primary identity: - -- `memoryId` -- `sectionAnchor` -- `sectionPath` - -Staleness guards: - -- `sectionTitle` -- optional section text offsets -- optional `contentHash` - -Policy: - -1. If the reader still has `sectionAnchor`, navigate to that anchor. -2. If the anchor is missing but `sectionPath` resolves uniquely, use the resolved section and update the stored anchor if safe. -3. If neither resolves uniquely, keep the Moment row but mark it stale in UI. -4. Do not guess between multiple candidate sections with the same title/path ambiguity. - -## API contract - -### List Moments - -```http -GET /api/moments -``` - -Response: - -```json -{ - "moments": [ - { - "id": "moment-id", - "memoryId": "memory-id", - "memoryTitle": "Memory title", - "memoryUrl": "https://example.com/article", - "sectionAnchor": "heading-anchor", - "sectionTitle": "Chapter title", - "sectionLevel": 2, - "createdAt": "2026-05-14T00:00:00.000Z" - } - ] -} -``` - -### Create Moment - -```http -POST /api/moments -content-type: application/json - -{ - "memoryId": "memory-id", - "sectionAnchor": "heading-anchor", - "sectionTitle": "Chapter title", - "sectionLevel": 2, - "sectionPath": "0/3/1", - "sectionStartOffset": 120, - "sectionEndOffset": 480, - "contentHash": "optional" -} -``` - -Responses: - -- `201` when created -- `200` with `alreadyExists: true` when the Moment already exists -- `400` for malformed payload -- `400` when the supplied section does not resolve uniquely in the memory reader section model -- `404` for missing memory - -Server validation: - -- Do not trust client-supplied `sectionAnchor`, `sectionTitle`, `sectionPath`, - or offsets as proof that a valid section exists. -- The create route must load the target memory's reader section model before - insertion. -- The route must verify that the supplied section identity resolves uniquely for - the memory. -- Prefer validating by `sectionAnchor` first, then by `sectionPath`; use title - and offsets as guards, not as sufficient identity on their own. -- Reject the request if the section is missing, ambiguous, or belongs to a - different memory. -- Store normalized section metadata from the server-resolved section, not - blindly from the request body. - -### Delete Moment - -```http -DELETE /api/moments/:momentId -``` - -Responses: - -- `204` when deleted -- `404` when missing - -## Reader UI contract - -Moment can be set from a section/chapter in three ways: - -1. Hover a ToC chapter row and show a Moment icon at its left edge. -2. Hover a reader section heading and show a Moment icon at its left edge. -3. Long-press a ToC chapter or reader section heading to open the same contextual menu component used for text selection; in heading mode, that menu includes a Moment item. - -Terminology: - -- UI label should use `Moment`. -- Icon may be a bookmark-shaped icon because the behaviour is bookmark-like. - -Hover icon rules: - -- The Moment icon appears only when hovering a chapter/section affordance. -- The icon must not shift text layout when it appears. -- Clicking the icon creates the Moment. -- If the section is already Momented, render an active state. - -Long-press/menu rules: - -- Reuse the same contextual menu component used by reader text selection. -- Text-selection mode shows Flashback actions only. -- Section-heading mode shows Moment actions. -- Arbitrary body text selection must not show Moment actions. -- A long-press on a heading opens the menu with `Moment`. -- A long-press on a ToC row opens the menu with `Moment`. - -This means the menu component needs a mode or action-list input, for example: - -```ts -type ReaderContextMenuMode = - | { kind: "text-selection"; actions: ["flashback"] } - | { kind: "section"; actions: ["moment"] }; -``` - -Do not fork two visually similar menu components. - -## ToC contract - -If the current reader already extracts a ToC/outline: - -- extend that section model with Moment metadata -- preserve existing anchors - -If the current reader does not expose a stable ToC model: - -- introduce a section model from sanitized reader headings -- generate stable anchors consistently with reader heading anchors -- use the same section model for ToC rendering and Moment creation - -Required section fields: - -- `anchor` -- `title` -- `level` -- `path` -- optional `startOffset` -- optional `endOffset` - -## `/moment` route contract - -Add a route: - -```text -/moment -``` - -Navigation: - -- Add `Moment` to the app shell navigation. - -Rendering: - -- List all Moments newest first unless a later design specifies grouping. -- Show source memory title and section title. -- Clicking a row navigates to the reader section anchor. -- Provide an empty state when there are no Moments. - -Do not implement category/tag filters for Moment in this subtask. - -## Tests - -Backend tests: - -- migration creates `moments` -- Moment row references memory and cascades on memory deletion -- create Moment succeeds -- create same `(memoryId, sectionAnchor)` is idempotent -- create missing memory returns `404` -- create missing or ambiguous section identity returns `400` -- create stores server-resolved section metadata rather than blindly trusting the request body -- list Moments includes memory metadata -- delete Moment succeeds -- deleting a memory removes its Moments - -Reader/section tests: - -- reader page data includes section metadata required by Moment -- heading hover renders Moment icon -- ToC hover renders Moment icon -- clicking heading Moment icon creates Moment -- clicking ToC Moment icon creates Moment -- long-press heading opens contextual menu with Moment action -- text selection contextual menu does not show Moment action -- arbitrary body text long-press does not create a section Moment - -Route tests: - -- `/moment` lists all Moments -- Moment item links to `/memories/:memoryId#` -- empty state renders when no Moments exist - -## Verification - -```sh -mise exec -- bun run test tests/server/db/schema.test.ts -mise exec -- bun run test tests/server/db/repositories.test.ts -mise exec -- bun run test tests/server/routes/api-moments.test.ts -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/components/reader-moment-actions.test.tsx -mise exec -- bun run test tests/components/moment-route.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- `/moment` route exists. -- App navigation exposes Moment. -- Moments persist in SQLite. -- Moments do not modify `CONTENT.md`. -- Moments attach to sections/chapters only. -- ToC chapter hover exposes a Moment icon. -- Reader section heading hover exposes a Moment icon. -- Heading/ToC long-press opens the shared reader contextual menu with Moment action. -- Ordinary text selection does not expose Moment. -- Moment list items navigate to the target memory section. -- Memory deletion cascades Moments. diff --git a/docs/workflows/archive/task-18-memory-actions/11-imported-media-display-policy.md b/docs/workflows/archive/task-18-memory-actions/11-imported-media-display-policy.md deleted file mode 100644 index 43b0d6a3..00000000 --- a/docs/workflows/archive/task-18-memory-actions/11-imported-media-display-policy.md +++ /dev/null @@ -1,195 +0,0 @@ -# 18.11 Imported media display policy - -## Goal - -Keep imported media safe without taking ownership of readable-content -extraction. Defuddle owns article cleanup and markdown serialization; TRAUMA -owns fetch/payload trust boundaries, persistence, and reader sanitization. - -The importer must not depend on a custom HTML-to-Markdown converter or a -TRAUMA-defined readability threshold. Media safety is enforced where markdown is -rendered, because persisted markdown can come from URL import, browser import, -fixtures, or future migration paths. - -## Files likely owned - -- `src/server/importer/extractor.ts` -- `src/server/reader/markdown-renderer.ts` -- `src/server/browser-import/import-browser-capture.ts` -- `tests/server/importer/importer.test.ts` -- `tests/server/reader/markdown-renderer.test.ts` -- `tests/server/browser-import/import-browser-capture.test.ts` - -Use actual existing test paths if they differ. - -## Product contract - -Imported content should: - -- Preserve Defuddle markdown as the canonical extracted content artifact. -- Accept short non-empty Defuddle markdown. Empty markdown is the only - readability fallback condition owned by TRAUMA. -- Render HTTPS images from public named hosts, even when the image host differs - from the article host. -- Render controlled HTTPS iframes when they pass the reader sanitizer. - -Reader rendering should reject: - -- `http:` media URLs -- `data:`, `blob:`, `javascript:`, `file:`, and other non-HTTPS media URLs -- URL userinfo -- blocked local/private hostnames -- `srcdoc` -- event-handler attributes such as `onclick` -- arbitrary iframe attributes not allowed by the reader sanitizer - -## Image URL Policy - -Do not rewrite imported markdown URLs in the importer. When rendering markdown: - -1. Require absolute `https:` image URLs. -2. Reject username/password userinfo. -3. Reject blocked local/private hostnames and IP literals. -4. Sanitize `srcset` candidate-by-candidate. -5. Strip unsafe image tags instead of making the browser fetch them. - -Do not require image hostname equality with the page hostname. - -This should preserve examples such as: - -- Medium article page with image on `miro.medium.com`. -- X/Twitter article page with image on `pbs.twimg.com`. -- Ordinary articles using a CDN or external image host. - -## `` Policy - -Defuddle may produce markdown image syntax or raw `` HTML. TRAUMA -does not reconstruct `` markup during import. The reader sanitizer is -responsible for allowing only safe `source srcset` candidates and safe fallback -`img src` values. - -Rationale: - -- The `` fallback is the browser-compatible canonical fallback. -- `srcset` selection requires viewport/DPR/media evaluation, which is out of scope for a storage importer. - -## Iframe policy - -Allowing iframes is acceptable only through a controlled policy. `https` alone is not enough because an iframe can execute a full third-party web app inside the reader. - -Required iframe preservation rules: - -1. Resolve `src` relative to the page URL. -2. Require `https:`. -3. Reject username/password userinfo. -4. Reject blocked local/private hostnames. -5. Remove `srcdoc`. -6. Remove all `on*` event-handler attributes. -7. Preserve only a minimal attribute set: - - `src` - - `title` - - `loading` - - `allowfullscreen` - - optional `width` - - optional `height` -8. Force or normalize: - - `loading="lazy"` - - `referrerpolicy="no-referrer"` - - `sandbox` with a deliberately limited value - -Recommended initial sandbox: - -```html -sandbox="allow-scripts allow-presentation" -``` - -Do not include `allow-same-origin` by default. Combining `allow-scripts` and -`allow-same-origin` materially weakens the sandbox boundary for third-party -content. If a specific allowlisted provider demonstrably requires -`allow-same-origin`, add it only for that provider with tests and a documented -reason. - -Initial iframe permission matrix: - -| Embed type | Initial sandbox | Rationale | -| --- | --- | --- | -| YouTube/Vimeo-style video embed | `allow-scripts allow-presentation` | Supports script-driven player boot while avoiding same-origin storage access by default. | -| Generic article embed | `allow-scripts allow-presentation` | Treat as untrusted executable content; do not grant same-origin unless provider-specific tests prove it is required. | -| X/Twitter timeline/post embed | Not guaranteed in this subtask | If it fails without `allow-same-origin`, keep it blocked or add a provider-specific policy with tests rather than broadening the default. | - -Reader sanitizer alignment: - -- Update `src/server/reader/markdown-renderer.ts` so reader-side iframe validation matches importer/capture policy. -- If reader remains host-allowlist-only, importer must not preserve broader iframes than reader can render. -- Prefer a shared helper for media URL and iframe policy if it prevents drift. - -## Browser Extension Capture Policy - -The extension captures a bounded visible DOM snapshot only. It must not decide -whether the page is long enough to become a memory, generate final markdown, or -write persisted content. The server reruns Defuddle against the captured HTML. - -CSS background images: - -- Out of scope unless a site-specific extractor needs it. -- Do not parse arbitrary `style="background-image: ..."` generically in this subtask. - -## Server URL Importer Policy - -Rules: - -- URL fetch and redirect validation remain importer-owned. -- Defuddle async fallback fetches remain disabled unless they are routed through - the same public-host/timeout/size controls. -- Non-empty Defuddle markdown is importable even when it is short. -- Empty Defuddle markdown becomes link-only fallback for URL import and a - browser-import error for browser-assisted import. - -## Tests - -Importer tests: - -- Defuddle markdown output is persisted without a TRAUMA readability threshold. -- Short non-empty Defuddle markdown succeeds. -- Empty Defuddle markdown falls back or errors at the appropriate import - boundary. - -Reader sanitizer tests: - -- HTTPS iframe with safe attributes is preserved. -- HTTP iframe is rejected. -- `srcdoc` iframe is rejected or stripped so it cannot render inline HTML. -- event-handler attributes are removed. -- Unsafe image and `srcset` URLs are stripped before the browser can load them. -- unsafe iframe attributes are removed. -- reader sanitizer applies `loading="lazy"` and `referrerpolicy="no-referrer"`. -- reader sanitizer applies or preserves the required sandbox. -- default iframe sandbox omits `allow-same-origin`. -- any provider-specific use of `allow-same-origin` has explicit tests and a documented reason. - -Extension capture tests: - -- safe HTTPS iframe survives sanitized snapshot with limited attrs. -- unsafe iframe is removed. -- cross-host HTTPS image survives snapshot. -- event handlers and `srcdoc` are removed. - -## Verification - -```sh -mise exec -- bun run test tests/server/importer/importer.test.ts -mise exec -- bun run test tests/server/reader/markdown-renderer.test.ts -mise exec -- bun run test tests/server/routes/api-browser-import.test.ts -mise exec -- bun run test tests/browser-extension/capture.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Same-host-only media display validation is removed for images. -- HTTPS cross-host images can be preserved in imported Markdown. -- `` with `` preserves the image. -- Controlled HTTPS iframes can be preserved. -- Unsafe iframe forms are still rejected or sanitized. -- Reader sanitizer and importer/capture media policy do not drift. -- Link validation is not accidentally relaxed unless explicitly tested. diff --git a/docs/workflows/archive/task-18-memory-actions/12-integration-verification-and-handoff.md b/docs/workflows/archive/task-18-memory-actions/12-integration-verification-and-handoff.md deleted file mode 100644 index fd73f1b0..00000000 --- a/docs/workflows/archive/task-18-memory-actions/12-integration-verification-and-handoff.md +++ /dev/null @@ -1,156 +0,0 @@ -# 18.12 Integration verification and handoff - -## Goal - -Run cross-domain verification after subtasks 18.1 through 18.11 are complete. This subtask should not add new feature scope except fixes required by failed verification. - -## Files likely owned - -- tests touched by prior subtasks -- optional Playwright/E2E coverage if the project already has a suitable flow -- PR description / handoff notes - -## Required integration checks - -Manual smoke: - -1. Start from an existing database with memories. -2. Confirm existing memories appear unread after migration. -3. Add a new memory and confirm it is unread. -4. Toggle a memory to read on `/memories`. -5. Open the reader and confirm it shows read. -6. Toggle back to unread in the reader. -7. Return to `/memories` and confirm unread. -8. Create `New tag` from the right pane. -9. Create `New category` from the right pane. -10. Attach a tag to a memory from the browse footer `Add tag`. -11. Attach a category to a memory from the memory action menu. -12. Confirm right-pane taxonomy lists include all records. -13. Confirm taxonomy sort follows count, recent assignment, name. -14. Confirm reader renders only the active memory's taxonomy. -15. Confirm a link-only memory renders `Link-only` and not `Saved`. -16. Delete a memory from `/memories`. -17. Confirm it disappears without refresh. -18. Confirm SQLite memory metadata is gone. -19. Confirm the content directory under `storePath` is gone. -20. Confirm git backup records a deletion for the removed content path when backup is enabled. -21. Delete a memory from reader mode. -22. Confirm the app navigates to `/memories`. -23. Open `/settings`. -24. Change translation target language and confirm the persisted API response uses BCP 47, for example `ja-JP`. -25. Enable OpenAI auth from disabled state only when a real provider or stored auth record is available. -26. If no real OpenAI auth provider exists, confirm enable returns not-configured and does not fake enabled state. -27. Send a direct enable request while already enabled and confirm the response is already-enabled without mutation. -28. Delete OpenAI auth and confirm UI returns to disabled state. -29. Select repeated text in reader content and confirm no flashback is created until the flashback icon is clicked. -30. Click the flashback icon and confirm only the selected occurrence is flashbacked. -31. Confirm `CONTENT.md` did not change after creating/removing the flashback. -32. Confirm flashback `contentHash` uses the documented `sha256:` canonical-text format. -33. Confirm SQLite-only flashback metadata has a backup/export path, or that restore-risk is explicitly documented if deferred. -34. Confirm reader Flashback tabs render `Current` on the left and `All` second, with `Current` as the default. -35. Confirm `/memories` right-rail Flashback shortcut list keeps the shared row design. -36. Create a Moment from a reader section hover icon. -37. Create a Moment from a ToC chapter hover icon. -38. Long-press a reader section and confirm the shared contextual menu contains Moment. -39. Select arbitrary body text and confirm the contextual menu does not contain Moment. -40. Confirm the Moment create API rejects missing or ambiguous section anchors/paths. -41. Open `/moments` and confirm the saved Moments are listed. -42. Click a Moment and confirm it navigates to the memory section anchor. -43. Import content containing a cross-host HTTPS image and confirm the image is preserved. -44. Import content containing a Medium-style `` with `miro.medium.com` image fallback and confirm the image is preserved. -45. Import content containing a controlled HTTPS iframe and confirm the reader sanitizer renders it with sandbox/referrer controls. -46. Confirm default iframe sandbox does not include `allow-same-origin`. -47. Confirm unsafe iframe forms such as `srcdoc`, `http:`, event handlers, or local hosts are rejected or stripped. - -## Regression risks to check - -- `CONTENT.md` frontmatter did not gain tags/categories/read. -- `CONTENT.md` body is not rewritten for flashback persistence. -- SQLite-only flashback persistence has backup/export coverage or an explicit restore-risk note. -- Flashbacks still render in reader mode. -- Flashback toggle/removal still persists if existing behaviour supports it. -- Browse category/tag filters still work. -- Browser import/add memory flow still creates memories. -- Backup queue still stages content paths only. -- Normal memory deletion has an explicit backup deletion path and does not leave deleted `CONTENT.md` tracked for restore. -- Backup failsafe delete-missing-record remains distinct from normal memory delete. -- Full taxonomy right pane does not disappear under active filters. -- Settings API does not return OpenAI credential material. -- Flashbacks do not modify `CONTENT.md`. -- Flashbacks cascade when the owning memory is deleted. -- Flashback create validates section identity server-side before insert. -- Cross-host HTTPS article images are preserved. -- Controlled HTTPS iframes are preserved without unsafe attributes. -- Controlled HTTPS iframes do not use `allow-same-origin` by default. - -## Commands - -Targeted suite: - -```sh -mise exec -- bun run test tests/server/db/schema.test.ts -mise exec -- bun run test tests/server/db/repositories.test.ts -mise exec -- bun run test tests/server/routes/api-memory-read-status.test.ts -mise exec -- bun run test tests/server/routes/api-memory-delete.test.ts -mise exec -- bun run test tests/server/routes/api-taxonomy.test.ts -mise exec -- bun run test tests/server/routes/api-settings.test.ts -mise exec -- bun run test tests/server/routes/api-flashbacks.test.ts -mise exec -- bun run test tests/server/routes/api-flashbacks-toggle.test.ts -mise exec -- bun run test tests/server/importer/importer.test.ts -mise exec -- bun run test tests/server/reader/markdown-renderer.test.ts -mise exec -- bun run test tests/server/routes/api-browser-import.test.ts -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/server/flashbacks/toggle.test.ts -mise exec -- bun run test tests/server/flashbacks/ranges.test.ts -mise exec -- bun run test tests/server/flashbacks/flashback-markers.test.ts -mise exec -- bun run test tests/server/settings/settings.test.ts -mise exec -- bun run test tests/memories/browse-data.test.ts -mise exec -- bun run test tests/components/memory-action-menu.test.tsx -mise exec -- bun run test tests/components/memory-read-status.test.tsx -mise exec -- bun run test tests/components/taxonomy-create-popover.test.tsx -mise exec -- bun run test tests/components/memory-browse-actions.test.tsx -mise exec -- bun run test tests/components/app-shell-taxonomy.test.tsx -mise exec -- bun run test tests/components/memory-reader-actions.test.tsx -mise exec -- bun run test tests/components/settings-page.test.tsx -mise exec -- bun run test tests/components/memory-reader-flashback-selection.test.tsx -mise exec -- bun run test tests/components/reader-flashback-tabs.test.tsx -mise exec -- bun run test tests/components/reader-moment-actions.test.tsx -mise exec -- bun run test tests/components/moment-route.test.tsx -mise exec -- bun run test tests/browser-extension/capture.test.ts -``` - -Full project verification: - -```sh -mise exec -- bun run verify -``` - -Optional E2E if local browser verification is available: - -```sh -mise exec -- bun run test:e2e -``` - -## PR handoff checklist - -PR body must include: - -- schema/migration summary -- API summary -- deletion consistency strategy -- backup deletion strategy for removed memory content -- flashback record strategy -- flashback metadata backup/export strategy or explicit restore-risk -- Moment section identity strategy -- settings/OpenAI auth validation strategy -- UI summary -- imported media validation strategy -- exact verification commands and outcomes -- any known deferred items, especially category/tag rename/delete, bulk actions, or richer OpenAI auth provider work - -## Acceptance criteria - -- All Task 18 subtasks are implemented. -- Full verification passes or failures are documented with clear blocker status. -- The PR does not include unrelated refine layout changes. -- The final behaviour matches the original Task 18 intent without relying on hidden branch state. diff --git a/docs/workflows/archive/task-18-memory-actions/13-review-followup-implementation-alignment.md b/docs/workflows/archive/task-18-memory-actions/13-review-followup-implementation-alignment.md deleted file mode 100644 index c90a268a..00000000 --- a/docs/workflows/archive/task-18-memory-actions/13-review-followup-implementation-alignment.md +++ /dev/null @@ -1,385 +0,0 @@ -# 18.13 Review follow-up implementation alignment - -## Goal - -Align the already-in-progress `feat/task-18-memory-actions` implementation with -the Task 18 review-driven specification changes added on -`workflow18-read-status`. - -This is a follow-up correction plan, not a new product feature slice. The goal -is to make the current Task 18 implementation conform to the revised docs -without reopening Task 19. - -## Branch and worktree contract - -Implementation branch: - -```text -feat/task-18-memory-actions -``` - -Implementation worktree: - -```text -/Users/vvx/projekt/www/trauma -``` - -Spec source branch: - -```text -workflow18-read-status -``` - -Before editing implementation code, merge the spec branch into the -implementation branch: - -```sh -cd /Users/vvx/projekt/www/trauma -git fetch origin -git merge origin/workflow18-read-status -``` - -Use merge rather than rebase. The implementation branch already contains -substantial Task 18 work and its history should not be rewritten for this -follow-up. - -## Scope - -In scope: - -- Task 18 settings language contract. -- Task 18 OpenAI auth placeholder behaviour. -- Task 18 memory deletion and backup deletion strategy. -- Task 18 flashback `contentHash` format. -- Task 18 Flashback server-side section validation. -- Task 18 imported iframe sandbox default. -- Tests and docs needed to prove the above implementation alignment. - -Out of scope: - -- Task 19 Codex translation runner. -- Task 19 translated content storage. -- Task 19 translation backup queueing. -- Task 19 stale translation status. -- Any unrelated UI redesign or Task 17 refine work. - -Do not edit files under: - -```text -docs/workflows/task-19-codex-translation/ -docs/workflows/task-19-codex-translation.md -``` - -## 1. Merge updated Task 18 specification - -Steps: - -1. Confirm the worktree is on `feat/task-18-memory-actions`. -2. Confirm there are no uncommitted implementation changes, or commit/stash - them before merging. -3. Fetch `origin`. -4. Merge `origin/workflow18-read-status`. -5. Resolve conflicts by preserving implementation work and accepting the revised - Task 18 contracts. - -Acceptance criteria: - -- `feat/task-18-memory-actions` contains the latest Task 18 docs from - `workflow18-read-status`. -- No Task 19 implementation is changed as part of the merge. - -## 2. Settings language contract - -Problem: - -The implementation may still use short language codes such as `ja`, while the -revised Task 18 contract requires BCP 47 values such as `ja-JP`. - -Required behaviour: - -- Persist supported BCP 47 language codes. -- Return BCP 47 language codes from settings APIs. -- Use `ja-JP` as the Japanese value. -- UI select values must match the server-supported values. -- Reject unsupported language values server-side. -- If short codes are accepted for compatibility, normalize them before storage - and always return the canonical BCP 47 value. - -Implementation targets: - -- Settings schema/repository/service. -- Settings API routes. -- Settings page loader/action code. -- Settings page select options. -- Settings tests. - -Acceptance criteria: - -- `GET /api/settings` returns `"translationTargetLanguage": "ja-JP"` for the - Japanese default or saved value. -- `PATCH /api/settings/translation-language` accepts supported BCP 47 values. -- The settings UI reloads without select-value mismatch. -- Tests cover supported, unsupported, and malformed language values. - -## 3. OpenAI auth placeholder behaviour - -Problem: - -Task 18 owns the settings page surface, but it does not yet implement Task 19's -real Codex-backed auth. The implementation must not fake enabled auth before a -real provider exists. - -Required behaviour: - -- If a real provider or stored auth record exists, enable may return enabled. -- If no real provider exists, enable returns a clear not-configured response. -- The canonical HTTP status for provider-missing `not_configured` is `409`. -- Provider-missing enable attempts must leave auth status disabled. -- Already-enabled requests remain idempotent and do not overwrite auth state. -- API responses must never include secret material. -- The service result type must represent `not_configured` explicitly as a normal - outcome, not as an exception and not as a fake enabled state. - -Implementation targets: - -- OpenAI auth adapter. -- Settings auth service. -- `POST /api/settings/openai-auth/enable`. -- `DELETE /api/settings/openai-auth`. -- Settings page UI error/status handling. -- Settings auth tests. - -Acceptance criteria: - -- Provider-missing enable returns `409` with `not_configured` and does not - create a cosmetic enabled row. -- Already-enabled enable returns the documented already-enabled response. -- Delete auth clears only auth state. -- Tests prove no secret material is returned. - -## 4. Memory deletion backup strategy - -Problem: - -Deleting a memory from SQLite and the local `storePath` can leave the removed -`CONTENT.md` tracked in the backup repository. That can resurrect or retain a -deleted memory during restore. - -Required behaviour: - -- Normal memory deletion must have an explicit backup deletion path. -- When backup is enabled, the deleted content path must be queued or recorded so - the backup worker can stage and commit the removal. -- Local SQLite/filesystem deletion success remains separate from remote push - success. -- Backup push failure should use the existing warning channel rather than - silently hiding the problem. - -Implementation targets: - -- Memory deletion service. -- Backup queue types and worker. -- Backup status/warning handling if needed. -- Delete API tests. - -Recommended design: - -- Add a backup job kind for deletion, for example - `{ kind: "delete", contentPaths: [...] }`. -- The backup worker stages removed paths and commits the deletion. -- If the current queue can already express deleted paths safely, document that - path and add regression tests. - -Acceptance criteria: - -- Deleting a memory removes SQLite metadata and local content directory. -- Backup-enabled deletion records/stages the removed content path. -- Restore cannot silently bring back a deleted memory from an old tracked - `CONTENT.md`. -- Tests cover backup-enabled deletion. - -## 5. Flashback `contentHash` format - -Problem: - -The revised flashback contract requires a concrete hash format so flashback -creation, validation, and stale detection cannot drift. - -Required behaviour: - -- `contentHash` format is `sha256:`. -- Hash input is canonical reader text, not raw Markdown. -- Use the exact same canonical text used for `startOffset` and `endOffset`. -- Normalize line endings to `\n` before offset calculation and hashing. -- Do not trim leading or trailing text for hashing. -- Do not apply Unicode compatibility normalization unless the same - normalization is shared by offset calculation and rendering. -- On hash mismatch, do not render a flashback at a guessed location. - -Implementation targets: - -- Canonical reader text utility. -- Flashback creation API. -- Flashback range validation. -- Flashback rendering/stale handling. -- Flashback tests. - -Acceptance criteria: - -- Duplicate selected text is disambiguated by offsets. -- `contentHash` uses `sha256:`. -- Hash and offset calculations share one canonical text path. -- Stale/hash-mismatched flashbacks are not rendered at the wrong occurrence. - -## 5a. Flashback metadata backup/export - -Problem: - -Task 18 makes flashback persistence SQLite-only and stops writing flashback -markers into `CONTENT.md`. If the built-in git backup still backs up only -Markdown content and not SQLite, new flashbacks can be lost on restore. - -Required behaviour: - -- Do not reintroduce `CONTENT.md` mutation as the normal flashback persistence - mechanism. -- Add a backup/export strategy for flashback metadata when SQLite is not backed - up directly. -- The backup representation must preserve enough data to restore flashback - records: `memoryId`, selected `text`, offsets, `prefix`, `suffix`, - `contentHash`, and timestamps where useful. -- Prefer a deterministic metadata export under the memory backup scope, or a - backup job that serializes flashback metadata with tests. -- If full metadata restore is intentionally deferred, the implementation PR must - state the restore-risk explicitly and must not claim full backup parity for - SQLite-only flashbacks. - -Implementation targets: - -- Flashback creation/removal service. -- Backup queue/export layer. -- Restore or import path if one exists. -- Flashback backup/export tests. - -Acceptance criteria: - -- Creating/removing flashbacks does not modify `CONTENT.md`. -- Flashback metadata is backed up/exported or the restore-risk is explicitly - documented in the PR. -- Tests cover the chosen backup/export behaviour, or the PR records the - intentionally deferred risk. - -## 6. Flashback server-side section validation - -Problem: - -The API must not trust client-supplied section anchors, titles, paths, or -offsets. A direct API caller should not be able to create Flashbacks for -sections that do not exist. - -Required behaviour: - -- The create route loads the target memory's reader section model server-side. -- Resolve by `sectionAnchor` first. -- If needed, resolve by `sectionPath`. -- Use title and offsets as guards, not sufficient identity. -- Reject missing or ambiguous section identity with `400`. -- Store normalized metadata from the server-resolved section. - -Implementation targets: - -- Reader section model/page-data. -- Flashback creation service. -- `POST /api/flashbacks`. -- Flashback repository if it currently trusts request metadata. -- Flashback API tests. - -Acceptance criteria: - -- Missing section anchors cannot create Flashbacks. -- Ambiguous sections are rejected rather than guessed. -- Stored Flashback metadata comes from the server-resolved section. -- `/moments` links cannot be broken by direct API calls with fake anchors. - -## 7. Imported iframe sandbox default - -Problem: - -The default iframe sandbox must not combine `allow-scripts` with -`allow-same-origin`, because that weakens the sandbox boundary for third-party -content. - -Required behaviour: - -- Default sandbox is: - -```html -sandbox="allow-scripts allow-presentation" -``` - -- Do not include `allow-same-origin` by default. -- Only add `allow-same-origin` for a provider-specific policy with tests and a - documented reason. -- Continue stripping `srcdoc`, `on*` handlers, unsafe attributes, and unsafe URL - schemes. -- Continue applying `loading="lazy"` and `referrerpolicy="no-referrer"`. - -Implementation targets: - -- Server importer media sanitizer. -- Reader Markdown sanitizer. -- Browser extension capture sanitizer. -- Media/importer/capture tests. - -Acceptance criteria: - -- Safe iframe output omits `allow-same-origin` by default. -- Unsafe iframe forms remain rejected or sanitized. -- Importer, reader, and browser capture policies do not drift. - -## 8. Focused verification - -Run the smallest relevant test set for changed implementation surfaces. - -Recommended targeted commands: - -```sh -mise exec -- bun run test tests/server/routes/api-settings.test.ts -mise exec -- bun run test tests/server/settings/settings.test.ts -mise exec -- bun run test tests/components/settings-page.test.tsx -mise exec -- bun run test tests/server/routes/api-memory-delete.test.ts -mise exec -- bun run test tests/server/routes/api-flashbacks-toggle.test.ts -mise exec -- bun run test tests/server/flashbacks/ranges.test.ts -mise exec -- bun run test tests/server/routes/api-flashbacks.test.ts -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/server/importer/importer.test.ts -mise exec -- bun run test tests/server/reader/markdown-renderer.test.ts -mise exec -- bun run test tests/server/routes/api-browser-import.test.ts -mise exec -- bun run test tests/browser-extension/capture.test.ts -mise exec -- bun run typecheck -``` - -If a listed path does not exist, use the nearest current test file that covers -the same surface and record the substitution in the PR body. - -## PR handoff - -The implementation PR must state: - -- Which `workflow18-read-status` commit was merged. -- Settings language normalization behaviour. -- OpenAI auth provider-missing behaviour. -- Memory deletion backup strategy. -- Flashback hash format. -- Flashback server-side section validation strategy. -- Iframe sandbox policy. -- Exact verification commands and outcomes. -- Task 19 review items remain intentionally out of scope. - -## Completion criteria - -- `feat/task-18-memory-actions` implements every in-scope revised Task 18 - contract above. -- Task 19 files and behaviour are not changed. -- Review comments for Task 18 can be answered with implementation evidence. -- Any remaining known gap is documented explicitly in the implementation PR. diff --git a/docs/workflows/archive/task-18-memory-actions/14-product-language-migration.md b/docs/workflows/archive/task-18-memory-actions/14-product-language-migration.md deleted file mode 100644 index 80d872d8..00000000 --- a/docs/workflows/archive/task-18-memory-actions/14-product-language-migration.md +++ /dev/null @@ -1,180 +0,0 @@ -# 18.14 Product language migration - -## Goal - -Change TRAUMA product language consistently across docs and define the -implementation impact for `feat/task-18-memory-actions`. - -The product language changes from: - -```text -flashback: marker -Flashback: bookmark -``` - -to: - -```text -Flashback: marker -Moment: bookmark -``` - -This file is the workflow plan for aligning implementation after the docs -language change. This branch edits docs only. SQLite schema, route, API, and -component changes are implemented on `feat/task-18-memory-actions`. - -## Canonical terms - -`Flashback` -: A user-created text marker inside reader content. This replaces the older -product term `flashback`. - -`Moment` -: A saved reader section/chapter bookmark. This replaces the older use of -`Flashback` for section bookmarks. - -Legacy terms: - -- `flashback` means old text-marker language and must not appear in new - user-facing docs or UI copy except in migration notes. -- `bookmark` may describe generic behaviour, but user-facing product copy should - use `Moment`. -- Older `Flashback` bookmark language must migrate to `Moment`. - -## Documentation impact inventory - -The docs search surface currently includes these areas: - -- `docs/architecture/` -- `docs/references/design-system/` -- `docs/references/glossary.md` -- `docs/quality/verification.md` -- `docs/operations/local-self-hosting.md` -- `docs/superpowers/specs/2026-05-09-trauma-foundation-design.md` -- `docs/workflows/README.md` -- `docs/workflows/task-18-memory-read-status.md` -- `docs/workflows/task-18-memory-actions/` -- active workflow files that still describe flashback or Flashback behaviour - -Docs that are historical execution records may keep legacy terms only when the -legacy term is clearly historical. Durable architecture, reference, design, and -active workflow docs must use the new product language. - -## Implementation impact inventory - -The implementation branch must account for the following likely rename surfaces. - -SQLite/data model: - -- Old marker table/records: `flashbacks` -- New marker product concept: Flashbacks -- Old bookmark table/records planned as `flashbacks` -- New bookmark product concept: Moments -- Decide whether physical table names migrate now or whether compatibility - aliases are kept temporarily. -- If renaming tables, provide migrations that preserve existing rows. -- If keeping legacy table names for implementation safety, ensure API/UI/product - language exposes `Flashback` and `Moment` while documenting the technical debt. - -Routes/API: - -- Old marker route: `/flashbacks` -- New marker route: `/flashbacks` -- Old section-bookmark route: `/flashback` -- New section-bookmark route: `/moments` -- Old marker API names likely under `/api/flashbacks` -- New marker API names should become `/api/flashbacks` or provide a compatibility - bridge. -- Old bookmark API planned under `/api/flashbacks` -- New bookmark API should become `/api/moments` -- Add redirects or compatibility routes only if needed for existing saved links. - -Components: - -- `FlashbackExcerpt` becomes Flashback excerpt language. -- Memories right-rail Flashback shortcuts use the island title `Flashback`. -- Reader flashback selection menu becomes Flashback marker selection. -- Flashback section bookmark UI becomes Moment UI. -- Flashback icon attached to headings/ToC becomes Moment icon. -- `/flashback` route components become Moments route components. - -Tests: - -- Rename user-facing assertions from flashback to Flashback. -- Rename section-bookmark assertions from Flashback to Moment. -- Keep implementation-level legacy names only where compatibility is deliberate. - -## Execution plan - -1. Merge `workflow18-read-status` into `feat/task-18-memory-actions`. -2. Audit current implementation names for marker and section-bookmark surfaces. -3. Decide whether database tables are renamed immediately or wrapped by - product-language API aliases. -4. Rename user-facing marker language from flashback to Flashback. -5. Rename section-bookmark language from Flashback/bookmark to Moment. -6. Update routes and navigation to the chosen canonical paths. -7. Add compatibility redirects if old routes may already be linked. -8. Update tests to assert the new product language. -9. Update durable docs after implementation decisions are known. - -## Required implementation decisions - -Table naming: - -- Preferred final state is `flashbacks` for marker records and `moments` for - section bookmarks. -- If this is too risky for the current implementation branch, use compatibility - adapters and record a follow-up debt item. - -Route naming: - -- Preferred final state is `/flashbacks` for marker browsing and `/moments` for - section bookmarks. -- If old routes remain, they should redirect or be explicitly marked legacy. - -Type naming: - -- Product-facing types should use `Flashback` and `Moment`. -- Internal transitional names are acceptable only behind repository/service - boundaries. - -Backup/export naming: - -- Flashback metadata backup/export from 18.13 becomes Flashback metadata - backup/export. -- Moment backup/deletion strategy must follow the section-bookmark data model, - not the old Flashback bookmark language. - -## Docs edit checklist - -Durable docs: - -- Update architecture docs to describe Flashbacks as text markers. -- Update architecture docs to describe Moments as section bookmarks. -- Update UI/routing docs for `/flashbacks` and `/moments`. -- Update data/storage docs for marker/bookmark schema ownership. -- Update quality docs for Flashback marker and Moment bookmark verification. -- Update design-system docs for labels, tabs, icons, excerpts, and navigation. -- Update glossary and foundation design references. - -Workflow docs: - -- Update Task 18 overview and subtask names. -- Update 18.9 from reader flashback selection to Flashback marker selection. -- Update 18.10 from Flashback section bookmarks to Moment section bookmarks. -- Update 18.12 integration checklist. -- Update 18.13 follow-up plan terms. -- Keep explicit migration notes for implementation agents so old code names can - be mapped safely. - -## Acceptance criteria - -- User-facing docs consistently use Flashback for text markers. -- User-facing docs consistently use Moment for section bookmarks. -- Any remaining `flashback` usage is explicitly legacy, code-path, syntax - flashbacking, or migration terminology. -- Any remaining `bookmark` usage is generic behaviour or explicitly mapped to - Moment. -- Implementation branch has a clear schema/API/component migration checklist. -- Task 19 docs are not modified by this Task 18 product-language migration - unless the user explicitly reopens Task 19. diff --git a/docs/workflows/archive/task-18-memory-actions/15-memory-delete-consistency-and-backup-hardening.md b/docs/workflows/archive/task-18-memory-actions/15-memory-delete-consistency-and-backup-hardening.md deleted file mode 100644 index 67cd0123..00000000 --- a/docs/workflows/archive/task-18-memory-actions/15-memory-delete-consistency-and-backup-hardening.md +++ /dev/null @@ -1,280 +0,0 @@ -# 18.15 Memory delete consistency and backup hardening - -## Goal - -Harden memory deletion so the API contract, SQLite state, local content store, -and git backup deletion path cannot drift. - -This is a correction subtask for Task 18. It does not add a new product feature. -It clarifies the delete contract and closes failure-boundary gaps found while -reviewing the current implementation. - -## Current evidence - -Current route behaviour: - -- `DELETE /api/memories/:memoryId` returns `204 No Content` on success. -- The client-side delete helper treats any `response.ok` result as success and - does not parse a response body. -- Therefore `No Content` is not itself an inconsistency. It is the current - success contract for DELETE. - -Current implementation risk: - -- The service stages the content directory, deletes the SQLite memory row, then - removes the staged directory and enqueues a backup deletion job. -- If post-row-delete staging cleanup throws, the memory row can be gone while - sensitive content remains under `.delete-staging/`. -- If backup enqueue throws, the request can fail after the memory row and - canonical content directory are already gone. -- API-level delete coverage proves Flashback and taxonomy cascades, but it does - not yet seed and verify Moment cascade through the public DELETE route. -- Backup deletion currently stages `CONTENT.md` only. Current Task 18 also - writes `FLASHBACKS.json`, so deletion backup must include that export path or - stage the whole deleted memory directory. - -## Target delete contract - -Delete target: - -1. The SQLite `memories` row for the requested memory id. -2. SQLite rows that are owned by that memory through cascade relationships: - Flashbacks, Moments, and memory taxonomy join rows. -3. The canonical local content directory under - `storePath/memories/{memoryId}/`. -4. The backup repository's tracked representation of that memory content, - including `CONTENT.md` and Flashback metadata exports such as - `FLASHBACKS.json`. - -Non-target: - -- Global `tags` and `categories` records. -- Unrelated store paths. -- Task 19 translation records or translated content, unless Task 19 later adds - its own cascade/delete contract. - -## Delete order decision - -Use staged filesystem deletion before SQLite row deletion. - -The preferred order is: - -1. Load the memory deletion target from SQLite. -2. Resolve `contentPath` against `storePath` and reject paths escaping the store. -3. Rename the canonical memory content directory to a private staging path under - `storePath/.delete-staging/`. -4. Delete the SQLite memory row. -5. If the SQLite deletion fails, restore the staged directory to the canonical - memory directory and return failure. -6. Remove the staged directory. If this fails, return a controlled failure and - log the staged path; content erasure is not complete while staged content - remains on disk. -7. Attempt to queue or record the backup deletion job. If this fails, record a - warning instead of changing the local deletion result. -8. Return `204 No Content` after local deletion is complete. - -Rationale: - -- Deleting the SQLite row first can leave an orphaned canonical content - directory if filesystem deletion fails. -- Deleting filesystem content first without staging can leave a SQLite row that - points to missing content if database deletion fails. -- Staging first makes the canonical content path disappear while preserving a - rollback path until the SQLite row deletion succeeds. -- Staging cleanup is part of local content erasure and must not be masked as - success. -- Backup enqueue/push must not decide whether local deletion succeeded. Backup - failures belong to the backup warning/retry channel. - -## Domain plan - -### 1. API contract - -Files: - -- `src/routes/api/memories/[memoryId].ts` -- `tests/server/routes/api-memory-delete.test.ts` - -Required decisions: - -- Keep `204 No Content` as successful DELETE output. -- Do not return deleted memory JSON. -- Ensure unexpected service exceptions are converted into a controlled `500` - response instead of leaking framework-level errors. -- Error responses should keep the existing user-facing shape: - -```json -{ "error": "failed to delete memory" } -``` - -Regression tests: - -- Successful delete returns status `204` and empty body. -- A service failure before local deletion returns `500`. -- A route-level unexpected service throw returns `500` without deleting - additional data beyond the failure point being tested. - -### 2. Memory deletion service boundary - -Files: - -- `src/server/memories/delete-memory.ts` -- `tests/server/memories/delete-memory.test.ts` - -Required behaviour: - -- Treat path-resolution failure as a normal `{ status: "failed" }` result. -- Treat non-`ENOENT` staging failure as `{ status: "failed" }` before mutating - SQLite. -- Keep missing content directory tolerant: a memory record may still be deleted - when its content directory is already absent. -- When git backup is enabled, stage the content removal and run the deletion - backup synchronously before deleting the SQLite row. A deletion backup must - not be delegated to the asynchronous memory backup queue because the row is - the retry state. -- Restore the staged directory if SQLite row deletion fails. -- Treat staged-directory cleanup failure after successful backup and SQLite - deletion as a `deleted` result with a precise cleanup warning. At that point - the canonical content directory is gone, the deletion backup is durable, and - the SQLite row is gone; surfacing it as a failed delete would misrepresent the - completed canonical delete. -- Do not let backup enqueue failure turn a completed local deletion into a - failed user-visible delete. -- Return success only after the canonical content directory is gone and the - SQLite row is gone. If staged cleanup fails, return `deleted` with a - `content_cleanup_failed` warning for operator cleanup. - -Recommended result shape: - -```ts -export type DeleteMemoryResult = - | { status: "deleted"; warnings?: DeleteMemoryWarning[] } - | { status: "not_found" } - | { status: "failed"; error: string }; - -export type DeleteMemoryWarning = - | { kind: "backup_enqueue_failed"; error: string } - | { kind: "content_cleanup_failed"; error: string }; -``` - -The route may still return `204` for `deleted` with warnings. The warning should -be logged or routed through the existing backup warning channel; it should not -make the user's local deletion appear to have failed. - -Regression tests: - -- Backup enqueue throws after SQLite/content deletion: service resolves - `{ status: "deleted", warnings: [...] }`, memory row is gone, content directory - is gone. -- Git-backed deletion: service creates a deletion backup commit before removing - the SQLite row, and the async backup queue rejects `memory_deletion` jobs. -- SQLite deletion throws after staging: service returns `failed` and restores the - content directory. -- Staged-directory cleanup throws after SQLite deletion: service returns - `deleted` with a `content_cleanup_failed` warning, and the warning includes - enough path context for manual cleanup. -- Missing content directory with existing SQLite row: service deletes the row - and returns `deleted`. - -### 3. SQLite cascade coverage - -Files: - -- `tests/server/routes/api-memory-delete.test.ts` -- `tests/server/db/repositories.test.ts` - -Required behaviour: - -- Public DELETE route coverage must seed at least one Moment owned by the memory - and assert it is gone after deletion. -- Repository-level cascade tests may remain as lower-level proof, but they are - not enough for the public route contract. - -Regression tests: - -```sql -select count(*) as count from moments -``` - -Expected after successful API delete: - -```json -{ "count": 0 } -``` - -### 4. Backup deletion path - -Files: - -- `src/server/backup/index.ts` -- `src/server/memories/delete-memory.ts` -- `tests/server/backup/git-backup.test.ts` -- `tests/server/memories/delete-memory.test.ts` - -Required behaviour: - -- A normal git-backed memory deletion must run the backup deletion job - synchronously for the deleted backup-tracked content before removing the - SQLite row. -- The backup runner must stage deletion for the tracked memory content path. -- Remote push failure during synchronous deletion backup must stop the local - deletion and restore staged content so the SQLite row remains retryable. - -Decision to apply now: - -- `CONTENT.md` is not the only backup-tracked file in a memory directory. -- Task 18 Flashbacks are backed by SQLite and exported to `FLASHBACKS.json`. -- Memory deletion must stage the deletion of both `CONTENT.md` and - `FLASHBACKS.json`, or stage every tracked path under - `storePath/memories/{memoryId}/`. -- If future work stores local images, captures, translations, or sidecar assets - under `storePath/memories/{memoryId}/`, that feature must update this backup - deletion contract to stage every tracked path under the memory directory. - -Regression tests: - -- Backup-enabled deletion creates a `reason: "memory_deletion"` backup commit - before the SQLite row is deleted. -- Backup runner commits deleted `CONTENT.md` and `FLASHBACKS.json` paths when - both were tracked. -- The asynchronous backup queue rejects `reason: "memory_deletion"` jobs. -- Backup enqueue failure does not make the delete API return failure after the - SQLite row and canonical content directory are gone. - -### 5. Manual verification - -Run after implementation: - -```sh -mise exec -- bun --bun x vitest run tests/server/routes/api-memory-delete.test.ts tests/server/memories/delete-memory.test.ts tests/server/db/repositories.test.ts tests/server/backup/git-backup.test.ts tests/components/memory-action-menu.test.ts tests/components/memory-browse-actions.test.ts tests/components/memory-reader-actions.test.ts -``` - -Manual smoke: - -1. Start the app with an existing memory. -2. Delete the memory from `/memories`. -3. Confirm the network response is `204 No Content`. -4. Confirm the memory disappears without refresh. -5. Confirm the SQLite `memories` row is gone. -6. Confirm `storePath/memories/{memoryId}/` is gone. -7. If backup is enabled, confirm git status or the latest backup commit records - deletion of `storePath/memories/{memoryId}/CONTENT.md` and - `storePath/memories/{memoryId}/FLASHBACKS.json` when the export exists. -8. Delete a memory from `/memories/:id`. -9. Confirm the app navigates back to `/memories`. -10. Confirm no user-facing failure is shown for successful `204 No Content`. - -## Acceptance criteria - -- `204 No Content` is documented and tested as successful DELETE output. -- A successful delete removes the SQLite row and canonical memory content - directory. -- A successful delete does not leave the deleted content under - `storePath/.delete-staging/`. -- Flashbacks, Moments, memory tags, and memory categories cascade through the - public DELETE route. -- Global tags and categories remain. -- Backup deletion is queued or recorded for the removed tracked content. -- Backup enqueue/push failure does not make completed local deletion appear to - have failed. -- The plan does not change Task 19 translation ownership. diff --git a/docs/workflows/archive/task-18-memory-actions/16-pr-review-followup-cache-and-backup.md b/docs/workflows/archive/task-18-memory-actions/16-pr-review-followup-cache-and-backup.md deleted file mode 100644 index 76fa4563..00000000 --- a/docs/workflows/archive/task-18-memory-actions/16-pr-review-followup-cache-and-backup.md +++ /dev/null @@ -1,440 +0,0 @@ -# 18.16 PR review follow-up: cache, Flashback backup, and stale anchors - -## Goal - -Turn the actionable Task 18 implementation PR review comments into implementation work after the -Task 18 product-language migration. - -This is a correction subtask. It does not reopen Task 19 and does not change the -desktop design work from Task 17. - -## Review sweep - -Source: - -- Pull request: `https://github.com/hauntedfail/Trauma/pull/21` -- Head branch: `feat/task-18-memory-actions` -- Base branch: `workflow18-read-status` -- Thread-aware review read: 16 review threads, no pagination. - -Vocabulary normalization: - -- Review comments that say `highlight` now mean Task 18 `Flashback`. -- Review comments that say section-bookmark `Flashback` now mean Task 18 - `Moment`. -- Old wording does not invalidate backend findings when the data-flow issue - still exists. - -Non-actionable review items: - -- CodeRabbit skipped automatic review because the base branch is not the - default branch. This is not an implementation issue. -- The top-level Codex review item about refreshing highlight lists after - successful toggles is covered by the Flashback revalidation tasks below. - -## Review thread ledger - -All actionable Task 18 implementation PR review threads are mapped below. -Outdated line anchors are still evaluated when the backend or cache issue -remains true after the product-language migration. - -| Review thread | Current product term | Disposition | -| --- | --- | --- | -| Keep highlight changes in backed-up markdown | Keep Flashback changes durable outside SQLite | Covered by domain C. Do not rewrite `CONTENT.md`; use `FLASHBACKS.json` durability. | -| Back up memory deletions before dropping the record | Memory deletion backup | Covered by domain F and 18.15. | -| Revalidate cached browse data after reader deletes | Reader delete cache coherence | Covered by domain A. | -| Resolve stale Flashback anchors before linking | Resolve stale Moment anchors before linking | Covered by domain E. | -| Revalidate browse data after card taxonomy actions | Card taxonomy cache coherence | Covered by domain A. | -| Revalidate browse cache after card deletes | Browse delete cache coherence | Covered by domain A. | -| Ignore stale highlight ranges when rendering | Ignore stale Flashback ranges when rendering | Covered by domain D. | -| Revalidate cached memories after read toggles | Read status cache coherence | Covered by domain A. | -| Revalidate browse data after reader category adds | Reader taxonomy cache coherence | Covered by domain A. | -| Refresh reader highlight tabs after toggles | Refresh reader Flashback tabs after toggles | Covered by domains A and B. | -| Revalidate deleted reader entries | Reader delete cache invalidation | Covered by domain A. | -| Reset reader-local state on memory changes | Reader state reset | Covered by domain B. | -| Persist highlights only after export succeeds | Persist Flashbacks only after export succeeds | Covered by domain C. | -| Mark highlight backups queued before returning | Mark Flashback backups queued before returning | Covered by domain C. | -| Include highlight exports in deletion backups | Include Flashback exports in deletion backups | Covered by domain F and 18.15. | -| Retry failed highlight backups with the export path | Retry failed Flashback backups with the export path | Covered by domain C. | - -## Required review threads by domain - -### A. Mutation cache revalidation - -Review findings covered: - -- Revalidate browse data after reader deletes. -- Revalidate browse cache after card deletes. -- Revalidate cached reader entry after reader deletes. -- Revalidate cached memories after read toggles. -- Revalidate browse data after card taxonomy actions. -- Revalidate browse data after reader category adds. -- Refresh Flashback lists after successful Flashback toggles. - -Files: - -- `src/components/memories/browse-loader.ts` -- `src/components/flashbacks/flashbacks-loader.ts` -- `src/components/moments/moments-loader.ts` -- `src/components/reader/reader-memory-loader.ts` -- `src/routes/memories/[id].tsx` -- `src/components/memories/MemoryBrowse.tsx` -- `src/components/memories/MemoryReadStatusControl.tsx` -- `src/components/reader/MemoryReader.tsx` -- `tests/components/memory-browse-actions.test.ts` -- `tests/components/memory-read-status.test.ts` -- `tests/components/memory-reader-actions.test.ts` -- `tests/components/memory-reader-flashback-selection.test.ts` - -Implementation plan: - -1. Move the reader query out of the route file into a reusable loader: - -```ts -// src/components/reader/reader-memory-loader.ts -import { query, revalidate } from "@solidjs/router"; - -import { loadReaderMemory } from "~/server/reader/page-data"; - -export const getReaderMemory = query(async (memoryId: string) => { - "use server"; - - return loadReaderMemory(memoryId); -}, "reader-memory"); - -export function revalidateReaderMemory(memoryId?: string) { - return revalidate( - memoryId === undefined ? getReaderMemory.key : getReaderMemory.keyFor(memoryId), - ); -} -``` - -2. Update `src/routes/memories/[id].tsx` to import `getReaderMemory` from the - new loader and remove the inline query. - -3. Add a Flashback browse revalidation helper: - -```ts -// src/components/flashbacks/flashbacks-loader.ts -import { query, revalidate } from "@solidjs/router"; - -export function revalidateFlashbackBrowseRows() { - return revalidate(getFlashbackBrowseRows.key); -} -``` - -4. Add a shared client helper that revalidates all browse-side data affected by - Task 18 mutations: - -```ts -// src/components/memories/browse-loader.ts -export function revalidateBrowseMemoryWorkspace() { - return Promise.all([ - revalidateBrowseMemories(), - revalidateBrowseTaxonomy(), - ]); -} -``` - -5. After read/unread success, revalidate browse memory data and the active - reader cache for the memory id. - -6. After card tag/category attach success, revalidate browse memories and - taxonomy. - -7. After card delete success, revalidate browse memories, taxonomy, Flashback - browse rows, Moment browse rows, and the deleted reader cache key. - -8. After reader delete success, revalidate the same affected caches before - navigating to `/memories`. - -9. After reader category attach success, revalidate browse memories, taxonomy, - and reader memory for the current id. - -10. After Flashback toggle success, parse the successful API response, update - the reader-local current Flashbacks list, and revalidate: - -```ts -void revalidateFlashbackBrowseRows(); -void revalidateReaderMemory(input.memoryId); -void revalidateBrowseMemories(); -``` - -Acceptance criteria: - -- A deleted memory cannot reappear after navigation back from `/memories`. -- A deleted reader page does not render from cached `reader-memory` when the - browser back button is used. -- Read/unread state remains correct after returning to `/memories`. -- Newly attached tags/categories update right-rail counts and filters without a - hard refresh. -- Flashback `Current` and `All` lists update after a successful toggle. - -### B. Reader-local state reset on memory changes - -Review finding covered: - -- Reset reader-local state on memory changes. - -Files: - -- `src/components/reader/MemoryReader.tsx` -- `tests/components/memory-reader-actions.test.ts` - -Implementation plan: - -1. Add a `currentFlashbacks` signal initialized from - `props.result.memory.flashbacks`. -2. Use `currentFlashbacks()` in `ReaderRightRailContent` instead of - `props.result.memory.flashbacks`. -3. Add a prop-change effect keyed by `props.result.memory.id`. -4. When the memory id changes, reset reader-local state: - -```ts -setCategories([...props.result.memory.categories]); -setMoments([...props.result.memory.moments]); -setCurrentFlashbacks([...props.result.memory.flashbacks]); -setPendingMomentKey(""); -setPendingSelectionKey(""); -setErrorMessage(""); -closeReaderMenus(); -``` - -5. Keep cleanup of right-rail content on component unmount. - -Acceptance criteria: - -- Direct client navigation from one reader memory to another does not inherit - category chips, Moment state, Flashback tabs, pending states, or errors from - the previous memory. - -### C. Flashback persistence, export, and backup ordering - -Review findings covered: - -- Keep Flashback changes durable outside SQLite. -- Persist Flashbacks only after export succeeds. -- Mark Flashback backups queued before returning. -- Retry failed Flashback backups with the export path. - -Files: - -- `src/server/flashbacks/toggle.ts` -- `src/server/flashbacks/export.ts` -- `src/server/backup/index.ts` -- `src/server/db/repositories.ts` -- `tests/server/flashbacks/toggle.test.ts` -- `tests/server/backup/git-backup.test.ts` - -Implementation plan: - -1. Preserve the Task 18 decision that `CONTENT.md` is not rewritten for - Flashbacks. The durable backup artifact for Flashbacks is - `memories/{memoryId}/FLASHBACKS.json`. - -2. Capture the previous Flashback DB rows and previous export file state before - replacement. - -3. Replace Flashback rows only as part of a service operation that can - compensate on export or enqueue failure. - -4. Write `FLASHBACKS.json` atomically. The export writer should write a temp file - under the same memory directory and rename it to `FLASHBACKS.json`. - -5. If export write fails after DB replacement, restore the previous Flashback DB - rows before returning an API failure. - -6. If backup enqueue fails after DB replacement/export write, restore the - previous Flashback DB rows and previous export file before returning an API - failure. - -7. If backup is enabled and enqueue returns `queued`, update the owning memory's - `backup_status` to `queued` before the API returns. Clear stale - `last_backup_error`. - -8. If backup is disabled, keep the memory backup status `disabled`. - -9. Update backup retry logic so retrying pending/queued/failed memories enqueues - both the memory content path and the Flashback export path: - -```ts -contentPaths: [ - backup.contentPath, - getFlashbackMetadataExportPath(backup.id), -] -``` - -The runner already tolerates paths with no staged diff after `git add`; this -keeps content and Flashback export retry behaviour aligned. - -Acceptance criteria: - -- A Flashback API failure after export/enqueue failure does not leave SQLite in - the new state. -- A successful Flashback toggle leaves SQLite, `FLASHBACKS.json`, and memory - backup status in one consistent state. -- Startup retry for a failed Flashback backup stages `FLASHBACKS.json`, not only - `CONTENT.md`. -- Tests cover backup enabled, backup disabled, export failure, enqueue failure, - and retry. - -### D. Flashback stale range tolerance in reader rendering - -Review finding covered: - -- Ignore stale Flashback ranges when rendering. - -Files: - -- `src/server/reader/page-data.ts` -- `src/server/flashbacks/toggle.ts` -- `src/server/store/flashback-markers.ts` -- `tests/server/reader/page-data.test.ts` -- `tests/server/flashbacks/flashback-markers.test.ts` - -Implementation plan: - -1. Treat `FlashbackMarkerError` from applying saved Flashback records as stale - Flashback metadata, not as a reader-route crash. - -2. Prefer filtering invalid Flashback records over dropping all records: - -```ts -const renderResult = renderMarkdownWithValidFlashbacks(content.markdown, memory.flashbacks); -``` - -The helper should return: - -```ts -{ - markdown: string; - staleFlashbackIds: string[]; -} -``` - -3. If focused filtering is too invasive, catch `FlashbackMarkerError` in - `loadReaderMemory`, render clean markdown with no Flashback marks, and keep - the route available. This is acceptable as an intermediate implementation - only if tests prove the route no longer crashes. - -4. Do not guess new offsets for stale Flashbacks. - -Acceptance criteria: - -- Out-of-bounds or protected-range Flashback records do not crash - `/memories/:id`. -- The reader opens the memory content without invalid marks. -- Stale Flashback metadata remains available for later cleanup/export work - unless explicitly deleted by user action. - -### E. Moment stale anchor resolution - -Review finding covered: - -- Resolve stale section-bookmark anchors before linking. - -Vocabulary mapping: - -- Old review term `Flashback` in this thread refers to current Task 18 `Moment`. - -Files: - -- `src/server/moments/browse.ts` -- `src/server/db/repositories.ts` -- `src/components/moments/MomentBrowse.tsx` -- `src/components/reader/MemoryReader.tsx` -- `tests/server/browse-loaders.test.ts` -- `tests/components/moment-route.test.ts` -- `tests/components/reader-moment-actions.test.ts` - -Implementation plan: - -1. Add Moment target resolution against the current reader ToC. - -2. Resolution order: - -```text -stored sectionAnchor exact match - -> unique current sectionPath match - -> stale unresolved Moment -``` - -3. Extend Moment browse rows with: - -```ts -targetAnchor: string | null; -targetStatus: "current" | "resolved_from_path" | "stale"; -``` - -4. Build hrefs with `targetAnchor`, not always the stored `sectionAnchor`. - -5. If `targetStatus` is `stale`, render the Moment row as non-blocking stale - state and avoid a dead fragment link. - -6. Do not silently mutate the stored Moment row while browsing. If automatic - repair is desired later, add a separate explicit repair workflow. - -Acceptance criteria: - -- A Moment whose heading id changed but section path is still unique links to - the current anchor. -- A Moment whose section cannot be resolved does not link to a dead fragment. -- Reader-side Moment active state uses the resolved target where available. - -### F. Delete backup coverage from PR review - -Review findings covered: - -- Back up memory deletions before dropping the record. -- Include Flashback exports in deletion backups. - -Owning plan: - -- Execute the detailed delete plan in - `docs/workflows/task-18-memory-actions/15-memory-delete-consistency-and-backup-hardening.md`. - -Additional requirement from this review sweep: - -- Deletion backup must include `FLASHBACKS.json` in addition to `CONTENT.md`, or - stage every tracked path under the deleted memory directory. - -## Verification - -Targeted test command: - -```sh -mise exec -- bun --bun x vitest run tests/server/routes/api-memory-delete.test.ts tests/server/memories/delete-memory.test.ts tests/server/flashbacks/toggle.test.ts tests/server/backup/git-backup.test.ts tests/server/reader/page-data.test.ts tests/server/browse-loaders.test.ts tests/components/memory-browse-actions.test.ts tests/components/memory-read-status.test.ts tests/components/memory-reader-actions.test.ts tests/components/memory-reader-flashback-selection.test.ts tests/components/reader-moment-actions.test.ts tests/components/moment-route.test.ts -``` - -Full verification before handoff: - -```sh -mise exec -- bun run typecheck -mise exec -- bun run verify -``` - -Manual smoke: - -1. Toggle read/unread on `/memories`, navigate away/back, and confirm state - stays current. -2. Attach tag/category from a memory card and confirm right rail updates. -3. Attach category from reader and confirm `/memories` reflects it. -4. Create and remove a Flashback; confirm reader body, `Current`, and `All` - Flashback lists update. -5. Delete from reader, navigate back with browser history, and confirm deleted - content does not reappear. -6. Edit or simulate stale Flashback offsets and confirm reader does not crash. -7. Edit or simulate stale Moment anchors and confirm Moment list avoids dead - fragments. -8. Delete a memory with `CONTENT.md` and `FLASHBACKS.json` tracked by backup and - confirm both deletions are staged/committed. - -## Acceptance criteria - -- Every actionable Task 18 implementation PR review thread is either implemented - by this subtask or explicitly delegated to 18.15. -- Current product language is used in new code and docs. -- No implementation writes Flashback marks into `CONTENT.md`. -- Flashback durability is provided through SQLite plus `FLASHBACKS.json` backup. -- Cache revalidation keeps browse, reader, right rail, Flashback, Moment, and - taxonomy views coherent after Task 18 mutations. diff --git a/docs/workflows/archive/task-18-memory-actions/README.md b/docs/workflows/archive/task-18-memory-actions/README.md deleted file mode 100644 index dcffeb29..00000000 --- a/docs/workflows/archive/task-18-memory-actions/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Task 18 subtasks - -Implement these subtasks sequentially on `feat/task-18-memory-actions`. - -## Order - -1. [18.1 Data model and repository foundation](01-data-model-and-repository-foundation.md) -2. [18.2 API and mutation service layer](02-api-and-mutation-service-layer.md) -3. [18.3 Shared UI primitives](03-shared-ui-primitives.md) -4. [18.4 Browse memory item actions](04-browse-memory-item-actions.md) -5. [18.5 Right-pane taxonomy management](05-right-pane-taxonomy-management.md) -6. [18.6 Reader memory actions](06-reader-memory-actions.md) -7. [18.8 Settings page and OpenAI auth state](08-settings-page-and-openai-auth.md) -8. [18.9 Reader Flashback marker selection and tabs](09-reader-flashback-marker-selection-and-tabs.md) -9. [18.10 Moment section bookmarks](10-moment-section-bookmarks.md) -10. [18.11 Imported media display policy](11-imported-media-display-policy.md) -11. [18.12 Integration verification and handoff](12-integration-verification-and-handoff.md) -12. [18.13 Review follow-up implementation alignment](13-review-followup-implementation-alignment.md) -13. [18.14 Product language migration](14-product-language-migration.md) -14. [18.15 Memory delete consistency and backup hardening](15-memory-delete-consistency-and-backup-hardening.md) -15. [18.16 PR review follow-up: cache, Flashback backup, and stale anchors](16-pr-review-followup-cache-and-backup.md) - -## Rules for agents - -- Own only the domain named by the subtask. -- Do not pull unrelated Task 17/refine layout work into this branch. -- Preserve the existing content-store frontmatter contract. -- Use current product language: Flashback means text marker, Moment means section bookmark. -- Prefer repository/service methods over raw SQL in route files. -- Add focused tests in the same subtask that introduces behaviour. -- Stop if a migration or filesystem deletion strategy would rewrite unrelated data. diff --git a/docs/workflows/archive/task-18-memory-read-status.md b/docs/workflows/archive/task-18-memory-read-status.md deleted file mode 100644 index b988d9fc..00000000 --- a/docs/workflows/archive/task-18-memory-read-status.md +++ /dev/null @@ -1,82 +0,0 @@ -# Task 18: Memory actions and read status - -## Status - -- State: Ready for sequential implementation -- Base branch: `main` -- Implementation branch: `feat/task-18-memory-actions` -- Execution model: implement the subtasks below in order, one PR/agent slice at a time unless explicitly approved otherwise. -- Out of scope: Task 17/refine layout redesign, reader typography redesign, bulk actions, category/tag rename/delete, full taxonomy management screens, backup repository migration. - -## Core intent - -Task 18 adds user-owned memory metadata and memory actions: - -- persistent `read: boolean` -- memory deletion -- tag/category creation -- tag/category assignment to memories -- browse rendering for link-only memories -- reusable memory action menu on browse and reader pages -- settings page for translation target language and OpenAI auth state -- explicit reader selection menu for Flashback marker creation -- Flashback marker records that identify the selected occurrence without mutating `CONTENT.md` -- reader-mode Flashback tabs for all markers vs current-memory markers -- Moment bookmarks for reader sections, with a `/moments` route listing all saved Moments -- product-language migration from the legacy text-marker term to `Flashback` and from section-bookmark `Flashback` to `Moment` -- relaxed imported media display policy for HTTPS cross-host images and controlled HTTPS iframes - -The implementation must stay domain-scoped. Do not import unrelated refine-branch UI work. If the refine branch is the only place where a reusable memory meatballs menu exists, extract only that reusable component contract or recreate the equivalent shared component in this branch. - -## Global invariants - -- SQLite owns memory metadata, read status, tags, categories, and memory-taxonomy assignments. -- `CONTENT.md` frontmatter does not gain `read`, `tags`, or `categories`. -- New memories default to unread. -- Link-only memories render `Link-only` with an error icon. -- Successful full-content memories do not render a redundant `Saved` label. -- One memory action menu component is shared by `/memories` items and `/memories/:id` reader header. -- Deleting a memory removes the SQLite memory row and the corresponding filesystem content directory. -- Deleting a memory cascades Flashbacks, Moments, and join-table rows, but does not delete global tags/categories. -- Right-pane tags/categories list all records, not only records visible under the current browse filter. -- Reader mode renders only tags/categories attached to the active memory. -- `/settings` is part of Task 18, not a separate workflow. -- Settings APIs must validate state server-side; frontend disabled controls are not a security boundary. -- Flashback marker persistence is record-based. `CONTENT.md` must not be rewritten to store marker marks. -- Flashback marker records identify a selected occurrence by canonical reader-text offsets plus guard context, not by selected text alone. -- `/memories` right-rail Flashback shortcut list keeps the shared row design. -- Reader-mode Flashback component gets tabs below its title: left tab `Current`, - second tab `All`, with `Current` as the default. -- Moment is the TRAUMA product name for section bookmarks. -- Moments attach to reader sections/chapters, not arbitrary selected text. -- Moment persistence is SQLite-backed metadata; it must not rewrite `CONTENT.md`. -- `/moments` lists every Moment with its section title and linked memory metadata. -- Imported article media may reference a different HTTPS host from the source page. Same-host-only display URL validation is too strict for real articles. -- Images may be preserved from HTTPS public hosts after stripping unsafe input. -- Iframes may be preserved only as controlled HTTPS embeds with dangerous attributes removed and reader-side sandbox/referrer controls applied. -- Do not add speculative indexes. Add indexes only when the subtask proves the query needs them. - -## Subtask execution order - -1. [18.1 Data model and repository foundation](task-18-memory-actions/01-data-model-and-repository-foundation.md) -2. [18.2 API and mutation service layer](task-18-memory-actions/02-api-and-mutation-service-layer.md) -3. [18.3 Shared UI primitives](task-18-memory-actions/03-shared-ui-primitives.md) -4. [18.4 Browse memory item actions](task-18-memory-actions/04-browse-memory-item-actions.md) -5. [18.5 Right-pane taxonomy management](task-18-memory-actions/05-right-pane-taxonomy-management.md) -6. [18.6 Reader memory actions](task-18-memory-actions/06-reader-memory-actions.md) -7. [18.8 Settings page and OpenAI auth state](task-18-memory-actions/08-settings-page-and-openai-auth.md) -8. [18.9 Reader Flashback marker selection and tabs](task-18-memory-actions/09-reader-flashback-marker-selection-and-tabs.md) -9. [18.10 Moment section bookmarks](task-18-memory-actions/10-moment-section-bookmarks.md) -10. [18.11 Imported media display policy](task-18-memory-actions/11-imported-media-display-policy.md) -11. [18.12 Integration verification and handoff](task-18-memory-actions/12-integration-verification-and-handoff.md) -12. [18.13 Review follow-up implementation alignment](task-18-memory-actions/13-review-followup-implementation-alignment.md) -13. [18.14 Product language migration](task-18-memory-actions/14-product-language-migration.md) -14. [18.15 Memory delete consistency and backup hardening](task-18-memory-actions/15-memory-delete-consistency-and-backup-hardening.md) -15. [18.16 PR review follow-up: cache, Flashback backup, and stale anchors](task-18-memory-actions/16-pr-review-followup-cache-and-backup.md) - -Subtask number 18.7 is intentionally unused. The settings subtask keeps the -`18.8` label and `08-...` filename because it was defined after the reader -actions slice and before the flashback slice; do not infer a missing execution -file. - -Each subtask file is written so an implementation agent can own that slice without guessing the broader intent. Later subtasks may reference earlier contracts, but should not reopen completed domain decisions unless implementation evidence proves the plan wrong. diff --git a/docs/workflows/archive/task-19-codex-translation-auth-repair.md b/docs/workflows/archive/task-19-codex-translation-auth-repair.md deleted file mode 100644 index a8e2ffcd..00000000 --- a/docs/workflows/archive/task-19-codex-translation-auth-repair.md +++ /dev/null @@ -1,286 +0,0 @@ -# Task 19R: Codex App-Server Auth Repair Workflow - -## Goal - -Repair the Brilliant Codex auth setup path so a completed Codex app-server -device-code login is detected by TRAUMA, reflected in Settings, and accepted by -the translation runner before scheduling Codex-backed translation work. - -## Status - -- State: Repair workflow draft, tied to Task 19 but separate from the original - Task 19 execution plan. -- Base workflow: [Task 19 Codex translation](task-19-codex-translation.md) -- Primary external reference: - [Codex App Server - OpenAI Developers](https://developers.openai.com/codex/app-server) -- Local protocol reference: generated schema from the installed Codex CLI via - `codex app-server generate-json-schema --out `. - -## Required Context - -- [Documentation index](../../INDEX.md) -- [Task 19 overview](task-19-codex-translation.md) -- [Task 19 app-server integration](task-19-codex-translation/05-codex-app-server-integration.md) -- [Task 19 auth setup flow](task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md) -- [Task 19 prompt and validation contract](task-19-codex-translation/contracts/06-codex-prompt-and-validation.md) -- [Configuration reference](../../references/configuration.md) -- [Verification strategy](../../quality/verification.md) -- [Coding standards](../../references/coding-standards/INDEX.md) - -## Current Failure To Reproduce - -Precondition: start Codex app-server with the Unix listener. - -```bash -codex app-server --listen unix:///tmp/trauma-codex.sock -TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock bun run dev -``` - -Observed failure: - -1. Settings shows `Start setup`. -2. Clicking `Start setup` returns a device-code login response. -3. TRAUMA renders `verificationUrl` and `userCode`. -4. The user completes sign-in at `https://auth.openai.com/codex/device`. -5. TRAUMA remains in `login_started` UI state and translation still fails with - auth/setup errors. - -Live app-server evidence from `account/read` after device-code completion: - -```json -{ - "account": { - "type": "chatgpt", - "email": "", - "planType": "prolite" - }, - "requiresOpenaiAuth": true -} -``` - -This is a valid authenticated ChatGPT account state. The current implementation -incorrectly treats `requiresOpenaiAuth: true` as unauthenticated before checking -whether `account` exists. - -## Root Causes - -1. `src/server/translation/codex-app-server.ts` interprets - `requiresOpenaiAuth === true` as `setup_required` before checking the - `account` field. -2. Auth notification parsing listens for non-protocol auth-prefixed event names, - but the official app-server protocol and generated schema use - `account/login/completed` and `account/updated`. -3. Settings enters `login_started` but does not poll or otherwise refresh - `/api/settings/codex-auth`, so a successful login is not reflected in the - frontend unless a separate refresh path happens. -4. Existing tests and Task 19 contract text encode assumptions that are weaker - than the official app-server auth examples. -5. `checkAuth()` currently calls `account/read` with `refreshToken: true` for - every auth check. This is too aggressive for Settings polling and should not - be the default read path. - -## Ownership - -Primary implementation files: - -- `src/server/translation/codex-app-server.ts` -- `src/server/settings/codex-auth.ts` -- `src/components/settings/SettingsPage.tsx` -- `src/components/settings/settings-submit.ts` -- `src/routes/api/settings/codex-auth.ts` -- `src/routes/api/settings/codex-auth/device-code.ts` - -Primary tests: - -- `tests/server/translation/codex-app-server.test.ts` -- `tests/server/settings/codex-auth.test.ts` -- `tests/components/settings-page.test.ts` -- `tests/server/routes/api-settings.test.ts` - -Documentation to update only where it currently contradicts the official -app-server auth contract: - -- `docs/workflows/task-19-codex-translation/05-codex-app-server-integration.md` -- `docs/workflows/task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md` -- `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` -- `tests/fixtures/translation/codex-app-server-protocol.focused.json` - -## Out Of Scope - -- Starting, supervising, or auto-installing Codex app-server from TRAUMA. -- Storing ChatGPT, Codex, OpenAI access, or refresh tokens in TRAUMA. -- Exposing Codex app-server directly to browser code. -- Replacing Codex app-server with OpenAI Responses API or `codex exec`. -- Reworking translation prompt, chunking, stitching, or reader rendering unless - needed to prove auth-gated translation startup. -- Multi-user auth or hosted OAuth callback service. - -## State And Cache Decision - -Codex app-server remains the source of truth for auth. TRAUMA must not persist -"authenticated" as a durable truth in SQLite because that can become stale after -logout, token expiry, app-server restart, or account switching. - -Allowed TRAUMA state: - -- Process-local pending login metadata: `loginId`, `verificationUrl`, and - `userCode`, used only while setup is pending. -- Optional process-local short-lived auth status cache, if needed to avoid - repeated local socket reads during UI polling. -- Non-secret UI messages derived from the latest confirmed app-server state. - -If an auth status cache is introduced, keep it narrow: - -- Cache only `enabled`, `setup_required`, `disabled`, or `unknown` plus safe - reason text. -- Use a short TTL measured in seconds. -- Invalidate on `account/login/completed`, `account/updated`, logout, cancel, - failed connection, and translation start. -- Bypass the cache before scheduling translation work. - -For this repair, prefer no persisted auth state and no broad cache. Use -`account/read` as the authoritative check, with `refreshToken: false` for UI -status polling and a deliberate forced refresh only where the translation -preflight needs it. - -## Implementation Steps - -1. Regenerate and inspect the installed app-server schema. - - Run `codex --version`. - - Run `codex app-server generate-json-schema --out `. - - Confirm `account/read`, `account/login/start`, - `account/login/completed`, and `account/updated` method names. - - Record the Codex CLI version and schema facts in the PR notes. - -2. Add failing auth interpretation tests. - - In `tests/server/translation/codex-app-server.test.ts`, cover - `account/read` returning both `account` and `requiresOpenaiAuth: true`. - - Expected result: `CodexAppServerClient.checkAuth()` returns - `{ status: "enabled" }`. - - Also cover `account: null` with `requiresOpenaiAuth: true`. - - Expected result: `setup_required` with `auth_required`. - -3. Fix auth interpretation. - - In `CodexAppServerClient.checkAuth()`, treat a non-null account object as - authenticated before considering `requiresOpenaiAuth`. - - Treat `requiresOpenaiAuth: true` as auth-required only when no account is - present. - - Treat `requiresOpenaiAuth: false` as runnable without additional OpenAI - auth. - - Keep unknown payloads as `unknown`, not enabled. - -4. Add failing notification tests. - - In `tests/server/translation/codex-app-server.test.ts`, simulate - `account/login/completed` and `account/updated` notifications from the - fake app-server. - - Expected result: `observeAuthEvents()` yields typed auth events. - - Include a canceled/failed login event with `success: false` and safe - `error` text. - -5. Fix auth notification parsing. - - Parse official methods `account/login/completed` and `account/updated`. - - Preserve strict typed events inside `codex-app-server.ts`; do not expose - raw app-server notification names to settings or frontend code. - - Do not mark auth enabled from the notification alone; call `account/read` - after notification as the confirmation step. - -6. Make `account/read` refresh behavior explicit. - - Extend `checkAuth()` to accept an options object, for example - `{ refreshToken?: boolean }`. - - Use `refreshToken: false` by default. - - Use `refreshToken: false` for Settings status polling. - - Use a deliberate preflight choice for translation startup. If forced - refresh is used there, keep it server-only and do not expose token - material. - -7. Add settings service tests for completed login. - - In `tests/server/settings/codex-auth.test.ts`, create a fake client that - starts pending login, emits an `account.login.completed` typed event from - `observeAuthEvents()`, then returns `{ status: "enabled" }` from - `checkAuth()`. - - Expected result: pending metadata is cleared and - `readCodexAuthStatus()` returns enabled. - - Add a failure path where `success: false` clears pending metadata without - returning enabled. - -8. Add frontend polling tests. - - In `tests/components/settings-page.test.ts`, cover the flow where - `submitEnableOpenAiAuth()` returns `login_started`, then - `/api/settings/codex-auth` later returns `enabled`. - - Expected UI: pending code panel disappears, button changes to `Enabled`, - and enabled copy is visible. - - Keep polling active only while `codexAuth().status === "login_started"`. - -9. Implement frontend auth refresh. - - Add a `submitReadCodexAuth()` helper that calls - `GET /api/settings/codex-auth`. - - In `SettingsPage`, start a cleanup-safe interval or backoff loop while - `login_started`. - - Abort or ignore stale requests when the component unmounts or auth state - changes. - - On enabled, update local state and call `revalidateSettingsState()`. - - On setup-required while pending metadata is still known server-side, keep - showing pending state; do not spam failure messages. - -10. Align docs and fixture assumptions. - - Update Task 19 auth text so `requiresOpenaiAuth` is described as provider - requirement, not a standalone unauthenticated flag. - - Update notification names to `account/login/completed` and - `account/updated`. - - Refresh the focused protocol fixture facts if they currently omit auth - notifications or encode stale method names. - -11. Verify against fake and live app-server paths. - - Run focused tests: - - ```bash - mise exec -- bun run test tests/server/translation/codex-app-server.test.ts - mise exec -- bun run test tests/server/settings/codex-auth.test.ts - mise exec -- bun run test tests/components/settings-page.test.ts - mise exec -- bun run test tests/server/routes/api-settings.test.ts - ``` - - - Run full verification: - - ```bash - mise exec -- bun run verify - git diff --check - ``` - - - Live smoke with app-server: - - ```bash - codex app-server --listen unix:///tmp/trauma-codex.sock - TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock mise exec -- bun run dev - ``` - - Complete device-code login, confirm Settings reaches `enabled`, then - start a translation and confirm it passes the auth preflight. - -## Acceptance Criteria - -- `account/read` with a non-null `account` is treated as authenticated even - when `requiresOpenaiAuth` is `true`. -- `account/read` with no account and `requiresOpenaiAuth: true` remains - setup/auth required. -- `account/login/completed` and `account/updated` notifications are parsed from - official app-server method names. -- Settings refreshes from `login_started` to `enabled` without a manual page - reload after device-code completion. -- Translation startup no longer fails auth preflight after device-code - completion. -- TRAUMA does not persist ChatGPT/OpenAI/Codex tokens or a durable - authenticated boolean in SQLite. -- UI polling does not force token refresh on every request. -- Focused tests and full `mise exec -- bun run verify` pass. - -## PR Handoff - -The PR description must include: - -- Codex CLI version used for schema verification. -- Official app-server auth fields and notification methods used by the fix. -- Before/after behavior for device-code completion. -- Whether auth status caching was added; if yes, list TTL and invalidation - triggers. -- Exact verification commands and outcomes. diff --git a/docs/workflows/archive/task-19-codex-translation-model-controls.md b/docs/workflows/archive/task-19-codex-translation-model-controls.md deleted file mode 100644 index 57159138..00000000 --- a/docs/workflows/archive/task-19-codex-translation-model-controls.md +++ /dev/null @@ -1,489 +0,0 @@ -# Task 19T: Codex Translation Model Controls Workflow - -## Goal - -Add user-controlled Codex translation model and reasoning effort defaults to -Settings, then route source-reader translation through a confirmation popup that -shows the selected model, effort, and language before scheduling Codex-backed -translation. - -## Status - -- State: Repair workflow draft, tied to Task 19 but separate from the original - Task 19 execution plan, Task 19R auth repair, and Task 19S protocol repair. -- Base workflow: [Task 19 Codex translation](task-19-codex-translation.md) -- Related repairs: - [Task 19R Codex app-server auth repair](task-19-codex-translation-auth-repair.md), - [Task 19S Codex app-server protocol repair](task-19-codex-translation-protocol-repair.md) -- Primary external reference: - [OpenAI: Unlocking the Codex harness](https://openai.com/index/unlocking-the-codex-harness/) -- Local protocol reference: the installed Codex app-server `model/list`, - `config/read`, and generated schema for the exact app-server version under - test. - -## Required Context - -- [Documentation index](../../INDEX.md) -- [Task 19 overview](task-19-codex-translation.md) -- [Task 19 app-server integration](task-19-codex-translation/05-codex-app-server-integration.md) -- [Task 19 frontend translation controls and progress UI](task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md) -- [Task 19 error handling and cancellation](task-19-codex-translation/15-error-handling-and-cancellation.md) -- [Task 19 test plan and fixtures](task-19-codex-translation/16-test-plan-and-fixtures.md) -- [Configuration reference](../../references/configuration.md) -- [Design system reference](../../references/design-system/DESIGN.md) -- [Verification strategy](../../quality/verification.md) -- [Coding standards](../../references/coding-standards/INDEX.md) - -## Current Behavior To Replace - -Current source-reader translation is too implicit: - -1. Settings stores only `translation_target_language` and Codex auth state. -2. The source-reader Codex icon starts translation on the first click. -3. The reader sends only `{ "lang_code": "" }` to - `POST /api/memories/:memory_id/translations`. -4. `translation_jobs.model` exists but new jobs currently store `null`. -5. There is no persisted reasoning effort column. -6. `CodexAppServerClient.translateChunk()` sends only prompt, output schema, and - sandbox/thread settings to `turn/start`; it does not pass model or reasoning - effort. - -The repaired flow must make model, effort, and language visible before the job -starts, while preserving one-click access to the popup trigger. - -## Confirmed App-Server Facts - -Codex app-server is the source of truth for available model choices. TRAUMA must -not hardcode a fixed model list. - -Observed local app-server `model/list` shape includes: - -- `id` -- `displayName` -- `description` -- `isDefault` -- `supportedReasoningEfforts` -- `defaultReasoningEffort` - -Observed local app-server `config/read` includes model-related settings such as -`model` and profile-level reasoning effort values. - -The OpenAI app-server integration article describes Codex app-server as exposing -model discovery and configuration management. This means TRAUMA should ask the -connected app-server for model metadata and treat that catalog as version, -configuration, and account dependent. - -## Product And UX Decisions - -Settings owns defaults: - -- Persist the user's default Codex translation model in TRAUMA settings. -- Persist the user's default Codex translation reasoning effort in TRAUMA - settings. -- Use `null` model as "Codex app-server default". -- Use `null` reasoning effort as "selected model default". -- Persist only non-secret choices. Do not persist ChatGPT, Codex, OpenAI access, - or refresh tokens. - -Reader owns per-translation confirmation: - -- The current translation button must not start translation on click. -- Hovering the button expands only its width, keeps the Codex icon left-aligned, - and reveals `Translate` text with CSS transition. -- Clicking the expanded trigger opens a popup/dialog. -- The popup shows model, reasoning effort, and target language before the job is - scheduled. -- The popup's `Translate` button is the submit action that starts translation. -- If the user changes model, effort, or language in the popup, TRAUMA saves those - choices as the new defaults and uses the same values for the submitted job. -- The popup submit button uses a 50% transparent visual treatment. -- The translation progress component uses the same transparent visual language. - -Do not show the translation trigger on translated reader routes or when the -configured target variant already exists. - -## Ownership - -Primary implementation files: - -- `src/server/translation/codex-app-server.ts` -- `src/server/translation/runner.ts` -- `src/server/translation/start-translation-route.ts` -- `src/server/translation/types.ts` -- `src/server/settings/settings.ts` -- `src/server/db/schema.ts` -- `src/server/db/repositories.ts` -- `src/components/settings/SettingsPage.tsx` -- `src/components/settings/settings-loader.ts` -- `src/components/settings/settings-submit.ts` -- `src/components/reader/MemoryReader.tsx` - -Routes to add or update: - -- Add `src/routes/api/settings/codex-models.ts` or an equivalent settings-scoped - model catalog route. -- Add `src/routes/api/settings/translation-codex-defaults.ts` or an equivalent - settings-scoped default update route. -- Update `src/routes/api/memories/[memoryId]/translations.ts`. - -Database files: - -- Add one new Drizzle SQL migration under `drizzle/`. -- Update the latest Drizzle metadata snapshot. -- Update `src/server/db/bundled-migrations.ts`. - -Primary tests: - -- `tests/server/translation/codex-app-server.test.ts` -- `tests/server/translation/runner.test.ts` -- `tests/server/translation/api-routes.test.ts` -- `tests/server/translation/translation-repositories.test.ts` -- `tests/server/settings/settings.test.ts` -- `tests/server/routes/api-settings.test.ts` -- `tests/components/settings-page.test.ts` -- `tests/components/memory-reader-actions.test.ts` -- `tests/server/db/schema.test.ts` - -Documentation to update only where it currently describes the older implicit -translation trigger or omits model and effort persistence: - -- `docs/workflows/task-19-codex-translation.md` -- `docs/workflows/task-19-codex-translation/05-codex-app-server-integration.md` -- `docs/workflows/task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md` -- `docs/workflows/task-19-codex-translation/15-error-handling-and-cancellation.md` -- `docs/workflows/task-19-codex-translation/16-test-plan-and-fixtures.md` -- `docs/references/configuration.md`, only if new configuration behavior is - introduced. Do not add model names there. - -## Out Of Scope - -- Starting, supervising, or auto-installing Codex app-server from TRAUMA. -- Direct OpenAI model-list calls from TRAUMA. -- Replacing Codex app-server with OpenAI Responses API, Codex SDK, or - `codex exec`. -- Storing ChatGPT, Codex, OpenAI access, or refresh tokens in TRAUMA. -- Making completed translation identity depend on model or effort. -- Re-translating automatically when the user changes the default model or - effort. -- Per-memory saved model presets beyond the single global default. -- Reworking translation chunking, prompt policy, stitching, or translated reader - rendering except where required to pass selected model and effort through the - existing job pipeline. - -## Data Model Decision - -Add these fields to `app_settings`: - -- `codex_translation_model text null` -- `codex_translation_reasoning_effort text null` - -Allowed reasoning effort values: - -- `none` -- `minimal` -- `low` -- `medium` -- `high` -- `xhigh` - -Keep both nullable: - -- `codex_translation_model = null` means app-server default model. -- `codex_translation_reasoning_effort = null` means selected model default - effort. - -Add this field to `translation_jobs`: - -- `reasoning_effort text null` - -Keep the existing `translation_jobs.model` column and write the effective -selected model string or `null` when the job uses app-server default. Write -`translation_jobs.reasoning_effort` similarly. - -Do not change `translation_jobs_current_complete_idx`. A completed translation -is still current by memory, language, source hash, prompt policy version, and -chunker version. Model and effort are execution metadata, not output identity. - -## App-Server Catalog Contract - -Add a server-side catalog reader that calls app-server `model/list` after -initialization. The browser must not talk to app-server directly. - -Normalize the response before it reaches frontend code: - -```json -{ - "models": [ - { - "id": "gpt-5.5", - "displayName": "GPT-5.5", - "description": "Frontier model...", - "isDefault": true, - "supportedReasoningEfforts": ["low", "medium", "high", "xhigh"], - "defaultReasoningEffort": "medium" - } - ] -} -``` - -The normalized route may omit hidden or non-text models if app-server returns -them. If a selected model is hidden but app-server still accepts it, do not -surface it as a normal choice unless the app-server catalog marks it as visible. - -Catalog errors: - -- Missing socket or refused connection: `app_server_unavailable`. -- Auth/setup required: reuse existing auth/setup error codes. -- Invalid `model/list` response: `app_server_protocol_error`. - -Settings UI should render a readable unavailable state when the catalog cannot be -loaded, while leaving auth controls and language settings usable. - -## Request Schema Decision - -Extend the reader start request body from: - -```json -{ "lang_code": "ja-JP" } -``` - -to: - -```json -{ - "lang_code": "ja-JP", - "model": "gpt-5.5", - "reasoning_effort": "medium" -} -``` - -`model` and `reasoning_effort` may be omitted or `null` to use defaults. - -Server validation rules: - -1. `lang_code` must match the saved or submitted supported target language. -2. `model` must be a string from the current app-server catalog, or `null`. -3. `reasoning_effort` must be one of the selected model's - `supportedReasoningEfforts`, or `null`. -4. If the model is unavailable, return `translation_model_unavailable`. -5. If the effort is unavailable for that model, return - `translation_reasoning_effort_unavailable`. -6. Do not silently fall back to another model or effort after the user explicitly - submits a popup choice. - -## Implementation Steps - -1. Regenerate and inspect the installed app-server schema. - - Run `codex --version`. - - Run `codex app-server generate-json-schema --out `. - - Confirm `model/list` response shape. - - Confirm whether `turn/start` accepts `model` and `effort` in the - stable schema for the installed version. - - Record the Codex version and schema facts in the PR notes. - -2. Add failing model catalog tests. - - In `tests/server/translation/codex-app-server.test.ts`, fake `model/list` - returning multiple models and efforts. - - Expected result: the client exposes normalized model catalog records. - - Add invalid payload coverage where `model/list` returns malformed model - rows. - - Expected result: `app_server_protocol_error`. - -3. Implement model catalog client support. - - Add typed model catalog structures to - `src/server/translation/codex-app-server.ts`. - - Add `listModels()` or an equivalent method on the app-server client. - - Keep parsing strict enough to catch protocol drift, but tolerant of extra - app-server fields. - -4. Add failing settings persistence tests. - - In `tests/server/settings/settings.test.ts`, assert default settings include - `codexTranslationModel: null` and - `codexTranslationReasoningEffort: null`. - - Assert updating model and effort persists both values. - - Assert setting either value back to `null` is supported. - -5. Add database migration and repository support. - - Add nullable `codex_translation_model` and - `codex_translation_reasoning_effort` columns to `app_settings`. - - Add nullable `reasoning_effort` to `translation_jobs`. - - Update `src/server/db/schema.ts`, repositories, bundled migrations, and - Drizzle metadata. - - Preserve existing settings rows without forcing a model choice. - -6. Add settings API coverage and handlers. - - Add or extend settings API tests so model/effort defaults can be saved - independently of language. - - Add a settings-scoped model catalog route that returns normalized - `model/list` data. - - Add a settings-scoped defaults route that validates against the current - catalog before saving submitted model and effort. - -7. Extend translation start API tests. - - In `tests/server/translation/api-routes.test.ts`, assert the start route - accepts `lang_code`, `model`, and `reasoning_effort`. - - Assert unknown keys are still rejected. - - Assert invalid model maps to `translation_model_unavailable`. - - Assert invalid effort maps to - `translation_reasoning_effort_unavailable`. - -8. Implement translation start validation. - - Extend `parseStartTranslationPayload()`. - - Extend `startTranslationJob()` input with optional model and reasoning - effort. - - Validate submitted values against app-server `model/list` before creating a - job. - - Save accepted values as the new settings defaults and as job metadata. - - If an active job already exists, return it without rewriting its model or - effort. - -9. Add runner and Codex payload tests. - - In `tests/server/translation/runner.test.ts`, assert newly created jobs - store model and effort. - - In `tests/server/translation/codex-app-server.test.ts`, capture - `turn/start` params and assert selected values are forwarded using the - field names confirmed by the generated schema. - - Keep existing sandbox, approval, and output-schema assertions intact. - -10. Pass selected model and effort through translation execution. - - Extend `TranslateChunkInput` with optional selected model and reasoning - effort. - - Pass job metadata from `runTranslationJob()` to every chunk attempt. - - Add model and effort fields to `turn/start` only when non-null. - - Do not add experimental app-server fields unless the generated stable - schema proves they are required and supported. - -11. Add Settings page component coverage. - - In `tests/components/settings-page.test.ts`, assert model and effort - controls render from initial settings and catalog state. - - Assert catalog failure still renders language and auth controls. - - Assert settings submit helpers call the new defaults endpoint. - -12. Implement Settings page controls. - - Load settings and model catalog through server-safe routes. - - Render model select with an app-server default option. - - Render effort select with a selected-model default option. - - Disable effort choices not supported by the selected model. - - Save defaults without requiring the user to start a translation. - -13. Add reader popup coverage. - - In `tests/components/memory-reader-actions.test.ts`, assert the source - reader renders an expandable translation trigger. - - Assert the trigger no longer calls the translation API directly on the - first click. - - Assert popup markup includes model, effort, language, and a submit - `Translate` button. - - Assert `startReaderTranslation()` sends `lang_code`, `model`, and - `reasoning_effort`. - -14. Implement reader translation popup. - - Replace the direct-click source-reader button with a `Popup`-based dialog. - - Add hover width expansion using CSS transition without layout jumps. - - Keep icon left-aligned and reveal `Translate` text on hover/focus. - - Show current language, model, and effort inside the popup. - - On submit, save changed defaults and start translation with the same - values. - - Close the popup only after a successful queue/current response or keep it - open with an inline error if validation fails. - -15. Apply transparent translation visual treatment. - - Update popup submit button to use a 50% transparent visual style. - - Update the translation progress component to use transparent surface - styling consistent with the popup. - - Preserve accessible focus states, disabled states, and `aria-live` progress - behavior. - -16. Update frontend error copy. - - Add user-facing copy for `translation_model_unavailable`. - - Add user-facing copy for `translation_reasoning_effort_unavailable`. - - Keep `app_server_unavailable`, `auth_required`, and - `app_server_protocol_error` meanings aligned with Tasks 19R and 19S. - -17. Update focused workflow docs. - - Update Task 19 frontend controls docs so click opens confirmation instead - of immediate execution. - - Update app-server integration docs so model catalog comes from - `model/list`. - - Update error handling docs with the two new model/effort errors. - - Do not hardcode current model names in durable docs. - -18. Verify focused tests. - - Run: - - ```bash - mise exec -- bun run test tests/server/translation/codex-app-server.test.ts - mise exec -- bun run test tests/server/settings/settings.test.ts - mise exec -- bun run test tests/server/translation/api-routes.test.ts - mise exec -- bun run test tests/server/translation/runner.test.ts - mise exec -- bun run test tests/server/routes/api-settings.test.ts - mise exec -- bun run test tests/components/settings-page.test.ts - mise exec -- bun run test tests/components/memory-reader-actions.test.ts - mise exec -- bun run test tests/server/db/schema.test.ts - ``` - - - Expected: all focused tests pass. - -19. Verify full project health. - - Run: - - ```bash - mise exec -- bun run verify - git diff --check - ``` - - - Expected: typecheck, unit tests, build, and whitespace checks pass. - -20. Live smoke against Codex app-server. - - Start app-server: - - ```bash - codex app-server --listen unix:///tmp/trauma-codex.sock - ``` - - - Start TRAUMA: - - ```bash - TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock mise exec -- bun run dev - ``` - - - Open Settings and confirm model/effort controls load from app-server. - - Save a non-default model and effort if the catalog offers more than one - choice. - - Open a source memory with no target variant. - - Hover the translation trigger and confirm width-only expansion. - - Click the trigger and confirm the popup appears without starting a job. - - Submit from the popup and confirm the job starts, progress appears with - transparent styling, and completion navigates to the translated reader. - -## Acceptance Criteria - -- Settings exposes Codex translation model and reasoning effort controls. -- Settings values persist in SQLite and default to app-server/model defaults - when unset. -- TRAUMA obtains model metadata from Codex app-server `model/list`; it does not - hardcode model names. -- Reader translation trigger expands on hover/focus and opens a popup on click. -- First click on the reader translation trigger does not start translation. -- Popup displays model, reasoning effort, and language before submission. -- Popup submit saves the selected values as defaults and starts the job with the - same values. -- Translation jobs persist model and reasoning effort metadata. -- Codex `turn/start` receives the selected model and reasoning effort using the - installed app-server's stable schema. -- Invalid or stale model and effort choices return specific stable errors, not - generic app-server availability errors. -- Translation progress component uses transparent styling and keeps accessible - live progress semantics. -- Focused tests and full `mise exec -- bun run verify` pass. - -## PR Handoff - -The PR description must include: - -- Codex CLI/app-server version used for schema verification. -- `model/list` response fields consumed by TRAUMA. -- Confirmed `turn/start` field names used for model and effort. -- Before/after reader translation trigger behavior. -- Database migration summary for settings and translation job metadata. -- Error-code additions and frontend copy. -- Exact verification commands and outcomes. diff --git a/docs/workflows/archive/task-19-codex-translation-protocol-repair.md b/docs/workflows/archive/task-19-codex-translation-protocol-repair.md deleted file mode 100644 index af2cd2ab..00000000 --- a/docs/workflows/archive/task-19-codex-translation-protocol-repair.md +++ /dev/null @@ -1,363 +0,0 @@ -# Task 19S: Codex App-Server Protocol Repair Workflow - -## Goal - -Repair the Brilliant translation app-server path so TRAUMA sends only the -stable Codex app-server request schema it negotiated, classifies reachable -app-server request rejections accurately, and stops showing protocol-contract -failures as app-server availability failures. - -## Status - -- State: Repair workflow draft, tied to Task 19 but separate from the original - Task 19 execution plan and Task 19R auth repair. -- Base workflow: [Task 19 Codex translation](task-19-codex-translation.md) -- Related repair: [Task 19R Codex app-server auth repair](task-19-codex-translation-auth-repair.md) -- Primary external reference: - [OpenAI Codex app-server README](https://github.com/openai/codex/blob/main/codex-rs/app-server/README.md) -- Local protocol reference: generated schema from the installed Codex CLI via - `codex app-server generate-json-schema --out ` and - `codex app-server generate-json-schema --out --experimental`. - -## Required Context - -- [Documentation index](../../INDEX.md) -- [Task 19 overview](task-19-codex-translation.md) -- [Task 19 app-server integration](task-19-codex-translation/05-codex-app-server-integration.md) -- [Task 19 error handling and cancellation](task-19-codex-translation/15-error-handling-and-cancellation.md) -- [Task 19 test plan and fixtures](task-19-codex-translation/16-test-plan-and-fixtures.md) -- [Task 19 prompt and validation contract](task-19-codex-translation/contracts/06-codex-prompt-and-validation.md) -- [Configuration reference](../../references/configuration.md) -- [Verification strategy](../../quality/verification.md) -- [Coding standards](../../references/coding-standards/INDEX.md) - -## Current Failure To Reproduce - -Precondition: start Codex app-server with the Unix listener and run TRAUMA with -that endpoint. - -```bash -codex app-server --listen unix:///tmp/trauma-codex.sock -TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock bun run dev -``` - -Observed failure after auth succeeds and translation starts: - -```json -{ - "code": "app_server_unavailable", - "message": "thread/start.environments requires experimentalApi capability", - "action": "retry" -} -``` - -The app-server is not unavailable in this case. It is reachable and rejecting a -request field that belongs to the experimental schema, while TRAUMA initialized -the connection without `experimentalApi`. - -## Confirmed Protocol Facts - -The app-server protocol is JSON-RPC-shaped, but the wire envelope intentionally -does not include a top-level `jsonrpc` field. - -Stable initialization: - -```json -{ - "id": "1", - "method": "initialize", - "params": { - "clientInfo": { "name": "TRAUMA Brilliant", "version": "0.2.0" }, - "capabilities": null - } -} -``` - -After successful initialization, the client sends: - -```json -{ "method": "initialized" } -``` - -Stable `thread/start` fields consumed by Brilliant: - -- `cwd` -- `ephemeral` -- `approvalPolicy` -- `approvalsReviewer` -- `sandbox` -- `threadSource` - -Fields that must not be sent in the stable `thread/start` request: - -- `environments` -- `experimentalRawEvents` -- `persistExtendedHistory` - -Stable `turn/start` fields consumed by Brilliant: - -- `threadId` -- `input` -- `approvalPolicy` -- `approvalsReviewer` -- `sandboxPolicy` -- `outputSchema` - -Fields that must not be sent in the stable `turn/start` request: - -- `environments` - -`outputSchema` is present in the stable `turn/start` schema and should remain -part of the structured-output attempt. If an app-server build rejects -`outputSchema`, that remains output-mode negotiation and should fall back to -prompt-only JSON mode without consuming chunk retry budget. - -## Root Causes - -1. `src/server/translation/codex-app-server.ts` initializes with - `capabilities: null`, but then sends experimental fields in `thread/start` - and `turn/start`. -2. Focused fixtures show the intended stable payload, but the fake app-server - tests do not assert the actual params sent by the client, so protocol drift - was not caught. -3. `createCodexWireError()` maps otherwise unknown JSON-RPC errors to - `app_server_unavailable`, even when the socket is connected and the - app-server returned a valid error response. -4. API and UI layers share the same coarse error code, so the reader tells the - user to start app-server for a request-schema bug. -5. Existing documentation lists `app_server_unavailable` but does not define a - separate app-server protocol or request-contract failure category. - -## Ownership - -Primary implementation files: - -- `src/server/translation/codex-app-server.ts` -- `src/server/translation/types.ts` -- `src/server/translation/start-translation-route.ts` -- `src/components/reader/MemoryReader.tsx` - -Primary tests: - -- `tests/server/translation/codex-app-server.test.ts` -- `tests/server/translation/api-routes.test.ts` -- `tests/components/settings-page.test.ts` -- `tests/fixtures/translation/codex-app-server-protocol.focused.json` - -Documentation to update only where it currently contradicts the stable protocol -or error taxonomy: - -- `docs/workflows/task-19-codex-translation/05-codex-app-server-integration.md` -- `docs/workflows/task-19-codex-translation/15-error-handling-and-cancellation.md` -- `docs/workflows/task-19-codex-translation/16-test-plan-and-fixtures.md` -- `docs/workflows/task-19-codex-translation/contracts/04-api-and-sse.md` -- `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` - -## Out Of Scope - -- Starting, supervising, or auto-installing Codex app-server from TRAUMA. -- Opting TRAUMA into Codex experimental app-server APIs to preserve unnecessary - experimental fields. -- Replacing Codex app-server with OpenAI Responses API, Codex SDK, or - `codex exec`. -- Reworking auth semantics already covered by Task 19R. -- Reworking translation prompt, chunking, stitching, persistence, or translated - reader rendering unless needed to verify the repaired app-server path. -- Storing Codex, ChatGPT, OpenAI access, or refresh tokens in TRAUMA. - -## Error Taxonomy Decision - -Keep `app_server_unavailable` narrow. Use it only for transport and process -availability failures such as a missing Unix socket listener, connection -refusal, connection open failure, or a connection that cannot be established. - -Add `app_server_protocol_error` for reachable app-server failures where the -client request contract is incompatible with the negotiated protocol, including: - -- invalid params -- unknown or gated request fields -- missing capability opt-in such as `requires experimentalApi capability` -- stable fixture or generated-schema drift -- unsupported method where TRAUMA expected a stable app-server method - -Suggested HTTP mapping: - -- `app_server_unavailable`: `503` -- `stream_disconnected`: `503` -- `timeout`: `504` -- `app_server_protocol_error`: `502` -- `invalid_final_output`: `502` - -Suggested frontend copy: - -- `app_server_unavailable`: `Codex app-server is unavailable. Start it and try again.` -- `app_server_protocol_error`: `Codex app-server rejected the translation request. Update the integration and retry.` - -Protocol errors should not use retry-oriented action text unless the retry will -first use a corrected request. For persisted job errors, prefer `action: "none"` -or another non-misleading action over `retry`. - -## Implementation Steps - -1. Regenerate and inspect installed Codex app-server schemas. - - Run `codex --version`. - - Run `codex app-server generate-json-schema --out `. - - Run `codex app-server generate-json-schema --out --experimental`. - - Confirm stable `thread/start` omits `environments`, - `experimentalRawEvents`, and `persistExtendedHistory`. - - Confirm stable `turn/start` omits `environments` and includes - `outputSchema`. - - Record the Codex CLI version and schema facts in the PR notes. - -2. Add failing protocol payload tests. - - Extend the fake app-server in - `tests/server/translation/codex-app-server.test.ts` to capture full - request messages, not only method names. - - Add an assertion that `initialize.params.capabilities` does not request - `experimentalApi`. - - Add an assertion that `thread/start.params` has no `environments`, - `experimentalRawEvents`, or `persistExtendedHistory`. - - Add an assertion that `turn/start.params` has no `environments`. - - Keep assertions that `turn/start.params.outputSchema` is present when the - caller supplies an output schema. - -3. Add a failing gated-field rejection test. - - In the fake app-server, reject `thread/start` if params include - `environments` with: - - ```json - { - "error": { - "code": -32602, - "message": "thread/start.environments requires experimentalApi capability" - } - } - ``` - - - Expected result: the client throws `CodexAppServerError` with - `code === "app_server_protocol_error"`, not - `app_server_unavailable`. - -4. Fix stable request construction. - - Remove `environments`, `experimentalRawEvents`, and - `persistExtendedHistory` from `thread/start`. - - Remove `environments` from `turn/start`. - - Do not change `initialize` to request `experimentalApi`. - - Keep the read-only `sandbox` and `sandboxPolicy` settings already covered - by the stable schema. - -5. Add and propagate `app_server_protocol_error`. - - Extend `CodexAppServerError.code` in `codex-app-server.ts`. - - Extend `TranslationErrorCode` and - `PersistableTranslationErrorCode` in `types.ts`. - - Update the translation runner error normalization if it currently assumes - every Codex app-server unknown error is availability-related. - - Update API route status mapping to return `502`. - - Update reader UI copy so protocol errors do not tell the user to start - app-server. - -6. Refine wire error classification. - - Classify explicit auth, usage, context, timeout, and stream errors as - their existing specific codes. - - Classify request-schema and capability-gating messages as - `app_server_protocol_error`. - - Treat valid JSON-RPC errors from a connected app-server as protocol or - upstream execution errors, not transport availability failures. - - Preserve the special `outputSchema` fallback path. A one-time - `outputSchema` rejection should still trigger prompt-only JSON mode - instead of becoming a terminal protocol error. - -7. Align route, persistence, and UI tests. - - In `tests/server/translation/api-routes.test.ts`, assert - `app_server_protocol_error` maps to `502` and the response body preserves - the code. - - Add or update component coverage so the reader maps - `app_server_protocol_error` to protocol-copy, not availability-copy. - - Add repository or runner coverage only if persisted error normalization is - not already covered through the runner tests. - -8. Update focused fixtures and docs. - - Update `tests/fixtures/translation/codex-app-server-protocol.focused.json` - so stable request examples match the generated stable schema. - - Update Task 19 app-server integration docs to state that Brilliant defaults - to stable protocol and does not request `experimentalApi`. - - Update error handling docs and API/SSE contract docs to include - `app_server_protocol_error`. - - Remove stale wording that implies every app-server JSON-RPC error is - `app_server_unavailable`. - -9. Verify with focused tests. - - Run: - - ```bash - mise exec -- bun run test tests/server/translation/codex-app-server.test.ts - mise exec -- bun run test tests/server/translation/api-routes.test.ts - mise exec -- bun run test tests/components/settings-page.test.ts - ``` - - - Expected: focused tests pass and the fake app-server no longer observes - stable-protocol experimental fields. - -10. Verify full project health. - - Run: - - ```bash - mise exec -- bun run verify - git diff --check - ``` - - - Expected: typecheck, unit tests, build, and whitespace checks pass. - -11. Live smoke against Codex app-server. - - Start app-server: - - ```bash - codex app-server --listen unix:///tmp/trauma-codex.sock - ``` - - - Start TRAUMA: - - ```bash - TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock mise exec -- bun run dev - ``` - - - With an already authenticated account, start translation for a known - memory. - - Expected: the job no longer fails with - `thread/start.environments requires experimentalApi capability`. - - If translation still fails, inspect `translation_jobs.error` and - `translation_chunks.last_error` before changing code. - -## Parallelization Opportunities - -This repair is small enough for one worker, but execution can be split safely: - -- Worker A: protocol request-shape tests, fixture refresh, and stable request - construction. -- Worker B: error taxonomy, API route mapping, reader copy, and related tests. -- Worker C: docs stale cleanup after A and B land, using their final code and - test names as source of truth. - -Do not run A and B in the same working tree concurrently. If using subagents, -create isolated worktrees before parallel implementation and merge through a -single review point. - -## Acceptance Criteria - -- TRAUMA initializes the app-server connection without `experimentalApi`. -- `thread/start` stable requests omit `environments`, `experimentalRawEvents`, - and `persistExtendedHistory`. -- `turn/start` stable requests omit `environments` and still pass - `outputSchema` when structured output is attempted. -- A reachable app-server rejection such as - `thread/start.environments requires experimentalApi capability` is persisted - and surfaced as `app_server_protocol_error`, not - `app_server_unavailable`. -- `app_server_protocol_error` maps to HTTP `502`. -- Reader UI no longer tells the user to start app-server for protocol-contract - failures. -- Existing output-schema fallback behavior remains intact and does not consume - chunk retry budget. -- Focused protocol fixtures and Task 19 docs reflect the stable schema and the - refined error taxonomy. -- Focused tests, full `bun run verify`, and `git diff --check` pass. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections.md b/docs/workflows/archive/task-19-codex-translation-reader-projections.md deleted file mode 100644 index e7f50c0a..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections.md +++ /dev/null @@ -1,118 +0,0 @@ -# Task 19V: Translated Reader Annotation Projection Implementation Plan - -> Superseded for Flashbacks by Task 19W. Do not execute Task 19V Flashback -> subtasks for translated Flashback behavior. Task 19V remains a historical -> projection design record and may inform future Moment or alignment work only -> after a new plan approves that scope. - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Let Flashbacks and Moments work from both source and translated reader variants while keeping source `CONTENT.md` metadata as the canonical state. - -**Architecture:** Keep `flashbacks` and `moments` canonical by `memory_id`, not by translated language. Translation commit writes a durable projection map from source reader ranges to translated reader ranges. Reader load, Flashback toggle, and Moment toggle use that map to project canonical source metadata into the translated variant and to reverse-project translated selections back to source metadata. - -**Tech Stack:** TypeScript, Bun, Vitest, Drizzle SQLite migrations, existing `markdown-it` reader renderer, existing `src/server/store/flashback-markers.ts` reader-text projection, Task 19U segment translation pipeline, SolidStart API routes and reader components. - ---- - -## Execution Model - -Task 19V is a correction workflow tied to Task 19. It must be executed after Task 19U's segment-based translation path is present because the projection map depends on segment ranges. - -This plan is intentionally decomposed. Do not ask one worker to hold translation persistence, reader rendering, Flashback mutation, Moment mutation, and docs verification in one context. - -Each subtask owns one domain: - -- A clear role. -- A bounded file surface. -- A focused test command. -- A handoff contract for the next subtask. - -The parent worker reviews each handoff before starting dependent subtasks. - -## Current Behavior To Replace - -- Source reader routes render canonical Flashbacks and allow Flashback/Moment creation. -- Translated reader routes load translated `CONTENT.md`, but do not render canonical Flashbacks and cannot safely create Flashbacks or Moments. -- `POST /api/flashbacks` resolves every selection against source `CONTENT.md`. -- `POST /api/moments` validates every section against source `CONTENT.md`. -- The translation pipeline commits translated `CONTENT.md` and then purges temporary chunk data, leaving no durable source-to-translation alignment data. - -## Product Contract - -- A Flashback created on source should render on translated content when its source range can be projected. -- A Flashback created on translated content should create or remove the same canonical source Flashback row after reverse projection. -- A Moment created on source should show active state on the translated section with the same section path. -- A Moment created on translated content should create or remove the same canonical source Moment row after reverse projection. -- Existing `flashbacks` and `moments` tables remain the canonical product state. -- Do not duplicate user annotations per language. -- Do not guess when the projection map is missing, stale, ambiguous, or too coarse for the selected range. - -## Alignment Granularity Decision - -Projection is deterministic at translation segment granularity. Task 19V must tighten segment extraction so prose text nodes are split into sentence-like alignment segments using `Intl.Segmenter` when available, with a small deterministic punctuation fallback only for environments without `Intl.Segmenter`. - -Supported in this workflow: - -- Full segment selections. -- Whole-sentence selections inside paragraphs. -- Unions of adjacent projected segments. -- Heading sections by `sectionPath`. - -Out of scope for this workflow: - -- Word-level cross-language alignment. -- Guessing a Japanese phrase for an arbitrary English substring inside one sentence. -- Calling Codex/OpenAI to align Flashback selections after translation. - -If a selected range only partially overlaps one projection span, the server rejects the cross-variant toggle with a stable 409 response instead of expanding silently or marking the wrong text. - -## Subtask Index - -1. [Projection contract and persistence](task-19-codex-translation-reader-projections/01-projection-contract-and-persistence.md) -2. [Translation projection generation](task-19-codex-translation-reader-projections/02-translation-projection-generation.md) -3. [Translated reader projection rendering](task-19-codex-translation-reader-projections/03-translated-reader-projection-rendering.md) -4. [Cross-variant Flashback toggle](task-19-codex-translation-reader-projections/04-cross-variant-flashback-toggle.md) -5. [Cross-variant Moment toggle](task-19-codex-translation-reader-projections/05-cross-variant-moment-toggle.md) -6. [Integration docs and verification](task-19-codex-translation-reader-projections/06-integration-docs-and-verification.md) - -## Dependency Graph - -```mermaid -flowchart TD - A["01 projection contract and persistence"] --> B["02 translation projection generation"] - B --> C["03 translated reader projection rendering"] - C --> D["04 cross-variant Flashback toggle"] - C --> E["05 cross-variant Moment toggle"] - D --> F["06 integration docs and verification"] - E --> F -``` - -## Parallelization Rules - -- `01` must run first. -- `02` depends on `01`. -- `03` depends on `02`. -- `04` and `05` can run in parallel after `03`. -- `06` runs last after both mutation paths are complete. - -## Cross-Cutting Constraints - -- Do not modify canonical `CONTENT.md` for normal Flashback or Moment persistence. -- Do not create language-specific canonical Flashback or Moment rows. -- Do not persist raw Codex request/response payloads in the projection table. -- Do not keep completed translated chunk Markdown in SQLite after final commit. -- Do not render stale projections when `source_hash` or `output_hash` differs. -- Do not let translated selections mutate source metadata unless reverse projection is exact at the configured alignment granularity. -- Keep browser-facing error messages safe and specific enough to distinguish missing projection from stale source and ambiguous selection. - -## Final Acceptance Criteria - -- Completing a new translation writes translated `CONTENT.md` plus a durable projection map. -- Completed chunk text remains purged after commit. -- Source Flashbacks render in translated reader variants when projection is current. -- Translated Flashback creation/removal mutates the canonical source `flashbacks` rows through exact reverse projection. -- Source Moments render active state in translated reader variants by projected section identity. -- Translated Moment creation/removal mutates canonical source `moments` rows. -- Missing, stale, ambiguous, or partial-span projections fail closed without guessed annotation placement. -- Focused server/component tests, typecheck, `git diff --check`, and full verification pass or document a confirmed unrelated existing flake. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/01-projection-contract-and-persistence.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/01-projection-contract-and-persistence.md deleted file mode 100644 index e2865301..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/01-projection-contract-and-persistence.md +++ /dev/null @@ -1,199 +0,0 @@ -# Task 19V.01: Projection Contract And Persistence Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add the durable data contract for source-to-translated reader projection spans. - -**Architecture:** Runtime reads projection spans from SQLite for fast lookup. Translation commit also writes a deterministic sidecar export beside translated `CONTENT.md` so git backup captures the projection artifact with the translated file. The table is canonical for the running app; the sidecar is a backup/export artifact. - -**Tech Stack:** TypeScript, Drizzle SQLite schema and migrations, Bun tests, existing translation paths. - ---- - -## Role - -Storage and contract owner. - -This worker must not change reader rendering or mutation APIs. It creates the projection data model, path helpers, repository methods, and storage tests that downstream workers consume. - -## Files - -- Modify: `src/server/db/schema.ts` -- Modify: `src/server/db/repositories.ts` -- Modify: `src/server/db/bundled-migrations.ts` -- Modify: `src/server/translation/types.ts` -- Modify: `src/server/translation/paths.ts` -- Create: `src/server/translation/projection-map.ts` -- Add migration: `drizzle/0012_translation_projection_spans.sql` -- Add Drizzle snapshot: `drizzle/meta/0012_snapshot.json` -- Modify: `drizzle/meta/_journal.json` -- Create: `tests/server/translation/translation-projection-map.test.ts` -- Test: `tests/server/translation/translation-repositories.test.ts` -- Test: `tests/server/db/schema.test.ts` - -## Data Contract - -Add `translation_projection_spans`. - -```sql -create table translation_projection_spans ( - job_id text not null references translation_jobs(job_id) on delete cascade, - span_index integer not null, - memory_id text not null references memories(id) on delete cascade, - lang_code text not null, - source_hash text not null, - output_hash text not null, - segment_id text not null, - block_id text not null, - source_markdown_start integer not null, - source_markdown_end integer not null, - translated_markdown_start integer not null, - translated_markdown_end integer not null, - source_reader_start integer not null, - source_reader_end integer not null, - translated_reader_start integer not null, - translated_reader_end integer not null, - created_at integer not null, - updated_at integer not null, - primary key (job_id, span_index), - check (source_hash glob 'sha256:*'), - check (output_hash glob 'sha256:*'), - check (source_markdown_end > source_markdown_start), - check (translated_markdown_end > translated_markdown_start), - check (source_reader_end > source_reader_start), - check (translated_reader_end > translated_reader_start) -); -create index translation_projection_current_idx - on translation_projection_spans(memory_id, lang_code, source_hash, output_hash, span_index); -``` - -Add a temporary column to `translation_chunks`: - -```sql -alter table translation_chunks add column projection_spans_json text; -``` - -`projection_spans_json` stores validated chunk-local projection spans only until -final commit. `purgeCompletedTranslationChunks()` must clear it together with -`translated_markdown`. - -Add this TypeScript shape to `src/server/translation/types.ts`: - -```ts -export interface TranslationProjectionSpan { - jobId: string; - spanIndex: number; - memoryId: string; - langCode: SupportedLanguageCode; - sourceHash: string; - outputHash: string; - segmentId: string; - blockId: string; - sourceMarkdownStart: number; - sourceMarkdownEnd: number; - translatedMarkdownStart: number; - translatedMarkdownEnd: number; - sourceReaderStart: number; - sourceReaderEnd: number; - translatedReaderStart: number; - translatedReaderEnd: number; - createdAt: Date; - updatedAt: Date; -} -``` - -Add this sidecar path helper to `src/server/translation/paths.ts`: - -```ts -export function resolveTranslatedMemoryProjectionPath(input: { - config: Pick; - langCode: string; - memoryId: string; -}) { - const contentPath = resolveTranslatedMemoryContentPath(input); - return { - absolutePath: join(dirname(contentPath.absolutePath), "TRANSLATION_MAP.json"), - relativePath: posix.join("memories", input.memoryId, input.langCode, "TRANSLATION_MAP.json"), - }; -} -``` - -## Task Steps - -- [ ] **Step 1: Write repository and path tests** - -Add tests that assert: - -```ts -expect(resolveTranslatedMemoryProjectionPath({ - config, - memoryId, - langCode: "ja-JP", -}).relativePath).toBe(`memories/${memoryId}/ja-JP/TRANSLATION_MAP.json`); -``` - -Add repository tests that insert two projection spans and read them back by `(memoryId, langCode, sourceHash, outputHash)`. - -- [ ] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-projection-map.test.ts tests/server/translation/translation-repositories.test.ts tests/server/db/schema.test.ts -``` - -Expected: FAIL because projection paths, schema, and repository methods do not exist. - -- [ ] **Step 3: Add schema and migration** - -Add `translationProjectionSpans` to `src/server/db/schema.ts` with the SQL constraints above. Generate or hand-maintain the Drizzle migration and metadata following the current migration style. - -- [ ] **Step 4: Add repository methods** - -Add these methods to the translation repository: - -```ts -replaceProjectionSpansForJob(jobId: string, spans: TranslationProjectionSpan[]): Promise; -listCurrentProjectionSpans(input: { - memoryId: string; - langCode: SupportedLanguageCode; - sourceHash: string; - outputHash: string; -}): Promise; -deleteProjectionSpansForJob(jobId: string): Promise; -``` - -`replaceProjectionSpansForJob` must delete previous rows for the job and insert the next set in one SQLite transaction. - -- [ ] **Step 5: Add sidecar serializer** - -Create `src/server/translation/projection-map.ts` with: - -```ts -export interface TranslationProjectionSidecar { - version: 1; - jobId: string; - memoryId: string; - langCode: SupportedLanguageCode; - sourceHash: string; - outputHash: string; - spans: TranslationProjectionSpan[]; -} -``` - -Add `serializeTranslationProjectionSidecar()` that emits stable pretty JSON with spans sorted by `spanIndex`. - -- [ ] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-projection-map.test.ts tests/server/translation/translation-repositories.test.ts tests/server/db/schema.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Downstream workers can persist and query current projection spans by memory, language, source hash, and output hash. The projection table is durable runtime state; the sidecar JSON is the backup artifact. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/02-translation-projection-generation.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/02-translation-projection-generation.md deleted file mode 100644 index 614a029a..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/02-translation-projection-generation.md +++ /dev/null @@ -1,198 +0,0 @@ -# Task 19V.02: Translation Projection Generation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Generate durable projection spans while committing translated `CONTENT.md`. - -**Architecture:** Translation validation already reassembles Markdown locally from source segments. Extend that path to return segment-level source and translated ranges, then rebase chunk-local ranges during final stitching and persist current projection spans before purging temporary chunk data. - -**Tech Stack:** TypeScript, Vitest, Task 19U segment manifest, existing translation runner/chunker/stitching modules, projection repository from Task 19V.01. - ---- - -## Role - -Translation runtime owner. - -This worker must not change reader rendering or Flashback/Moment APIs. It owns alignment generation and persistence at translation commit time. - -## Files - -- Modify: `src/server/translation/types.ts` -- Modify: `src/server/translation/translation-segments.ts` -- Modify: `src/server/translation/chunker.ts` -- Modify: `src/server/translation/prompt.ts` -- Modify: `src/server/translation/runner.ts` -- Modify: `src/server/translation/stitching.ts` -- Modify: `src/server/translation/projection-map.ts` -- Test: `tests/server/translation/translation-segments.test.ts` -- Test: `tests/server/translation/prompt.test.ts` -- Test: `tests/server/translation/chunker.test.ts` -- Test: `tests/server/translation/runner.test.ts` -- Test: `tests/server/translation/stitching.test.ts` - -## Contract Changes - -Keep `TranslationTextSegment.sourceStart` and `sourceEnd` as chunk-local Markdown offsets for deterministic splicing. Add document-level and reader-level offsets: - -```ts -export interface TranslationTextSegment { - blockId: string; - id: string; - sourceStart: number; - sourceEnd: number; - sourceDocumentStart: number; - sourceDocumentEnd: number; - sourceReaderStart: number; - sourceReaderEnd: number; - text: string; -} -``` - -Add a validated chunk result: - -```ts -export interface ValidatedCodexChunkOutput extends RawCodexChunkOutput { - translated_markdown: string; - projectionSpans: TranslationChunkProjectionSpan[]; -} -``` - -Chunk projection spans are temporary and use translated chunk-local offsets until final stitch: - -```ts -export interface TranslationChunkProjectionSpan { - segmentId: string; - blockId: string; - sourceDocumentStart: number; - sourceDocumentEnd: number; - sourceReaderStart: number; - sourceReaderEnd: number; - translatedChunkStart: number; - translatedChunkEnd: number; - translatedReaderStart: number; - translatedReaderEnd: number; -} -``` - -## Alignment Granularity - -Before extracting segments, split prose text node ranges into sentence-like spans: - -```ts -const segmenter = typeof Intl.Segmenter === "function" - ? new Intl.Segmenter(undefined, { granularity: "sentence" }) - : undefined; -``` - -Fallback split pattern is only: - -```ts -/[^.!?。!?]+(?:[.!?。!?]+["')\]]*)?\s*/gu -``` - -Do not add language-specific full Markdown parsing here. Sentence splitting only applies inside mdast `text` nodes that are already known to be translatable. - -## Task Steps - -- [ ] **Step 1: Write segment range tests** - -Add assertions that a paragraph with two English sentences creates two segments and each segment has source document and reader offsets: - -```ts -expect(manifest.segments.map((segment) => segment.text)).toEqual([ - "Top 5 repos defining it, the academic case for why, and who says it's wrong.", - "Keep the second sentence separate.", -]); -expect(manifest.segments[0]).toMatchObject({ - sourceReaderStart: 0, - sourceReaderEnd: 75, -}); -``` - -- [ ] **Step 2: Write chunk projection tests** - -Add a runner or prompt test where fake Codex returns: - -```json -{ - "chunk_index": 0, - "segments": [ - { - "id": "s000001", - "translated_text": "それを定義するトップ5リポジトリ、なぜそうなるかの学術的根拠、そしてそれが誤りだとする立場。" - } - ], - "warnings": [] -} -``` - -Assert the validated output contains one `projectionSpans` entry with non-null source and translated reader ranges. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-segments.test.ts tests/server/translation/prompt.test.ts tests/server/translation/chunker.test.ts tests/server/translation/runner.test.ts tests/server/translation/stitching.test.ts -``` - -Expected: FAIL because sentence-level segments and projection spans do not exist. - -- [ ] **Step 4: Export reader projection primitives** - -Extract reusable reader projection from `src/server/store/flashback-markers.ts` without changing existing behavior: - -```ts -export interface ReaderTextProjection { - text: string; - sourceOffsets: number[]; - sourceEndOffsets: number[]; - protectedOffsets: boolean[]; -} - -export function projectMarkdownToReaderText(markdown: string): ReaderTextProjection; -``` - -Existing Flashback marker tests must continue passing. - -- [ ] **Step 5: Add document offsets to blocks and chunks** - -Add source document offsets to `TranslationBlock` and compute them in `parseMarkdownTranslationBlocks()`. In `createTranslationChunks()`, pass the first block's document offset into `createTranslationSegmentManifest()` so segment document offsets are stable across chunks. - -- [ ] **Step 6: Return projection spans from validation** - -In `validateCodexChunkOutput()`, after reassembly: - -- record translated chunk-local Markdown start/end for each replacement, -- project source and translated Markdown into reader text, -- map Markdown ranges to reader ranges, -- return `projectionSpans` on the validated output. - -- [ ] **Step 7: Persist projections at final commit** - -In `commitTranslatedContent()`: - -- stitch translated chunks in order, -- rebase each chunk projection span to translated document offsets, -- compute final `outputHash`, -- replace projection rows for the job, -- write `TRANSLATION_MAP.json`, -- enqueue backup with both translated `CONTENT.md` and `TRANSLATION_MAP.json`, -- purge temporary chunk Markdown and `translation_chunks.projection_spans_json` - after successful commit. - -- [ ] **Step 8: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-segments.test.ts tests/server/translation/prompt.test.ts tests/server/translation/chunker.test.ts tests/server/translation/runner.test.ts tests/server/translation/stitching.test.ts tests/server/translation/translation-repositories.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Downstream reader and API workers can assume every current translation has zero or more projection spans keyed by the exact source and output hashes. Old translations without projection spans are unsupported for cross-variant annotation and must fail closed. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/03-translated-reader-projection-rendering.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/03-translated-reader-projection-rendering.md deleted file mode 100644 index cf9dd508..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/03-translated-reader-projection-rendering.md +++ /dev/null @@ -1,130 +0,0 @@ -# Task 19V.03: Translated Reader Projection Rendering Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Render canonical source Flashbacks and Moment active state on translated reader variants. - -**Architecture:** `loadReaderMemory(memoryId, { langCode })` resolves the current translation, loads projection spans, projects canonical source annotations into translated reader ranges, and renders translated Markdown with transient marks. Source reader behavior remains unchanged. - -**Tech Stack:** TypeScript, Vitest, existing reader page-data loader, reader markdown renderer, projection repository from Task 19V.01. - ---- - -## Role - -Reader read-path owner. - -This worker must not implement mutation APIs. It makes translated reader variants display already-saved canonical annotations. - -## Files - -- Modify: `src/server/reader/page-data.ts` -- Modify: `src/server/flashbacks/toggle.ts` -- Modify: `src/server/store/flashback-markers.ts` -- Modify: `src/components/reader/MemoryReader.tsx` -- Test: `tests/server/reader/page-data.test.ts` -- Test: `tests/server/flashbacks/flashback-markers.test.ts` -- Test: `tests/components/memory-reader-actions.test.ts` -- Test: `tests/components/reader-moment-actions.test.ts` - -## Projection Rules - -Add server helper: - -```ts -export function projectFlashbacksToTranslatedRanges(input: { - flashbacks: ReaderFlashbackItem[]; - projectionSpans: TranslationProjectionSpan[]; -}): ReaderFlashbackItem[]; -``` - -It must: - -- require every projected source range to align exactly with one or more adjacent projection spans, -- return translated `text`, `prefix`, `suffix`, `startOffset`, and `endOffset`, -- keep the original Flashback `id` and `createdAt`, -- drop unprojectable Flashbacks from translated rendering. - -Moment active state uses `sectionPath`: - -```ts -resolveReaderMomentTarget(moment, translatedToc) -``` - -must keep using exact `(sectionAnchor, sectionPath)` first, then unique `sectionPath` fallback. No language-specific Moment rows are created. - -## Task Steps - -- [ ] **Step 1: Write translated Flashback rendering test** - -Add a `loadReaderMemory(memoryId, { langCode: "ja-JP" })` fixture with: - -- source `CONTENT.md` containing the English sentence, -- translated `CONTENT.md` containing the Japanese sentence, -- one canonical source Flashback row, -- one current translation job, -- one projection span. - -Assert: - -```ts -expect(result.rendered.html).toContain( - 'それを定義するトップ5リポジトリ' -); -expect(result.memory.flashbacks[0]).toMatchObject({ - id: "flashback-1", - text: "それを定義するトップ5リポジトリ、なぜそうなるかの学術的根拠、そしてそれが誤りだとする立場。", -}); -``` - -- [ ] **Step 2: Write stale projection test** - -Assert translated reader does not render a Flashback when projection rows are missing or when `outputHash` differs from the current translated file hash. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/reader/page-data.test.ts tests/server/flashbacks/flashback-markers.test.ts tests/components/memory-reader-actions.test.ts tests/components/reader-moment-actions.test.ts -``` - -Expected: FAIL because translated reader still skips Flashbacks. - -- [ ] **Step 4: Load projection spans in translated reader path** - -In `readTranslatedReaderContent()` or adjacent page-data helper, return: - -```ts -{ - markdown, - relativePath, - currentTranslation, - projectionSpans, -} -``` - -Only query spans after `resolveCurrentTranslationReadOnly()` returns `current`, using the current `sourceHash` and `outputHash`. - -- [ ] **Step 5: Render projected Flashbacks** - -Change `renderMemoryMarkdownSafely()` call so translated readers pass projected Flashbacks instead of an empty array. Keep source readers passing `memory.flashbacks`. - -- [ ] **Step 6: Surface projected state to frontend** - -`toReaderMemory()` should receive the active variant's rendered Flashbacks so the right rail current-memory tab lists translated snippets on translated routes. The canonical ids remain unchanged. - -- [ ] **Step 7: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/reader/page-data.test.ts tests/server/flashbacks/flashback-markers.test.ts tests/components/memory-reader-actions.test.ts tests/components/reader-moment-actions.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Translated reader routes now display projected annotations but still cannot create or remove them from translated selections. Mutation workers must use the same projection rules and fail closed when projection is missing or partial. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/04-cross-variant-flashback-toggle.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/04-cross-variant-flashback-toggle.md deleted file mode 100644 index 3d3d33c0..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/04-cross-variant-flashback-toggle.md +++ /dev/null @@ -1,138 +0,0 @@ -# Task 19V.04: Cross-Variant Flashback Toggle Implementation Plan - -> Superseded for Flashbacks by Task 19W. Do not execute this subtask for -> translated Flashback behavior. - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow translated reader text selections to create or remove canonical source Flashbacks through exact reverse projection. - -**Architecture:** The frontend includes the active reader variant in the Flashback toggle payload. The server resolves source selections normally, and resolves translated selections by mapping translated reader offsets back to source reader offsets through the current projection map before applying existing range merge/split logic. - -**Tech Stack:** TypeScript, Solid component tests, SolidStart API route tests, existing Flashback toggle service, projection repository. - ---- - -## Role - -Flashback mutation owner. - -This worker must not change Moment APIs or translation commit behavior. - -## Files - -- Modify: `src/components/reader/MemoryReader.tsx` -- Modify: `src/routes/api/flashbacks.ts` -- Modify: `src/server/flashbacks/toggle.ts` -- Modify: `src/server/store/flashback-markers.ts` -- Test: `tests/server/routes/api-flashbacks-toggle.test.ts` -- Test: `tests/server/flashbacks/toggle.test.ts` -- Test: `tests/components/memory-reader-actions.test.ts` -- Test: `tests/server/flashbacks/flashback-markers.test.ts` - -## API Contract - -Extend `POST /api/flashbacks` payload: - -```json -{ - "memoryId": "019e...", - "operation": "flashback", - "langCode": "ja-JP", - "selection": { - "text": "それを定義するトップ5リポジトリ、なぜそうなるかの学術的根拠、そしてそれが誤りだとする立場。", - "prefix": "", - "suffix": "", - "startOffset": 0, - "endOffset": 45 - } -} -``` - -Source reader payloads omit `langCode`. - -Stable errors: - -- `missing_projection`: current translation has no projection map. -- `stale_projection`: projection map does not match current source or output hash. -- `ambiguous_projection`: selected translated range maps to multiple non-adjacent source ranges. -- `partial_projection`: selection starts or ends inside one projection span. - -Return these as HTTP 409 with `{ "error": "...", "code": "" }`. - -## Task Steps - -- [ ] **Step 1: Write API payload tests** - -Assert the parser accepts source and translation variants and rejects unknown keys: - -```ts -expect(await parseFlashbackTogglePayload(request)).toMatchObject({ - ok: true, - langCode: "ja-JP", -}); -``` - -- [ ] **Step 2: Write reverse projection service test** - -Create a source sentence, translated sentence, projection span, and translated selection. Assert `toggleMemoryFlashback()` writes a canonical Flashback row whose `text` is the source English sentence while the API response returns active variant snippets for the translated reader. - -- [ ] **Step 3: Write partial projection rejection test** - -Select only half of the translated sentence and assert: - -```ts -await expect(toggleMemoryFlashback(input)).rejects.toMatchObject({ - code: "partial_projection", -}); -``` - -- [ ] **Step 4: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/routes/api-flashbacks-toggle.test.ts tests/server/flashbacks/toggle.test.ts tests/components/memory-reader-actions.test.ts tests/server/flashbacks/flashback-markers.test.ts -``` - -Expected: FAIL because Flashback API only accepts source selections. - -- [ ] **Step 5: Extend frontend payload** - -In `MemoryReader.tsx`, derive: - -```ts -const langCode = props.result.content.langCode; -``` - -Include `langCode` in the `/api/flashbacks` body only for translated routes. - -- [ ] **Step 6: Extend route parsing** - -Parse optional `langCode` in `src/routes/api/flashbacks.ts`. Keep old source payloads backward-compatible. Add `code` to formatted 409 errors when stable projection error codes are introduced. - -- [ ] **Step 7: Implement reverse projection** - -In `toggleMemoryFlashback()`: - -- source variant follows the current path, -- translated variant resolves current translation, -- loads projection spans by current source/output hash, -- maps translated reader range to one adjacent source reader range, -- calls existing source Markdown selection resolver with the projected source selection, -- returns projected active-variant Flashbacks when the request came from a translated reader. - -- [ ] **Step 8: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/routes/api-flashbacks-toggle.test.ts tests/server/flashbacks/toggle.test.ts tests/components/memory-reader-actions.test.ts tests/server/flashbacks/flashback-markers.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Translated Flashback toggle is available only when projection is exact. Existing source Flashback behavior, range merging, range splitting, export writing, and backup enqueue behavior must remain unchanged. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/05-cross-variant-moment-toggle.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/05-cross-variant-moment-toggle.md deleted file mode 100644 index 50fe6ab1..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/05-cross-variant-moment-toggle.md +++ /dev/null @@ -1,120 +0,0 @@ -# Task 19V.05: Cross-Variant Moment Toggle Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Allow translated reader headings and ToC rows to create or remove canonical source Moments. - -**Architecture:** Moment identity remains source canonical. Translated reader payloads include the active language, and the server resolves the translated section to the source section by `sectionPath`, with anchor/title as validation signals when available. - -**Tech Stack:** TypeScript, Solid component tests, SolidStart API route tests, existing Moment repository and reader ToC. - ---- - -## Role - -Moment mutation owner. - -This worker must not change Flashback range projection or translation commit behavior. - -## Files - -- Modify: `src/components/reader/moment-requests.ts` -- Modify: `src/components/reader/MemoryReader.tsx` -- Modify: `src/routes/api/moments.ts` -- Modify: `src/server/moments/browse.ts` -- Test: `tests/server/routes/api-moments.test.ts` -- Test: `tests/components/reader-moment-actions.test.ts` -- Create: `tests/server/moments/browse.test.ts` -- Test: `tests/server/reader/page-data.test.ts` - -## API Contract - -Extend `POST /api/moments` payload: - -```json -{ - "memoryId": "019e...", - "langCode": "ja-JP", - "sectionAnchor": "translated-anchor", - "sectionTitle": "翻訳された見出し", - "sectionLevel": 2, - "sectionPath": "1/2", - "sectionStartOffset": null, - "sectionEndOffset": null, - "contentHash": null -} -``` - -Source payloads omit `langCode`. - -Resolution rules: - -1. Source variant validates against source ToC exactly as today. -2. Translation variant validates the payload exists in translated ToC. -3. Translation variant finds exactly one source ToC entry with the same `sectionPath` and `sectionLevel`. -4. The row stored in `moments` uses source anchor, source title, source level, and source path. -5. If source path is missing or ambiguous, return 409 with `code: "ambiguous_projection"`. - -## Task Steps - -- [ ] **Step 1: Write Moment request tests** - -Assert `createMomentForSection()` posts the active variant: - -```ts -expect(await requests[0]?.json()).toMatchObject({ - langCode: "ja-JP", - sectionPath: "1/2", -}); -``` - -- [ ] **Step 2: Write translated API test** - -Build source and translated Markdown with the same section path but different anchors/titles. Assert `POST /api/moments` from the translated heading stores the source section identity. - -- [ ] **Step 3: Write ambiguous path test** - -Build source Markdown where the translated `sectionPath` cannot resolve to exactly one source entry. Assert HTTP 409 with `code: "ambiguous_projection"`. - -- [ ] **Step 4: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/routes/api-moments.test.ts tests/components/reader-moment-actions.test.ts tests/server/moments/browse.test.ts tests/server/reader/page-data.test.ts -``` - -Expected: FAIL because Moment API only validates against source payload identity. - -- [ ] **Step 5: Extend frontend Moment payload** - -In `MemoryReader.tsx`, pass the active translated language into `createMomentForSection()`. In `moment-requests.ts`, include `langCode` in the JSON body only for translated routes while keeping source behavior backward-compatible. - -- [ ] **Step 6: Implement translated section resolution** - -In `src/routes/api/moments.ts`: - -- read source `CONTENT.md` and render source ToC, -- read translated `CONTENT.md` and render translated ToC when `langCode` is present, -- validate the translated payload against translated ToC, -- resolve source ToC by `sectionPath` and `sectionLevel`, -- store source section identity in SQLite. - -- [ ] **Step 7: Keep browse links stable** - -Update `src/server/moments/browse.ts` only if needed so Moment browse rows continue linking to source memory anchors by default. Do not make `/moments` language-specific in this workflow. - -- [ ] **Step 8: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/routes/api-moments.test.ts tests/components/reader-moment-actions.test.ts tests/server/moments/browse.test.ts tests/server/reader/page-data.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Translated Moment creation/removal now mutates canonical source Moment rows. Rendering active state on translated reader variants remains path-based and does not require language-specific Moment persistence. diff --git a/docs/workflows/archive/task-19-codex-translation-reader-projections/06-integration-docs-and-verification.md b/docs/workflows/archive/task-19-codex-translation-reader-projections/06-integration-docs-and-verification.md deleted file mode 100644 index a5229aa5..00000000 --- a/docs/workflows/archive/task-19-codex-translation-reader-projections/06-integration-docs-and-verification.md +++ /dev/null @@ -1,130 +0,0 @@ -# Task 19V.06: Integration Docs And Verification Implementation Plan - -> Superseded for Flashbacks by Task 19W. Do not execute this subtask for -> translated Flashback behavior. - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Verify cross-variant annotations end to end and update durable docs so future work does not reintroduce source-only annotation behavior. - -**Architecture:** This task adds integration coverage across translation commit, translated reader rendering, translated Flashback toggle, translated Moment toggle, and stale projection failures. It updates architecture docs after behavior is implemented. - -**Tech Stack:** TypeScript, Vitest, Playwright where practical, docs under `docs/architecture/` and `docs/workflows/`. - ---- - -## Role - -Integration and documentation owner. - -This worker must not introduce new architecture. It validates the completed behavior and updates docs to match the implementation. - -## Files - -- Modify: `docs/architecture/data-and-storage.md` -- Modify: `docs/architecture/flows.md` -- Modify: `docs/architecture/ui-and-routing.md` -- Modify: `docs/workflows/task-19-codex-translation.md` -- Modify: `docs/workflows/task-19-codex-translation/10-stitching-and-atomic-commit.md` -- Modify: `docs/workflows/task-19-codex-translation/11-sqlite-cleanup-and-purge-policy.md` -- Modify: `docs/workflows/task-19-codex-translation/13-reader-render-integration-for-translated-content.md` -- Modify: `docs/workflows/task-19-codex-translation/contracts/03-sqlite-and-repositories.md` -- Modify: `docs/workflows/task-19-codex-translation/contracts/07-atomic-commit-purge-recovery.md` -- Test: `tests/server/translation/runner.test.ts` -- Test: `tests/server/reader/page-data.test.ts` -- Test: `tests/server/routes/api-flashbacks-toggle.test.ts` -- Test: `tests/server/routes/api-moments.test.ts` -- Optional E2E: `tests/e2e/translated-reader-annotations.spec.ts` - -## Required Integration Scenarios - -Add focused coverage for: - -1. Translation commit writes `CONTENT.md`, `TRANSLATION_MAP.json`, and projection rows. -2. Completed chunk `translated_markdown` and temporary projection JSON are purged. -3. Source Flashback renders on translated reader using projected Japanese text. -4. Translated Flashback selection writes canonical English source row. -5. Source Moment active state appears on translated heading with the same path. -6. Translated Moment selection writes canonical source Moment row. -7. Missing projection map disables translated annotation mutation with 409. -8. Stale projection hash disables translated annotation rendering and mutation. - -## Task Steps - -- [ ] **Step 1: Add integration tests** - -Add tests that use a short source/translated fixture: - -```md -# Reader Projection Fixture - -Top 5 repos defining it, the academic case for why, and who says it's wrong. - -## Why it matters - -Second section body. -``` - -Translated text: - -```md -# リーダー投影フィクスチャ - -それを定義するトップ5リポジトリ、なぜそうなるかの学術的根拠、そしてそれが誤りだとする立場。 - -## なぜ重要か - -2番目のセクション本文。 -``` - -Assert source canonical rows remain English/source identity while translated reader renders Japanese projected snippets. - -- [ ] **Step 2: Verify focused RED or PASS from previous subtasks** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/runner.test.ts tests/server/reader/page-data.test.ts tests/server/routes/api-flashbacks-toggle.test.ts tests/server/routes/api-moments.test.ts -``` - -Expected: PASS if subtasks 19V.01-19V.05 are complete; otherwise fail at the missing integration boundary. - -- [ ] **Step 3: Update architecture docs** - -Document: - -- `flashbacks` and `moments` remain source canonical. -- `translation_projection_spans` is runtime projection state. -- `TRANSLATION_MAP.json` is a backup/export artifact. -- translated reader annotations fail closed when projection is missing or stale. -- arbitrary sub-sentence cross-language selection is not guessed in Task 19V. - -- [ ] **Step 4: Update Task 19 docs** - -Update Task 19 translation docs so completed translation identity includes the projection sidecar as part of commit output. Update cleanup docs so temporary chunk projection data is purged but durable projection rows remain. - -- [ ] **Step 5: Run focused verification** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-projection-map.test.ts tests/server/translation/runner.test.ts tests/server/reader/page-data.test.ts tests/server/routes/api-flashbacks-toggle.test.ts tests/server/routes/api-moments.test.ts tests/components/memory-reader-actions.test.ts tests/components/reader-moment-actions.test.ts -mise exec -- bun run typecheck -git diff --check -``` - -Expected: all commands pass. - -- [ ] **Step 6: Run full verification** - -Run: - -```sh -mise exec -- bun run verify -``` - -Expected: typecheck, unit tests, and build pass. If the build emits the existing `temml` deprecation warning, record it as non-failing only when exit code is 0. - -## Handoff - -Task 19V is complete only after docs and tests agree that translated reader annotations are projection-backed, canonical source metadata remains single-source-of-truth, and stale or partial projection cases fail closed. diff --git a/docs/workflows/archive/task-19-codex-translation-review.md b/docs/workflows/archive/task-19-codex-translation-review.md deleted file mode 100644 index 94748d33..00000000 --- a/docs/workflows/archive/task-19-codex-translation-review.md +++ /dev/null @@ -1,196 +0,0 @@ -# Workflow 19 Review: Brilliant — Codex app-server translation pipeline - -**Reviewed:** 2026-05-21 -**Reviewer:** Claude (pre-implementation gate review) -**Scope:** Historical review of files under `docs/workflows/task-19-codex-translation/` plus parent README, against an archived scratch instruction note formerly named `TASK_19_INSTRUCTION.md`. That scratch note is not part of the current implementation source of truth. -**Verdict:** **HISTORICAL REVIEW SNAPSHOT — DO NOT USE FOR IMPLEMENTATION DECISIONS.** The canonical implementation plan is `docs/workflows/task-19-codex-translation/README.md` plus `00-execution-contracts.md` and the focused contracts under `docs/workflows/task-19-codex-translation/contracts/`. Findings below may be superseded by later plan edits and are retained only as review history. - ---- - -## 1. Files reviewed - -| File | Role | -|------|------| -| Archived scratch instruction note formerly named `TASK_19_INSTRUCTION.md` | Historical review input, not current source of truth | -| `docs/workflows/task-19-codex-translation.md` | Parent README / Brilliant overview | -| `…/00-execution-contracts.md` | Contract routing index | -| `contracts/README.md` | Contract directory index | -| `contracts/01-architecture-and-ownership.md` | File ownership map | -| `contracts/02-types-state-and-settings.md` | TypeScript types, state machines | -| `contracts/03-sqlite-and-repositories.md` | DDL, repository signatures | -| `contracts/04-api-and-sse.md` | API shapes, SSE, error codes | -| `contracts/05-markdown-chunking.md` | Block scanner, chunking defaults | -| `contracts/06-codex-prompt-and-validation.md` | Client interface, prompt, validation | -| `contracts/07-atomic-commit-purge-recovery.md` | Commit sequence, purge, recovery | -| Subtask files `01` through `17` | Goal, ownership, contract ref, tests, acceptance criteria per subtask | - ---- - -## 2. INSTRUCT acceptance criteria verdicts - -| # | Criterion | Verdict | Where satisfied | -|---|-----------|---------|-----------------| -| 1 | Uses `memory///CONTENT.md` | **Satisfied (with intentional divergence noted)** | Parent README line 33–34 explicitly maps conceptual `memory/…` → plural `memories/…`; contracts/07 and contracts/04 use `memories///CONTENT.md` throughout. Divergence is intentional and documented. | -| 2 | Uses `ja-JP`-style BCP 47 language codes | **Satisfied** | contracts/02 defines `SUPPORTED_TRANSLATION_LANGUAGES` with BCP 47 codes; contracts/04 uses `ja-JP` in all examples; parent README line 38. | -| 3 | Does not introduce `.work/` | **Satisfied** | Parent README line 41 states this explicitly. contracts/07 uses `.CONTENT..tmp` for atomic write only; no `.work/` directory anywhere. | -| 4 | Allows temporary SQLite chunk storage during translation | **Satisfied** | contracts/03 DDL includes `translated_markdown TEXT` column; contracts/07 purge step is post-commit only. | -| 5 | Requires immediate purge of translated chunk bodies after final commit | **Satisfied** | contracts/07 step 12: same-transaction purge; purge SQL explicit. Parent README lines 43–44. | -| 6 | Uses atomic final file write | **Satisfied** | contracts/07 steps 6–9: same-directory temp → flush → rename → dir flush. | -| 7 | Supports long documents and academic papers through deterministic chunking | **Satisfied** | contracts/05: 16-step block scanner, stable `b000001` ids, `maxRoughTokens: 2500`, section-boundary preference, oversized-block handling. Subtask 04 owns implementation. | -| 8 | Includes frontend streaming progress | **Satisfied** | Parent README: SSE default; contracts/04: full event envelope + 15 named event types; subtask 07 owns bridge. | -| 9 | Uses Codex app-server as preferred integration path | **Satisfied** | Parent README lines 22–29; contracts/06: JSON-RPC 2.0 `initialize`/`thread/start`/`turn/start` lifecycle; `CodexAppServerClient` interface. | -| 10 | Keeps Codex tokens out of the frontend | **Satisfied** | contracts/06 explicit rules; contracts/02 types/settings contract. Multiple explicit `NEVER` statements. `translation_unavailable` error path does not leak credential info. | -| 11 | Treats external article content as untrusted data | **Satisfied** | contracts/06 prompt sections require: role as data processor, security framing, explicit delimiters around source content. contracts/06 rules: "network/tool access disabled for translation turns if app-server exposes such controls". | -| 12 | Defines validation and retry at chunk level | **Satisfied** | contracts/06: 13-step validation algorithm per chunk; retry behavior section: increment `retry_count`, include failures in retry prompt, fail after `maxRetries`. | -| 13 | Defines final stitching and full-document validation | **Satisfied** | contracts/07 step 3 (stitch in block order), step 4 (validate final full document). Subtask 10 owns implementation. | -| 14 | Includes a future `reader-translate` skill subtask | **Satisfied** | Subtask 14 (`14-translation-skill-definition.md`) defines the skill document. Parent README lists it explicitly at position 14. | -| 15 | Produces numbered subtasks with dependencies and parallelization notes | **Satisfied** | Parent README: 18 numbered subtasks (0–17); parallelization map (Tracks A–F); dependency order stated per track. | -| 16 | Does not start implementation unless explicitly authorized | **Satisfied** | Parent README line 173: "Do not start implementation until the plan is accepted." Status block line 5: "ready for implementation after this workflow is accepted." | - -**All 16 criteria satisfied.** - ---- - -## 3. Security requirements alignment - -The INSTRUCT states security requirements across lines 1–705. This review checks the most critical ones. - -| Requirement | Status | Contract reference | -|-------------|--------|--------------------| -| Source content wrapped in explicit delimiters | Satisfied | contracts/06 prompt section 8: "Source chunk inside explicit delimiters" | -| Source content declared as data, not instructions | Satisfied | contracts/06 prompt section 2: security framing; section 1: role as faithful translation worker | -| Never let source content override system instructions | Satisfied | Prompt section order (2 before 8) + explicit security prompt section | -| Disable network/tool access for translation turns if possible | Satisfied | contracts/06 rule: "Disable network/tool access for translation turns if app-server exposes such controls" | -| Codex must not write canonical files | Satisfied | contracts/06 rule: "Do not let Codex write files" | -| Codex app-server not exposed to browser | Satisfied | contracts/06 rule: "Do not expose app-server URL, token, or raw auth state to the browser"; contracts/02 types contract | -| Reader backend is the only Codex client | Satisfied | Parent README line 26; contracts/01 ownership map | -| OpenAI/ChatGPT tokens never enter frontend, SQLite, or logs | Satisfied | contracts/02: explicit token exclusion from schema; contracts/06 auth contract; `chatgptAuthTokens` mode excluded | -| Do not persist partial streamed deltas as completed translation | Satisfied | contracts/06 rule: "Final output must come from completed item content and pass schema validation"; parent README line 127 | - ---- - -## 4. Intentional divergences from INSTRUCT - -These are deliberate plan decisions, not defects. - -### 4.1 Path plural: `memories/` vs `memory/` - -INSTRUCT uses conceptual singular `memory//...`. The plan uses TRAUMA's existing store layout `memories//...`. - -**Documented:** Parent README line 34; `00-execution-contracts.md`. -**Verdict:** Correct. TRAUMA's `storePath` has always used the plural form. Any agent implementing subtasks must follow the plural form. - -### 4.2 Extended job status values: `unavailable` and `stale` - -INSTRUCT defines job states `pending / running / cancel_requested / canceled / stitching / committing / complete / failed`. The plan adds `unavailable` (complete job whose output file is missing or hash-mismatched) and `stale` (source changed under a non-complete job). - -**Verdict:** Necessary operational additions. `unavailable` prevents silently serving stale or missing output. `stale` gives a precise, actionable terminal state. Both have corresponding error codes in contracts/04 and recovery cases in contracts/07. No INSTRUCT rule prohibits them. - -### 4.3 Settings dependency - -Parent README declares a dependency on "merged `/settings` page, SQLite-backed BCP 47 target-language setting, and current OpenAI auth settings boundary." - -**Verified present:** -- `src/routes/settings.tsx` exists -- `src/routes/api/settings/translation-language.ts` exists -- `src/server/db/schema.ts` contains `lang_code`/`translation_target` references - -**Verdict:** Dependency satisfied in codebase. Not a blocker. - -### 4.4 Frontmatter preservation - -INSTRUCT does not specify frontmatter handling. The plan adds explicit preservation: contracts/05 step 1 parses frontmatter separately and stitching prepends raw bytes unchanged. - -**Verdict:** Necessary addition. Without it, any frontmatter in source `CONTENT.md` would be lost or incorrectly assigned a block id and translated. The plan correctly scopes frontmatter as metadata-only. - -### 4.5 `outputSchema` fallback chain - -INSTRUCT does not define a fallback when the app-server rejects structured output. The plan adds: try `outputSchema` → fall back to prompt-only JSON → fail chunk with `invalid_final_output` if both fail. - -**Verdict:** Correct defensive design for MVP compatibility. Well-specified in contracts/06. - -### 4.6 `resolveCurrentTranslation()` and `repairUnavailableTranslation()` split - -INSTRUCT does not specify this boundary. The plan introduces a read-only resolver shared by reader route, tabs, and API, and a separate mutating repair function scoped to API/job-start boundaries only. - -**Verdict:** Good design. Prevents accidental mutation from read-path code. - ---- - -## 5. Historical weaknesses and risks (non-blocking review history) - -These notes are historical review context, not current implementation instructions. Before acting on any item in this section, check the canonical focused contracts and subtask files; later edits may have resolved or superseded the risk. - -### 5.1 Historical omission-marker detection was brittle - -Earlier contract drafts checked for strings such as `omitted`, `summary`, `summarized`, `省略`, `要約`, and `...`. These heuristic string matches could produce: - -- **False positives**: legitimate translated text containing "要約" as a section title. -- **False negatives**: novel omission patterns not in the list (e.g., Chinese/Korean equivalents, `[...]`, `[cut]`). - -**Current direction:** Prompting forbids summarization and placeholder substitution, while validation relies on schema, segment identity, non-empty output, parser-backed structure checks, protected-span preservation, and length-ratio checks. Legitimate translated labels such as `Abstract` -> `要約` must not be rejected by lexical matching alone. - -### 5.2 Concurrent POST race for the same `(memory_id, lang_code)` not fully specified - -contracts/04 uses the unique partial index `(memory_id, lang_code, source_hash) WHERE status = 'complete'` and an active-states index. But two simultaneous `POST /api/memories/:id/translations` requests with the same `(memory_id, lang_code)` could create two `pending` jobs before either transitions to `running`. The contracts define "reuse active job" behavior but do not specify whether the serialization happens at SQLite level (advisory lock, unique constraint) or at application level (mutex, queue). - -**Recommendation:** Subtask 03 (state machine) or 15 (error handling) should add an explicit "at most one active job per (memory_id, lang_code)" invariant with a concrete serialization mechanism. - -### 5.3 `outputSchema` fallback is only specified for app-server rejection, not for model refusal - -If the app-server accepts `outputSchema` but the model returns output that fails JSON validation on the first attempt, retry is used. If the model cannot produce valid JSON across all retries, the chunk fails with `invalid_final_output`. This path is correct. However, there is no specified behavior for partial JSON that passes schema but fails semantic validation (e.g., wrong block ids). The validation algorithm handles this case (steps 3–4) but the error code assigned is `validation_failed`, not `invalid_final_output`. Subtask 09 implementors should confirm error code assignment at the chunk level is consistent with the API error codes in contracts/04. - -### 5.4 `translation.job.snapshot` reconnect — no durable event replay - -contracts/04 states: "On reconnect, emit `translation.job.snapshot` first using current SQLite job/chunk state, then stream new events." But "stream new events" means events emitted after the reconnect — any events fired while the client was disconnected between the snapshot and re-subscription are lost. - -For MVP this is acceptable (the snapshot covers state). Flag for subtask 07 to document this gap explicitly so future implementors do not assume replay coverage. - -### 5.5 `repairUnavailableTranslation()` ownership — resolved - -**Resolved after this review snapshot.** `repairUnavailableTranslation()` is now owned by 19.3 through `src/server/translation/current-translation.ts`. 19.11 recovery reuses that helper instead of implementing a second unavailable-repair path. - -**Canonical source:** use the focused contracts and subtask files, not this historical note, when implementing unavailable repair. - -### 5.6 Oversized single block handling is deferred - -contracts/05: "If a single block exceeds `maxRoughTokens`, mark the chunk as oversized and let Codex validation/retry handle context errors." This means a large `code_fence` or `math_block` may fail and exhaust retries without any recovery path. - -**Recommendation:** Subtask 04 or 17 should add a test fixture with an oversized block and specify the observable failure mode (chunk fails with `context_overflow`, job fails) so it is not silently swallowed. - ---- - -## 6. Out-of-scope items (not part of this review) - -The following were mentioned in earlier project context but are not part of Workflow 19: - -- `bun run dev` crash (exit code 1) — separate debugging workflow needed -- Test suite improvements — not part of Brilliant planning -- GitHub Actions CI/CD review — not part of Brilliant planning -- Agent-doc automation — not part of Brilliant planning - -These require separate workflow definitions. - ---- - -## 7. Open questions - -Per INSTRUCT line 725: "Open questions should be minimal and only include issues that block implementation." - -**No blocking open questions found.** - -The plan fully specifies: storage layout, state machine, chunking algorithm, Codex protocol, auth flow, streaming, validation, retry, atomic commit, purge, recovery, frontend integration, and reader routes. All INSTRUCT decisions are captured. The settings dependency is verified in the codebase. - -The weaknesses in §5 are tracked risks for subtask implementors, not pre-implementation blockers. - ---- - -## 8. Conclusion - -Workflow 19 satisfies all 16 INSTRUCT acceptance criteria and all stated security requirements. Intentional divergences from INSTRUCT are documented and justified. No blocking issues found. - -**The plan is ready for implementation authorization.** - -Implementation must begin with subtask 19.1 (requirements and architecture finalization), which reads all contracts before any code is written. Subtask implementors must follow the file ownership map in contracts/01 and the path mapping note in the parent README (plural `memories/`). - -The five risks in §5 should be addressed as the relevant subtasks are implemented: §5.2 by subtask 03/15, §5.5 by subtask 11, §5.6 by subtask 04/17. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly.md deleted file mode 100644 index b895b332..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly.md +++ /dev/null @@ -1,83 +0,0 @@ -# Task 19U: Segment-Based Markdown Translation Reassembly Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace the fragile full-scratch Markdown translation path with parser-backed text segment translation and deterministic Markdown reassembly. - -**Architecture:** This file is the orchestration index only. Domain work is split into small subtask plans so each worker can load one focused contract, implement that domain, run its verification slice, and hand off concrete artifacts to the next worker. The backend will parse source Markdown, extract only translatable text segments with source ranges, ask Codex for `{ id, translated_text }`, splice translated text back into the original Markdown, and validate preserved structure with parser-backed fingerprints. - -**Tech Stack:** TypeScript, Bun, Vitest, existing `markdown-it` renderer, `unified`, `remark-parse`, `remark-gfm`, `remark-math`, `unist-util-visit-parents`, mdast/unist position offsets, optional narrow `micromark` fallback, existing Codex app-server client, SQLite translation job/chunk tables. - ---- - -## Execution Model - -Task 19U is intentionally decomposed. Do not ask a single worker to carry the entire Markdown parser, prompt schema, runner, docs, and E2E context at once. - -Execution status: complete. All domain subtasks below have been executed and -verified against the commands listed in this workflow. - -Each subtask owns one domain: - -- A clear role. -- A small file surface. -- A focused test command. -- A concrete handoff contract for downstream subtasks. - -Subagents should be assigned one subtask file at a time. The parent worker reviews each handoff before starting dependent subtasks. - -## Subtask Index - -1. [Regression fixtures and library decision](task-19-codex-translation-segment-reassembly/01-regression-fixtures-and-library-decision.md) -2. [Parser adapter](task-19-codex-translation-segment-reassembly/02-parser-adapter.md) -3. [Segment manifest and reassembly](task-19-codex-translation-segment-reassembly/03-segment-manifest-and-reassembly.md) -4. [Structure fingerprint validation](task-19-codex-translation-segment-reassembly/04-structure-fingerprint-validation.md) -5. [Prompt schema and policy](task-19-codex-translation-segment-reassembly/05-prompt-schema-and-policy.md) -6. [Chunker, runner, and stitching integration](task-19-codex-translation-segment-reassembly/06-chunker-runner-and-stitching.md) -7. [Workflow contracts and docs cleanup](task-19-codex-translation-segment-reassembly/07-workflow-contracts-and-docs.md) -8. [End-to-end verification](task-19-codex-translation-segment-reassembly/08-end-to-end-verification.md) - -## Dependency Graph - -```mermaid -flowchart TD - A["01 regression fixtures and library decision"] --> B["02 parser adapter"] - B --> C["03 segment manifest and reassembly"] - B --> D["04 structure fingerprint validation"] - C --> E["05 prompt schema and policy"] - E --> F["06 chunker runner stitching"] - D --> G["07 workflow contracts and docs"] - F --> G - G --> H["08 end-to-end verification"] -``` - -## Parallelization Rules - -- `01` must run first. -- `02` runs after `01`. -- `03` and `04` can run in parallel after `02`. -- `05` depends on `03`. -- `06` depends on `05`. -- `07` can begin after `05`, but must be finalized after `06`. -- `08` runs last. - -## Cross-Cutting Constraints - -- Do not continue expanding the regex scanner to cover all Markdown. -- Do not use a Markdown stringifier for production output. -- Do not introduce `tree-sitter-markdown` for this task. -- Keep `markdown-it` as the Reader renderer, not the segment extraction source of truth. -- Use `micromark` only as a narrow token-offset fallback when mdast positions cannot represent a required source range. -- Preserve existing storage, auth, app-server transport, frontend controls, and backup behavior unless a subtask explicitly says otherwise. -- Completed translated output remains a normal `CONTENT.md` variant under `memories///CONTENT.md`. -- Temporary translated chunk bodies remain temporary SQLite data and are still purged after final commit. -- `BRILLIANT_PROMPT_POLICY_VERSION` must move to a new monotonic segment policy such as `brilliant-segments-v1`. - -## Final Acceptance Criteria - -- Codex no longer receives instructions to emit full translated Markdown for the primary path. -- Codex returns only segment ids and translated text. -- Original Markdown syntax is preserved by deterministic source splicing. -- Link destinations, image destinations, code, inline code, math, HTML, footnote labels, table shape, and frontmatter are preserved without relying on model behavior. -- The scratch regex scanner is not the source of truth for Markdown dialect coverage. -- Focused translation tests, typecheck, whitespace checks, and full verification pass or document a confirmed unrelated existing flake. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/01-regression-fixtures-and-library-decision.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/01-regression-fixtures-and-library-decision.md deleted file mode 100644 index 62439ada..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/01-regression-fixtures-and-library-decision.md +++ /dev/null @@ -1,117 +0,0 @@ -# Task 19U.01: Regression Fixtures And Library Decision Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Lock the current Markdown translation regression surface and install the parser stack that prevents further full-scratch parser growth. - -**Architecture:** This subtask owns evidence and dependency selection only. It captures known scratch-parser weak points as tests/fixtures, then adds the unified/remark/mdast parser dependencies that later subtasks consume. - -**Tech Stack:** Bun, Vitest, `unified`, `remark-parse`, `remark-gfm`, `remark-math`, `unist-util-visit-parents`, `@types/mdast`, `@types/unist`. - ---- - -## Role - -Regression and library-decision owner. - -This worker must not implement segment extraction, prompt schema, runner integration, or docs cleanup. Its output is a fixture matrix, characterization tests, and dependency updates. - -## Files - -- Modify: `package.json` -- Modify: `bun.lock` -- Modify: `tests/server/translation/markdown-blocks.test.ts` -- Modify: `tests/server/translation/prompt.test.ts` -- Create: `tests/fixtures/translation/markdown-segment-matrix.md` - -## Task Steps - -- [x] **Step 1: Add the Markdown segment matrix fixture** - -Create `tests/fixtures/translation/markdown-segment-matrix.md`: - -````md ---- -id: segment-matrix -title: Segment Matrix ---- - -# ATX Heading - -Setext Heading --------------- - -Paragraph with `inlineCode`, $e^{i\pi}+1=0$, \(x+y\), [the docs](https://example.com/docs "Docs title"), and [Smith et al., 2020]. - - const value = 1; - console.log(value); - -`````md -```ts -const nested = true; -``` -````` - -| Term | Meaning | -| --- | --- | -| API | Application interface | - -> Quoted text with **strong words**. - -- First item - continuation text -- Second item - -![Diagram alt text](https://example.com/diagram.png) - -[^1]: Footnote text with [source](https://example.com/source). - -[ref-docs]: https://example.com/ref "Reference title" -Read [reference docs][ref-docs]. -```` - -- [x] **Step 2: Add characterization tests for known gaps** - -In `tests/server/translation/markdown-blocks.test.ts`, add these explicit pending tests: - -```ts -it.todo("does not translate inline math spans"); -it.todo("treats indented code blocks as non-translatable code"); -it.todo("preserves setext heading structure"); -it.todo("preserves Markdown link titles and reference labels"); -it.todo("does not protect ordinary prose as shell commands"); -``` - -- [x] **Step 3: Add parser dependencies** - -Run: - -```sh -mise exec -- bun add unified remark-parse remark-gfm remark-math unist-util-visit-parents -mise exec -- bun add -d @types/mdast @types/unist -``` - -Expected: - -- `package.json` and `bun.lock` are updated. -- `remark-stringify`, `mdast-util-to-markdown`, and `tree-sitter-markdown` are not added. -- `remark-frontmatter` is not added unless a later task intentionally stops using raw frontmatter splitting. - -- [x] **Step 4: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/markdown-blocks.test.ts tests/server/translation/prompt.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Existing tests pass. -- New `it.todo` entries are reported as todo/pending. -- Typecheck passes. - -## Handoff - -Downstream workers can assume parser dependencies exist and the fixture matrix is available. They must not reinterpret the library decision without updating this subtask and the parent orchestration index. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/02-parser-adapter.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/02-parser-adapter.md deleted file mode 100644 index 79495e66..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/02-parser-adapter.md +++ /dev/null @@ -1,162 +0,0 @@ -# Task 19U.02: Parser Adapter Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Add the parser adapter that exposes frontmatter splitting, parsed mdast, and source-offset diagnostics for translation subtasks. - -**Architecture:** The adapter wraps unified/remark setup behind a small TRAUMA-owned module. It keeps raw frontmatter out of Markdown parsing so final output can preserve frontmatter exactly. - -**Tech Stack:** TypeScript, Vitest, `unified`, `remark-parse`, `remark-gfm`, `remark-math`, mdast `Root`. - ---- - -## Role - -Parser adapter owner. - -This worker must not implement segment extraction, validation fingerprints, prompt changes, or runner changes. - -## Files - -- Create: `src/server/translation/markdown-parser.ts` -- Create: `tests/server/translation/markdown-parser.test.ts` - -## Task Steps - -- [x] **Step 1: Write parser adapter tests** - -Create `tests/server/translation/markdown-parser.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; - -import { - parseTranslationMarkdownAst, - splitMarkdownFrontmatter, -} from "../../../src/server/translation/markdown-parser"; - -describe("translation Markdown parser adapter", () => { - it("preserves raw frontmatter separately from the Markdown body", () => { - const parsed = splitMarkdownFrontmatter("---\nid: memory\n---\n# Title\n"); - - expect(parsed.frontmatter).toBe("---\nid: memory\n---\n"); - expect(parsed.bodyMarkdown).toBe("# Title\n"); - expect(parsed.bodyOffset).toBe("---\nid: memory\n---\n".length); - }); - - it("parses GFM tables, footnotes, math, and indented code with positions", () => { - const parsed = parseTranslationMarkdownAst([ - "# Title", - "", - "Paragraph with $x+y$ and [docs](https://example.com).", - "", - " const value = 1;", - "", - "| A | B |", - "| --- | --- |", - "| x | y |", - "", - "[^1]: Footnote text.", - "", - ].join("\n")); - - expect(parsed.diagnostics).toEqual([]); - expect(parsed.tree.type).toBe("root"); - expect(parsed.bodyMarkdown).toContain("| A | B |"); - expect(parsed.tree.children.some((node) => node.type === "table")).toBe(true); - expect(parsed.tree.children.some((node) => node.type === "code")).toBe(true); - expect(JSON.stringify(parsed.tree)).toContain("\"position\""); - }); -}); -``` - -- [x] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/markdown-parser.test.ts -``` - -Expected: - -- FAIL because `src/server/translation/markdown-parser.ts` does not exist. - -- [x] **Step 3: Implement parser adapter** - -Create `src/server/translation/markdown-parser.ts`: - -```ts -import type { Root } from "mdast"; -import remarkGfm from "remark-gfm"; -import remarkMath from "remark-math"; -import remarkParse from "remark-parse"; -import { unified } from "unified"; - -export interface ParsedTranslationMarkdown { - bodyMarkdown: string; - bodyOffset: number; - diagnostics: string[]; - frontmatter: string; - tree: Root; -} - -export function splitMarkdownFrontmatter(sourceMarkdown: string): { - bodyMarkdown: string; - bodyOffset: number; - frontmatter: string; -} { - const opening = /^---(?:\r?\n)/.exec(sourceMarkdown); - if (opening === null) { - return { bodyMarkdown: sourceMarkdown, bodyOffset: 0, frontmatter: "" }; - } - - const afterOpening = sourceMarkdown.slice(opening[0].length); - const closing = /\r?\n---(?:\r?\n|$)/.exec(afterOpening); - if (closing === null || closing.index === undefined) { - return { bodyMarkdown: sourceMarkdown, bodyOffset: 0, frontmatter: "" }; - } - - const end = opening[0].length + closing.index + closing[0].length; - return { - bodyMarkdown: sourceMarkdown.slice(end), - bodyOffset: end, - frontmatter: sourceMarkdown.slice(0, end), - }; -} - -export function parseTranslationMarkdownAst(sourceMarkdown: string): ParsedTranslationMarkdown { - const split = splitMarkdownFrontmatter(sourceMarkdown); - const processor = unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkMath); - const tree = processor.parse(split.bodyMarkdown) as Root; - - return { - bodyMarkdown: split.bodyMarkdown, - bodyOffset: split.bodyOffset, - diagnostics: [], - frontmatter: split.frontmatter, - tree, - }; -} -``` - -- [x] **Step 4: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/markdown-parser.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Parser adapter tests pass. -- Typecheck passes. - -## Handoff - -Downstream workers can import `parseTranslationMarkdownAst()` and `splitMarkdownFrontmatter()`. Segment extraction and fingerprint validation must use this adapter rather than constructing separate remark processors. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/03-segment-manifest-and-reassembly.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/03-segment-manifest-and-reassembly.md deleted file mode 100644 index fe17f26c..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/03-segment-manifest-and-reassembly.md +++ /dev/null @@ -1,176 +0,0 @@ -# Task 19U.03: Segment Manifest And Reassembly Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Extract translatable text segments from parsed Markdown and deterministically splice translated text back into original source ranges. - -**Architecture:** Codex should never rewrite Markdown syntax. This subtask creates the segment IR and reassembly function that later prompt and runner code use as the only production output path. - -**Tech Stack:** TypeScript, Vitest, mdast positions, `unist-util-visit-parents`, parser adapter from Task 19U.02. - ---- - -## Role - -Segment IR and source-splicing owner. - -This worker must not change the Codex prompt schema or runner. It only creates the manifest and deterministic reassembly primitives. - -## Files - -- Modify: `src/server/translation/types.ts` -- Create: `src/server/translation/translation-segments.ts` -- Create: `tests/server/translation/translation-segments.test.ts` - -## Task Steps - -- [x] **Step 1: Write segment extraction and splicing tests** - -Create `tests/server/translation/translation-segments.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; - -import { - applyTranslatedSegments, - createTranslationSegmentManifest, -} from "../../../src/server/translation/translation-segments"; - -describe("translation segment manifest", () => { - it("extracts only translatable text while preserving syntax ranges", () => { - const manifest = createTranslationSegmentManifest([ - "# Source Title", - "", - "Read [the docs](https://example.com/docs \"Docs\") and `inlineCode`.", - "", - "$$", - "E = mc^2", - "$$", - "", - ].join("\n")); - - expect(manifest.segments.map((segment) => segment.text)).toEqual([ - "Source Title", - "Read ", - "the docs", - " and ", - ]); - expect(manifest.segments.some((segment) => segment.text.includes("inlineCode"))).toBe(false); - expect(manifest.protectedRanges.some((range) => range.kind === "math")).toBe(true); - }); - - it("reassembles translated text into the original Markdown syntax", () => { - const source = "Read [the docs](https://example.com/docs \"Docs\") and `inlineCode`.\n"; - const manifest = createTranslationSegmentManifest(source); - const output = applyTranslatedSegments({ - manifest, - translations: [ - { segmentId: "s000001", translatedText: "読んでください " }, - { segmentId: "s000002", translatedText: "ドキュメント" }, - { segmentId: "s000003", translatedText: " と " }, - ], - }); - - expect(output).toBe("読んでください [ドキュメント](https://example.com/docs \"Docs\") と `inlineCode`.\n"); - }); -}); -``` - -- [x] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-segments.test.ts -``` - -Expected: - -- FAIL because `translation-segments.ts` does not exist. - -- [x] **Step 3: Add segment types** - -Modify `src/server/translation/types.ts`: - -```ts -export interface TranslationTextSegment { - blockId: string; - id: string; - sourceEnd: number; - sourceStart: number; - text: string; -} - -export interface TranslationProtectedRange { - kind: - | "code" - | "inline_code" - | "math" - | "html" - | "link_destination" - | "link_title" - | "image_destination" - | "footnote_label" - | "table_delimiter" - | "frontmatter"; - sourceEnd: number; - sourceStart: number; - value: string; -} - -export interface TranslationSegmentReplacement { - segmentId: string; - translatedText: string; -} -``` - -- [x] **Step 4: Implement manifest and splicing** - -Create `src/server/translation/translation-segments.ts` with these exports: - -```ts -export interface TranslationSegmentManifest { - frontmatter: string; - protectedRanges: TranslationProtectedRange[]; - segments: TranslationTextSegment[]; - sourceMarkdown: string; -} - -export function createTranslationSegmentManifest(sourceMarkdown: string): TranslationSegmentManifest; - -export function applyTranslatedSegments(input: { - manifest: TranslationSegmentManifest; - translations: TranslationSegmentReplacement[]; -}): string; -``` - -Implementation requirements: - -- Parse with `parseTranslationMarkdownAst()`. -- Traverse with `unist-util-visit-parents`. -- Create segment ids as `s000001`, `s000002`, then continue in source order. -- Extract `text` nodes under translatable containers. -- Exclude descendants of `code`, `inlineCode`, `math`, `inlineMath`, `html`, `yaml`, and link/image URL/title metadata. -- Allow link labels, emphasis text, heading text, list item text, blockquote text, table cell text, and footnote body text. -- Use mdast `position.start.offset` and `position.end.offset` for `sourceStart` and `sourceEnd`, adjusted by `bodyOffset` when using full-source offsets. -- If a required node does not expose usable offsets, add a narrow `micromark` fallback for that construct and a regression test proving the fallback range. -- Apply replacements in descending `sourceStart` order. -- Throw a `TranslationOutputValidationError` if a segment id is missing, duplicated, unknown, or maps to an empty translated string after trim. - -- [x] **Step 5: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/translation-segments.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Segment tests pass. -- Typecheck passes. - -## Handoff - -Downstream prompt and runner workers can call `createTranslationSegmentManifest()` to build prompt input and `applyTranslatedSegments()` to produce reassembled Markdown. They must not ask Codex to return full Markdown for the primary path. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/04-structure-fingerprint-validation.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/04-structure-fingerprint-validation.md deleted file mode 100644 index b260844f..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/04-structure-fingerprint-validation.md +++ /dev/null @@ -1,145 +0,0 @@ -# Task 19U.04: Structure Fingerprint Validation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Validate that reassembled translated Markdown preserves source Markdown structure and protected syntax. - -**Architecture:** Structural validation compares parser-backed fingerprints for source and translated Markdown. It replaces regex protected-span scanning as the primary correctness mechanism. - -**Tech Stack:** TypeScript, Vitest, parser adapter from Task 19U.02, `TranslationOutputValidationError`. - ---- - -## Role - -Structural validation owner. - -This worker must not change prompt construction or runner persistence. It only owns fingerprint creation and comparison. - -## Files - -- Create: `src/server/translation/structure-fingerprint.ts` -- Create: `tests/server/translation/structure-fingerprint.test.ts` - -## Task Steps - -- [x] **Step 1: Write fingerprint tests** - -Create `tests/server/translation/structure-fingerprint.test.ts`: - -```ts -import { describe, expect, it } from "vitest"; - -import { - assertMarkdownStructurePreserved, - createMarkdownStructureFingerprint, -} from "../../../src/server/translation/structure-fingerprint"; - -describe("translation structure fingerprint", () => { - it("treats translated prose as equivalent when Markdown syntax is unchanged", () => { - const source = "Read [docs](https://example.com/docs) and `code`.\n"; - const translated = "読む [ドキュメント](https://example.com/docs) と `code`.\n"; - - expect(() => assertMarkdownStructurePreserved({ source, translated })).not.toThrow(); - }); - - it("rejects changed link destinations and inline code", () => { - expect(() => - assertMarkdownStructurePreserved({ - source: "Read [docs](https://example.com/docs).\n", - translated: "読む [docs](https://wrong.example/docs).\n", - }) - ).toThrow(/link destination/); - - expect(() => - assertMarkdownStructurePreserved({ - source: "Use `inlineCode`.\n", - translated: "Use `translatedCode`.\n", - }) - ).toThrow(/inline code/); - }); - - it("creates stable fingerprints for table shape and inline math", () => { - const fingerprint = createMarkdownStructureFingerprint([ - "| A | B |", - "| --- | --- |", - "| x | y |", - "", - "$x+y$", - ].join("\n")); - - expect(fingerprint.entries.some((entry) => entry.kind === "table")).toBe(true); - expect(fingerprint.entries.some((entry) => entry.kind === "inline_math")).toBe(true); - }); -}); -``` - -- [x] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/structure-fingerprint.test.ts -``` - -Expected: - -- FAIL because `structure-fingerprint.ts` does not exist. - -- [x] **Step 3: Implement fingerprint comparison** - -Create `src/server/translation/structure-fingerprint.ts` with these exports: - -```ts -export interface MarkdownStructureFingerprint { - entries: MarkdownStructureFingerprintEntry[]; -} - -export interface MarkdownStructureFingerprintEntry { - kind: - | "block" - | "code" - | "inline_code" - | "math" - | "inline_math" - | "html" - | "link_destination" - | "image_destination" - | "table" - | "footnote_definition"; - value: string; -} - -export function createMarkdownStructureFingerprint(markdown: string): MarkdownStructureFingerprint; - -export function assertMarkdownStructurePreserved(input: { - source: string; - translated: string; -}): void; -``` - -Implementation requirements: - -- Parse source and translated Markdown with `parseTranslationMarkdownAst()`. -- Compare ordered block kinds for structural block nodes. -- Compare exact values for code, inline code, math, inline math, HTML, link URLs, image URLs, and footnote identifiers. -- Compare table shape as row count and cell count per row, not translated cell text. -- Throw `TranslationOutputValidationError` with messages that identify the changed structure class. - -- [x] **Step 4: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/structure-fingerprint.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Fingerprint tests pass. -- Typecheck passes. - -## Handoff - -Prompt and runner workers can use `assertMarkdownStructurePreserved()` after segment reassembly. Regex protected-span checks may remain as legacy guardrails, but not as the primary validation contract. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/05-prompt-schema-and-policy.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/05-prompt-schema-and-policy.md deleted file mode 100644 index 7083b8f4..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/05-prompt-schema-and-policy.md +++ /dev/null @@ -1,145 +0,0 @@ -# Task 19U.05: Prompt Schema And Policy Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Change the Codex translation prompt from full Markdown block output to segment-only output. - -**Architecture:** The prompt must provide segment ids and source text, then require Codex to return only `{ id, translated_text }` entries. Validation reassembles Markdown locally and runs structural fingerprint checks. - -**Tech Stack:** TypeScript, Vitest, segment manifest from Task 19U.03, structure fingerprint from Task 19U.04, reader-translate skill policy. - ---- - -## Role - -Prompt schema and policy owner. - -This worker must not change chunk persistence or final stitching. It owns prompt construction, Codex output schema validation, prompt policy version, and the local policy skill update. - -## Files - -- Modify: `src/server/translation/prompt.ts` -- Modify: `tests/server/translation/prompt.test.ts` -- Modify: `.agents/skills/reader-translate/SKILL.md` - -## Task Steps - -- [x] **Step 1: Write prompt schema tests** - -Add tests to `tests/server/translation/prompt.test.ts`: - -```ts -it("builds a segment translation prompt that does not ask Codex to return Markdown", () => { - const chunk = createPromptChunk("Read [docs](https://example.com/docs) and `code`.\n"); - const prompt = buildTranslationPrompt({ - chunk, - targetLanguage: "ja-JP", - }); - - expect(prompt).toContain("Return translated text segments only"); - expect(prompt).toContain("\"segments\""); - expect(prompt).not.toContain("\"translated_markdown\""); -}); - -it("validates segment output and reassembles source Markdown syntax", () => { - const chunk = createPromptChunk("Read [docs](https://example.com/docs) and `code`.\n"); - const output = validateCodexChunkOutput({ - chunk, - output: { - chunk_index: 0, - segments: [ - { id: "s000001", translated_text: "読む " }, - { id: "s000002", translated_text: "ドキュメント" }, - { id: "s000003", translated_text: " と " }, - ], - warnings: [], - }, - }); - - expect(stringifyCodexChunkOutput(output)).toBe("読む [ドキュメント](https://example.com/docs) と `code`.\n"); -}); -``` - -- [x] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/prompt.test.ts -``` - -Expected: - -- FAIL because current prompt schema still expects `blocks[].translated_markdown`. - -- [x] **Step 3: Update prompt policy and schema** - -Modify `src/server/translation/prompt.ts`: - -- Set `BRILLIANT_PROMPT_POLICY_VERSION` to `brilliant-segments-v1`. -- Include segment list metadata in the prompt. -- Require this output schema: - -```json -{ - "type": "object", - "additionalProperties": false, - "required": ["chunk_index", "segments", "warnings"], - "properties": { - "chunk_index": { "type": "integer" }, - "segments": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["id", "translated_text"], - "properties": { - "id": { "type": "string" }, - "translated_text": { "type": "string" } - } - } - }, - "warnings": { - "type": "array", - "items": { "type": "string" } - } - } -} -``` - -Validation order: - -1. Validate JSON shape. -2. Validate `chunk_index`. -3. Validate exact segment ids in source order. -4. Reject missing, duplicate, and unknown ids. -5. Reassemble with `applyTranslatedSegments()`. -6. Run `assertMarkdownStructurePreserved()`. -7. Run existing length-ratio checks against the reassembled output. - -- [x] **Step 4: Update reader-translate skill policy** - -Modify `.agents/skills/reader-translate/SKILL.md` required behavior: - -```md -- Return translated text for the requested segment ids only. -- Never return full Markdown blocks unless the runtime explicitly uses the legacy block schema. -``` - -- [x] **Step 5: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/prompt.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Prompt tests pass. -- Typecheck passes. - -## Handoff - -Runner workers can call prompt validation and receive reassembled Markdown. They should not parse raw Codex output themselves except through prompt-domain helpers. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/06-chunker-runner-and-stitching.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/06-chunker-runner-and-stitching.md deleted file mode 100644 index e695e7f1..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/06-chunker-runner-and-stitching.md +++ /dev/null @@ -1,120 +0,0 @@ -# Task 19U.06: Chunker Runner And Stitching Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Thread segment manifests through chunking, Codex invocation, temporary persistence, and final stitching. - -**Architecture:** Chunking still follows source order, but chunks carry segment metadata. Runner submits segment prompts, validates segment output, stores reassembled chunk Markdown in the existing temporary chunk path, and final stitching commits normal translated Markdown content. - -**Tech Stack:** TypeScript, Vitest, existing translation repositories, segment prompt helpers from Task 19U.05. - ---- - -## Role - -Runtime integration owner. - -This worker must not change parser library selection or policy docs. It owns the production translation flow after prompt helpers exist. - -## Files - -- Modify: `src/server/translation/chunker.ts` -- Modify: `src/server/translation/runner.ts` -- Modify: `src/server/translation/stitching.ts` -- Modify: `src/server/translation/types.ts` -- Modify: `tests/server/translation/chunker.test.ts` -- Modify: `tests/server/translation/runner.test.ts` - -## Task Steps - -- [x] **Step 1: Write chunker and runner tests** - -Add chunker assertions: - -```ts -expect(chunks[0]?.segments.map((segment) => segment.id)).toEqual([ - "s000001", - "s000002", -]); -``` - -Add runner fake-client assertions: - -```ts -expect(observedPrompt).toContain("\"segments\""); -expect(observedPrompt).not.toContain("\"translated_markdown\""); -``` - -Add committed output assertions: - -```ts -expect(committedMarkdown).toContain("[ドキュメント](https://example.com/docs)"); -expect(committedMarkdown).toContain("`inlineCode`"); -``` - -- [x] **Step 2: Verify RED** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/chunker.test.ts tests/server/translation/runner.test.ts -``` - -Expected: - -- FAIL because chunks do not carry segment metadata and runner still expects block Markdown output. - -- [x] **Step 3: Extend chunk types** - -Modify `src/server/translation/types.ts`: - -```ts -export interface TranslationChunk { - segments: TranslationTextSegment[]; -} -``` - -Keep existing `TranslationChunk` fields. Add `segments` without removing persisted job/chunk status types. - -- [x] **Step 4: Update chunk creation** - -Modify `src/server/translation/chunker.ts`: - -- Create or receive the parser-backed segment manifest. -- Include all segments whose source ranges belong to the chunk's block ids. -- Preserve segment order by source offset. -- Keep chunk size limits based on source Markdown, not translated output. - -- [x] **Step 5: Update runner and stitching** - -Modify `src/server/translation/runner.ts`: - -- Build segment prompts from `chunk.segments`. -- Pass the segment schema to Codex app-server. -- Validate returned segment output through prompt-domain helpers. -- Persist reassembled chunk Markdown in the existing temporary chunk storage path. -- Continue retrying on `TranslationOutputValidationError`. - -Modify `src/server/translation/stitching.ts`: - -- Preserve raw frontmatter exactly. -- Stitch reassembled chunk Markdown in source order. -- Continue writing the final translated `CONTENT.md` variant through existing storage boundaries. - -- [x] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/chunker.test.ts tests/server/translation/runner.test.ts -mise exec -- bun run typecheck -``` - -Expected: - -- Chunker and runner tests pass. -- Typecheck passes. - -## Handoff - -Docs and E2E workers can assume the primary runtime path uses segment prompts and deterministic reassembly. Existing temporary SQLite `translated_markdown` storage may still hold reassembled chunk Markdown until purge. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/07-workflow-contracts-and-docs.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/07-workflow-contracts-and-docs.md deleted file mode 100644 index 5c5f722f..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/07-workflow-contracts-and-docs.md +++ /dev/null @@ -1,77 +0,0 @@ -# Task 19U.07: Workflow Contracts And Docs Cleanup Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Remove stale scratch-parser and block-output claims from Task 19 workflow contracts. - -**Architecture:** Documentation must match the new runtime contract: parser-backed segment extraction, segment-only Codex output, deterministic reassembly, and parser-backed structural validation. - -**Tech Stack:** Markdown docs, `rg`, `git diff --check`. - ---- - -## Role - -Workflow contract owner. - -This worker must not modify runtime code. It only updates durable docs after code subtasks define the final contracts. - -## Files - -- Modify: `docs/workflows/task-19-codex-translation/04-markdown-block-manifest-and-chunker.md` -- Modify: `docs/workflows/task-19-codex-translation/08-chunk-translation-prompt-and-output-schema.md` -- Modify: `docs/workflows/task-19-codex-translation/09-chunk-validation-and-retry-logic.md` -- Modify: `docs/workflows/task-19-codex-translation/contracts/05-markdown-chunking.md` -- Modify: `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` - -## Task Steps - -- [x] **Step 1: Update Markdown chunking contract** - -Replace line-oriented parser language with: - -```md -Markdown parsing is parser-backed. The implementation must not attempt to cover the Markdown dialect through ad hoc regex scanning. Line-oriented fallback logic may exist only for controlled diagnostics or migration compatibility. -``` - -- [x] **Step 2: Update prompt output contract** - -Replace block-output examples with: - -```json -{ - "chunk_index": 0, - "segments": [ - { "id": "s000001", "translated_text": "翻訳されたテキスト" } - ], - "warnings": [] -} -``` - -- [x] **Step 3: Update validation contract** - -Document these primary validation steps: - -- Segment id and count validation. -- Deterministic reassembly from original source Markdown. -- Parser-backed structural fingerprint comparison. -- Regex protected spans are legacy guardrails, not the primary correctness mechanism. - -- [x] **Step 4: Verify docs references** - -Run: - -```sh -rg -n "translated_markdown|scan Markdown line by line|protected spans are used by validation" docs/workflows/task-19-codex-translation docs/workflows/task-19-codex-translation.md -git diff --check -- docs/workflows/task-19-codex-translation docs/workflows/task-19-codex-translation.md -``` - -Expected: - -- Remaining `translated_markdown` mentions are explicitly labeled legacy storage or temporary reassembled chunk storage. -- No contract requires full scratch Markdown parsing. -- No whitespace errors. - -## Handoff - -E2E verification can use updated docs as the canonical Task 19U contract. If implementation and docs conflict, the worker must stop and report the exact file and line mismatch. diff --git a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/08-end-to-end-verification.md b/docs/workflows/archive/task-19-codex-translation-segment-reassembly/08-end-to-end-verification.md deleted file mode 100644 index c17110bc..00000000 --- a/docs/workflows/archive/task-19-codex-translation-segment-reassembly/08-end-to-end-verification.md +++ /dev/null @@ -1,134 +0,0 @@ -# Task 19U.08: End-To-End Verification Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking. - -**Goal:** Verify the segment translation pipeline end to end with long article and academic-paper fixtures. - -**Architecture:** This subtask proves the complete pipeline preserves Markdown syntax while translating prose through segment output. It does not introduce new architecture. - -**Tech Stack:** Vitest, Bun, parser fixtures, fake Codex client, translation repositories. - ---- - -## Role - -End-to-end verification owner. - -This worker must not change library selection or runtime architecture unless a failing E2E test exposes a concrete implementation bug. Any such bug fix must remain scoped to the failing domain. - -## Files - -- Create: `tests/fixtures/translation/academic-paper-segments.md` -- Modify: `tests/server/translation/runner.test.ts` -- Modify: `tests/server/translation/api-routes.test.ts` -- Modify: `tests/server/translation/translation-repositories.test.ts` - -## Task Steps - -- [x] **Step 1: Add academic fixture** - -Create `tests/fixtures/translation/academic-paper-segments.md`: - -````md ---- -id: academic-segments -title: Segment Translation Fixture ---- - -# Segment Translation for Reader Archives - -## Abstract - -We evaluate a local-first reader archive that translates article prose while preserving Markdown syntax [Smith et al., 2024]. - -## Method - -The system keeps inline math such as $p(y|x)$ unchanged and translates only surrounding prose. - -$$ -\operatorname*{argmax}_y p(y|x) -$$ - -| Component | Requirement | -| --- | --- | -| Parser | Preserve structure | -| Translator | Return segments | - -> Block quotes remain block quotes even when their text changes. - -Use `inlineCode` and fenced code without translation: - -```ts -const preserved = "code"; -``` - -See [the reference implementation](https://example.com/reference "Reference title"). - -[^1]: Footnotes may contain prose and [links](https://example.com/footnote). - -## References - -Smith, A. and Lee, K. (2024). Segment translation for structured documents. -```` - -- [x] **Step 2: Add fake Codex segment translator** - -In runner tests, adapt the fake client to parse prompt segment metadata and return one entry per segment id: - -```json -{ - "chunk_index": 0, - "segments": [ - { "id": "s000001", "translated_text": "JA:s000001" }, - { "id": "s000002", "translated_text": "JA:s000002" } - ], - "warnings": [] -} -``` - -The fake translator must not return Markdown syntax. - -- [x] **Step 3: Assert committed translated Markdown preserves syntax** - -Add assertions that committed output: - -- Contains original link destinations. -- Contains original code fence content. -- Contains original inline code. -- Contains original math. -- Has the same table row and column counts. -- Preserves frontmatter exactly. -- Contains translated prose markers from fake segment output. - -- [x] **Step 4: Run focused verification** - -Run: - -```sh -mise exec -- bun run test tests/server/translation/markdown-parser.test.ts tests/server/translation/translation-segments.test.ts tests/server/translation/structure-fingerprint.test.ts tests/server/translation/prompt.test.ts tests/server/translation/chunker.test.ts tests/server/translation/runner.test.ts tests/server/translation/api-routes.test.ts tests/server/translation/translation-repositories.test.ts -mise exec -- bun run typecheck -git diff --check -``` - -Expected: - -- All focused translation tests pass. -- Typecheck passes. -- No whitespace errors. - -- [x] **Step 5: Run full verification** - -Run: - -```sh -mise exec -- bun run verify -``` - -Expected: - -- Full verify passes. -- If unrelated backup/delete timeout flakes recur, re-run the failing test files individually and record the exact unrelated failure in the handoff. - -## Handoff - -This subtask closes Task 19U only after focused translation tests, typecheck, whitespace checks, and full verification have evidence. Do not claim Task 19U complete from partial focused tests alone. diff --git a/docs/workflows/archive/task-19-codex-translation-validation-feedback-repair.md b/docs/workflows/archive/task-19-codex-translation-validation-feedback-repair.md deleted file mode 100644 index ddda9831..00000000 --- a/docs/workflows/archive/task-19-codex-translation-validation-feedback-repair.md +++ /dev/null @@ -1,453 +0,0 @@ -# Task 19X: Translation Validation Feedback Repair Workflow - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make Brilliant translation validation failures actionable and retryable without weakening Markdown preservation guarantees. - -**Architecture:** Keep Task 19U's segment-only translation and deterministic Markdown reassembly architecture. Add structured validation diagnostics at the validator boundary, persist safe diagnostics through the existing translation error JSON, and feed the previous failure summary into fresh chunk retry prompts. Do not store raw Codex output or completed translated article bodies. - -**Tech Stack:** TypeScript, Bun, Vitest, existing `unified`/`remark` Markdown parser, existing Codex app-server translation client, existing SQLite `translation_jobs.error` and `translation_chunks.error` JSON columns. - ---- - -## Status - -- State: Implemented in this branch; focused verification and live Amp retry - passed. Full `bun run verify` handoff remains pending until the unrelated - runtime `package.json` host contract drift is resolved. -- Base workflow: [Task 19 Codex translation](task-19-codex-translation.md) -- Depends on: [Task 19U segment reassembly](task-19-codex-translation-segment-reassembly.md) -- Related context: - - [Task 19 chunk validation and retry logic](task-19-codex-translation/09-chunk-validation-and-retry-logic.md) - - [Task 19 prompt and validation contract](task-19-codex-translation/contracts/06-codex-prompt-and-validation.md) - - [Task 19 error handling and cancellation](task-19-codex-translation/15-error-handling-and-cancellation.md) - - [Coding standards](../../references/coding-standards/INDEX.md) - - [Verification strategy](../../quality/verification.md) - -## Current Failure To Reproduce - -Runtime database: `/Users/vvx/.trauma/trauma.sqlite` - -Failing memory: - -- Memory id: `019e5eee-566c-7732-9182-68ad075f3276` -- Title: `Amp Owner's Manual` -- URL: `https://ampcode.com/manual` -- Language: `ja-JP` -- Model: `gpt-5.3-codex-spark` -- Reasoning effort: `xhigh` - -Observed failed jobs: - -```text -019e5eee-8853-74fa-bb7d-4a091158aefd -chunk_index=1 -retry_count=3 -error={"code":"validation_failed","message":"Codex output changed inline code.","action":"retry"} - -019e5ef6-bac4-7223-9bae-32400eebfb54 -chunk_index=3 -retry_count=3 -error={"code":"validation_failed","message":"Codex output changed block structure.","action":"retry"} -``` - -The app-server path and auth path were working. Codex returned schema-valid -segment JSON. TRAUMA rejected the locally reassembled Markdown during semantic -validation. - -Resolved live retry evidence from this branch: - -```text -019e5fa5-9b46-775f-98b2-4a54dc63a9ba -status=complete -output_path=memories/019e5eee-566c-7732-9182-68ad075f3276/ja-JP/CONTENT.md -output_hash=sha256:20a495929da4ecfa28779b788ae2fc802eab1179209238835edb7e793769ebf9 -translation_projection_spans=921 -``` - -All chunks for the completed job were purged after commit. The final -`ja-JP/CONTENT.md` and `TRANSLATION_MAP.json` were present under the live -memory variant, and no raw retry diagnostics or invalid model output were -persisted in the committed translated variant. - -## Diagnosis - -The validator is strict in the right direction: it must reject translated output -that changes inline code, code fences, tables, links, destinations, HTML, math, -footnotes, or Markdown block structure. - -The insufficient implementation is the validation feedback loop: - -1. Validation errors collapse to coarse messages such as - `Codex output changed block structure`. -2. Persisted chunk/job errors do not identify the failing chunk entry, expected - fingerprint kind/value, actual fingerprint kind/value, segment id, block id, - or safe protected-span summary. -3. Retry attempts rebuild the same prompt instead of including the previous - structured validation failure summary. -4. Tests cover rejection but do not prove that retry prompts receive actionable - validator feedback. - -## Affected Scope - -Primary implementation files: - -- `src/server/translation/errors.ts` - - Add a typed validation diagnostic error shape. - - Keep ordinary `TranslationOutputValidationError` construction ergonomic for - existing tests. -- `src/server/translation/structure-fingerprint.ts` - - Include safe expected/actual fingerprint diagnostics when structure - comparison fails. - - Do not include raw source chunks or raw model output. -- `src/server/translation/prompt.ts` - - Carry validation diagnostics from segment validation and structure - fingerprinting. - - Add retry-prompt support that accepts the previous failure summary and the - expected segment ids. -- `src/server/translation/runner.ts` - - Preserve the latest failed attempt's safe diagnostic. - - Pass that diagnostic into the next fresh chunk retry prompt. - - Persist the same safe diagnostic in chunk/job error JSON. -- `src/server/translation/types.ts` - - Add optional diagnostic metadata to `TranslationJobSnapshotError` and - `TranslationPersistedError`. - - Keep the current `code`, `message`, and `action` fields stable for UI/API - consumers. -- `src/server/db/repositories.ts` - - Accept and parse the optional persisted diagnostic field. - - No migration is needed because the existing `error` columns store JSON text. - -Primary tests: - -- `tests/server/translation/prompt.test.ts` -- `tests/server/translation/structure-fingerprint.test.ts` -- `tests/server/translation/runner.test.ts` -- `tests/server/translation/translation-repositories.test.ts` - -Optional documentation updates if implementation changes contracts: - -- `docs/workflows/task-19-codex-translation/09-chunk-validation-and-retry-logic.md` -- `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` -- `docs/workflows/task-19-codex-translation/15-error-handling-and-cancellation.md` - -## Out Of Scope - -- Relaxing validation so changed Markdown structure, inline code, code fences, - destinations, HTML, math, tables, or footnotes can pass. -- Replacing Task 19U's segment-only output schema with full Markdown output. -- Redesigning chunking, stitching, translated reader routing, Flashbacks, - Moments, backup, auth, model controls, or Codex app-server transport. -- Adding a new database table or migration for attempt logs. -- Persisting raw Codex prompts, raw Codex responses, source chunks, translated - article bodies, app-server URLs, auth state, tokens, or credential paths. -- Committing the imported `Amp Owner's Manual` `CONTENT.md` or raw failed model - output as a fixture. Use synthetic fixtures that reproduce the structural - pattern instead. -- Browser UI redesign. Existing user-facing copy may remain coarse as long as - server-side diagnostics are actionable and safe. - -## Correction Strategy - -### 1. Keep strict validation, add structured diagnostics - -Validation should continue to throw `TranslationOutputValidationError`, but the -error should optionally carry a safe diagnostic: - -```ts -interface TranslationValidationDiagnostic { - kind: - | "markdown_structure" - | "protected_span" - | "segment_schema" - | "segment_length_ratio" - | "projection"; - message: string; - chunkIndex: number; - segmentId?: string; - blockId?: string; - sourceEntry?: { - kind: string; - valuePreview: string; - }; - translatedEntry?: { - kind: string; - valuePreview: string; - }; - protectedSpan?: { - kind: string; - valuePreview: string; - }; -} -``` - -`valuePreview` must be short, deterministic, and redacted enough for logs and -SQLite. It may include code-like atoms such as `` `AGENTS.md` `` or -`inline_code`, but it must not include whole source chunks or full translated -paragraphs. - -### 2. Persist diagnostics without breaking existing consumers - -Extend translation error JSON with optional diagnostics: - -```ts -interface TranslationPersistedError { - code: PersistableTranslationErrorCode; - message: string; - action?: TranslationErrorAction; - reason?: TranslationUnavailableReason | string; - diagnostics?: TranslationValidationDiagnostic[]; -} -``` - -Existing UI/API consumers should keep using `code`, `message`, and `action`. -The diagnostic field is for server-side investigation, tests, and retry prompt -construction. - -### 3. Feed previous validation failure into retry prompts - -Each chunk attempt still starts a fresh ephemeral Codex thread. For attempts -after the initial attempt, `buildTranslationPrompt()` should receive a compact -retry context: - -```ts -interface TranslationRetryContext { - attempt: number; - previousError: TranslationJobSnapshotError; -} -``` - -The prompt should add a short retry section before the source chunk: - -```text -Retry correction: -The previous output was rejected by TRAUMA validation. -Do not add Markdown syntax inside translated_text unless it exists in the source segment. -Preserve the expected segment ids and translate only prose. -Validation diagnostics: -... -``` - -The retry section must include only structured, safe diagnostics and expected -segment ids. It must not include raw invalid model output. - -### 4. Add targeted synthetic regression fixtures - -Create compact fixtures in tests, not durable article fixtures, covering the -Amp failure shape: - -- A table/list chunk with many inline code spans such as `` `AGENTS.md` ``, - `$HOME`, and shell commands. -- A skill/MCP chunk with code fences, JSON, YAML frontmatter examples, and - inline code references. -- A model output that is schema-valid but introduces backticks or structural - Markdown in a translated segment. - -Tests should prove: - -- The validator rejects the mutated output. -- The thrown/persisted error contains a diagnostic. -- The retry prompt includes that diagnostic. -- The retry prompt does not include raw invalid translated output. -- A corrected retry can pass and commit. - -## Implementation Tasks - -### Task 1: Diagnostic Error Model - -**Files:** - -- Modify: `src/server/translation/errors.ts` -- Modify: `src/server/translation/types.ts` -- Test: `tests/server/translation/prompt.test.ts` - -- [x] Add `TranslationValidationDiagnostic` and optional `diagnostics` fields - to translation error types. -- [x] Extend `TranslationOutputValidationError` so callers can pass - `{ diagnostics }`, while existing `new TranslationOutputValidationError(message)` - call sites continue to work. -- [x] Add a focused test that catches a validation error and asserts the - diagnostic array is present for a Markdown structure failure. -- [x] Run: - -```bash -bun run test tests/server/translation/prompt.test.ts -``` - -Expected: the new test fails before implementation and passes after. - -### Task 2: Structure Fingerprint Diagnostics - -**Files:** - -- Modify: `src/server/translation/structure-fingerprint.ts` -- Test: `tests/server/translation/structure-fingerprint.test.ts` - -- [x] When fingerprint entry counts differ, include the mismatch index and - expected/actual entry presence. -- [x] When entry kinds differ, include expected and actual kind previews. -- [x] When entry values differ, include expected and actual value previews. -- [x] Add tests for inline-code mutation and block-structure mutation. -- [x] Run: - -```bash -bun run test tests/server/translation/structure-fingerprint.test.ts -``` - -Expected: diagnostics identify the failure without exposing full Markdown -documents. - -### Task 3: Prompt Retry Context - -**Files:** - -- Modify: `src/server/translation/prompt.ts` -- Test: `tests/server/translation/prompt.test.ts` - -- [x] Add an optional retry context parameter to `buildTranslationPrompt()`. -- [x] Render a compact retry-correction section only when retry context is - supplied. -- [x] Include diagnostic kind, message, chunk index, segment id, block id, and - short expected/actual previews when present. -- [x] Include expected segment ids in the retry section. -- [x] Add tests proving initial prompts are unchanged and retry prompts contain - diagnostics but not raw failed translated output. -- [x] Run: - -```bash -bun run test tests/server/translation/prompt.test.ts -``` - -Expected: initial prompt tests still pass; retry prompt tests pass. - -### Task 4: Runner Retry Feedback Loop - -**Files:** - -- Modify: `src/server/translation/runner.ts` -- Test: `tests/server/translation/runner.test.ts` - -- [x] Track the latest persisted error from a failed attempt inside - `translateAndPersistChunk()`. -- [x] Pass retry context to `buildTranslationPrompt()` when `attempt > 0`. -- [x] Preserve diagnostics in `toPersistedError()` and `toPersistableError()`. -- [x] Add a fake translation client that fails validation on the first attempt, - records prompts, then returns corrected output. -- [x] Assert the second prompt contains the validation diagnostic and the job - completes. -- [x] Assert cancellation still prevents retry after a failed chunk. -- [x] Run: - -```bash -bun run test tests/server/translation/runner.test.ts -``` - -Expected: retry uses diagnostic context and existing cancellation behavior -remains intact. - -### Task 5: Persisted Error Parsing - -**Files:** - -- Modify: `src/server/db/repositories.ts` -- Test: `tests/server/translation/translation-repositories.test.ts` - -- [x] Allow optional `diagnostics` in `TranslationPersistedError` parsing. -- [x] Reject malformed diagnostics that are not arrays of safe objects. -- [x] Add a repository round-trip test for a chunk error with diagnostics. -- [x] Run: - -```bash -bun run test tests/server/translation/translation-repositories.test.ts -``` - -Expected: persisted diagnostics round-trip without changing legacy error -records. - -### Task 6: Contract Documentation Cleanup - -**Files:** - -- Modify when needed: - - `docs/workflows/task-19-codex-translation/09-chunk-validation-and-retry-logic.md` - - `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` - - `docs/workflows/task-19-codex-translation/15-error-handling-and-cancellation.md` - -- [x] Document that validation diagnostics are safe metadata, not raw model - output. -- [x] Document that retry prompts include previous validation failure summaries. -- [x] Document that no new database table or raw attempt log is introduced. -- [x] Run: - -```bash -git diff --check -``` - -Expected: no whitespace errors. - -## Verification - -Focused verification: - -```bash -bun run test tests/server/translation/prompt.test.ts -bun run test tests/server/translation/structure-fingerprint.test.ts -bun run test tests/server/translation/runner.test.ts -bun run test tests/server/translation/translation-repositories.test.ts -``` - -Broader verification: - -```bash -bun run typecheck -bun run test -bun run verify -git diff --check -``` - -Current broader verification note: focused translation tests, `bun run -typecheck`, `bun run build`, and `git diff --check` passed in this branch. When -run outside the sandbox, full `bun run test` reached 102 passing test files and -failed only `tests/scripts/runtime-command.test.ts` because the current worktree -contains an unrelated `package.json` host script drift from the expected runtime -command contract. `bun run verify` stops at the same test failure. Do not treat -this workflow as ready for handoff until the runtime host contract is restored -or explicitly accepted out of scope. - -Live verification when Codex app-server and ChatGPT auth are available: - -1. Start Codex app-server: - -```bash -codex app-server --listen unix:///tmp/trauma-codex.sock -``` - -2. Start TRAUMA with the app-server endpoint: - -```bash -TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock bun run dev -``` - -3. Retry `Amp Owner's Manual` translation or a synthetic imported memory with - the same code-heavy structure. -4. Confirm new failed attempts, if any, persist diagnostics in - `translation_chunks.error`. -5. Confirm successful retries persist only final translated `CONTENT.md` plus - normal translation metadata, not raw invalid model output. - -Live verification is a confidence check, not a substitute for deterministic -fixtures and unit/integration tests. - -## Acceptance Criteria - -- Schema-valid but semantically invalid output still fails closed. -- Validation failures include safe structured diagnostics. -- Retry prompts include the previous validation failure summary. -- Retry prompts do not include raw invalid model output. -- Existing user-facing error behavior remains compatible. -- Existing persisted error records remain readable. -- No new migration is required. -- No raw source chunks, prompts, model responses, app-server endpoints, auth - state, tokens, or completed translated article bodies are persisted. -- Focused translation tests, typecheck, full unit tests, and `bun run verify` - pass before handoff. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks.md deleted file mode 100644 index 1b799e86..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks.md +++ /dev/null @@ -1,131 +0,0 @@ -# Task 19W: Variant-Local Flashbacks Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Make Flashbacks work independently on source and translated reader variants while keeping `/flashbacks`, memory browse, and search as one unified Flashback surface. - -**Architecture:** Keep a single `flashbacks` table and add variant identity to each row. A source Flashback stores source reader offsets; a translated Flashback stores translated reader offsets scoped to the current translation output hash. Reader routes render only the active variant's rows, while global browse routes union all renderable source and translated rows. - -**Tech Stack:** TypeScript, Bun, Vitest, Drizzle SQLite migrations, SolidStart API routes, existing reader selection logic, existing Flashback range merge/split logic, existing translation current-state resolver. - ---- - -## Status - -Task 19W supersedes the Flashback portion of Task 19V. - -Task 19V tried to keep Flashbacks source-canonical and project translated selections through `translation_projection_spans`. That design is too coarse for arbitrary phrase selection across languages and makes normal translated Flashback creation fail. Task 19W removes Flashback projection from the write path and treats Flashbacks as local to the reader variant where they were created. - -Moment projection is not part of this workflow. Do not change Moment behavior unless a later workflow explicitly scopes that work. - -## Execution Model - -This workflow is intentionally decomposed. Do not assign all schema, repository, reader, browse, backup, and docs changes to one worker. - -Each subtask owns one domain: - -- A bounded file surface. -- Focused red tests before implementation. -- A local verification command. -- A handoff contract for the next worker. - -The parent worker reviews each subtask handoff before starting dependent subtasks. - -## Product Contract - -- A Flashback created on `/memories/:id` belongs to the source variant. -- A Flashback created on `/memories/:langCode/:id` belongs to that translated variant. -- Source and translated Flashbacks for the same memory are independent rows and independent ranges. -- Translated Flashback rows are scoped to the translation `outputHash` that produced the visible translated `CONTENT.md`. -- Re-translation changes `outputHash`; translated Flashbacks from older output hashes are hidden from reader and browse surfaces. -- `/flashbacks`, memory browse, search, and right-rail "All" views include renderable Flashbacks from both source and translated variants. -- Source reader routes render source Flashbacks only. -- Translated reader routes render translated Flashbacks for the current `langCode + outputHash` only. -- Translated Flashback writes must not mutate source Flashback rows. -- Flashback validation remains structural: resolve the selected text against the active variant's Markdown, preserve range merge/split behavior, reject stale content hashes, and do not guess a placement. - -## Data Model Decision - -Use one variant-aware table instead of introducing `translated_flashbacks`. - -This preserves the product model that Flashbacks are one concept, one global list, one search dimension, and one deletion interaction. The variant columns determine where a row is valid: - -```ts -export type FlashbackVariant = - | { kind: "source" } - | { - kind: "translation"; - langCode: SupportedLanguageCode; - outputHash: string; - }; -``` - -Database columns: - -```sql -variant_kind text not null default 'source' -lang_code text -translation_output_hash text -``` - -Rules: - -- `variant_kind = 'source'` requires `lang_code is null` and `translation_output_hash is null`. -- `variant_kind = 'translation'` requires a supported `lang_code` and a `translation_output_hash` matching `sha256:*`. -- `content_hash` still hashes the active variant's reader text. - -## Subtask Index - -1. [Contract and schema migration](task-19-codex-translation-variant-local-flashbacks/01-contract-and-schema-migration.md) -2. [Repository and variant domain](task-19-codex-translation-variant-local-flashbacks/02-repository-and-variant-domain.md) -3. [Toggle service and API route](task-19-codex-translation-variant-local-flashbacks/03-toggle-service-and-api-route.md) -4. [Reader rendering and current-variant state](task-19-codex-translation-variant-local-flashbacks/04-reader-rendering-and-current-variant-state.md) -5. [Browse, Flashbacks route, deletion, and export](task-19-codex-translation-variant-local-flashbacks/05-browse-route-delete-and-export.md) -6. [Docs cleanup and verification](task-19-codex-translation-variant-local-flashbacks/06-docs-cleanup-and-verification.md) - -## Dependency Graph - -```mermaid -flowchart TD - A["01 contract and schema migration"] --> B["02 repository and variant domain"] - B --> C["03 toggle service and API route"] - B --> D["04 reader rendering and current-variant state"] - C --> D - D --> E["05 browse route delete and export"] - E --> F["06 docs cleanup and verification"] -``` - -## Parallelization Rules - -- `01` must run first. -- `02` depends on `01`. -- `03` and `04` both depend on `02`; `04` must be finalized after `03` because reader state consumes API response shape. -- `05` depends on `03` and `04`. -- `06` runs last. - -## Cross-Cutting Constraints - -- Do not create a second Flashback table. -- Do not project translated Flashback selections back to source. -- Do not render source Flashbacks on translated routes. -- Do not render translated Flashbacks on source routes. -- Do not render translated Flashbacks when `translation_output_hash` differs from the current translation `outputHash`. -- Do not mutate `CONTENT.md` for normal Flashback persistence. -- Do not remove existing range merge/split behavior. -- Do not change Moment behavior in this workflow. -- Do not remove `translation_projection_spans`; it remains translation runtime data outside Flashback writes. -- Keep SQLite as runtime source of truth and JSON files as backup/export artifacts. - -## Final Acceptance Criteria - -- Existing source Flashbacks migrate to `variant_kind = 'source'`. -- Translated reader selection creates a translated Flashback row without requiring projection spans. -- Source and translated Flashbacks for the same memory can coexist without replacing each other. -- Source reader renders only source rows. -- Translated reader renders only rows for the active `langCode + outputHash`. -- `/flashbacks` includes renderable source and translated rows. -- Global Flashback links navigate to the correct source or translated reader route. -- Deleting a translated Flashback removes only the translated row. -- Translated Flashback backup/export writes `memories///FLASHBACKS.json`. -- Architecture and Task 19 docs no longer claim Flashbacks are source-canonical across translated readers. -- Focused tests, typecheck, `git diff --check`, and full verification pass or document a confirmed unrelated existing flake. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/01-contract-and-schema-migration.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/01-contract-and-schema-migration.md deleted file mode 100644 index f6435032..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/01-contract-and-schema-migration.md +++ /dev/null @@ -1,219 +0,0 @@ -# Task 19W.01: Contract And Schema Migration Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add variant identity to `flashbacks` while migrating existing rows to the source variant. - -**Architecture:** Rebuild the existing `flashbacks` table with three variant columns and a cross-column check. Existing data copies into `variant_kind = 'source'`. Drizzle schema, bundled migrations, and schema tests stay in lockstep. - -**Tech Stack:** Drizzle SQLite schema, Bun SQLite runtime migrations, Vitest schema/repository tests. - ---- - -## Role - -Schema owner. - -This worker must not change API routes, reader rendering, or Flashback toggle logic. - -## Files - -- Create: `drizzle/0013_variant_local_flashbacks.sql` -- Create: `drizzle/meta/0013_snapshot.json` -- Modify: `drizzle/meta/_journal.json` -- Modify: `src/server/db/schema.ts` -- Modify: `src/server/db/bundled-migrations.ts` -- Test: `tests/server/db/schema.test.ts` -- Test: `tests/server/flashbacks/repository.test.ts` - -## Schema Contract - -Add: - -```ts -variantKind: text("variant_kind") - .$type<"source" | "translation">() - .notNull() - .default("source"), -langCode: text("lang_code").$type(), -translationOutputHash: text("translation_output_hash"), -``` - -Add checks: - -```ts -check( - "flashbacks_variant_kind_check", - sql`${table.variantKind} in ('source', 'translation')`, -), -check( - "flashbacks_variant_scope_check", - sql`(${table.variantKind} = 'source' and ${table.langCode} is null and ${table.translationOutputHash} is null) or (${table.variantKind} = 'translation' and ${table.langCode} in (${supportedLanguageSqlList}) and ${table.translationOutputHash} glob 'sha256:*')`, -), -``` - -Add index: - -```ts -index("flashbacks_memory_variant_idx").on( - table.memoryId, - table.variantKind, - table.langCode, - table.translationOutputHash, - table.startOffset, -), -``` - -## Migration SQL - -Create `drizzle/0013_variant_local_flashbacks.sql` in the current migration style: - -```sql -PRAGMA foreign_keys=OFF; ---> statement-breakpoint -CREATE TABLE `__new_flashbacks` ( - `id` text PRIMARY KEY NOT NULL, - `memory_id` text NOT NULL, - `variant_kind` text DEFAULT 'source' NOT NULL, - `lang_code` text, - `translation_output_hash` text, - `text` text NOT NULL, - `prefix` text NOT NULL, - `suffix` text NOT NULL, - `start_offset` integer NOT NULL, - `end_offset` integer NOT NULL, - `content_hash` text, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`memory_id`) REFERENCES `memories`(`id`) ON UPDATE no action ON DELETE cascade, - CONSTRAINT "flashbacks_variant_kind_check" CHECK(`variant_kind` in ('source', 'translation')), - CONSTRAINT "flashbacks_variant_scope_check" CHECK((`variant_kind` = 'source' and `lang_code` is null and `translation_output_hash` is null) or (`variant_kind` = 'translation' and `lang_code` in ('ja-JP') and `translation_output_hash` glob 'sha256:*')), - CONSTRAINT "flashbacks_start_offset_check" CHECK(`start_offset` >= 0), - CONSTRAINT "flashbacks_end_offset_check" CHECK(`end_offset` > `start_offset`) -); ---> statement-breakpoint -INSERT INTO `__new_flashbacks` ( - `id`, - `memory_id`, - `variant_kind`, - `lang_code`, - `translation_output_hash`, - `text`, - `prefix`, - `suffix`, - `start_offset`, - `end_offset`, - `content_hash`, - `created_at`, - `updated_at` -) -SELECT - `id`, - `memory_id`, - 'source', - NULL, - NULL, - `text`, - `prefix`, - `suffix`, - `start_offset`, - `end_offset`, - `content_hash`, - `created_at`, - `updated_at` -FROM `flashbacks`; ---> statement-breakpoint -DROP TABLE `flashbacks`; ---> statement-breakpoint -ALTER TABLE `__new_flashbacks` RENAME TO `flashbacks`; ---> statement-breakpoint -CREATE INDEX `flashbacks_memory_id_idx` ON `flashbacks` (`memory_id`); ---> statement-breakpoint -CREATE INDEX `flashbacks_created_at_idx` ON `flashbacks` (`created_at`); ---> statement-breakpoint -CREATE INDEX `flashbacks_memory_variant_idx` ON `flashbacks` (`memory_id`,`variant_kind`,`lang_code`,`translation_output_hash`,`start_offset`); ---> statement-breakpoint -PRAGMA foreign_keys=ON; -``` - -If supported languages expand before this task runs, replace the hard-coded SQL list with the generated list from current schema output. - -## Task Steps - -- [ ] **Step 1: Write migration compatibility test** - -Add a test in `tests/server/db/schema.test.ts` that creates a database through migrations before `0013`, inserts one legacy Flashback, applies all bundled migrations, and asserts: - -```ts -expect(row).toMatchObject({ - id: "existing-flashback", - variantKind: "source", - langCode: null, - translationOutputHash: null, -}); -``` - -- [ ] **Step 2: Write constraint tests** - -Add assertions that SQLite rejects: - -```sql -insert into flashbacks (..., variant_kind, lang_code, translation_output_hash, ...) -values (..., 'source', 'ja-JP', 'sha256:abc', ...); -``` - -and rejects: - -```sql -insert into flashbacks (..., variant_kind, lang_code, translation_output_hash, ...) -values (..., 'translation', null, null, ...); -``` - -Expected errors contain `flashbacks_variant_scope_check`. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/db/schema.test.ts tests/server/flashbacks/repository.test.ts -``` - -Expected: FAIL because the current schema has no variant columns. - -- [ ] **Step 4: Add schema fields and migration** - -Update `src/server/db/schema.ts`, add `drizzle/0013_variant_local_flashbacks.sql`, generate or hand-maintain `drizzle/meta/0013_snapshot.json`, and append a journal entry in `drizzle/meta/_journal.json`. - -- [ ] **Step 5: Bundle the migration** - -Update `src/server/db/bundled-migrations.ts`: - -```ts -import migration0013Sql from "../../../drizzle/0013_variant_local_flashbacks.sql?raw"; -``` - -Append: - -```ts -{ - sql: migration0013Sql, - folderMillis: 1779449000000, - bps: true, -}, -``` - -- [ ] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/db/schema.test.ts tests/server/flashbacks/repository.test.ts -git diff --check -``` - -Expected: all commands pass. - -## Handoff - -The database exposes variant-aware Flashback columns. Existing data is source-scoped. No runtime code has started reading or writing translated Flashbacks yet. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/02-repository-and-variant-domain.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/02-repository-and-variant-domain.md deleted file mode 100644 index 40d3fb83..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/02-repository-and-variant-domain.md +++ /dev/null @@ -1,187 +0,0 @@ -# Task 19W.02: Repository And Variant Domain Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a small Flashback variant domain and repository methods that can list and replace only one variant without deleting rows from another variant. - -**Architecture:** Keep legacy source helpers as wrappers, then add variant-aware methods used by translated flows. Repository replacement is scoped by `(memory_id, variant_kind, lang_code, translation_output_hash)`. - -**Tech Stack:** TypeScript, Drizzle repositories, Vitest repository tests. - ---- - -## Role - -Repository owner. - -This worker must not change Solid components or route rendering. - -## Files - -- Create: `src/server/flashbacks/variant.ts` -- Modify: `src/server/db/repositories.ts` -- Modify: `src/server/flashbacks/index.ts` -- Test: `tests/server/flashbacks/repository.test.ts` -- Test: `tests/server/db/repositories.test.ts` - -## Domain Type - -Create `src/server/flashbacks/variant.ts`: - -```ts -import type { SupportedLanguageCode } from "../translation/languages"; - -export type FlashbackVariant = - | { kind: "source" } - | { - kind: "translation"; - langCode: SupportedLanguageCode; - outputHash: string; - }; - -export interface FlashbackVariantColumns { - variantKind: "source" | "translation"; - langCode: SupportedLanguageCode | null; - translationOutputHash: string | null; -} - -export const sourceFlashbackVariant: FlashbackVariant = { kind: "source" }; - -export function toFlashbackVariantColumns( - variant: FlashbackVariant, -): FlashbackVariantColumns { - if (variant.kind === "source") { - return { - variantKind: "source", - langCode: null, - translationOutputHash: null, - }; - } - - return { - variantKind: "translation", - langCode: variant.langCode, - translationOutputHash: variant.outputHash, - }; -} -``` - -## Repository Contract - -Extend `FlashbackRepository`: - -```ts -listForMemoryVariant: ( - input: { memoryId: string; variant: FlashbackVariant }, -) => Promise; -replaceForMemoryVariant: ( - input: { - memoryId: string; - variant: FlashbackVariant; - flashbacks: Flashback[]; - }, -) => Promise; -``` - -Keep existing wrappers: - -```ts -listForMemory: (memoryId) => - repositories.flashbacks.listForMemoryVariant({ - memoryId, - variant: sourceFlashbackVariant, - }), -replaceForMemory: (memoryId, flashbacks) => - repositories.flashbacks.replaceForMemoryVariant({ - memoryId, - variant: sourceFlashbackVariant, - flashbacks, - }), -``` - -`replaceForMemoryVariant` must delete only rows matching the exact variant columns: - -```ts -and( - eq(schema.flashbacks.memoryId, memoryId), - eq(schema.flashbacks.variantKind, columns.variantKind), - columns.langCode === null - ? isNull(schema.flashbacks.langCode) - : eq(schema.flashbacks.langCode, columns.langCode), - columns.translationOutputHash === null - ? isNull(schema.flashbacks.translationOutputHash) - : eq(schema.flashbacks.translationOutputHash, columns.translationOutputHash), -) -``` - -Import `isNull` from `drizzle-orm`. - -## Task Steps - -- [ ] **Step 1: Write variant replacement test** - -In `tests/server/flashbacks/repository.test.ts`, seed one source row and one translated row for the same memory. Call `replaceForMemoryVariant({ variant: { kind: "source" } })`. Assert the translated row remains. - -Expected final row ids: - -```ts -expect(rows.map((row) => row.id)).toEqual([ - "source-new", - "translated-existing", -]); -``` - -- [ ] **Step 2: Write translated replacement test** - -Seed two translated rows for the same memory: - -```ts -{ langCode: "ja-JP", translationOutputHash: "sha256:" + "a".repeat(64) } -{ langCode: "ja-JP", translationOutputHash: "sha256:" + "b".repeat(64) } -``` - -Replace only the first output hash and assert the second output hash row remains. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/flashbacks/repository.test.ts tests/server/db/repositories.test.ts -``` - -Expected: FAIL because `listForMemoryVariant` and `replaceForMemoryVariant` do not exist. - -- [ ] **Step 4: Add variant domain file** - -Create `src/server/flashbacks/variant.ts` with the type and conversion helpers above. Export it through `src/server/flashbacks/index.ts`. - -- [ ] **Step 5: Implement repository methods** - -Update `src/server/db/repositories.ts` to: - -- include `variantKind`, `langCode`, and `translationOutputHash` in `FlashbackBrowseRow`, -- add variant-aware list and replace methods, -- keep legacy wrappers source-scoped, -- validate every replacement row has the same memory and same variant columns as the requested variant. - -Validation failure message: - -```ts -"Cannot replace flashbacks for one memory variant with rows from another memory variant." -``` - -- [ ] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/flashbacks/repository.test.ts tests/server/db/repositories.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -The repository can safely replace one Flashback variant without deleting source rows, translated rows, or rows tied to a different translation output hash. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/03-toggle-service-and-api-route.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/03-toggle-service-and-api-route.md deleted file mode 100644 index 53dde312..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/03-toggle-service-and-api-route.md +++ /dev/null @@ -1,265 +0,0 @@ -# Task 19W.03: Toggle Service And API Route Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Let translated reader selections create and remove translated Flashback rows directly, without source projection. - -**Architecture:** The API already receives optional `langCode`. Source requests use source `CONTENT.md`; translated requests resolve the current translation, read translated `CONTENT.md`, scope rows by `langCode + outputHash`, and call the same range merge/split logic against translated reader text. - -**Tech Stack:** SolidStart API route, existing `toggleMemoryFlashback`, translation current-state resolver, Vitest route/service tests. - ---- - -## Role - -Mutation owner. - -This worker must not change browse UI or route list rendering. - -## Files - -- Modify: `src/routes/api/flashbacks.ts` -- Modify: `src/server/flashbacks/toggle.ts` -- Modify: `src/server/flashbacks/export.ts` -- Modify: `src/server/flashbacks/variant.ts` -- Test: `tests/server/routes/api-flashbacks-toggle.test.ts` -- Test: `tests/server/flashbacks/toggle.test.ts` - -## Service Contract - -Extend `ToggleMemoryFlashbackInput`: - -```ts -variant?: FlashbackVariant; -content?: { - markdown: string; - relativePath: string; -}; -``` - -Default is source: - -```ts -const variant = input.variant ?? sourceFlashbackVariant; -``` - -Use: - -```ts -const content = input.content ?? await readMemoryContent({ - config: { storePath: input.config.storePath }, - memoryId: input.memoryId, -}); -``` - -Replace repository calls: - -```ts -const existingFlashbacks = - await repositories.flashbacks.listForMemoryVariant({ - memoryId: input.memoryId, - variant, - }); -await repositories.flashbacks.replaceForMemoryVariant({ - memoryId: input.memoryId, - variant, - flashbacks: nextFlashbacks, -}); -``` - -Every row built by `buildFlashbackRows` must include: - -```ts -...toFlashbackVariantColumns(variant) -``` - -## API Contract - -`POST /api/flashbacks` keeps the current payload shape: - -```json -{ - "memoryId": "019e...", - "langCode": "ja-JP", - "operation": "flashback", - "selection": { - "text": "ジャン・ボードリヤール", - "prefix": "あるいは、", - "suffix": "が言ったように:", - "startOffset": 5, - "endOffset": 17 - } -} -``` - -Route behavior: - -- no `langCode`: source variant, -- with `langCode`: resolve current translation, -- current translation missing/unavailable: HTTP 409 with `code: "translation_unavailable"`, -- current translation found: read translated `CONTENT.md`, pass translated Markdown and variant to `toggleMemoryFlashback`. - -Remove these Flashback-specific projection imports from `src/routes/api/flashbacks.ts`: - -```ts -projectFlashbacksToTranslatedReader -projectTranslatedSelectionToSourceReader -``` - -## Task Steps - -- [ ] **Step 1: Replace projection test with variant-local test** - -In `tests/server/routes/api-flashbacks-toggle.test.ts`, replace the current test named `"projects translated reader flashback selections back to source before saving"` with: - -```ts -it("stores translated reader flashback selections as translated variant rows", async () => { - const root = await makeRoot(); - const configPath = await writeConfig(root, { backupEnabled: false }); - process.env.TRAUMA_CONFIG_PATH = configPath; - const config = loadTraumaConfig({ configPath }); - const sourceMarkdown = "Or as Jean Baudrillard has said:"; - const translatedMarkdown = "あるいは、ジャン・ボードリヤールが言ったように:"; - await seedTranslatedFlashbackFixture({ - config, - sourceMarkdown, - translatedMarkdown, - }); - - const selected = "ジャン・ボードリヤール"; - const startOffset = translatedMarkdown.indexOf(selected); - const response = await POST(createApiEvent(new Request("http://localhost/api/flashbacks", { - method: "POST", - body: JSON.stringify({ - memoryId, - langCode: "ja-JP", - operation: "flashback", - selection: { - text: selected, - prefix: translatedMarkdown.slice(0, startOffset), - suffix: translatedMarkdown.slice(startOffset + selected.length), - startOffset, - endOffset: startOffset + selected.length, - }, - }), - }))); - const body = await response.json(); - - expect(response.status).toBe(200); - expect(body.result.flashbacks).toEqual([ - expect.objectContaining({ - text: selected, - startOffset, - endOffset: startOffset + selected.length, - }), - ]); - - const connection = initializeDatabase(config); - try { - expect(await connection.repositories.flashbacks.listForMemory(memoryId)).toEqual([]); - const rows = connection.sqlite - .prepare( - "select text, variant_kind as variantKind, lang_code as langCode, translation_output_hash as translationOutputHash from flashbacks where memory_id = ? order by start_offset", - ) - .all(memoryId); - expect(rows).toEqual([ - expect.objectContaining({ - text: selected, - variantKind: "translation", - langCode: "ja-JP", - translationOutputHash: expect.stringMatching(/^sha256:[0-9a-f]{64}$/), - }), - ]); - } finally { - connection.close(); - } -}); -``` - -- [ ] **Step 2: Write direct toggle service test** - -In `tests/server/flashbacks/toggle.test.ts`, call `toggleMemoryFlashback` with: - -```ts -variant: { - kind: "translation", - langCode: "ja-JP", - outputHash: "sha256:" + "a".repeat(64), -}, -content: { - markdown: "翻訳された本文です。", - relativePath: `memories/${memoryId}/ja-JP/CONTENT.md`, -}, -``` - -Assert source rows are untouched and translated rows are merged/split exactly like source rows. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/routes/api-flashbacks-toggle.test.ts tests/server/flashbacks/toggle.test.ts -``` - -Expected: FAIL because translated requests still project to source and the toggle service has no variant input. - -- [ ] **Step 4: Implement translated content resolution in the route** - -Add a helper in `src/routes/api/flashbacks.ts`: - -```ts -async function resolveTranslatedFlashbackVariant(input: { - config: ReturnType; - connection: ReturnType; - langCode: SupportedLanguageCode; - memoryId: string; -}) { - const current = await resolveCurrentTranslationReadOnly({ - config: input.config, - langCode: input.langCode, - memoryId: input.memoryId, - repository: input.connection.repositories.translations, - }); - if (current.status !== "current") { - throw new FlashbackToggleError( - "Translated flashback selection is unavailable.", - "stale_selection", - ); - } - const content = await readResolvedMemoryContent( - resolveTranslatedMemoryContentPath({ - config: input.config, - langCode: input.langCode, - memoryId: input.memoryId, - }), - ); - return { - content, - variant: { - kind: "translation" as const, - langCode: input.langCode, - outputHash: current.outputHash, - }, - }; -} -``` - -- [ ] **Step 5: Implement variant-aware toggle** - -Update `toggleMemoryFlashback` and `buildFlashbackRows` with the contract above. Include `variantKind`, `langCode`, and `translationOutputHash` in returned Flashback items so frontend and delete actions can preserve variant identity. - -- [ ] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/routes/api-flashbacks-toggle.test.ts tests/server/flashbacks/toggle.test.ts tests/server/flashbacks/flashback-markers.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Translated Flashback API writes no longer depend on projection spans. Source and translated rows can be created independently through the same endpoint. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/04-reader-rendering-and-current-variant-state.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/04-reader-rendering-and-current-variant-state.md deleted file mode 100644 index 75ac6b60..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/04-reader-rendering-and-current-variant-state.md +++ /dev/null @@ -1,141 +0,0 @@ -# Task 19W.04: Reader Rendering And Current-Variant State Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Render only the active reader variant's Flashbacks and keep the right-rail "Current" tab scoped to that variant. - -**Architecture:** Reader load resolves content first, then asks the repository for Flashbacks matching the active variant. Source routes use `{ kind: "source" }`; translated routes use `{ kind: "translation", langCode, outputHash }`. The existing marker renderer applies rows to the active Markdown only. - -**Tech Stack:** Reader server data loader, existing markdown renderer, Solid reader component, Vitest reader tests. - ---- - -## Role - -Reader owner. - -This worker must not change global `/flashbacks` browse behavior. - -## Files - -- Modify: `src/server/reader/page-data.ts` -- Modify: `src/components/reader/MemoryReader.tsx` -- Modify: `src/components/reader/reader-memory-loader.ts` -- Test: `tests/server/reader/page-data.test.ts` -- Test: `tests/components/memory-reader-actions.test.ts` -- Test: `tests/components/memory-reader-flashback-selection.test.ts` - -## Reader Contract - -`ReaderFlashbackItem` should carry variant identity: - -```ts -export interface ReaderFlashbackItem { - id: string; - text: string; - prefix: string; - suffix: string; - startOffset: number; - endOffset: number; - contentHash?: string | null; - variantKind: "source" | "translation"; - langCode?: SupportedLanguageCode | null; - translationOutputHash?: string | null; - createdAt: string; -} -``` - -`loadReaderMemory` should compute: - -```ts -const activeVariant = content.langCode === undefined - ? sourceFlashbackVariant - : { - kind: "translation" as const, - langCode: content.langCode, - outputHash: content.outputHash, - }; -``` - -Then: - -```ts -const flashbackMarkers = - await connection.repositories.flashbacks.listForMemoryVariant({ - memoryId, - variant: activeVariant, - }); -``` - -Remove Flashback rendering through `projectFlashbacksToTranslatedReader`. - -## Task Steps - -- [ ] **Step 1: Write reader isolation test** - -In `tests/server/reader/page-data.test.ts`, seed: - -- source `CONTENT.md`: `Source Jean Baudrillard sentence.` -- translated `CONTENT.md`: `翻訳されたジャン・ボードリヤールの文。` -- one source Flashback on `Source Jean Baudrillard`, -- one translated Flashback on `ジャン・ボードリヤール` with current output hash. - -Assert: - -```ts -const source = await loadReaderMemory(memoryId, { config }); -expect(source.status).toBe("ready"); -expect(source.memory.flashbacks.map((row) => row.text)).toEqual([ - "Source Jean Baudrillard", -]); -expect(source.rendered.html).toContain("data-flashback-id=\"source-flashback\""); -expect(source.rendered.html).not.toContain("translated-flashback"); - -const translated = await loadReaderMemory(memoryId, { - config, - langCode: "ja-JP", -}); -expect(translated.status).toBe("ready"); -expect(translated.memory.flashbacks.map((row) => row.text)).toEqual([ - "ジャン・ボードリヤール", -]); -expect(translated.rendered.html).toContain("data-flashback-id=\"translated-flashback\""); -expect(translated.rendered.html).not.toContain("source-flashback"); -``` - -- [ ] **Step 2: Write stale translation hash test** - -Seed a translated Flashback with `translation_output_hash = "sha256:" + "b".repeat(64)` while the current translation output hash is `"sha256:" + "a".repeat(64)`. Assert translated reader renders no Flashback rows. - -- [ ] **Step 3: Verify RED** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/reader/page-data.test.ts tests/components/memory-reader-actions.test.ts tests/components/memory-reader-flashback-selection.test.ts -``` - -Expected: FAIL because translated reader currently uses projected source Flashbacks or source aggregate rows. - -- [ ] **Step 4: Load active variant rows** - -Update `src/server/reader/page-data.ts` to load active variant rows through the repository after content resolution. Remove the translated Flashback projection branch for normal reader rendering. - -- [ ] **Step 5: Return variant identity to the component** - -Map `variantKind`, `langCode`, and `translationOutputHash` into `ReaderFlashbackItem`. Keep `MemoryReader.tsx` state updates compatible with `payload.result.flashbacks`. - -- [ ] **Step 6: Verify this slice** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/reader/page-data.test.ts tests/components/memory-reader-actions.test.ts tests/components/memory-reader-flashback-selection.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Reader pages are variant-local. The active reader route no longer imports source Flashbacks into translated content through projection. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/05-browse-route-delete-and-export.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/05-browse-route-delete-and-export.md deleted file mode 100644 index 008be99a..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/05-browse-route-delete-and-export.md +++ /dev/null @@ -1,213 +0,0 @@ -# Task 19W.05: Browse, Flashbacks Route, Deletion, And Export Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Include translated Flashbacks in global list/search surfaces and make deletion/export variant-aware. - -**Architecture:** Global browse rows include variant identity. Renderability filtering reads the correct source or translated `CONTENT.md` for each row and hides stale translated output hashes. Links and deletion requests preserve `langCode` for translated rows. - -**Tech Stack:** Flashback browse loader, memory browse loader, Solid components, backup/export JSON, Vitest component/server tests. - ---- - -## Role - -Browse and backup/export owner. - -This worker must not change the database schema. - -## Files - -- Modify: `src/server/flashbacks/browse.ts` -- Modify: `src/server/flashbacks/export.ts` -- Modify: `src/server/flashbacks/toggle.ts` -- Modify: `src/server/memories/browse.ts` -- Modify: `src/components/memories/browse-data.ts` -- Modify: `src/components/memories/memory-anchor-hrefs.ts` -- Modify: `src/components/flashbacks/FlashbackActionMenu.tsx` -- Modify: `src/routes/flashbacks/index.tsx` -- Test: `tests/server/flashbacks/repository.test.ts` -- Test: `tests/server/memories/browse.test.ts` -- Test: `tests/components/flashbacks-loader.test.ts` -- Test: `tests/components/flashback-action-menu.test.ts` -- Test: `tests/memories/browse-data.test.ts` - -## Browse Row Contract - -Extend `FlashbackBrowseRow` and corresponding component data: - -```ts -variantKind: "source" | "translation"; -langCode: SupportedLanguageCode | null; -translationOutputHash: string | null; -``` - -Add link helper: - -```ts -export function buildMemoryVariantAnchorHref(input: { - anchorId?: null | string; - langCode?: null | string; - memoryId: string; -}): string { - const memoryPath = input.langCode === undefined || input.langCode === null - ? buildMemoryHref(input.memoryId) - : `/memories/${encodeURIComponent(input.langCode)}/${encodeURIComponent(input.memoryId)}`; - const anchorId = input.anchorId?.trim() ?? ""; - return anchorId.length === 0 - ? memoryPath - : `${memoryPath}${buildSameMemoryAnchorHref(anchorId)}`; -} -``` - -## Export Contract - -`getFlashbackMetadataExportPath` becomes variant-aware: - -```ts -export function getFlashbackMetadataExportPath(input: { - memoryId: string; - variant: FlashbackVariant; -}): string { - if (input.variant.kind === "source") { - return `memories/${input.memoryId}/FLASHBACKS.json`; - } - return `memories/${input.memoryId}/${input.variant.langCode}/FLASHBACKS.json`; -} -``` - -Keep a compatibility wrapper only where tests still need the old source path: - -```ts -export function getSourceFlashbackMetadataExportPath(memoryId: string): string { - return getFlashbackMetadataExportPath({ - memoryId, - variant: sourceFlashbackVariant, - }); -} -``` - -## Task Steps - -- [ ] **Step 1: Write global browse test** - -In `tests/server/flashbacks/repository.test.ts`, seed one source row and one translated row. Assert `listForBrowse()` returns both and includes variant identity. - -Expected shape: - -```ts -expect(rows).toEqual([ - expect.objectContaining({ - id: "translated-new", - variantKind: "translation", - langCode: "ja-JP", - translationOutputHash: "sha256:" + "a".repeat(64), - }), - expect.objectContaining({ - id: "source-old", - variantKind: "source", - langCode: null, - translationOutputHash: null, - }), -]); -``` - -- [ ] **Step 2: Write renderability test** - -In `tests/server/memories/browse.test.ts`, seed: - -- current translation output hash `sha256:a...`, -- translated Flashback with matching hash, -- translated Flashback with stale hash. - -Assert browse memory rows include the matching translated row and hide the stale row. - -- [ ] **Step 3: Write link and delete component tests** - -In `tests/memories/browse-data.test.ts`, assert translated Flashback links build: - -```ts -expect(buildMemoryVariantAnchorHref({ - memoryId: "memory-1", - langCode: "ja-JP", - anchorId: "flashback-1", -})).toBe("/memories/ja-JP/memory-1#flashback-1"); -``` - -In `tests/components/flashback-action-menu.test.ts`, assert delete sends: - -```json -{ - "memoryId": "memory-1", - "langCode": "ja-JP", - "operation": "unflashback" -} -``` - -for translated rows. - -- [ ] **Step 4: Verify RED** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/flashbacks/repository.test.ts tests/server/memories/browse.test.ts tests/components/flashbacks-loader.test.ts tests/components/flashback-action-menu.test.ts tests/memories/browse-data.test.ts -``` - -Expected: FAIL because browse rows, links, delete payloads, and export paths are still source-only. - -- [ ] **Step 5: Implement variant-aware browse filtering** - -Update `filterRenderableFlashbackRows` to group by: - -```ts -`${row.memoryId}:${row.variantKind}:${row.langCode ?? ""}:${row.translationOutputHash ?? ""}` -``` - -For source rows, read source `CONTENT.md`. - -For translation rows: - -1. Resolve current translation for `row.memoryId + row.langCode`. -2. Require `current.status === "current"`. -3. Require `current.outputHash === row.translationOutputHash`. -4. Read translated `CONTENT.md`. -5. Apply markers to translated Markdown. - -- [ ] **Step 6: Implement variant-aware links and deletion** - -Update `/flashbacks` and memory browse right-rail links to call `buildMemoryVariantAnchorHref`. Update `FlashbackActionMenuItem` with optional `langCode`, and include `langCode` in the delete body for translated rows. - -- [ ] **Step 7: Implement variant-aware export** - -Update `writeFlashbackMetadataExport` to accept `variant`. Include variant identity in the JSON payload: - -```json -{ - "version": 2, - "memoryId": "...", - "variant": { - "kind": "translation", - "langCode": "ja-JP", - "translationOutputHash": "sha256:..." - }, - "flashbacks": [] -} -``` - -Source exports may remain version `1` only if that avoids rewriting existing source backup fixtures. If source remains version `1`, translated exports must still include variant identity. - -- [ ] **Step 8: Verify this slice** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/flashbacks/repository.test.ts tests/server/memories/browse.test.ts tests/components/flashbacks-loader.test.ts tests/components/flashback-action-menu.test.ts tests/memories/browse-data.test.ts tests/server/backup/git-backup.test.ts -mise exec -- bun run typecheck -``` - -Expected: tests and typecheck pass. - -## Handoff - -Global Flashback surfaces include translated rows, links route to the correct variant, deletion is variant-scoped, and backup/export artifacts are variant-aware. diff --git a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/06-docs-cleanup-and-verification.md b/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/06-docs-cleanup-and-verification.md deleted file mode 100644 index 5565b513..00000000 --- a/docs/workflows/archive/task-19-codex-translation-variant-local-flashbacks/06-docs-cleanup-and-verification.md +++ /dev/null @@ -1,133 +0,0 @@ -# Task 19W.06: Docs Cleanup And Verification Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Remove stale Flashback projection claims from durable docs and verify the full variant-local implementation. - -**Architecture:** Durable docs should describe Flashbacks as variant-local. Task 19V remains a historical projection plan, but workers must not execute its Flashback subtasks after Task 19W is approved. - -**Tech Stack:** Markdown docs, Vitest, typecheck, full `bun run verify`. - ---- - -## Role - -Integration and documentation owner. - -This worker must not introduce new product behavior. - -## Files - -- Modify: `docs/architecture/data-and-storage.md` -- Modify: `docs/architecture/flows.md` -- Modify: `docs/architecture/ui-and-routing.md` -- Modify: `docs/workflows/README.md` -- Modify: `docs/workflows/task-19-codex-translation.md` -- Modify: `docs/workflows/task-19-codex-translation/13-reader-render-integration-for-translated-content.md` -- Modify: `docs/workflows/task-19-codex-translation-reader-projections.md` -- Test: `tests/server/routes/api-flashbacks-toggle.test.ts` -- Test: `tests/server/reader/page-data.test.ts` -- Test: `tests/server/memories/browse.test.ts` -- Test: `tests/components/flashback-action-menu.test.ts` - -## Documentation Contract - -Use this wording in durable docs: - -```md -Flashbacks are local to the reader content variant where they are created. -Source Flashbacks use source reader offsets. Translated Flashbacks use translated -reader offsets and are scoped to the completed translation output hash. Global -Flashback browse and memory search surfaces include renderable Flashbacks from -both source and translated variants. -``` - -Remove or rewrite old projection-era claims that say translated Flashbacks are -derived from source rows, translated writes mutate source rows, translated -readers show source Flashbacks through alignment data, or translated Flashback -creation fails because an alignment span is only partially covered. - -Keep translation projection documentation only for translation maps and future non-Flashback uses. - -## Task Steps - -- [ ] **Step 1: Add final integration assertions** - -Add one integration test that exercises the full flow after previous subtasks: - -```ts -const source = await loadReaderMemory(memoryId, { config }); -const translated = await loadReaderMemory(memoryId, { - config, - langCode: "ja-JP", -}); - -expect(source.status).toBe("ready"); -expect(translated.status).toBe("ready"); -expect(source.memory.flashbacks.map((row) => row.variantKind)).toEqual([ - "source", -]); -expect(translated.memory.flashbacks.map((row) => row.variantKind)).toEqual([ - "translation", -]); -``` - -- [ ] **Step 2: Run focused verification before docs** - -Run: - -```sh -mise exec -- bun --bun x vitest run tests/server/routes/api-flashbacks-toggle.test.ts tests/server/reader/page-data.test.ts tests/server/memories/browse.test.ts tests/components/flashback-action-menu.test.ts -``` - -Expected: PASS if implementation subtasks are complete. - -- [ ] **Step 3: Update architecture docs** - -Update: - -- `docs/architecture/data-and-storage.md`: variant-aware `flashbacks` columns, translated export path, stale output hash behavior. -- `docs/architecture/flows.md`: translated Flashback toggle resolves active translated content and never writes back into source rows. -- `docs/architecture/ui-and-routing.md`: translated reader shows translated Flashbacks only; `/flashbacks` shows all renderable variants. - -- [ ] **Step 4: Update Task 19 docs** - -Update Task 19 translation docs so reader rendering no longer promises canonical Flashback projection. Keep `translation_projection_spans` as translation alignment data, but remove it from Flashback write requirements. - -- [ ] **Step 5: Mark Task 19V Flashback work superseded** - -At the top of `docs/workflows/task-19-codex-translation-reader-projections.md`, add: - -```md -> Superseded for Flashbacks by Task 19W. Do not execute Task 19V Flashback -> subtasks for translated Flashback behavior. Task 19V remains a historical -> projection design record and may inform future Moment or alignment work only -> after a new plan approves that scope. -``` - -- [ ] **Step 6: Run stale-doc search** - -Run: - -```sh -rg -n "Flashbacks.*source.canonical|reverse.?project|project.canonical.Flashbacks|projected.snippets|partial.projection|projection.map" docs src tests -``` - -Expected: remaining matches are either non-Flashback projection docs, archived history, or the Task 19V supersession note. Update active docs until the result is unambiguous. - -- [ ] **Step 7: Run final verification** - -Run: - -```sh -mise exec -- bun run typecheck -mise exec -- bun --bun x vitest run tests/server/routes/api-flashbacks-toggle.test.ts tests/server/reader/page-data.test.ts tests/server/memories/browse.test.ts tests/components/flashback-action-menu.test.ts tests/server/backup/git-backup.test.ts -git diff --check -mise exec -- bun run verify -``` - -Expected: all commands pass. If full verification emits an existing non-failing dependency warning, record the exact warning and exit code. - -## Handoff - -Task 19W is complete when implementation, docs, and tests all agree that Flashbacks are variant-local and global Flashback surfaces union all renderable variants. diff --git a/docs/workflows/archive/task-19-codex-translation.md b/docs/workflows/archive/task-19-codex-translation.md deleted file mode 100644 index 26056183..00000000 --- a/docs/workflows/archive/task-19-codex-translation.md +++ /dev/null @@ -1,236 +0,0 @@ -# Brilliant: Codex app-server translation for memories - -> Archived execution plan. The Brilliant translation work has landed or been -> superseded; use current code, architecture/reference docs, and tests for -> active implementation decisions. - -## Status - -- Implementation source of truth: this parent file is an overview only. Implementation workers must start from `docs/workflows/task-19-codex-translation/README.md`, then `00-execution-contracts.md`, then the focused contracts listed for their assigned subtask. -- State: Implementation authorized; this workflow is the execution contract - baseline for Brilliant implementation and review. -- Base branch: `main` -- Implementation branch: `feat/brilliant` -- Depends on: merged `/settings` page, SQLite-backed BCP 47 target-language setting, and current OpenAI auth settings boundary. -- Scope: Add a Codex-powered translation pipeline for Reader memory content, including app-server integration, Reader-owned chunk orchestration, streaming progress, validation, retry, atomic translated `CONTENT.md` persistence, and SQLite cleanup. -- Out of scope: multi-user auth, hosted OAuth service, direct browser access to Codex app-server, direct OpenAI Responses API integration, non-Codex providers, collaborative translation editing, and storing completed translated article bodies in SQLite. - -## Plan basis - -This plan supersedes the earlier `codex exec`-first design. The preferred integration surface is Codex app-server because it supports application integration, thread/turn control, managed ChatGPT sign-in flows, and streamed agent events. The Reader backend remains the owner of memory storage, chunking, validation, retry, final file writes, SQLite cleanup, and frontend event streaming. - -Research reference captured by the instruction: - -- [App Server - Codex | OpenAI Developers](https://developers.openai.com/codex/app-server) - -## Core architecture decisions - -- Use Codex app-server as the production integration path. -- Treat Codex app-server as a Codex app-server wire-protocol integration, not a REST endpoint. The backend client must connect over a configured app-server transport, run `initialize` plus `initialized`, create an ephemeral thread with `thread/start`, then start translation work with `turn/start`. -- Brilliant MVP uses the Codex app-server wire protocol over an explicitly configured Unix socket listener (`codex app-server --listen unix:///tmp/trauma-codex.sock`) as TRAUMA's required local transport. This is not the Codex CLI's no-flag default; `codex app-server` without `--listen unix:///tmp/trauma-codex.sock` uses `stdio` and is not a valid Brilliant backend endpoint. Loopback WebSocket is not a supported TRAUMA transport. HTTP is allowed only for health probes such as `/readyz` or `/healthz`, not for app-server wire-protocol calls. `stdio` process ownership is out of scope because TRAUMA does not start or supervise the Codex app-server process. -- Use Codex-managed ChatGPT sign-in; surface app-server `chatgptDeviceCode` login when auth is missing. -- Keep OpenAI/ChatGPT tokens out of TRAUMA SQLite, logs, frontend responses, and browser storage. -- Do not expose Codex app-server directly to the browser; only the Reader backend talks to Codex. -- Use one ephemeral Codex thread per chunk attempt by default so long documents do not depend on one long model context and retry attempts do not inherit invalid prior model output. -- Start translation work with a locked-down thread and turn policy: no approvals, read-only sandboxing, no project/store filesystem access beyond the prompt input supplied by the Reader backend, and disabled network access when the installed app-server schema can express it for the selected sandbox. If the schema cannot express network-disabled read-only turns directly, do not attach network-capable tools, dynamic tools, MCP servers, or writable/readable project roots; update the Brilliant contract with the equivalent minimum-privilege payload before implementation. -- Keep document-level state, glossary, style profile, chunk manifest, retries, and stitching in Reader code and SQLite. -- Use SSE as the default frontend transport because the MVP only needs server-to-client progress streaming. -- Add cancellation through a normal backend endpoint and job state; defer WebSocket until bidirectional live steering is required. -- Resolve source and translated content under configured `storePath`. -- Store source memory content at store-relative `memories//CONTENT.md`. -- Store translated content at store-relative `memories///CONTENT.md`, for example `memories/abc123/ja-JP/CONTENT.md`. -- Store translated projection backup data at store-relative `memories///TRANSLATION_MAP.json`. -- Store layout note: implementation must use TRAUMA's existing plural store layout under configured `storePath`: `memories//...`. Do not depend on root-level scratch instruction files for the current source of truth. -- Expose translated content through a dedicated reader route shaped as `/memories/:lang_code/:id`, mapping to store-relative `memories///CONTENT.md`. -- On the source memory reader page, show a Codex icon at the right edge of the title only when the configured target-language translation does not exist yet. -- Do not show the Codex translation icon on translated reader routes. -- Show variant tabs under the memory header only when more than one `CONTENT.md` variant exists. Hide tabs when only the default source `CONTENT.md` exists. -- Use BCP 47 language codes such as `ja-JP`. -- Persist the user-selected translation target language in SQLite through `/settings`; Brilliant reads this server-side value when starting translation. -- Do not introduce persistent `.work/` artifacts. -- Allow temporary SQLite chunk content during translation only. -- Immediately purge completed translated chunk bodies from SQLite after final `CONTENT.md` has been atomically committed. -- Immediately purge temporary chunk projection JSON after final commit while retaining durable `translation_projection_spans` rows keyed by source and output hashes. -- Persist only metadata needed for status, stale detection, audit, retry diagnostics, and cache validation. - -## End-to-end pipeline - -1. Source loading reads store-relative `memories//CONTENT.md` under configured `storePath`, computes `source_hash`, file size, rough token estimate, document type hint, source URL, and source title. -2. Block manifest generation parses Markdown into deterministic blocks with stable ids such as `b000001`. -3. Chunking groups contiguous blocks, prefers section boundaries, splits oversized sections by block groups, and preserves document order. -4. Codex translation creates or opens the configured app-server transport, completes JSON-RPC initialization, starts one ephemeral thread for the chunk attempt, then sends one chunk plus metadata and policy through `turn/start` with an output schema when supported. -5. Streaming maps Codex notifications such as `item/agentMessage/delta`, `item/started`, and `item/completed` into Reader SSE events. -6. Validation checks every completed chunk before it can become authoritative. -7. Retry handles chunk-level validation, auth, usage, timeout, context, and stream failures without retrying the whole document unnecessarily. -8. Stitching reassembles translated blocks in manifest order and performs final full-document validation. -9. Atomic commit writes a same-directory temp file, flushes it, renames it to `CONTENT.md`, writes `TRANSLATION_MAP.json`, stores projection rows, flushes the parent directory when supported, marks the job complete, and purges completed chunk bodies plus temporary chunk projection JSON. -10. Reader rendering reloads `memories///CONTENT.md` only after commit succeeds, then exposes it through the translated reader route and variant tabs. Flashbacks are local to the reader content variant where they are created. Source Flashbacks use source reader offsets. Translated Flashbacks use translated reader offsets and are scoped to the completed translation output hash. Global Flashback browse and memory search surfaces include renderable Flashbacks from both source and translated variants. Moment projection remains outside the Brilliant MVP unless a later workflow explicitly approves it. - -## Minimal SQLite schema direction - -Brilliant should add these tables or equivalent Drizzle schema objects: - -```sql -translation_jobs -translation_chunks -translation_projection_spans -``` - -`translation_jobs` tracks job metadata, status, hashes, output path, selected -Codex model, selected Codex reasoning effort, chunker version, prompt policy -version, errors, and timestamps. Model and effort are execution metadata; they -do not change completed-translation identity. - -`translation_chunks` tracks per-chunk source hash, ordered block ids, status, retry count, temporary translated Markdown, translated hash, error state, and timestamps. - -Status values must be explicit and separated by job/chunk domain. Job status includes `pending`, `running`, `stale`, `cancel_requested`, `canceled`, `unavailable`, `stitching`, `committing`, `complete`, and `failed`. Chunk status includes `pending`, `running`, `validating`, `retrying`, `complete`, `purged`, and `failed`. - -Completed chunk body cleanup is required: - -```sql -UPDATE translation_chunks -SET translated_markdown = NULL, - status = 'purged' -WHERE job_id = ? - AND status = 'complete'; -``` - -## Reader streaming event contract - -Use SSE at: - -```http -GET /api/translation-jobs/:job_id/events -``` - -Use this event envelope: - -```json -{ - "type": "translation.codex.delta", - "job_id": "...", - "memory_id": "...", - "lang_code": "ja-JP", - "chunk_index": 3, - "timestamp": 1710000000000, - "data": { "text": "..." } -} -``` - -Required event types: - -```text -translation.job.started -translation.chunk.queued -translation.chunk.started -translation.codex.delta -translation.codex.item.started -translation.codex.item.completed -translation.chunk.validating -translation.chunk.completed -translation.chunk.failed -translation.chunk.retrying -translation.job.snapshot -translation.job.stitching -translation.job.committing -translation.job.completed -translation.job.failed -translation.job.stale -translation.job.canceled -``` - -Partial Codex deltas are non-authoritative progress only. Persistence must use final completed, parsed, validated chunk output. - -`translation.job.completed` must include `output_path`, `output_hash`, and `reader_url`. `reader_url` is `/memories//` and is the frontend's canonical completion navigation target. - -## Minimal backend API shape - -```http -POST /api/memories/:memory_id/translations -GET /api/memories/:memory_id/translations/:lang_code -GET /api/translation-jobs/:job_id -GET /api/translation-jobs/:job_id/events -POST /api/translation-jobs/:job_id/cancel -GET /api/settings/codex-models -PATCH /api/settings/translation-codex-defaults -``` - -`POST /api/memories/:memory_id/translations` starts or reuses a translation job. -The body may include `lang_code`, `model`, and `reasoning_effort`; omitted -`model` or `reasoning_effort` values use the persisted Codex translation -defaults. Submitted model and effort are validated against Codex app-server -`model/list`, saved as new defaults when accepted, written to -`translation_jobs`, and forwarded to app-server `turn/start`. An existing active -job is reused without rewriting its model or effort. The route schedules work on -the local in-process Brilliant runner and returns without waiting for full -translation. - -`GET /api/memories/:memory_id/translations/:lang_code` returns committed translation metadata and renderability state. - -`GET /api/translation-jobs/:job_id` returns current job state from SQLite. - -`GET /api/translation-jobs/:job_id/events` streams SSE progress. - -`POST /api/translation-jobs/:job_id/cancel` requests cancellation by marking backend job state; WebSocket is not required for MVP cancellation. - -## Subtask execution order - -0. [Execution contract index](task-19-codex-translation/00-execution-contracts.md) - read this first, then only the focused contract files listed for your subtask. - -1. [19.1 Requirements and architecture finalization](task-19-codex-translation/01-requirements-and-architecture-finalization.md) -2. [19.2 SQLite schema and migration design](task-19-codex-translation/02-sqlite-schema-and-migration-design.md) -3. [19.3 Translation job state machine](task-19-codex-translation/03-translation-job-state-machine.md) -4. [19.4 Markdown block manifest and chunker](task-19-codex-translation/04-markdown-block-manifest-and-chunker.md) -5. [19.5 Codex app-server integration](task-19-codex-translation/05-codex-app-server-integration.md) -6. [19.6 Codex auth and device-code setup flow](task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md) -7. [19.7 Streaming event bridge to frontend](task-19-codex-translation/07-streaming-event-bridge-to-frontend.md) -8. [19.8 Chunk translation prompt and output schema](task-19-codex-translation/08-chunk-translation-prompt-and-output-schema.md) -9. [19.9 Chunk validation and retry logic](task-19-codex-translation/09-chunk-validation-and-retry-logic.md) -10. [19.10 Stitching and atomic commit](task-19-codex-translation/10-stitching-and-atomic-commit.md) -11. [19.11 SQLite cleanup and purge policy](task-19-codex-translation/11-sqlite-cleanup-and-purge-policy.md) -12. [19.12 Frontend translation controls and progress UI](task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md) -13. [19.13 Reader render integration for translated CONTENT.md](task-19-codex-translation/13-reader-render-integration-for-translated-content.md) -14. [19.14 Translation skill definition](task-19-codex-translation/14-translation-skill-definition.md) -15. [19.15 Error handling and cancellation](task-19-codex-translation/15-error-handling-and-cancellation.md) -16. [19.16 Test plan and fixtures](task-19-codex-translation/16-test-plan-and-fixtures.md) -17. [19.17 End-to-end validation with long paper fixture](task-19-codex-translation/17-end-to-end-validation-with-long-paper-fixture.md) - -Implementation must continue to follow the focused contract files above; update -this workflow when accepted contracts change. - -## Parallelization map - -- Track A: 19.2 and 19.3 after 19.1 freezes schema/status names. -- Track B: 19.4 after 19.1 freezes block id and manifest contracts. -- Track C: 19.5, 19.6, and 19.7 after 19.1 freezes app-server and SSE boundaries. -- Track D: 19.12 and 19.13 after 19.7 freezes the event envelope and 19.10 freezes output metadata. -- Track E: 19.8, 19.9, and 19.14 after 19.4 freezes block ids and protected-span representation. -- Track F: 19.16 and 19.17 after interfaces from 19.2 through 19.10 are stable. - -Subagents may work only on non-overlapping files and must report changed files, new interfaces, assumptions, risks, and required follow-up. - -## Redefined plan acceptance criteria - -- Uses store-relative `memories///CONTENT.md` under configured `storePath`. -- Explicitly maps the instruction's conceptual `memory//...` path to the existing plural `memories//...` store layout. -- Uses a dedicated translated reader route for translated `CONTENT.md` variants. -- Shows the Codex icon trigger only on the source reader page when the configured target translation is missing. -- Shows memory variant tabs only when two or more `CONTENT.md` variants exist. -- Uses `ja-JP`-style BCP 47 language codes. -- Does not introduce `.work/`. -- Allows temporary SQLite chunk storage during translation. -- Requires immediate purge of translated chunk bodies after final commit. -- Keeps source and translated Flashbacks variant-local while preserving unified global Flashback browse/search surfaces. -- Uses atomic final file write. -- Supports long documents and academic papers through deterministic chunking. -- Includes frontend streaming progress through SSE. -- Uses Codex app-server as the preferred integration path. -- Defines Codex app-server wire-protocol transport, initialization, thread, turn, auth, and cancellation boundaries. -- Defines explicitly configured Unix socket as TRAUMA's required local app-server transport, rejects WebSocket, and keeps HTTP wire-protocol calls plus `stdio` process ownership out of scope for Brilliant MVP. -- Keeps Codex tokens out of the frontend and TRAUMA SQLite. -- Treats external article content as untrusted data. -- Defines validation and retry at chunk level. -- Defines final stitching and full-document validation. -- Includes repo-local `reader-translate` skill planning. -- Produces numbered subtasks with dependencies and parallelization notes. -- Keeps Brilliant planning-only until implementation is explicitly authorized. diff --git a/docs/workflows/archive/task-19-codex-translation/00-execution-contracts.md b/docs/workflows/archive/task-19-codex-translation/00-execution-contracts.md deleted file mode 100644 index 0dca9290..00000000 --- a/docs/workflows/archive/task-19-codex-translation/00-execution-contracts.md +++ /dev/null @@ -1,82 +0,0 @@ -# Brilliant execution contract index - -## Purpose - -This file is intentionally small. It routes each implementation subtask to the focused contract files it needs, so workers do not have to load one large context file before every change. - -## Rule for implementation agents - -Read this file first, then read only the contract files listed for your assigned subtask plus the assigned subtask file itself. - -If a contract conflict exists, use this precedence order: - -1. Focused files under `docs/workflows/task-19-codex-translation/contracts/`. -2. The assigned `19.x` subtask file. -3. Parent workflow and README summaries. - -Implementation workers must treat this workflow directory as the source of truth. Do not depend on any root-level scratch instruction files being present. - -## Canonical names - -- Product task name: `Brilliant` -- Implementation branch: `feat/brilliant` -- Feature purpose: Codex app-server powered translation for Reader memory content -- Source content path: store-relative `memories//CONTENT.md` under configured `storePath` -- Translated content path: store-relative `memories///CONTENT.md` under configured `storePath` -- Do not introduce singular `memory//...` paths; the existing store layout uses plural `memories/`. -- Instruction path mapping: when the instruction says `memory//...`, implement it as store-relative `memories//...` because that is the merged TRAUMA store layout. -- Source reader route: `/memories/:id` -- Translated reader route: `/memories/:lang_code/:id`, mapping to store-relative `memories///CONTENT.md` -- Translation trigger: Codex icon at the source reader title right edge, shown only when the configured target-language variant is missing -- Variant tabs: shown under the memory header only when two or more `CONTENT.md` variants exist -- Japanese language code: `ja-JP` -- Translation target language source: `/settings` user setting persisted in SQLite -- Default progress transport: SSE -- Codex integration: backend-only Codex app-server client -- Codex protocol: Codex app-server wire protocol over the configured app-server transport; initialize before any request, then `thread/start`, then `turn/start`. The wire envelope intentionally omits top-level `jsonrpc`. -- Codex app-server transport: TRAUMA's required default is an explicitly configured Unix socket listener via `codex app-server --listen unix:///tmp/trauma-codex.sock` and `TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock`; loopback WebSocket is not supported; HTTP is health-probe-only and must not be used for app-server wire-protocol calls. Unix socket endpoint URIs use three slashes and an absolute filesystem path. `codex app-server` without `--listen unix:///tmp/trauma-codex.sock` uses `stdio`, which is not a Brilliant endpoint because TRAUMA does not start or supervise the app-server process. -- Codex thread/turn policy: translation threads and turns use `approvalPolicy: "never"`, read-only sandboxing, disabled network access when supported by the generated schema, and prompt-supplied source content only. If the schema cannot represent network-disabled read-only turns directly, implement the closest minimum-privilege payload only after updating the Brilliant contract. -- Default Codex thread strategy: one ephemeral Codex thread per chunk attempt, including retry attempts -- Codex cancellation: call `turn/interrupt` with both `threadId` and `turnId` when known - -## Contract files - -- [Contracts README](contracts/README.md): map of focused contract files. -- [Architecture and ownership](contracts/01-architecture-and-ownership.md): file ownership, boundaries, and subagent write scopes. -- [Types, state, and settings](contracts/02-types-state-and-settings.md): shared TypeScript types, job/chunk states, and SQLite-backed language setting. -- [SQLite and repositories](contracts/03-sqlite-and-repositories.md): DDL, indexes, hashes, paths, and repository methods. -- [API and SSE](contracts/04-api-and-sse.md): backend endpoints, payloads, SSE envelope, reconnect, and cancellation. -- [Markdown chunking](contracts/05-markdown-chunking.md): block scanner, protected spans, chunk config, and chunk metadata. -- [Codex prompt and validation](contracts/06-codex-prompt-and-validation.md): app-server client boundary, prompt sections, output schema, validation, and retry. -- [Atomic commit, purge, and recovery](contracts/07-atomic-commit-purge-recovery.md): final write sequence, purge policy, and crash recovery. - -## Subtask contract map - -- 19.1 Requirements and architecture finalization: read all contract files. -- 19.2 SQLite schema and migration design: read 02, 03, 07. -- 19.3 Translation job state machine: read 02, 03, 04, 07. -- 19.4 Markdown block manifest and chunker: read 02, 05. -- 19.5 Codex app-server integration: read 04, 06. -- 19.6 Codex auth and device-code setup flow: read 02, 04, 06. -- 19.7 Streaming event bridge to frontend: read 04, 06. -- 19.8 Chunk translation prompt and output schema: read 05, 06. -- 19.9 Chunk validation and retry logic: read 05, 06. -- 19.10 Stitching and atomic commit: read 03, 05, 07. -- 19.11 SQLite cleanup and purge policy: read 03, 07. -- 19.12 Frontend translation controls and progress UI: read 02, 04. -- 19.13 Reader render integration for translated CONTENT.md: read 02, 03, 07. -- 19.14 Translation skill definition: read 06. -- 19.15 Error handling and cancellation: read 02, 04, 06, 07. -- 19.16 Test plan and fixtures: read the contract files for the tests being implemented. -- 19.17 End-to-end validation with long paper fixture: read 02, 03, 04, 05, 06, 07. - -## Per-subtask report requirements - -Every implementation subtask must report: - -- Changed files -- New interfaces -- Assumptions -- Risks -- Required follow-up -- Verification commands and results, or explicit reason validation was skipped diff --git a/docs/workflows/archive/task-19-codex-translation/01-requirements-and-architecture-finalization.md b/docs/workflows/archive/task-19-codex-translation/01-requirements-and-architecture-finalization.md deleted file mode 100644 index 6823e3fd..00000000 --- a/docs/workflows/archive/task-19-codex-translation/01-requirements-and-architecture-finalization.md +++ /dev/null @@ -1,96 +0,0 @@ -# 19.1 Requirements and architecture finalization - -## Goal - -Freeze Brilliant implementation boundaries before code work starts. This subtask turns the product instruction into stable implementation contracts and does not implement application code. - -## Files likely owned - -- `docs/workflows/task-19-codex-translation/00-execution-contracts.md` -- `docs/workflows/task-19-codex-translation/contracts/README.md` -- `docs/workflows/task-19-codex-translation/contracts/01-architecture-and-ownership.md` -- `docs/workflows/task-19-codex-translation/contracts/02-types-state-and-settings.md` -- `docs/workflows/task-19-codex-translation/contracts/03-sqlite-and-repositories.md` -- `docs/workflows/task-19-codex-translation/contracts/04-api-and-sse.md` -- `docs/workflows/task-19-codex-translation/contracts/05-markdown-chunking.md` -- `docs/workflows/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md` -- `docs/workflows/task-19-codex-translation/contracts/07-atomic-commit-purge-recovery.md` - -## Contract references - -Read all focused contract files for this subtask. - -## Instruction alignment - -Scope: planning contracts only; no application code. - -Inputs: the archived scratch instruction note formerly named `TASK_19_INSTRUCTION.md`, current TRAUMA docs, current settings/storage implementation, and official Codex app-server protocol. - -Outputs: frozen Brilliant contract files, storage path mapping, route shape, app-server wire-protocol boundary, auth boundary, and subtask dependency map. - -Dependencies: none; this subtask gates all other Brilliant work. - -Parallelization notes: do not parallelize downstream implementation until this subtask freezes shared names and interfaces. - -Implementation risks: copying the instruction's conceptual `memory/` path literally would create a second store tree; omitting app-server wire-protocol lifecycle details would make Codex integration non-implementable. - -## Architecture contract - -Brilliant uses Codex app-server as the preferred production integration surface. The Reader backend owns source loading, chunking, metadata, translation job state, validation, retry, stitching, atomic final writes, SQLite cleanup, and frontend event streaming. - -Codex receives chunk text, metadata, and translation instructions. Codex does not own article storage layout and must not write canonical translated `CONTENT.md` files. - -The browser never talks to Codex app-server directly. The browser talks only to TRAUMA backend APIs and SSE endpoints. - -The canonical translated file layout is: - -```text -memories//CONTENT.md -memories///CONTENT.md -``` - -This is the repo-correct implementation of the instruction's conceptual `memory///CONTENT.md` requirement. Do not create a singular `memory/` directory. - -The selected target language comes from the SQLite-backed `/settings` value, for example `translation_target_lang_code = "ja-JP"`. - -## Freeze checklist - -Before later subtasks start, confirm these names and contracts are stable: - -- `feat/brilliant` implementation branch. -- `translation_jobs` and `translation_chunks` table names. -- `TranslationJobStatus` and `TranslationChunkStatus` values. -- `POST /api/memories/:memory_id/translations` job start route. -- `GET /api/translation-jobs/:job_id/events` SSE route. -- SSE event names under `translation.*`. -- Markdown block id format `b000001`. -- Hash format `sha256:`. -- Final output path `memories///CONTENT.md`. -- Translated reader route `/memories/:lang_code/:id`. -- Codex app-server wire-protocol lifecycle: connect, `initialize`, `initialized`, `account/read`, `thread/start`, `turn/start`, notifications, and optional `turn/interrupt`. -- Codex app-server transport boundary: `codex app-server --listen unix:///tmp/trauma-codex.sock` is TRAUMA's required default wire-protocol transport; Unix socket endpoint URIs use three slashes and an absolute filesystem path; loopback WebSocket is not supported; HTTP is health-probe-only; `stdio` process ownership and the Codex CLI no-flag `stdio` default are outside Brilliant MVP. -- Codex translation thread/turn security policy: `approvalPolicy: "never"`, read-only sandbox, no direct filesystem reads from the TRAUMA project or store, and no network access when the installed app-server schema supports that field for the selected sandbox. If schema support differs, update the Brilliant contract with the equivalent minimum-privilege payload before implementation. -- Temp final-write path `.CONTENT..tmp` in the language directory. - -## Tests - -No application tests are required in this subtask. If docs tooling exists, run the focused docs validation command used by the project. - -## Verification - -```sh -# Optional only if docs validation exists in the repo -mise exec -- bun run verify:docs -``` - -If no docs verification command exists, record that this subtask is documentation-only and no validation was run. - -## Acceptance criteria - -- The focused contract files are small enough for subtask workers to load selectively. -- The parent README maps each subtask to only the contracts it needs. -- Codex app-server is documented as the preferred integration path. -- Reader-owned orchestration is explicit. -- SQLite-backed settings language is explicit. -- Persistent `.work/` artifacts are forbidden. -- Later subtasks do not need to invent table names, route names, event names, or storage paths. diff --git a/docs/workflows/archive/task-19-codex-translation/02-sqlite-schema-and-migration-design.md b/docs/workflows/archive/task-19-codex-translation/02-sqlite-schema-and-migration-design.md deleted file mode 100644 index 4ee3d3fd..00000000 --- a/docs/workflows/archive/task-19-codex-translation/02-sqlite-schema-and-migration-design.md +++ /dev/null @@ -1,138 +0,0 @@ -# 19.2 SQLite schema and migration design - -## Goal - -Create the database foundation for Brilliant translation jobs, temporary chunk state, and SQLite-backed target-language settings. This subtask does not implement Codex calls, route handlers, or UI. - -## Files likely owned - -- `src/server/db/schema.ts` -- `drizzle/.sql` -- `src/server/db/repositories.ts` -- optional `src/server/db/translation-repositories.ts` -- `src/server/translation/languages.ts` -- optional `src/server/settings/translation-language.ts` -- `tests/server/db/translation-schema.test.ts` -- `tests/server/db/translation-repositories.test.ts` -- `tests/server/settings/translation-language.test.ts` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/03-sqlite-and-repositories.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: SQLite tables, migrations, and repository methods for jobs/chunks/settings only. - -Inputs: frozen status names, current `memories` table, current settings schema, and atomic commit requirements. - -Outputs: `translation_jobs`, `translation_chunks`, repository methods, central supported-language table, and SQLite-backed target language access. - -Dependencies: 19.1 must freeze table/status/path names first. - -Parallelization notes: can proceed with 19.3 after the schema shape is frozen; avoid parallel edits to `src/server/db/schema.ts` and repositories. - -Implementation risks: storing completed translated bodies in SQLite violates the instruction; adding credential/token columns violates the auth boundary. - -## Data model contract - -Add `translation_jobs` and `translation_chunks` using the SQL shape in `contracts/03-sqlite-and-repositories.md`. - -Rules: - -- `translation_jobs.memory_id` references `memories.id` with `ON DELETE CASCADE`. -- Complete jobs use a partial unique index on `(memory_id, lang_code, source_hash)` where `status = 'complete'`. -- Active jobs use a partial unique index on `(memory_id, lang_code, source_hash)` where status is one of `pending`, `running`, `cancel_requested`, `stitching`, or `committing`. -- Failed, canceled, stale, and unavailable jobs do not block a new retry job for the same source hash. -- `translation_chunks` uses `(job_id, chunk_index)` as primary key. -- `translated_markdown` is nullable and temporary. -- `output_path` is store-relative. -- Hash values use `sha256:`. -- `prompt_policy_version` records the deterministic Brilliant prompt policy version used by `src/server/translation/prompt.ts`; it does not imply runtime `$reader-translate` skill invocation. The segment-reassembly policy uses `BRILLIANT_PROMPT_POLICY_VERSION = "brilliant-segments-v1"` and must be bumped when prompt semantics or validation assumptions intentionally change. -- No table stores Codex credentials, ChatGPT tokens, app-server tokens, refresh tokens, or raw auth files. - -## Settings language contract - -Persist the `/settings` translation target language in SQLite. Use the existing settings table and settings repository if present. If no focused settings service exists, add `src/server/settings/translation-language.ts`. - -Rules: - -- Create `src/server/translation/languages.ts` in this subtask and export `SUPPORTED_TRANSLATION_LANGUAGES` before downstream workers implement prompt, reader, or route behaviour. -- The persisted value is a supported BCP 47 code. -- Japanese is `ja-JP`. -- Supported values and display labels come from the central `SUPPORTED_TRANSLATION_LANGUAGES` table in the shared types/settings contract. -- Frontend component state is not canonical. -- Translation jobs copy the current persisted value into `translation_jobs.lang_code` at job creation. -- Existing jobs keep their copied `lang_code` if the user changes settings later. - -## Repository contract - -Expose focused methods or exact equivalents: - -```ts -createTranslationJob(input): Promise -getTranslationJob(jobId): Promise -findCompleteTranslationRecord(memoryId, langCode, sourceHash): Promise -findActiveTranslationJob(memoryId, langCode, sourceHash): Promise -updateTranslationJobStatus(jobId, status, patch): Promise -claimTranslationJob(jobId, expectedStatus: "pending"): Promise -cancelPendingTranslationJob(jobId): Promise -requestRunningTranslationJobCancellation(jobId): Promise -markTranslationUnavailable(jobId, reason): Promise -insertTranslationChunks(jobId, chunks): Promise -getTranslationChunks(jobId): Promise -updateTranslationChunk(jobId, chunkIndex, patch): Promise -purgeCompletedTranslationChunks(jobId): Promise -countTranslationChunksByStatus(jobId): Promise> -getTranslationTargetLanguage(): Promise -setTranslationTargetLanguage(langCode: string): Promise -``` - -Repository rules: - -- Language validation occurs before writes that can affect paths. -- Missing jobs and chunks are reported explicitly. -- Repository methods are SQLite-only; output file existence and hash checks belong to service/page-data/orchestrator code. -- Purge sets completed chunk `translated_markdown` to `NULL` and status to `purged`. -- Public `completed_chunks` aggregation counts both `complete` and `purged` chunks. -- Repository methods do not perform Codex app-server calls. - -## Tests - -Cover: - -- migration creates `translation_jobs` and `translation_chunks` -- memory delete cascades translation job and chunk rows -- duplicate complete `(memory_id, lang_code, source_hash)` is rejected or returned idempotently according to repository contract -- duplicate active `(memory_id, lang_code, source_hash)` is rejected or returned idempotently according to repository contract -- failed/canceled/stale jobs do not block a new retry job for the same `(memory_id, lang_code, source_hash)` -- unavailable jobs do not block a new retry job for the same `(memory_id, lang_code, source_hash)` -- repository complete lookup does not touch filesystem state -- `output_path` stores a relative path -- `translated_markdown` can be nulled while `translated_hash` remains -- purge converts complete chunks to purged chunks with `translated_markdown = NULL` -- public progress aggregation reports purged chunks as completed chunks -- settings language persists `ja-JP` -- settings language rejects non-canonical casing and unsupported codes -- `SUPPORTED_TRANSLATION_LANGUAGES` is exported from `src/server/translation/languages.ts` -- unsupported and traversal-like language values are rejected -- no schema column stores credential material - -## Verification - -```sh -mise exec -- bun run test tests/server/db/translation-schema.test.ts -mise exec -- bun run test tests/server/db/translation-repositories.test.ts -mise exec -- bun run test tests/server/settings/translation-language.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Database schema supports all later Brilliant subtasks. -- Settings language is server-side SQLite state. -- Completed translated article bodies cannot become permanently stored in SQLite. -- No Codex credential material is stored. -- No API or UI behaviour is introduced here. diff --git a/docs/workflows/archive/task-19-codex-translation/03-translation-job-state-machine.md b/docs/workflows/archive/task-19-codex-translation/03-translation-job-state-machine.md deleted file mode 100644 index 942e5a7b..00000000 --- a/docs/workflows/archive/task-19-codex-translation/03-translation-job-state-machine.md +++ /dev/null @@ -1,185 +0,0 @@ -# 19.3 Translation job state machine - -## Goal - -Implement the Reader-owned Brilliant job and chunk lifecycle. This subtask creates orchestration state primitives but does not implement Markdown chunking, Codex transport, or reader UI. - -## Files likely owned - -- `src/server/translation/types.ts` -- `src/server/translation/current-translation.ts` -- `src/server/translation/source-loader.ts` -- `src/server/translation/job-state.ts` -- `src/server/translation/orchestrator.ts` -- `src/server/translation/job-runner.ts` -- `tests/server/translation/source-loader.test.ts` -- `tests/server/translation/job-state.test.ts` -- `tests/server/translation/orchestrator.test.ts` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/03-sqlite-and-repositories.md` -- `contracts/04-api-and-sse.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: Reader-owned job lifecycle, source freshness, idempotent job start, and local runner scheduling. - -Inputs: SQLite repositories, settings language, source `CONTENT.md`, source hash, and runner recovery contract. - -Outputs: job start orchestration, state transition guards, source snapshot loading, and recoverable runner scheduling. - -Dependencies: 19.2 provides schema/repository methods; 19.4/19.5 plug into this later. - -Parallelization notes: can run beside 19.4 after shared types are frozen; do not implement Codex transport or Markdown chunking here. - -Implementation risks: blocking the POST route until full translation completes violates async pipeline requirements; failing to re-check source hash before commit risks stale output. - -Ownership: this subtask creates and owns `src/server/translation/current-translation.ts`. It implements `resolveCurrentTranslationReadOnly()` and the core `repairUnavailableTranslation()` helper used by job start. Later reader subtasks consume `resolveCurrentTranslationReadOnly()` but do not edit this file. - -## Source loading contract - -Load store-relative `memories//CONTENT.md` under configured `storePath` and compute: - -- `source_hash` as `sha256:` -- byte size -- rough token estimate -- source title from metadata when available -- source URL from SQLite metadata when available -- document type hint: `article`, `paper`, or `unknown` - -Do not load translated files in this subtask. - -Hashing rule: - -- Hash the exact UTF-8 bytes read from source `CONTENT.md`. -- Do not normalize line endings. -- Do not trim bytes. -- Do not parse and reserialize Markdown before hashing. -- If the file cannot be decoded as UTF-8 for Markdown parsing, fail source loading before chunk creation. - -## Job start contract - -Start algorithm: - -1. Validate `memory_id`. -2. Read `translation_target_lang_code` from SQLite settings. -3. If the request supplied `lang_code`, verify it matches the SQLite setting. -4. Reject with `translation_language_required` when no settings language exists. -5. Load source `CONTENT.md` and compute source metadata. -6. Before active lookup or new job creation, run focused recovery for interrupted `pending`, `running`, `stitching`, `committing`, and `cancel_requested` jobs for the same `(memory_id, settingsLangCode)`. If a recovered active job's stored `source_hash` no longer matches the current source hash, mark that old job `stale`. -7. Look up a complete job for `(memory_id, settingsLangCode, source_hash)`. -8. Resolve the complete job through `resolveCurrentTranslationReadOnly()` from `src/server/translation/current-translation.ts`, which checks output file existence and output hash under `storePath`. -9. If the complete job has an existing output path and the output file hash matches `translation_jobs.output_hash`, return current translation metadata without checking Codex auth, because no new Codex work is required. -10. If the complete job's output file is missing or hash-mismatched, call `repairUnavailableTranslation()` to mark that job `unavailable` before continuing. -11. If a compatible active job exists in `pending`, `running`, `stitching`, or `committing`, return `200` with `status = "active"`, the actual `job_status`, and `event_url` without creating another job. Active job reuse does not perform a request-path auth precondition check, but the runner/recovery path must process the reused `pending` or `running` job and mark it failed with `auth_required` or `setup_required` if Codex auth/setup is no longer available when execution resumes. -12. If the only compatible active row is `cancel_requested`, return `409 cancellation_conflict` and do not create a new job until the cancellation reaches `canceled`. -13. Check Codex auth/setup preconditions with `account/read` through the app-server client immediately before creating a new job. -14. If Codex auth/setup is missing, return `409 auth_required` or `409 setup_required` and do not create a `translation_jobs` row. -15. Create a new `pending` job with `lang_code = settingsLangCode`. -16. Schedule the job on the local in-process Brilliant runner. -17. The runner emits `translation.job.started` after it claims the job and transitions `pending -> running`. - -## Runner contract - -Brilliant uses a local in-process runner for the MVP. - -Rules: - -- `POST /api/memories/:memory_id/translations` must not block until full translation finishes. -- The route creates or reuses a job, schedules it, and returns `202` or `200`. -- The runner processes one job at a time by default. -- Chunks inside a job are processed sequentially by default. -- Runner state is recoverable from SQLite rows. -- Runner scheduling is idempotent by job id. Enqueuing the same job id multiple times is a no-op. -- After a job is claimed, the runner executes chunks whose status is `pending` or `retrying`. -- `retrying` chunks enter the normal retry-attempt path: create a fresh ephemeral Codex thread, increment `retry_count` exactly once before the attempt starts, and use the retry prompt contract from 19.9. -- The runner claims work with an atomic compare-and-set repository method such as `claimTranslationJob(jobId, "pending")` that transitions `pending -> running` only if the row is still `pending`. -- If `claimTranslationJob()` returns false, the runner does not execute the job body and instead reloads the job state. -- The cancel API races the runner through compare-and-set repository methods. `cancelPendingTranslationJob()` transitions `pending -> canceled`; `requestRunningTranslationJobCancellation()` transitions `running -> cancel_requested`. -- If pending cancellation loses the race to runner claim, reload the job. If it is now `running`, request running cancellation; if it is terminal, return the idempotent or conflict response for that terminal state. -- Jobs already in `running`, `stitching`, `committing`, terminal states, or `cancel_requested` must not be re-enqueued as new work. -- Before accepting a new job, recover interrupted `pending`, `running`, `stitching`, `committing`, and `cancel_requested` jobs. -- Job-start recovery must run before active lookup or new job creation for the same `(memory_id, lang_code)`. -- A recovered `pending` job is either scheduled when the source hash still matches or marked `stale` when the source changed before execution. -- A recovered `running` job with no in-process runner ownership is converted back to `pending` only after orphaned non-terminal chunks are normalized, unless source hash has changed, in which case the job becomes `stale`. -- Orphaned chunk recovery handles `running`, `validating`, and `retrying` explicitly. `running` or `validating` chunks are treated as abandoned attempts with safe `stream_disconnected` diagnostics: if `retry_count < BRILLIANT_MAX_RETRIES`, set the chunk to `retrying` without incrementing `retry_count`; the next normal retry attempt consumes the retry budget and increments once. If retry budget is already exhausted, mark the chunk `failed` and mark the job `failed`. Existing `retrying` chunks keep their current `retry_count` and remain retryable; recovery must not increment them. -- A recovered job containing retryable `retrying` chunks must be scheduled normally after the job is converted back to `pending`; otherwise recovery would leave retryable chunks stranded. -- A recovered `stitching` job re-runs final stitching from completed/purged chunk metadata when possible; if required completed chunk bodies have already been purged before a committed final output exists, mark the job failed with `filesystem_failure` and safe diagnostics. -- A recovered `committing` job delegates to the atomic commit recovery contract: verify final output, complete and purge when safe, or fail/stale/unavailable according to `contracts/07-atomic-commit-purge-recovery.md`. -- A server restart may pause a job, but must not corrupt an existing completed translation. -- A recovered `pending` or `running` job whose source hash still matches must re-check Codex auth/setup before continuing execution. If auth/setup is now missing, mark the existing job failed with `auth_required` or `setup_required` and emit a safe failure event/snapshot instead of returning an indefinitely running job. -- A recovered `cancel_requested` job with no resumable in-flight `threadId` and `turnId` is finalized as `canceled`; late Codex output is ignored if it appears after restart. This prevents `cancel_requested` from blocking future retries through the active unique index indefinitely. -- In-flight Codex `threadId` and `turnId` live only in the in-process runner registry for Brilliant MVP. Do not add SQLite columns for them. Restart recovery treats missing in-memory ids as non-resumable for cancellation. - -## State transition contract - -Use the exact job and chunk transition values from `contracts/02-types-state-and-settings.md`. Do not introduce aliases such as `success`, `completed`, or `in_progress`. - -Rules: - -- A job cannot become `complete` before atomic commit and chunk purge finish. -- A chunk cannot become `purged` unless `translated_markdown IS NULL` and `translated_hash` remains available. -- A complete job becomes `unavailable` only when its committed output file is missing or its file hash differs from `translation_jobs.output_hash`. -- `unavailable` is terminal history state. Runner recovery and scheduling must treat it like `failed`, `canceled`, and `stale`: it is not active, not resumed, and does not block a new job. -- `repairUnavailableTranslation()` is implemented here for job-start, metadata API, and job status/snapshot repair. It calls the repository `markTranslationUnavailable()` method with `output_missing` or `output_hash_mismatch`. 19.11 may add recovery callers, but it must reuse this helper rather than reimplementing unavailable repair. -- Late Codex output for canceled jobs is ignored. -- Source hash is checked at job start and again before commit. -- `stale` is terminal for a job attempt and emits `translation.job.stale`, not `translation.job.failed`. - -## Tests - -Cover: - -- source loader computes `sha256:` -- missing source content returns a typed missing-source error -- job start uses SQLite settings language when request body omits `lang_code` -- request language mismatch returns `translation_language_mismatch` -- missing settings language returns `translation_language_required` -- current completed translation is returned without requiring current Codex auth -- active job reuse returns `status = "active"`, actual `job_status`, and `event_url` without creating another row -- `cancel_requested` compatible job returns `cancellation_conflict` until cancellation reaches `canceled` -- missing Codex auth/setup returns `auth_required` or `setup_required` before creating a new job row -- in-flight auth/setup loss after job creation persists `auth_required` or `setup_required` as the job failure code -- current completed job is reused -- completed job with mismatched `output_hash` is not returned as current -- completed job with missing or hash-mismatched output is marked unavailable and does not block a new job -- `repairUnavailableTranslation()` marks unavailable through the shared helper used by metadata API, job status/snapshot API, and later recovery code -- unavailable jobs are not scheduled or resumed by runner recovery -- active non-terminal job is reused -- active job reuse returns job metadata with `event_url` -- failed/canceled/stale job does not block a user retry job -- runner schedules a newly created pending job without blocking the request -- duplicate route calls do not enqueue the same job id more than once -- runner uses atomic `pending -> running` claim and does not execute a job when claim fails -- runner recovery schedules an interrupted pending job when the source hash still matches -- runner recovery marks an interrupted pending job stale when the source hash changed -- runner recovery converts orphaned `running` jobs back to `pending` or `stale` -- runner recovery delegates `stitching` and `committing` to final-output recovery instead of claiming them through `claimTranslationJob()` -- job start runs focused recovery before active lookup or new job creation -- recovered pending/running job with missing Codex auth/setup becomes failed with `auth_required` or `setup_required` -- runner recovery handles interrupted active jobs -- runner recovery finalizes non-resumable `cancel_requested` jobs as `canceled` -- stale source hash prevents commit -- stale source hash emits `translation.job.stale` -- invalid state transitions are rejected -- canceled jobs ignore late chunk output - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/source-loader.test.ts -mise exec -- bun run test tests/server/translation/job-state.test.ts -mise exec -- bun run test tests/server/translation/orchestrator.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Job and chunk states are centralized in shared types. -- Job start is idempotent. -- Translation language comes from SQLite settings. -- Source freshness is enforced. -- Later chunking and Codex subtasks can plug into the orchestrator and runner without redefining state. diff --git a/docs/workflows/archive/task-19-codex-translation/04-markdown-block-manifest-and-chunker.md b/docs/workflows/archive/task-19-codex-translation/04-markdown-block-manifest-and-chunker.md deleted file mode 100644 index 2f0f722e..00000000 --- a/docs/workflows/archive/task-19-codex-translation/04-markdown-block-manifest-and-chunker.md +++ /dev/null @@ -1,151 +0,0 @@ -# 19.4 Markdown block manifest and chunker - -## Goal - -Parse source Markdown into deterministic chunks with parser-backed text segments so long articles and academic papers can be translated completely without asking Codex to rewrite Markdown syntax. - -## Files likely owned - -- `src/server/translation/markdown-blocks.ts` -- `src/server/translation/markdown-parser.ts` -- `src/server/translation/translation-segments.ts` -- `src/server/translation/chunker.ts` -- `tests/server/translation/markdown-blocks.test.ts` -- `tests/server/translation/markdown-parser.test.ts` -- `tests/server/translation/translation-segments.test.ts` -- `tests/server/translation/chunker.test.ts` -- `tests/fixtures/translation/markdown-protected-spans.md` -- `tests/fixtures/translation/markdown-segment-matrix.md` -- `tests/fixtures/translation/academic-paper.md` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/05-markdown-chunking.md` - -## Instruction alignment - -Scope: deterministic chunk construction, parser-backed segment manifest creation, and legacy block compatibility only. - -Inputs: source Markdown body, frontmatter parser behaviour, required block types, and chunk config defaults. - -Outputs: ordered block ids for chunk grouping, ordered segment ids and source text for Codex prompts, protected structure ranges, section paths, and contiguous chunk Markdown. - -Dependencies: 19.1 freezes block id format and 19.3 supplies source snapshots. - -Parallelization notes: can run in parallel with Codex client work after shared translation types are frozen. - -Implementation risks: growing an ad hoc regex parser to cover the full Markdown dialect will remain fragile. Markdown parsing is parser-backed. The implementation must not attempt to cover the Markdown dialect through ad hoc regex scanning. Line-oriented fallback logic may exist only for controlled diagnostics or migration compatibility. - -## Block manifest contract - -Generate blocks with ids in source order: - -```text -b000001 -b000002 -b000003 -``` - -Required block types: - -- heading -- paragraph -- list -- blockquote -- table -- code fence -- inline-code-bearing paragraph -- math block -- HTML block -- image or figure reference -- caption -- footnote -- bibliography/reference entry -- unknown/raw block - -Rules: - -- Frontmatter is metadata and is not a translatable block. -- Do not split inside code fences, math blocks, HTML blocks, tables, citation markers, footnote markers, or Markdown links. -- `unknown_raw` is a safety fallback, not a default classification. - -## Protected span contract - -The legacy block parser may still extract protected spans as guardrails, but segment extraction and structural validation are parser-backed. Extract protected ranges for: - -- code fences -- inline code -- math delimiters and content -- HTML tags and attributes -- URLs -- Markdown link destinations -- citation markers -- footnote markers -- identifiers -- file paths -- commands -- placeholders - -These spans are legacy diagnostics and additional guardrails. They are not the primary correctness mechanism for Task 19U; parser-backed segment reassembly and structural fingerprints are. - -## Chunking contract - -Use defaults from `contracts/05-markdown-chunking.md`: - -- `maxRoughTokens: 2500` -- `softRoughTokens: 1800` -- `maxBlocks: 80` -- `maxRetries: 3` -- `minLengthRatio: 0.35` -- `maxLengthRatio: 2.8` - -Rules: - -- Prefer section boundaries. -- Split oversized sections by contiguous block groups. -- Group adjacent small sections when safe. -- Preserve document order. -- Never split inside a block. -- Keep one oversized block whole and flag it rather than slicing it. - -## Tests - -Cover: - -- deterministic block ids for the same source -- frontmatter excluded from translation blocks -- every required block type classification -- protected spans extracted for code, math, links, URLs, citations, footnotes, HTML, commands, paths, identifiers, and placeholders -- section path updates when headings change -- table remains one block -- code fence remains one block -- math block remains one block -- chunking groups small sections -- chunking splits oversized sections by block groups -- chunks include ordered text segment ids and source text -- autolinks are excluded from translatable segments -- inline HTML tags are protected while surrounding prose remains translatable -- table and footnote prose produce segments without exposing identifiers as translatable text -- oversized single block is preserved and flagged - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/markdown-blocks.test.ts -mise exec -- bun run test tests/server/translation/markdown-parser.test.ts -mise exec -- bun run test tests/server/translation/translation-segments.test.ts -mise exec -- bun run test tests/server/translation/chunker.test.ts -mise exec -- bun run typecheck -``` - -`maxRetries: 3` means one initial attempt plus up to three retry attempts for a -maximum of four total attempts per chunk. - -## Acceptance criteria - -- Chunking is deterministic. -- Chunks preserve document order. -- Chunks expose deterministic segment ids in source order. -- Long papers can be split without relying on one Codex context window. -- Later prompt, validation, retry, and stitching subtasks can rely on stable segment ids and reassembled chunk Markdown. diff --git a/docs/workflows/archive/task-19-codex-translation/05-codex-app-server-integration.md b/docs/workflows/archive/task-19-codex-translation/05-codex-app-server-integration.md deleted file mode 100644 index 1330ac22..00000000 --- a/docs/workflows/archive/task-19-codex-translation/05-codex-app-server-integration.md +++ /dev/null @@ -1,215 +0,0 @@ -# 19.5 Codex app-server integration - -## Goal - -Implement the backend-only Codex app-server client used by Brilliant. This subtask does not create UI or write translated files. - -## Files likely owned - -- `src/server/translation/codex-app-server.ts` -- `tests/server/translation/codex-app-server.test.ts` -- optional `tests/server/translation/fakes/fake-codex-app-server.ts` - -## Contract references - -- `contracts/04-api-and-sse.md` -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: backend-only Codex app-server client, auth probe, thread/turn creation, notification parsing, and cancellation primitive. - -Inputs: configured app-server transport, Codex app-server wire protocol, output schema from 19.8, and chunk payloads from the orchestrator. - -Outputs: fakeable `CodexAppServerClient`, typed events, typed errors, and app-server-safe auth/device-code adapters. - -Dependencies: 19.1 freezes app-server boundary; 19.8 owns prompt/schema content. - -Parallelization notes: can run with 19.6 and 19.7 after app-server wire types are frozen; avoid editing frontend or final file writing. - -Implementation risks: treating app-server as REST or skipping `initialize` will fail against the real protocol; exposing app-server details to the browser violates security requirements. - -## Client contract - -Implement a server-only client with these operations: - -```ts -checkAuth(): Promise -startDeviceCodeLogin(): Promise -observeAuthEvents(): AsyncIterable -cancelDeviceCodeLogin(input: { loginId: string }): Promise -logout(): Promise -listModels(): Promise -translateChunk(input: CodexTranslateChunkInput): AsyncIterable -cancelTurn(input: { threadId: string; turnId: string }): Promise -``` - -## Connection contract - -The MVP connects to an already-running Codex app-server. It does not auto-start or supervise the app-server process. - -Rules: - -- Read app-server endpoint from `TRAUMA_CODEX_APP_SERVER_ENDPOINT` or the equivalent typed TRAUMA config value. -- Default Brilliant MVP endpoint is `unix:///tmp/trauma-codex.sock`, but only when the operator started Codex with `codex app-server --listen unix:///tmp/trauma-codex.sock`. -- Default local app-server startup command is `codex app-server --listen unix:///tmp/trauma-codex.sock`. -- Unix socket endpoint URIs use three slashes and an absolute filesystem path, for example `unix:///tmp/trauma-codex.sock`. -- `codex app-server` without `--listen unix:///tmp/trauma-codex.sock` uses the Codex CLI `stdio` default and is not a Brilliant endpoint. -- Support Unix socket as the only app-server wire-protocol transport. -- Reject loopback WebSocket endpoints; TRAUMA does not support `ws://` app-server transports. -- HTTP is health-probe-only and must not be used for app-server wire-protocol requests. -- Reject `http://` and `https://` endpoints for app-server wire-protocol calls. -- Reject `stdio` configuration because TRAUMA does not own app-server process startup or supervision in Brilliant. -- Reject all WebSocket endpoints until a separate transport design explicitly adds authenticated remote listener support. -- Speak the Codex app-server wire protocol over the configured app-server transport; do not implement app-server calls as REST fetches to `turn/start`-style URLs. -- Use the Codex app-server wire envelope exactly: requests are `{ method, params, id }`, responses are `{ id, result }` or `{ id, error }`, and notifications are `{ method, params }`. Do not add a top-level `jsonrpc` field unless the generated schema or focused fixtures for the installed Codex version explicitly accept it. -- After transport connection opens, send `initialize` with TRAUMA client metadata and then send `initialized`. -- Brilliant defaults to the stable app-server schema and does not request - `experimentalApi`. Do not send request fields that appear only in the - generated `--experimental` schema. -- Reject or retry connection setup if any request is attempted before initialization. -- Missing endpoint returns `setup_required`. -- Configured but unreachable app-server returns `app_server_unavailable`. -- Health/auth probe uses `account/read` before scheduling translation work. -- Device-code completion and account update notifications are converted to typed auth events for settings/auth services. -- No fallback to `codex exec` exists in this client. -- Request timeout and health timeout are explicit config values or documented defaults. - -Protocol schema rules: - -- Record the local Codex CLI/app-server version used to implement the client. -- Prefer generated Codex app-server TypeScript or JSON Schema artifacts from `codex app-server generate-ts` or `codex app-server generate-json-schema` for fake app-server tests. -- If generated artifacts are too large, add focused protocol fixtures for the app-server methods and notifications Brilliant consumes. -- Hand-written event types must be checked against the generated schema or focused fixture version. -- Before implementing the production transport, run a focused Unix socket adapter - spike. Confirm how Bun/Node connects to Codex app-server's - `unix:///tmp/trauma-codex.sock` endpoint, which uses a Unix domain socket plus - HTTP Upgrade/WebSocket framing, and define how the configured absolute socket - path maps to the app-server control socket. -- If Bun/Node cannot support the Unix socket WebSocket upgrade, document the - blocker in the subtask handoff. Do not fall back to a `ws://` endpoint. - -Rules: - -- Use Codex app-server `thread/start` to create one ephemeral thread per chunk attempt, then `turn/start` for chunk translation. -- Use Codex app-server `model/list` as the backend-only source of truth for - available translation models and supported reasoning efforts. Browser code - must receive only TRAUMA-normalized catalog data through settings-scoped API - routes. -- `turn/start` must include the locked-down Brilliant translation turn policy from `contracts/06-codex-prompt-and-validation.md`. -- `thread/start` must also receive the locked-down Brilliant policy when supported by the generated schema. If `turn/start` is the only method that accepts the exact sandbox fields, document that the turn payload overrides broader thread defaults before implementation. -- Stable `thread/start` sends only the fields supported by the stable generated - schema for Brilliant: `cwd`, `ephemeral`, `approvalPolicy`, - `approvalsReviewer`, `sandbox`, and `threadSource`. It must omit - `environments`, `experimentalRawEvents`, and `persistExtendedHistory` unless - a later task deliberately opts into `experimentalApi`. -- Stable `turn/start` sends `threadId`, `input`, `approvalPolicy`, - `approvalsReviewer`, `sandboxPolicy`, and `outputSchema` when structured - output is attempted. It sends the selected model as `model` and selected - reasoning effort as `effort` only when the job metadata is non-null. It must - omit `environments` unless a later task deliberately opts into - `experimentalApi`. -- Runtime `cwd` comes from a job-scoped empty directory under `TRAUMA_CODEX_RUNTIME_DIR` or OS temp `trauma-codex-runtime/`; never use the TRAUMA project root or memory store path as `cwd`. -- `networkAccess = false` applies to sandboxed agent/tool execution only. It must not block the backend from connecting to app-server or app-server from contacting Codex/OpenAI services required for translation. -- Accept an `outputSchema` from caller code and pass it to app-server when supported. -- If `outputSchema` is unsupported or rejected, switch to prompt-only JSON response mode. The returned JSON must still pass `CodexChunkOutput` validation before persistence. This is output-mode negotiation, not chunk validation retry; it must not increment `retry_count`, must not consume `maxRetries`, and should be cached per app-server client/job when possible. -- Prefer probing or caching output-mode support before chunk translation starts. Once a job/client determines prompt-only JSON mode is required, use that mode for later chunk attempts without first sending rejected `outputSchema` payloads again. -- If `outputSchema` is rejected after a chunk attempt thread has already been created, discard that thread and start a fresh ephemeral thread in prompt-only JSON mode. This fresh thread is still the same chunk attempt for retry accounting and must not increment `retry_count`. -- If both structured-output mode and prompt-only JSON mode are unavailable or rejected, mark the chunk/job with `invalid_final_output`. -- Do not define the Brilliant translation output schema in this module; schema construction is owned by 19.8. -- Use one ephemeral Codex thread per chunk attempt by default. -- Do not expose app-server URL, auth state, or connection details to frontend code. -- Do not allow Codex to write canonical `CONTENT.md` files. -- Streamed app-server deltas are progress only. -- Final output must come from completed app-server item content. -- `translateChunk()` yields `thread.started` and `turn.started` when ids are available so the orchestrator can cancel the in-flight turn. -- Cancellation uses `turn/interrupt` with both `threadId` and `turnId`. -- In-flight `threadId` and `turnId` are kept in the local runner registry only. The app-server client does not require SQLite columns for these ids in Brilliant MVP. -- Raw app-server notifications are parsed only in this module and converted into typed `CodexAppServerEvent` values before reaching orchestrator or SSE code. -- Auth app-server notifications are parsed only in this module and converted into typed `CodexAuthEvent` values before reaching settings/auth services. - -## Error contract - -Map app-server failures to typed backend errors: - -- `auth_required` -- `setup_required` -- `app_server_unavailable` -- `app_server_protocol_error` -- `usage_limit` -- `context_overflow` -- `stream_disconnected` -- `timeout` -- `invalid_final_output` -- `unknown` - -## Tests - -Use a fake app-server client. Cover: - -- auth check success and auth-required failure -- missing app-server endpoint returns setup-required -- default Unix socket endpoint config is accepted only when paired with the explicit `codex app-server --listen unix:///tmp/trauma-codex.sock` startup requirement -- loopback WebSocket config is rejected -- Unix socket app-server wire transport is accepted -- WebSocket app-server wire transport is rejected -- HTTP endpoints are rejected for app-server wire-protocol calls -- all WebSocket endpoints are rejected for Brilliant MVP -- `stdio` transport configuration is rejected as unsupported for Brilliant MVP -- unreachable app-server returns app-server-unavailable -- app-server `initialize` and `initialized` happen before `account/read`, `thread/start`, or `turn/start` -- generated schema or focused protocol fixtures cover the app-server methods and notifications used by the fake app-server -- fake app-server fixtures omit top-level `jsonrpc` unless generated schema proves it is accepted -- `account/read` auth checks treat a non-null `account` as enabled even when - `requiresOpenaiAuth` is true; `requiresOpenaiAuth: true` is auth-required only - when no account is available -- device-code login response is safe to return to settings UI -- device-code cancel wraps `account/login/cancel` and requires a known `loginId` -- logout wraps `account/logout` when supported and reports unsupported logout explicitly -- raw `account/login/completed` and `account/updated` notifications map to - typed `CodexAuthEvent` values including login success, failure, and - cancellation -- auth check uses `account/read` -- chunk translation starts an ephemeral thread before starting a turn -- retry attempts start a fresh ephemeral thread and do not reuse the prior failed attempt's thread -- `turn/start` request passes through the caller-provided output schema -- `model/list` response parsing normalizes visible models and supported - reasoning effort objects into the frontend-safe catalog shape -- `turn/start` request passes through selected model and reasoning effort using - the generated stable schema field names `model` and `effort` -- output-schema rejection falls back to prompt-only JSON mode without incrementing `retry_count` -- output-schema fallback after thread creation discards the rejected thread and starts a fresh thread without consuming retry budget -- `turn/start` request includes locked-down approval, sandbox, network, and cwd settings -- `turn/start` locked-down policy is verified against generated schema or focused fixtures before implementation -- `thread/start` request includes the same locked-down policy where the generated schema supports it, or tests document that `turn/start` overrides thread defaults -- `thread/start` and `turn/start` use a job-scoped empty runtime `cwd`, never project root or store root -- network-disabled sandbox policy does not disable required app-server/model traffic -- rejected `outputSchema` falls back to prompt-only JSON output and still validates `CodexChunkOutput` -- `translateChunk()` yields thread id and turn id before item events when available -- `cancelTurn()` sends `turn/interrupt` with thread id and turn id -- in-flight thread/turn ids are exposed to the runner for cancellation but are not persisted in SQLite -- chunk translation uses ephemeral chunk scope -- delta event is yielded as non-final progress -- completed item content is yielded separately from deltas -- usage limit, context overflow, timeout, and disconnect are typed -- reachable app-server request-contract rejections, including - `requires experimentalApi capability`, are typed as - `app_server_protocol_error` instead of `app_server_unavailable` -- raw app-server notifications are converted to typed internal events inside the app-server client module -- no token, credential file content, or app-server secret is returned - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/codex-app-server.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Codex app-server integration is isolated behind one backend module. -- Prompt and output-schema construction remain owned by 19.8. -- App-server startup is outside MVP scope; connection is through server-side URL config. -- Frontend code cannot call app-server directly. -- The client can be faked for deterministic tests. -- No canonical file writes happen inside the Codex client. diff --git a/docs/workflows/archive/task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md b/docs/workflows/archive/task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md deleted file mode 100644 index 259c956e..00000000 --- a/docs/workflows/archive/task-19-codex-translation/06-codex-auth-and-device-code-setup-flow.md +++ /dev/null @@ -1,182 +0,0 @@ -# 19.6 Codex auth and device-code setup flow - -## Goal - -Expose Codex managed ChatGPT sign-in state through TRAUMA settings without TRAUMA owning credential material. - -## Files likely owned - -- `src/server/settings/codex-auth.ts` -- current settings API routes under `src/routes/api/settings*` -- `src/components/settings/SettingsPage.tsx` -- `tests/server/settings/codex-auth.test.ts` -- `tests/components/settings-codex-auth.test.tsx` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/04-api-and-sse.md` -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: Codex managed ChatGPT sign-in state surfaced through settings without TRAUMA owning credential material. - -Inputs: current settings UI/API, `CodexAppServerClient`, `account/read`, `account/login/start`, login notifications, and logout support. - -Outputs: settings-visible auth status, safe device-code setup metadata, login cancellation behaviour, and logout/delete semantics. - -Dependencies: 19.5 provides app-server auth methods; current settings routes provide the UI/API surface. - -Parallelization notes: can run beside 19.5 only after shared auth types are frozen; do not edit translation chunking or persistence. - -Implementation risks: storing ChatGPT tokens, printing credential paths, or faking enabled state would violate the instruction's auth boundary. - -## Auth boundary contract - -Rules: - -- Codex owns ChatGPT credentials through its managed auth flow. -- TRAUMA stores only non-secret auth metadata and status. -- TRAUMA does not read, parse, print, copy, or back up Codex credential files. -- Frontend responses never include access tokens, refresh tokens, auth file contents, or sensitive credential paths. -- Auth state and `translation_target_lang_code` may share the settings page but must remain separate values. - -## Device-code setup contract - -If auth is missing and app-server supports `chatgptDeviceCode`, return safe setup metadata for the settings UI. The UI can show user code and verification URL if provided by Codex app-server. - -Do not invent a direct ChatGPT OAuth URL flow outside Codex app-server. - -App-server methods: - -- Read status through `CodexAppServerClient.checkAuth()`, backed by - `account/read`. -- Treat a non-null `account` in the `account/read` response as confirmed - authentication. `requiresOpenaiAuth` describes whether the selected provider - requires OpenAI auth; it is not a standalone unauthenticated flag. Only - `requiresOpenaiAuth: true` with no account is auth-required. -- Start device-code login through `CodexAppServerClient.startDeviceCodeLogin()`, backed by `account/login/start` and `{ "type": "chatgptDeviceCode" }`. -- Return only `loginId`, `verificationUrl`, and `userCode` to the frontend. -- Treat typed `account.login.completed` and `account.updated` events from - `CodexAppServerClient.observeAuthEvents()` as setup progress, then confirm - enabled state with `checkAuth()`. -- Raw app-server notification names are `account/login/completed` and - `account/updated`; the client adapter may expose them as dot-named typed - internal events, but docs and tests must not refer to stale `auth/*` raw - methods. -- `account.login.completed` includes `loginId`, `success`, and safe `error` - text. `success: false` ends the pending login flow and returns a safe failed - or canceled state; it must not mark auth enabled. -- Cancel a pending login through `CodexAppServerClient.cancelDeviceCodeLogin({ loginId })` when the user abandons setup and `loginId` is known. -- Delete auth through `CodexAppServerClient.logout()`, which wraps app-server `account/logout` when supported. - -The settings/auth service must not subscribe to raw JSON-RPC notifications or call app-server methods directly. Raw JSON-RPC payloads are adapted only inside `src/server/translation/codex-app-server.ts`. - -Backend API shape: - -- `GET /api/settings/codex-auth` -- `POST /api/settings/codex-auth/device-code` -- `POST /api/settings/codex-auth/device-code/cancel` -- `DELETE /api/settings/codex-auth` - -If existing Task 18 settings UI or routes still use `OpenAI Auth` or -`openai-auth` naming for compatibility, those routes delegate to the same -`codex-auth` service and return the same response shapes. - -Status response: - -```json -{ - "status": "enabled", - "provider": "codex", - "message": "Codex ChatGPT sign-in is enabled." -} -``` - -Setup-required response: - -```json -{ - "status": "setup_required", - "provider": "codex", - "reason": "codex_app_server_unavailable" -} -``` - -Device-code start response: - -```json -{ - "status": "login_started", - "provider": "codex", - "loginId": "login_018f...", - "verificationUrl": "https://...", - "userCode": "ABCD-EFGH" -} -``` - -Rules: - -- Server stores only non-secret pending-login metadata needed to correlate `loginId` and cancellation. -- Frontend never receives tokens, credential paths, app-server URL, app-server transport details, or raw account payloads. -- While login is pending, refreshes call `account/read` and return confirmed enabled state if available. -- If login is still pending and the server has known pending metadata, refresh returns non-secret pending state with `status: "login_started"`, `loginId`, `verificationUrl`, and `userCode`. -- If login is still pending but pending metadata was lost, refresh returns latest confirmed `account/read` state instead of inventing a pending login. -- `account.login.completed` and `account.updated` typed events are progress - signals only; enabled state is set only after a successful `checkAuth()`. -- `account.login.completed` with `success: false` clears the pending observer - and returns a safe failure/cancellation status without exposing raw app-server - payloads. -- Start `CodexAppServerClient.observeAuthEvents()` only while a device-code login is pending. -- Correlate completion with pending `loginId` when the event includes one; - otherwise treat `account.updated` as a prompt to call `checkAuth()` and - confirm state. -- Stop the observer when login completes, is canceled, fails, times out, or the process/request scope is disposed. -- Server restart or listener loss does not expose secrets and does not fake failure. `GET /api/settings/codex-auth` calls `checkAuth()` and returns confirmed enabled state, safe pending metadata if still known, or setup-required/unknown state. -- Multiple concurrent login starts for the same user/session reuse the existing pending login metadata instead of creating duplicate observers. - -## Enable/delete behaviour - -- Enable does not mark auth as enabled until the server verifies Codex can run an authenticated operation. -- If provider setup is missing, return a normal `setup_required` or `not_configured` response rather than a fake enabled state. -- Already-enabled enable requests are idempotent. -- Delete auth calls app-server `account/logout` when supported. If app-server logout is unavailable, delete only TRAUMA-owned metadata and explicitly report that Codex-owned credentials were not removed. - -## Tests - -Cover: - -- auth status disabled, enabled, setup-required, unknown, and error -- enable verifies server-side auth before marking enabled -- provider-missing enable does not fake enabled state -- already-enabled enable is idempotent -- device-code response contains only safe fields -- device-code response includes `loginId`, `verificationUrl`, and `userCode` -- device-code login completion waits for app-server notifications and confirms with `account/read` -- device-code login failure or cancellation from - `account.login.completed.success = false` clears pending state and does not - mark auth enabled -- auth service consumes typed `CodexAuthEvent` values, not raw JSON-RPC notification names -- auth observer is active only during pending login and is cleaned up on completion, cancel, failure, timeout, or scope disposal -- listener loss/restart falls back to `checkAuth()` plus safe pending metadata -- pending device-code status refresh returns safe pending metadata when known and confirmed `account/read` state otherwise -- login cancel calls `account/login/cancel` only with a known `loginId` -- logout uses `account/logout` when supported -- delete auth does not delete arbitrary `~/.codex` files -- no secret material is returned to frontend - -## Verification - -```sh -mise exec -- bun run test tests/server/settings/codex-auth.test.ts -mise exec -- bun run test tests/components/settings-codex-auth.test.tsx -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Settings UI can explain Codex setup state. -- Auth verification is server-side. -- TRAUMA does not own raw Codex credentials. -- SQLite-backed target-language settings remain independent from auth metadata. diff --git a/docs/workflows/archive/task-19-codex-translation/07-streaming-event-bridge-to-frontend.md b/docs/workflows/archive/task-19-codex-translation/07-streaming-event-bridge-to-frontend.md deleted file mode 100644 index 16e89c92..00000000 --- a/docs/workflows/archive/task-19-codex-translation/07-streaming-event-bridge-to-frontend.md +++ /dev/null @@ -1,143 +0,0 @@ -# 19.7 Streaming event bridge to frontend - -## Goal - -Expose Brilliant job progress to the browser through SSE. This subtask does not implement reader controls or Codex prompt validation. - -## Files likely owned - -- `src/server/translation/events.ts` -- route file implementing `GET /api/translation-jobs/:job_id/events`, following existing `src/routes/api/` conventions -- `tests/server/translation/events.test.ts` -- `tests/server/routes/api-translation-events.test.ts` - -## Contract references - -- `contracts/04-api-and-sse.md` -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: Reader backend SSE progress stream and mapping from backend/Codex events to frontend events. - -Inputs: translation job state, chunk progress, Codex app-server notifications, and terminal job metadata. - -Outputs: `text/event-stream` route, stable event envelope, reconnect snapshot, and completion payload with `reader_url`. - -Dependencies: 19.3 defines job state, 19.5 defines app-server event types, and 19.10 defines final output metadata. - -Parallelization notes: can run beside frontend progress UI after event envelope is frozen; do not persist streamed deltas as completed output. - -Implementation risks: treating partial deltas as authoritative or omitting reconnect snapshots breaks the instruction's streaming and persistence boundaries. - -## SSE contract - -Endpoint: - -```http -GET /api/translation-jobs/:job_id/events -``` - -Rules: - -- Response content type is `text/event-stream`. -- Event ids are monotonic decimal strings padded to 12 digits per job. -- Event name equals `TranslationEventEnvelope.type`. -- Event data is the JSON envelope. -- Send heartbeat comments every 15 seconds while active. -- Close stream after `translation.job.completed`, `translation.job.failed`, `translation.job.stale`, or `translation.job.canceled`. -- Disconnecting the SSE client does not cancel the backend job. - -## Reconnect contract - -The MVP does not require a durable event replay table. On reconnect, send `translation.job.snapshot` using current SQLite job/chunk state before new events. - -`Last-Event-ID` may be supported later if an in-memory or SQLite event buffer is added. - -The frontend must be able to combine `GET /api/translation-jobs/:job_id` and SSE to recover after refresh. - -Snapshot payload: - -- `translation.job.snapshot` uses the same payload shape as `GET /api/translation-jobs/:job_id`. -- The payload includes `job_id`, `memory_id`, `lang_code`, `status`, `source_hash`, `chunk_count`, `completed_chunks`, `failed_chunks`, `retrying_chunks`, `output_path`, `reader_url`, and `error`. -- `completed_chunks` follows the public aggregation rule from the API contract: chunks with status `complete` or `purged` count as completed. - -## Event mapping contract - -Map backend and Codex events to: - -- `translation.job.started` -- `translation.chunk.queued` -- `translation.chunk.started` -- `translation.codex.delta` -- `translation.codex.item.started` -- `translation.codex.item.completed` -- `translation.chunk.validating` -- `translation.chunk.completed` -- `translation.chunk.failed` -- `translation.chunk.retrying` -- `translation.job.snapshot` -- `translation.job.stitching` -- `translation.job.committing` -- `translation.job.completed` -- `translation.job.failed` -- `translation.job.stale` -- `translation.job.canceled` - -Typed Codex app-server event mapping: - -- `thread.started` is internal orchestration state unless debugging output is enabled. -- `turn.started` stores `threadId` and `turnId` for cancellation. -- `item.started` maps to `translation.codex.item.started`. -- `item.agentMessage.delta` maps to `translation.codex.delta`. -- `item.completed` maps to `translation.codex.item.completed`. -- `turn.completed` with successful final output is followed by validation events, not immediate persistence. -- `turn.completed` with interrupted status maps to cancellation flow. - -Raw app-server notification names such as `item/agentMessage/delta` are parsed only by `src/server/translation/codex-app-server.ts`. This subtask consumes typed internal events only. - -Completed event payload includes `output_path`, `output_hash`, and `reader_url`. -Failed event payloads include `error` with the same `code`, `message`, and optional `action` shape as `TranslationJobSnapshotError`. Chunk failure payloads also include `retry_count` and `will_retry`. -Stale event payload includes `reason`, `job_source_hash`, and `current_source_hash`. - -`unavailable` is snapshot-only. Do not add `translation.job.unavailable` as an -SSE event in the MVP. If an SSE reconnect snapshot or job status payload has -`status = "unavailable"`, the frontend renders the `translation_unavailable` -recovery UI and does not wait for another terminal event. - -## Tests - -Cover: - -- SSE envelope formatting -- monotonic event ids -- heartbeat output -- terminal events close the stream -- `translation.job.stale` is terminal and closes the stream -- unavailable status is handled through snapshot/job-status payloads, not a dedicated SSE event -- reconnect emits `translation.job.snapshot` before new events -- snapshot payload matches job status API payload -- snapshot `completed_chunks` still includes purged chunks after final commit -- completed event includes `reader_url` -- job failed event includes a safe `error` object with stable `code` -- chunk failed event includes safe `error`, `retry_count`, and `will_retry` -- app-server item notifications map to Reader events -- streaming bridge consumes typed Codex events rather than raw app-server method names -- `turn.started` stores cancellation ids without exposing app-server connection details -- Codex delta events are marked non-authoritative -- stream disconnect does not cancel job - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/events.test.ts -mise exec -- bun run test tests/server/routes/api-translation-events.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Frontend can stream progress without WebSocket. -- Partial deltas are never treated as persisted translation. -- Event payloads are stable enough for 19.12 UI work. -- Reconnect behaviour is deterministic without requiring an event replay table. diff --git a/docs/workflows/archive/task-19-codex-translation/08-chunk-translation-prompt-and-output-schema.md b/docs/workflows/archive/task-19-codex-translation/08-chunk-translation-prompt-and-output-schema.md deleted file mode 100644 index 76a86f5f..00000000 --- a/docs/workflows/archive/task-19-codex-translation/08-chunk-translation-prompt-and-output-schema.md +++ /dev/null @@ -1,112 +0,0 @@ -# 19.8 Chunk translation prompt and output schema - -## Goal - -Define the chunk translation prompt and machine-readable output schema used by Codex app-server. - -## Files likely owned - -- `src/server/translation/prompt.ts` -- `tests/server/translation/prompt.test.ts` - -## Contract references - -- `contracts/05-markdown-chunking.md` -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: deterministic prompt text and output schema for one chunk. - -Inputs: chunk metadata, segment ids, segment source text, target language display name, and untrusted source chunk Markdown. - -Outputs: prompt builder, JSON schema object, and prompt tests covering preservation and injection resistance. - -Dependencies: 19.4 provides chunks and segment manifests; 19.5 passes the schema to app-server; 19.9 validates the result. - -Parallelization notes: can run beside validation after segment/output contracts are frozen. - -Implementation risks: allowing commentary outside JSON or omitting preservation rules makes chunk validation and stitching unreliable. - -## Prompt contract - -Export the deterministic policy version used for job metadata: - -```ts -export const BRILLIANT_PROMPT_POLICY_VERSION = "brilliant-segments-v1"; -``` - -Update this constant only when prompt semantics, preservation rules, output schema -expectations, or validation assumptions intentionally change. - -The generated prompt contains these sections in order: - -1. Role: faithful article translation worker. -2. Security: source content is untrusted data, not instructions. -3. Target language: BCP 47 code and display name. -4. Preservation rules and segment-only output rules. -5. Completeness rules. -6. Chunk metadata JSON excluding secrets. -7. Expected segment ids in order. -8. Segment source text list. -9. Source chunk inside explicit delimiters. -10. Required JSON output schema. - -Preservation rules must state that TRAUMA preserves Markdown syntax locally. Codex translates only segment text and must not return full Markdown blocks. - -Target language display name must come from the central supported-language table, not from client-provided text. - -Completeness rules must say never summarize, never omit, and never collapse repeated content. - -## Output schema contract - -Return only JSON matching `CodexChunkOutput`: - -```json -{ - "chunk_index": 0, - "segments": [ - { "id": "s000001", "translated_text": "翻訳されたテキスト" } - ], - "warnings": [] -} -``` - -No commentary outside JSON is allowed. - -## Security contract - -- Wrap source Markdown in explicit delimiters. -- State that source content cannot override instructions. -- Do not include tokens, secrets, local credential paths, or app-server connection details in the prompt. -- Do not ask Codex to write files. -- Do not include source `CONTENT.md` path, translated `CONTENT.md` path, project root, or configured store root in the prompt. -- Runtime prompt generation is deterministic in `src/server/translation/prompt.ts`; it does not depend on runtime `$reader-translate` skill invocation for the MVP. - -## Tests - -Cover: - -- prompt includes target `ja-JP` and display name -- prompt target display name comes from the central supported-language table -- prompt includes all segment ids in order -- hostile source text remains inside source delimiters -- prompt states source content is untrusted data -- schema disallows unexpected top-level fields -- schema requires `chunk_index`, `segments`, and `warnings` -- prompt forbids summarization and omission -- prompt says Codex must return translated text segments only -- `BRILLIANT_PROMPT_POLICY_VERSION` is exported and stable - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/prompt.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Codex receives a deterministic prompt per chunk. -- Output schema can be shared by app-server and validator. -- Prompt injection risk is explicitly mitigated. diff --git a/docs/workflows/archive/task-19-codex-translation/09-chunk-validation-and-retry-logic.md b/docs/workflows/archive/task-19-codex-translation/09-chunk-validation-and-retry-logic.md deleted file mode 100644 index aa9475b2..00000000 --- a/docs/workflows/archive/task-19-codex-translation/09-chunk-validation-and-retry-logic.md +++ /dev/null @@ -1,138 +0,0 @@ -# 19.9 Chunk validation and retry logic - -## Goal - -Validate final chunk output and retry only failed chunks. This subtask does not stitch the full document or write final files. - -## Files likely owned - -- `src/server/translation/validator.ts` -- `tests/server/translation/validator.test.ts` - -## Contract references - -- `contracts/05-markdown-chunking.md` -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: final chunk output validation and chunk-level retry decisions. - -Inputs: expected chunk manifest, segment ids, Codex final item output, retry count, and chunk config thresholds. - -Outputs: structured validation errors, retry prompt context, chunk status updates, and retry exhaustion behaviour. - -Dependencies: 19.4 provides expected segments; 19.8 defines output schema; 19.3 owns job/chunk state transitions. - -Parallelization notes: can run beside prompt work after schema names are frozen; do not stitch full documents here. - -Implementation risks: retrying the whole document or accepting missing segment ids violates completeness guarantees. - -## Validation contract - -Validate in this order: - -1. JSON parses and matches output schema. -2. `chunk_index` equals requested chunk index. -3. Output segment ids exactly equal input segment ids in the same order. -4. No duplicate segment ids. -5. Each `translated_text` is non-empty. -6. Reassemble translated text into the original source Markdown ranges. -7. Parser-backed structural fingerprint comparison preserves Markdown structure. -8. Code, inline code, math, HTML, URLs, link/image destinations, reference definitions, footnote identifiers, and table shape are unchanged. -9. Prose segment length ratio stays within configured thresholds. - -Error code boundary: - -- `invalid_final_output` means Codex final output cannot be parsed as JSON or - does not match the required `CodexChunkOutput` schema after all configured - output-mode fallbacks are exhausted. -- `validation_failed` means the output is schema-valid `CodexChunkOutput` JSON - but semantic validation fails. -- Store chunk validation failures in `translation_chunks.error` as structured - `TranslationPersistedError` JSON. -- Validation failures may include a `diagnostics` array containing safe, - Reader-generated metadata such as diagnostic kind, chunk index, segment id, - block id, and short expected/actual fingerprint previews. Diagnostics must - not contain raw prompts, raw Codex responses, full source chunks, app-server - endpoints, auth state, tokens, or completed translated article bodies. -- Initial diagnostic kinds are `markdown_structure`, `segment_schema`, and - `segment_length_ratio`. Future validators may use `protected_span` and - `projection` for the same safe metadata envelope. -- The existing `translation_jobs.error` and `translation_chunks.error` JSON - fields carry diagnostics; no attempt-log table or migration is introduced. - -## Retry contract - -- Retry only the failed chunk. -- Start a fresh ephemeral Codex thread for each retry attempt. -- Do not reuse the failed attempt's Codex thread because the thread history may contain invalid output or failed repair context. -- Increment `retry_count` exactly once before starting each retry attempt. -- Runner recovery must not increment `retry_count` when it only normalizes orphaned `running` or `validating` chunks to `retrying`; the next normal retry attempt owns the increment. -- Include structured validation failures in the retry prompt. -- Retry prompts include only Reader-generated structured validation failure summaries and original segment ids, not raw invalid model output beyond the minimal safe excerpts needed for validation diagnostics. -- Retry prompts must not reuse the previous raw model response. The prompt may - include the previous error code and sanitized diagnostics, then restates the - expected segment ids for the fresh attempt. -- When diagnostics show a `source_entry` and `translated_entry`, the retry - prompt instructs Codex to preserve the source entry in the original protected - position and remove the translated entry value from `translated_text`. -- Retry prompts explicitly tell Codex not to repeat protected code, command - flags, identifiers, URLs, file paths, or escaped Markdown punctuation inside - `translated_text`, because TRAUMA reinserts protected Markdown locally. -- Use `BRILLIANT_MAX_RETRIES = 3` from the shared types/settings contract for Brilliant MVP. -- `BRILLIANT_MAX_RETRIES` is the number of retry attempts after the initial attempt, so total attempts are `1 + BRILLIANT_MAX_RETRIES`. -- The initial attempt starts with `retry_count = 0`; increment `retry_count` before each retry attempt starts, and never from recovery-only normalization. -- Job execution must pick up chunks in `pending` or `retrying` status. A `retrying` chunk is not terminal and must not be skipped by the runner. -- After retry exhaustion, mark chunk and job failed. -- `outputSchema` rejection or fallback to prompt-only JSON mode is not a validation retry and does not change `retry_count`. -- If `outputSchema` is rejected after a thread has been created, the app-server client discards that thread and starts a fresh prompt-only thread for the same chunk attempt. -- Retry logic starts only after Codex returns final output for the selected output mode and that output fails parsing/schema/semantic validation. - -## Failure examples to test - -- missing segment id -- duplicate segment id -- reordered segment ids -- lost URL -- lost citation marker -- lost footnote marker -- corrupted code fence -- corrupted math delimiter -- corrupted HTML tag -- changed table row/cell shape -- changed reference definition -- changed footnote identifier -- prose length ratio outside threshold - -## Tests - -Cover: - -- valid chunk passes -- each failure example returns a structured validation error -- JSON parse/schema failures use `invalid_final_output` -- semantic validation failures use `validation_failed` -- chunk error persistence uses structured `TranslationPersistedError` JSON -- chunks with no text segments reassemble unchanged -- retry prompt includes validation errors and original segment ids -- retry starts a fresh ephemeral Codex thread for each attempt -- retry prompt does not depend on prior failed thread history -- output-mode fallback does not increment retry count -- output-mode fallback starts a fresh thread when the rejected mode already created one -- retry count increments -- `maxRetries: 3` allows four total attempts: one initial attempt and three retry attempts -- retry exhaustion marks failed - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/validator.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Validation is deterministic. -- Retry is chunk-level, not full-document. -- Validator preserves the structures needed for academic papers. diff --git a/docs/workflows/archive/task-19-codex-translation/10-stitching-and-atomic-commit.md b/docs/workflows/archive/task-19-codex-translation/10-stitching-and-atomic-commit.md deleted file mode 100644 index 7b41b5b7..00000000 --- a/docs/workflows/archive/task-19-codex-translation/10-stitching-and-atomic-commit.md +++ /dev/null @@ -1,97 +0,0 @@ -# 19.10 Stitching and atomic commit - -## Goal - -Stitch validated translated chunks and atomically commit the final translated `CONTENT.md`. - -## Files likely owned - -- `src/server/translation/stitcher.ts` -- `src/server/translation/atomic-writer.ts` -- `tests/server/translation/stitcher.test.ts` -- `tests/server/translation/atomic-writer.test.ts` - -## Contract references - -- `contracts/03-sqlite-and-repositories.md` -- `contracts/05-markdown-chunking.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: validated chunk stitching, durable projection sidecar emission, and atomic final translated `CONTENT.md` commit. - -Inputs: complete validated chunks, original manifest order, current source hash, and output path contract. - -Outputs: stitched Markdown, same-directory temp file, atomic rename, output hash, projection rows, `TRANSLATION_MAP.json`, and final output metadata. - -Dependencies: 19.9 completes chunk validation; 19.2 provides repository methods; 19.11 purges completed chunk bodies. - -Parallelization notes: can run with purge policy only after final path and transaction boundaries are frozen. - -Implementation risks: writing outside the language directory, skipping source re-hash, or emitting completion before purge can corrupt existing translations. - -## Stitching contract - -- Stitch completed translated chunk Markdown in original chunk order. -- Rebase validated chunk projection spans to final translated document offsets. -- Do not include internal chunk metadata in final Markdown. -- If the source file had frontmatter, prepend the exact original frontmatter unchanged before the stitched translated body. -- If the source file had no frontmatter, do not invent frontmatter. -- Preserve source-level document structure after translation. -- Final validation checks chunk count, incomplete chunks, duplicate sections caused by retries, frontmatter preservation, and Markdown sanity. Segment ids are validated before chunk Markdown is persisted. - -## Atomic write contract - -Final path: - -```text -memories///CONTENT.md -memories///TRANSLATION_MAP.json -``` - -Temporary path: - -```text -memories///.CONTENT..tmp -``` - -Commit sequence follows `contracts/07-atomic-commit-purge-recovery.md` exactly. - -Rules: - -- Re-read source hash before commit. -- Do not mutate source `CONTENT.md`. -- Use same-directory temp file. -- Existing completed translation must remain intact if commit fails. -- Job completion event is emitted only after purge succeeds. - -## Tests - -Cover: - -- stitched output follows manifest order -- source frontmatter is preserved unchanged at the top of translated output -- projection rows and `TRANSLATION_MAP.json` are written for the committed output hash -- translated output without source frontmatter does not invent frontmatter -- incomplete chunk fails final validation -- duplicate or missing chunk index fails final validation -- source hash mismatch marks job stale before write -- temp file is same-directory -- failure before rename leaves existing translation intact -- failure after rename is recoverable -- source `CONTENT.md` is unchanged - -## Verification - -```sh -mise exec -- bun run test tests/server/translation/stitcher.test.ts -mise exec -- bun run test tests/server/translation/atomic-writer.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Final translated file is committed atomically. -- Existing translations are not corrupted by failed jobs. -- Commit is safe for long multi-chunk documents. diff --git a/docs/workflows/archive/task-19-codex-translation/11-sqlite-cleanup-and-purge-policy.md b/docs/workflows/archive/task-19-codex-translation/11-sqlite-cleanup-and-purge-policy.md deleted file mode 100644 index 8726e8e6..00000000 --- a/docs/workflows/archive/task-19-codex-translation/11-sqlite-cleanup-and-purge-policy.md +++ /dev/null @@ -1,105 +0,0 @@ -# 19.11 SQLite cleanup and purge policy - -## Goal - -Ensure SQLite does not retain completed translated chunk bodies after final commit. - -## Files likely owned - -- `src/server/translation/job-state.ts` -- `src/server/translation/orchestrator.ts` -- `src/server/db/repositories.ts` -- optional `src/server/db/translation-repositories.ts` -- `tests/server/translation/job-state.test.ts` -- `tests/server/translation/orchestrator.test.ts` -- `tests/server/db/translation-repositories.test.ts` - -## Contract references - -- `contracts/03-sqlite-and-repositories.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: post-commit SQLite cleanup, completed chunk body purge, and crash recovery for final write states. - -Inputs: job rows, chunk rows, final output file, temp file state, source hash, and output hash. - -Outputs: purged chunk bodies, retained metadata, recovered terminal states, and stale non-complete jobs. - -Dependencies: 19.2 provides schema/repository methods and 19.10 writes final output. - -Parallelization notes: can run beside 19.10 only after commit sequence is frozen; avoid changing Codex client or frontend UI. - -Implementation risks: retaining completed translated chunks in SQLite violates the instruction; reporting complete before purge creates false completion. - -## Purge contract - -After successful final file commit: - -```sql -UPDATE translation_chunks -SET translated_markdown = NULL, - projection_spans_json = NULL, - status = 'purged', - updated_at = ? -WHERE job_id = ? - AND status = 'complete'; -``` - -Rules: - -- Preserve `translated_hash`. -- Preserve durable `translation_projection_spans`; only purge temporary per-chunk `projection_spans_json`. -- Preserve `block_ids_json`. -- Preserve retry count and timestamps. -- Do not emit `translation.job.completed` before purge succeeds. -- Public progress and snapshot aggregation counts purged chunks as completed chunks. -- Final-write temp files are not retained for failed-job debugging. Keep diagnostics in SQLite metadata/logs and delete `.CONTENT..tmp` after failed commit or recovery cleanup. - -## Recovery contract - -Handle these startup or job-resume cases: - -1. Temp file exists, final file absent, job not complete: delete temp and mark failed or retryable. -2. Final file exists, job complete, chunks not purged: purge before reporting complete. -3. Final file exists, job not complete, all chunks complete: verify hash, complete job, purge. -4. Complete job with missing or hash-mismatched final output: call the shared `repairUnavailableTranslation()` helper from `src/server/translation/current-translation.ts`. -5. Source hash changed during interrupted non-complete job: mark stale. - -Completed jobs are immutable history while their final output remains available. If the source changes after completion, do not mutate the completed job to `stale`; reader/API freshness is derived by comparing current source hash with job `source_hash`. If the final output is missing or hash-mismatched, mark the job `unavailable` so the user can retry the same source hash. - -Recovery does not own a second unavailable-repair implementation. It reuses -`repairUnavailableTranslation()` from 19.3 so job start, metadata API, and -startup recovery mark broken completed translations consistently. - -## Tests - -Cover: - -- completed chunks are purged after commit -- purged chunks retain hash and block ids -- failed jobs do not retain `.CONTENT..tmp` final-write files -- recovery purges complete job with unpurged chunks -- recovery handles temp file without final output -- recovery deletes orphan final-write temp files -- recovery handles final file without complete DB status -- recovery marks complete jobs with missing or hash-mismatched final output unavailable -- recovery uses the shared `repairUnavailableTranslation()` helper instead of duplicating unavailable repair -- recovery marks interrupted non-complete jobs stale when source hash changed -- completed jobs remain complete when source hash later changes; stale/current is derived at read time - -## Verification - -```sh -mise exec -- bun run test tests/server/db/translation-repositories.test.ts -mise exec -- bun run test tests/server/translation/job-state.test.ts -mise exec -- bun run test tests/server/translation/orchestrator.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Completed translated body exists only on disk. -- SQLite retains metadata but not completed article text. -- Crash recovery cannot falsely report complete without purge. diff --git a/docs/workflows/archive/task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md b/docs/workflows/archive/task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md deleted file mode 100644 index bfd55214..00000000 --- a/docs/workflows/archive/task-19-codex-translation/12-frontend-translation-controls-and-progress-ui.md +++ /dev/null @@ -1,157 +0,0 @@ -# 19.12 Frontend translation controls and progress UI - -## Goal - -Add reader controls for starting Brilliant translation and showing streaming progress. - -## Files likely owned - -- `src/components/reader/MemoryReader.tsx` -- `tests/components/memory-reader-actions.test.ts` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/04-api-and-sse.md` - -## Instruction alignment - -Scope: source reader translation trigger, progress UI, Codex setup state, and variant tabs. - -Inputs: page settings data, current variant metadata, translation API responses, SSE events, and completed `reader_url`. - -Outputs: title-edge Codex translation trigger, confirmation popup, -setup/failure/progress states, SSE subscription, and tab navigation for current -variants. - -Dependencies: 19.7 freezes event payloads; 19.13 provides reader variant metadata; current settings UI/API provides target language. - -Parallelization notes: can run with 19.13 after page-data types are frozen; do not talk to Codex app-server from browser code. - -Implementation risks: rendering the icon on translated routes, hiding the icon for stale files, or navigating without `reader_url` creates route/content mismatches. - -## Rendering contract - -Reader UI shows: - -- selected target language from persisted settings/page data -- Codex trigger at the right edge of the source reader title when the configured target-language `CONTENT.md` variant is missing -- hover expands only the trigger width, keeps the Codex icon left-aligned, and reveals `Translate` -- click opens a confirmation popup instead of starting translation directly -- popup shows and can submit target language, Codex model, and reasoning effort -- popup submit button uses the 50% transparent visual treatment shared with translation progress -- tooltip text `Translate to ` or the Japanese UI equivalent `に翻訳する` -- no Codex translation icon on translated reader routes -- settings-required state when no target language exists -- auth/setup-required state when Codex cannot run -- translate action when ready, triggered by submitting the popup -- current chunk index and total chunk count while running -- live Codex delta transcript labelled as progress, not saved content -- validation and retry events -- committing state -- unavailable state when a job snapshot reports `status = "unavailable"` or `error.code = "translation_unavailable"` -- completion action to open translated reader variant -- actionable failure state - -## Start behaviour - -1. Read configured `lang_code` from page/settings data. -2. Read reader variant metadata from page data. Variant metadata must be current, meaning the translated file exists, its completed `translation_jobs.source_hash` matches the current source hash, and the file hash matches `translation_jobs.output_hash`. -3. If the current reader route is translated, do not render the Codex icon. -4. If a current translated variant for `memories///CONTENT.md` already exists, do not render the Codex icon for that language. -5. If the configured target-language variant is missing on the source reader route, render the Codex trigger at the title right edge. -6. On trigger click, open the confirmation popup. Do not start translation on this click. -7. The popup reads the current settings defaults for `lang_code`, `model`, and `reasoning_effort`; model options are loaded through `/api/settings/codex-models`, never by calling app-server from browser code. -8. On popup submit, POST `/api/memories/:memory_id/translations` with `lang_code`, `model`, and `reasoning_effort`. `model` and `reasoning_effort` may be `null` to use app-server/model defaults. -9. If `202`, open the returned `event_url`. -10. If `200 active`, open the returned `event_url` for the reused active job and use `job_status` plus the first snapshot to render the exact progress state. -11. If `200 current`, navigate to the response `reader_url`. -12. For non-2xx responses, branch on the stable response `code` field, not free-form `message`. -13. If `code = "translation_language_required"`, link to `/settings`. -14. If `code = "translation_language_mismatch"`, refresh settings state and ask the user to retry. -15. If `code = "translation_model_unavailable"` or `code = "translation_reasoning_effort_unavailable"`, tell the user to update Codex translation defaults in settings. -16. If `code = "setup_required"` or `code = "auth_required"`, show Codex auth setup guidance. -17. If `code = "app_server_unavailable"`, tell the user Codex app-server is unavailable and offer retry after setup is fixed. -18. If `code = "app_server_protocol_error"`, tell the user Codex app-server rejected the translation request and do not show app-server startup guidance. -19. If `code = "translation_unavailable"` or `action = "start_fresh_translation"`, tell the user the translated output is no longer available, navigate to the source reader route `/memories/:id` if necessary, and start a fresh translation through `POST /api/memories/:memory_id/translations`. -20. If `code = "timeout"`, tell the user the Codex turn timed out and offer retry. -21. If `code = "stream_disconnected"`, tell the user the Codex stream disconnected and offer retry or fresh translation depending on job status. -22. If `code = "invalid_final_output"`, tell the user Codex returned invalid final output and offer retry. -23. If `code = "stale_source"`, tell the user the source changed and offer to start a fresh translation. -24. If `code = "cancellation_conflict"`, tell the user cancellation is already in progress and offer retry after cancellation completes. -25. If `code = "usage_limit"` or `code = "context_overflow"`, present the stable backend message and offer retry after the limit resets or after the translation plan is adjusted. -26. If `code = "validation_failed"`, `code = "filesystem_failure"`, `code = "unknown_failure"`, or any unrecognized stable code is returned, present a generic safe failure message and offer manual retry or fresh translation according to the job status. Do not expose raw source, prompts, app-server payloads, credential paths, or local filesystem details. - -Progress and reconnect behaviour: - -- If `GET /api/translation-jobs/:job_id` or an SSE `translation.job.snapshot` returns `status = "unavailable"`, render the same recovery UI as `code = "translation_unavailable"`. -- The unavailable recovery UI must not link to `reader_url` because unavailable snapshots have `reader_url = null`. -- The primary action is to navigate to the source reader route `/memories/:id` if the user is not already there, then call `POST /api/memories/:memory_id/translations`. - -## Variant tab contract - -The memory reader header owns variant tabs. - -Rules: - -- Load available variants from source content plus current translated variants. A translated variant is current only when the file exists, the complete translation row matches the current source hash, and the file hash matches the row `output_hash`. -- Do not render tabs when only the default source `CONTENT.md` exists. -- Render tabs under the memory header when two or more `CONTENT.md` variants exist. -- The default source tab label is `Original` unless a reliable source language label is introduced later. -- Translated tab labels use language display names, not raw codes. For example, `ja-JP` renders as `Japanese`. -- Clicking a translated tab navigates to its `reader_url`, for example `/memories//`. -- Clicking the default tab navigates to `/memories/:id`. - -## Tests - -Cover: - -- target language renders from persisted settings value -- missing language renders settings-required state -- auth unavailable renders setup-required state -- missing configured target-language variant renders the title-edge Codex icon -- existing configured target-language variant hides the Codex icon -- stale configured target-language file does not hide the Codex icon -- translated reader route hides the Codex icon -- Codex icon tooltip contains the target language code -- trigger click opens the confirmation popup and does not start translation -- popup submit starts translation API request with `lang_code`, `model`, and `reasoning_effort` -- popup submit and progress UI use the transparent visual treatment -- `202` opens SSE progress -- `200 active` opens SSE progress for the reused active job -- reused active response renders from `job_status` and snapshot instead of assuming `running` -- `200 current` navigates to translated reader route -- completion event navigates with `reader_url` -- tabs are hidden when only default `CONTENT.md` exists -- tabs render under the header when translated variants exist -- stale translated files do not render as current tabs -- translated files whose hash differs from `translation_jobs.output_hash` do not render as current tabs -- `ja-JP` tab label renders as `Japanese` -- progress shows chunk count and current chunk -- delta transcript is labelled non-authoritative -- retry event renders visibly -- live job/chunk failure events branch on `data.error.code` -- API failure UI branches on stable `code` values -- job snapshot with `status = "unavailable"` renders a fresh-translation recovery action -- app-server-unavailable failure is rendered separately from auth/setup-required -- timeout and stream-disconnected failures are rendered separately from validation and auth/setup failures -- invalid-final-output failure is rendered separately from generic validation failure -- translation-unavailable failure navigates to the source reader route and starts a fresh translation -- stale-source failure offers a fresh translation action -- cancellation-conflict failure tells the user to retry after cancellation completes -- failure message is actionable and does not expose secrets - -## Verification - -```sh -mise exec -- bun run test tests/components/memory-reader-actions.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Reader can open a confirmation popup and start translation from its submit action. -- UI uses SQLite-backed settings state through backend/page data. -- UI never talks to Codex app-server directly. -- UI does not present partial deltas as saved translation. -- UI exposes translated variants through tabs only when variants exist. diff --git a/docs/workflows/archive/task-19-codex-translation/13-reader-render-integration-for-translated-content.md b/docs/workflows/archive/task-19-codex-translation/13-reader-render-integration-for-translated-content.md deleted file mode 100644 index 6d13cdc0..00000000 --- a/docs/workflows/archive/task-19-codex-translation/13-reader-render-integration-for-translated-content.md +++ /dev/null @@ -1,143 +0,0 @@ -# 19.13 Reader render integration for translated CONTENT.md - -## Goal - -Render committed translated content variants without overwriting source reader content. - -## Files likely owned - -- `src/server/reader/page-data.ts` -- `src/components/reader/MemoryReader.tsx` variant tab rendering -- `src/routes/memories/[id].tsx` -- `src/routes/memories/[langCode]/[id].tsx` or project-equivalent translated reader route -- `tests/server/reader/page-data.test.ts` -- `tests/components/reader-flashback-tabs.test.ts` -- `tests/components/memory-reader-actions.test.ts` - -Consumed but not owned: - -- `src/server/translation/current-translation.ts` from 19.3. Reader work imports `resolveCurrentTranslationReadOnly()` and must not edit this helper. - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/03-sqlite-and-repositories.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: source and translated reader route loading, current-variant detection, not-found behaviour, and tab metadata. - -Inputs: memory metadata, source `CONTENT.md`, translated output file, current source hash, complete translation jobs, and settings language. - -Outputs: source page data, translated page data, active-variant Flashback state, current variant list, derived `reader_url`, and Codex trigger visibility state. - -Dependencies: 19.2 provides translation repository methods; 19.10/19.11 define committed output and purge rules; 19.12 consumes page data. - -Parallelization notes: can run with 19.12 only after route shape and variant metadata types are frozen. - -Implementation risks: silently falling back to source on translated routes or listing stale files as current tabs breaks URL/content truthfulness. - -Ownership boundary: do not edit `src/server/translation/current-translation.ts` in this subtask. If reader work needs a new resolver shape, update the 19.3 current-translation contract first. - -## Route contract - -```text -/memories/:id -> source CONTENT.md -/memories/:lang_code/:id -> translated CONTENT.md for the requested BCP 47 language when current -``` - -## Loader contract - -1. Load memory metadata. -2. If `lang_code` route param is absent, resolve source `CONTENT.md`. -3. If `lang_code` route param is present, validate BCP 47, traversal safety, and canonical casing against the supported-language table. -4. Compute current source hash. -5. Look up complete translation for `(memory_id, lang_code, source_hash)`. -6. Use `resolveCurrentTranslationReadOnly()` from `src/server/translation/current-translation.ts`, not the SQLite repository alone, to verify output file existence and hash under `storePath`. -7. If output path exists and its hash matches `translation_jobs.output_hash` on the completed translation row, render translated file from store-relative `memories///CONTENT.md`. -8. If the complete row exists but the output file is missing or hash-mismatched, return the project-standard not-found response for the translated route without mutating SQLite. Backend API/job-start recovery owns `repairUnavailableTranslation()`. -9. If missing or stale, return the project-standard not-found response for the translated route; do not silently fall back to source content. -10. Resolve the active Flashback variant after content resolution. Source routes use source Flashback rows; translated routes use rows scoped to `(memory_id, lang_code, output_hash)`. -11. Render active-variant Flashbacks only. Do not project source Flashbacks into translated reader content. -12. Load available variants from actual `CONTENT.md` files plus current translation metadata for tab rendering. -13. Load configured translation target language from SQLite-backed settings data for the source-route trigger. - -## Rendering rules - -- Source route remains unchanged. -- Translated route uses the same Markdown safety/sanitization rules as source content. -- Source metadata remains available when viewing translated content. -- Stale translations are not silently served as current. -- Unavailable translations are not silently served as current. -- Missing translations do not create jobs automatically. -- Missing or stale translated routes return the project-standard not-found response and include a link back to `/memories/:id` when the route renderer supports contextual links. -- Non-canonical language casing is not accepted as a distinct route. `ja-JP` is valid; `ja-jp` or `JA-JP` must redirect to the canonical route only if the project already has a canonical redirect helper, otherwise return the project-standard not-found response. -- Translated routes do not render the Codex translation icon. -- Translated routes render translated Flashbacks for the active translated variant only. Source Flashbacks do not automatically appear in translated content. -- Translated Flashback rows are scoped to the completed translation output hash. Missing or stale translated output fails closed instead of reusing source rows. -- The source route renders a Codex icon at the right edge of the memory title only when the configured target-language variant is missing. -- The icon tooltip is `に翻訳する` in the Japanese UI, or `Translate to ` if the current UI copy remains English. -- Clicking the icon starts translation through `POST /api/memories/:memory_id/translations`. - -## Variant tab rules - -- Variants are based on actual files under the memory store directory plus current translation metadata. -- The default source variant is store-relative `memories//CONTENT.md`. -- Translated variants are store-relative `memories///CONTENT.md`. -- A translated variant is current only when a complete translation row exists for `(memory_id, lang_code, current_source_hash)`, the output file exists, and the file hash matches `translation_jobs.output_hash`. -- Stale translated files are not shown as normal tabs. If surfaced later, they must be disabled and labelled stale. -- If only the default source variant exists, do not render tabs. -- If two or more variants exist, render tabs immediately below the memory header. -- The source tab label is `Original`. -- Translated tab labels use language display names from the supported-language table. For example, `ja-JP` renders as `Japanese`. -- The initial supported-language table is defined in `contracts/02-types-state-and-settings.md`; route validation, settings select options, prompt display names, and tab labels must all use that same table. -- The active tab matches the current reader route. -- Source tab href is `/memories/:id`. -- Translated tab href is the derived `reader_url`, for example `/memories//`. - -## Tests - -Cover: - -- source route renders source `CONTENT.md` -- translated route renders committed translated `CONTENT.md` -- translated route reads store-relative `memories///CONTENT.md` -- stale translated output is not rendered as current -- complete row with missing or hash-mismatched output is treated as unavailable/not current and is not rendered -- reader route and variant tab logic use the shared read-only `resolveCurrentTranslationReadOnly()` helper -- reader route and variant tab rendering do not mutate SQLite when translated output is missing or hash-mismatched -- missing translated route returns not found without silently rendering source content -- stale translated route returns not found without silently rendering source content -- invalid `lang_code` is rejected -- traversal-like `lang_code` is rejected -- non-canonical `lang_code` casing is redirected to canonical only through the project-standard redirect helper, otherwise rejected as not found -- source route renders Codex icon when configured target variant is missing -- source route hides Codex icon when configured target variant exists -- translated route hides Codex icon -- tabs are hidden when only default source `CONTENT.md` exists -- tabs render when one or more translated variants exist -- stale translated files do not render as current tabs -- output files whose hash differs from `translation_jobs.output_hash` do not render or tab as current -- `ja-JP` tab label renders as `Japanese` -- tab labels for all supported language codes come from the central supported-language table -- source metadata remains present -- source file is not mutated - -## Verification - -```sh -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/components/reader-flashback-tabs.test.ts -mise exec -- bun run test tests/components/memory-reader-actions.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Reader supports translated variants. -- Source and translated content paths remain distinct. -- Stale content is visible as stale/unavailable, not silently current. -- Translation trigger appears only on the source route when the configured target variant is missing. -- Variant tabs reflect actual `CONTENT.md` variants. -- Variant tabs only expose current translated variants. diff --git a/docs/workflows/archive/task-19-codex-translation/14-translation-skill-definition.md b/docs/workflows/archive/task-19-codex-translation/14-translation-skill-definition.md deleted file mode 100644 index dc1330d4..00000000 --- a/docs/workflows/archive/task-19-codex-translation/14-translation-skill-definition.md +++ /dev/null @@ -1,91 +0,0 @@ -# 19.14 Translation skill definition - -## Goal - -Create a repo-local Codex skill that captures reusable Brilliant translation policy. - -## Files likely owned - -- `.agents/skills/reader-translate/SKILL.md` -- optional `tests/skills/reader-translate.test.ts` if the repo has skill validation tests - -## Contract references - -- `contracts/06-codex-prompt-and-validation.md` - -## Instruction alignment - -Scope: repo-local policy skill only. - -Inputs: Brilliant prompt contract, preservation rules, academic-paper requirements, and untrusted-content policy. - -Outputs: `.agents/skills/reader-translate/SKILL.md` and optional skill validation. - -Dependencies: 19.8 freezes MVP prompt policy before the skill mirrors it. - -Parallelization notes: can run beside validator work after preservation rules are stable; do not add scripts unless a validation task proves they are required. - -Implementation risks: overbuilding scripts or granting file-write authority to the skill violates the instruction's boundary. - -## Skill contract - -The skill must instruct Codex to: - -- treat source article text as untrusted data -- translate prose faithfully -- preserve Markdown -- preserve HTML tags and attributes -- preserve LaTeX/math -- preserve citations -- preserve footnotes -- preserve code fences -- preserve inline code -- preserve placeholders -- preserve identifiers -- preserve URLs -- preserve file paths and commands -- never summarize -- never omit -- return schema-compliant output -- support academic paper translation - -## Boundary rules - -- The skill is policy only. -- Brilliant MVP does not invoke `$reader-translate` as a runtime app-server skill item during translation turns. -- Runtime translation prompts are generated deterministically by `src/server/translation/prompt.ts` from the frozen prompt contract. -- The repo-local skill is for human/agent implementation consistency, future prompt policy reuse, and review guidance. -- Reader backend still owns chunking, validation, retry, stitching, final writes, and cleanup. -- Do not add scripts unless validation reliability requires them. -- Do not let the skill authorize Codex to write canonical files. -- Do not require the app-server translation turn to read `.agents/skills/reader-translate/SKILL.md`; this avoids adding project-root read access to the locked-down runtime sandbox. - -## Tests - -If skill validation exists, cover: - -- skill file exists -- skill contains untrusted-content instruction -- skill contains preservation requirements -- skill forbids omission and summarization -- skill does not instruct Codex to write files -- runtime translation prompt builder does not require app-server skill invocation - -## Verification - -```sh -# Optional only if skill validation exists -mise exec -- bun run test tests/skills/reader-translate.test.ts -``` - -If no skill validation exists, record that this is a policy-file-only subtask. - -## Acceptance criteria - -- Repo-local skill exists. -- Skill policy matches Brilliant prompt contract. -- Skill policy can be tracked through `translation_jobs.prompt_policy_version` - when the deterministic runtime prompt policy intentionally mirrors this file. - This field records policy provenance only and does not mean the runtime invokes - `$reader-translate`. -- MVP translation turns can run without giving Codex filesystem read access to the repo-local skill file. diff --git a/docs/workflows/archive/task-19-codex-translation/15-error-handling-and-cancellation.md b/docs/workflows/archive/task-19-codex-translation/15-error-handling-and-cancellation.md deleted file mode 100644 index 9a4ff254..00000000 --- a/docs/workflows/archive/task-19-codex-translation/15-error-handling-and-cancellation.md +++ /dev/null @@ -1,185 +0,0 @@ -# 19.15 Error handling and cancellation - -## Goal - -Define consistent failure, retry, cancellation, and recovery behaviour across Brilliant backend, frontend, SQLite, and Codex app-server boundaries. - -## Files likely owned - -- `src/server/translation/types.ts` -- `src/server/translation/job-state.ts` -- `src/server/translation/codex-app-server.ts` -- `src/server/translation/orchestrator.ts` -- route file implementing `POST /api/translation-jobs/:job_id/cancel`, following existing `src/routes/api/` conventions -- `tests/server/routes/api-translation-jobs.test.ts` -- `tests/server/translation/orchestrator.test.ts` - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/04-api-and-sse.md` -- `contracts/06-codex-prompt-and-validation.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: typed errors, user-actionable failures, cancellation endpoint, and late-output safety. - -Inputs: job state, app-server typed errors, in-flight `threadId`/`turnId`, filesystem failures, and SSE terminal events. - -Outputs: cancellation state transitions, `turn/interrupt` usage, failure payload rules, and no-corruption guarantees. - -Dependencies: 19.3 defines states, 19.5 defines cancellation primitive, and 19.7 defines terminal events. - -Parallelization notes: can run after app-server and state contracts are frozen; do not change prompt or chunking logic here. - -Implementation risks: committing canceled output, leaking prompts/secrets in errors, or canceling without known ids breaks safety requirements. - -## Error taxonomy - -Use typed errors for: - -- auth required -- setup required -- app-server unavailable -- app-server protocol or request-contract error -- usage limit -- context overflow -- timeout -- stream disconnect -- invalid final output -- validation failure -- stale source -- unavailable translation output -- filesystem failure -- cancellation -- unknown failure - -User-facing errors must not include tokens, raw prompts, credential paths, app-server secrets, or full source chunks. - -API error responses use the shared shape from `contracts/04-api-and-sse.md`. The stable frontend branch key is `code`, not free-form `message`. - -Auth/setup failures during translation start are precondition failures. `POST -/api/memories/:memory_id/translations` returns `409 auth_required` or `409 -setup_required` and does not create a `translation_jobs` row when Codex cannot -run authenticated translation work. This precondition check happens only after -the backend has ruled out a current committed translation and compatible active -job reuse. If Codex auth/setup is lost after a job row has been created, the -job records a normal execution failure with `auth_required` or `setup_required`. -Reused active jobs must not remain indefinitely running after auth loss; runner -recovery or the next execution tick marks them failed with the same safe code. - -SSE failure events use the same stable error shape. `translation.job.failed` -emits `{ error }`; `translation.chunk.failed` emits `{ error, retry_count, -will_retry }`. Error payloads must not include raw prompts, source chunks, -tokens, credential paths, app-server URLs, or raw app-server payloads. - -Codex transport failures map to stable API/job error codes: - -- `timeout` remains `timeout` and maps to HTTP `504` when returned from an API request. -- `stream_disconnected` remains `stream_disconnected` and maps to HTTP `503` when returned from an API request. -- `app_server_protocol_error` means the app-server was reachable but rejected - TRAUMA's request contract, for example an invalid param or a field requiring - `experimentalApi`; it maps to HTTP `502` when returned from an API request. -- `translation_model_unavailable` means a submitted or persisted Codex - translation model is not present in the current app-server `model/list` - catalog; it maps to HTTP `409` and should send the user to settings. -- `translation_reasoning_effort_unavailable` means a submitted or persisted - reasoning effort is not supported by the selected/default model in - `model/list`; it maps to HTTP `409` and should send the user to settings. -- `invalid_final_output` remains `invalid_final_output` and maps to HTTP `502` when returned from an API request. -- Do not collapse `timeout`, `stream_disconnected`, - `app_server_protocol_error`, `translation_model_unavailable`, - `translation_reasoning_effort_unavailable`, or `invalid_final_output` into - `unknown`, `app_server_unavailable`, or `validation_failed`. - -Validation error boundary: - -- Use `invalid_final_output` only for JSON parse failure or `CodexChunkOutput` - schema mismatch after all configured output-mode fallbacks are exhausted. -- Use `validation_failed` for schema-valid output that fails semantic checks. -- Persist job and chunk errors as structured `TranslationPersistedError` JSON, - never raw exception text. - -Stale source is not reported as a generic failed job. When a pending or running job becomes stale because the source hash changed, emit `translation.job.stale` with safe hash metadata and close the stream. - -Unavailable translation output is not reported as current. If a previously -complete job loses its output file or its output hash no longer matches, mark it -`unavailable`, return `translation_unavailable` where an API error is needed, -and allow a fresh translation for the same `(memory_id, lang_code, source_hash)`. - -## Cancellation contract - -- `POST /api/translation-jobs/:job_id/cancel` handles cancellation by current job state. -- Cancellation is accepted for `pending` and `running` jobs. -- Canceling a `pending` job with no in-flight Codex turn transitions directly to `canceled`; it does not wait in `cancel_requested`. -- Canceling a `running` job transitions to `cancel_requested`, then interrupts the in-flight turn when ids are known or ignores late output when ids/cancel support are unavailable. -- Pending and running cancellation must be compare-and-set transitions so the cancel API cannot race incorrectly with runner claim. If `pending -> canceled` fails because the runner already claimed the job, reload the job and apply the `running -> cancel_requested` path when applicable. -- A `cancel_requested` job must finalize to `canceled` when any one of these conditions is true: app-server acknowledges `turn/interrupt`, app-server emits `turn.completed` with interrupted status, the in-process runner registry has no in-flight `threadId`/`turnId`, or the cancel request exceeds the configured cancellation grace timeout. -- The cancellation grace timeout is a fixed MVP server constant of `BRILLIANT_CANCEL_GRACE_MS = 30000`. -- Cancellation is idempotent for `cancel_requested` and `canceled` jobs. -- Cancellation returns `409 cancellation_conflict` for `stitching`, `committing`, `complete`, `stale`, `failed`, and `unavailable` jobs. -- Scheduler stops starting new chunks. -- In-flight Codex turn is canceled if app-server supports cancellation. -- The orchestrator stores the latest in-flight Codex `threadId` and `turnId` from app-server events. -- `cancelTurn({ threadId, turnId })` sends app-server `turn/interrupt`. -- `cancelTurn()` is called only when both an in-flight `threadId` and `turnId` are known. -- In-flight `threadId` and `turnId` are stored only in the in-process runner registry for Brilliant MVP. Do not add SQLite columns for them. -- After process restart, a `cancel_requested` job without registry ids is non-resumable and is finalized as `canceled`; late output is ignored. -- If app-server cannot cancel, late output is ignored. -- Canceled jobs do not commit final `CONTENT.md`. -- SSE emits `translation.job.canceled` when cancellation completes. - -## Tests - -Cover: - -- cancel pending job -- cancel pending job transitions directly to `canceled` -- cancel running job -- cancel already requested or canceled job is idempotent -- cancel non-cancelable job returns `cancellation_conflict` -- canceled job stops scheduling chunks -- known in-flight thread id and turn id trigger `turn/interrupt` -- missing in-flight thread id or turn id falls back to ignoring late output -- in-flight thread id and turn id are not persisted in SQLite -- late chunk output is ignored -- canceled job never commits final file -- auth and usage errors surface actionable messages -- auth/setup precondition failures do not create job rows -- current committed translation reuse does not require Codex auth -- in-flight auth/setup loss after job creation is persisted as a safe job error -- reused active job with auth/setup loss transitions to failed instead of remaining indefinitely running -- timeout and stream-disconnected errors preserve their stable codes -- app-server protocol/request-contract errors preserve - `app_server_protocol_error` and do not display app-server startup guidance -- invalid-final-output errors preserve their stable code -- validation-failed errors are reserved for schema-valid semantic validation failures -- job and chunk errors persist as structured JSON, not raw strings -- validation-failed job and chunk errors may include safe diagnostics for - server-side investigation and retry prompt construction; diagnostics do not - include raw prompts, raw Codex responses, source chunks, app-server endpoints, - auth state, tokens, or completed translated article bodies -- validation diagnostics may identify Markdown structure mismatches, segment - schema mismatches, empty translated segments, and segment length-ratio - failures without exposing raw invalid model output -- filesystem failure does not corrupt existing translation -- unavailable completed output does not block a fresh translation -- stream disconnect does not cancel job -- stale source emits `translation.job.stale` rather than `translation.job.failed` -- error payloads contain no secrets -- SSE job/chunk failure payloads expose stable codes and safe messages only - -## Verification - -```sh -mise exec -- bun run test tests/server/routes/api-translation-jobs.test.ts -mise exec -- bun run test tests/server/translation/orchestrator.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance criteria - -- Cancellation works without WebSocket. -- Failures are typed and user-actionable. -- Failed and canceled jobs do not corrupt committed translations. diff --git a/docs/workflows/archive/task-19-codex-translation/16-test-plan-and-fixtures.md b/docs/workflows/archive/task-19-codex-translation/16-test-plan-and-fixtures.md deleted file mode 100644 index db9cc0f5..00000000 --- a/docs/workflows/archive/task-19-codex-translation/16-test-plan-and-fixtures.md +++ /dev/null @@ -1,225 +0,0 @@ -# 19.16 Test plan and fixtures - -## Goal - -Create deterministic fixtures and test coverage for Brilliant without requiring live Codex for normal verification. - -## Files likely owned - -- `tests/fixtures/translation/simple-article.md` -- `tests/fixtures/translation/academic-paper.md` -- `tests/fixtures/translation/hostile-prompt-injection.md` -- `tests/fixtures/translation/markdown-protected-spans.md` -- optional `tests/server/translation/fakes/fake-codex-app-server.ts` -- tests listed by the subtasks above - -## Contract references - -Read the focused contract files for the tests being implemented. Do not load every contract unless building the final E2E test. - -## Instruction alignment - -Scope: deterministic fixtures, fake app-server support, and normal verification coverage. - -Inputs: all frozen Brilliant contracts, hostile article examples, academic-paper structure, and fake Codex app-server events. - -Outputs: translation fixtures, test coverage checklist, fake app-server utility if needed, and non-live verification command list. - -Dependencies: runs after interface shapes from 19.2 through 19.13 are frozen. - -Parallelization notes: fixture writing can proceed early, but integrated fake app-server tests should wait for event and client contracts. - -Implementation risks: relying on live Codex for normal tests makes CI nondeterministic; omitting hostile/long fixtures misses core instruction requirements. - -## Fixture contract - -`simple-article.md` contains: - -- frontmatter -- one heading -- two paragraphs -- one image -- one link - -`markdown-protected-spans.md` contains: - -- code fence -- inline code -- math block -- citation marker -- footnote -- Markdown link -- raw HTML block -- command and file path examples - -`hostile-prompt-injection.md` contains: - -- text telling the model to ignore instructions -- text asking the model to omit paragraphs -- text asking the model to print secrets - -`academic-paper.md` contains: - -- abstract -- numbered sections -- equations -- citations -- table -- footnotes -- references/bibliography -- enough repeated structure to require multiple chunks under default test config - -## Required verification commands - -```sh -mise exec -- bun run test tests/server/db/translation-schema.test.ts -mise exec -- bun run test tests/server/db/translation-repositories.test.ts -mise exec -- bun run test tests/server/settings/translation-language.test.ts -mise exec -- bun run test tests/server/translation/source-loader.test.ts -mise exec -- bun run test tests/server/translation/markdown-blocks.test.ts -mise exec -- bun run test tests/server/translation/chunker.test.ts -mise exec -- bun run test tests/server/translation/job-state.test.ts -mise exec -- bun run test tests/server/translation/codex-app-server.test.ts -mise exec -- bun run test tests/server/translation/prompt.test.ts -mise exec -- bun run test tests/server/translation/validator.test.ts -mise exec -- bun run test tests/server/translation/stitcher.test.ts -mise exec -- bun run test tests/server/translation/atomic-writer.test.ts -mise exec -- bun run test tests/server/translation/events.test.ts -mise exec -- bun run test tests/server/translation/orchestrator.test.ts -mise exec -- bun run test tests/server/routes/api-memory-translations.test.ts -mise exec -- bun run test tests/server/routes/api-translation-jobs.test.ts -mise exec -- bun run test tests/server/routes/api-translation-events.test.ts -mise exec -- bun run test tests/server/reader/page-data.test.ts -mise exec -- bun run test tests/components/reader-flashback-tabs.test.ts -mise exec -- bun run test tests/components/memory-reader-actions.test.ts -mise exec -- bun run typecheck -``` - -## Coverage checklist - -- BCP 47 language persistence and traversal rejection -- supported-language canonical casing: `ja-JP` accepted, `ja-jp`/`JA-JP` redirected only through a project-standard canonical redirect helper or rejected as not found -- settings select options, prompt display names, and variant tab labels use one central supported-language table -- translation start using SQLite settings language -- current committed translation reuse returns `200 current` without checking Codex auth -- active job reuse returns `status = "active"`, the actual `job_status`, and the existing `event_url` without creating another row -- translation start/reuse returns `409 cancellation_conflict` while a compatible `cancel_requested` job is finalizing -- canceling `cancel_requested` and `canceled` jobs remains idempotent -- runner claim uses atomic compare-and-set `pending -> running` semantics -- active job reuse triggers or depends on focused recovery so stale old-source jobs are marked `stale` before new job creation -- reused pending/running job whose Codex auth/setup is now missing becomes failed with `auth_required` or `setup_required` -- auth/setup precondition failures return `409` without creating `translation_jobs` rows only when a new job would be required -- in-flight auth/setup loss after job creation persists `auth_required` or `setup_required` as a safe job error -- `409 translation_language_required` -- `409 translation_language_mismatch` -- source hash and stale detection -- stale source emits `translation.job.stale` as a terminal event -- deterministic block ids and chunk grouping -- source frontmatter is preserved unchanged in translated `CONTENT.md` -- prompt injection containment -- partial delta streaming as non-authoritative progress -- SSE job/chunk failure events include safe error objects with stable codes -- chunk validation success and failure -- chunk errors persist structured `TranslationPersistedError` JSON in `translation_chunks.error` -- invalid final output covers JSON parse/schema failure; validation failed covers schema-valid semantic failure -- chunk-level retry -- final stitching order -- atomic writer failure cases -- purge of `translated_markdown` after commit -- public `completed_chunks` counts purged chunks as completed after commit -- failed or interrupted final writes delete `.CONTENT..tmp` temp files -- source rendering and translated variant rendering -- auth-required and setup-required UI states -- app-server initialization before requests -- Unix socket default app-server wire transport support only when Codex is started with `codex app-server --listen unix:///tmp/trauma-codex.sock`, HTTP wire-protocol rejection, WebSocket rejection, and `stdio` rejection for Brilliant MVP -- Unix socket adapter spike documents Bun/Node support for Unix domain socket plus HTTP Upgrade/WebSocket framing and the configured `unix:///tmp/trauma-codex.sock` socket path -- Codex app-server protocol schema or focused fixture version is recorded and used by fake app-server tests -- Codex app-server fixtures use `{ method, params, id }` requests, `{ id, result/error }` responses, and `{ method, params }` notifications without top-level `jsonrpc` -- Codex app-server fixtures record that Brilliant uses stable schema mode - without `experimentalApi`; stable `thread/start` omits `environments`, - `experimentalRawEvents`, and `persistExtendedHistory`, and stable - `turn/start` omits `environments` -- `thread/start`, `turn/start`, and `turn/interrupt` coverage -- `model/list` coverage normalizes visible models, filters hidden models, and - preserves supported reasoning efforts -- translation `turn/start` includes selected model as `model` and selected - reasoning effort as `effort` when job metadata is non-null -- settings API coverage reads `/api/settings/codex-models` and validates - `/api/settings/translation-codex-defaults` against the current app-server - catalog -- reader translation submit coverage sends `lang_code`, `model`, and - `reasoning_effort` only from the confirmation popup submit flow -- retry attempts create fresh ephemeral Codex threads and do not reuse failed attempt thread history -- `maxRetries: 3` means one initial attempt plus three retry attempts -- output-mode fallback does not increment `retry_count` or consume `maxRetries` -- output-mode fallback after a rejected thread starts a fresh prompt-only thread without consuming retry budget -- translation `turn/start` uses locked-down approval, sandbox, network, and cwd settings -- job-scoped runtime `cwd` is created outside project/store roots and cleaned up on terminal job states -- runtime directory cleanup validates canonical root containment, rejects symlinks/traversal, refuses project/store/backup/article paths, and deletes only empty job-scoped directories -- non-empty runtime cleanup leftovers produce safe server logs only and do not create a blocking frontend popup or require a diagnostics SQLite column in MVP -- `networkAccess = false` is tested as sandbox/tool-network control and does not block required app-server/model traffic -- locked-down `turn/start` policy payload is verified against generated app-server schema or focused protocol fixtures before implementation -- translation `thread/start` also uses locked-down policy where supported, or tests document that `turn/start` overrides broader thread defaults -- `outputSchema` rejection falls back to prompt-only JSON output and still validates `CodexChunkOutput` -- `app_server_unavailable` maps to HTTP `503` -- reachable app-server request-contract errors such as - `requires experimentalApi capability` map to - `app_server_protocol_error` and HTTP `502` -- Codex `timeout` maps to stable `timeout` code and HTTP `504` -- Codex `stream_disconnected` maps to stable `stream_disconnected` code and HTTP `503` -- Codex `invalid_final_output` maps to stable `invalid_final_output` code and HTTP `502` -- unavailable model and unsupported reasoning effort map to stable - settings-correctable `409` errors -- `account/read` with a non-null `account` is authenticated even when - `requiresOpenaiAuth` is true -- `account/read` with no account and `requiresOpenaiAuth: true` is auth-required -- device-code login safe fields and success/failure/cancellation notification handling -- device-code notifications use raw app-server names `account/login/completed` - and `account/updated` -- pending device-code refresh returns only safe metadata or latest confirmed `account/read` state -- device-code auth observer is created only while login is pending and is cleaned up on completion/cancel/failure/timeout -- auth listener loss or server restart falls back to `checkAuth()` and safe pending metadata -- default app-server endpoint uses Unix socket `unix:///tmp/trauma-codex.sock` -- loopback WebSocket endpoint `ws://127.0.0.1:4500` is tested as rejected -- cancellation accepts pending/running jobs, is idempotent for already canceling/canceled jobs, and rejects non-cancelable terminal/final-write states with `cancellation_conflict` -- pending cancellation transitions directly to `canceled` and returns `status = "canceled"`; running cancellation uses `cancel_requested` and returns `status = "cancel_requested"` -- pending cancellation races runner claim through compare-and-set; if `pending -> canceled` loses to `pending -> running`, cancel API reloads and requests `running -> cancel_requested` -- `cancel_requested` finalizes to `canceled` after interrupt acknowledgement, interrupted turn completion, missing in-flight registry ids, or `BRILLIANT_CANCEL_GRACE_MS` timeout -- recovered non-resumable `cancel_requested` job becomes `canceled` so it does not block future retries indefinitely -- recovered orphaned `running` job becomes `pending` or `stale`; recovered `stitching`/`committing` job uses final-output recovery -- recovered orphaned `running` or `validating` chunks become `retrying` without incrementing `retry_count`; the next normal retry attempt increments exactly once, and exhausted retry budget fails the chunk/job -- runner executes both `pending` and `retrying` chunks after job claim -- retry budget uses fixed `BRILLIANT_MAX_RETRIES = 3` for MVP and is not read from mutable runtime settings during recovery -- interrupted final-file recovery verifies the existing final file hash against the re-stitched output hash before marking a job complete -- interrupted final-file recovery overwrites an existing final file only when completed chunk bodies can be re-stitched, source hash still matches, final validation passes, and the full atomic commit sequence can be rerun safely -- in-flight `threadId` and `turnId` live only in the in-process runner registry and are not added to SQLite -- duplicate route calls do not enqueue the same job id more than once -- completed event includes `reader_url` -- API errors use stable `code` values consumed by frontend state branches -- historical completed jobs for older source hashes return `reader_url: null` -- stale translated files are not exposed as current tabs -- translated output hash mismatch is not exposed as a current route, current tab, or non-null `reader_url` -- missing or hash-mismatched output for a complete row marks the job unavailable and does not block retranslation -- `translation_unavailable` is a required API error code and frontend branch -- `translation_unavailable` uses `action = "start_fresh_translation"` -- unavailable job snapshots return `reader_url: null` and `error.code = "translation_unavailable"` -- current translation metadata API returns `409 translation_unavailable` for complete rows with missing or hash-mismatched output -- job start and metadata API use `resolveCurrentTranslationReadOnly()` plus explicit `repairUnavailableTranslation()` when mutation is allowed -- reader route and variant tabs use `resolveCurrentTranslationReadOnly()` only and never call `repairUnavailableTranslation()` -- reader route and variant tab rendering use read-only current-translation resolution and do not mark rows unavailable -- runtime translation prompt builder does not require `$reader-translate` skill invocation or project-root read access -- `prompt_policy_version` records deterministic prompt policy provenance without implying runtime skill invocation -- `BRILLIANT_PROMPT_POLICY_VERSION = "brilliant-segments-v1"` is stored on new jobs and changes only by explicit prompt policy bump -- job start and metadata API use explicit unavailable repair before retry/recovery -- job status/snapshot API uses explicit unavailable repair before returning unavailable snapshots -- unavailable repair persists structured JSON error with reason `output_missing` or `output_hash_mismatch` -- 19.3 owns `current-translation.ts`; 19.13 consumes it read-only; 19.11 recovery reuses `repairUnavailableTranslation()` -- job snapshot errors include optional `action`, including `start_fresh_translation` -- unavailable status is snapshot-only and does not emit a dedicated SSE terminal event - -## Acceptance criteria - -- Normal tests use fake Codex app-server. -- Live Codex smoke is optional and separate from deterministic CI. -- Fixtures cover hostile content and long academic-paper structure. -- Cleanup and purge are tested, not only successful file output. diff --git a/docs/workflows/archive/task-19-codex-translation/17-end-to-end-validation-with-long-paper-fixture.md b/docs/workflows/archive/task-19-codex-translation/17-end-to-end-validation-with-long-paper-fixture.md deleted file mode 100644 index 19c704e4..00000000 --- a/docs/workflows/archive/task-19-codex-translation/17-end-to-end-validation-with-long-paper-fixture.md +++ /dev/null @@ -1,103 +0,0 @@ -# 19.17 End-to-end validation with long paper fixture - -## Goal - -Verify that Brilliant completes a long academic-style document without omission. - -## Files likely owned - -- `tests/server/translation/brilliant-e2e.test.ts` or the project-equivalent E2E test path -- `tests/fixtures/translation/academic-paper.md` -- PR description / handoff notes - -## Contract references - -- `contracts/02-types-state-and-settings.md` -- `contracts/03-sqlite-and-repositories.md` -- `contracts/04-api-and-sse.md` -- `contracts/05-markdown-chunking.md` -- `contracts/06-codex-prompt-and-validation.md` -- `contracts/07-atomic-commit-purge-recovery.md` - -## Instruction alignment - -Scope: integrated Brilliant validation over a long academic-style fixture using fake Codex by default. - -Inputs: persisted settings language, source fixture memory, fake app-server stream, chunker, validator, stitcher, atomic writer, and reader route. - -Outputs: end-to-end proof that long document translation completes, commits, purges, streams, and renders through dedicated routes/tabs. - -Dependencies: all prior Brilliant subtasks. - -Parallelization notes: this is final integration and should not run before schema, runner, Codex client, validation, commit, and reader route contracts are implemented. - -Implementation risks: validating only the happy path misses retry, purge, stale, and route/content mismatch requirements. - -## Required integration checks - -Manual or automated smoke: - -1. Set `/settings` translation target language to `ja-JP` and confirm SQLite persists it. -2. Load a memory backed by the long academic fixture. -3. Start translation with `POST /api/memories/:memory_id/translations` without trusting a client language as canonical. -4. Confirm job uses `ja-JP` from SQLite settings. -5. Confirm chunker creates multiple block-group chunks. -6. Confirm fake Codex app-server requires app-server initialization, starts ephemeral chunk-attempt threads, emits deltas, and returns final structured outputs. -7. Inject one validation failure and confirm only the failed chunk retries. -8. Confirm every source block id appears exactly once in validated translated output. -9. Confirm source frontmatter, when present, is preserved unchanged at the top of translated output. -10. Confirm final stitched Markdown passes full-document validation. -11. Confirm final file is committed to `memories//ja-JP/CONTENT.md`. -12. Confirm source `memories//CONTENT.md` is unchanged. -13. Confirm `translation_jobs` records completion, output path, output hash, and source hash. -14. Confirm `translation_chunks.translated_markdown` is purged after commit. -15. Confirm the dedicated translated reader route renders the `ja-JP` variant only when the current source hash matches `translation_jobs.source_hash` and the file hash matches `translation_jobs.output_hash`. -16. Confirm translated routes do not render the Codex translation icon. -17. Confirm the source route hides the Codex icon after the `ja-JP` variant exists. -18. Confirm variant tabs render under the header and label `ja-JP` as `Japanese`. -19. Confirm stale translated files are not shown as current tabs after source hash changes. -20. Confirm historical completed jobs for older source hashes return `reader_url: null`. -21. Confirm complete jobs with missing or hash-mismatched output are marked unavailable and do not block a fresh translation. -22. Confirm stale running jobs emit `translation.job.stale`. -23. Confirm a missing or stale translated route returns the project-standard not-found response and does not silently render source content. -24. Confirm SSE shows progress from job start through completion and completed payload includes `reader_url`. - -## Commands - -```sh -mise exec -- bun run test tests/server/translation/brilliant-e2e.test.ts -mise exec -- bun run typecheck -mise exec -- bun run verify -``` - -Optional live Codex smoke is not required for MVP completion. If a live Codex run is attempted, document the exact app-server URL, command, credentials boundary, and outcome in the PR handoff. If it is not attempted, explicitly record `live Codex smoke: not attempted`. - -## PR handoff checklist - -PR body must include: - -- SQLite schema/migration summary -- settings language persistence strategy -- API summary -- Codex app-server integration boundary -- SSE progress strategy -- chunking and validation strategy -- atomic commit and purge strategy -- crash recovery strategy -- reader UI/rendering summary -- exact verification commands and outcomes -- live Codex smoke status, if attempted -- known deferred work - -## Acceptance criteria - -- Long paper translation completes through chunking, validation, retry, stitching, atomic commit, and purge. -- Source content remains unchanged. -- Translated content is stored only at `memories///CONTENT.md` after completion. -- SQLite retains metadata but not completed translated article bodies. -- Reader can render the translated variant. -- Reader exposes translated variants through dedicated routes and tabs. -- Reader shows the Codex icon only before the configured target variant exists. -- Reader does not expose stale translated files as current variants. -- Completed job events include the translated reader URL. -- Verification results are documented for handoff. diff --git a/docs/workflows/archive/task-19-codex-translation/README.md b/docs/workflows/archive/task-19-codex-translation/README.md deleted file mode 100644 index 9ca3575a..00000000 --- a/docs/workflows/archive/task-19-codex-translation/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# Brilliant subtasks - -Implement these subtasks sequentially on `feat/brilliant`. - -## Order - -0. [Execution contract index](00-execution-contracts.md) - read this first, then only the focused contract files listed for your subtask. -1. [19.1 Requirements and architecture finalization](01-requirements-and-architecture-finalization.md) -2. [19.2 SQLite schema and migration design](02-sqlite-schema-and-migration-design.md) -3. [19.3 Translation job state machine](03-translation-job-state-machine.md) -4. [19.4 Markdown block manifest and chunker](04-markdown-block-manifest-and-chunker.md) -5. [19.5 Codex app-server integration](05-codex-app-server-integration.md) -6. [19.6 Codex auth and device-code setup flow](06-codex-auth-and-device-code-setup-flow.md) -7. [19.7 Streaming event bridge to frontend](07-streaming-event-bridge-to-frontend.md) -8. [19.8 Chunk translation prompt and output schema](08-chunk-translation-prompt-and-output-schema.md) -9. [19.9 Chunk validation and retry logic](09-chunk-validation-and-retry-logic.md) -10. [19.10 Stitching and atomic commit](10-stitching-and-atomic-commit.md) -11. [19.11 SQLite cleanup and purge policy](11-sqlite-cleanup-and-purge-policy.md) -12. [19.12 Frontend translation controls and progress UI](12-frontend-translation-controls-and-progress-ui.md) -13. [19.13 Reader render integration for translated CONTENT.md](13-reader-render-integration-for-translated-content.md) -14. [19.14 Translation skill definition](14-translation-skill-definition.md) -15. [19.15 Error handling and cancellation](15-error-handling-and-cancellation.md) -16. [19.16 Test plan and fixtures](16-test-plan-and-fixtures.md) -17. [19.17 End-to-end validation with long paper fixture](17-end-to-end-validation-with-long-paper-fixture.md) - -## Rules for agents - -- Own only the domain named by the subtask. -- Start from this README, `00-execution-contracts.md`, the focused contract files listed for the subtask, and the assigned subtask file. -- Do not load every Brilliant contract file unless the subtask explicitly says to do so. -- Do not pull unrelated Task 18 memory-action work into `feat/brilliant`. -- Treat archived Task 18 docs as history only; use current settings code and contracts as implementation inputs. -- Do not rewrite source `CONTENT.md` during translation. -- Use the SQLite-backed `/settings` translation target language as the server-side source of truth. -- Use BCP 47 language codes, with Japanese represented as `ja-JP`. -- Map the instruction's conceptual `memory//...` storage language onto TRAUMA's existing `memories//...` store layout. -- Do not create persistent `.work/` translation artifacts. -- Keep Codex app-server backend-only; never expose Codex credentials or app-server connection details to the browser. -- Implement Codex app-server through its wire protocol, not REST: initialize the connection, start an ephemeral thread, start a turn, and stream notifications. -- Treat source article Markdown as untrusted data, not instructions. -- Allow SQLite to hold translated chunk bodies only while a job is in progress. -- Purge completed translated chunk bodies from SQLite immediately after atomic final commit. -- Stream frontend progress from backend event state, not from persistent files. -- Do not treat streamed partial deltas as persisted translation. -- Add focused tests in the same subtask that introduces behaviour. -- Stop if a migration, credential, filesystem, or backup decision would rewrite unrelated data. - -## Parallelization guidance - -- Freeze contracts in 19.1 before parallel implementation starts. -- Track A owns schema and state: 19.2, 19.3, 19.11. -- Track B owns Markdown analysis: 19.4, 19.9. -- Track C owns Codex and streaming: 19.5, 19.6, 19.7. -- Track D owns reader UI/rendering: 19.12, 19.13. -- Track E owns prompt and skill policy: 19.8, 19.14. -- Track F owns fixtures and integrated validation: 19.16, 19.17. -- Do not parallelize tasks that edit the same shared file unless the earlier task has frozen the interface. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/01-architecture-and-ownership.md b/docs/workflows/archive/task-19-codex-translation/contracts/01-architecture-and-ownership.md deleted file mode 100644 index 6724ce38..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/01-architecture-and-ownership.md +++ /dev/null @@ -1,79 +0,0 @@ -# Brilliant architecture and ownership contract - -## Boundary rules - -- The Reader backend owns source loading, chunking, metadata, job state, validation, retry, stitching, atomic file writes, SQLite cleanup, and frontend events. -- Codex receives chunk text plus translation instructions and returns machine-readable translated chunk output. -- Codex must not write canonical `CONTENT.md` files. -- Codex app-server is backend-only. The browser must not connect to it directly. -- Codex app-server uses its documented wire protocol over the configured transport. Backend code must not model `thread/start`, `turn/start`, or auth methods as ordinary REST endpoints, and must not inject a top-level `jsonrpc` field unless generated fixtures prove the installed app-server accepts it. -- OpenAI/ChatGPT tokens must not enter TRAUMA SQLite, browser state, logs, or API responses. -- Source article Markdown is untrusted data, not instructions. - -## File ownership map - -### Schema and repositories - -- Modify: `src/server/db/schema.ts` -- Modify: `src/server/db/repositories.ts` -- Create: `src/server/db/translation-repositories.ts` if repositories are already split by domain -- Create: `drizzle/_brilliant_translation_jobs.sql` -- Test: `tests/server/db/translation-schema.test.ts` -- Test: `tests/server/db/translation-repositories.test.ts` - -### Translation domain - -- Create: `src/server/translation/types.ts` -- Create: `src/server/translation/languages.ts` in 19.2 so settings validation, prompt display names, route validation, and reader tabs share one frozen table. -- Create: `src/server/translation/source-loader.ts` -- Create: `src/server/translation/current-translation.ts` in 19.3. This file is owned by the job-state/current-translation domain; reader route work consumes it but does not edit it. -- Create: `src/server/translation/markdown-blocks.ts` -- Create: `src/server/translation/chunker.ts` -- Create: `src/server/translation/job-state.ts` -- Create: `src/server/translation/codex-app-server.ts` -- Create: `src/server/translation/prompt.ts` -- Create: `src/server/translation/validator.ts` -- Create: `src/server/translation/stitcher.ts` -- Create: `src/server/translation/atomic-writer.ts` -- Create: `src/server/translation/events.ts` -- Create: `src/server/translation/orchestrator.ts` -- Create: `src/server/translation/job-runner.ts` - -### API routes - -- Create route files that implement these endpoint paths, following the existing route-file convention in `src/routes/api/`: -- `POST /api/memories/:memory_id/translations` -- `GET /api/memories/:memory_id/translations/:lang_code` -- `GET /api/translation-jobs/:job_id` -- `GET /api/translation-jobs/:job_id/events` -- `POST /api/translation-jobs/:job_id/cancel` - -### Settings and auth - -- Modify: `src/components/settings/SettingsPage.tsx` -- Modify: current settings persistence schema/repository used for SQLite-backed settings -- Modify or create: `src/server/settings/codex-auth.ts` -- Create: `src/server/settings/translation-language.ts` if no focused settings service exists -- Modify: current settings API routes under `src/routes/api/settings*` - -### Reader frontend - -- Modify: `src/server/reader/page-data.ts` -- Modify: `src/routes/memories/[id].tsx` -- Create or modify: `src/routes/memories/[langCode]/[id].tsx` -- Modify: `src/components/reader/MemoryReader.tsx` -- Implement or modify variant tabs, translation controls, and translation - progress inside `src/components/reader/MemoryReader.tsx` unless a later - refactor deliberately extracts focused child components. - -### Skill and fixtures - -- Create: `.agents/skills/reader-translate/SKILL.md` -- Create: `tests/fixtures/translation/simple-article.md` -- Create: `tests/fixtures/translation/academic-paper.md` -- Create: `tests/fixtures/translation/hostile-prompt-injection.md` -- Create: `tests/fixtures/translation/markdown-protected-spans.md` - -## Parallel write-scope rule - -A subagent may edit only the files owned by its assigned subtask. Shared files such as `types.ts`, `schema.ts`, and route contracts must be frozen before downstream workers edit dependent code. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/02-types-state-and-settings.md b/docs/workflows/archive/task-19-codex-translation/contracts/02-types-state-and-settings.md deleted file mode 100644 index cd2199b8..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/02-types-state-and-settings.md +++ /dev/null @@ -1,443 +0,0 @@ -# Brilliant types, state, and settings contract - -## TypeScript contracts - -Create shared interfaces in `src/server/translation/types.ts` and import them everywhere. - -```ts -export type TranslationJobStatus = - | "pending" - | "running" - | "stale" - | "cancel_requested" - | "canceled" - | "unavailable" - | "stitching" - | "committing" - | "complete" - | "failed"; - -export type TranslationChunkStatus = - | "pending" - | "running" - | "validating" - | "retrying" - | "complete" - | "purged" - | "failed"; - -export type TranslationBlockType = - | "heading" - | "paragraph" - | "list" - | "blockquote" - | "table" - | "code_fence" - | "inline_code_paragraph" - | "math_block" - | "html_block" - | "image_figure" - | "caption" - | "footnote" - | "bibliography_entry" - | "unknown_raw"; - -export interface TranslationSourceSnapshot { - memoryId: string; - sourcePath: string; - sourceMarkdown: string; - sourceHash: string; - byteSize: number; - roughTokenEstimate: number; - title: string | null; - sourceUrl: string | null; - documentType: "article" | "paper" | "unknown"; -} - -export interface TranslationJobSnapshot { - job_id: string; - memory_id: string; - lang_code: string; - status: TranslationJobStatus; - source_hash: string; - chunk_count: number; - completed_chunks: number; - failed_chunks: number; - retrying_chunks: number; - output_path: string | null; - reader_url: string | null; - error: TranslationJobSnapshotError | null; -} - -export type TranslationErrorAction = - | "open_settings" - | "setup_codex_auth" - | "retry" - | "open_source_reader" - | "start_fresh_translation" - | "none"; - -export type TranslationErrorCode = - | "translation_unavailable" - | "translation_language_required" - | "translation_language_mismatch" - | "invalid_language" - | "missing_memory" - | "missing_source_content" - | "auth_required" - | "setup_required" - | "app_server_unavailable" - | "app_server_protocol_error" - | "stale_source" - | "cancellation_conflict" - | "usage_limit" - | "context_overflow" - | "timeout" - | "stream_disconnected" - | "invalid_final_output" - | "validation_failed" - | "filesystem_failure" - | "unknown"; - -export type PersistableTranslationErrorCode = Exclude< - TranslationErrorCode, - | "translation_language_required" - | "translation_language_mismatch" - | "invalid_language" - | "missing_memory" - | "missing_source_content" - | "cancellation_conflict" ->; - -export interface TranslationJobSnapshotError { - code: TranslationErrorCode; - message: string; - action?: TranslationErrorAction; -} - -export type TranslationUnavailableReason = - | "output_missing" - | "output_hash_mismatch"; - -export interface TranslationPersistedError { - code: PersistableTranslationErrorCode; - message: string; - action?: TranslationErrorAction; - reason?: TranslationUnavailableReason | string; -} - -`TranslationPersistedError` is the storage format for both -`translation_jobs.error` and `translation_chunks.error`. Store it as a JSON -string in SQLite, or `NULL` when no error exists. Do not store raw exception -strings, prompts, source chunks, credential paths, tokens, app-server URLs, or -raw app-server payloads in either column. - -export interface ProtectedSpan { - kind: - | "code_fence" - | "inline_code" - | "math" - | "html_tag" - | "url" - | "markdown_link_destination" - | "citation_marker" - | "footnote_marker" - | "identifier" - | "file_path" - | "command" - | "placeholder"; - value: string; - blockId: string; -} - -export interface TranslationBlock { - id: string; - type: TranslationBlockType; - markdown: string; - sectionPath: string[]; - protectedSpans: ProtectedSpan[]; - metadata: Record; -} - -export interface TranslationChunk { - jobId: string; - memoryId: string; - langCode: string; - sourceHash: string; - chunkIndex: number; - chunkCount: number; - sectionPath: string[]; - docTitle: string | null; - sourceUrl: string | null; - documentType: "article" | "paper" | "unknown"; - styleProfile: string | null; - glossary: Record; - blockIds: string[]; - sourceMarkdown: string; - sourceChunkHash: string; - segments: TranslationTextSegment[]; -} - -export interface CodexChunkOutput { - chunk_index: number; - segments: Array<{ - id: string; - translated_text: string; - }>; - translated_markdown: string; - warnings: string[]; -} - -export interface TranslationTextSegment { - blockId: string; - id: string; - sourceEnd: number; - sourceStart: number; - text: string; -} -``` - -## Retry constants - -```ts -export const BRILLIANT_MAX_RETRIES = 3; -``` - -`BRILLIANT_MAX_RETRIES` is a fixed MVP constant. Do not read retry budget from -mutable runtime settings during a job. If retry policy becomes configurable in a -later task, copy the selected value into `translation_jobs` at job creation and -use that copied value for all retry and recovery decisions for the job. - -`TranslationErrorCode` is the shared safe error-code namespace used by API error -responses, job snapshots, and SSE failure events. `TranslationPersistedError` -uses the narrower `PersistableTranslationErrorCode` because request-boundary -errors such as `translation_language_required`, `translation_language_mismatch`, -`missing_memory`, `missing_source_content`, `invalid_language`, and -`cancellation_conflict` must not be stored as job/chunk lifecycle failures in -SQLite. `auth_required` and `setup_required` are special: translation-start -precondition failures with those codes do not create or update job rows, but -the same codes are persistable when Codex auth/setup is lost after a job row has -already been created and the app-server reports them during execution. - -`TranslationJobSnapshot.reader_url` is derived, not stored. It is non-null only when a current committed translation exists for `(memory_id, lang_code, source_hash)` and the output file hash matches the completed translation row. For pending, running, cancel-requested, canceled, failed, stale, or renderable-output-missing states, it is `null`. - -For `GET /api/translation-jobs/:job_id`, `reader_url` is non-null only when the -job is complete, its `source_hash` still matches the current source -`CONTENT.md` hash, its output file exists, and the output file hash matches -`translation_jobs.output_hash`. A historical complete job for an older source -hash reports `reader_url = null`. - -For `status = "unavailable"`, public snapshots set `reader_url = null` and -`error.code = "translation_unavailable"`. The error message must be safe for UI -display and must not include local absolute paths unless the project-standard -diagnostics UI explicitly allows them. - -`TranslationJobSnapshot.completed_chunks` counts chunks whose status is -`complete` or `purged`. After a successful final commit and purge, a complete job -still reports `completed_chunks = chunk_count` even though chunk bodies have been -purged from SQLite. Use raw status counts only for internal diagnostics. - -## Event types - -```ts -export type TranslationEventType = - | "translation.job.started" - | "translation.chunk.queued" - | "translation.chunk.started" - | "translation.codex.delta" - | "translation.codex.item.started" - | "translation.codex.item.completed" - | "translation.chunk.validating" - | "translation.chunk.completed" - | "translation.chunk.failed" - | "translation.chunk.retrying" - | "translation.job.snapshot" - | "translation.job.stitching" - | "translation.job.committing" - | "translation.job.completed" - | "translation.job.failed" - | "translation.job.stale" - | "translation.job.canceled"; - -export interface TranslationEventEnvelope { - id: string; - type: TranslationEventType; - job_id: string; - memory_id: string; - lang_code: string; - chunk_index: number | null; - timestamp: number; - data: TData; -} -``` - -Terminal `translation.job.completed` event data must include: - -```ts -export interface TranslationJobCompletedData { - output_path: string; - output_hash: string; - reader_url: string; -} -``` - -Terminal `translation.job.stale` event data must include: - -```ts -export interface TranslationJobStaleData { - reason: "source_changed"; - job_source_hash: string; - current_source_hash: string; -} -``` - -Terminal `translation.job.failed` event data must include: - -```ts -export interface TranslationJobFailedData { - error: TranslationJobSnapshotError; -} -``` - -`translation.chunk.failed` event data must include: - -```ts -export interface TranslationChunkFailedData { - error: TranslationJobSnapshotError; - retry_count: number; - will_retry: boolean; -} -``` - -Failure event messages must be safe for UI display and must not include source -chunks, prompts, credential paths, tokens, app-server URLs, or raw app-server -payloads. - -`stale` is a terminal state for a job attempt. It is distinct from `failed` -because user action is to start a new translation for the changed source, not to -retry the old source hash. - -## Job transitions - -```text -pending -> running -pending -> stale -pending -> canceled -running -> stale -running -> stitching -running -> failed -running -> cancel_requested -cancel_requested -> canceled -complete -> unavailable only when the committed output file is missing or its hash no longer matches `output_hash` -stitching -> committing -stitching -> failed -committing -> complete -committing -> failed -failed remains immutable history; user retry creates a new `pending` job row -``` - -Completed jobs are immutable history while their committed output file remains available. Do not mutate `complete -> stale` when source content changes; Reader/API freshness is derived by comparing the job `source_hash` with the current source `CONTENT.md` hash. If the committed output file is missing or its hash no longer matches `output_hash`, mark the row `unavailable` so the same `(memory_id, lang_code, source_hash)` can be translated again. - -`unavailable` is a terminal history status. It is not active work, it must not be -scheduled by the runner, and it must not emit its own SSE terminal event. It is -surfaced through job snapshots and API error responses with -`error.code = "translation_unavailable"`. - -Cancellation is allowed only while a job is `pending`, `running`, or already in -`cancel_requested`. Requests for `cancel_requested` or `canceled` jobs are -idempotent. Requests for `stitching`, `committing`, `complete`, `stale`, -`failed`, or `unavailable` jobs return `cancellation_conflict`. - -## Hash contract - -Hash values use `sha256:`. - -Source hash input: - -- Hash the exact UTF-8 bytes read from `memories//CONTENT.md`. -- Resolve that path under configured `storePath`. -- Do not normalize line endings. -- Do not trim leading or trailing bytes. -- Do not parse and reserialize Markdown before hashing. -- If the file cannot be decoded as UTF-8 for Markdown parsing, fail source loading before creating translation chunks. - -Translated output hash input: - -- Hash the exact UTF-8 bytes of the committed translated `CONTENT.md` after atomic rename. - -## Chunk transitions - -```text -pending -> running -running -> validating -running -> failed -validating -> complete -validating -> retrying -retrying -> running -complete -> purged -failed -> retrying while retry_count < maxRetryCount -failed -> failed when retry_count >= maxRetryCount -``` - -Retry count semantics: - -- `maxRetries` / `maxRetryCount` means the number of additional retry attempts allowed after the initial attempt fails. -- `maxAttempts = 1 + maxRetries`. -- The initial attempt starts with `retry_count = 0`. -- Increment `retry_count` before each retry attempt is started. -- Exhaustion occurs when the next retry would make `retry_count > maxRetries`. - -## Settings language contract - -Canonical setting: - -```text -translation_target_lang_code = "ja-JP" -``` - -Supported language table: - -```ts -export const SUPPORTED_TRANSLATION_LANGUAGES = [ - { code: "ja-JP", displayName: "Japanese", nativeName: "日本語" }, - { code: "en-US", displayName: "English (US)", nativeName: "English" }, - { code: "en-GB", displayName: "English (UK)", nativeName: "English" }, - { code: "ko-KR", displayName: "Korean", nativeName: "한국어" }, - { code: "zh-CN", displayName: "Chinese (Simplified)", nativeName: "简体中文" }, - { code: "zh-TW", displayName: "Chinese (Traditional)", nativeName: "繁體中文" }, - { code: "fr-FR", displayName: "French", nativeName: "Français" }, - { code: "de-DE", displayName: "German", nativeName: "Deutsch" }, - { code: "es-ES", displayName: "Spanish", nativeName: "Español" }, - { code: "pt-BR", displayName: "Portuguese (Brazil)", nativeName: "Português (Brasil)" } -] as const; -``` - -Rules: - -- `/settings` lets the user select the translation target language. -- The settings API persists the selected value in SQLite. -- The persisted value must exactly match a `code` in `SUPPORTED_TRANSLATION_LANGUAGES`. -- `ja-JP` is the Japanese value. -- Canonical casing comes from the supported-language table; do not normalize and persist non-canonical casing such as `ja-jp` or `JA-JP`. -- Prompt target language labels and reader variant tab labels must come from the same supported-language table. -- Brilliant translation start reads this value server-side before creating a job. -- The browser may display the selected language, but the translation backend must not trust a client-provided language as the source of truth. -- If `POST /api/memories/:memory_id/translations` includes a `lang_code`, the backend must verify that it matches the persisted SQLite setting. -- If the request language differs from the persisted setting, reject with `409 translation_language_mismatch`. -- If no translation target language is configured, reject with `409 translation_language_required`. -- Old jobs retain their own `lang_code`; future jobs use the latest settings value. - -## Local runner contract - -Brilliant uses a local in-process runner for the MVP because TRAUMA is a local-first single-user app. - -Rules: - -- `POST /api/memories/:memory_id/translations` creates or reuses a job, schedules it on the local runner, and returns `202` or `200`. -- The runner processes one translation job at a time by default. -- Chunks inside a job are processed sequentially by default. -- Later concurrency tuning may add configurable chunk concurrency, but the MVP should avoid parallel Codex turns for the same document. -- Runner state is recoverable from SQLite job/chunk rows. -- On server startup, or before accepting a new translation job, recover interrupted `pending`, `running`, `stitching`, `committing`, and `cancel_requested` jobs according to the recovery contract. -- A `pending` job left by a process restart must either be scheduled if the source hash still matches or marked `stale` if the source changed before it started. -- A process restart may pause a job, but must not corrupt an existing completed translation. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/03-sqlite-and-repositories.md b/docs/workflows/archive/task-19-codex-translation/contracts/03-sqlite-and-repositories.md deleted file mode 100644 index 76d37d73..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/03-sqlite-and-repositories.md +++ /dev/null @@ -1,159 +0,0 @@ -# Brilliant SQLite and repository contract - -## Tables - -```sql -CREATE TABLE translation_jobs ( - job_id TEXT PRIMARY KEY, - memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - lang_code TEXT NOT NULL, - source_hash TEXT NOT NULL, - model TEXT, - prompt_policy_version TEXT NOT NULL, - chunker_version TEXT NOT NULL, - status TEXT NOT NULL, - chunk_count INTEGER NOT NULL DEFAULT 0, - output_path TEXT, - output_hash TEXT, - error TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - completed_at INTEGER -); - -CREATE UNIQUE INDEX translation_jobs_current_complete_idx - ON translation_jobs(memory_id, lang_code, source_hash) - WHERE status = 'complete'; - -CREATE UNIQUE INDEX translation_jobs_active_idx - ON translation_jobs(memory_id, lang_code, source_hash) - WHERE status IN ('pending', 'running', 'cancel_requested', 'stitching', 'committing'); - -CREATE INDEX translation_jobs_memory_lang_idx - ON translation_jobs(memory_id, lang_code, updated_at); - -CREATE TABLE translation_chunks ( - job_id TEXT NOT NULL REFERENCES translation_jobs(job_id) ON DELETE CASCADE, - chunk_index INTEGER NOT NULL, - source_chunk_hash TEXT NOT NULL, - block_ids_json TEXT NOT NULL, - status TEXT NOT NULL, - retry_count INTEGER NOT NULL DEFAULT 0, - projection_spans_json TEXT, - translated_markdown TEXT, - translated_hash TEXT, - error TEXT, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY (job_id, chunk_index) -); - -CREATE INDEX translation_chunks_status_idx - ON translation_chunks(job_id, status, chunk_index); - -CREATE TABLE translation_projection_spans ( - job_id TEXT NOT NULL REFERENCES translation_jobs(job_id) ON DELETE CASCADE, - span_index INTEGER NOT NULL, - memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE, - lang_code TEXT NOT NULL, - source_hash TEXT NOT NULL, - output_hash TEXT NOT NULL, - segment_id TEXT NOT NULL, - block_id TEXT NOT NULL, - source_markdown_start INTEGER NOT NULL, - source_markdown_end INTEGER NOT NULL, - translated_markdown_start INTEGER NOT NULL, - translated_markdown_end INTEGER NOT NULL, - source_reader_start INTEGER NOT NULL, - source_reader_end INTEGER NOT NULL, - translated_reader_start INTEGER NOT NULL, - translated_reader_end INTEGER NOT NULL, - created_at INTEGER NOT NULL, - updated_at INTEGER NOT NULL, - PRIMARY KEY (job_id, span_index) -); - -CREATE INDEX translation_projection_current_idx - ON translation_projection_spans(memory_id, lang_code, source_hash, output_hash, span_index); -``` - -## Rules - -- `source_hash`, `source_chunk_hash`, and `output_hash` use `sha256:`. -- `output_path` is store-relative, for example `memories/abc123/ja-JP/CONTENT.md`. -- `reader_url` is derived from `memory_id` and `lang_code` as `/memories//`; do not add a column unless implementation proves a durable need. -- `translation_jobs.error` stores either `NULL` or a JSON string matching `TranslationPersistedError` from `contracts/02-types-state-and-settings.md`. -- `translation_chunks.error` stores either `NULL` or a JSON string matching `TranslationPersistedError` from `contracts/02-types-state-and-settings.md`. -- Do not persist request-boundary API errors such as `translation_language_required`, `translation_language_mismatch`, `invalid_language`, `missing_memory`, `missing_source_content`, or `cancellation_conflict` in `translation_jobs.error` or `translation_chunks.error`. -- `auth_required` and `setup_required` are not persisted when they happen before job creation. They may be persisted only for an already-created job when Codex auth/setup is lost during app-server execution. -- `markTranslationUnavailable(jobId, reason)` stores `error` as JSON with `code = "translation_unavailable"`, `action = "start_fresh_translation"`, and `reason = "output_missing"` or `"output_hash_mismatch"`. -- `translated_markdown` is temporary and must be `NULL` after final commit and purge. -- `projection_spans_json` is temporary chunk-local alignment data and must be `NULL` after final commit and purge. -- `translation_projection_spans` is durable runtime alignment data for translated reader annotations. Query it only with the current `(memory_id, lang_code, source_hash, output_hash)`. -- Do not add token, refresh token, credential, or raw Codex auth columns. -- The user-selected target language is persisted in SQLite settings state, not frontend-only state. -- Translation jobs copy the currently configured settings language into `translation_jobs.lang_code` at job creation. -- Settings language writes must validate against `SUPPORTED_TRANSLATION_LANGUAGES` from the shared language contract. -- Failed, canceled, and stale jobs remain as history and must not block a user retry for the same `(memory_id, lang_code, source_hash)`. -- `unavailable` jobs are historical records for completed translations whose output file is missing or whose file hash no longer matches `output_hash`; they must not block a user retry for the same `(memory_id, lang_code, source_hash)`. -- At most one complete job and at most one active job may exist for the same `(memory_id, lang_code, source_hash)`. - -## Required repository methods - -Expose these methods or exact equivalents: - -```ts -createTranslationJob(input): Promise -getTranslationJob(jobId): Promise -findCompleteTranslationRecord(memoryId, langCode, sourceHash): Promise -findActiveTranslationJob(memoryId, langCode, sourceHash): Promise -updateTranslationJobStatus(jobId, status, patch): Promise -claimTranslationJob(jobId, expectedStatus: "pending"): Promise -cancelPendingTranslationJob(jobId): Promise -requestRunningTranslationJobCancellation(jobId): Promise -markTranslationUnavailable(jobId, reason: "output_missing" | "output_hash_mismatch"): Promise -insertTranslationChunks(jobId, chunks): Promise -getTranslationChunks(jobId): Promise -updateTranslationChunk(jobId, chunkIndex, patch): Promise -purgeCompletedTranslationChunks(jobId): Promise -countTranslationChunksByStatus(jobId): Promise> -getTranslationTargetLanguage(): Promise -setTranslationTargetLanguage(langCode: string): Promise -``` - -Progress aggregation rules: - -- `completed_chunks` in public snapshots equals `complete + purged`. -- `failed_chunks` equals chunks with status `failed`. -- `retrying_chunks` equals chunks with status `retrying`. -- Raw per-status counts may still be returned by repository/internal methods for diagnostics, but frontend/API snapshot math must use the public aggregation above. - -Runner claim rules: - -- `claimTranslationJob()` is a compare-and-set transition used by the runner to atomically claim `pending -> running`. -- It returns `false` if another runner tick already claimed, canceled, failed, or otherwise changed the job. -- `cancelPendingTranslationJob()` is a compare-and-set transition used by the cancel API to atomically claim `pending -> canceled`. -- `requestRunningTranslationJobCancellation()` is a compare-and-set transition used by the cancel API to atomically claim `running -> cancel_requested`. -- If either cancellation compare-and-set returns `false`, the cancel API reloads the job and branches on the current status instead of assuming the previous state still holds. -- In-flight Codex `threadId` and `turnId` are not persisted in SQLite for Brilliant MVP. They live only in the local in-process runner registry. -- After restart, a `cancel_requested` job without in-memory ids is recovered as `canceled`. - -Current-translation lookup rules: - -- Repository methods remain SQLite-only. They do not read `storePath`, check file existence, compute output file hashes, or open `CONTENT.md`. -- `findCompleteTranslationRecord()` returns only the SQLite `status = 'complete'` row for `(memory_id, lang_code, source_hash)`. -- `src/server/translation/current-translation.ts` owns current-translation resolution and is the single shared boundary for storePath resolution, output file existence checks, output hash verification, unavailable repair, and `reader_url` derivation. -- It exposes a read-only resolver and an explicit repair helper: - - `resolveCurrentTranslationReadOnly()` verifies current output and derives `reader_url` without mutating SQLite. - - `repairUnavailableTranslation()` marks a known broken complete row `unavailable` after the caller has decided mutation is allowed. -- Job start, current translation metadata API, and job status/snapshot API may call `repairUnavailableTranslation()` because they are backend API boundaries that can return `translation_unavailable`, expose unavailable snapshots, or create a replacement job. -- Reader route loading and variant tab page-data must call `resolveCurrentTranslationReadOnly()` and must not mutate SQLite while rendering. If they detect missing or hash-mismatched output, they return not-found/unavailable UI state and leave repair to API/job-start recovery. -- The partial unique index on `status = 'complete'` remains valid because `unavailable` rows are not current and do not block replacement translations. - -## Path constraints - -- Source content remains store-relative `memories//CONTENT.md` under configured `storePath`. -- Translated content is store-relative `memories///CONTENT.md` under configured `storePath`. -- Do not create a parallel singular `memory/` storage tree. -- `lang_code` must be validated before path resolution. -- Never store translated content outside the memory directory. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/04-api-and-sse.md b/docs/workflows/archive/task-19-codex-translation/contracts/04-api-and-sse.md deleted file mode 100644 index 7d5480e9..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/04-api-and-sse.md +++ /dev/null @@ -1,362 +0,0 @@ -# Brilliant API and SSE contract - -## Start or reuse translation - -```http -POST /api/memories/:memory_id/translations -content-type: application/json - -{ - "lang_code": "ja-JP" -} -``` - -For the MVP frontend, the request body may be `{}`. The backend reads `translation_target_lang_code` from SQLite and uses that value. If `lang_code` is present, it is a consistency assertion, not the canonical source. - -New job response: - -```json -{ - "status": "started", - "job_id": "018f...", - "memory_id": "018f...", - "lang_code": "ja-JP", - "source_hash": "sha256:...", - "event_url": "/api/translation-jobs/018f.../events" -} -``` - -Current translation response: - -```json -{ - "status": "current", - "job_id": "018f...", - "memory_id": "018f...", - "lang_code": "ja-JP", - "source_hash": "sha256:...", - "output_path": "memories/018f.../ja-JP/CONTENT.md", - "reader_url": "/memories/ja-JP/018f..." -} -``` - -Active job reuse response: - -```json -{ - "status": "active", - "job_status": "pending", - "job_id": "018f...", - "memory_id": "018f...", - "lang_code": "ja-JP", - "source_hash": "sha256:...", - "event_url": "/api/translation-jobs/018f.../events" -} -``` - -Status codes: - -- `202` for newly started async job -- `200` for current committed translation or active job reuse -- `400` for invalid language code -- `404` for missing memory or source content -- `409` for missing configured target language, request/setting language mismatch, Codex auth/setup required, stale running conflict, or cancellation conflict -- `502` for invalid final Codex/model output -- `503` for configured but unavailable Codex app-server -- `504` for Codex timeout -- `500` for unexpected server failure - -Error response shape: - -```json -{ - "status": "error", - "code": "translation_language_required", - "message": "Translation target language is not configured.", - "action": "open_settings" -} -``` - -Rules: - -- `code` is the stable frontend branch key. -- `message` is safe for display and must not include source chunks, prompts, - credential paths, tokens, app-server URLs, or raw app-server payloads. -- `action` is optional and uses one of `open_settings`, `setup_codex_auth`, - `retry`, `open_source_reader`, `start_fresh_translation`, or `none`. - -Active reuse rules: - -- `POST /api/memories/:memory_id/translations` returns `200` with `status = "active"` and the actual `job_status` for reused jobs in `pending`, `running`, `stitching`, or `committing`. -- The response must not collapse reused `pending`, `stitching`, or `committing` jobs into `status = "running"`. -- A reused active response always includes `event_url`; the frontend then reads `GET /api/translation-jobs/:job_id` or the first SSE `translation.job.snapshot` for exact progress state. -- If creating a new job loses the `translation_jobs_active_idx` uniqueness race, reload the active job for the same `(memory_id, lang_code, source_hash)` and apply these same active reuse or cancellation-conflict rules instead of surfacing the raw SQLite uniqueness error. -- A `cancel_requested` job remains covered by the active unique index until cancellation reaches `canceled`, but start/reuse does not return it as an active translation. Return `409` with `code = "cancellation_conflict"` and `action = "none"` so the user can retry after cancellation completes. - -Required error codes: - -- `translation_language_required` -- `translation_language_mismatch` -- `invalid_language` -- `missing_memory` -- `missing_source_content` -- `auth_required` -- `setup_required` -- `app_server_unavailable` -- `app_server_protocol_error` -- `translation_unavailable` -- `stale_source` -- `cancellation_conflict` -- `usage_limit` -- `context_overflow` -- `timeout` -- `stream_disconnected` -- `invalid_final_output` -- `validation_failed` -- `filesystem_failure` -- `unknown` - -HTTP mapping: - -- `translation_language_required`: `409` -- `translation_language_mismatch`: `409` -- `invalid_language`: `400` -- `missing_memory`: `404` -- `missing_source_content`: `404` -- `auth_required`: `409` -- `setup_required`: `409` -- `app_server_unavailable`: `503` -- `app_server_protocol_error`: `502` -- `translation_unavailable`: `409` -- `stale_source`: `409` -- `cancellation_conflict`: `409` -- `usage_limit`: `409` -- `context_overflow`: `409` -- `timeout`: `504` -- `stream_disconnected`: `503` -- `invalid_final_output`: `502` -- `validation_failed`: `409` -- `filesystem_failure`: `500` -- `unknown`: `500` - -## Read committed translation metadata - -```http -GET /api/memories/:memory_id/translations/:lang_code -``` - -```json -{ - "memory_id": "018f...", - "lang_code": "ja-JP", - "status": "current", - "source_hash": "sha256:...", - "output_hash": "sha256:...", - "output_path": "memories/018f.../ja-JP/CONTENT.md", - "reader_url": "/memories/ja-JP/018f...", - "completed_at": "2026-05-20T00:00:00.000Z" -} -``` - -Rules: - -- This endpoint returns only current committed translation metadata. -- It must use `resolveCurrentTranslationReadOnly()` from `src/server/translation/current-translation.ts`. -- If no complete translation exists for the current source hash, return the project-standard not-found response. -- If a complete row exists but the output file is missing or hash-mismatched, call `repairUnavailableTranslation()` to mark the row `unavailable` and return `409` with `code = "translation_unavailable"`. -- Clients recover from `translation_unavailable` by navigating to the source reader route `/memories/:id` and starting a fresh translation through `POST /api/memories/:memory_id/translations`; do not retry this metadata endpoint as the recovery action. -- It must not silently return metadata for stale, unavailable, missing, or hash-mismatched output. - -`translation.job.snapshot` uses the same payload shape as `GET /api/translation-jobs/:job_id`. - -`reader_url` is derived, not stored. It is non-null only when a current committed translation exists for `(memory_id, lang_code, source_hash)` and the output file hash matches the completed translation row. For pending, running, cancel-requested, canceled, failed, stale, or renderable-output-missing states, `reader_url` is `null`. - -For historical completed jobs whose `source_hash` no longer matches the current -source `CONTENT.md` hash, `GET /api/translation-jobs/:job_id` returns -`reader_url: null` even if the old translated file still exists. For complete -jobs whose output file is missing or hash-mismatched, the job status API may -call `repairUnavailableTranslation()`, mark the job `unavailable`, and return -`reader_url: null`. - -Unavailable job status response: - -```json -{ - "job_id": "018f...", - "memory_id": "018f...", - "lang_code": "ja-JP", - "status": "unavailable", - "source_hash": "sha256:...", - "chunk_count": 42, - "completed_chunks": 42, - "failed_chunks": 0, - "retrying_chunks": 0, - "output_path": null, - "reader_url": null, - "error": { - "code": "translation_unavailable", - "message": "The translated output is no longer available. Start a new translation.", - "action": "start_fresh_translation" - } -} -``` - -## Read job status - -```http -GET /api/translation-jobs/:job_id -``` - -```json -{ - "job_id": "018f...", - "memory_id": "018f...", - "lang_code": "ja-JP", - "status": "running", - "source_hash": "sha256:...", - "chunk_count": 42, - "completed_chunks": 13, - "failed_chunks": 0, - "retrying_chunks": 1, - "output_path": null, - "reader_url": null, - "error": null -} -``` - -`completed_chunks` in this response counts chunks with status `complete` or -`purged`, so a successfully committed-and-purged job still reports all chunks as -completed. - -## Stream job events - -```http -GET /api/translation-jobs/:job_id/events -accept: text/event-stream -``` - -SSE message: - -```text -id: 000000000013 -event: translation.chunk.completed -data: {"id":"000000000013","type":"translation.chunk.completed","job_id":"018f...","memory_id":"018f...","lang_code":"ja-JP","chunk_index":3,"timestamp":1710000000000,"data":{"translated_hash":"sha256:..."}} -``` - -Completed event data: - -```json -{ - "output_path": "memories/018f.../ja-JP/CONTENT.md", - "output_hash": "sha256:...", - "reader_url": "/memories/ja-JP/018f..." -} -``` - -Job failed event data: - -```json -{ - "error": { - "code": "timeout", - "message": "The Codex turn timed out.", - "action": "retry" - } -} -``` - -Chunk failed event data: - -```json -{ - "error": { - "code": "validation_failed", - "message": "Translated chunk failed validation.", - "action": "retry" - }, - "retry_count": 1, - "will_retry": true -} -``` - -Failure event error objects use the same `code`, `message`, and optional -`action` contract as `TranslationJobSnapshotError`. Chunk failure events derive -their `error` object from the structured `translation_chunks.error` JSON when it -exists; otherwise they synthesize a safe structured error before emitting. - -Stale event data: - -```json -{ - "reason": "source_changed", - "job_source_hash": "sha256:...", - "current_source_hash": "sha256:..." -} -``` - -Rules: - -- Event ids are monotonic decimal strings padded to 12 digits per job. -- MVP does not require a durable event replay table. -- On reconnect, emit `translation.job.snapshot` first using current SQLite job/chunk state, then stream new events. -- `Last-Event-ID` support may be added later if an in-memory or SQLite event buffer is implemented. -- Send heartbeat comments every 15 seconds while the job is active. -- Stream closes after completed, failed, stale, or canceled terminal events. -- Stream disconnect does not cancel the backend job. -- Frontend completion navigation uses `translation.job.completed.data.reader_url`; it must not reconstruct a different route shape. -- Frontend failure rendering uses failure event `data.error.code` as the stable branch key, not free-form `message`. - -## Cancel job - -```http -POST /api/translation-jobs/:job_id/cancel -``` - -Cancelable statuses: - -- `pending` with no in-flight Codex turn transitions directly to `canceled`. -- `running` transitions to `cancel_requested`. -- Both transitions are compare-and-set operations. If `pending -> canceled` loses the race to the runner's `pending -> running` claim, reload the job and apply `running -> cancel_requested`. -- `cancel_requested` finalizes to `canceled` when `turn/interrupt` is acknowledged, `turn.completed` reports interrupted status, no in-flight registry ids are available, or `BRILLIANT_CANCEL_GRACE_MS = 30000` expires. -- While a job remains `cancel_requested`, start/reuse returns `409 cancellation_conflict` for the same `(memory_id, lang_code, source_hash)`. -- `cancel_requested` and `canceled` are idempotent success responses. -- `stitching`, `committing`, `complete`, `stale`, `failed`, and `unavailable` - return `409` with `code = "cancellation_conflict"`. - -Pending cancellation response: - -```json -{ - "job_id": "018f...", - "status": "canceled" -} -``` - -Running cancellation response: - -```json -{ - "job_id": "018f...", - "status": "cancel_requested" -} -``` - -Already-canceled idempotent response: - -```json -{ - "job_id": "018f...", - "status": "canceled" -} -``` - -Conflict response: - -```json -{ - "status": "error", - "code": "cancellation_conflict", - "message": "This translation job can no longer be canceled.", - "action": "none" -} -``` diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/05-markdown-chunking.md b/docs/workflows/archive/task-19-codex-translation/contracts/05-markdown-chunking.md deleted file mode 100644 index 11470523..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/05-markdown-chunking.md +++ /dev/null @@ -1,98 +0,0 @@ -# Brilliant Markdown chunking contract - -## Parser-backed chunking algorithm - -1. Parse frontmatter separately. Frontmatter is metadata, not a translatable body block. - Preserve the exact raw frontmatter bytes/text so stitching can prepend it unchanged to translated output when the source file had frontmatter. -2. Parse Markdown with the unified/remark/mdast parser stack for segment extraction and structural preservation. The implementation must not attempt to cover the Markdown dialect through ad hoc regex scanning. Line-oriented fallback logic may exist only for controlled diagnostics or migration compatibility. -3. Create translatable text segments from mdast `text` nodes with stable source offsets. -4. Exclude code, inline code, math, HTML, definitions, footnote identifiers, link/image destinations, image metadata, autolink URL text, and frontmatter from translatable segments. -5. Use legacy block grouping only as a chunk boundary compatibility layer: - - Treat fenced code blocks as one `code_fence` block from opening fence to closing fence. - - Treat `$$` math blocks as one `math_block` block. - - Treat contiguous HTML block lines as one `html_block` when they start with block-level tags or comments. - - Treat ATX headings as `heading` blocks and update `sectionPath`. - - Treat contiguous table lines as one `table` block. - - Treat contiguous list lines, including indented continuation lines, as one `list` block. - - Treat contiguous blockquote lines as one `blockquote` block. - - Treat Markdown image lines, image-in-link lines, or figure HTML as `image_figure` blocks. - - Treat likely caption lines immediately following image/figure as `caption` blocks. - - Treat footnote definitions as `footnote` blocks. - - Treat bibliography/reference entries under references-like headings as `bibliography_entry` blocks. - - Treat other prose paragraphs containing inline code as `inline_code_paragraph`. - - Treat other prose paragraphs as `paragraph`. - - Use `unknown_raw` only when the scanner cannot classify without risking structural damage. - -## Block id rule - -```text -b000001, b000002, b000003, ... in source order after frontmatter removal -``` - -## Segment id rule - -```text -s000001, s000002, s000003, ... in source order within each chunk -``` - -Segment ids are prompt-local. They are deterministic for the chunk source -Markdown and are not stored as durable database state. - -## Chunking defaults - -```ts -export const DEFAULT_TRANSLATION_CHUNK_CONFIG = { - maxRoughTokens: 2500, - softRoughTokens: 1800, - maxBlocks: 80, - maxRetries: 3, - minLengthRatio: 0.35, - maxLengthRatio: 2.8, -} as const; -``` - -`maxRetries` is the number of retry attempts after the initial attempt. With -`maxRetries: 3`, the runner may make at most four total attempts for one chunk: -one initial attempt plus three retry attempts. - -## Chunking rules - -- Prefer section boundaries from heading path. -- Group adjacent small sections while under `softRoughTokens`. -- Split oversized sections by contiguous block groups under `maxRoughTokens`. -- Never split inside a block. -- If a single block exceeds `maxRoughTokens`, mark the chunk as oversized and let Codex validation/retry handle context errors. -- Do not slice an oversized block unless a later task defines block-specific splitting. - -## Required fixture example - -```md -# Heading - -Paragraph with `inlineCode` and [a link](https://example.com). - -$$ -E = mc^2 -$$ - -```ts -const value = "do not translate"; -``` - -| Term | Meaning | -| --- | --- | -| API | Application interface | - -[^1]: Footnote text. -``` - -Expected ids: - -```text -b000001 heading -b000002 inline_code_paragraph -b000003 math_block -b000004 code_fence -b000005 table -b000006 footnote -``` diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md b/docs/workflows/archive/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md deleted file mode 100644 index c29d381b..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/06-codex-prompt-and-validation.md +++ /dev/null @@ -1,353 +0,0 @@ -# Brilliant Codex prompt and validation contract - -## Codex app-server client - -```ts -export interface CodexAppServerClient { - checkAuth(): Promise; - startDeviceCodeLogin(): Promise; - observeAuthEvents(): AsyncIterable; - cancelDeviceCodeLogin(input: { loginId: string }): Promise; - logout(): Promise; - translateChunk(input: CodexTranslateChunkInput): AsyncIterable; - cancelTurn(input: { threadId: string; turnId: string }): Promise; -} -``` - -Supporting types: - -```ts -export interface CodexAppServerConfig { - endpoint: string; - transport: "unix_socket"; - healthProbeUrl?: string; - healthTimeoutMs: number; - requestTimeoutMs: number; -} - -export type CodexAuthStatus = - | { status: "enabled" } - | { status: "setup_required"; reason: string } - | { status: "disabled"; reason: string } - | { status: "unknown"; reason: string } - | { status: "error"; error: string }; - -export interface CodexDeviceCodeLogin { - loginId: string; - userCode: string; - verificationUrl: string; -} - -export type CodexDeviceCodeLoginState = - | { status: "login_started"; loginId: string; verificationUrl: string; userCode: string } - | { status: "enabled" } - | { status: "setup_required"; reason: string } - | { status: "failed"; loginId: string | null; error: string } - | { status: "canceled"; loginId: string }; - -export type CodexAuthEvent = - | { type: "account.login.completed"; loginId: string | null; success: boolean; error: string | null } - | { type: "account.updated" }; - -export type CodexLogoutResult = - | { status: "logged_out" } - | { status: "unsupported"; message: string }; - -export interface CodexTranslateChunkInput { - jobId: string; - memoryId: string; - langCode: string; - chunkIndex: number; - prompt: string; - outputSchema: Record; - timeoutMs: number; -} - -export type CodexAppServerEvent = - | { type: "thread.started"; threadId: string } - | { type: "turn.started"; threadId: string; turnId: string } - | { type: "item.started"; threadId: string; turnId: string; itemId: string; title: string | null } - | { type: "item.agentMessage.delta"; threadId: string; turnId: string; itemId: string; delta: string } - | { type: "item.completed"; threadId: string; turnId: string; itemId: string; outputText: string } - | { type: "turn.completed"; threadId: string; turnId: string; status: "completed" | "interrupted" | "failed" } - | { type: "turn.failed"; threadId: string | null; turnId: string | null; error: CodexAppServerError }; - -export interface CodexAppServerError { - code: - | "auth_required" - | "setup_required" - | "app_server_unavailable" - | "app_server_protocol_error" - | "usage_limit" - | "context_overflow" - | "stream_disconnected" - | "timeout" - | "invalid_final_output" - | "unknown"; - message: string; -} -``` - -Rules: - -- MVP connects to an already-running Codex app-server through server-side config. -- Use `TRAUMA_CODEX_APP_SERVER_ENDPOINT` or the equivalent typed TRAUMA config value as `endpoint`. -- Default Brilliant MVP configuration is an explicitly configured Codex app-server Unix socket listener. This is TRAUMA's default, not the Codex CLI's no-flag default. -- Default startup command: `codex app-server --listen unix:///tmp/trauma-codex.sock`. -- Default endpoint example: `TRAUMA_CODEX_APP_SERVER_ENDPOINT=unix:///tmp/trauma-codex.sock`. -- Unix socket endpoint URIs use three slashes and an absolute filesystem path, for example `unix:///tmp/trauma-codex.sock`. -- `codex app-server` without `--listen unix:///tmp/trauma-codex.sock` uses `stdio`; do not treat that process as a Brilliant endpoint. -- Loopback WebSocket is not supported by TRAUMA. -- If Unix socket support is blocked by platform/runtime support, treat that as a blocked integration task rather than silently falling back to WebSocket. -- The app-server client speaks the Codex app-server wire protocol over the configured transport. Do not treat `account/read`, `thread/start`, `turn/start`, or `turn/interrupt` as REST endpoints. -- Codex app-server wire messages use the app-server's documented JSON-RPC-like envelope and omit a top-level `jsonrpc: "2.0"` field. Do not use a generic JSON-RPC client that injects `jsonrpc` unless generated schema/fixtures prove it is accepted by the installed Codex app-server. -- Request envelope shape is `{ "method": "...", "params": {...}, "id": 1 }`. -- Response envelope shape is `{ "id": 1, "result": {...} }` or `{ "id": 1, "error": { "code": 123, "message": "..." } }`. -- Notification envelope shape is `{ "method": "...", "params": {...} }`. -- Immediately after opening a connection, send one `initialize` request with TRAUMA client metadata and then send the `initialized` notification. No app-server method may run before that handshake. -- Brilliant initializes against the stable schema and does not request - `experimentalApi`. Any request field present only in the generated - `--experimental` schema is out of scope unless a later task explicitly opts - into that capability. -- Do not auto-start Codex app-server in the MVP. If app-server process management is added later, define it as a separate subtask. -- If `endpoint` is missing, return `setup_required`. -- Brilliant MVP supports the app-server wire protocol only over a Unix socket. -- HTTP is not an app-server wire-protocol transport for Brilliant. It may be used only for app-server health probes such as `/readyz` or `/healthz` when the selected endpoint exposes them. -- Reject `http://` and `https://` endpoints for app-server wire-protocol calls with `setup_required`. -- Reject `stdio` process ownership because TRAUMA does not auto-start or supervise the app-server process in Brilliant MVP. -- Reject WebSocket endpoints in the MVP. Remote app-server exposure and WebSocket bearer/capability-token management require a separate security subtask. -- If `endpoint` is configured but health/auth probing fails due connection failure or timeout, return `app_server_unavailable`. -- Run `account/read` before scheduling translation work. A non-null `account` - confirms usable auth even when `requiresOpenaiAuth` is `true`. Treat - `requiresOpenaiAuth: true` as `auth_required` or `setup_required` only when - no ChatGPT/API account is available. -- Do not fall back to `codex exec` from this app-server client. -- Use one ephemeral `thread/start` per chunk attempt, then app-server `turn/start`. -- Stable `thread/start` uses only `cwd`, `ephemeral`, `approvalPolicy`, - `approvalsReviewer`, `sandbox`, and `threadSource` for Brilliant. It omits - `environments`, `experimentalRawEvents`, and `persistExtendedHistory`. -- Stable `turn/start` uses `threadId`, `input`, `approvalPolicy`, - `approvalsReviewer`, `sandboxPolicy`, and `outputSchema` when structured - output is attempted. It omits `environments`. -- Prefer `outputSchema` on `turn/start`. If the configured app-server rejects or does not advertise `outputSchema`, fall back to prompt-only JSON output and require the same `CodexChunkOutput` validation before persistence. If the app-server rejects both structured output and prompt-only JSON output, fail the chunk with `invalid_final_output`. -- The concrete output schema builder is owned by 19.8. The app-server client accepts a schema object from caller code rather than defining Brilliant translation schema internally. -- Do not send the full document unless the chunker produced one chunk. -- Do not let Codex write files. -- Do not expose app-server URL, token, or raw auth state to the browser. -- Deltas are progress only. Final output must come from completed item content and pass schema validation. -- Translation threads and turns must use a locked-down policy. Apply the policy on `thread/start` and again on `turn/start` where the schema supports it so turn-specific settings cannot inherit a broader thread default: - - `approvalPolicy: "never"`. - - `sandboxPolicy.type = "readOnly"` or the generated schema's equivalent read-only sandbox value. - - restricted filesystem access with no TRAUMA project root or memory store root in readable roots. - - `networkAccess = false` where the generated schema supports network control for the selected sandbox. - - `cwd` must be a safe empty runtime directory, not the TRAUMA project root and not the configured memory store path. - - Source Markdown is supplied only as a prompt/input item by the Reader backend. - - Codex must not receive local paths to source `CONTENT.md`, translated `CONTENT.md`, credential files, the project root, or the store root. - - If the generated schema cannot express `readOnly` plus disabled network access directly, do not attach network-capable tools, dynamic tools, MCP servers, or project/store readable roots. Update this contract with the exact equivalent minimum-privilege payload before implementing the client. - -`networkAccess = false` refers only to sandboxed agent/tool execution inside the -translation turn. It must not disable the TRAUMA backend's connection to Codex -app-server or Codex app-server's own authenticated model/service traffic needed -to run the translation. - -Runtime `cwd` rules: - -- Add `TRAUMA_CODEX_RUNTIME_DIR` as an optional config value for the parent runtime directory. -- If unset, use an OS temp directory under `trauma-codex-runtime/`. -- Create one empty job-scoped directory such as `//` before `thread/start`. -- The directory must not be inside the TRAUMA project root, the configured memory store path, backup store paths, or user-controlled article content paths. -- Do not copy source `CONTENT.md`, translated `CONTENT.md`, prompts, credentials, or chunk bodies into this directory. -- Remove the job-scoped runtime directory when the job reaches `complete`, `failed`, `stale`, or `canceled`. -- Startup recovery may delete stale empty runtime directories after confirming they are under `TRAUMA_CODEX_RUNTIME_DIR` or the OS temp `trauma-codex-runtime/` root. -- Runtime cleanup must be defensive: - - Validate `job_id` before deriving the directory name; reject separators, traversal segments, and empty ids. - - Resolve the runtime root to a canonical path before cleanup. - - Build the candidate path as `/` only. - - Use `lstat` on the candidate and reject symlinks; do not follow or delete symlink targets. - - Delete only a directory whose canonical parent is exactly the canonical runtime root. - - Refuse deletion if the candidate path equals, contains, or is contained by the TRAUMA project root, configured memory store path, backup store path, or any user-controlled article content path. - - Delete only empty job-scoped runtime directories. If a runtime directory is non-empty, leave it in place and persist/log a safe cleanup warning instead of recursively deleting unknown content. - -Runtime cleanup warnings: - -- MVP surfaces non-empty runtime cleanup leftovers as safe server logs only. -- Do not show a blocking frontend popup for runtime cleanup leftovers in Brilliant MVP. -- Runtime cleanup leftovers do not by themselves fail an otherwise completed translation job after the final `CONTENT.md` commit and SQLite purge succeeded. -- Warning text must include the job id and a safe runtime-root-relative path only; do not include source content, prompts, credentials, app-server payloads, or absolute project/store paths unless the project-standard diagnostics UI later explicitly permits them. -- `translateChunk()` must yield `thread.started` and `turn.started` before item events when app-server returns those ids. The orchestrator stores the latest in-flight `threadId` and `turnId` so cancellation can call `turn/interrupt`. - -## Retry and cancellation constants - -- `BRILLIANT_MAX_RETRIES = 3` is the fixed MVP retry budget and is imported from the shared translation types/settings contract. -- `BRILLIANT_CANCEL_GRACE_MS = 30000` is the fixed MVP timeout for finalizing a `cancel_requested` job when app-server interrupt acknowledgement or interrupted completion does not arrive. -- Do not read either value from mutable runtime settings during a job. If either becomes configurable later, copy the selected value into job metadata at job creation and use the copied value during retry, cancellation, and recovery. - -## Protocol schema and version contract - -- Record the Codex CLI/app-server version used for Brilliant implementation in the PR handoff. -- Generate protocol fixtures or schemas with `codex app-server generate-ts --out tests/fixtures/translation/codex-app-server-schema` or `codex app-server generate-json-schema --out tests/fixtures/translation/codex-app-server-schema` when the local Codex CLI supports it. -- If generated artifacts are too large for the repo, commit a focused fixture set covering `initialize`, `account/read`, `account/login/start`, `account/login/completed`, `thread/start`, `turn/start`, `turn/started`, `item/agentMessage/delta`, `item/completed`, `turn/completed`, and `turn/interrupt`. -- Focused fixtures must use the Codex app-server wire envelope without top-level `jsonrpc`. -- Focused fixtures must record which consumed `thread/start` and `turn/start` - fields are stable and which experimental fields Brilliant deliberately omits. -- Fake app-server tests must be based on the recorded schema/fixture version, not only on hand-written assumptions. -- Before implementing `translateChunk()`, confirm the exact `turn/start` - payload shape for `approvalPolicy`, `sandboxPolicy`, `cwd`, and - network-disable semantics from the generated schema or focused fixtures. If - the literal values in this contract differ from the installed Codex - app-server schema, update the Brilliant contracts first with the equivalent - minimum-privilege payload, then implement. - -## Auth JSON-RPC contract - -- Auth status uses `account/read` with `{ "refreshToken": false }` by default. -- Forced refresh is allowed only in server-side auth status checks and must not expose tokens. -- `account/read.requiresOpenaiAuth` is not an authenticated/unauthenticated - boolean. It means the current provider requires OpenAI auth. Auth is enabled - when `account/read` returns a non-null `account`; auth is required when - `requiresOpenaiAuth` is `true` and `account` is absent. -- Device-code login uses `account/login/start` with `{ "type": "chatgptDeviceCode" }`. -- Safe device-code response fields are `loginId`, `verificationUrl`, and `userCode`. -- Device-code cancellation uses `account/login/cancel` through `cancelDeviceCodeLogin({ loginId })`. -- Completion is detected from `account/login/completed` and `account/updated` notifications, followed by `account/read` confirmation. -- `account/login/completed` is adapted to the typed - `account.login.completed` `CodexAuthEvent` with `loginId`, `success`, and - safe `error` text. `account/updated` is adapted to `account.updated`. If - `success` is false, the settings/auth service must clean up the pending - observer and return a safe failed or canceled state instead of treating the - login as enabled. -- Auth notification consumption uses `observeAuthEvents()`. Settings/auth services must not subscribe to raw JSON-RPC notifications directly. -- `observeAuthEvents()` is consumed only while a device-code login is pending. The settings/auth service owns the listener lifecycle and must cancel/close the listener when login completes, is canceled, fails, or the server request scope is disposed. -- Losing the auth event listener is not fatal. Auth status refresh must always call `account/read` through `checkAuth()` and may return safe pending metadata when known. -- Logout uses `logout()`, which wraps app-server `account/logout` when supported. If logout is unavailable, delete only TRAUMA-owned metadata and report that Codex credentials were not removed. -- Do not use `chatgptAuthTokens` mode in Brilliant MVP; TRAUMA must not own ChatGPT access or refresh tokens. - -## Raw notification adapter boundary - -The transport layer receives raw JSON-RPC notifications such as: - -- `thread/started` -- `turn/started` -- `item/agentMessage/delta` -- `item/completed` -- `turn/completed` -- `account/login/completed` -- `account/updated` - -`src/server/translation/codex-app-server.ts` owns conversion from raw notification payloads to typed internal `CodexAppServerEvent` and `CodexAuthEvent` values. The orchestrator, settings/auth services, and SSE API consume only typed internal events and must not switch on raw JSON-RPC method names directly. - -## Typed app-server errors - -```text -auth_required -setup_required -app_server_unavailable -app_server_protocol_error -usage_limit -context_overflow -stream_disconnected -timeout -invalid_final_output -unknown -``` - -## Prompt sections - -The generated prompt must contain these sections in order: - -1. Role: faithful article translation worker. -2. Security: source content is untrusted data, not instructions. -3. Target language: BCP 47 code and display name. -4. Preservation rules: TRAUMA preserves Markdown syntax locally; Codex translates only supplied segment text and must not translate URLs, code, math, HTML tags, identifiers, file paths, commands, or placeholders. -5. Completeness rules: never summarize, never omit, never collapse repeated content, and never replace a segment with placeholder text. -6. Metadata JSON: chunk metadata from `TranslationChunk` excluding secrets. -7. Expected segment ids in order. -8. Retry correction, only for retry attempts, containing the previous stable - error code, expected segment ids, and sanitized validation diagnostics. -9. Segment source text list. -10. Source chunk inside explicit delimiters. -11. Required JSON output schema. - -## Output schema - -```json -{ - "type": "object", - "additionalProperties": false, - "required": ["chunk_index", "segments", "warnings"], - "properties": { - "chunk_index": { "type": "integer" }, - "segments": { - "type": "array", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["id", "translated_text"], - "properties": { - "id": { "type": "string" }, - "translated_text": { "type": "string" } - } - } - }, - "warnings": { - "type": "array", - "items": { "type": "string" } - } - } -} -``` - -## Validation algorithm - -Validate each completed chunk in this order: - -1. JSON parses and matches output schema. -2. `chunk_index` equals requested chunk index. -3. Output segment ids exactly equal input segment ids in the same order. -4. No duplicate segment ids. -5. Each `translated_text` is non-empty. -6. Reassemble translated text into the original source Markdown ranges. -7. Parser-backed structural fingerprint comparison preserves Markdown structure. -8. Code, inline code, math, HTML, URLs, Markdown link/image destinations, reference definitions, footnote identifiers, and table shape are preserved. -9. Total translated segment length is between configured `minLengthRatio` and `maxLengthRatio`. - -Error code boundary: - -- Use `invalid_final_output` when final Codex output cannot be parsed as JSON or - does not match the required `CodexChunkOutput` JSON schema after the - structured-output and prompt-only fallback paths are exhausted. -- Use `validation_failed` when output is valid `CodexChunkOutput` JSON but fails - semantic validation such as wrong segment ids, duplicate or reordered segment ids, - corrupted Markdown/HTML/math structure, or length-ratio checks. -- Persist chunk failures as structured `TranslationPersistedError` JSON in - `translation_chunks.error`. -- `TranslationPersistedError` may include optional `diagnostics` entries. - Diagnostics are safe validation metadata only: diagnostic kind, safe message, - chunk index, segment id, block id, and short expected/actual fingerprint or - protected-span previews. They are not raw failed model output or an attempt - log. -- The initial diagnostic kinds are `markdown_structure`, `segment_schema`, and - `segment_length_ratio`. The shared envelope also permits `protected_span` and - `projection` for validators that are introduced later. - -## Retry behavior - -- Retry only the failed chunk. -- Start a fresh ephemeral Codex thread for each retry attempt. -- Do not reuse the failed attempt's Codex thread because the thread history may contain invalid output or failed repair context. -- `maxRetries` is the number of retry attempts after the initial attempt, so total attempts are `1 + maxRetries`. -- The initial chunk attempt starts with `retry_count = 0`. -- Increment `retry_count` before each retry attempt. -- On validation retry, include validation failures in the retry prompt. -- Retry prompts include only Reader-generated structured validation failure summaries and original segment ids, not raw invalid model output beyond the minimal safe excerpts needed for validation diagnostics. -- The retry prompt starts a fresh thread and includes a compact retry-correction - section before the source chunk. That section must include the previous error - code, sanitized diagnostics when present, and the expected segment ids. -- Retry correction must instruct Codex not to repeat protected code, command - flags, identifiers, URLs, file paths, or escaped Markdown punctuation inside - `translated_text`. -- When diagnostics contain both `source_entry` and `translated_entry`, retry - correction must direct Codex to preserve the source entry exactly in its - original protected position and remove the translated entry value from - `translated_text`. -- When retry exhaustion would require `retry_count > maxRetries`, mark chunk and job failed. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/07-atomic-commit-purge-recovery.md b/docs/workflows/archive/task-19-codex-translation/contracts/07-atomic-commit-purge-recovery.md deleted file mode 100644 index c5bd8b89..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/07-atomic-commit-purge-recovery.md +++ /dev/null @@ -1,64 +0,0 @@ -# Brilliant atomic commit, purge, and recovery contract - -## Commit sequence - -Use this exact sequence: - -1. Re-read current source `CONTENT.md` hash. -2. If current source hash differs from job `source_hash`, mark job `stale`, emit `translation.job.stale`, and stop. -3. Stitch validated chunks in block order. -4. Validate final full document. -5. Ensure store-relative `memories///` exists under configured `storePath`. -6. Write full Markdown to store-relative `memories///.CONTENT..tmp`. -7. Flush file contents. -8. Rename temp file to `memories///CONTENT.md`. -9. Flush parent directory if supported. -10. Compute `output_hash` from committed `CONTENT.md`. -11. Rebase validated chunk projection spans to full translated document offsets. -12. Write `TRANSLATION_MAP.json` beside translated `CONTENT.md`. -13. Replace durable `translation_projection_spans` rows for the job. -14. In SQLite, set job `status = 'complete'`, `output_path`, `output_hash`, and `completed_at`. -15. Set completed chunks to `status = 'purged'`, `translated_markdown = NULL`, and `projection_spans_json = NULL`, preserving `translated_hash`. -16. Derive `reader_url` as `/memories//`. -17. Emit `translation.job.completed` only after purge succeeds, including `output_path`, `output_hash`, and derived `reader_url`. - -## Final paths - -```text -memories///CONTENT.md -memories///TRANSLATION_MAP.json -memories///.CONTENT..tmp -``` - -## Purge SQL - -```sql -UPDATE translation_chunks -SET translated_markdown = NULL, - projection_spans_json = NULL, - status = 'purged', - updated_at = ? -WHERE job_id = ? - AND status = 'complete'; -``` - -## Recovery cases - -1. Pending job exists without a running worker: if source hash still matches, schedule it; if source hash changed, mark stale. -2. Temp file exists, final file absent, job not complete: delete temp and mark failed or retryable. -3. Final file missing or hash-mismatched for a complete job: mark the job `unavailable` and allow a future retry for the same `(memory_id, lang_code, source_hash)`. -4. Final file exists, job complete, chunks not purged: purge before reporting complete. -5. Final file exists, job not complete, all chunks complete: re-stitch completed chunk bodies, compute the expected output hash, and compare it with the existing final file hash. Complete and purge only when the current source hash still matches and the final file hash equals the re-stitched output hash. If the final file differs, do not assume it belongs to this job. A rewrite is allowed only when the current source hash still matches `translation_jobs.source_hash`, all required completed chunk bodies are still available for re-stitching, final validation passes, and the same-directory temp-file plus atomic rename sequence can run again safely. Otherwise preserve the existing final file and mark the interrupted job failed with `filesystem_failure`. -6. Source hash changed during interrupted non-complete job: mark stale. - -## Rules - -- Existing completed translation must remain intact if write, flush, rename, DB update, or purge fails. -- Source `CONTENT.md` is never mutated. -- Completion event is emitted only after purge succeeds. -- Startup recovery cannot report complete until final file exists, hash matches, and purge is done. -- Recovery must never mark an interrupted job complete merely because `memories///CONTENT.md` exists. The file must match the re-stitched output hash for that job. -- Recovery must never overwrite an existing final `CONTENT.md` unless it can re-stitch this job's completed chunk bodies, validate the output, confirm the current source hash still matches, and perform the full atomic commit sequence again. -- Completed jobs are immutable history. If source content changes later, keep the completed job status unchanged and derive stale/current state at read time. -- If a complete job's committed output file is missing or hash-mismatched, mark it `unavailable` instead of reporting it as current. -- Same-directory `.CONTENT..tmp` files are short-lived final-write artifacts only. Do not retain them for failed-job debugging. If a commit fails before final rename or recovery finds an orphan temp file, delete the temp file and preserve failure diagnostics in SQLite metadata/logs instead. diff --git a/docs/workflows/archive/task-19-codex-translation/contracts/README.md b/docs/workflows/archive/task-19-codex-translation/contracts/README.md deleted file mode 100644 index a01d597e..00000000 --- a/docs/workflows/archive/task-19-codex-translation/contracts/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Brilliant focused contracts - -This directory keeps Brilliant implementation contracts split by domain. Do not merge these details back into one large file. The point of this directory is to let each implementation worker load only the contract relevant to its assigned subtask. - -## Files - -- `01-architecture-and-ownership.md`: file ownership and boundary rules. -- `02-types-state-and-settings.md`: shared TypeScript types, state machine values, and SQLite-backed language setting. -- `03-sqlite-and-repositories.md`: database tables, indexes, hashes, paths, and repository methods. -- `04-api-and-sse.md`: API contracts, SSE envelope, reconnect policy, cancellation. -- `05-markdown-chunking.md`: Markdown block scanner, protected spans, chunking defaults. -- `06-codex-prompt-and-validation.md`: Codex app-server client, prompt, schema, validator, retry. -- `07-atomic-commit-purge-recovery.md`: final write, SQLite purge, and crash recovery. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/01-browse-query-and-page-contract.md b/docs/workflows/archive/task-20-lazy-loading-performance/01-browse-query-and-page-contract.md deleted file mode 100644 index 23e0a446..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/01-browse-query-and-page-contract.md +++ /dev/null @@ -1,73 +0,0 @@ -# Task 20.1: Browse Query and Page Contract - -## Goal - -Define the shared request and response contracts that allow `/memories` to ask -for one filtered page at a time without losing the current query semantics. - -## Ownership - -Primary files: - -- Modify `src/components/memories/browse-data.ts` -- Modify `tests/memories/browse-data.test.ts` -- Modify `tests/components/browse-data-query.test.ts` - -Do not change database access in this subtask. - -## Contract - -Add these exported types and helpers to `src/components/memories/browse-data.ts` -or a small adjacent module imported by that file: - -- `BROWSE_MEMORY_PAGE_SIZE = 30` -- `BrowseMemoryCursor` - - `createdAt: string` - - `id: string` -- `BrowseMemoryPageRequest` - - `query: BrowseQuery` - - `cursor: BrowseMemoryCursor | null` - - `limit: number` -- `BrowseMemoryPage` - - `memories: BrowseMemory[]` - - `nextCursor: BrowseMemoryCursor | null` -- `createInitialBrowseMemoryPageRequest(query: BrowseQuery)` -- `createNextBrowseMemoryPageRequest(query: BrowseQuery, cursor: BrowseMemoryCursor)` -- `isSameBrowseQuery(left: BrowseQuery, right: BrowseQuery)` - -Keep `BrowseMemory` as the UI row type, but allow later subtasks to pass -`flashbacks: []` on initial rows. The absence of Flashback excerpts is a lazy -loading state, not a failed search state. - -## Implementation Steps - -1. Add failing tests that assert: - - The first page request uses `BROWSE_MEMORY_PAGE_SIZE`, the parsed - `BrowseQuery`, and a `null` cursor. - - The next page request carries the cursor unchanged. - - `isSameBrowseQuery` treats identical query values as equal and any changed - `q`, `category`, `tag`, `flashback`, or `view` as different. - - Existing parsing for `highlight` legacy parameters, read-state search - tokens, fielded search, and ampersand field values still passes. - -2. Implement only the contract helpers. - -3. Run: - -```bash -mise exec -- bun run test tests/memories/browse-data.test.ts tests/components/browse-data-query.test.ts -``` - -4. Commit with: - -```bash -git add src/components/memories/browse-data.ts tests/memories/browse-data.test.ts tests/components/browse-data-query.test.ts -git commit -m "feat: define browse page contract" -``` - -## Acceptance Criteria - -- Page request objects are serializable through SolidStart query arguments. -- Existing search and filter tests continue to pass unchanged except where they - explicitly assert the new contract. -- No repository, route, or component fetch behaviour changes in this subtask. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/02-sqlite-repository-pagination.md b/docs/workflows/archive/task-20-lazy-loading-performance/02-sqlite-repository-pagination.md deleted file mode 100644 index ecc1bc49..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/02-sqlite-repository-pagination.md +++ /dev/null @@ -1,132 +0,0 @@ -# Task 20.2: SQLite Repository Pagination - -## Goal - -Move memory browse filtering and pagination into SQLite so the server never has -to load the full archive for `/memories`. - -## Ownership - -Primary files: - -- Modify `src/server/db/schema.ts` -- Create `drizzle/0015_memory_browse_pagination.sql` -- Modify `src/server/db/bundled-migrations.ts` -- Modify `src/server/db/repositories.ts` -- Modify `tests/server/db/schema.test.ts` -- Modify `tests/server/db/repositories.test.ts` - -Do not change `src/components/memories/MemoryBrowse.tsx` in this subtask. - -## Repository Contract - -Add a repository method for page loading. Keep `listForBrowse()` temporarily as -a compatibility method with its current Flashback-inclusive shape until later -subtasks finish replacing callers. - -```ts -listForBrowsePage: (input: { - categoryId: string; - cursor: { createdAt: Date; id: string } | null; - flashbackId: string; - limit: number; - readState: "all" | "both" | "read" | "unread"; - searchFields: { - field: "title" | "url" | "tag" | "category" | "flashback"; - values: string[]; - }[]; - searchTerms: string[]; - tagId: string; -}) => Promise<{ - rows: MemoryBrowsePageRow[]; - nextCursor: { createdAt: Date; id: string } | null; -}>; -``` - -Add `MemoryBrowsePageRow` for page results. It must include memory metadata, -categories, and tags. It must not include nested Flashbacks. Keep -`MemoryBrowseRow` as the legacy Flashback-inclusive type until callers are -migrated. - -## Query Semantics - -- Sort by `memories.created_at desc, memories.id desc`. -- Use `limit + 1` rows to decide `nextCursor`. -- Cursor predicate: - - `created_at < cursor.createdAt` - - or `created_at = cursor.createdAt and id < cursor.id` -- `readState = "both"` returns an empty page. -- Explicit `categoryId` and `tagId` use archive-wide AND semantics. -- Explicit `flashbackId` uses an `exists` filter on `flashbacks.id`. -- Free search terms match any of: - - memory title - - memory URL - - memory description - - category name - - tag name - - Flashback text, prefix, or suffix -- Fielded search terms preserve current field names: - - `title` - - `url` - - `tag` - - `category` - - `flashback` -- Multiple values in the same field filter keep the current AND semantics. -- SQL string matching must escape `%`, `_`, and `\` before using `LIKE`. - -## Index and Migration - -Add a composite index that supports the cursor order: - -```ts -index("memories_created_at_id_idx").on(table.createdAt, table.id) -``` - -Add the bundled migration and a schema test that proves the index exists after -runtime migration. - -## Implementation Steps - -1. Add repository tests with at least five memories sharing mixed created times, - categories, tags, read state, and Flashbacks. Assert: - - first page ordering; - - second page cursor continuation; - - stable ordering for identical `createdAt`; - - `read`, `unread`, and `read + unread` behaviour; - - explicit category/tag filters; - - explicit Flashback ID filter; - - free search matching category/tag/Flashback metadata outside the first - page; - - escaped LIKE characters match literally. - -2. Add the schema index and bundled migration. - -3. Implement `listForBrowsePage()` using Drizzle query builders and `sql` - fragments only where needed for cursor and `exists` predicates. - -4. Keep the existing `listForBrowse()` implementation intact for compatibility - until Task 20.3 and Task 20.4 replace production route usage. Do not make - `listForBrowse()` call `listForBrowsePage()` unless it preserves the old - Flashback-inclusive return shape. - -5. Run: - -```bash -mise exec -- bun run test tests/server/db/schema.test.ts tests/server/db/repositories.test.ts -``` - -6. Commit with: - -```bash -git add drizzle/0015_memory_browse_pagination.sql src/server/db/schema.ts src/server/db/bundled-migrations.ts src/server/db/repositories.ts tests/server/db/schema.test.ts tests/server/db/repositories.test.ts -git commit -m "feat: paginate memory browse queries" -``` - -## Acceptance Criteria - -- Repository pagination never loads nested Flashbacks for memory page rows. -- Search and filters apply before pagination. -- Cursor pages are deterministic when multiple memories have the same - `createdAt`. -- Existing callers of `listForBrowse()` still receive the old - Flashback-inclusive shape until replaced. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/03-browse-loader-contract.md b/docs/workflows/archive/task-20-lazy-loading-performance/03-browse-loader-contract.md deleted file mode 100644 index f582040e..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/03-browse-loader-contract.md +++ /dev/null @@ -1,81 +0,0 @@ -# Task 20.3: Browse Loader Contract - -## Goal - -Expose page-based SolidStart server functions and revalidation helpers so UI -work can switch from all-row loading to page loading without mixing database -details into components. - -## Ownership - -Primary files: - -- Modify `src/server/memories/browse.ts` -- Modify `src/components/memories/browse-loader.ts` -- Modify `tests/server/memories/browse.test.ts` -- Modify `tests/components/browse-loader.test.ts` -- Modify `tests/server/browse-loaders.test.ts` - -Do not change visual UI in this subtask. - -## Loader Contract - -Add: - -- `loadBrowseMemoryPage(request: BrowseMemoryPageRequest, options?: LoadBrowseMemoriesOptions): Promise` -- `getBrowseMemoryPage = query(async (request: BrowseMemoryPageRequest) => ...)` -- `revalidateBrowseMemoryPages()` -- `revalidateBrowseMemoryFirstPage()` - -Keep `getBrowseMemories()` and `loadBrowseMemories()` only as compatibility -helpers until Task 20.4 removes route usage. - -## Behaviour - -- Fixture mode returns a deterministic page slice after applying the same - `filterBrowseMemories()` semantics used today. -- Non-fixture mode: - - loads runtime config; - - starts the backup retry queue once for the page load; - - initializes SQLite; - - converts `BrowseQuery` into repository filter input; - - maps repository rows to `BrowseMemory` with `flashbacks: []`; - - returns `nextCursor` from the repository. -- The loader must not call `filterBrowseMemoryFlashbacks()` for page rows. -- The loader must not call `filterRenderableFlashbackRows()` for page rows. - -## Implementation Steps - -1. Add tests proving `loadBrowseMemoryPage()`: - - surfaces missing config errors; - - starts the backup retry queue; - - returns `nextCursor` for fixture and SQLite-backed data; - - returns rows without Flashbacks on the initial memory page; - - applies fielded search and explicit Flashback filters before pagination. - -2. Add loader tests proving: - - `getBrowseMemoryPage.key` is used for page query invalidation; - - `revalidateBrowseMemoryWorkspace()` revalidates memory pages and taxonomy; - - `revalidateBrowseMemoryFirstPage()` exists for extraction success. - -3. Implement the page loader and query wrapper. - -4. Run: - -```bash -mise exec -- bun run test tests/server/memories/browse.test.ts tests/components/browse-loader.test.ts tests/server/browse-loaders.test.ts -``` - -5. Commit with: - -```bash -git add src/server/memories/browse.ts src/components/memories/browse-loader.ts tests/server/memories/browse.test.ts tests/components/browse-loader.test.ts tests/server/browse-loaders.test.ts -git commit -m "feat: expose memory browse page loader" -``` - -## Acceptance Criteria - -- The route layer can request the first page and subsequent pages without - knowing repository internals. -- Initial page rows carry no Flashback excerpts. -- Compatibility functions remain available until the component migration lands. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/04-memories-infinite-scroll-ui.md b/docs/workflows/archive/task-20-lazy-loading-performance/04-memories-infinite-scroll-ui.md deleted file mode 100644 index 24d87343..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/04-memories-infinite-scroll-ui.md +++ /dev/null @@ -1,90 +0,0 @@ -# Task 20.4: Memories Infinite Scroll UI - -## Goal - -Change `/memories` from rendering one all-row array to accumulating server pages -with infinite scroll, while preserving current filters, read-state tabs, action -menus, and deletion behaviour. - -## Ownership - -Primary files: - -- Modify `src/components/memories/MemoryBrowse.tsx` -- Modify `src/components/memories/AddMemoryForm.tsx` -- Modify `tests/components/memory-browse-actions.test.ts` -- Modify `tests/components/app-shell.test.ts` -- Modify `tests/e2e/**` when a memories route E2E exists or must be added. - -Do not implement Flashback excerpt lazy loading in this subtask; memory cards -should render correctly without excerpts. - -## UI State Contract - -`MemoryBrowse` owns: - -- loaded pages for the current `BrowseQuery`; -- the current `nextCursor`; -- `isLoadingNextPage`; -- `loadNextPageError`; -- removed memory IDs for optimistic deletion; -- a scroll sentinel observed after hydration. - -When `q`, `category`, `tag`, `flashback`, or `view` changes, discard old pages -and request the first page for the new query. - -## Behaviour - -- Server filtering replaces `filterBrowseMemories()` in the route component. -- `filterBrowseMemories()` remains covered by tests for fixture and shared - query semantics, but it must not be the production route filter after this - subtask. -- The fallback empty state means no server results for the current query. -- Use a manual `Load more` button as a deterministic fallback when - `IntersectionObserver` is unavailable or when the previous load failed. -- Deleting a memory removes it from all loaded pages and revalidates memory - pages, taxonomy, Flashbacks, Moments, and reader data as before. -- Add-memory success should revalidate the first memory page and taxonomy - without starting an archive-wide memory fetch before navigating to the - reader. - -## Implementation Steps - -1. Add component/source tests proving: - - `MemoryBrowse` imports `getBrowseMemoryPage`, not `getBrowseMemories`; - - `MemoryBrowse` no longer calls `filterBrowseMemories()` for production - route rendering; - - query changes reset accumulated pages; - - delete removes a memory from loaded page state; - - `AddMemoryForm` uses first-page scoped revalidation after successful - creation. - -2. Add or update E2E coverage for: - - first page renders memories; - - search still finds a memory that is not part of the initial page fixture; - - loading the next page appends rows without duplicating the first page; - - read-state tabs still update the URL and results. - -3. Implement page accumulation with Solid signals and memos. Keep effects pure; - perform async loading from event handlers or lifecycle hooks. - -4. Run: - -```bash -mise exec -- bun run test tests/components/memory-browse-actions.test.ts tests/components/app-shell.test.ts -mise exec -- bun run test:e2e -``` - -5. Commit with: - -```bash -git add src/components/memories/MemoryBrowse.tsx src/components/memories/AddMemoryForm.tsx tests/components/memory-browse-actions.test.ts tests/components/app-shell.test.ts tests/e2e -git commit -m "feat: add infinite scroll to memories" -``` - -## Acceptance Criteria - -- `/memories` does not request all memories on initial load. -- Query changes produce server-filtered first pages. -- Infinite scroll appends deterministic cursor pages. -- Extraction success no longer kicks off the old all-memory browse query. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/05-lazy-flashback-browse-data.md b/docs/workflows/archive/task-20-lazy-loading-performance/05-lazy-flashback-browse-data.md deleted file mode 100644 index e4e1026e..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/05-lazy-flashback-browse-data.md +++ /dev/null @@ -1,95 +0,0 @@ -# Task 20.5: Lazy Flashback Browse Data - -## Goal - -Split Flashback data from memory page loading so shell shortcuts and memory-card -excerpts can be fetched only when that surface needs them. - -## Ownership - -Primary files: - -- Modify `src/server/db/repositories.ts` -- Modify `src/server/flashbacks/browse.ts` -- Modify `src/components/flashbacks/flashbacks-loader.ts` -- Modify `src/components/shell/AppShell.tsx` -- Modify `src/components/memories/MemoryBrowse.tsx` -- Modify `src/components/memories/browse-data.ts` -- Modify `tests/server/flashbacks/repository.test.ts` -- Modify `tests/server/browse-loaders.test.ts` -- Modify `tests/components/flashbacks-loader.test.ts` -- Modify `tests/components/app-shell.test.ts` -- Modify `tests/memories/browse-data.test.ts` - -## Data Contracts - -Add dedicated loaders: - -- `loadRecentFlashbackBrowseRows({ limit }: { limit: number }): Promise` -- `loadBrowseFlashbacksForMemories(input: { memoryIds: string[]; selectedFlashbackId: string }): Promise>` -- `getRecentFlashbackBrowseRows(limit: number)` -- `getBrowseFlashbacksForMemories(input: { memoryIds: string[]; selectedFlashbackId: string })` - -Repository support: - -- recent Flashback candidates ordered by `flashbacks.createdAt desc`; -- Flashbacks for a bounded list of memory IDs; -- a direct lookup for `selectedFlashbackId` so `?flashback=` can show the - selected excerpt even when the page has only one matching memory. - -## Behaviour - -- `AppShell` must not derive recent Flashbacks from `getBrowseMemories()` or - memory pages. -- Memory cards render without a Flashback excerpt until their page's Flashback - batch is loaded. -- For ordinary browse pages, fetch Flashbacks for only the currently loaded page - of memory IDs. -- For `?flashback=`, ensure the selected Flashback is present for the - matching memory card. -- Renderability validation still runs for Flashback rows, but only for the - bounded candidate rows requested by the lazy loader. -- Existing `/flashbacks` route can continue using `loadFlashbackBrowseRows()`. - -## Implementation Steps - -1. Add repository tests proving: - - recent candidates are ordered by Flashback creation time; - - bounded memory-ID lookup returns only requested memories; - - selected Flashback lookup works across source and translated variants. - -2. Add loader tests proving: - - recent shortcut loader filters stale translated Flashbacks; - - memory-card Flashback loader returns a record keyed by memory ID; - - empty memory ID input returns an empty record without opening content - files. - -3. Update `AppShell` to call the recent Flashback query lazily for the right - rail. It should keep taxonomy and backup alert loading unchanged. - -4. Update `MemoryBrowse` to request Flashbacks for loaded page memory IDs after - memory rows render. Pass the loaded Flashbacks into `MemoryItem`. - -5. Update browse-data helpers so `getMemoryDisplayFlashback()` operates on the - memory row plus the lazily loaded Flashback list. - -6. Run: - -```bash -mise exec -- bun run test tests/server/flashbacks/repository.test.ts tests/server/browse-loaders.test.ts tests/components/flashbacks-loader.test.ts tests/components/app-shell.test.ts tests/memories/browse-data.test.ts -``` - -7. Commit with: - -```bash -git add src/server/db/repositories.ts src/server/flashbacks/browse.ts src/components/flashbacks/flashbacks-loader.ts src/components/shell/AppShell.tsx src/components/memories/MemoryBrowse.tsx src/components/memories/browse-data.ts tests/server/flashbacks/repository.test.ts tests/server/browse-loaders.test.ts tests/components/flashbacks-loader.test.ts tests/components/app-shell.test.ts tests/memories/browse-data.test.ts -git commit -m "feat: lazy load browse flashbacks" -``` - -## Acceptance Criteria - -- Initial memory page data contains no nested Flashbacks. -- Shell recent Flashbacks use their own bounded query. -- Memory-card Flashback excerpts are hydrated per loaded page, not archive-wide. -- Flashback filter deep links still show the matching memory and selected - excerpt. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/06-reader-lazy-flashback-tab.md b/docs/workflows/archive/task-20-lazy-loading-performance/06-reader-lazy-flashback-tab.md deleted file mode 100644 index 794ba953..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/06-reader-lazy-flashback-tab.md +++ /dev/null @@ -1,73 +0,0 @@ -# Task 20.6: Reader Lazy Flashback Tab - -## Goal - -Prevent reader pages from fetching global Flashbacks until the user opens the -All tab in the reader right rail. - -## Ownership - -Primary files: - -- Modify `src/components/reader/MemoryReader.tsx` -- Modify `tests/components/reader-flashback-tabs.test.ts` -- Modify `tests/components/reader-memory-loader.test.ts` -- Modify `tests/components/memory-reader-actions.test.ts` -- Modify `tests/components/memory-reader-flashback-selection.test.ts` - -Do not change `loadReaderMemory()` in this subtask unless a type boundary -requires it. - -## Behaviour - -- `ReadyMemoryReader` must not create a global `getFlashbackBrowseRows()` - resource during initial render. -- `ReaderFlashbackTabs` keeps Current as the default active tab. -- Opening All for the first time starts the global Flashback query. -- Returning to Current does not discard already loaded All-tab rows. -- If `initialTab: "all"` is provided in tests or future route state, the All - query can start immediately. -- The All tab loading state remains visible while rows are unresolved. -- Flashback toggle revalidation still invalidates: - - the reader memory query; - - the global Flashback query; - - browse memory page caches. - -## Implementation Steps - -1. Add tests proving: - - render with the default Current tab does not call the global Flashback - query; - - rendering with `initialTab: "all"` shows the loading state when rows are - unavailable; - - the All tab still builds source and translated memory hrefs with - `buildMemoryVariantAnchorHref`; - - the Current tab still uses `buildSameMemoryAnchorHref`; - - source text no longer contains `createAsync(() => getFlashbackBrowseRows())` - inside `ReadyMemoryReader`. - -2. Move global Flashback query ownership into `ReaderFlashbackTabs` or a small - child component. Gate the query behind a signal such as `shouldLoadAll`. - -3. Keep `flashbackRows` as an optional test/fixture override so existing - render-to-string tests can provide rows directly. - -4. Run: - -```bash -mise exec -- bun run test tests/components/reader-flashback-tabs.test.ts tests/components/reader-memory-loader.test.ts tests/components/memory-reader-actions.test.ts tests/components/memory-reader-flashback-selection.test.ts -``` - -5. Commit with: - -```bash -git add src/components/reader/MemoryReader.tsx tests/components/reader-flashback-tabs.test.ts tests/components/reader-memory-loader.test.ts tests/components/memory-reader-actions.test.ts tests/components/memory-reader-flashback-selection.test.ts -git commit -m "feat: lazy load reader all flashbacks" -``` - -## Acceptance Criteria - -- Opening a reader page with the default Current tab does not request global - Flashbacks. -- Opening All still exposes every renderable Flashback across memories. -- Current-memory Flashbacks remain available from `loadReaderMemory()`. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/07-integration-verification-and-handoff.md b/docs/workflows/archive/task-20-lazy-loading-performance/07-integration-verification-and-handoff.md deleted file mode 100644 index bc13e364..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/07-integration-verification-and-handoff.md +++ /dev/null @@ -1,91 +0,0 @@ -# Task 20.7: Integration Verification and Handoff - -## Goal - -Prove that the lazy-loading branch improves the intended access paths without -breaking browse, reader, Flashback, extraction, or backup safety behaviour. - -## Ownership - -Primary files: - -- Modify `docs/workflows/README.md` if task status or notes changed during - implementation. -- Modify durable architecture or reference docs only if implementation changes - a lasting contract. -- Do not add product code in this subtask except small fixes required by failed - verification. - -## Required Behaviour Checks - -Manual or automated checks must cover: - -- `/memories` initial route loads a bounded first page. -- Infinite scroll appends a second page without duplicates. -- `/memories?q=` searches the full archive, not only loaded rows. -- `/memories?flashback=` opens the memory that owns the Flashback and shows - the selected excerpt after lazy hydration. -- Right-rail recent Flashbacks render without forcing all memories to load. -- Reader default Current tab does not fetch global Flashbacks. -- Reader All tab fetches global Flashbacks on demand. -- Add-memory success navigates to the reader without starting the old all-memory - browse query. -- Backup failsafe alerts still surface when configured tests simulate backup - drift or inconsistency. - -## Performance Evidence - -Record before/after timings in the PR body for the same local data set: - -- `loadBrowseMemoryPage()` first page. -- `loadBrowseFlashbacksForMemories()` for one page of memory IDs. -- `loadRecentFlashbackBrowseRows({ limit: 5 })`. -- `loadReaderMemory()` for a large memory source page. -- Reader default render path with Current tab. -- Reader All-tab Flashback query. -- Add-memory API success path through navigation-triggered revalidation. - -Use the same measurement harness shape as the earlier `fix/perform` inspection: -run loaders directly under `mise exec -- bun`, use the configured local -`trauma.config.json`, and keep browser-trace claims separate from loader -timings. - -## Verification Commands - -Run focused suites first: - -```bash -mise exec -- bun run test tests/memories/browse-data.test.ts tests/components/browse-data-query.test.ts -mise exec -- bun run test tests/server/db/schema.test.ts tests/server/db/repositories.test.ts -mise exec -- bun run test tests/server/memories/browse.test.ts tests/server/browse-loaders.test.ts tests/server/flashbacks/repository.test.ts -mise exec -- bun run test tests/components/browse-loader.test.ts tests/components/flashbacks-loader.test.ts tests/components/app-shell.test.ts tests/components/memory-browse-actions.test.ts tests/components/reader-flashback-tabs.test.ts -``` - -Then run the broad checks: - -```bash -mise exec -- bun run typecheck -mise exec -- bun run test -mise exec -- bun run build -mise exec -- bun run test:e2e -``` - -## PR Handoff - -The PR description must include: - -- Subtask checklist and commit mapping. -- Exact verification commands and outcomes. -- Performance timing table. -- Any known blocker from unrelated local dirty state. -- Confirmation that no unrelated files from `.sawyer/exclude-whitelist.txt` - were staged. -- Confirmation that backup failsafe behaviour was not weakened. - -## Acceptance Criteria - -- The branch implements the workflow without unrelated visual redesign. -- Initial memory and reader paths no longer perform archive-wide Flashback work. -- Search and deep links remain semantically global. -- Verification results and performance evidence are reproducible from the PR - description. diff --git a/docs/workflows/archive/task-20-lazy-loading-performance/README.md b/docs/workflows/archive/task-20-lazy-loading-performance/README.md deleted file mode 100644 index 415c1466..00000000 --- a/docs/workflows/archive/task-20-lazy-loading-performance/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# Task 20: Lazy Loading Performance Workflow - -Implement these subtasks sequentially on `feat/lazy-loading`, which is derived -from `fix/perform`. - -## Goal - -Reduce perceived latency when opening `/memories`, opening reader pages, and -returning from memory extraction by moving expensive archive-wide work behind -server-side pagination and explicit lazy-loading boundaries. - -## Architecture - -The initial memories route must load a small page of memory summaries from -SQLite, not the entire archive and not every Flashback. Search, taxonomy, -read-state, and Flashback filters must remain global by moving their semantics -to the server query layer before infinite scroll is enabled. Global Flashback -lists must use dedicated lazy queries rather than piggybacking on the memories -payload. - -## Required Context - -- [Documentation index](../../../INDEX.md) -- [Data and storage architecture](../../../architecture/data-and-storage.md) -- [UI and routing architecture](../../../architecture/ui-and-routing.md) -- [Runtime flows](../../../architecture/flows.md) -- [Verification strategy](../../../quality/verification.md) -- [SolidStart UI rules](../../../references/coding-standards/solidstart-ui.md) -- [Drizzle and SQLite rules](../../../references/coding-standards/drizzle-sqlite.md) -- [Testing and verification rules](../../../references/coding-standards/testing-verification.md) - -## Scope - -In scope: - -- Cursor-based server pagination for `/memories`. -- Server-side browse filtering that preserves existing query semantics. -- Infinite scroll for memory rows with deterministic fallback controls. -- Lazy Flashback loading for memory cards, shell shortcuts, and reader All tab. -- Focused revalidation so extraction success does not trigger archive-wide - fetch work before reader navigation. -- Performance evidence recorded before handoff. - -Out of scope for this branch: - -- Markdown render caching. -- Extractor worker pooling. -- Backup integrity algorithm replacement. -- Visual redesign of the memories or reader routes. -- Changing the meaning of Flashback, Moment, category, tag, read, or - translation variant state. - -## Non-Negotiable Contracts - -- Do not implement pagination as client-side slicing after loading every row. -- Do not use offset pagination. Use a stable cursor based on - `createdAt desc, id desc`. -- Search must apply to the whole archive, not only the loaded pages. -- `?flashback=` must find the owning memory even when that memory is not - in the first page. -- Initial reader render must not fetch global Flashbacks while the Current tab - is active. -- The `/flashbacks` route may keep its full-route behaviour until explicitly - paginated, but shell and reader shortcuts must not depend on `/memories` - loading all Flashbacks. -- Keep source and translated Flashback hrefs variant-aware. -- Keep backup failsafe safety checks intact; only move browse revalidation work - out of the extraction hot path. - -## Subtask Order - -| Order | Subtask | Weight | Purpose | -| --- | --- | --- | --- | -| 20.1 | [Browse query and page contract](01-browse-query-and-page-contract.md) | M | Define shared query, cursor, and page response contracts before changing data access. | -| 20.2 | [SQLite repository pagination](02-sqlite-repository-pagination.md) | L | Add indexed cursor pagination and server-side filters at the repository boundary. | -| 20.3 | [Browse loader contract](03-browse-loader-contract.md) | M | Expose page-based server functions and scoped revalidation helpers. | -| 20.4 | [Memories infinite scroll UI](04-memories-infinite-scroll-ui.md) | L | Replace all-row rendering with page accumulation and intersection loading. | -| 20.5 | [Lazy Flashback browse data](05-lazy-flashback-browse-data.md) | L | Split Flashback shortcuts and card excerpts from the initial memory page. | -| 20.6 | [Reader lazy Flashback tab](06-reader-lazy-flashback-tab.md) | M | Defer global Flashbacks until the reader All tab is opened. | -| 20.7 | [Integration verification and handoff](07-integration-verification-and-handoff.md) | M | Validate behaviour, performance evidence, docs, and PR handoff. | - -## Implementation Rules - -- Implement all subtasks on the same branch, but keep commits grouped by the - subtask boundaries above. -- Use TDD for repository, loader, search, and component state changes. -- Prefer repository/service functions over raw SQL in routes or components. -- Add a migration for any new SQLite index and bundle it through - `src/server/db/bundled-migrations.ts`. -- Keep old public functions only as compatibility shims during the branch. By - the final verification subtask, route code should use the page/lazy APIs. -- Preserve fixture mode with `TRAUMA_BROWSE_FIXTURES=1`. -- Preserve existing dirty or untracked local files that are unrelated to this - branch. - -## Verification Baseline - -Each subtask lists focused commands. Before PR handoff, run: - -```bash -mise exec -- bun run typecheck -mise exec -- bun run test -mise exec -- bun run build -``` - -Run E2E after the UI subtasks land: - -```bash -mise exec -- bun run test:e2e -``` - -If full verification is blocked by unrelated local state, record the exact -blocker and still run the focused suites listed by each subtask. diff --git a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/01-codex-default-persistence-contract.md b/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/01-codex-default-persistence-contract.md deleted file mode 100644 index e4d3bebf..00000000 --- a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/01-codex-default-persistence-contract.md +++ /dev/null @@ -1,86 +0,0 @@ -# Task 21.1: Codex Default Persistence Contract - -## Goal - -Make Codex translation model and reasoning effort defaults an explicit -DB-backed settings contract before touching reader popover UI. - -## Ownership - -Primary files: - -- Modify `src/server/db/schema.ts` -- Modify `src/server/db/repositories.ts` -- Modify `src/server/settings/settings.ts` -- Modify `tests/server/db/schema.test.ts` -- Modify `tests/server/settings/settings.test.ts` -- Modify `tests/server/translation/translation-repositories.test.ts` -- Modify `docs/architecture/data-and-storage.md` -- Modify `docs/references/configuration.md` only if the docs currently omit the - persisted settings fields. - -Do not modify `src/components/reader/MemoryReader.tsx` in this subtask. - -## Contract - -The durable default state lives in SQLite settings: - -- `app_settings.codex_translation_model` - - nullable text - - `null` means Codex app-server default model - - non-null values should be normalized to the catalog `model` string when the - current catalog is available -- `app_settings.codex_translation_reasoning_effort` - - nullable text - - `null` means selected-model default effort - - non-null values must be one of the shared `CodexReasoningEffort` values - -Each translation job also records the resolved runtime choice: - -- `translation_jobs.model` -- `translation_jobs.reasoning_effort` - -Settings defaults and job-attempt status are related but separate: - -- Updating the default affects future popover selections and future jobs. -- A completed or failed job keeps the resolved values it used at creation. -- Changing defaults must not rewrite existing `translation_jobs` rows. - -## Implementation Steps - -1. Add failing persistence tests that write `codexTranslationModel: "gpt-5.5"` - and `codexTranslationReasoningEffort: "high"` through the settings - repository, then read settings back and expect the same values. -2. Add failing tests that update only one default and preserve the other value. - For example, updating `model: "gpt-5.5"` without an effort must leave an - existing `codexTranslationReasoningEffort: "medium"` unchanged. -3. Add failing schema tests that reject unsupported - `codex_translation_reasoning_effort` values. -4. Add failing translation repository tests that a created job records the - resolved `model` and `reasoning_effort`, and that later settings updates do - not mutate the job row. -5. If the columns and repository methods already exist, tighten tests around the - exact default-state semantics instead of adding a new migration. -6. If any column is missing on the branch being executed, add a Drizzle - migration and update bundled migrations before implementation continues. -7. Update data/storage docs to state that settings defaults are current UI state - while job rows are historical attempt state. - -## Tests - -Run: - -```bash -mise exec -- bun run test tests/server/settings/settings.test.ts tests/server/db/schema.test.ts tests/server/translation/translation-repositories.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance Criteria - -- DB-backed settings can persist and read the selected model and reasoning - effort. -- Partial settings updates preserve omitted default fields. -- Unsupported persisted effort values are rejected by schema or repository - boundaries. -- Translation job rows keep the resolved model/effort used for that attempt. -- No reader UI code is changed in this subtask. diff --git a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/02-settings-api-and-route-state.md b/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/02-settings-api-and-route-state.md deleted file mode 100644 index 36bc1626..00000000 --- a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/02-settings-api-and-route-state.md +++ /dev/null @@ -1,90 +0,0 @@ -# Task 21.2: Settings API and Route State - -## Goal - -Ensure settings API routes validate remembered Codex defaults and reader routes -receive fresh persisted defaults for the translation popover. - -## Ownership - -Primary files: - -- Modify `src/server/settings/codex-model-routes.ts` -- Modify `src/components/settings/settings-submit.ts` -- Modify `src/components/settings/settings-loader.ts` -- Modify `src/routes/api/settings/translation-codex-defaults.ts` -- Modify `src/routes/memories/[id].tsx` -- Modify `src/components/reader/reader-memory-loader.ts` only if route data - currently drops persisted translation defaults. -- Modify `tests/server/routes/api-settings.test.ts` -- Modify `tests/components/settings-page.test.ts` -- Modify `tests/components/reader-memory-loader.test.ts` or - `tests/server/reader/page-data.test.ts`, depending on where the route-state - contract is enforced. - -Do not change shared popover chrome in this subtask. - -## Contract - -Settings API: - -- `PATCH /api/settings/translation-codex-defaults` accepts `model` and - `reasoning_effort`. -- Omitted fields preserve current DB values. -- `null` model resets to Codex app-server default model. -- `null` reasoning effort resets to selected-model default effort. -- Non-null model must exist in the current Codex app-server catalog. -- Non-null reasoning effort must be supported by the selected or persisted model. -- Successful PATCH returns the persisted `codexTranslationModel` and - `codexTranslationReasoningEffort` values after normalization. - -Reader route state: - -- Source reader pages pass `translationModel` and - `translationReasoningEffort` from DB settings into `MemoryReader`. -- The values must reflect the latest successful settings/default update, not a - stale initial app load snapshot. -- If a user selects `gpt-5.5` and `high`, closes the popover after a successful - translation start, then opens another source reader page, the popover should - seed `gpt-5.5` and `high`. - -## Implementation Steps - -1. Add failing API tests that PATCH `{ "model": "gpt-5.5", - "reasoning_effort": "high" }` and assert the response and subsequent - settings read both return those values. -2. Add failing API tests for partial updates: - - PATCH `{ "model": "gpt-5.5" }` preserves existing effort. - - PATCH `{ "reasoning_effort": "medium" }` preserves existing model. - - PATCH `{ "model": null }` resets only the model. - - PATCH `{ "reasoning_effort": null }` resets only the effort. -3. Add failing API tests for catalog mismatch: - - unavailable model returns `translation_model_unavailable` - - unsupported effort for the selected model returns - `translation_reasoning_effort_unavailable` -4. Add failing route-state tests showing reader page data includes persisted - `codexTranslationModel` and `codexTranslationReasoningEffort`. -5. Ensure server-side route data reads current settings from SQLite at the - reader route boundary. -6. If SolidStart cache invalidation is needed after PATCH or translation start, - use the existing revalidation helper pattern rather than adding ad-hoc global - state. -7. Keep browser code talking only to TRAUMA settings/translation routes, never - directly to Codex app-server. - -## Tests - -Run: - -```bash -mise exec -- bun run test tests/server/routes/api-settings.test.ts tests/components/settings-page.test.ts tests/components/reader-memory-loader.test.ts tests/server/reader/page-data.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance Criteria - -- Settings API persists normalized model and effort defaults. -- Omitted PATCH fields preserve existing DB state. -- Reader route data exposes current persisted defaults to `MemoryReader`. -- No browser code calls Codex app-server directly. -- Shared popover visual code remains unchanged in this subtask. diff --git a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/03-shared-popover-visual-contract.md b/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/03-shared-popover-visual-contract.md deleted file mode 100644 index 6f140e87..00000000 --- a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/03-shared-popover-visual-contract.md +++ /dev/null @@ -1,70 +0,0 @@ -# Task 21.3: Shared Popover Visual Contract - -## Goal - -Make the transparent elevated panel currently used by the reader translation -popover the shared `Popup` visual contract. - -## Ownership - -Primary files: - -- Modify `src/components/ui/Popup.tsx` -- Modify `tests/components/popup.test.tsx` -- Modify `tests/components/app-shell.test.ts` -- Modify `docs/references/design-system/components-and-surfaces.md` -- Modify `docs/references/design-system/interaction-and-accessibility.md` -- Modify `docs/references/design-system/verification.md` - -Do not modify DB/settings persistence or reader translation behaviour in this -subtask. - -## Contract - -Update `Popup` so `panelBaseClass` owns the unified panel recipe: - -- `z-[70]` -- `rounded-[20px]` -- `border border-trauma-border` -- `bg-trauma-bg-elev/50` -- `shadow-trauma-2` -- `backdrop-blur` -- `animate-trauma-pop-bounce` - -Keep placement, phone panel positioning, `mode="dialog"`, `mode="menu"`, -`aria-controls`, `aria-expanded`, `aria-haspopup`, and `role={mode()}` -unchanged. - -`panelClass` remains only for consumer-specific width, grid, spacing, text, and -phone/tablet positioning. Consumers must not pass a second opaque background to -restore the old `bg-trauma-bg-elev` panel. - -## Implementation Steps - -1. Add failing assertions in `tests/components/popup.test.tsx` that require - `bg-trauma-bg-elev/50` and `backdrop-blur` in `Popup.tsx`. -2. Update existing app shell assertions that assume opaque popover chrome. -3. Change `panelBaseClass` in `src/components/ui/Popup.tsx` to the shared - transparent recipe. -4. Update design-system docs so Shell Popovers describe translucent elevated - panels as the app-wide default. -5. Keep existing consumer dimensions in `AppShell.tsx`, `KebabActionMenu.tsx`, - and `TaxonomyAddControl.tsx` unchanged. - -## Tests - -Run: - -```bash -mise exec -- bun run test tests/components/popup.test.tsx tests/components/app-shell.test.ts -mise exec -- bun run typecheck -``` - -## Acceptance Criteria - -- `Popup` is the only file that defines the default transparent panel chrome. -- Existing shell, phone, taxonomy, and action-menu popovers inherit the new - transparent background without local duplicated background classes. -- Design docs say outside pointer and Escape dismissal are shared popover - behaviour. -- No DB/settings or reader translation code changes in this subtask. diff --git a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/04-reader-translation-popover-migration.md b/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/04-reader-translation-popover-migration.md deleted file mode 100644 index d8da2d0f..00000000 --- a/docs/workflows/archive/task-21-popover-and-translation-ui-fixes/04-reader-translation-popover-migration.md +++ /dev/null @@ -1,140 +0,0 @@ -# Task 21.4: Reader Translation Popover Migration - -## Goal - -Migrate the memory reader translation confirmation form to the shared `Popup` -component, seed the model/effort selects from persisted DB-backed defaults, -make outside dismissal cancel the form, and make the submit button visibly -enabled. - -## Ownership - -Primary files: - -- Modify `src/components/reader/MemoryReader.tsx` -- Modify `src/components/settings/settings-submit.ts` only if reader submit - needs a shared helper to persist defaults before starting translation. -- Modify `tests/components/memory-reader-actions.test.ts` -- Modify `tests/components/reader-style-contract.test.ts` only if style - contract assertions need to move from source strings into this focused area. - -Do not change translation server modules or settings repositories in this -subtask; Tasks 21.1 and 21.2 own those contracts. - -## Behaviour Contract - -Reader translation uses `Popup` with: - -- `mode="dialog"` -- stable id derived from the memory id, for example - `memory-${props.result.memory.id}-translation-settings` -- `label="Translation settings"` -- `placement="bottom-end"` -- `panelClass` containing only width, grid, gap, padding, and text alignment -- trigger button using the `triggerProps` returned by `Popup` - -Opening the popover must: - -- reset form language, model, and reasoning effort from the latest persisted - defaults exposed by route/settings state -- refresh the Codex model catalog when the local catalog is empty -- show the remembered model as the selected option when - `props.translationModel` is non-null, including `gpt-5.5` -- show the remembered reasoning effort as the selected option when - `props.translationReasoningEffort` is non-null -- include a fallback option for a remembered model/effort that is not currently - in the catalog so the UI does not silently fall back to the blank default -- mirror open state only if reader logic still needs a signal for tests or - disabled/progress display - -Closing the popover must: - -- happen through `Popup` for the Cancel button, outside pointer dismissal, and - Escape -- reset unsaved language/model/effort edits back to the latest persisted - defaults -- not call `startReaderTranslation` -- not open an EventSource -- not mutate translation progress - -Submitting the form must: - -- prevent default form submission -- call the existing `startTranslation` path with `langCode`, the selected - canonical `model`, and selected `reasoningEffort` -- persist explicit model/effort choices through the existing backend settings - update path before future popover opens are seeded -- refresh or revalidate local settings/reader state after successful persistence - so a still-mounted reader does not reopen with stale defaults -- close the popover only after `current`, `active`, or queued translation start - succeeds, matching the current success behaviour -- leave the popover available for correction when API start fails - -The popover submit button must use an active primary treatment: - -- `bg-trauma-accent` -- `text-trauma-accent-ink` -- `hover:bg-trauma-accent-hover` -- disabled opacity only when the button is actually disabled - -Do not use `bg-trauma-accent/50` for the enabled submit button. - -## Implementation Steps - -1. Add failing tests in `tests/components/memory-reader-actions.test.ts` that - assert `MemoryReader.tsx` imports and renders `Popup` for translation - settings. -2. Add failing tests that render the reader with - `translationModel: "gpt-5.5"` and `translationReasoningEffort: "high"`, open - the translation popover, and assert those options are selected by default. -3. Add failing tests that a remembered catalog model compares against the same - canonical value used by `
} > void; + onNext: () => void; + onPrevious: () => void; +}) { + const buttonClass = + "rounded-full border border-trauma-border px-3 py-1.5 text-xs font-bold text-trauma-text-primary transition hover:bg-trauma-bg-tint disabled:cursor-not-allowed disabled:opacity-60"; + return ( + + ); +} + function getReaderSelectionKey(selection: ReaderSelectionPayload): string { return `${selection.startOffset}:${selection.endOffset}:${selection.text}`; } @@ -2229,6 +2791,9 @@ function ReaderToc(props: { toc: ReaderTocEntry[]; }) { let scrollRef: HTMLOListElement | undefined; + const activeMomentTargetIds = createMemo(() => + collectResolvedReaderMomentTargetIds(props.moments, props.toc) + ); const [tocScrollState, setTocScrollState] = createSignal(noTocScrollState); const [readingBand, setReadingBand] = createSignal<{ @@ -2363,7 +2928,7 @@ function ReaderToc(props: { class={`${readerTocScrollContent} relative m-0 grid gap-2.5 pl-0`} onScroll={updateTocScrollHint} > - +

+ {transcriptWindow().rangeLabel} +

+ + 0} fallback={

No messages yet.

}> {(pair) => ( @@ -728,18 +1048,30 @@ export function PsychiatristDock(props: PsychiatristDockProps) { 0}>
    - {(citation) => ( -
  • - - {citation.title} - -
  • - )} + {(citation) => { + const safeHref = projectPublicPsychiatristCitationHref( + citation.url, + ); + return ( +
  • + {citation.title}} + > + {(href) => ( + + {citation.title} + + )} + +
  • + ); + }}
@@ -777,7 +1109,9 @@ export function PsychiatristDock(props: PsychiatristDockProps) { />
-

{errorMessage()}

+ @@ -831,39 +1165,71 @@ export function PsychiatristDock(props: PsychiatristDockProps) { function connectPsychiatristStream( eventUrl: string, + scope: Omit, onEvent: (event: PsychiatristStreamEvent) => void, + onMalformedTerminal: () => void, ): () => void { const eventSource = new EventSource(eventUrl); - const handleMessage = (message: MessageEvent) => { - const event = parsePsychiatristStreamEvent(message.data); - if (event !== undefined) { - onEvent(event); + let terminalObserved = false; + const handleMessage = ( + eventType: PsychiatristStreamEventType, + terminal = false, + ) => (message: MessageEvent) => { + if (terminal) { + if (terminalObserved) { + return; + } + terminalObserved = true; + eventSource.close(); } + const event = parsePsychiatristStreamEvent(message.data, { + ...scope, + eventType, + }); + if (event === undefined) { + if (terminal) { + onMalformedTerminal(); + } + return; + } + onEvent(event); }; - eventSource.addEventListener("psychiatrist.turn.started", handleMessage); - eventSource.addEventListener("psychiatrist.process.delta", handleMessage); - eventSource.addEventListener("psychiatrist.answer.delta", handleMessage); - eventSource.addEventListener("psychiatrist.answer.completed", (message) => { - handleMessage(message); - eventSource.close(); - }); - eventSource.addEventListener("psychiatrist.regenerate.started", handleMessage); - eventSource.addEventListener("psychiatrist.regenerate.completed", (message) => { - handleMessage(message); - eventSource.close(); - }); - eventSource.addEventListener("psychiatrist.answer.failed", (message) => { - handleMessage(message); - eventSource.close(); - }); - eventSource.addEventListener("psychiatrist.network.permission_required", (message) => { - handleMessage(message); - eventSource.close(); - }); - eventSource.addEventListener("psychiatrist.turn.canceled", (message) => { - handleMessage(message); - eventSource.close(); - }); + eventSource.addEventListener( + "psychiatrist.turn.started", + handleMessage("psychiatrist.turn.started"), + ); + eventSource.addEventListener( + "psychiatrist.process.delta", + handleMessage("psychiatrist.process.delta"), + ); + eventSource.addEventListener( + "psychiatrist.answer.delta", + handleMessage("psychiatrist.answer.delta"), + ); + eventSource.addEventListener( + "psychiatrist.answer.completed", + handleMessage("psychiatrist.answer.completed", true), + ); + eventSource.addEventListener( + "psychiatrist.regenerate.started", + handleMessage("psychiatrist.regenerate.started"), + ); + eventSource.addEventListener( + "psychiatrist.regenerate.completed", + handleMessage("psychiatrist.regenerate.completed", true), + ); + eventSource.addEventListener( + "psychiatrist.answer.failed", + handleMessage("psychiatrist.answer.failed", true), + ); + eventSource.addEventListener( + "psychiatrist.network.permission_required", + handleMessage("psychiatrist.network.permission_required", true), + ); + eventSource.addEventListener( + "psychiatrist.turn.canceled", + handleMessage("psychiatrist.turn.canceled", true), + ); return () => eventSource.close(); } @@ -926,28 +1292,6 @@ function getStreamErrorMessage(data: unknown): string { })); } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); -} - -function parsePsychiatristStreamEvent(data: string): PsychiatristStreamEvent | undefined { - try { - const value = JSON.parse(data) as unknown; - return isPsychiatristStreamEvent(value) ? value : undefined; - } catch { - return undefined; - } -} - -function isPsychiatristStreamEvent(value: unknown): value is PsychiatristStreamEvent { - return typeof value === "object" && - value !== null && - "type" in value && - "turnId" in value && - typeof value.type === "string" && - typeof value.turnId === "string"; -} - const psychiatristDockStyles = ` @media (prefers-reduced-motion: no-preference) { .trauma-psychiatrist-panel { diff --git a/src/components/reader/flashback-backup-warning.ts b/src/components/reader/flashback-backup-warning.ts new file mode 100644 index 00000000..5727ec35 --- /dev/null +++ b/src/components/reader/flashback-backup-warning.ts @@ -0,0 +1,36 @@ +export interface FlashbackBackupWarning { + code: "backup_enqueue_failed"; + message: string; + status: "failed" | "pending"; +} + +export function readFlashbackBackupWarning( + value: unknown, +): FlashbackBackupWarning | undefined { + if (!isRecord(value) || !isRecord(value.result)) { + return undefined; + } + const backup = value.result.backup; + if (!isRecord(backup) || !isRecord(backup.warning)) { + return undefined; + } + if (backup.status !== "failed" && backup.status !== "pending") { + return undefined; + } + if ( + backup.warning.code !== "backup_enqueue_failed" || + typeof backup.warning.message !== "string" || + backup.warning.message.trim() === "" + ) { + return undefined; + } + return { + code: "backup_enqueue_failed", + message: backup.warning.message, + status: backup.status, + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/src/components/reader/flashback-durability-warning.ts b/src/components/reader/flashback-durability-warning.ts new file mode 100644 index 00000000..e33a6cbe --- /dev/null +++ b/src/components/reader/flashback-durability-warning.ts @@ -0,0 +1,33 @@ +export interface FlashbackDurabilityWarning { + code: "flashback_export_durability_unconfirmed"; + message: string; + status: "unconfirmed"; +} + +export function readFlashbackDurabilityWarning( + value: unknown, +): FlashbackDurabilityWarning | undefined { + if (!isRecord(value) || !isRecord(value.result)) { + return undefined; + } + const durability = value.result.durability; + if ( + !isRecord(durability) || + durability.status !== "unconfirmed" || + !isRecord(durability.warning) || + durability.warning.code !== "flashback_export_durability_unconfirmed" || + typeof durability.warning.message !== "string" || + durability.warning.message.trim() === "" + ) { + return undefined; + } + return { + code: "flashback_export_durability_unconfirmed", + message: durability.warning.message, + status: "unconfirmed", + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/src/components/reader/flashback-events.ts b/src/components/reader/flashback-events.ts index e20c178c..81b0a6c2 100644 --- a/src/components/reader/flashback-events.ts +++ b/src/components/reader/flashback-events.ts @@ -7,6 +7,11 @@ export interface FlashbackKeyboardEvent { shiftKey: boolean; } +export interface FlashbackKeyboardToggleContext { + hasReaderSelection: boolean; + targetIsReaderContent: boolean; +} + export function isExplicitFlashbackKeyboardToggle( event: FlashbackKeyboardEvent, ): boolean { @@ -21,6 +26,23 @@ export function isExplicitFlashbackKeyboardToggle( return event.key === "Enter" || event.key === " "; } +export function shouldHandleFlashbackKeyboardToggle( + event: FlashbackKeyboardEvent, + context: FlashbackKeyboardToggleContext, +): boolean { + return context.hasReaderSelection && + context.targetIsReaderContent && + isExplicitFlashbackKeyboardToggle(event); +} + +export function shouldPreventFlashbackSpaceDefault( + event: FlashbackKeyboardEvent, + context: FlashbackKeyboardToggleContext, +): boolean { + return event.key === " " && + shouldHandleFlashbackKeyboardToggle(event, context); +} + export function canStartFlashbackToggle(pendingSelectionKey: string): boolean { return pendingSelectionKey.length === 0; } diff --git a/src/components/reader/psychiatrist-requests.ts b/src/components/reader/psychiatrist-requests.ts index b9ab070d..aa7647c6 100644 --- a/src/components/reader/psychiatrist-requests.ts +++ b/src/components/reader/psychiatrist-requests.ts @@ -1,9 +1,16 @@ import type { + PsychiatristActiveTurnResponse, PsychiatristCancelResult, + PsychiatristThreadPairResponse, PsychiatristThreadResponse, PsychiatristTurnStartedResponse, PsychiatristWebSourcePermission, } from "./psychiatrist-types"; +import { + isNonEmptyString, + isPsychiatristSourceCitation, + isRecord, +} from "./psychiatrist-runtime-validation"; type BrowserFetch = ( input: RequestInfo | URL, @@ -17,6 +24,13 @@ interface PsychiatristRequestScopeInput { variantKind: "source" | "translation"; } +interface PsychiatristJsonRequestInput { + body?: unknown; + fetch?: BrowserFetch; + method: string; + path: string; +} + export class PsychiatristRequestError extends Error { action: string; code: string; @@ -52,6 +66,7 @@ export function getPsychiatristErrorMessage(error: unknown): string { case "usage_limit": case "timeout": case "stream_disconnected": + case "event_limit_exceeded": return "Psychiatrist could not finish. Retry when ready."; case "context_overflow": return "This memory is too large for the current assistant context."; @@ -82,12 +97,26 @@ export async function createPsychiatristThread(input: { if (input.langCode !== undefined) { body.lang_code = input.langCode; } - return requestJson({ + const response = await requestJson({ body, fetch: input.fetch, + invalidResponseMessage: "Psychiatrist thread response was invalid.", method: "POST", path: `/api/memories/${encodeURIComponent(input.memoryId)}/psychiatrist/threads`, }); + const expectedLangCode = input.langCode ?? null; + const expectedVariantKind = input.langCode === undefined ? "source" : "translation"; + if (!isPsychiatristThreadResponse(response.value) || + response.value.memory_id !== input.memoryId || + response.value.lang_code !== expectedLangCode || + response.value.variant_kind !== expectedVariantKind + ) { + throw invalidPsychiatristResponse( + "Psychiatrist thread response was invalid.", + response.status, + ); + } + return response.value; } export async function sendPsychiatristMessage(input: { @@ -107,12 +136,22 @@ export async function sendPsychiatristMessage(input: { if (input.langCode !== undefined && input.langCode !== null) { body.lang_code = input.langCode; } - return requestJson({ + const response = await requestJson({ body, fetch: input.fetch, + invalidResponseMessage: "Psychiatrist message response was invalid.", method: "POST", path: scopedThreadPath(input) + "/messages", }); + if (!isPsychiatristTurnStartedResponse(response.value) || + response.value.thread_id !== input.threadId + ) { + throw invalidPsychiatristResponse( + "Psychiatrist message response was invalid.", + response.status, + ); + } + return response.value; } export async function cancelPsychiatristTurn(input: PsychiatristRequestScopeInput & { @@ -120,7 +159,7 @@ export async function cancelPsychiatristTurn(input: PsychiatristRequestScopeInpu pairId: string; turnId: string; }): Promise { - const value = await requestJson({ + const value = await requestCancelJson({ body: { lang_code: input.langCode ?? null, memory_id: input.memoryId, @@ -149,7 +188,7 @@ export async function regeneratePsychiatristResponse(input: PsychiatristRequestS pairId: string; webSourcePermission?: PsychiatristWebSourcePermission; }): Promise { - return requestJson({ + const response = await requestJson({ body: { lang_code: input.langCode ?? null, memory_id: input.memoryId, @@ -158,10 +197,21 @@ export async function regeneratePsychiatristResponse(input: PsychiatristRequestS web_source_permission: input.webSourcePermission ?? "deny", }, fetch: input.fetch, + invalidResponseMessage: "Psychiatrist regenerate response was invalid.", method: "POST", path: scopedThreadPath(input) + `/pairs/${encodeURIComponent(input.pairId)}/regenerate`, }); + if (!isPsychiatristTurnStartedResponse(response.value) || + response.value.thread_id !== input.threadId || + response.value.pair_id !== input.pairId + ) { + throw invalidPsychiatristResponse( + "Psychiatrist regenerate response was invalid.", + response.status, + ); + } + return response.value; } function scopedThreadPath(input: PsychiatristRequestScopeInput): string { @@ -169,12 +219,36 @@ function scopedThreadPath(input: PsychiatristRequestScopeInput): string { `/psychiatrist/threads/${encodeURIComponent(input.threadId)}`; } -async function requestJson(input: { - body?: unknown; - fetch?: BrowserFetch; - method: string; - path: string; -}): Promise { +async function requestJson(input: PsychiatristJsonRequestInput & { + invalidResponseMessage: string; +}): Promise<{ status: number; value: unknown }> { + const response = await requestSuccessfulResponse(input); + if (response.status === 204) { + throw invalidPsychiatristResponse(input.invalidResponseMessage, response.status); + } + try { + return { + status: response.status, + value: await response.json() as unknown, + }; + } catch { + throw invalidPsychiatristResponse(input.invalidResponseMessage, response.status); + } +} + +async function requestCancelJson( + input: PsychiatristJsonRequestInput, +): Promise { + const response = await requestSuccessfulResponse(input); + if (response.status === 204) { + return undefined; + } + return await response.json() as unknown; +} + +async function requestSuccessfulResponse( + input: PsychiatristJsonRequestInput, +): Promise { const fetcher = input.fetch ?? fetch; const response = await fetcher(input.path, { body: input.body === undefined ? undefined : JSON.stringify(input.body), @@ -184,10 +258,19 @@ async function requestJson(input: { if (!response.ok) { throw await readRequestError(response); } - if (response.status === 204) { - return undefined as T; - } - return await response.json() as T; + return response; +} + +function invalidPsychiatristResponse( + message: string, + responseStatus: number, +): PsychiatristRequestError { + return new PsychiatristRequestError({ + action: "retry", + code: "request_failed", + message, + responseStatus, + }); } async function readRequestError(response: Response): Promise { @@ -231,6 +314,84 @@ function isPsychiatristCancelResult(value: unknown): value is PsychiatristCancel typeof value.warning.message === "string"; } -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null && !Array.isArray(value); +function isPsychiatristThreadResponse(value: unknown): value is PsychiatristThreadResponse { + return isRecord(value) && + (value.active_turn === null || isPsychiatristActiveTurnResponse(value.active_turn)) && + isNonEmptyString(value.content_hash) && + (value.lang_code === null || isNonEmptyString(value.lang_code)) && + isNonEmptyString(value.memory_id) && + Array.isArray(value.pairs) && + value.pairs.every(isPsychiatristThreadPairResponse) && + isPsychiatristThreadStatus(value.status) && + isNonEmptyString(value.thread_id) && + (value.variant_kind === "source" || value.variant_kind === "translation"); +} + +function isPsychiatristActiveTurnResponse( + value: unknown, +): value is PsychiatristActiveTurnResponse { + return isRecord(value) && + isNonEmptyString(value.event_url) && + isNonEmptyString(value.pair_id) && + value.status === "running" && + isNonEmptyString(value.turn_id); +} + +function isPsychiatristThreadPairResponse( + value: unknown, +): value is PsychiatristThreadPairResponse { + return isRecord(value) && + (value.assistant_response === undefined || + isPsychiatristAssistantResponse(value.assistant_response)) && + isNonEmptyString(value.pair_id) && + (value.retry_action === undefined || value.retry_action === "allow_web_sources") && + (value.retry_mode === undefined || + value.retry_mode === "first_answer" || + value.retry_mode === "regenerate") && + (value.retry_turn_id === undefined || isNonEmptyString(value.retry_turn_id)) && + isPsychiatristPairStatus(value.status) && + isNonEmptyString(value.turn_id) && + isPsychiatristUserPrompt(value.user_prompt); +} + +function isPsychiatristAssistantResponse(value: unknown): boolean { + return isRecord(value) && + isNonEmptyString(value.completed_at) && + typeof value.content === "string" && + Array.isArray(value.source_citations) && + value.source_citations.every(isPsychiatristSourceCitation); +} + +function isPsychiatristUserPrompt(value: unknown): boolean { + return isRecord(value) && + typeof value.content === "string" && + isNonEmptyString(value.created_at); +} + +function isPsychiatristTurnStartedResponse( + value: unknown, +): value is PsychiatristTurnStartedResponse { + return isRecord(value) && + isNonEmptyString(value.event_url) && + isNonEmptyString(value.pair_id) && + isNonEmptyString(value.replay_url) && + value.status === "started" && + isNonEmptyString(value.thread_id) && + isNonEmptyString(value.turn_id); +} + +function isPsychiatristThreadStatus(value: unknown): boolean { + return value === "ready" || + value === "running" || + value === "stale" || + value === "failed" || + value === "canceled"; +} + +function isPsychiatristPairStatus(value: unknown): boolean { + return value === "pending" || + value === "completed" || + value === "failed" || + value === "canceled" || + value === "stale"; } diff --git a/src/components/reader/psychiatrist-runtime-validation.ts b/src/components/reader/psychiatrist-runtime-validation.ts new file mode 100644 index 00000000..c4e61cdd --- /dev/null +++ b/src/components/reader/psychiatrist-runtime-validation.ts @@ -0,0 +1,35 @@ +import { projectPublicPsychiatristCitationUrl } from "../../psychiatrist/source-citation-url"; + +export function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +export function isNonEmptyString(value: unknown): value is string { + return typeof value === "string" && value !== ""; +} + +export function isPsychiatristSourceCitation(value: unknown): value is { + source_id: string; + title: string; + url: string; +} { + return isRecord(value) && + isNonEmptyString(value.source_id) && + typeof value.title === "string" && + typeof value.url === "string"; +} + +export function projectPublicPsychiatristCitationHref( + value: string, +): string | undefined { + return projectPublicPsychiatristCitationUrl(value); +} + +export function isPsychiatristWarning(value: unknown): value is { + code: string; + message?: string; +} { + return isRecord(value) && + isNonEmptyString(value.code) && + (value.message === undefined || typeof value.message === "string"); +} diff --git a/src/components/reader/psychiatrist-stream-events.ts b/src/components/reader/psychiatrist-stream-events.ts new file mode 100644 index 00000000..d7dcde1f --- /dev/null +++ b/src/components/reader/psychiatrist-stream-events.ts @@ -0,0 +1,137 @@ +import { + PSYCHIATRIST_STREAM_EVENT_TYPES, + type PsychiatristStreamEvent, + type PsychiatristStreamEventType, +} from "./psychiatrist-types"; +import { + isNonEmptyString, + isPsychiatristSourceCitation, + isPsychiatristWarning, + isRecord, +} from "./psychiatrist-runtime-validation"; + +export interface PsychiatristStreamEventScope { + eventType: PsychiatristStreamEventType; + memoryId: string; + threadId: string; + turnId: string; +} + +export function parsePsychiatristStreamEvent( + raw: unknown, + expected: PsychiatristStreamEventScope, +): PsychiatristStreamEvent | undefined { + if (typeof raw !== "string") { + return undefined; + } + try { + const value = JSON.parse(raw) as unknown; + return isPsychiatristStreamEvent(value, expected) ? value : undefined; + } catch { + return undefined; + } +} + +function isPsychiatristStreamEvent( + value: unknown, + expected: PsychiatristStreamEventScope, +): value is PsychiatristStreamEvent { + return isRecord(value) && + isKnownPsychiatristStreamEventType(value.type) && + value.type === expected.eventType && + isNonEmptyString(value.eventId) && + value.memoryId === expected.memoryId && + value.threadId === expected.threadId && + value.turnId === expected.turnId && + typeof value.timestamp === "number" && + Number.isFinite(value.timestamp) && + value.timestamp >= 0 && + isPsychiatristStreamEventData(value.type, value.data); +} + +function isKnownPsychiatristStreamEventType( + value: unknown, +): value is PsychiatristStreamEventType { + return typeof value === "string" && + (PSYCHIATRIST_STREAM_EVENT_TYPES as readonly string[]).includes(value); +} + +function isPsychiatristStreamEventData( + type: PsychiatristStreamEventType, + data: unknown, +): boolean { + if (!isRecord(data)) { + return false; + } + switch (type) { + case "psychiatrist.turn.started": + return isStartedData(data, true); + case "psychiatrist.regenerate.started": + return isStartedData(data, false); + case "psychiatrist.process.delta": + case "psychiatrist.answer.delta": + return typeof data.text === "string"; + case "psychiatrist.answer.completed": + case "psychiatrist.regenerate.completed": + return isCompletionData(data); + case "psychiatrist.answer.failed": + return isFailureData(data); + case "psychiatrist.turn.canceled": + return isCanceledData(data); + case "psychiatrist.network.permission_required": + return isNetworkPermissionData(data); + } +} + +function isStartedData( + data: Record, + permitsUserPrompt: boolean, +): boolean { + return isNonEmptyString(data.pair_id) && + data.status === "running" && + (!permitsUserPrompt || data.user_prompt === undefined || + typeof data.user_prompt === "string"); +} + +function isCompletionData(data: Record): boolean { + return isNonEmptyString(data.pair_id) && + isOptionalString(data.text) && + (data.source_citations === undefined || + Array.isArray(data.source_citations) && + data.source_citations.every(isPsychiatristSourceCitation)) && + (data.warning === undefined || isPsychiatristWarning(data.warning)); +} + +function isFailureData(data: Record): boolean { + return isNonEmptyString(data.code) && + isOptionalNonEmptyString(data.pair_id) && + isOptionalString(data.message) && + isOptionalString(data.action) && + (data.retry_mode === undefined || + data.retry_mode === "first_answer" || + data.retry_mode === "regenerate"); +} + +function isCanceledData(data: Record): boolean { + return isNonEmptyString(data.code) && + (data.status === undefined || data.status === "canceled") && + (data.warning === undefined || isPsychiatristWarning(data.warning)); +} + +function isNetworkPermissionData(data: Record): boolean { + return data.code === "network_permission_required" && + isNonEmptyString(data.pair_id) && + data.retry_action === "allow_web_sources" && + (data.retry_mode === "first_answer" || data.retry_mode === "regenerate") && + isNonEmptyString(data.retry_turn_id) && + typeof data.user_prompt === "string" && + isOptionalString(data.message); +} + +function isOptionalString(value: unknown): boolean { + return value === undefined || typeof value === "string"; +} + +function isOptionalNonEmptyString(value: unknown): boolean { + return value === undefined || isNonEmptyString(value); +} diff --git a/src/components/reader/psychiatrist-transcript-window.ts b/src/components/reader/psychiatrist-transcript-window.ts new file mode 100644 index 00000000..e086a3bd --- /dev/null +++ b/src/components/reader/psychiatrist-transcript-window.ts @@ -0,0 +1,174 @@ +import type { PsychiatristTranscriptPair } from "./psychiatrist-transcript"; + +export const PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE = 24; +export const PSYCHIATRIST_TRANSCRIPT_MAX_VISIBLE_PAIRS = + PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE + 2; + +export type PsychiatristTranscriptPinReason = "active" | "web_source_retry"; + +export interface PsychiatristTranscriptPinnedPair { + pairId: string; + reasons: PsychiatristTranscriptPinReason[]; + transcriptNumber: number; +} + +export interface PsychiatristTranscriptWindowProjection { + endExclusive: number; + hasNewer: boolean; + hasOlder: boolean; + pairs: PsychiatristTranscriptPair[]; + pinnedPairs: PsychiatristTranscriptPinnedPair[]; + rangeLabel: string; + startIndex: number; + total: number; +} + +export function projectPsychiatristTranscriptWindow(input: { + activePairId?: string; + endExclusive: number; + pairs: readonly PsychiatristTranscriptPair[]; + retryPairId?: string; +}): PsychiatristTranscriptWindowProjection { + const total = input.pairs.length; + const endExclusive = normalizeWindowEnd(input.endExclusive, total); + const startIndex = Math.max( + 0, + endExclusive - PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE, + ); + const visibleIndices = new Set(); + for (let index = startIndex; index < endExclusive; index += 1) { + visibleIndices.add(index); + } + const pinnedReasonsByIndex = new Map(); + addPinnedPairIndex( + visibleIndices, + pinnedReasonsByIndex, + input.pairs, + input.activePairId, + "active", + startIndex, + endExclusive, + ); + addPinnedPairIndex( + visibleIndices, + pinnedReasonsByIndex, + input.pairs, + input.retryPairId, + "web_source_retry", + startIndex, + endExclusive, + ); + const pinnedPairs = [...pinnedReasonsByIndex] + .sort(([left], [right]) => left - right) + .map(([index, reasons]) => ({ + pairId: input.pairs[index]!.pairId, + reasons, + transcriptNumber: index + 1, + })); + + return { + endExclusive, + hasNewer: endExclusive < total, + hasOlder: startIndex > 0, + pairs: [...visibleIndices] + .sort((left, right) => left - right) + .map((index) => input.pairs[index]!), + pinnedPairs, + rangeLabel: formatTranscriptRangeLabel({ + endExclusive, + pinnedPairs, + startIndex, + total, + }), + startIndex, + total, + }; +} + +export function movePsychiatristTranscriptWindowOlder( + endExclusive: number, + total: number, +): number { + const normalizedEnd = normalizeWindowEnd(endExclusive, total); + const startIndex = Math.max( + 0, + normalizedEnd - PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE, + ); + return startIndex === 0 ? normalizedEnd : startIndex; +} + +export function movePsychiatristTranscriptWindowNewer( + endExclusive: number, + total: number, +): number { + const normalizedEnd = normalizeWindowEnd(endExclusive, total); + return Math.min(total, normalizedEnd + PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE); +} + +function normalizeWindowEnd(endExclusive: number, total: number): number { + if (total <= 0) { + return 0; + } + if (!Number.isFinite(endExclusive)) { + return total; + } + const normalized = Math.trunc(endExclusive); + if (normalized <= 0) { + return Math.min(total, PSYCHIATRIST_TRANSCRIPT_WINDOW_SIZE); + } + return Math.min(total, normalized); +} + +function addPinnedPairIndex( + visibleIndices: Set, + pinnedReasonsByIndex: Map, + pairs: readonly PsychiatristTranscriptPair[], + pairId: string | undefined, + reason: PsychiatristTranscriptPinReason, + startIndex: number, + endExclusive: number, +): void { + if (pairId === undefined || pairId === "") { + return; + } + const index = pairs.findIndex((pair) => pair.pairId === pairId); + if (index >= 0) { + visibleIndices.add(index); + if (index < startIndex || index >= endExclusive) { + const reasons = pinnedReasonsByIndex.get(index) ?? []; + if (!reasons.includes(reason)) { + reasons.push(reason); + } + pinnedReasonsByIndex.set(index, reasons); + } + } +} + +function formatTranscriptRangeLabel(input: { + endExclusive: number; + pinnedPairs: readonly PsychiatristTranscriptPinnedPair[]; + startIndex: number; + total: number; +}): string { + const visibleRange = input.total === 0 + ? "Showing 0–0 of 0" + : `Showing ${input.startIndex + 1}–${input.endExclusive} of ${input.total}`; + if (input.pinnedPairs.length === 0) { + return `${visibleRange}.`; + } + const descriptions = input.pinnedPairs.map((pair) => + `pair ${pair.transcriptNumber} (${formatPinReasons(pair.reasons)})` + ); + return `${visibleRange}; ${input.pinnedPairs.length} pinned ${ + input.pinnedPairs.length === 1 ? "pair" : "pairs" + } also shown: ${descriptions.join(", ")}.`; +} + +function formatPinReasons( + reasons: readonly PsychiatristTranscriptPinReason[], +): string { + if (reasons.length === 2) { + return "active and web-source retry"; + } + return reasons[0] === "active" ? "active" : "web-source retry"; +} diff --git a/src/components/reader/psychiatrist-transcript.ts b/src/components/reader/psychiatrist-transcript.ts index b4bc0126..662cf9f5 100644 --- a/src/components/reader/psychiatrist-transcript.ts +++ b/src/components/reader/psychiatrist-transcript.ts @@ -19,6 +19,8 @@ export interface PsychiatristTranscriptPair { userPrompt: string; } +export const PSYCHIATRIST_MAX_PROCESS_ROWS_PER_PAIR = 8; + export function toPsychiatristTranscriptPairs( pairs: readonly PsychiatristThreadPairResponse[], ): PsychiatristTranscriptPair[] { @@ -70,7 +72,7 @@ export function applyPsychiatristStreamEvent( } if (event.type === "psychiatrist.process.delta") { const text = readSafeProcessText(event.data); - return text === undefined ? pair : { ...pair, process: [...pair.process, text] }; + return text === undefined ? pair : appendProcessText(pair, text); } if (event.type === "psychiatrist.answer.delta") { const text = readAnswerText(event.data) ?? ""; @@ -97,6 +99,7 @@ export function applyPsychiatristStreamEvent( ) { return { ...pair, + process: [], ...(event.type === "psychiatrist.regenerate.started" ? { draftAnswer: "", draftOriginalTurnId: pair.turnId, draftTurnId: event.turnId } : {}), @@ -199,7 +202,11 @@ function readSafeProcessText(data: unknown): string | undefined { if (!isRecord(data) || typeof data.text !== "string") { return undefined; } - const normalized = data.text.toLowerCase(); + const text = data.text.replace(/\s+/g, " ").trim(); + if (text === "") { + return undefined; + } + const normalized = text.toLowerCase(); if ( normalized.includes("chain-of-thought") || normalized.includes("chain of thought") || @@ -210,7 +217,30 @@ function readSafeProcessText(data: unknown): string | undefined { ) { return undefined; } - return data.text; + return text; +} + +function appendProcessText( + pair: PsychiatristTranscriptPair, + text: string, +): PsychiatristTranscriptPair { + if (pair.process.at(-1) === text) { + return pair; + } + if (pair.process.length < PSYCHIATRIST_MAX_PROCESS_ROWS_PER_PAIR) { + return { ...pair, process: [...pair.process, text] }; + } + const first = pair.process[0]; + return { + ...pair, + process: first === undefined + ? [text] + : [ + first, + ...pair.process.slice(-(PSYCHIATRIST_MAX_PROCESS_ROWS_PER_PAIR - 2)), + text, + ], + }; } function readUserPrompt(data: unknown): string | undefined { diff --git a/src/components/reader/psychiatrist-types.ts b/src/components/reader/psychiatrist-types.ts index 763e713f..bd8c5074 100644 --- a/src/components/reader/psychiatrist-types.ts +++ b/src/components/reader/psychiatrist-types.ts @@ -58,16 +58,20 @@ export interface PsychiatristCancelResult { }; } +export const PSYCHIATRIST_STREAM_EVENT_TYPES = [ + "psychiatrist.turn.started", + "psychiatrist.process.delta", + "psychiatrist.answer.delta", + "psychiatrist.answer.completed", + "psychiatrist.answer.failed", + "psychiatrist.turn.canceled", + "psychiatrist.network.permission_required", + "psychiatrist.regenerate.started", + "psychiatrist.regenerate.completed", +] as const; + export type PsychiatristStreamEventType = - | "psychiatrist.turn.started" - | "psychiatrist.process.delta" - | "psychiatrist.answer.delta" - | "psychiatrist.answer.completed" - | "psychiatrist.answer.failed" - | "psychiatrist.turn.canceled" - | "psychiatrist.network.permission_required" - | "psychiatrist.regenerate.started" - | "psychiatrist.regenerate.completed"; + (typeof PSYCHIATRIST_STREAM_EVENT_TYPES)[number]; export interface PsychiatristStreamEvent { data: unknown; diff --git a/src/components/reader/reader-generation.ts b/src/components/reader/reader-generation.ts new file mode 100644 index 00000000..57e7dee4 --- /dev/null +++ b/src/components/reader/reader-generation.ts @@ -0,0 +1,69 @@ +import type { SupportedLanguageCode } from "../../settings/languages"; + +export interface ReaderGenerationIdentity { + readonly langCode?: SupportedLanguageCode; + readonly memoryId: string; +} + +export interface ReaderGenerationSnapshot extends ReaderGenerationIdentity { + readonly generation: number; +} + +export interface ReaderGenerationGuard { + activate: (identity: ReaderGenerationIdentity) => ReaderGenerationSnapshot; + capture: () => ReaderGenerationSnapshot; + invalidate: () => void; + isCurrent: (snapshot: ReaderGenerationSnapshot) => boolean; +} + +export function createReaderGenerationGuard( + initialIdentity: ReaderGenerationIdentity, +): ReaderGenerationGuard { + let active = true; + let generation = 1; + let identity = copyReaderGenerationIdentity(initialIdentity); + + const capture = (): ReaderGenerationSnapshot => ({ + ...identity, + generation, + }); + + return { + activate(nextIdentity) { + if (active && isSameReaderGenerationIdentity(identity, nextIdentity)) { + return capture(); + } + + active = true; + generation += 1; + identity = copyReaderGenerationIdentity(nextIdentity); + return capture(); + }, + capture, + invalidate() { + active = false; + generation += 1; + }, + isCurrent(snapshot) { + return active && + snapshot.generation === generation && + isSameReaderGenerationIdentity(identity, snapshot); + }, + }; +} + +function copyReaderGenerationIdentity( + identity: ReaderGenerationIdentity, +): ReaderGenerationIdentity { + return { + langCode: identity.langCode, + memoryId: identity.memoryId, + }; +} + +function isSameReaderGenerationIdentity( + left: ReaderGenerationIdentity, + right: ReaderGenerationIdentity, +): boolean { + return left.memoryId === right.memoryId && left.langCode === right.langCode; +} diff --git a/src/components/reader/reader-moment-targets.ts b/src/components/reader/reader-moment-targets.ts new file mode 100644 index 00000000..9183fa8b --- /dev/null +++ b/src/components/reader/reader-moment-targets.ts @@ -0,0 +1,78 @@ +import type { ReaderMomentItem } from "../../server/reader/page-data"; +import type { ReaderTocEntry } from "../../server/reader/markdown-renderer"; +import type { ReaderMomentSection } from "./moment-requests"; + +interface ReaderMomentPathTargets { + exactByAnchor: Map; + uniqueTarget: ReaderTocEntry | undefined; +} + +function indexReaderMomentTargets( + toc: readonly ReaderTocEntry[], +): Map { + const targetsByPath = new Map(); + + for (const entry of toc) { + const path = entry.path; + const anchor = entry.id; + const existing = targetsByPath.get(path); + if (existing === undefined) { + targetsByPath.set(path, { + exactByAnchor: new Map([[anchor, entry]]), + uniqueTarget: entry, + }); + continue; + } + + if (!existing.exactByAnchor.has(anchor)) { + existing.exactByAnchor.set(anchor, entry); + } + existing.uniqueTarget = undefined; + } + + return targetsByPath; +} + +function resolveIndexedReaderMomentTarget( + moment: ReaderMomentItem, + targetsByPath: ReadonlyMap, +): ReaderTocEntry | undefined { + const pathTargets = targetsByPath.get(moment.sectionPath); + return pathTargets?.exactByAnchor.get(moment.sectionAnchor) ?? + pathTargets?.uniqueTarget; +} + +export function resolveReaderMomentTarget( + moment: ReaderMomentItem, + toc: readonly ReaderTocEntry[], +): ReaderTocEntry | undefined { + return resolveIndexedReaderMomentTarget(moment, indexReaderMomentTargets(toc)); +} + +export function collectResolvedReaderMomentTargetIds( + moments: readonly ReaderMomentItem[], + toc: readonly ReaderTocEntry[], +): ReadonlySet { + const targetsByPath = indexReaderMomentTargets(toc); + const targetIds = new Set(); + + for (const moment of moments) { + const target = resolveIndexedReaderMomentTarget(moment, targetsByPath); + if (target !== undefined) { + targetIds.add(target.id); + } + } + + return targetIds; +} + +export function findReaderMomentForSection( + moments: readonly ReaderMomentItem[], + toc: readonly ReaderTocEntry[], + section: ReaderMomentSection, +): ReaderMomentItem | undefined { + const targetsByPath = indexReaderMomentTargets(toc); + return moments.find((moment) => + resolveIndexedReaderMomentTarget(moment, targetsByPath)?.id === section.id + ); +} diff --git a/src/components/reader/toc-scroll-spy.ts b/src/components/reader/toc-scroll-spy.ts index a4a09348..57255b84 100644 --- a/src/components/reader/toc-scroll-spy.ts +++ b/src/components/reader/toc-scroll-spy.ts @@ -1,6 +1,74 @@ import type { ActiveTocRange, HeadingPosition } from "./toc-reading-range"; const SECTION_ANCHOR_ATTRIBUTE = "data-reader-section-anchor"; +export const READER_TOC_DESKTOP_MEDIA_QUERY = "(min-width: 1041px)"; + +export interface ReaderTocScrollSpyBinding { + dispose: () => void; + isEnabled: () => boolean; +} + +/** + * Keeps the layout-reading scroll spy attached only while its desktop right + * rail is rendered. The media-query listener remains active so moving across + * the shell breakpoint re-enables measurement without remounting the reader. + */ +export function bindReaderTocScrollSpy(input: { + cancel: () => void; + schedule: () => void; + target: Pick; +}): ReaderTocScrollSpyBinding { + const desktop = input.target.matchMedia(READER_TOC_DESKTOP_MEDIA_QUERY); + const passive: AddEventListenerOptions = { passive: true }; + let disposed = false; + let enabled = false; + + const enable = (): void => { + if (disposed || enabled) { + return; + } + + enabled = true; + input.target.addEventListener("scroll", input.schedule, passive); + input.target.addEventListener("resize", input.schedule, passive); + input.target.addEventListener("hashchange", input.schedule); + input.schedule(); + }; + const disable = (): void => { + if (!enabled) { + return; + } + + enabled = false; + input.target.removeEventListener("scroll", input.schedule); + input.target.removeEventListener("resize", input.schedule); + input.target.removeEventListener("hashchange", input.schedule); + input.cancel(); + }; + const syncWithLayout = (): void => { + if (desktop.matches) { + enable(); + } else { + disable(); + } + }; + + desktop.addEventListener("change", syncWithLayout); + syncWithLayout(); + + return { + dispose: () => { + if (disposed) { + return; + } + + disposed = true; + desktop.removeEventListener("change", syncWithLayout); + disable(); + }, + isEnabled: () => enabled, + }; +} /** * Reads the viewport-relative top of every rendered section heading inside the diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 8406c326..877ca0be 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -18,13 +18,25 @@ import type { CodexReasoningEffort } from "../../server/translation/types"; import { submitCodexTranslationDefaults, pollCodexAuthSetup, + submitCancelCodexAuthSetup, submitReadCodexModels, + submitReadCodexAuth, submitDeleteOpenAiAuth, submitEnableOpenAiAuth, submitTranslationTargetLanguage, } from "./settings-submit"; import { revalidateSettingsState } from "./settings-loader"; +import { + createAsyncActionTracker, + type AsyncActionToken, +} from "./action-state"; +import { createCodexModelCatalogController } from "./codex-model-catalog-state"; +import { + captureAsyncActionFocusIntent, + type AsyncActionFocusOwnership, +} from "../async-action-focus"; import { RouteHeader } from "../layout/RouteHeader"; +import { ConfirmationPopup } from "../ui/ConfirmationPopup"; export interface SettingsPageProps { initialCodexModelCatalog?: CodexModelCatalog | null; @@ -36,6 +48,12 @@ type PendingCodexAuth = Extract< { status: "login_started" } >; +type SettingsAction = + | "language" + | "codex-defaults" + | "openai-auth" + | "openai-auth-poll"; + const pageFrame = "trauma-route-surface trauma-mobile-stable-viewport w-full bg-trauma-bg-surface"; const contentClass = "trauma-fluid-route-padding grid gap-5 py-7"; @@ -66,21 +84,73 @@ export function SettingsPage(props: SettingsPageProps) { props.initialCodexModelCatalog?.models ?? [], ); const [codexCatalogError, setCodexCatalogError] = createSignal(""); + const [codexCatalogPending, setCodexCatalogPending] = createSignal(false); const [codexAuth, setCodexAuth] = createSignal(props.initialSettings.openaiAuth); const pendingCodexAuth = () => codexAuth().status === "login_started" ? codexAuth() as PendingCodexAuth : undefined; - const [pending, setPending] = createSignal(""); + const [pendingActions, setPendingActions] = createSignal< + ReadonlySet + >(new Set()); const [message, setMessage] = createSignal(""); const [error, setError] = createSignal(""); const authPollControllers = new Set(); - - onCleanup(() => { + let codexAuthSetupButton: HTMLButtonElement | undefined; + let codexModelSelect: HTMLSelectElement | undefined; + let settingsPageActive = true; + const codexCatalogController = createCodexModelCatalogController({ + initialModels: codexModels(), + onStateChange: (state) => { + setCodexModels(state.models); + setCodexCatalogError(state.error); + setCodexCatalogPending(state.pending); + }, + readCatalog: ({ signal }) => submitReadCodexModels({ signal }), + }); + const actionTracker = createAsyncActionTracker( + setPendingActions, + ); + const isPending = (action: SettingsAction): boolean => + pendingActions().has(action); + const beginAction = ( + action: SettingsAction, + options: { clearFeedback?: boolean } = {}, + ): AsyncActionToken => { + const token = actionTracker.begin(action); + if (options.clearFeedback !== false) { + setError(""); + setMessage(""); + } + return token; + }; + const setActionMessage = ( + token: AsyncActionToken, + value: string, + ): void => { + if (actionTracker.isCurrent(token) && actionTracker.isLatestFeedback(token)) { + setMessage(value); + } + }; + const setActionError = ( + token: AsyncActionToken, + value: string, + ): void => { + if (actionTracker.isCurrent(token) && actionTracker.isLatestFeedback(token)) { + setError(value); + } + }; + const abortCodexAuthPolls = (): void => { for (const controller of authPollControllers) { controller.abort(); } authPollControllers.clear(); + }; + + onCleanup(() => { + settingsPageActive = false; + codexCatalogController.dispose(); + abortCodexAuthPolls(); }); onMount(() => { @@ -112,35 +182,39 @@ export function SettingsPage(props: SettingsPageProps) { }; const updateLanguage = async (): Promise => { - setPending("language"); - setError(""); - setMessage(""); + const action = beginAction("language"); try { const settings = await submitTranslationTargetLanguage({ language: language(), }); + if (!actionTracker.isCurrent(action)) { + return; + } setLanguage(settings.translationTargetLanguage); - setMessage("Translation target language saved."); + setActionMessage(action, "Translation target language saved."); void revalidateSettingsState(); } catch { - setError("Failed to update translation target language."); + setActionError(action, "Failed to update translation target language."); } finally { - setPending(""); + actionTracker.finish(action); } }; - const refreshCodexModels = async (): Promise => { - setCodexCatalogError(""); - try { - const catalog = await submitReadCodexModels(); - setCodexModels(catalog.models); - } catch (error) { - setCodexCatalogError( - error instanceof Error - ? error.message - : "Failed to read Codex model catalog.", - ); - } + const refreshCodexModels = (): Promise<"error" | "ignored" | "success"> => + codexCatalogController.refresh(); + + const retryCodexModels = (retryButton: HTMLButtonElement): void => { + const shouldRestoreFocus = captureCodexCatalogRetryFocusIntent(retryButton); + void refreshCodexModels().then((outcome) => { + if (outcome !== "success" || !settingsPageActive) { + return; + } + queueMicrotask(() => { + if (settingsPageActive && shouldRestoreFocus()) { + codexModelSelect?.focus(); + } + }); + }); }; const saveCodexDefaults: JSX.EventHandler = ( @@ -151,122 +225,216 @@ export function SettingsPage(props: SettingsPageProps) { }; const updateCodexDefaults = async (): Promise => { - setPending("codex-defaults"); - setError(""); - setMessage(""); + const action = beginAction("codex-defaults"); try { const selectedEffort = codexEffort(); const settings = await submitCodexTranslationDefaults({ model: codexModel() === "" ? null : codexModel(), reasoningEffort: selectedEffort === "" ? null : selectedEffort, }); + if (!actionTracker.isCurrent(action)) { + return; + } setCodexModel(settings.codexTranslationModel ?? ""); setCodexEffort(settings.codexTranslationReasoningEffort ?? ""); - setMessage("Codex translation defaults saved."); + setActionMessage(action, "Codex translation defaults saved."); void revalidateSettingsState(); } catch (error) { - setError( + setActionError( + action, error instanceof Error ? error.message : "Failed to update Codex translation defaults.", ); } finally { - setPending(""); + actionTracker.finish(action); } }; const enableOpenAiAuth = async (): Promise => { - setPending("openai-auth"); - setError(""); - setMessage(""); + const action = beginAction("openai-auth"); try { const response = await submitEnableOpenAiAuth(); + if (!actionTracker.isCurrent(action)) { + return; + } if (response.status === "enabled") { setCodexAuth(response); - setMessage(response.message); + setActionMessage(action, response.message); void revalidateSettingsState(); } else if (response.status === "login_started") { setCodexAuth(response); - setMessage("Codex device-code setup started."); + setActionMessage(action, "Codex device-code setup started."); void refreshCodexAuthAfterLogin(); } else if (response.status === "failed") { - setError(response.error); + setActionError(action, response.error); } else { setCodexAuth(response); - setMessage("Codex auth setup state refreshed."); + setActionMessage(action, "Codex auth setup state refreshed."); void revalidateSettingsState(); } } catch (error) { - setError(error instanceof Error ? error.message : "Failed to enable OpenAI auth."); + setActionError( + action, + error instanceof Error ? error.message : "Failed to enable OpenAI auth.", + ); } finally { - setPending(""); + actionTracker.finish(action); } }; const refreshCodexAuthAfterLogin = async (): Promise => { + const action = beginAction("openai-auth-poll", { clearFeedback: false }); const controller = new AbortController(); authPollControllers.add(controller); try { const response = await pollCodexAuthSetup({ signal: controller.signal, }); - if (response === undefined || controller.signal.aborted) { + if ( + response === undefined || + controller.signal.aborted || + !actionTracker.isCurrent(action) + ) { return; } setCodexAuth(response); if (response.status === "enabled") { - setMessage(response.message); + setActionMessage(action, response.message); void revalidateSettingsState(); } else if (response.status === "error") { - setError(response.error); + setActionError(action, response.error); + } else if (response.status === "login_started") { + setActionMessage( + action, + "Codex setup is still pending. Check status or cancel and restart.", + ); } else { - setMessage("Codex auth setup state refreshed."); + setActionMessage(action, "Codex auth setup state refreshed."); void revalidateSettingsState(); } } catch (error) { if (!controller.signal.aborted) { - setError(error instanceof Error ? error.message : "Failed to refresh Codex auth."); + setActionError( + action, + error instanceof Error ? error.message : "Failed to refresh Codex auth.", + ); } } finally { authPollControllers.delete(controller); + actionTracker.finish(action); } }; - const deleteOpenAiAuth = async (): Promise => { - setPending("openai-auth"); - setError(""); - setMessage(""); + const refreshCodexAuthStatus = async (): Promise => { + const action = beginAction("openai-auth-poll"); try { - const response = await submitDeleteOpenAiAuth({ - confirm: (text) => - typeof window === "undefined" ? false : window.confirm(text), - }); - if (response !== undefined) { - if (response.status === "unsupported") { - setCodexAuth({ - status: "enabled", - provider: "codex", - message: response.message, - }); - setMessage(response.message ?? "Codex auth logout is unsupported."); - void revalidateSettingsState(); - return; - } + const response = await submitReadCodexAuth(); + if (!actionTracker.isCurrent(action)) { + return; + } + setCodexAuth(response); + if (response.status === "enabled") { + setActionMessage(action, response.message); + } else if (response.status === "error") { + setActionError(action, response.error); + } else if (response.status === "login_started") { + setActionMessage( + action, + "Codex setup is still pending. Check status or cancel and restart.", + ); + } else { + setActionMessage(action, "Codex auth setup state refreshed."); + } + void revalidateSettingsState(); + } catch (error) { + setActionError( + action, + error instanceof Error ? error.message : "Failed to refresh Codex auth.", + ); + } finally { + actionTracker.finish(action); + } + }; + + const cancelOpenAiAuthSetup = async (): Promise => { + const action = beginAction("openai-auth"); + abortCodexAuthPolls(); + try { + const canceled = await submitCancelCodexAuthSetup(); + if (!actionTracker.isCurrent(action)) { + return; + } + const refreshed = await submitReadCodexAuth(); + if (!actionTracker.isCurrent(action)) { + return; + } + setCodexAuth(refreshed); + setActionMessage( + action, + canceled.status === "canceled" + ? "Codex auth setup canceled. You can start again." + : "No pending Codex auth setup was found. State refreshed.", + ); + void revalidateSettingsState(); + } catch (error) { + setActionError( + action, + error instanceof Error ? error.message : "Failed to cancel Codex auth setup.", + ); + } finally { + actionTracker.finish(action); + } + }; + + const deleteOpenAiAuth = async ( + focusOwnership: AsyncActionFocusOwnership, + ): Promise => { + const action = beginAction("openai-auth"); + try { + const response = await submitDeleteOpenAiAuth(); + if (!actionTracker.isCurrent(action)) { + return false; + } + if (response.status === "unsupported") { setCodexAuth({ - status: "disabled", + status: "enabled", provider: "codex", - reason: "logged_out", + message: response.message, }); - setMessage("Codex auth was deleted."); + setActionMessage( + action, + response.message ?? "Codex auth logout is unsupported.", + ); void revalidateSettingsState(); + return true; } + setCodexAuth({ + status: "disabled", + provider: "codex", + reason: "logged_out", + }); + restoreCodexAuthSetupFocus({ + focusOwnership, + getTarget: () => codexAuthSetupButton, + }); + setActionMessage(action, "Codex auth was deleted."); + void revalidateSettingsState(); + return true; } catch { - setError("Failed to delete OpenAI auth."); + setActionError(action, "Failed to delete OpenAI auth."); + return false; } finally { - setPending(""); + actionTracker.finish(action); } }; + onMount(() => { + if (props.initialSettings.openaiAuth.status === "login_started") { + void refreshCodexAuthAfterLogin(); + } + }); + return (
Translation target language setCodexModel(event.currentTarget.value)} > - + - + {(model) => ( - )} @@ -341,7 +521,7 @@ export function SettingsPage(props: SettingsPageProps) { Reasoning effort
- - {(value) =>

{value()}

} + +

Loading Codex model catalog...

+
@@ -388,11 +579,12 @@ export function SettingsPage(props: SettingsPageProps) {
+ ( + + )} + /> + + +
@@ -420,14 +640,18 @@ export function SettingsPage(props: SettingsPageProps) {

{pendingAuth().userCode}

- - {pendingAuth().verificationUrl} - + + {(verificationUrl) => ( + + {verificationUrl()} + + )} + )}
@@ -443,7 +667,7 @@ export function SettingsPage(props: SettingsPageProps) { {(value) => ( -

+

{value()}

)} @@ -459,3 +683,72 @@ export function SettingsPage(props: SettingsPageProps) { ); } + +export function CodexCatalogFeedback(props: { + error: string; + pending: boolean; + retry: (button: HTMLButtonElement) => void; +}) { + return ( + + + + ); +} + +export function captureCodexCatalogRetryFocusIntent( + retryButton: HTMLButtonElement, + readActiveElement: () => Element | null = () => + typeof document === "undefined" ? null : document.activeElement, + readBody: () => HTMLElement | undefined = () => + typeof document === "undefined" ? undefined : document.body, +): () => boolean { + return captureAsyncActionFocusIntent( + retryButton, + readActiveElement, + readBody, + ); +} + +export function restoreCodexAuthSetupFocus(input: { + focusOwnership: AsyncActionFocusOwnership; + getTarget: () => HTMLButtonElement | undefined; +}): void { + queueMicrotask(() => { + const target = input.getTarget(); + if ( + target?.isConnected !== true || + target.disabled || + !input.focusOwnership.ownsCurrentFocus() + ) { + return; + } + target.focus({ preventScroll: true }); + }); +} + +export function readSafeVerificationUrl(value: string): string | undefined { + try { + const url = new URL(value); + if ( + url.protocol !== "https:" || + url.username !== "" || + url.password !== "" + ) { + return undefined; + } + return url.href; + } catch { + return undefined; + } +} diff --git a/src/components/settings/action-state.ts b/src/components/settings/action-state.ts new file mode 100644 index 00000000..d1468248 --- /dev/null +++ b/src/components/settings/action-state.ts @@ -0,0 +1,45 @@ +export interface AsyncActionToken { + action: Action; + id: number; +} + +export interface AsyncActionTracker { + begin: (action: Action) => AsyncActionToken; + finish: (token: AsyncActionToken) => void; + isCurrent: (token: AsyncActionToken) => boolean; + isLatestFeedback: (token: AsyncActionToken) => boolean; + isPending: (action: Action) => boolean; +} + +export function createAsyncActionTracker( + onPendingChange: (pending: ReadonlySet) => void, +): AsyncActionTracker { + const activeIds = new Map(); + const latestFeedbackIds = new Map(); + let nextId = 0; + + const publishPending = (): void => { + onPendingChange(new Set(activeIds.keys())); + }; + + return { + begin(action) { + nextId += 1; + activeIds.set(action, nextId); + latestFeedbackIds.set(action, nextId); + publishPending(); + return { action, id: nextId }; + }, + finish(token) { + if (activeIds.get(token.action) !== token.id) { + return; + } + activeIds.delete(token.action); + publishPending(); + }, + isCurrent: (token) => activeIds.get(token.action) === token.id, + isLatestFeedback: (token) => + latestFeedbackIds.get(token.action) === token.id, + isPending: (action) => activeIds.has(action), + }; +} diff --git a/src/components/settings/codex-model-catalog-state.ts b/src/components/settings/codex-model-catalog-state.ts new file mode 100644 index 00000000..677dab32 --- /dev/null +++ b/src/components/settings/codex-model-catalog-state.ts @@ -0,0 +1,108 @@ +import type { + CodexModelCatalog, + CodexModelInfo, +} from "../../server/translation/codex-app-server"; + +export interface CodexModelCatalogState { + error: string; + models: CodexModelInfo[]; + pending: boolean; +} + +export interface CodexModelCatalogController { + dispose: () => void; + refresh: () => Promise<"error" | "ignored" | "success">; +} + +export function createCodexModelCatalogController(input: { + initialModels: CodexModelInfo[]; + onStateChange: (state: CodexModelCatalogState) => void; + readCatalog: (input: { signal: AbortSignal }) => Promise; +}): CodexModelCatalogController { + let active = true; + let generation = 0; + let state: CodexModelCatalogState = { + error: "", + models: input.initialModels, + pending: false, + }; + let currentRequest: + | { + controller: AbortController; + generation: number; + promise: Promise<"error" | "ignored" | "success">; + } + | undefined; + + const publish = (next: CodexModelCatalogState): void => { + state = next; + input.onStateChange(next); + }; + + const isCurrent = (requestGeneration: number): boolean => + active && generation === requestGeneration; + const clearCurrentRequest = (requestGeneration: number): void => { + if (currentRequest?.generation === requestGeneration) { + currentRequest = undefined; + } + }; + + return { + dispose() { + active = false; + generation += 1; + currentRequest?.controller.abort(); + currentRequest = undefined; + }, + refresh() { + if (!active) { + return Promise.resolve("ignored"); + } + if (currentRequest !== undefined) { + return currentRequest.promise; + } + + generation += 1; + const requestGeneration = generation; + const controller = new AbortController(); + publish({ ...state, pending: true }); + const promise = (async (): Promise<"error" | "ignored" | "success"> => { + try { + const catalog = await input.readCatalog({ signal: controller.signal }); + if (!isCurrent(requestGeneration) || controller.signal.aborted) { + return "ignored"; + } + publish({ error: "", models: catalog.models, pending: false }); + return "success"; + } catch (error) { + if ( + !isCurrent(requestGeneration) || + controller.signal.aborted || + isAbortError(error) + ) { + return "ignored"; + } + publish({ + ...state, + error: error instanceof Error + ? error.message + : "Failed to read Codex model catalog.", + pending: false, + }); + return "error"; + } finally { + clearCurrentRequest(requestGeneration); + } + })(); + currentRequest = { controller, generation: requestGeneration, promise }; + return promise; + }, + }; +} + +function isAbortError(error: unknown): boolean { + return typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError"; +} diff --git a/src/components/settings/settings-submit.ts b/src/components/settings/settings-submit.ts index d2df0d1b..cb02863e 100644 --- a/src/components/settings/settings-submit.ts +++ b/src/components/settings/settings-submit.ts @@ -1,9 +1,13 @@ import type { SettingsState } from "../../server/settings/settings"; import type { CodexModelCatalog } from "../../server/translation/codex-app-server"; -import type { CodexReasoningEffort } from "../../server/translation/types"; +import { + isCodexReasoningEffort, + type CodexReasoningEffort, +} from "../../server/translation/types"; import type { CodexAuthDeleteResponse, CodexAuthStatusResponse, + CodexDeviceCodeCancelResponse, CodexDeviceCodeStartResponse, } from "../../server/settings/codex-auth"; @@ -12,6 +16,9 @@ type FetchFunction = ( init?: RequestInit, ) => Promise; +const INVALID_CODEX_MODEL_CATALOG_MESSAGE = + "Codex model catalog response was invalid."; + export async function submitTranslationTargetLanguage(input: { fetch?: FetchFunction; language: string; @@ -33,16 +40,27 @@ export async function submitTranslationTargetLanguage(input: { export async function submitReadCodexModels(input: { fetch?: FetchFunction; + signal?: AbortSignal; } = {}): Promise { const requestFetch = input.fetch ?? fetch; const response = await requestFetch("/api/settings/codex-models", { method: "GET", + signal: input.signal, }); if (!response.ok) { throw new Error(await readErrorMessage(response, "failed to read Codex models")); } - return response.json() as Promise; + let value: unknown; + try { + value = await response.json() as unknown; + } catch { + throw new Error(INVALID_CODEX_MODEL_CATALOG_MESSAGE); + } + if (!isCodexModelCatalog(value)) { + throw new Error(INVALID_CODEX_MODEL_CATALOG_MESSAGE); + } + return value; } export async function submitCodexTranslationDefaults(input: { @@ -70,6 +88,33 @@ export async function submitCodexTranslationDefaults(input: { return response.json() as Promise; } +export async function submitTranslationDefaults(input: { + fetch?: FetchFunction; + language: string; + model: string | null; + reasoningEffort: CodexReasoningEffort | null; +}): Promise { + const requestFetch = input.fetch ?? fetch; + const response = await requestFetch("/api/settings/translation-defaults", { + method: "PATCH", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({ + language: input.language, + model: input.model, + reasoning_effort: input.reasoningEffort, + }), + }); + if (!response.ok) { + throw new Error( + await readErrorMessage(response, "failed to update translation defaults"), + ); + } + + return response.json() as Promise; +} + export async function submitEnableOpenAiAuth(input: { fetch?: FetchFunction; } = {}): Promise { @@ -86,10 +131,12 @@ export async function submitEnableOpenAiAuth(input: { export async function submitReadCodexAuth(input: { fetch?: FetchFunction; + signal?: AbortSignal; } = {}): Promise { const requestFetch = input.fetch ?? fetch; const response = await requestFetch("/api/settings/codex-auth", { method: "GET", + signal: input.signal, }); if (!response.ok) { throw new Error(await readErrorMessage(response, "failed to read Codex auth")); @@ -106,28 +153,63 @@ export async function pollCodexAuthSetup(input: { } = {}): Promise { const intervalMs = input.intervalMs ?? 1_500; const maxAttempts = input.maxAttempts ?? 120; + let lastStatus: CodexAuthStatusResponse | undefined; for (let attempt = 0; attempt < maxAttempts; attempt += 1) { const ready = await waitForPollDelay(intervalMs, input.signal); if (!ready) { return undefined; } - const status = await submitReadCodexAuth({ fetch: input.fetch }); + let status: CodexAuthStatusResponse; + try { + status = await submitReadCodexAuth({ + fetch: input.fetch, + signal: input.signal, + }); + } catch (error) { + if (input.signal?.aborted === true || isAbortError(error)) { + return undefined; + } + throw error; + } + if (input.signal?.aborted === true) { + return undefined; + } + lastStatus = status; if (status.status !== "login_started") { return status; } } - return undefined; + return lastStatus; } -export async function submitDeleteOpenAiAuth(input: { - confirm: (message: string) => boolean; +function isAbortError(error: unknown): boolean { + return typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError"; +} + +export async function submitCancelCodexAuthSetup(input: { fetch?: FetchFunction; -}): Promise { - if (!input.confirm("Delete Codex auth?")) { - return undefined; +} = {}): Promise { + const requestFetch = input.fetch ?? fetch; + const response = await requestFetch( + "/api/settings/codex-auth/device-code/cancel", + { method: "POST" }, + ); + if (!response.ok) { + throw new Error( + await readErrorMessage(response, "failed to cancel Codex auth setup"), + ); } + return response.json() as Promise; +} + +export async function submitDeleteOpenAiAuth(input: { + fetch?: FetchFunction; +} = {}): Promise { const requestFetch = input.fetch ?? fetch; const response = await requestFetch("/api/settings/codex-auth", { method: "DELETE", @@ -194,3 +276,30 @@ async function readErrorMessage( return fallback; } + +function isCodexModelCatalog(value: unknown): value is CodexModelCatalog { + return isRecord(value) && + Array.isArray(value.models) && + value.models.every(isCodexModelInfo); +} + +function isCodexModelInfo( + value: unknown, +): value is CodexModelCatalog["models"][number] { + return isRecord(value) && + typeof value.id === "string" && + typeof value.model === "string" && + typeof value.displayName === "string" && + typeof value.description === "string" && + typeof value.isDefault === "boolean" && + typeof value.defaultReasoningEffort === "string" && + isCodexReasoningEffort(value.defaultReasoningEffort) && + Array.isArray(value.supportedReasoningEfforts) && + value.supportedReasoningEfforts.every((effort) => + typeof effort === "string" && isCodexReasoningEffort(effort) + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/src/components/shell/AppShell.tsx b/src/components/shell/AppShell.tsx index 528e85e6..fadfc95d 100644 --- a/src/components/shell/AppShell.tsx +++ b/src/components/shell/AppShell.tsx @@ -10,7 +10,12 @@ import { } from "solid-js"; import { AddMemoryForm } from "../memories/AddMemoryForm"; +import { + createAddMemorySubmissionController, + type AddMemorySubmissionController, +} from "../memories/add-memory-controller"; import { BackupFailsafeBanner } from "../backup/BackupFailsafeBanner"; +import { revalidateBackupFailsafeAlert } from "../backup/backup-failsafe-loader"; import { TraumaMark } from "../brand/TraumaMark"; import { TaxonomyInlineCreateControl } from "../memories/TaxonomyInlineCreateControl"; import { @@ -18,7 +23,6 @@ import { validateTagName, } from "../../taxonomy/name-policy"; import { - KebabIcon, HermesIcon, LockIcon, MoonIcon, @@ -31,6 +35,7 @@ import { import { getBackupFailsafeAlert } from "../backup/backup-failsafe-loader"; import { getBrowseTaxonomy, + revalidateBrowseMemoryWorkspace, revalidateBrowseTaxonomy, } from "../memories/browse-loader"; import { @@ -41,7 +46,6 @@ import { toggleBrowseSearchFieldFilter, type BrowseSearchField, type BrowseFlashback, - type BrowseQuery, type BrowseTaxonomySummaryItem, } from "../memories/browse-data"; import { FlashbackShortcutList } from "../flashbacks/FlashbackShortcutList"; @@ -71,8 +75,6 @@ interface AppShellProps { children: JSX.Element; } -const buttonBase = - "inline-flex min-h-[38px] items-center justify-center rounded-lg border border-trauma-border-strong px-3 py-2 font-bold"; const surfaceInput = "min-h-[42px] min-w-0 rounded-lg border border-trauma-border-strong bg-trauma-bg-surface px-3 text-trauma-text-primary placeholder:text-trauma-text-placeholder"; const sideSurface = @@ -157,6 +159,10 @@ const phoneTabItems = [ export function AppShell(props: AppShellProps) { const location = useLocation(); const navigate = useNavigate(); + const addMemorySubmission = createAddMemorySubmissionController({ + onBackupFailsafe: revalidateBackupFailsafeAlert, + onCreationSettled: () => revalidateBrowseMemoryWorkspace(), + }); const [isHydrated, setIsHydrated] = createSignal(false); const [rightRailContent, setRightRailContent] = createSignal< JSX.Element | undefined @@ -249,6 +255,7 @@ export function AppShell(props: AppShellProps) {