From f81eda80bba19929808b04ca72f0bd41d76cd154 Mon Sep 17 00:00:00 2001 From: eust-w Date: Thu, 26 Mar 2026 17:56:23 +0800 Subject: [PATCH] :sparkles: feat: add web surfaces artifacts and developer commands --- .githooks/pre-push | 2 +- .github/workflows/guardrails.yml | 15 +- .gitignore | 2 + CONTRIBUTING.md | 16 +- Makefile | 26 ++ README.md | 56 ++++ apps/api/package.json | 2 +- apps/api/src/main.ts | 35 ++ .../modules/artifacts/artifacts.controller.ts | 20 ++ .../src/modules/artifacts/artifacts.module.ts | 12 + .../modules/artifacts/artifacts.service.ts | 28 ++ .../modules/plugins/builtin/delivery-nodes.ts | 38 +++ apps/api/test/artifacts.e2e-spec.ts | 35 ++ apps/web/package.json | 6 +- apps/web/src/app/router.tsx | 90 +++++ .../src/features/assets/asset-detail-page.tsx | 42 +++ .../src/features/assets/assets-page.test.tsx | 62 ++++ apps/web/src/features/assets/assets-page.tsx | 35 ++ .../features/assets/components/asset-list.tsx | 26 ++ .../assets/components/asset-summary-panel.tsx | 33 ++ .../features/explore/explore-page.test.tsx | 61 ++++ .../web/src/features/explore/explore-page.tsx | 51 +++ .../explore/renderers/directory-renderer.tsx | 11 + .../explore/renderers/json-renderer.tsx | 8 + .../explore/renderers/video-renderer.tsx | 10 + apps/web/src/features/layout/app-shell.tsx | 43 +++ .../features/projects/project-selector.tsx | 7 + .../runs/components/run-graph-view.tsx | 22 ++ .../runs/components/task-log-panel.tsx | 20 ++ .../web/src/features/runs/run-detail-page.tsx | 37 +++ .../components/node-config-panel.tsx | 34 ++ .../workflows/components/node-library.tsx | 76 +++++ .../workflows/components/workflow-canvas.tsx | 30 ++ .../workflows/workflow-editor-page.test.tsx | 63 ++++ .../workflows/workflow-editor-page.tsx | 31 ++ .../src/features/workflows/workflows-page.tsx | 25 ++ .../workspaces/workspace-switcher.tsx | 9 + apps/web/src/main.tsx | 12 + apps/worker/package.json | 2 +- apps/worker/src/main.ts | 4 + .../03-workflows/workflow-execution-model.md | 22 ++ design/05-data/mongodb-data-model.md | 13 + docs/development-workflow.md | 17 +- ...26-03-26-emboflow-v1-foundation-and-mvp.md | 1 + package.json | 7 +- pnpm-lock.yaml | 310 +++++++++++++++++- tests/test_dev_commands.py | 39 +++ 47 files changed, 1532 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/modules/artifacts/artifacts.controller.ts create mode 100644 apps/api/src/modules/artifacts/artifacts.module.ts create mode 100644 apps/api/src/modules/artifacts/artifacts.service.ts create mode 100644 apps/api/src/modules/plugins/builtin/delivery-nodes.ts create mode 100644 apps/api/test/artifacts.e2e-spec.ts create mode 100644 apps/web/src/app/router.tsx create mode 100644 apps/web/src/features/assets/asset-detail-page.tsx create mode 100644 apps/web/src/features/assets/assets-page.test.tsx create mode 100644 apps/web/src/features/assets/assets-page.tsx create mode 100644 apps/web/src/features/assets/components/asset-list.tsx create mode 100644 apps/web/src/features/assets/components/asset-summary-panel.tsx create mode 100644 apps/web/src/features/explore/explore-page.test.tsx create mode 100644 apps/web/src/features/explore/explore-page.tsx create mode 100644 apps/web/src/features/explore/renderers/directory-renderer.tsx create mode 100644 apps/web/src/features/explore/renderers/json-renderer.tsx create mode 100644 apps/web/src/features/explore/renderers/video-renderer.tsx create mode 100644 apps/web/src/features/layout/app-shell.tsx create mode 100644 apps/web/src/features/projects/project-selector.tsx create mode 100644 apps/web/src/features/runs/components/run-graph-view.tsx create mode 100644 apps/web/src/features/runs/components/task-log-panel.tsx create mode 100644 apps/web/src/features/runs/run-detail-page.tsx create mode 100644 apps/web/src/features/workflows/components/node-config-panel.tsx create mode 100644 apps/web/src/features/workflows/components/node-library.tsx create mode 100644 apps/web/src/features/workflows/components/workflow-canvas.tsx create mode 100644 apps/web/src/features/workflows/workflow-editor-page.test.tsx create mode 100644 apps/web/src/features/workflows/workflow-editor-page.tsx create mode 100644 apps/web/src/features/workflows/workflows-page.tsx create mode 100644 apps/web/src/features/workspaces/workspace-switcher.tsx create mode 100644 apps/web/src/main.tsx create mode 100644 tests/test_dev_commands.py diff --git a/.githooks/pre-push b/.githooks/pre-push index bde3dd4..f71ad44 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -16,4 +16,4 @@ else python3 scripts/check_commit_message.py --rev-range HEAD fi -python3 -m unittest discover -s tests -p 'test_*.py' +make test diff --git a/.github/workflows/guardrails.yml b/.github/workflows/guardrails.yml index 8eb501b..67ee81b 100644 --- a/.github/workflows/guardrails.yml +++ b/.github/workflows/guardrails.yml @@ -19,6 +19,19 @@ jobs: with: python-version: "3.11" + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.12.3 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + - name: Compute git range id: git_range shell: bash @@ -42,4 +55,4 @@ jobs: - name: Run repository tests run: | - python3 -m unittest discover -s tests -p 'test_*.py' + make test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 427d1a4..fef8fa2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Do not treat design files as background notes. If a code change affects product 5. Install the local git hooks once per clone: ```bash -bash scripts/install_hooks.sh +make bootstrap ``` 6. Use English-only commit messages with a gitmoji prefix, for example: @@ -27,7 +27,7 @@ bash scripts/install_hooks.sh 7. Run the local sync check when needed: ```bash -python3 scripts/check_doc_code_sync.py . --strict +make guardrails ``` 8. If design and code still diverge, document that explicitly in your final summary. @@ -71,13 +71,13 @@ This repository includes: - a hook installer: ```bash -bash scripts/install_hooks.sh +make bootstrap ``` - a design/code sync checker: ```bash -python3 scripts/check_doc_code_sync.py . --strict +make guardrails ``` - a commit message validator: @@ -86,6 +86,14 @@ python3 scripts/check_doc_code_sync.py . --strict python3 scripts/check_commit_message.py --rev-range HEAD ``` +- package-level development entry commands: + +```bash +make dev-api +make dev-web +make dev-worker +``` + The hooks and CI enforce: - English-only commit messages with a gitmoji prefix diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4eb4930 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +SHELL := /bin/bash + +.PHONY: bootstrap test dev-api dev-web dev-worker guardrails + +bootstrap: + pnpm install + bash scripts/install_hooks.sh + +test: + python3 -m unittest discover -s tests -p 'test_*.py' + pnpm --filter api test + pnpm --filter web test src/features/assets/assets-page.test.tsx src/features/workflows/workflow-editor-page.test.tsx src/features/explore/explore-page.test.tsx + pnpm --filter worker test + +dev-api: + pnpm --filter api dev + +dev-web: + pnpm --filter web dev + +dev-worker: + pnpm --filter worker dev + +guardrails: + python3 scripts/check_doc_code_sync.py . --strict + python3 scripts/check_commit_message.py --rev-range HEAD diff --git a/README.md b/README.md new file mode 100644 index 0000000..36bea70 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# EmboFlow + +EmboFlow is a B/S embodied-data workflow platform for raw asset ingestion, delivery normalization, dataset transformation, workflow execution, preview, and export. + +## Bootstrap + +From the repository root: + +```bash +make bootstrap +``` + +This installs workspace dependencies and runs `scripts/install_hooks.sh` so local commit and push guardrails are active. + +## Local Commands + +Run the full repository test suite: + +```bash +make test +``` + +Run the strict repository guardrails: + +```bash +make guardrails +``` + +Start package-level development entrypoints: + +```bash +make dev-api +make dev-web +make dev-worker +``` + +## Repository Structure + +- `apps/api` contains the control-plane modules for workspaces, assets, workflows, runs, and artifacts. +- `apps/web` contains the initial shell, asset workspace, workflow editor surface, run detail view, and explore renderers. +- `apps/worker` contains the local scheduler, task runner, and executor contracts. +- `design/` contains the architecture and product design documents that must stay aligned with implementation. +- `docs/` contains workflow guidance and the executable implementation plan. + +## Developer Workflow + +1. Read the relevant design files under `design/` before editing code. +2. Implement code and update impacted docs in the same change set. +3. Use English-only commit messages with a gitmoji prefix. +4. Run `make test` and `make guardrails` before pushing changes. + +For direct hook installation or reinstallation: + +```bash +bash scripts/install_hooks.sh +``` diff --git a/apps/api/package.json b/apps/api/package.json index 14f2620..500e6d5 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "type": "module", "scripts": { - "dev": "echo 'api app scaffold pending'", + "dev": "tsx watch src/main.ts", "test": "node --test --experimental-strip-types" } } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..48f68b2 --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,35 @@ +import { createArtifactsModule } from "./modules/artifacts/artifacts.module.ts"; +import { createAssetsModule } from "./modules/assets/assets.module.ts"; +import { createAuthModule } from "./modules/auth/auth.module.ts"; +import { createProjectsModule } from "./modules/projects/projects.module.ts"; +import { createRunsModule } from "./modules/runs/runs.module.ts"; +import { createStorageModule } from "./modules/storage/storage.module.ts"; +import { createWorkflowsModule } from "./modules/workflows/workflows.module.ts"; +import { createWorkspaceModule } from "./modules/workspaces/workspaces.module.ts"; + +export function createApiApp() { + const auth = createAuthModule(); + const workspaces = createWorkspaceModule(); + const projects = createProjectsModule(workspaces.service); + const storage = createStorageModule(); + const assets = createAssetsModule(storage.service); + const workflows = createWorkflowsModule(); + const runs = createRunsModule(workflows.service); + const artifacts = createArtifactsModule(); + + return { + status: "ready" as const, + auth, + workspaces, + projects, + storage, + assets, + workflows, + runs, + artifacts, + }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + process.stdout.write(JSON.stringify({ status: createApiApp().status }, null, 2)); +} diff --git a/apps/api/src/modules/artifacts/artifacts.controller.ts b/apps/api/src/modules/artifacts/artifacts.controller.ts new file mode 100644 index 0000000..255c428 --- /dev/null +++ b/apps/api/src/modules/artifacts/artifacts.controller.ts @@ -0,0 +1,20 @@ +import { + ArtifactsService, + type ArtifactRecord, +} from "./artifacts.service.ts"; + +export class ArtifactsController { + private readonly service: ArtifactsService; + + constructor(service: ArtifactsService) { + this.service = service; + } + + createArtifact(input: ArtifactRecord) { + return this.service.createArtifact(input); + } + + listArtifactsByProducer(producerType: string, producerId: string) { + return this.service.listArtifactsByProducer(producerType, producerId); + } +} diff --git a/apps/api/src/modules/artifacts/artifacts.module.ts b/apps/api/src/modules/artifacts/artifacts.module.ts new file mode 100644 index 0000000..a5f06f5 --- /dev/null +++ b/apps/api/src/modules/artifacts/artifacts.module.ts @@ -0,0 +1,12 @@ +import { ArtifactsController } from "./artifacts.controller.ts"; +import { ArtifactsService } from "./artifacts.service.ts"; + +export function createArtifactsModule() { + const service = new ArtifactsService(); + const controller = new ArtifactsController(service); + + return { + service, + controller, + }; +} diff --git a/apps/api/src/modules/artifacts/artifacts.service.ts b/apps/api/src/modules/artifacts/artifacts.service.ts new file mode 100644 index 0000000..7c8d010 --- /dev/null +++ b/apps/api/src/modules/artifacts/artifacts.service.ts @@ -0,0 +1,28 @@ +export type ArtifactRecord = { + id: string; + type: "json" | "directory" | "video"; + title: string; + producerType: string; + producerId: string; + payload: Record; +}; + +export class ArtifactsService { + private readonly artifacts: ArtifactRecord[] = []; + + createArtifact(input: ArtifactRecord): ArtifactRecord { + this.artifacts.push(input); + return input; + } + + listArtifactsByProducer( + producerType: string, + producerId: string, + ): ArtifactRecord[] { + return this.artifacts.filter( + (artifact) => + artifact.producerType === producerType && + artifact.producerId === producerId, + ); + } +} diff --git a/apps/api/src/modules/plugins/builtin/delivery-nodes.ts b/apps/api/src/modules/plugins/builtin/delivery-nodes.ts new file mode 100644 index 0000000..d58231b --- /dev/null +++ b/apps/api/src/modules/plugins/builtin/delivery-nodes.ts @@ -0,0 +1,38 @@ +export const DELIVERY_NODE_DEFINITIONS = [ + { + id: "source-asset", + name: "Source Asset", + category: "Source", + description: "Load the uploaded asset or registered path into the workflow.", + }, + { + id: "extract-archive", + name: "Extract Archive", + category: "Transform", + description: "Unpack tar, zip, or zst archives for downstream processing.", + }, + { + id: "rename-folder", + name: "Rename Delivery Folder", + category: "Transform", + description: "Rename the top-level folder to the delivery naming convention.", + }, + { + id: "validate-structure", + name: "Validate Structure", + category: "Inspect", + description: "Check required directories and metadata files.", + }, + { + id: "validate-metadata", + name: "Validate Metadata", + category: "Inspect", + description: "Validate meta.json, intrinsics.json, and video_meta.json.", + }, + { + id: "export-delivery-package", + name: "Export Delivery Package", + category: "Export", + description: "Publish the normalized package for downstream upload or handoff.", + }, +] as const; diff --git a/apps/api/test/artifacts.e2e-spec.ts b/apps/api/test/artifacts.e2e-spec.ts new file mode 100644 index 0000000..39417c2 --- /dev/null +++ b/apps/api/test/artifacts.e2e-spec.ts @@ -0,0 +1,35 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { createArtifactsModule } from "../src/modules/artifacts/artifacts.module.ts"; + +test("retrieve artifacts by producer", () => { + const module = createArtifactsModule(); + + module.controller.createArtifact({ + id: "artifact-1", + type: "json", + title: "Probe Report", + producerType: "run_task", + producerId: "task-1", + payload: { + detectedFormats: ["delivery_package"], + }, + }); + module.controller.createArtifact({ + id: "artifact-2", + type: "directory", + title: "Normalized Folder", + producerType: "run_task", + producerId: "task-1", + payload: { + entries: ["DJI_001", "meta.json"], + }, + }); + + const artifacts = module.controller.listArtifactsByProducer("run_task", "task-1"); + + assert.equal(artifacts.length, 2); + assert.equal(artifacts[0]?.title, "Probe Report"); + assert.equal(artifacts[1]?.type, "directory"); +}); diff --git a/apps/web/package.json b/apps/web/package.json index 6ab7aff..26b6995 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,8 +1,10 @@ { - "name": "@emboflow/web", + "name": "web", "private": true, "version": "0.1.0", + "type": "module", "scripts": { - "dev": "echo 'web app scaffold pending'" + "dev": "tsx watch src/main.tsx", + "test": "tsx --test" } } diff --git a/apps/web/src/app/router.tsx b/apps/web/src/app/router.tsx new file mode 100644 index 0000000..1dd7196 --- /dev/null +++ b/apps/web/src/app/router.tsx @@ -0,0 +1,90 @@ +import { renderAssetsPage } from "../features/assets/assets-page.tsx"; +import { renderAssetDetailPage } from "../features/assets/asset-detail-page.tsx"; +import { renderExplorePage } from "../features/explore/explore-page.tsx"; +import { renderRunDetailPage } from "../features/runs/run-detail-page.tsx"; +import { renderWorkflowEditorPage } from "../features/workflows/workflow-editor-page.tsx"; +import { renderWorkflowsPage } from "../features/workflows/workflows-page.tsx"; + +export type WebRouteMap = { + "/assets": string; + "/assets/:assetId": string; + "/workflows": string; + "/workflows/:workflowId": string; + "/runs/:runId": string; + "/explore/:artifactId": string; +}; + +export function createRouter(): WebRouteMap { + return { + "/assets": renderAssetsPage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + assets: [], + }), + "/assets/:assetId": renderAssetDetailPage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + asset: { + id: "asset-demo", + displayName: "Demo Asset", + type: "folder", + status: "pending", + sourceType: "upload", + }, + probeReport: { + detectedFormats: ["folder"], + warnings: [], + recommendedNextNodes: ["inspect_asset"], + }, + }), + "/workflows": renderWorkflowsPage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + workflowNames: ["Delivery Normalize"], + }), + "/workflows/:workflowId": renderWorkflowEditorPage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + workflowName: "Delivery Normalize", + selectedNodeId: "rename-folder", + }), + "/runs/:runId": renderRunDetailPage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + run: { + id: "run-demo", + workflowName: "Delivery Normalize", + status: "queued", + }, + tasks: [ + { + id: "task-source", + nodeId: "source-asset", + nodeName: "Source Asset", + status: "success", + logLines: ["Source asset available"], + }, + { + id: "task-validate", + nodeId: "validate-structure", + nodeName: "Validate Structure", + status: "queued", + logLines: ["Waiting for upstream task"], + }, + ], + selectedTaskId: "task-validate", + }), + "/explore/:artifactId": renderExplorePage({ + workspaceName: "Personal Workspace", + projectName: "Demo Project", + artifact: { + id: "artifact-demo", + type: "json", + title: "Probe Report", + payload: { + detectedFormats: ["delivery_package"], + }, + }, + }), + }; +} diff --git a/apps/web/src/features/assets/asset-detail-page.tsx b/apps/web/src/features/assets/asset-detail-page.tsx new file mode 100644 index 0000000..4f3a1f0 --- /dev/null +++ b/apps/web/src/features/assets/asset-detail-page.tsx @@ -0,0 +1,42 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; +import { + renderAssetSummaryPanel, + type ProbeSummary, +} from "./components/asset-summary-panel.tsx"; + +export type AssetDetailInput = { + workspaceName: string; + projectName: string; + asset: { + id: string; + displayName: string; + type: string; + status: string; + sourceType: string; + }; + probeReport: ProbeSummary; +}; + +export function renderAssetDetailPage(input: AssetDetailInput): string { + const content = ` +
+
File Tree
+
Preview Surface
+
+

