chore: enforce test directory conventions (#496)

* chore: enforce test directory conventions

Move package, app, and tool tests out of src and add guard enforcement so source directories stay source-only.

* ci: use guard and package-scoped tests

Run the new repository guard in CI and keep test execution aligned with package-scoped commands after removing root aliases.

* ci: align stable release guard check

Use the new repository guard in stable release verification after replacing the residual-JS-only script.

* chore: tighten test layout enforcement

Enforce sibling tests directories, typecheck moved test suites with dedicated configs, and refresh remaining guidance that pointed at src-based tests.

* chore: clarify no-emit test tsconfigs

Explicitly disable declaration-only emit in test tsconfigs so review tooling sees they are no-emit typecheck configs.
This commit is contained in:
PerishFire 2026-05-05 15:34:22 +08:00 committed by GitHub
parent 16693fdfa8
commit bbdd4e84b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 562 additions and 399 deletions

View file

@ -72,11 +72,20 @@ jobs:
- name: Typecheck workspaces
run: pnpm -r --workspace-concurrency=1 --if-present run typecheck
- name: Check residual JS in TypeScript packages
run: pnpm check:residual-js
- name: Check repository layout policies
run: pnpm guard
- name: Test
run: pnpm test
run: |
pnpm --filter @open-design/e2e test
pnpm --filter @open-design/contracts test
pnpm --filter @open-design/platform test
pnpm --filter @open-design/sidecar test
pnpm --filter @open-design/sidecar-proto test
pnpm --filter @open-design/daemon test
pnpm --filter @open-design/web test
pnpm --filter @open-design/tools-dev test
pnpm --filter @open-design/tools-pack test
# Keep workspace builds serialized so generated dist output and local
# runtime artifacts are produced in a deterministic order. Parallel

View file

@ -86,8 +86,8 @@ jobs:
- name: Typecheck workspaces
run: pnpm -r --workspace-concurrency=1 --if-present run typecheck
- name: Check residual JS in TypeScript packages
run: pnpm check:residual-js
- name: Check repository layout policies
run: pnpm guard
# Workspace tests are intentionally not gated here. apps/web's
# i18n content-coverage tests assert that every locale carries

View file

@ -35,7 +35,7 @@ This file is the single source of truth for agents entering this repository. Rea
- Runtime target is Node `~24` and `pnpm@10.33.2`; use Corepack so the pnpm version pinned in `package.json` is selected.
- New project-owned entrypoints, modules, scripts, tests, reporters, and configs should default to TypeScript.
- Residual JavaScript is limited to generated output, vendored dependencies, explicitly documented compatibility build artifacts, and the allowlist in `scripts/check-residual-js.ts`.
- Residual JavaScript is limited to generated output, vendored dependencies, explicitly documented compatibility build artifacts, and the allowlist in `scripts/guard.ts`.
## Local lifecycle
@ -44,12 +44,19 @@ This file is the single source of truth for agents entering this repository. Rea
- Ports are governed by `tools-dev` flags: `--daemon-port` and `--web-port`.
- `tools-dev` exports `OD_PORT` for the web proxy target and `OD_WEB_PORT` for the web listener; do not use `NEXT_PORT`.
## Root command boundary
- Keep root scripts reserved for true repo-level checks and tools control-plane entrypoints: `pnpm guard`, `pnpm typecheck`, `pnpm tools-dev`, and `pnpm tools-pack`.
- Do not add root aggregate `pnpm build` or `pnpm test` aliases. Build/test commands must stay package-scoped (`pnpm --filter <package> ...`) or tool-scoped (`pnpm tools-pack ...`).
- E2E commands must be run from the e2e package boundary with `pnpm -C e2e ...`; do not add root e2e aliases.
## Boundary constraints
- Tests under `apps/`, `packages/`, and `tools/` live in a package/app/tool-level `tests/` directory sibling to `src/`; keep `src/` source-only and do not add new `*.test.ts` or `*.test.tsx` files under `src/`.
- Keep shared API DTOs, SSE event unions, error shapes, task shapes, and example payloads in `packages/contracts`; update contracts before wiring divergent web/daemon request or response shapes.
- Keep `packages/contracts` pure TypeScript and free of Next.js, Express, Node filesystem/process APIs, browser APIs, SQLite, daemon internals, and sidecar control-plane dependencies.
- Keep project-owned entrypoints, modules, scripts, tests, reporters, and configs TypeScript-first; generated `dist/*.js` is runtime output, and source edits belong in `.ts` files.
- New `.js`, `.mjs`, or `.cjs` files need an explicit generated/vendor/compatibility reason and must pass `pnpm check:residual-js`.
- New `.js`, `.mjs`, or `.cjs` files need an explicit generated/vendor/compatibility reason and must pass `pnpm guard`.
- App business logic must not know about sidecar/control-plane concepts. Keep sidecar awareness in `apps/<app>/sidecar` or the desktop sidecar entry wrapper.
- Shared web/daemon app contracts belong in `packages/contracts`; that package must not depend on Next.js, Express, Node filesystem/process APIs, browser APIs, SQLite, daemon internals, or the sidecar control-plane protocol.
- Sidecar process stamps must have exactly five fields: `app`, `mode`, `namespace`, `ipc`, and `source`.
@ -64,7 +71,7 @@ This file is the single source of truth for agents entering this repository. Rea
## Validation strategy
- After package, workspace, or command-entry changes, run `pnpm install` so workspace links and generated dist entries stay fresh.
- Before marking regular work ready, run at least `pnpm typecheck` and `pnpm test`; run `pnpm build` as well when build boundaries are involved.
- Before marking regular work ready, run at least `pnpm guard` and `pnpm typecheck`, plus the package-scoped tests/builds that match the files changed. Do not use or add root `pnpm test`/`pnpm build` aliases.
- For the web/e2e loop, prefer `pnpm tools-dev run web --daemon-port <port> --web-port <port>`.
- On a GUI-capable machine, validate desktop by running `pnpm tools-dev`, then `pnpm tools-dev inspect desktop status`.
- Stamp/namespace changes must validate two concurrent namespaces and run desktop `inspect eval` plus `inspect screenshot` for each namespace.
@ -86,23 +93,22 @@ pnpm tools-dev check
```
```bash
pnpm guard
pnpm typecheck
pnpm test
pnpm build
pnpm test:ui
pnpm test:ui:headed
pnpm test:e2e:live
pnpm check:residual-js
pnpm -C e2e test:ui
pnpm -C e2e test:ui:headed
pnpm -C e2e test:e2e:live
```
```bash
pnpm --filter @open-design/web typecheck
pnpm --filter @open-design/web test
pnpm --filter @open-design/web build
pnpm --filter @open-design/daemon test
pnpm --filter @open-design/daemon build
pnpm --filter @open-design/desktop build
pnpm --filter @open-design/tools-dev build
pnpm --filter @open-design/tools-pack build
pnpm -r --if-present run typecheck
pnpm -r --if-present run test
```
```bash

View file

@ -33,7 +33,7 @@ corepack enable # wählt das gepinnte pnpm aus packageManager
pnpm install
pnpm tools-dev run web # daemon + web foreground loop
pnpm typecheck # tsc -b --noEmit
pnpm build # production build
pnpm --filter @open-design/web build # Web-Paket bei Bedarf bauen
```
Node `~24` und pnpm `10.33.x` sind erforderlich. `nvm` / `fnm` sind optional; nutzen Sie `nvm install 24 && nvm use 24` oder `fnm install 24 && fnm use 24`, wenn Sie Node so verwalten. macOS, Linux und WSL2 sind die primären Pfade. Windows nativ sollte funktionieren, ist aber kein primäres Ziel.

View file

@ -41,7 +41,7 @@ corepack enable # sélectionne la version de pnpm définie par package
pnpm install
pnpm tools-dev run web # boucle daemon + web au premier plan
pnpm typecheck # tsc -b --noEmit
pnpm build # build production
pnpm --filter @open-design/web build # build du paquet web si nécessaire
```
Node `~24` et pnpm `10.33.x` sont requis. `nvm` / `fnm` sont optionnels ;

View file

@ -33,7 +33,7 @@ corepack enable # packageManager で指定された pnpm を選択
pnpm install
pnpm tools-dev run web # daemon + web フォアグラウンドループ
pnpm typecheck # tsc -b --noEmit
pnpm build # プロダクションビルド
pnpm --filter @open-design/web build # 必要に応じて web パッケージをビルド
```
Node `~24` と pnpm `10.33.x` が必要です。`nvm` / `fnm` はオプション。使用する場合は `nvm install 24 && nvm use 24` または `fnm install 24 && fnm use 24` を実行してください。macOS、Linux、WSL2 が主要プラットフォームです。Windows ネイティブでも動作するはずですが、主要ターゲットではありません — 動作しない場合は issue を作成してください。

View file

@ -33,7 +33,7 @@ corepack enable # selects the pinned pnpm from packageManager
pnpm install
pnpm tools-dev run web # daemon + web foreground loop
pnpm typecheck # tsc -b --noEmit
pnpm build # production build
pnpm --filter @open-design/web build # web package build when needed
```
Node `~24` and pnpm `10.33.x` are required. `nvm` / `fnm` are optional; use `nvm install 24 && nvm use 24` or `fnm install 24 && fnm use 24` if you prefer managing Node that way. macOS, Linux, and WSL2 are the primary paths. Windows native should work but isn't a primary target — file an issue if it doesn't.

View file

@ -33,7 +33,7 @@ corepack enable # selects the pinned pnpm from packageManager
pnpm install
pnpm tools-dev run web # daemon + web foreground loop
pnpm typecheck # tsc -b --noEmit
pnpm build # production build
pnpm --filter @open-design/web build # build do pacote web quando necessário
```
Node `~24` e pnpm `10.33.x` são obrigatórios. `nvm` / `fnm` são opcionais; use `nvm install 24 && nvm use 24` ou `fnm install 24 && fnm use 24` se preferir gerenciar Node assim. macOS, Linux e WSL2 são os caminhos principais. Windows nativo costuma funcionar, mas não é alvo principal — abra uma issue se quebrar.

View file

@ -33,7 +33,7 @@ corepack enable # 使用 packageManager 固定的 pnpm
pnpm install
pnpm tools-dev run web # daemon + web 前台闭环
pnpm typecheck # tsc -b --noEmit
pnpm build # 生产构建
pnpm --filter @open-design/web build # 需要时构建 web package
```
要求 Node `~24` 和 pnpm `10.33.x`。`nvm` / `fnm` 是可选路径;如果你习惯用它们,先执行 `nvm install 24 && nvm use 24``fnm install 24 && fnm use 24`。macOS、Linux、WSL2 是主要路径。Windows 原生应该能跑但不是主要目标 —— 跑不起来请开 issue。

View file

@ -69,7 +69,7 @@ pnpm tools-dev logs # daemon/web/desktop logs anzeigen
pnpm tools-dev check # status + aktuelle logs + gängige Diagnosen
pnpm tools-dev stop # verwaltete Runtimes stoppen
pnpm --filter @open-design/daemon build # apps/daemon/dist/cli.js für `od` bauen
pnpm build # Production Build + static export nach apps/web/out/
pnpm --filter @open-design/web build # Web-Paket bei Bedarf bauen
pnpm typecheck # Workspace-Typecheck
```

View file

@ -70,7 +70,7 @@ pnpm tools-dev logs # affiche les logs daemon/web/desktop
pnpm tools-dev check # statut + logs récents + diagnostics courants
pnpm tools-dev stop # arrête les runtimes gérés
pnpm --filter @open-design/daemon build # build apps/daemon/dist/cli.js pour `od`
pnpm build # build production + export static vers apps/web/out/
pnpm --filter @open-design/web build # build du paquet web si nécessaire
pnpm typecheck # typecheck du workspace
```

View file

@ -69,7 +69,7 @@ pnpm tools-dev logs # daemon/web/desktop のログを表示
pnpm tools-dev check # status + 最近のログ + 一般的な診断
pnpm tools-dev stop # 管理対象ランタイムを停止
pnpm --filter @open-design/daemon build # `od` 用に apps/daemon/dist/cli.js をビルド
pnpm build # 本番ビルド + apps/web/out/ への静的エクスポート
pnpm --filter @open-design/web build # 必要に応じて web パッケージをビルド
pnpm typecheck # workspace の typecheck
```

View file

@ -69,7 +69,7 @@ pnpm tools-dev logs # show daemon/web/desktop logs
pnpm tools-dev check # status + recent logs + common diagnostics
pnpm tools-dev stop # stop managed runtimes
pnpm --filter @open-design/daemon build # build apps/daemon/dist/cli.js for `od`
pnpm build # production build + static export to apps/web/out/
pnpm --filter @open-design/web build # build the web package when needed
pnpm typecheck # workspace typecheck
```

View file

@ -69,7 +69,7 @@ pnpm tools-dev logs # show daemon/web/desktop logs
pnpm tools-dev check # status + recent logs + common diagnostics
pnpm tools-dev stop # stop managed runtimes
pnpm --filter @open-design/daemon build # build apps/daemon/dist/cli.js for `od`
pnpm build # production build + static export to apps/web/out/
pnpm --filter @open-design/web build # build do pacote web quando necessário
pnpm typecheck # workspace typecheck
```

View file

@ -16,6 +16,11 @@ Follow the root `AGENTS.md` first. This file only records module-level boundarie
- `apps/daemon/sidecar/` contains the daemon sidecar entry.
- CLI/agent argument changes or stdout parser changes belong in `apps/daemon/src/agents.ts` and the matching parser tests.
## Test layout
- App tests live in each app's `tests/` directory, sibling to `src/`; preserve source-relative subpaths inside `tests/` when useful.
- Keep app `src/` directories source-only; do not add new `*.test.ts` or `*.test.tsx` files under `src/`.
## Sidecar awareness
- App business layers must not import sidecar packages or branch on `runtime.mode`, `namespace`, `ipc`, or `source`.

View file

@ -33,10 +33,10 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@open-design/contracts": "workspace:0.3.0",
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0",
"@open-design/contracts": "workspace:*",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"better-sqlite3": "^12.9.0",
"chokidar": "^5.0.0",
"express": "^4.19.2",

View file

@ -862,7 +862,7 @@ export async function startServer({ port = 7456, host = process.env.OD_BIND_HOST
const hints: string[] = [];
if (!cliExists) {
hints.push(
'apps/daemon/dist/cli.js is missing. Run `pnpm --filter @open-design/daemon build` (or just `pnpm build`) and refresh.',
'apps/daemon/dist/cli.js is missing. Run `pnpm --filter @open-design/daemon build` and refresh.',
);
}
if (!nodeExists) {

View file

@ -18,9 +18,9 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0"
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*"
},
"devDependencies": {
"@types/node": "24.12.2",

View file

@ -20,12 +20,12 @@
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@open-design/daemon": "workspace:0.3.0",
"@open-design/desktop": "workspace:0.3.0",
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0",
"@open-design/web": "workspace:0.3.0"
"@open-design/daemon": "workspace:*",
"@open-design/desktop": "workspace:*",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"@open-design/web": "workspace:*"
},
"devDependencies": {
"@types/node": "24.12.2",

View file

@ -29,10 +29,10 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@open-design/contracts": "workspace:0.3.0",
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0",
"@open-design/contracts": "workspace:*",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"next": "^16.2.4",
"openai": "^6.35.0",
"react": "^18.3.1",

View file

@ -1519,7 +1519,7 @@ function IntegrationsSection() {
: 'Node binary is missing.'}
</strong>{' '}
{info.buildHint ??
'apps/daemon/dist/cli.js is missing. Run `pnpm build` and refresh.'}
'apps/daemon/dist/cli.js is missing. Run `pnpm --filter @open-design/daemon build` and refresh.'}
</div>
) : null}

View file

@ -1,33 +0,0 @@
import { describe, expect, it } from 'vitest';
import { de } from './locales/de';
import { en } from './locales/en';
import { esES } from './locales/es-ES';
import { fa } from './locales/fa';
import { fr } from './locales/fr';
import { ja } from './locales/ja';
import { ptBR } from './locales/pt-BR';
import { ru } from './locales/ru';
import { zhCN } from './locales/zh-CN';
import { zhTW } from './locales/zh-TW';
const LOCALE_DICTS = {
de,
en,
esES,
fa,
fr,
ja,
ptBR,
ru,
zhCN,
zhTW,
};
describe('Design Files agent copy', () => {
it('uses neutral agent wording in shared helper text', () => {
for (const [locale, dict] of Object.entries(LOCALE_DICTS)) {
expect(dict['designFiles.dropDesc'], locale).not.toMatch(/claude/i);
}
});
});

View file

@ -1,33 +0,0 @@
import { describe, expect, it } from 'vitest';
import { de } from './locales/de';
import { en } from './locales/en';
import { esES } from './locales/es-ES';
import { fa } from './locales/fa';
import { fr } from './locales/fr';
import { ja } from './locales/ja';
import { ptBR } from './locales/pt-BR';
import { ru } from './locales/ru';
import { zhCN } from './locales/zh-CN';
import { zhTW } from './locales/zh-TW';
const LOCALE_DICTS = {
de,
en,
esES,
fa,
fr,
ja,
ptBR,
ru,
zhCN,
zhTW,
};
describe('Design Files dropzone copy', () => {
it('does not advertise unsupported Figma link drops', () => {
for (const [locale, dict] of Object.entries(LOCALE_DICTS)) {
expect(dict['designFiles.dropDesc'], locale).not.toMatch(/figma/i);
}
});
});

View file

@ -5,7 +5,7 @@ import {
createHtmlArtifactManifest,
inferLegacyManifest,
parseArtifactManifest,
} from './manifest';
} from '../../src/artifacts/manifest';
describe('parseArtifactManifest', () => {
it('returns null for malformed json', () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { renderMarkdownToSafeHtml } from './markdown';
import { renderMarkdownToSafeHtml } from '../../src/artifacts/markdown';
describe('renderMarkdownToSafeHtml', () => {
it('renders common markdown blocks', () => {

View file

@ -8,9 +8,9 @@ import {
RendererRegistry,
SvgRenderer,
artifactRendererRegistry,
} from './renderer-registry';
import { renderMarkdownToSafeHtml } from './markdown';
import type { ProjectFile } from '../types';
} from '../../src/artifacts/renderer-registry';
import { renderMarkdownToSafeHtml } from '../../src/artifacts/markdown';
import type { ProjectFile } from '../../src/types';
function baseFile(overrides: Partial<ProjectFile> & Pick<ProjectFile, 'name'>): ProjectFile {
return {

View file

@ -8,8 +8,8 @@ import {
overlayBoundsFromSnapshot,
removeAttachedComment,
targetFromSnapshot,
} from './comments';
import type { ChatMessage, PreviewComment } from './types';
} from '../src/comments';
import type { ChatMessage, PreviewComment } from '../src/types';
describe('preview comment attachment helpers', () => {
it('builds compact target context from an iframe snapshot', () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { assistantRoleLabel } from './AssistantMessage';
import type { ChatMessage } from '../types';
import { assistantRoleLabel } from '../../src/components/AssistantMessage';
import type { ChatMessage } from '../../src/types';
const t = () => 'Assistant';

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { STATUS_LABEL_KEYS, STATUS_ORDER } from './DesignsTab';
import { STATUS_LABEL_KEYS, STATUS_ORDER } from '../../src/components/DesignsTab';
describe('DesignsTab status metadata', () => {
it('places awaiting_input between running and succeeded', () => {

View file

@ -1,8 +1,8 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import { FileViewer, SvgViewer } from './FileViewer';
import type { ProjectFile } from '../types';
import { FileViewer, SvgViewer } from '../../src/components/FileViewer';
import type { ProjectFile } from '../../src/types';
function baseFile(overrides: Partial<ProjectFile>): ProjectFile {
return {

View file

@ -1,7 +1,7 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it, vi } from 'vitest';
import { FileWorkspace } from './FileWorkspace';
import { FileWorkspace } from '../../src/components/FileWorkspace';
describe('FileWorkspace upload input', () => {
it('keeps the Design Files picker aligned with drag-and-drop file support', () => {

View file

@ -1,7 +1,7 @@
import { renderToStaticMarkup } from 'react-dom/server';
import { describe, expect, it } from 'vitest';
import { PreviewModal } from './PreviewModal';
import { PreviewModal } from '../../src/components/PreviewModal';
describe('PreviewModal sandbox isolation', () => {
it('renders generated previews without same-origin sandbox access', () => {

View file

@ -3,8 +3,8 @@ import {
isValidApiBaseUrl,
switchApiProtocolConfig,
updateCurrentApiProtocolConfig,
} from './SettingsDialog';
import type { AppConfig } from '../types';
} from '../../src/components/SettingsDialog';
import type { AppConfig } from '../../src/types';
const baseConfig: AppConfig = {
mode: 'api',

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { decideAutoOpenAfterWrite } from './auto-open-file';
import { decideAutoOpenAfterWrite } from '../../src/components/auto-open-file';
describe('decideAutoOpenAfterWrite', () => {
it('returns shouldOpen=false when filePath is empty', () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { parseForceInline, shouldUrlLoadHtmlPreview } from './file-viewer-render-mode';
import { parseForceInline, shouldUrlLoadHtmlPreview } from '../../src/components/file-viewer-render-mode';
describe('shouldUrlLoadHtmlPreview', () => {
const base = { mode: 'preview' as const, isDeck: false, commentMode: false, forceInline: false };

View file

@ -3,7 +3,7 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { parseFrontmatter } from '../../../daemon/src/frontmatter';
import { LOCALIZED_CONTENT_IDS } from './content';
import { LOCALIZED_CONTENT_IDS } from '../../src/i18n/content';
const repoRoot = fileURLToPath(new URL('../../../../', import.meta.url));

View file

@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { de } from '../../src/i18n/locales/de';
import { en } from '../../src/i18n/locales/en';
import { esES } from '../../src/i18n/locales/es-ES';
import { fa } from '../../src/i18n/locales/fa';
import { fr } from '../../src/i18n/locales/fr';
import { ja } from '../../src/i18n/locales/ja';
import { ptBR } from '../../src/i18n/locales/pt-BR';
import { ru } from '../../src/i18n/locales/ru';
import { zhCN } from '../../src/i18n/locales/zh-CN';
import { zhTW } from '../../src/i18n/locales/zh-TW';
const LOCALE_DICTS = {
de,
en,
esES,
fa,
fr,
ja,
ptBR,
ru,
zhCN,
zhTW,
};
describe('Design Files agent copy', () => {
it('uses neutral agent wording in shared helper text', () => {
for (const [locale, dict] of Object.entries(LOCALE_DICTS)) {
expect(dict['designFiles.dropDesc'], locale).not.toMatch(/claude/i);
}
});
});

View file

@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest';
import { de } from '../../src/i18n/locales/de';
import { en } from '../../src/i18n/locales/en';
import { esES } from '../../src/i18n/locales/es-ES';
import { fa } from '../../src/i18n/locales/fa';
import { fr } from '../../src/i18n/locales/fr';
import { ja } from '../../src/i18n/locales/ja';
import { ptBR } from '../../src/i18n/locales/pt-BR';
import { ru } from '../../src/i18n/locales/ru';
import { zhCN } from '../../src/i18n/locales/zh-CN';
import { zhTW } from '../../src/i18n/locales/zh-TW';
const LOCALE_DICTS = {
de,
en,
esES,
fa,
fr,
ja,
ptBR,
ru,
zhCN,
zhTW,
};
describe('Design Files dropzone copy', () => {
it('does not advertise unsupported Figma link drops', () => {
for (const [locale, dict] of Object.entries(LOCALE_DICTS)) {
expect(dict['designFiles.dropDesc'], locale).not.toMatch(/figma/i);
}
});
});

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { en } from './locales/en';
import { LOCALES, LOCALE_LABEL, type Dict, type Locale } from './types';
import { en } from '../../src/i18n/locales/en';
import { LOCALES, LOCALE_LABEL, type Dict, type Locale } from '../../src/i18n/types';
const EXPECTED_LOCALES = ['en', 'de', 'zh-CN', 'zh-TW', 'pt-BR', 'es-ES', 'ru', 'fa', 'ar', 'ja', 'ko', 'pl', 'hu', 'fr', 'uk'];
@ -15,7 +15,7 @@ function placeholders(value: string): string[] {
}
async function loadDict(locale: Locale): Promise<Dict> {
const module = await import(`./locales/${locale}.ts`);
const module = await import(`../../src/i18n/locales/${locale}.ts`);
const dict = Object.values(module).find((value): value is Dict => {
return Boolean(value) && typeof value === 'object';
});

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { isOpenAICompatible } from './openai-compatible';
import { isOpenAICompatible } from '../../src/providers/openai-compatible';
describe('isOpenAICompatible', () => {
it('preserves explicit OpenAI model routing when the URL contains anthropic', () => {

View file

@ -4,7 +4,7 @@ import {
createProjectEventsConnection,
projectEventsUrl,
type ProjectFileChangeEvent,
} from './project-events';
} from '../../src/providers/project-events';
type Listener = (evt: unknown) => void;

View file

@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { fetchAppVersionInfo, fetchProjectFileText, uploadProjectFiles } from './registry';
import { fetchAppVersionInfo, fetchProjectFileText, uploadProjectFiles } from '../../src/providers/registry';
describe('fetchAppVersionInfo', () => {
afterEach(() => {

View file

@ -1,8 +1,8 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { reattachDaemonRun, streamViaDaemon } from './daemon';
import { streamMessageOpenAI } from './openai-compatible';
import { parseSseFrame } from './sse';
import { reattachDaemonRun, streamViaDaemon } from '../../src/providers/daemon';
import { streamMessageOpenAI } from '../../src/providers/openai-compatible';
import { parseSseFrame } from '../../src/providers/sse';
afterEach(() => {
vi.unstubAllGlobals();

View file

@ -6,7 +6,7 @@ import {
exportAsMd,
exportAsPdf,
openSandboxedPreviewInNewTab,
} from './exports';
} from '../../src/runtime/exports';
function mockResponse(headers: Record<string, string>): Response {
return { headers: new Headers(headers) } as Response;

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { buildReactComponentSrcdoc, prepareReactComponentSource } from './react-component';
import { buildReactComponentSrcdoc, prepareReactComponentSource } from '../../src/runtime/react-component';
describe('prepareReactComponentSource', () => {
it('adapts a default function export for iframe rendering', () => {

View file

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { buildSrcdoc } from './srcdoc';
import { buildSrcdoc } from '../../src/runtime/srcdoc';
const deckHtml = `<!doctype html>
<html>

View file

@ -2,16 +2,16 @@ import { useState } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ToolCard } from '../components/ToolCard';
import { ToolCard } from '../../src/components/ToolCard';
import {
clearToolRenderers,
deriveToolStatus,
getToolRenderer,
registerToolRenderer,
toRenderProps,
} from './tool-renderers';
import type { ToolRenderProps } from './tool-renderers';
import type { AgentEvent } from '../types';
} from '../../src/runtime/tool-renderers';
import type { ToolRenderProps } from '../../src/runtime/tool-renderers';
import type { AgentEvent } from '../../src/types';
type ToolUse = Extract<AgentEvent, { kind: 'tool_use' }>;
type ToolResult = Extract<AgentEvent, { kind: 'tool_result' }>;

View file

@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { DEFAULT_CONFIG, loadConfig } from './config';
import type { AppConfig } from '../types';
import { DEFAULT_CONFIG, loadConfig } from '../../src/state/config';
import type { AppConfig } from '../../src/types';
const store = new Map<string, string>();

View file

@ -1,13 +1,13 @@
import { describe, expect, it } from 'vitest';
import litellmData from './litellm-models.json';
import litellmData from '../../src/state/litellm-models.json';
import {
effectiveMaxTokens,
FALLBACK_MAX_TOKENS,
MAX_MAX_TOKENS,
MIN_MAX_TOKENS,
modelMaxTokensDefault,
} from './maxTokens';
} from '../../src/state/maxTokens';
describe('modelMaxTokensDefault', () => {
it('falls through to LiteLLM data for canonical Anthropic ids', () => {

View file

@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
import { apiProtocolLabel, apiProtocolModelLabel } from './apiProtocol';
import { agentModelDisplayName } from './agentLabels';
import { apiProtocolLabel, apiProtocolModelLabel } from '../../src/utils/apiProtocol';
import { agentModelDisplayName } from '../../src/utils/agentLabels';
describe('api protocol labels', () => {
it('labels the selected API protocol instead of assuming Anthropic', () => {

View file

@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { ChatMessage } from '../types';
import { messageTime } from './chatTime';
import type { ChatMessage } from '../../src/types';
import { messageTime } from '../../src/utils/chatTime';
describe('messageTime', () => {
it('uses assistant startedAt before persisted createdAt', () => {

View file

@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { showCompletionNotification } from './notifications';
import { showCompletionNotification } from '../../src/utils/notifications';
type NotificationOptionsWithRenotify = NotificationOptions & { renotify?: boolean };

View file

@ -37,6 +37,7 @@
"app/**/*",
"sidecar/**/*",
"src/**/*",
"tests/**/*",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"out/types/**/*.ts",

View file

@ -3,6 +3,6 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.{ts,tsx,js,mjs,cjs}'],
include: ['tests/**/*.test.{ts,tsx,js,mjs,cjs}'],
},
});

View file

@ -91,13 +91,7 @@ Evite priorizar:
## Como executar
```bash
pnpm run test:ui
```
Ou direto dentro do pacote de teste:
```bash
pnpm --filter @open-design/e2e test:ui
pnpm -C e2e test:ui
```
Após a execução são gerados automaticamente:

View file

@ -91,13 +91,7 @@
## 运行方式
```bash
pnpm run test:ui
```
也可以直接在独立测试包内运行:
```bash
pnpm --filter @open-design/e2e test:ui
pnpm -C e2e test:ui
```
运行完成后会自动生成:

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"test": "vitest run -c vitest.config.ts",
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p ../scripts/tsconfig.json --noEmit",
"typecheck": "tsc -p tsconfig.json --noEmit",
"test:ui:clean": "node --experimental-strip-types scripts/reset-artifacts.ts",
"test:ui": "corepack pnpm run test:ui:clean && playwright test -c playwright.config.ts",
"test:ui:headed": "corepack pnpm run test:ui:clean && playwright test -c playwright.config.ts --headed",

View file

@ -11,7 +11,7 @@ Este diretório guarda os resultados de execução e relatórios legíveis dos t
- `junit.xml`: resultado em JUnit, prático para integrar com CI
- `test-results/`: anexos brutos dos casos com falha (screenshots, traces, error-context)
Antes de cada execução de `pnpm run test:ui` (ou `pnpm --filter @open-design/e2e test:ui`), o sistema limpa automaticamente:
Antes de cada execução de `pnpm -C e2e test:ui`, o sistema limpa automaticamente:
- `e2e/.od-data/`
- `e2e/reports/test-results/`

View file

@ -11,7 +11,7 @@
- `junit.xml`JUnit 格式结果,方便接 CI
- `test-results/`失败用例的截图、trace、error-context 等原始附件
每次执行 `pnpm run test:ui`(或 `pnpm --filter @open-design/e2e test:ui`前,系统会先自动清理旧的:
每次执行 `pnpm -C e2e test:ui` 前,系统会先自动清理旧的:
- `e2e/.od-data/`
- `e2e/reports/test-results/`

View file

@ -13,21 +13,18 @@
"postinstall": "node ./scripts/postinstall.mjs",
"tools-dev": "pnpm exec tools-dev",
"tools-pack": "pnpm exec tools-pack",
"build": "pnpm --filter @open-design/web build",
"check:residual-js": "node --experimental-strip-types scripts/check-residual-js.ts",
"guard": "tsx ./scripts/guard.ts",
"sync:community-pets": "node --experimental-strip-types scripts/sync-community-pets.ts",
"bake:community-pets": "node --experimental-strip-types scripts/bake-community-pets.ts",
"seed:test-projects": "node --experimental-strip-types scripts/seed-test-projects.ts",
"test:e2e:live": "pnpm --filter @open-design/e2e test:e2e:live",
"test": "pnpm -r --workspace-concurrency=1 --if-present run test",
"test:ui:clean": "pnpm --filter @open-design/e2e test:ui:clean",
"test:ui": "pnpm --filter @open-design/e2e test:ui",
"test:ui:headed": "pnpm --filter @open-design/e2e test:ui:headed",
"typecheck": "pnpm -r --workspace-concurrency=1 --if-present run typecheck && pnpm --filter @open-design/daemon build && pnpm check:residual-js"
"typecheck": "pnpm -r --workspace-concurrency=1 --if-present run typecheck && tsc -p scripts/tsconfig.json --noEmit"
},
"devDependencies": {
"@open-design/tools-dev": "workspace:0.3.0",
"@open-design/tools-pack": "workspace:0.3.0"
"@open-design/tools-dev": "workspace:*",
"@open-design/tools-pack": "workspace:*",
"@types/node": "^20.17.10",
"tsx": "4.21.0",
"typescript": "^5.6.3"
},
"engines": {
"node": "~24",

View file

@ -16,6 +16,7 @@ Follow the root `AGENTS.md` first. This file only records module-level boundarie
## Boundary checklist
- Package tests live in each package's `tests/` directory, sibling to `src/`; keep `src/` source-only and do not add new `*.test.ts` or `*.test.tsx` files under `src/`.
- Do not move runtime validation/schema enforcement into `contracts` prematurely; current contracts define the typed target shape only.
- Do not let app packages depend directly on sidecar control-plane details.
- Do not hard-code Open Design app/source/mode constants in `sidecar` or `platform`.

View file

@ -17,7 +17,7 @@
"types": "./src/index.ts",
"scripts": {
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"zod": "^3.23.8"

View file

@ -5,7 +5,7 @@ import {
defaultCritiqueConfig,
isPanelEvent,
type PanelEvent,
} from './critique';
} from '../src/critique';
describe('CritiqueConfig', () => {
it('defaults validate against the schema', () => {

View file

@ -1,10 +1,10 @@
import { describe, expect, it } from 'vitest';
import type { PanelEvent } from '../critique';
import type { PanelEvent } from '../../src/critique';
import {
panelEventToSse,
type CritiqueSseEvent,
CRITIQUE_SSE_EVENT_NAMES,
} from './critique';
} from '../../src/sse/critique';
describe('CritiqueSseEvent', () => {
it('panelEventToSse maps PanelEvent.type "run_started" to event "critique.run_started"', () => {

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}

View file

@ -17,7 +17,7 @@
"scripts": {
"build": "node ./esbuild.config.mjs && tsc -p tsconfig.json --emitDeclarationOnly",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"devDependencies": {
"@types/node": "24.12.2",

View file

@ -7,7 +7,7 @@ import {
matchesStampedProcess,
readProcessStampFromCommand,
type ProcessStampContract,
} from "./index.js";
} from "../src/index.js";
type FakeStamp = {
app: "api" | "ui";

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}

View file

@ -17,7 +17,7 @@
"scripts": {
"build": "node ./esbuild.config.mjs && tsc -p tsconfig.json --emitDeclarationOnly",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"devDependencies": {
"@types/node": "24.12.2",

View file

@ -15,7 +15,7 @@ import {
STAMP_MODE_FLAG,
STAMP_NAMESPACE_FLAG,
STAMP_SOURCE_FLAG,
} from "./index.js";
} from "../src/index.js";
const validStamp = {
app: APP_KEYS.WEB,

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}

View file

@ -17,7 +17,7 @@
"scripts": {
"build": "node ./esbuild.config.mjs && tsc -p tsconfig.json --emitDeclarationOnly",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"devDependencies": {
"@types/node": "24.12.2",

View file

@ -12,7 +12,7 @@ import {
resolveSourceRuntimeRoot,
type SidecarContractDescriptor,
type SidecarStampShape,
} from "./index.js";
} from "../src/index.js";
type FakeStamp = SidecarStampShape & {
app: "api" | "ui";

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}

View file

@ -9,11 +9,20 @@ importers:
.:
devDependencies:
'@open-design/tools-dev':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:tools/dev
'@open-design/tools-pack':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:tools/pack
'@types/node':
specifier: ^20.17.10
version: 20.19.39
tsx:
specifier: 4.21.0
version: 4.21.0
typescript:
specifier: ^5.6.3
version: 5.9.3
apps/daemon:
dependencies:
@ -21,16 +30,16 @@ importers:
specifier: ^1.0.0
version: 1.29.0(zod@4.4.2)
'@open-design/contracts':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/contracts
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
better-sqlite3:
specifier: ^12.9.0
@ -70,13 +79,13 @@ importers:
apps/desktop:
dependencies:
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
devDependencies:
'@types/node':
@ -123,22 +132,22 @@ importers:
apps/packaged:
dependencies:
'@open-design/daemon':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../daemon
'@open-design/desktop':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../desktop
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
'@open-design/web':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../web
devDependencies:
'@types/node':
@ -160,16 +169,16 @@ importers:
specifier: ^0.32.1
version: 0.32.1
'@open-design/contracts':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/contracts
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
next:
specifier: ^16.2.4
@ -288,13 +297,13 @@ importers:
tools/dev:
dependencies:
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
cac:
specifier: 6.7.14
@ -319,13 +328,13 @@ importers:
specifier: 3.1.0
version: 3.1.0
'@open-design/platform':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/platform
'@open-design/sidecar':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar
'@open-design/sidecar-proto':
specifier: workspace:0.3.0
specifier: workspace:*
version: link:../../packages/sidecar-proto
cac:
specifier: 6.7.14

View file

@ -1,122 +0,0 @@
import { readdir } from "node:fs/promises";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
const residualExtensions = new Set([".js", ".mjs", ".cjs"]);
const skippedDirectories = new Set([
".agents",
".astro",
".claude",
".claude-sessions",
".codex",
".cursor",
".git",
".od",
".od-e2e",
".opencode",
".task",
".tmp",
".vite",
"dist",
"node_modules",
"out",
]);
const allowedExactPaths = new Set([
"packages/platform/esbuild.config.mjs",
"packages/sidecar/esbuild.config.mjs",
"packages/sidecar-proto/esbuild.config.mjs",
// Maintainer utility scripts ported from the media branch. They are
// executed directly by Node and are not loaded by the app runtime.
"scripts/import-prompt-templates.mjs",
"scripts/postinstall.mjs",
"apps/packaged/esbuild.config.mjs",
// Browser service workers must be served as JavaScript files.
"apps/web/public/od-notifications-sw.js",
"scripts/bake-html-ppt-examples.mjs",
"scripts/scaffold-html-ppt-skills.mjs",
"scripts/sync-hyperframes-skill.mjs",
"scripts/verify-media-models.mjs",
"tools/dev/bin/tools-dev.mjs",
"tools/dev/esbuild.config.mjs",
"tools/pack/bin/tools-pack.mjs",
"tools/pack/esbuild.config.mjs",
"tools/pack/resources/mac/notarize.cjs",
// electron-builder hook path; CJS compatibility entry used by tools-pack mac builds.
"tools/pack/resources/mac/web-standalone-after-pack.cjs",
]);
const allowedPathPrefixes = [
"apps/daemon/dist/",
"apps/web/.next/",
"apps/web/out/",
"generated/",
"e2e/playwright-report/",
"e2e/reports/html/",
"e2e/reports/playwright-html-report/",
"e2e/reports/test-results/",
// Vendored upstream HyperFrames skill helper scripts.
"skills/hyperframes/scripts/",
// Vendored upstream html-ppt skill runtime assets (lewislulu/html-ppt-skill).
"skills/html-ppt/assets/",
"test-results/",
"vendor/",
];
function toRepositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
function isAllowedOutputPath(repositoryPath: string): boolean {
if (allowedExactPaths.has(repositoryPath)) return true;
return allowedPathPrefixes.some((prefix) => repositoryPath.startsWith(prefix));
}
function isSkippedDirectoryName(directoryName: string): boolean {
return skippedDirectories.has(directoryName) || directoryName === ".next" || directoryName.startsWith(".next-");
}
async function collectResidualJavaScript(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const residualFiles: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
const repositoryPath = toRepositoryPath(fullPath);
if (entry.isDirectory()) {
if (isSkippedDirectoryName(entry.name) || isAllowedOutputPath(`${repositoryPath}/`)) {
continue;
}
residualFiles.push(...(await collectResidualJavaScript(fullPath)));
continue;
}
if (!entry.isFile() || !residualExtensions.has(path.extname(entry.name))) {
continue;
}
if (isAllowedOutputPath(repositoryPath)) {
continue;
}
residualFiles.push(repositoryPath);
}
return residualFiles;
}
const residualFiles = await collectResidualJavaScript(repoRoot);
if (residualFiles.length > 0) {
console.error("Residual project-owned JavaScript files found:");
for (const filePath of residualFiles) {
console.error(`- ${filePath}`);
}
console.error("Convert these files to TypeScript or add a documented generated/vendor/output allowlist entry.");
process.exitCode = 1;
} else {
console.log("Residual JavaScript check passed: project-owned code is TypeScript-only.");
}

223
scripts/guard.ts Normal file
View file

@ -0,0 +1,223 @@
import { readdir } from "node:fs/promises";
import path from "node:path";
const repoRoot = path.resolve(import.meta.dirname, "..");
type GuardCheck = {
name: string;
run: () => Promise<boolean>;
};
function toRepositoryPath(filePath: string): string {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
const residualExtensions = new Set([".js", ".mjs", ".cjs"]);
const residualSkippedDirectories = new Set([
".agents",
".astro",
".claude",
".claude-sessions",
".codex",
".cursor",
".git",
".od",
".od-e2e",
".opencode",
".task",
".tmp",
".vite",
"dist",
"node_modules",
"out",
]);
const residualAllowedExactPaths = new Set([
"packages/platform/esbuild.config.mjs",
"packages/sidecar/esbuild.config.mjs",
"packages/sidecar-proto/esbuild.config.mjs",
// Maintainer utility scripts ported from the media branch. They are
// executed directly by Node and are not loaded by the app runtime.
"scripts/import-prompt-templates.mjs",
"scripts/postinstall.mjs",
"apps/packaged/esbuild.config.mjs",
// Browser service workers must be served as JavaScript files.
"apps/web/public/od-notifications-sw.js",
"scripts/bake-html-ppt-examples.mjs",
"scripts/scaffold-html-ppt-skills.mjs",
"scripts/sync-hyperframes-skill.mjs",
"scripts/verify-media-models.mjs",
"tools/dev/bin/tools-dev.mjs",
"tools/dev/esbuild.config.mjs",
"tools/pack/bin/tools-pack.mjs",
"tools/pack/esbuild.config.mjs",
"tools/pack/resources/mac/notarize.cjs",
// electron-builder hook path; CJS compatibility entry used by tools-pack mac builds.
"tools/pack/resources/mac/web-standalone-after-pack.cjs",
]);
const residualAllowedPathPrefixes = [
"apps/daemon/dist/",
"apps/web/.next/",
"apps/web/out/",
"generated/",
"e2e/playwright-report/",
"e2e/reports/html/",
"e2e/reports/playwright-html-report/",
"e2e/reports/test-results/",
// Vendored upstream HyperFrames skill helper scripts.
"skills/hyperframes/scripts/",
// Vendored upstream html-ppt skill runtime assets (lewislulu/html-ppt-skill).
"skills/html-ppt/assets/",
"test-results/",
"vendor/",
];
function isResidualAllowedPath(repositoryPath: string): boolean {
if (residualAllowedExactPaths.has(repositoryPath)) return true;
return residualAllowedPathPrefixes.some((prefix) => repositoryPath.startsWith(prefix));
}
function isResidualSkippedDirectoryName(directoryName: string): boolean {
return (
residualSkippedDirectories.has(directoryName) || directoryName === ".next" || directoryName.startsWith(".next-")
);
}
async function collectResidualJavaScript(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const residualFiles: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
const repositoryPath = toRepositoryPath(fullPath);
if (entry.isDirectory()) {
if (isResidualSkippedDirectoryName(entry.name) || isResidualAllowedPath(`${repositoryPath}/`)) {
continue;
}
residualFiles.push(...(await collectResidualJavaScript(fullPath)));
continue;
}
if (!entry.isFile() || !residualExtensions.has(path.extname(entry.name))) {
continue;
}
if (isResidualAllowedPath(repositoryPath)) {
continue;
}
residualFiles.push(repositoryPath);
}
return residualFiles;
}
async function checkResidualJavaScript(): Promise<boolean> {
const residualFiles = await collectResidualJavaScript(repoRoot);
if (residualFiles.length > 0) {
console.error("Residual project-owned JavaScript files found:");
for (const filePath of residualFiles) {
console.error(`- ${filePath}`);
}
console.error("Convert these files to TypeScript or add a documented generated/vendor/output allowlist entry.");
return false;
}
console.log("Residual JavaScript check passed: project-owned code is TypeScript-only.");
return true;
}
const testLayoutScopedDirectories = ["apps", "packages", "tools"];
const testLayoutSkippedDirectories = new Set([".next", "dist", "node_modules", "out"]);
function isTestFile(fileName: string): boolean {
return /\.test\.tsx?$/.test(fileName);
}
function expectedTestPath(repositoryPath: string): string {
const [scope, project, ...relativeParts] = repositoryPath.split("/");
if (!testLayoutScopedDirectories.includes(scope ?? "") || project == null || relativeParts.length === 0) {
return repositoryPath;
}
const normalizedRelativeParts = relativeParts[0] === "src" ? relativeParts.slice(1) : relativeParts;
return [scope, project, "tests", ...normalizedRelativeParts].join("/");
}
function isAllowedScopedTestPath(repositoryPath: string): boolean {
const [scope, project, directory] = repositoryPath.split("/");
return testLayoutScopedDirectories.includes(scope ?? "") && project != null && directory === "tests";
}
async function collectTestLayoutViolations(directory: string): Promise<string[]> {
const entries = await readdir(directory, { withFileTypes: true });
const violations: string[] = [];
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (testLayoutSkippedDirectories.has(entry.name)) {
continue;
}
violations.push(...(await collectTestLayoutViolations(fullPath)));
continue;
}
if (!entry.isFile() || !isTestFile(entry.name)) {
continue;
}
const repositoryPath = toRepositoryPath(fullPath);
if (!isAllowedScopedTestPath(repositoryPath)) {
violations.push(repositoryPath);
}
}
return violations;
}
async function checkTestLayout(): Promise<boolean> {
const violations = (
await Promise.all(
testLayoutScopedDirectories.map((directory) => collectTestLayoutViolations(path.join(repoRoot, directory))),
)
).flat();
if (violations.length > 0) {
console.error("Test files under apps/, packages/, and tools/ must live in tests/ sibling to src/:");
for (const violation of violations) {
console.error(`- ${violation} -> ${expectedTestPath(violation)}`);
}
return false;
}
console.log("Test layout check passed: apps/packages/tools tests live in sibling tests directories.");
return true;
}
const checks: GuardCheck[] = [
{ name: "residual JavaScript", run: checkResidualJavaScript },
{ name: "test layout", run: checkTestLayout },
];
const results: boolean[] = [];
for (const check of checks) {
try {
results.push(await check.run());
} catch (error) {
console.error(`Guard check failed unexpectedly: ${check.name}`);
console.error(error);
results.push(false);
}
}
if (results.some((passed) => !passed)) {
process.exitCode = 1;
}

View file

@ -13,7 +13,6 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"typeRoots": ["../e2e/node_modules/@types"],
"types": ["node"]
},
"include": ["./**/*.ts"]

View file

@ -45,8 +45,9 @@ Expected: pnpm 10.33.2, no errors, all workspace packages linked.
```bash
pnpm typecheck
pnpm test
pnpm check:residual-js
pnpm guard
pnpm --filter @open-design/web test
pnpm --filter @open-design/daemon test
```
Expected: all pass on the unmodified `feat/critique-theater` branch.
@ -74,12 +75,12 @@ Expected: build completes; capture bundle size baseline for the size-limit gate
**Files:**
- Create: `packages/contracts/src/critique.ts`
- Test: `packages/contracts/src/critique.test.ts`
- Test: `packages/contracts/tests/critique.test.ts`
- [ ] **Step 1: Write the failing test**
```ts
// packages/contracts/src/critique.test.ts
// packages/contracts/tests/critique.test.ts
import { describe, expect, it } from 'vitest';
import {
CritiqueConfigSchema,
@ -204,7 +205,7 @@ Expected: PASS, 5/5.
- [ ] **Step 5: Commit**
```bash
git add packages/contracts/src/critique.ts packages/contracts/src/critique.test.ts
git add packages/contracts/src/critique.ts packages/contracts/tests/critique.test.ts
git commit -m "feat(contracts): add CritiqueConfig schema and defaults"
```
@ -212,11 +213,11 @@ git commit -m "feat(contracts): add CritiqueConfig schema and defaults"
**Files:**
- Modify: `packages/contracts/src/critique.ts`
- Test: `packages/contracts/src/critique.test.ts`
- Test: `packages/contracts/tests/critique.test.ts`
- [ ] **Step 1: Add failing tests for the union exhaustiveness**
Append to `packages/contracts/src/critique.test.ts`:
Append to `packages/contracts/tests/critique.test.ts`:
```ts
import { isPanelEvent, type PanelEvent } from './critique';
@ -316,7 +317,7 @@ Expected: PASS, all assertions.
- [ ] **Step 5: Commit**
```bash
git add packages/contracts/src/critique.ts packages/contracts/src/critique.test.ts
git add packages/contracts/src/critique.ts packages/contracts/tests/critique.test.ts
git commit -m "feat(contracts): add PanelEvent discriminated union and isPanelEvent guard"
```
@ -325,7 +326,7 @@ git commit -m "feat(contracts): add PanelEvent discriminated union and isPanelEv
**Files:**
- Modify: `packages/contracts/src/sse.ts` (existing)
- Modify: `packages/contracts/src/index.ts` (re-export critique)
- Test: `packages/contracts/src/sse.test.ts`
- Test: `packages/contracts/tests/sse.test.ts`
- [ ] **Step 1: Inspect the existing `sse.ts` to learn its pattern**
@ -337,7 +338,7 @@ Expected: existing `SseEvent` discriminated union pattern. Match it exactly when
- [ ] **Step 2: Write the failing test**
```ts
// packages/contracts/src/sse.test.ts (append, do not overwrite if file exists)
// packages/contracts/tests/sse.test.ts (append, do not overwrite if file exists)
import { describe, expect, it } from 'vitest';
import { isSseEvent, panelEventToSse, type SseEvent } from './sse';
@ -401,7 +402,7 @@ pnpm --filter @open-design/contracts test
Expected: all sse tests pass.
```bash
git add packages/contracts/src/sse.ts packages/contracts/src/sse.test.ts packages/contracts/src/index.ts
git add packages/contracts/src/sse.ts packages/contracts/tests/sse.test.ts packages/contracts/src/index.ts
git commit -m "feat(contracts): extend SseEvent with critique.* variants and panelEventToSse mapper"
```
@ -449,12 +450,12 @@ git commit -m "test(critique): add v1 wire-protocol golden fixtures"
- Create: `apps/daemon/src/critique/parser.ts`
- Create: `apps/daemon/src/critique/parsers/v1.ts`
- Create: `apps/daemon/src/critique/errors.ts`
- Test: `apps/daemon/src/critique/__tests__/parser.test.ts`
- Test: `apps/daemon/tests/critique/parser.test.ts`
- [ ] **Step 1: Write the failing test against the happy fixture**
```ts
// apps/daemon/src/critique/__tests__/parser.test.ts
// apps/daemon/tests/critique/parser.test.ts
import { describe, expect, it } from 'vitest';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
@ -776,7 +777,7 @@ git commit -m "feat(daemon): add v1 streaming parser for Critique Theater wire p
### Task 2.3: Cover failure-mode fixtures
**Files:**
- Modify: `apps/daemon/src/critique/__tests__/parser.test.ts`
- Modify: `apps/daemon/tests/critique/parser.test.ts`
- [ ] **Step 1: Add failing tests for malformed inputs**
@ -843,12 +844,12 @@ git commit -m "test(daemon): cover parser failure modes with golden fixtures"
**Files:**
- Create: `apps/daemon/src/critique/scoreboard.ts`
- Test: `apps/daemon/src/critique/__tests__/scoreboard.test.ts`
- Test: `apps/daemon/tests/critique/scoreboard.test.ts`
- [ ] **Step 1: Write the failing test**
```ts
// apps/daemon/src/critique/__tests__/scoreboard.test.ts
// apps/daemon/tests/critique/scoreboard.test.ts
import { describe, expect, it } from 'vitest';
import { defaultCritiqueConfig } from '@open-design/contracts/critique';
import { computeComposite } from '../scoreboard';
@ -909,7 +910,7 @@ pnpm --filter @open-design/daemon test scoreboard.test.ts
- [ ] **Step 5: Commit**
```bash
git add apps/daemon/src/critique/scoreboard.ts apps/daemon/src/critique/__tests__/scoreboard.test.ts
git add apps/daemon/src/critique/scoreboard.ts apps/daemon/tests/critique/scoreboard.test.ts
git commit -m "feat(daemon): scoreboard composite formula with weight redistribution"
```
@ -917,7 +918,7 @@ git commit -m "feat(daemon): scoreboard composite formula with weight redistribu
**Files:**
- Modify: `apps/daemon/src/critique/scoreboard.ts`
- Modify: `apps/daemon/src/critique/__tests__/scoreboard.test.ts`
- Modify: `apps/daemon/tests/critique/scoreboard.test.ts`
- [ ] **Step 1: Write the failing test**
@ -980,7 +981,7 @@ pnpm --filter @open-design/daemon test scoreboard.test.ts
- [ ] **Step 5: Commit**
```bash
git add apps/daemon/src/critique/scoreboard.ts apps/daemon/src/critique/__tests__/scoreboard.test.ts
git add apps/daemon/src/critique/scoreboard.ts apps/daemon/tests/critique/scoreboard.test.ts
git commit -m "feat(daemon): scoreboard round-end gate with maxRounds fallback"
```
@ -988,7 +989,7 @@ git commit -m "feat(daemon): scoreboard round-end gate with maxRounds fallback"
**Files:**
- Modify: `apps/daemon/src/critique/scoreboard.ts`
- Modify: `apps/daemon/src/critique/__tests__/scoreboard.test.ts`
- Modify: `apps/daemon/tests/critique/scoreboard.test.ts`
- [ ] **Step 1: Write failing test**
@ -1054,7 +1055,7 @@ git commit -m "feat(daemon): fallback-policy round selector"
**Files:**
- Create: `apps/daemon/src/db/migrations/0042_critique_rounds.up.sql` (number after the latest existing migration; rename if collides)
- Create: `apps/daemon/src/db/migrations/0042_critique_rounds.down.sql`
- Test: `apps/daemon/src/db/__tests__/migrations.test.ts` (extend existing)
- Test: `apps/daemon/tests/db/migrations.test.ts` (extend existing)
- [ ] **Step 1: Inspect current migration list to pick the next ordinal**
@ -1089,7 +1090,7 @@ ALTER TABLE artifacts DROP COLUMN critique_score;
- [ ] **Step 3: Add a migration test that exercises up/down round-trip**
```ts
// apps/daemon/src/db/__tests__/migrations.test.ts (append)
// apps/daemon/tests/db/migrations.test.ts (append)
import Database from 'better-sqlite3';
import { runMigrationsTo, migrationIds } from '../runner';
@ -1122,7 +1123,7 @@ git commit -m "feat(daemon): add critique_* columns to artifacts via reversible
**Files:**
- Create: `apps/daemon/src/critique/transcript.ts`
- Test: `apps/daemon/src/critique/__tests__/transcript.test.ts`
- Test: `apps/daemon/tests/critique/transcript.test.ts`
- [ ] **Step 1: Failing test**
@ -1193,7 +1194,7 @@ export async function writeTranscript(
- [ ] **Step 5: Commit**
```bash
git add apps/daemon/src/critique/transcript.ts apps/daemon/src/critique/__tests__/transcript.test.ts
git add apps/daemon/src/critique/transcript.ts apps/daemon/tests/critique/transcript.test.ts
git commit -m "feat(daemon): transcript writer with ndjson + gzip threshold"
```
@ -1201,7 +1202,7 @@ git commit -m "feat(daemon): transcript writer with ndjson + gzip threshold"
**Files:**
- Create: `apps/daemon/src/critique/orchestrator.ts`
- Test: `apps/daemon/src/critique/__tests__/orchestrator.test.ts`
- Test: `apps/daemon/tests/critique/orchestrator.test.ts`
- Modify: `apps/daemon/src/agents/spawn.ts` (existing) to call orchestrator when `enabled`
- [ ] **Step 1: Failing test against the happy fixture wired through orchestrator**
@ -1365,7 +1366,7 @@ function writeTranscriptSync(dir: string, events: PanelEvent[]): string {
- [ ] **Step 5: Commit**
```bash
git add apps/daemon/src/critique/orchestrator.ts apps/daemon/src/critique/__tests__/orchestrator.test.ts
git add apps/daemon/src/critique/orchestrator.ts apps/daemon/tests/critique/orchestrator.test.ts
git commit -m "feat(daemon): orchestrator wires parser, scoreboard, SSE, and persistence"
```
@ -1389,7 +1390,7 @@ In `spawn.ts`, after stdout is established, branch on `cfg.enabled`:
- [ ] **Step 3: Add an integration test**
```ts
// apps/daemon/src/agents/__tests__/spawn-critique.test.ts
// apps/daemon/tests/agents/spawn-critique.test.ts
import { spawnAgent } from '../spawn';
it('routes through critique orchestrator when OD_CRITIQUE_ENABLED=true', async () => {
@ -1421,7 +1422,7 @@ git commit -m "feat(daemon): branch agent spawn through critique orchestrator wh
**Files:**
- Create: `apps/web/src/prompts/panel.ts`
- Test: `apps/web/src/prompts/__tests__/panel.test.ts`
- Test: `apps/web/tests/prompts/panel.test.ts`
- [ ] **Step 1: Failing snapshot test**
@ -1529,7 +1530,7 @@ Skill: ${skill.id}.
- [ ] **Step 5: Commit**
```bash
git add apps/web/src/prompts/panel.ts apps/web/src/prompts/__tests__/panel.test.ts
git add apps/web/src/prompts/panel.ts apps/web/tests/prompts/panel.test.ts
git commit -m "feat(web): add Critique Theater prompt protocol addendum"
```
@ -1547,7 +1548,7 @@ grep -n "compose\|render\|prompt" apps/web/src/prompts/discovery.ts | head -20
- [ ] **Step 2: Add failing test that final composed prompt contains PROTOCOL block**
```ts
// apps/web/src/prompts/__tests__/discovery.test.ts (extend)
// apps/web/tests/prompts/discovery.test.ts (extend)
it('appends Critique Theater protocol when cfg.enabled', () => {
const out = composeDiscoveryPrompt({ ...input, critique: { enabled: true } });
expect(out).toContain('<CRITIQUE_RUN');
@ -1593,7 +1594,7 @@ git commit -m "feat(web): wire panel prompt addendum into discovery composer"
**Files:**
- Create: `apps/daemon/src/api/projects/critique/interrupt.ts`
- Test: `apps/daemon/src/api/projects/critique/__tests__/interrupt.test.ts`
- Test: `apps/daemon/tests/api/projects/critique/interrupt.test.ts`
- [ ] **Step 1: Failing test**
@ -1641,7 +1642,7 @@ git commit -m "feat(daemon): /api/projects/:id/critique/:runId/interrupt endpoin
**Files:**
- Create: `apps/daemon/src/api/projects/critique/rerun.ts`
- Test: `apps/daemon/src/api/projects/critique/__tests__/rerun.test.ts`
- Test: `apps/daemon/tests/api/projects/critique/rerun.test.ts`
- [ ] **Step 15: Same TDD shape as 6.1.** Endpoint resolves the original brief, builds a new artifact row (immutable original), and starts a fresh run with the previous artifact attached as prior-art context.
@ -1657,7 +1658,7 @@ git commit -m "feat(daemon): /api/projects/:id/artifacts/:artifactId/critique/re
**Files:**
- Create: `apps/web/src/components/Theater/state/reducer.ts`
- Test: `apps/web/src/components/Theater/state/__tests__/reducer.test.ts`
- Test: `apps/web/tests/components/Theater/state/reducer.test.ts`
- [ ] **Step 1: Write failing reducer tests**
@ -1800,7 +1801,7 @@ git commit -m "feat(web): pure reducer for Critique Theater states"
**Files:**
- Create: `apps/web/src/components/Theater/hooks/useCritiqueStream.ts`
- Test: `apps/web/src/components/Theater/hooks/__tests__/useCritiqueStream.test.tsx`
- Test: `apps/web/tests/components/Theater/hooks/useCritiqueStream.test.tsx`
- [ ] **Step 15:** Standard React hook TDD. Hook subscribes to the existing `useProjectEvents()` SSE bus, filters to `critique.*` events, feeds them into the reducer via `useReducer`, and returns `[state, dispatch]`. Use RTL with a stub event source to drive the test.
@ -1812,7 +1813,7 @@ git commit -m "feat(web): useCritiqueStream hook subscribes to SSE and feeds red
**Files:**
- Create: `apps/web/src/components/Theater/hooks/useCritiqueReplay.ts`
- Test: same `__tests__/`
- Test: same `tests/` component area
- [ ] **Step 15:** Hook fetches `transcript_path`, decompresses if `.gz`, splits ndjson lines, dispatches into the reducer at the chosen speed. Test with a fixture transcript on disk.
@ -1839,7 +1840,7 @@ For each of `PanelistLane.tsx`, `ScoreTicker.tsx`, `RoundDivider.tsx`, `TheaterS
- [ ] **Step 5: Commit.** One component per commit:
```bash
git add apps/web/src/components/Theater/<Component>.tsx apps/web/src/components/Theater/__tests__/<Component>.test.tsx
git add apps/web/src/components/Theater/<Component>.tsx apps/web/tests/components/Theater/<Component>.test.tsx
git commit -m "feat(web): Theater <Component>"
```
@ -1944,7 +1945,7 @@ The conformance harness runs against every adapter listed `status: production` i
- Create: `apps/daemon/src/critique/__fixtures__/adapters/synthetic-good.ts` — child-process stub that writes `happy-3-rounds.txt`.
- Create: `apps/daemon/src/critique/__fixtures__/adapters/synthetic-bad.ts` — stub that writes `malformed-unbalanced.txt`.
- [ ] **Step 15:** Write each as a tiny Node script invoked through the daemon's existing CLI-spawn primitive. Tests in `apps/daemon/src/critique/__tests__/conformance.test.ts` register both as fake adapters and assert good ⇒ shipped, bad ⇒ degraded with `critique:degraded` mark and 24h TTL.
- [ ] **Step 15:** Write each as a tiny Node script invoked through the daemon's existing CLI-spawn primitive. Tests in `apps/daemon/tests/critique/conformance.test.ts` register both as fake adapters and assert good ⇒ shipped, bad ⇒ degraded with `critique:degraded` mark and 24h TTL.
```bash
git commit -m "feat(daemon): adapter conformance synthetic fixtures and degraded TTL"
@ -2012,7 +2013,7 @@ git commit -m "test(a11y): Theater self-audits to WCAG AA"
**Files:**
- Modify: `apps/daemon/src/metrics/index.ts` (existing)
- Test: `apps/daemon/src/metrics/__tests__/critique.test.ts`
- Test: `apps/daemon/tests/metrics/critique.test.ts`
- [ ] **Step 1: Failing test.** Register the metrics, drive a synthetic run through the orchestrator, scrape `/api/metrics`, assert the named series exist with sane labels.
@ -2194,7 +2195,7 @@ git commit -m "chore(rollout): M0 ships behind OD_CRITIQUE_ENABLED=false"
### Task 15.2: Final validation matrix
- [ ] **Step 1: Run** `pnpm typecheck`, `pnpm test`, `pnpm test:ui`, `pnpm test:e2e:live`, `pnpm build`, `pnpm check:residual-js`, `pnpm check:dead-exports`, `pnpm check:critique-coverage`, `pnpm size-limit`. All must pass.
- [ ] **Step 1: Run** `pnpm guard`, `pnpm typecheck`, package-scoped tests/builds for changed packages, `pnpm -C e2e test:ui`, `pnpm -C e2e test:e2e:live`, `pnpm check:dead-exports`, `pnpm check:critique-coverage`, `pnpm size-limit`. All must pass.
- [ ] **Step 2: Run** `pnpm tools-dev run web --daemon-port 17456 --web-port 17573` and validate live happy path with a real CLI on PATH.
@ -2213,8 +2214,8 @@ gh pr create --title "feat: Critique Theater (panel-tempered, scored, replayable
- Zero new processes; same BYOK story; works across all 12 adapters with conformance grading.
## Test plan
- [ ] pnpm typecheck && pnpm test && pnpm test:ui
- [ ] pnpm test:e2e:live (Playwright happy + interrupt + visual + a11y)
- [ ] pnpm guard && pnpm typecheck && pnpm -C e2e test:ui
- [ ] pnpm -C e2e test:e2e:live (Playwright happy + interrupt + visual + a11y)
- [ ] pnpm size-limit (Theater bundle < 18 KiB gz)
- [ ] pnpm check:critique-coverage (no orphan surfaces)
- [ ] manual: enable in Settings, submit a brief, watch Theater, ship at >= 8.0

View file

@ -55,7 +55,7 @@ The optimization work should proceed in dependency order. Some items can run in
|---|---|---|---|---|---|
| W1 | Completed | Confirm architecture and capability boundaries | R4, R15 | — | Written ownership rules for web, daemon, shared contracts, and dangerous local capabilities. See `specs/current/architecture-boundaries.md`. |
| W2 | Completed | Define API, SSE, and error contracts | R2, R7, R8 | W1 | `packages/contracts` now provides shared request/response types, SSE event unions, and error model helpers consumed by web and daemon. |
| W3 | Completed | Migrate project-owned code to TypeScript | R1 | W2 for highest-value shared types | Daemon, root scripts, and e2e support now use TypeScript sources; daemon compiles to `apps/daemon/dist`; residual JS is checked by `pnpm check:residual-js`. |
| W3 | Completed | Migrate project-owned code to TypeScript | R1 | W2 for highest-value shared types | Daemon, root scripts, and e2e support now use TypeScript sources; daemon compiles to `apps/daemon/dist`; residual JS is checked by `pnpm guard`. |
| W4 | Planned | Add runtime validation at daemon boundaries | R3, R4 | W2 | Schemas for HTTP requests, paths, agents, models, uploads, task IDs, and command args. |
| W5 | Planned | Modularize `server.ts` | R6 | W2, W3, W4 | Thin route handlers plus services/adapters for agents, DB, FS, streams, and artifacts. |
| W6 | Planned | Introduce agent process/task manager | R5, R8, R11 | W2, W5 | Task state machine, cancellation, timeout, cleanup, exit handling, and concurrency controls. |

View file

@ -15,10 +15,11 @@ Follow the root `AGENTS.md` first. This file only records module-level boundarie
- Keep `tools-pack` focused on packaging/runtime control and release artifact preparation. Runtime updater product integration remains a later phase.
- Pack-specific Electron builder resources belong under `tools/pack/resources/`; do not reference app/docs/download assets directly from pack logic.
- Namespace controls packaged data/log/runtime/cache paths. Ports are transient transport details and must not participate in path decisions.
- The package/build boundary of root `pnpm build` is intentionally unchanged in this round and should be handled by the future `tools-pack` task.
- There is no root `pnpm build` aggregate. Use package-scoped builds for source packages and `pnpm tools-pack ...` for packaged artifact build/install/release flows.
## Orchestration boundary
- Tool tests live in each tool's `tests/` directory, sibling to `src/`; keep `src/` source-only and do not add new `*.test.ts` or `*.test.tsx` files under `src/`.
- Orchestration layers must consume primitives from `@open-design/sidecar-proto`, `@open-design/sidecar`, and `@open-design/platform`.
- Do not hand-build `--od-stamp-*` args, process-scan regexes, runtime tokens, process roles, or duplicate namespace/source args in `tools/dev`, future `tools/pack`, or packaged launchers.
- Port flags are authoritative inputs: `--daemon-port` and `--web-port`. Internal env vars are `OD_PORT` and `OD_WEB_PORT`; do not introduce `NEXT_PORT`.

View file

@ -9,13 +9,13 @@
"scripts": {
"build": "node ./esbuild.config.mjs",
"dev": "tsx ./src/index.ts",
"test": "node --import tsx --test src/*.test.ts",
"test": "node --import tsx --test tests/*.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"cac": "6.7.14"
},
"devDependencies": {

View file

@ -909,7 +909,7 @@ function addPortOptions(command: ReturnType<typeof cli.command>) {
return command
.option("--daemon-port <port>", "force daemon port; conflict quick-fails")
.option("--web-port <port>", "force web port; conflict quick-fails")
.option("--prod", "use production build (requires pnpm build first)");
.option("--prod", "use production build (requires pnpm --filter @open-design/web build first)");
}
addPortOptions(addSharedOptions(cli.command("start [app]", "Start daemon, web, desktop, or all when app is omitted"))).action(

View file

@ -5,7 +5,7 @@ import {
appendStartupLogDiagnostics,
createStartupLogDiagnostics,
detectLogDiagnostics,
} from "./diagnostics.js";
} from "../src/diagnostics.js";
describe("tools-dev diagnostics", () => {
it("detects native addon ABI mismatches", () => {

View file

@ -14,5 +14,5 @@
"target": "ES2024",
"types": ["node"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "esbuild.config.mjs"]
"include": ["src/**/*.ts", "src/**/*.d.ts", "tests/**/*.ts", "esbuild.config.mjs"]
}

View file

@ -10,12 +10,12 @@
"build": "node ./esbuild.config.mjs && tsc -p tsconfig.json --emitDeclarationOnly",
"dev": "tsx ./src/index.ts",
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit"
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit"
},
"dependencies": {
"@open-design/platform": "workspace:0.3.0",
"@open-design/sidecar": "workspace:0.3.0",
"@open-design/sidecar-proto": "workspace:0.3.0",
"@open-design/platform": "workspace:*",
"@open-design/sidecar": "workspace:*",
"@open-design/sidecar-proto": "workspace:*",
"@electron/notarize": "3.1.0",
"cac": "6.7.14",
"electron-builder": "26.8.1"

View file

@ -1,12 +1,12 @@
import { describe, expect, it } from "vitest";
import type { ToolPackConfig } from "./config.js";
import type { ToolPackConfig } from "../src/config.js";
import {
buildDockerArgs,
matchesAppImageProcess,
renderDesktopTemplate,
sanitizeNamespace,
} from "./linux.js";
} from "../src/linux.js";
function makeConfig(): ToolPackConfig {
return {

View file

@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm, writeFile, mkdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { copyBundledResourceTrees } from "./resources.js";
import { copyBundledResourceTrees } from "../src/resources.js";
describe("copyBundledResourceTrees", () => {
it("includes prompt templates", async () => {

View file

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"emitDeclarationOnly": false,
"noEmit": true,
"rootDir": "."
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
}