${input.asset.displayName}

+

Asset ID: ${input.asset.id}

+

Type: ${input.asset.type}

+

Status: ${input.asset.status}

+

Source: ${input.asset.sourceType}

+
+ ${renderAssetSummaryPanel(input.probeReport)} +
+ `; + + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Assets", + content, + }); +} diff --git a/apps/web/src/features/assets/assets-page.test.tsx b/apps/web/src/features/assets/assets-page.test.tsx new file mode 100644 index 0000000..b5e52a2 --- /dev/null +++ b/apps/web/src/features/assets/assets-page.test.tsx @@ -0,0 +1,62 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { renderAppShell } from "../../features/layout/app-shell.tsx"; +import { renderAssetsPage } from "./assets-page.tsx"; +import { renderAssetDetailPage } from "./asset-detail-page.tsx"; + +test("app shell renders primary navigation", () => { + const html = renderAppShell({ + workspaceName: "Personal Workspace", + projectName: "Delivery Demo", + content: "
Assets
", + }); + + assert.match(html, /Assets/); + assert.match(html, /Workflows/); + assert.match(html, /Runs/); + assert.match(html, /Explore/); +}); + +test("assets page renders asset rows from API data", () => { + const html = renderAssetsPage({ + workspaceName: "Team Workspace", + projectName: "Xspark Delivery", + assets: [ + { + id: "asset-1", + displayName: "BJ_001_0001_OsmoNano_2026-03-19_14-51-43", + status: "pending", + sourceType: "upload", + probeSummary: "Detected delivery package structure", + }, + ], + }); + + assert.match(html, /BJ_001_0001_OsmoNano_2026-03-19_14-51-43/); + assert.match(html, /Detected delivery package structure/); + assert.match(html, /Create Workflow/); +}); + +test("asset detail page renders probe summary", () => { + const html = renderAssetDetailPage({ + workspaceName: "Team Workspace", + projectName: "Xspark Delivery", + asset: { + id: "asset-1", + displayName: "BJ_001_0001_OsmoNano_2026-03-19_14-51-43", + type: "folder", + status: "pending", + sourceType: "upload", + }, + probeReport: { + detectedFormats: ["delivery_package"], + warnings: ["intrinsics.json missing"], + recommendedNextNodes: ["validate_structure", "validate_metadata"], + }, + }); + + assert.match(html, /delivery_package/); + assert.match(html, /intrinsics.json missing/); + assert.match(html, /validate_structure/); +}); diff --git a/apps/web/src/features/assets/assets-page.tsx b/apps/web/src/features/assets/assets-page.tsx new file mode 100644 index 0000000..8b0288f --- /dev/null +++ b/apps/web/src/features/assets/assets-page.tsx @@ -0,0 +1,35 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; +import { + renderAssetList, + type AssetListItem, +} from "./components/asset-list.tsx"; + +export type AssetsPageInput = { + workspaceName: string; + projectName: string; + assets: AssetListItem[]; +}; + +export function renderAssetsPage(input: AssetsPageInput): string { + const content = ` +
+
+

Assets

+
+ + + + +
+
+ ${renderAssetList(input.assets)} +
+ `; + + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Assets", + content, + }); +} diff --git a/apps/web/src/features/assets/components/asset-list.tsx b/apps/web/src/features/assets/components/asset-list.tsx new file mode 100644 index 0000000..365539f --- /dev/null +++ b/apps/web/src/features/assets/components/asset-list.tsx @@ -0,0 +1,26 @@ +export type AssetListItem = { + id: string; + displayName: string; + status: string; + sourceType: string; + probeSummary?: string; +}; + +export function renderAssetList(items: AssetListItem[]): string { + const rows = items + .map( + (item) => ` +
+

${item.displayName}

+

Status: ${item.status}

+

Source: ${item.sourceType}

+

${item.probeSummary ?? "Probe pending"}

+ + +
+ `, + ) + .join(""); + + return `
${rows}
`; +} diff --git a/apps/web/src/features/assets/components/asset-summary-panel.tsx b/apps/web/src/features/assets/components/asset-summary-panel.tsx new file mode 100644 index 0000000..e5b7a14 --- /dev/null +++ b/apps/web/src/features/assets/components/asset-summary-panel.tsx @@ -0,0 +1,33 @@ +export type ProbeSummary = { + detectedFormats: string[]; + warnings: string[]; + recommendedNextNodes: string[]; +}; + +export function renderAssetSummaryPanel(summary: ProbeSummary): string { + const detectedFormats = summary.detectedFormats + .map((item) => `
  • ${item}
  • `) + .join(""); + const warnings = summary.warnings.map((item) => `
  • ${item}
  • `).join(""); + const nextNodes = summary.recommendedNextNodes + .map((item) => `
  • ${item}
  • `) + .join(""); + + return ` + + `; +} diff --git a/apps/web/src/features/explore/explore-page.test.tsx b/apps/web/src/features/explore/explore-page.test.tsx new file mode 100644 index 0000000..c7a2f7f --- /dev/null +++ b/apps/web/src/features/explore/explore-page.test.tsx @@ -0,0 +1,61 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { renderExplorePage } from "./explore-page.tsx"; + +test("open and render json artifacts", () => { + const html = renderExplorePage({ + workspaceName: "Team Workspace", + projectName: "Pipeline Project", + artifact: { + id: "artifact-json", + type: "json", + title: "Probe Report", + payload: { + detectedFormats: ["delivery_package"], + }, + }, + }); + + assert.match(html, /Probe Report/); + assert.match(html, /Json Renderer/); + assert.match(html, /delivery_package/); +}); + +test("open and render directory artifacts", () => { + const html = renderExplorePage({ + workspaceName: "Team Workspace", + projectName: "Pipeline Project", + artifact: { + id: "artifact-directory", + type: "directory", + title: "Normalized Folder", + payload: { + entries: ["DJI_001", "meta.json"], + }, + }, + }); + + assert.match(html, /Directory Renderer/); + assert.match(html, /DJI_001/); + assert.match(html, /meta.json/); +}); + +test("open and render video artifacts", () => { + const html = renderExplorePage({ + workspaceName: "Team Workspace", + projectName: "Pipeline Project", + artifact: { + id: "artifact-video", + type: "video", + title: "Preview Clip", + payload: { + path: "DJI_001/DJI_20260318112834_0001_D.mp4", + }, + }, + }); + + assert.match(html, /Video Renderer/); + assert.match(html, /Preview Clip/); + assert.match(html, /DJI_20260318112834_0001_D.mp4/); +}); diff --git a/apps/web/src/features/explore/explore-page.tsx b/apps/web/src/features/explore/explore-page.tsx new file mode 100644 index 0000000..a573e31 --- /dev/null +++ b/apps/web/src/features/explore/explore-page.tsx @@ -0,0 +1,51 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; +import { renderDirectoryRenderer } from "./renderers/directory-renderer.tsx"; +import { renderJsonRenderer } from "./renderers/json-renderer.tsx"; +import { renderVideoRenderer } from "./renderers/video-renderer.tsx"; + +export type ExploreArtifact = { + id: string; + type: "json" | "directory" | "video"; + title: string; + payload: Record; +}; + +export type ExplorePageInput = { + workspaceName: string; + projectName: string; + artifact: ExploreArtifact; +}; + +function renderArtifact(input: ExploreArtifact): string { + if (input.type === "json") { + return renderJsonRenderer(input.payload); + } + if (input.type === "directory") { + return renderDirectoryRenderer({ + entries: Array.isArray(input.payload.entries) + ? input.payload.entries.map((item) => String(item)) + : [], + }); + } + return renderVideoRenderer({ + path: typeof input.payload.path === "string" ? input.payload.path : undefined, + }); +} + +export function renderExplorePage(input: ExplorePageInput): string { + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Explore", + content: ` +
    +
    +

    ${input.artifact.title}

    +

    Artifact ${input.artifact.id}

    +

    Type: ${input.artifact.type}

    +
    + ${renderArtifact(input.artifact)} +
    + `, + }); +} diff --git a/apps/web/src/features/explore/renderers/directory-renderer.tsx b/apps/web/src/features/explore/renderers/directory-renderer.tsx new file mode 100644 index 0000000..8db0e15 --- /dev/null +++ b/apps/web/src/features/explore/renderers/directory-renderer.tsx @@ -0,0 +1,11 @@ +export function renderDirectoryRenderer(payload: { + entries?: string[]; +}): string { + const items = (payload.entries ?? []).map((entry) => `
  • ${entry}
  • `).join(""); + return ` +
    +

    Directory Renderer

    +
      ${items || "
    • No entries
    • "}
    +
    + `; +} diff --git a/apps/web/src/features/explore/renderers/json-renderer.tsx b/apps/web/src/features/explore/renderers/json-renderer.tsx new file mode 100644 index 0000000..fcc922e --- /dev/null +++ b/apps/web/src/features/explore/renderers/json-renderer.tsx @@ -0,0 +1,8 @@ +export function renderJsonRenderer(payload: Record): string { + return ` +
    +

    Json Renderer

    +
    ${JSON.stringify(payload, null, 2)}
    +
    + `; +} diff --git a/apps/web/src/features/explore/renderers/video-renderer.tsx b/apps/web/src/features/explore/renderers/video-renderer.tsx new file mode 100644 index 0000000..ec01135 --- /dev/null +++ b/apps/web/src/features/explore/renderers/video-renderer.tsx @@ -0,0 +1,10 @@ +export function renderVideoRenderer(payload: { + path?: string; +}): string { + return ` +
    +

    Video Renderer

    +

    ${payload.path ?? "No video path"}

    +
    + `; +} diff --git a/apps/web/src/features/layout/app-shell.tsx b/apps/web/src/features/layout/app-shell.tsx new file mode 100644 index 0000000..24eebd6 --- /dev/null +++ b/apps/web/src/features/layout/app-shell.tsx @@ -0,0 +1,43 @@ +import { renderProjectSelector } from "../projects/project-selector.tsx"; +import { renderWorkspaceSwitcher } from "../workspaces/workspace-switcher.tsx"; + +export const PRIMARY_NAV_ITEMS = [ + "Assets", + "Workflows", + "Runs", + "Explore", + "Labels", + "Admin", +] as const; + +export type AppShellInput = { + workspaceName: string; + projectName: string; + content: string; + activeItem?: (typeof PRIMARY_NAV_ITEMS)[number]; +}; + +export function renderAppShell(input: AppShellInput): string { + const navigation = PRIMARY_NAV_ITEMS.map((item) => { + const active = item === input.activeItem ? "data-active=\"true\"" : ""; + return `
  • ${item}
  • `; + }).join(""); + + return ` +
    +
    + ${renderWorkspaceSwitcher({ workspaceName: input.workspaceName })} + ${renderProjectSelector({ projectName: input.projectName })} +
    Search
    +
    Runs
    +
    User
    +
    + +
    ${input.content}
    +
    + `; +} diff --git a/apps/web/src/features/projects/project-selector.tsx b/apps/web/src/features/projects/project-selector.tsx new file mode 100644 index 0000000..6091a15 --- /dev/null +++ b/apps/web/src/features/projects/project-selector.tsx @@ -0,0 +1,7 @@ +export type ProjectSelectorInput = { + projectName: string; +}; + +export function renderProjectSelector(input: ProjectSelectorInput): string { + return `
    ${input.projectName}
    `; +} diff --git a/apps/web/src/features/runs/components/run-graph-view.tsx b/apps/web/src/features/runs/components/run-graph-view.tsx new file mode 100644 index 0000000..e1efc16 --- /dev/null +++ b/apps/web/src/features/runs/components/run-graph-view.tsx @@ -0,0 +1,22 @@ +export type RunTaskView = { + id: string; + nodeId: string; + nodeName: string; + status: string; + logLines: string[]; +}; + +export function renderRunGraphView(tasks: RunTaskView[]): string { + const nodes = tasks + .map( + (task) => ` +
    + ${task.nodeName} + ${task.status} +
    + `, + ) + .join(""); + + return `
    ${nodes}
    `; +} diff --git a/apps/web/src/features/runs/components/task-log-panel.tsx b/apps/web/src/features/runs/components/task-log-panel.tsx new file mode 100644 index 0000000..7e969c8 --- /dev/null +++ b/apps/web/src/features/runs/components/task-log-panel.tsx @@ -0,0 +1,20 @@ +import type { RunTaskView } from "./run-graph-view.tsx"; + +export function renderTaskLogPanel( + tasks: RunTaskView[], + selectedTaskId?: string, +): string { + const task = tasks.find((item) => item.id === selectedTaskId) ?? tasks[0]; + if (!task) { + return ``; + } + + const lines = task.logLines.map((line) => `
  • ${line}
  • `).join(""); + return ` + + `; +} diff --git a/apps/web/src/features/runs/run-detail-page.tsx b/apps/web/src/features/runs/run-detail-page.tsx new file mode 100644 index 0000000..745171f --- /dev/null +++ b/apps/web/src/features/runs/run-detail-page.tsx @@ -0,0 +1,37 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; +import { + renderRunGraphView, + type RunTaskView, +} from "./components/run-graph-view.tsx"; +import { renderTaskLogPanel } from "./components/task-log-panel.tsx"; + +export type RunDetailPageInput = { + workspaceName: string; + projectName: string; + run: { + id: string; + workflowName: string; + status: string; + }; + tasks: RunTaskView[]; + selectedTaskId?: string; +}; + +export function renderRunDetailPage(input: RunDetailPageInput): string { + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Runs", + content: ` +
    +
    +

    ${input.run.workflowName}

    +

    Run ${input.run.id}

    +

    Status: ${input.run.status}

    +
    + ${renderRunGraphView(input.tasks)} + ${renderTaskLogPanel(input.tasks, input.selectedTaskId)} +
    + `, + }); +} diff --git a/apps/web/src/features/workflows/components/node-config-panel.tsx b/apps/web/src/features/workflows/components/node-config-panel.tsx new file mode 100644 index 0000000..13dd5aa --- /dev/null +++ b/apps/web/src/features/workflows/components/node-config-panel.tsx @@ -0,0 +1,34 @@ +import { WORKFLOW_NODE_DEFINITIONS } from "./node-library.tsx"; + +export function renderNodeConfigPanel(selectedNodeId?: string): string { + if (!selectedNodeId) { + return ` + + `; + } + + const node = WORKFLOW_NODE_DEFINITIONS.find((item) => item.id === selectedNodeId); + if (!node) { + return ` + + `; + } + + return ` + + `; +} diff --git a/apps/web/src/features/workflows/components/node-library.tsx b/apps/web/src/features/workflows/components/node-library.tsx new file mode 100644 index 0000000..f6e45e9 --- /dev/null +++ b/apps/web/src/features/workflows/components/node-library.tsx @@ -0,0 +1,76 @@ +export type WorkflowNodeDefinition = { + id: string; + name: string; + category: "Source" | "Transform" | "Inspect" | "Annotate" | "Export" | "Utility"; + description: string; + executorType: "python" | "docker" | "http"; + inputSchemaSummary: string; + outputSchemaSummary: string; + supportsCodeHook?: boolean; +}; + +export const WORKFLOW_NODE_DEFINITIONS: WorkflowNodeDefinition[] = [ + { + id: "source-asset", + name: "Source Asset", + category: "Source", + description: "Load an uploaded asset or registered storage path.", + executorType: "python", + inputSchemaSummary: "assetRef", + outputSchemaSummary: "assetRef", + }, + { + id: "extract-archive", + name: "Extract Archive", + category: "Transform", + description: "Expand a compressed archive into a managed folder artifact.", + executorType: "docker", + inputSchemaSummary: "assetRef", + outputSchemaSummary: "artifactRef", + supportsCodeHook: true, + }, + { + id: "rename-folder", + name: "Rename Delivery Folder", + category: "Transform", + description: "Rename the top-level delivery folder to the business naming convention.", + executorType: "python", + inputSchemaSummary: "artifactRef", + outputSchemaSummary: "artifactRef", + supportsCodeHook: true, + }, + { + id: "validate-structure", + name: "Validate Structure", + category: "Inspect", + description: "Validate required directories and metadata files.", + executorType: "python", + inputSchemaSummary: "artifactRef", + outputSchemaSummary: "artifactRef + report", + supportsCodeHook: true, + }, + { + id: "export-delivery-package", + name: "Export Delivery Package", + category: "Export", + description: "Produce the final delivery package artifact for upload.", + executorType: "http", + inputSchemaSummary: "artifactRef", + outputSchemaSummary: "artifactRef", + }, +]; + +export function renderNodeLibrary(): string { + const sections = ["Source", "Transform", "Inspect", "Annotate", "Export", "Utility"] + .map((category) => { + const nodes = WORKFLOW_NODE_DEFINITIONS.filter( + (item) => item.category === category, + ) + .map((item) => `
  • ${item.name}
  • `) + .join(""); + return `

    ${category}

      ${nodes || "
    • none
    • "}
    `; + }) + .join(""); + + return ``; +} diff --git a/apps/web/src/features/workflows/components/workflow-canvas.tsx b/apps/web/src/features/workflows/components/workflow-canvas.tsx new file mode 100644 index 0000000..eeb02a5 --- /dev/null +++ b/apps/web/src/features/workflows/components/workflow-canvas.tsx @@ -0,0 +1,30 @@ +import { WORKFLOW_NODE_DEFINITIONS } from "./node-library.tsx"; + +export type WorkflowCanvasInput = { + selectedNodeId?: string; +}; + +export function renderWorkflowCanvas(input: WorkflowCanvasInput): string { + const defaultNodes = ["source-asset", "rename-folder", "validate-structure"]; + const nodes = defaultNodes + .map((nodeId) => { + const node = WORKFLOW_NODE_DEFINITIONS.find((item) => item.id === nodeId); + if (!node) { + return ""; + } + const selected = node.id === input.selectedNodeId ? "data-selected=\"true\"" : ""; + return `
    ${node.name}
    `; + }) + .join(""); + + return ` +
    +
    + + +
    +
    ${nodes}
    +
    Zoom | Pan | Mini-map
    +
    + `; +} diff --git a/apps/web/src/features/workflows/workflow-editor-page.test.tsx b/apps/web/src/features/workflows/workflow-editor-page.test.tsx new file mode 100644 index 0000000..4aa439d --- /dev/null +++ b/apps/web/src/features/workflows/workflow-editor-page.test.tsx @@ -0,0 +1,63 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { renderNodeLibrary } from "./components/node-library.tsx"; +import { renderWorkflowEditorPage } from "./workflow-editor-page.tsx"; +import { renderRunDetailPage } from "../runs/run-detail-page.tsx"; + +test("node library renders categories", () => { + const html = renderNodeLibrary(); + + assert.match(html, /Source/); + assert.match(html, /Transform/); + assert.match(html, /Inspect/); + assert.match(html, /Export/); +}); + +test("node config panel opens when a node is selected", () => { + const html = renderWorkflowEditorPage({ + workspaceName: "Team Workspace", + projectName: "Pipeline Project", + workflowName: "Delivery Normalize", + selectedNodeId: "rename-folder", + }); + + assert.match(html, /Node Configuration/); + assert.match(html, /Rename Delivery Folder/); + assert.match(html, /Executor/); +}); + +test("run detail view shows node status badges from run data", () => { + const html = renderRunDetailPage({ + workspaceName: "Team Workspace", + projectName: "Pipeline Project", + run: { + id: "run-1", + workflowName: "Delivery Normalize", + status: "running", + }, + tasks: [ + { + id: "task-1", + nodeId: "source-asset", + nodeName: "Source Asset", + status: "success", + logLines: ["Asset loaded"], + }, + { + id: "task-2", + nodeId: "validate-structure", + nodeName: "Validate Structure", + status: "running", + logLines: ["Checking metadata"], + }, + ], + selectedTaskId: "task-2", + }); + + assert.match(html, /Source Asset/); + assert.match(html, /success/); + assert.match(html, /Validate Structure/); + assert.match(html, /running/); + assert.match(html, /Checking metadata/); +}); diff --git a/apps/web/src/features/workflows/workflow-editor-page.tsx b/apps/web/src/features/workflows/workflow-editor-page.tsx new file mode 100644 index 0000000..230b2aa --- /dev/null +++ b/apps/web/src/features/workflows/workflow-editor-page.tsx @@ -0,0 +1,31 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; +import { renderNodeConfigPanel } from "./components/node-config-panel.tsx"; +import { renderNodeLibrary } from "./components/node-library.tsx"; +import { renderWorkflowCanvas } from "./components/workflow-canvas.tsx"; + +export type WorkflowEditorPageInput = { + workspaceName: string; + projectName: string; + workflowName: string; + selectedNodeId?: string; +}; + +export function renderWorkflowEditorPage( + input: WorkflowEditorPageInput, +): string { + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Workflows", + content: ` +
    +

    ${input.workflowName}

    +
    + ${renderNodeLibrary()} + ${renderWorkflowCanvas({ selectedNodeId: input.selectedNodeId })} + ${renderNodeConfigPanel(input.selectedNodeId)} +
    +
    + `, + }); +} diff --git a/apps/web/src/features/workflows/workflows-page.tsx b/apps/web/src/features/workflows/workflows-page.tsx new file mode 100644 index 0000000..e494dab --- /dev/null +++ b/apps/web/src/features/workflows/workflows-page.tsx @@ -0,0 +1,25 @@ +import { renderAppShell } from "../layout/app-shell.tsx"; + +export type WorkflowsPageInput = { + workspaceName: string; + projectName: string; + workflowNames: string[]; +}; + +export function renderWorkflowsPage(input: WorkflowsPageInput): string { + const items = input.workflowNames.map((name) => `
  • ${name}
  • `).join(""); + return renderAppShell({ + workspaceName: input.workspaceName, + projectName: input.projectName, + activeItem: "Workflows", + content: ` +
    +
    +

    Workflows

    + +
    +
      ${items || "
    • No workflows yet
    • "}
    +
    + `, + }); +} diff --git a/apps/web/src/features/workspaces/workspace-switcher.tsx b/apps/web/src/features/workspaces/workspace-switcher.tsx new file mode 100644 index 0000000..68e50fb --- /dev/null +++ b/apps/web/src/features/workspaces/workspace-switcher.tsx @@ -0,0 +1,9 @@ +export type WorkspaceSwitcherInput = { + workspaceName: string; +}; + +export function renderWorkspaceSwitcher( + input: WorkspaceSwitcherInput, +): string { + return `
    ${input.workspaceName}
    `; +} diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..4c75c67 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,12 @@ +import { createRouter } from "./app/router.tsx"; + +export function createWebApp() { + return { + status: "ready" as const, + routes: createRouter(), + }; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + process.stdout.write(JSON.stringify({ status: createWebApp().status }, null, 2)); +} diff --git a/apps/worker/package.json b/apps/worker/package.json index b0eab84..a824255 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -4,7 +4,7 @@ "version": "0.1.0", "type": "module", "scripts": { - "dev": "echo 'worker app scaffold pending'", + "dev": "tsx watch src/main.ts", "test": "node --test --experimental-strip-types" } } diff --git a/apps/worker/src/main.ts b/apps/worker/src/main.ts index e67a376..8205aa3 100644 --- a/apps/worker/src/main.ts +++ b/apps/worker/src/main.ts @@ -3,3 +3,7 @@ export function bootstrapWorker() { status: "ready" as const, }; } + +if (import.meta.url === `file://${process.argv[1]}`) { + process.stdout.write(JSON.stringify(bootstrapWorker(), null, 2)); +} diff --git a/design/03-workflows/workflow-execution-model.md b/design/03-workflows/workflow-execution-model.md index f92850c..04b72ab 100644 --- a/design/03-workflows/workflow-execution-model.md +++ b/design/03-workflows/workflow-execution-model.md @@ -259,6 +259,28 @@ Each task must emit: - structured task summary - artifact refs +## Current V1 Implementation Notes + +The current foundation implementation keeps the control plane in memory while stabilizing contracts for: + +- workspace and project bootstrap +- asset registration and probe reporting +- workflow definition and immutable version snapshots +- workflow runs and task creation +- artifact registration and producer lookup + +The first web authoring surface already follows the three-pane layout contract with: + +- left node library +- center workflow canvas +- right node configuration panel + +The first explore surface currently includes built-in renderers for: + +- JSON artifacts +- directory artifacts +- video artifacts + The UI must allow: - graph-level run status diff --git a/design/05-data/mongodb-data-model.md b/design/05-data/mongodb-data-model.md index 1c05940..f858b93 100644 --- a/design/05-data/mongodb-data-model.md +++ b/design/05-data/mongodb-data-model.md @@ -22,6 +22,19 @@ The database must support: - Large, fast-growing arrays should be split into separate collections - Platform contracts should use references, not embedded file blobs +## Current V1 Implementation Notes + +The first code pass stabilizes these collection boundaries with in-memory services before full MongoDB persistence is wired through every module. + +This means the implementation currently validates: + +- document shapes +- controller and service boundaries +- workflow/run/task separation +- artifact lookup by producer + +while still targeting the collection model below as the persistent shape. + ## Primary Collections - `users` diff --git a/docs/development-workflow.md b/docs/development-workflow.md index e51f75f..5a8709a 100644 --- a/docs/development-workflow.md +++ b/docs/development-workflow.md @@ -44,7 +44,7 @@ Do not defer the design update. Treat design edits as part of the implementation From the repo root: ```bash -python3 scripts/check_doc_code_sync.py . --strict +make guardrails ``` Interpret warnings manually. The script is a guardrail, not a replacement for judgment. @@ -54,7 +54,7 @@ Interpret warnings manually. The script is a guardrail, not a replacement for ju Install local hooks once per clone: ```bash -bash scripts/install_hooks.sh +make bootstrap ``` This enables: @@ -63,6 +63,19 @@ This enables: - `pre-commit`: block staged code/config drift without doc updates - `pre-push`: run commit-message validation, doc/code sync checks, and repository tests +### 5.1 Preferred Local Commands + +Use the repo entry commands instead of ad hoc command strings whenever possible: + +```bash +make bootstrap +make test +make dev-api +make dev-web +make dev-worker +make guardrails +``` + ### 6. Close With Explicit Status Every implementation summary should state one of: diff --git a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md index d8f768c..47ea661 100644 --- a/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md +++ b/docs/plans/2026-03-26-emboflow-v1-foundation-and-mvp.md @@ -15,6 +15,7 @@ - `2026-03-26`: Tasks 1 and 2 are complete and committed. - `2026-03-26`: Tasks 3 through 6 are implemented against in-memory V1 control-plane services so the API and worker contracts can stabilize before persistence and framework wiring are deepened. - `2026-03-26`: Package-level verification continues to use the Node 22 built-in test runner with direct file targets such as `pnpm --filter api test test/projects.e2e-spec.ts` and `pnpm --filter worker test test/task-runner.spec.ts`. +- `2026-03-26`: Tasks 7 through 10 add the first web shell, workflow editor surfaces, artifact explore renderers, developer entry commands, and CI/pre-push test execution through `make test`. --- diff --git a/package.json b/package.json index 41a3dd1..f3a240f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,11 @@ "version": "0.1.0", "packageManager": "pnpm@9.12.3", "scripts": { - "test": "python3 -m unittest discover -s tests -p 'test_*.py'" + "bootstrap": "make bootstrap", + "guardrails": "make guardrails", + "test": "make test" + }, + "devDependencies": { + "tsx": "^4.21.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25c73ae..e015884 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,11 @@ settings: importers: - .: {} + .: + devDependencies: + tsx: + specifier: ^4.21.0 + version: 4.21.0 apps/api: {} @@ -15,3 +19,307 @@ importers: apps/worker: {} packages/contracts: {} + +packages: + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + resolve-pkg-maps@1.0.0: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 diff --git a/tests/test_dev_commands.py b/tests/test_dev_commands.py new file mode 100644 index 0000000..8d93edd --- /dev/null +++ b/tests/test_dev_commands.py @@ -0,0 +1,39 @@ +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] + + +class DevCommandDocsTest(unittest.TestCase): + def test_makefile_exposes_expected_local_commands(self): + makefile = (REPO_ROOT / "Makefile").read_text(encoding="utf-8") + + for target in ( + "bootstrap:", + "test:", + "dev-api:", + "dev-web:", + "dev-worker:", + "guardrails:", + ): + with self.subTest(target=target): + self.assertIn(target, makefile) + + def test_readme_documents_bootstrap_hooks_test_and_local_run(self): + readme = (REPO_ROOT / "README.md").read_text(encoding="utf-8") + + for phrase in ( + "make bootstrap", + "scripts/install_hooks.sh", + "make test", + "make dev-api", + "make dev-web", + "make dev-worker", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, readme) + + +if __name__ == "__main__": + unittest.main()