✨ feat: add web surfaces artifacts and developer commands
This commit is contained in:
parent
668602de22
commit
f81eda80bb
@ -16,4 +16,4 @@ else
|
|||||||
python3 scripts/check_commit_message.py --rev-range HEAD
|
python3 scripts/check_commit_message.py --rev-range HEAD
|
||||||
fi
|
fi
|
||||||
|
|
||||||
python3 -m unittest discover -s tests -p 'test_*.py'
|
make test
|
||||||
|
|||||||
15
.github/workflows/guardrails.yml
vendored
15
.github/workflows/guardrails.yml
vendored
@ -19,6 +19,19 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: "3.11"
|
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
|
- name: Compute git range
|
||||||
id: git_range
|
id: git_range
|
||||||
shell: bash
|
shell: bash
|
||||||
@ -42,4 +55,4 @@ jobs:
|
|||||||
|
|
||||||
- name: Run repository tests
|
- name: Run repository tests
|
||||||
run: |
|
run: |
|
||||||
python3 -m unittest discover -s tests -p 'test_*.py'
|
make test
|
||||||
|
|||||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
@ -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:
|
5. Install the local git hooks once per clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install_hooks.sh
|
make bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Use English-only commit messages with a gitmoji prefix, for example:
|
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:
|
7. Run the local sync check when needed:
|
||||||
|
|
||||||
```bash
|
```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.
|
8. If design and code still diverge, document that explicitly in your final summary.
|
||||||
@ -71,13 +71,13 @@ This repository includes:
|
|||||||
- a hook installer:
|
- a hook installer:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install_hooks.sh
|
make bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
- a design/code sync checker:
|
- a design/code sync checker:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/check_doc_code_sync.py . --strict
|
make guardrails
|
||||||
```
|
```
|
||||||
|
|
||||||
- a commit message validator:
|
- 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
|
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:
|
The hooks and CI enforce:
|
||||||
|
|
||||||
- English-only commit messages with a gitmoji prefix
|
- English-only commit messages with a gitmoji prefix
|
||||||
|
|||||||
26
Makefile
Normal file
26
Makefile
Normal file
@ -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
|
||||||
56
README.md
Normal file
56
README.md
Normal file
@ -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
|
||||||
|
```
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "echo 'api app scaffold pending'",
|
"dev": "tsx watch src/main.ts",
|
||||||
"test": "node --test --experimental-strip-types"
|
"test": "node --test --experimental-strip-types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
apps/api/src/main.ts
Normal file
35
apps/api/src/main.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
20
apps/api/src/modules/artifacts/artifacts.controller.ts
Normal file
20
apps/api/src/modules/artifacts/artifacts.controller.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/api/src/modules/artifacts/artifacts.module.ts
Normal file
12
apps/api/src/modules/artifacts/artifacts.module.ts
Normal file
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
28
apps/api/src/modules/artifacts/artifacts.service.ts
Normal file
28
apps/api/src/modules/artifacts/artifacts.service.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
export type ArtifactRecord = {
|
||||||
|
id: string;
|
||||||
|
type: "json" | "directory" | "video";
|
||||||
|
title: string;
|
||||||
|
producerType: string;
|
||||||
|
producerId: string;
|
||||||
|
payload: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
apps/api/src/modules/plugins/builtin/delivery-nodes.ts
Normal file
38
apps/api/src/modules/plugins/builtin/delivery-nodes.ts
Normal file
@ -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;
|
||||||
35
apps/api/test/artifacts.e2e-spec.ts
Normal file
35
apps/api/test/artifacts.e2e-spec.ts
Normal file
@ -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");
|
||||||
|
});
|
||||||
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "@emboflow/web",
|
"name": "web",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "echo 'web app scaffold pending'"
|
"dev": "tsx watch src/main.tsx",
|
||||||
|
"test": "tsx --test"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
apps/web/src/app/router.tsx
Normal file
90
apps/web/src/app/router.tsx
Normal file
@ -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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
42
apps/web/src/features/assets/asset-detail-page.tsx
Normal file
42
apps/web/src/features/assets/asset-detail-page.tsx
Normal file
@ -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 = `
|
||||||
|
<section data-view="asset-detail-page">
|
||||||
|
<div data-slot="file-tree">File Tree</div>
|
||||||
|
<div data-slot="preview-surface">Preview Surface</div>
|
||||||
|
<section data-slot="asset-metadata">
|
||||||
|
<h1>${input.asset.displayName}</h1>
|
||||||
|
<p>Asset ID: ${input.asset.id}</p>
|
||||||
|
<p>Type: ${input.asset.type}</p>
|
||||||
|
<p>Status: ${input.asset.status}</p>
|
||||||
|
<p>Source: ${input.asset.sourceType}</p>
|
||||||
|
</section>
|
||||||
|
${renderAssetSummaryPanel(input.probeReport)}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return renderAppShell({
|
||||||
|
workspaceName: input.workspaceName,
|
||||||
|
projectName: input.projectName,
|
||||||
|
activeItem: "Assets",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
62
apps/web/src/features/assets/assets-page.test.tsx
Normal file
62
apps/web/src/features/assets/assets-page.test.tsx
Normal file
@ -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: "<section>Assets</section>",
|
||||||
|
});
|
||||||
|
|
||||||
|
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/);
|
||||||
|
});
|
||||||
35
apps/web/src/features/assets/assets-page.tsx
Normal file
35
apps/web/src/features/assets/assets-page.tsx
Normal file
@ -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 = `
|
||||||
|
<section data-view="assets-page">
|
||||||
|
<header>
|
||||||
|
<h1>Assets</h1>
|
||||||
|
<div data-slot="import-actions">
|
||||||
|
<button>Upload File</button>
|
||||||
|
<button>Upload Archive</button>
|
||||||
|
<button>Import Object Storage Prefix</button>
|
||||||
|
<button>Register Storage Path</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
${renderAssetList(input.assets)}
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return renderAppShell({
|
||||||
|
workspaceName: input.workspaceName,
|
||||||
|
projectName: input.projectName,
|
||||||
|
activeItem: "Assets",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
||||||
26
apps/web/src/features/assets/components/asset-list.tsx
Normal file
26
apps/web/src/features/assets/components/asset-list.tsx
Normal file
@ -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) => `
|
||||||
|
<article data-asset-id="${item.id}">
|
||||||
|
<h3>${item.displayName}</h3>
|
||||||
|
<p>Status: ${item.status}</p>
|
||||||
|
<p>Source: ${item.sourceType}</p>
|
||||||
|
<p>${item.probeSummary ?? "Probe pending"}</p>
|
||||||
|
<button>Create Workflow</button>
|
||||||
|
<button>Open Preview</button>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<section data-view="asset-list">${rows}</section>`;
|
||||||
|
}
|
||||||
@ -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) => `<li>${item}</li>`)
|
||||||
|
.join("");
|
||||||
|
const warnings = summary.warnings.map((item) => `<li>${item}</li>`).join("");
|
||||||
|
const nextNodes = summary.recommendedNextNodes
|
||||||
|
.map((item) => `<li>${item}</li>`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<aside data-view="asset-summary-panel">
|
||||||
|
<h2>Probe Summary</h2>
|
||||||
|
<section>
|
||||||
|
<h3>Detected Formats</h3>
|
||||||
|
<ul>${detectedFormats}</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Warnings</h3>
|
||||||
|
<ul>${warnings || "<li>none</li>"}</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h3>Recommended Next Nodes</h3>
|
||||||
|
<ul>${nextNodes || "<li>inspect_asset</li>"}</ul>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
61
apps/web/src/features/explore/explore-page.test.tsx
Normal file
61
apps/web/src/features/explore/explore-page.test.tsx
Normal file
@ -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/);
|
||||||
|
});
|
||||||
51
apps/web/src/features/explore/explore-page.tsx
Normal file
51
apps/web/src/features/explore/explore-page.tsx
Normal file
@ -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<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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: `
|
||||||
|
<section data-view="explore-page">
|
||||||
|
<header>
|
||||||
|
<h1>${input.artifact.title}</h1>
|
||||||
|
<p>Artifact ${input.artifact.id}</p>
|
||||||
|
<p>Type: ${input.artifact.type}</p>
|
||||||
|
</header>
|
||||||
|
${renderArtifact(input.artifact)}
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
export function renderDirectoryRenderer(payload: {
|
||||||
|
entries?: string[];
|
||||||
|
}): string {
|
||||||
|
const items = (payload.entries ?? []).map((entry) => `<li>${entry}</li>`).join("");
|
||||||
|
return `
|
||||||
|
<section data-view="directory-renderer">
|
||||||
|
<h2>Directory Renderer</h2>
|
||||||
|
<ul>${items || "<li>No entries</li>"}</ul>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export function renderJsonRenderer(payload: Record<string, unknown>): string {
|
||||||
|
return `
|
||||||
|
<section data-view="json-renderer">
|
||||||
|
<h2>Json Renderer</h2>
|
||||||
|
<pre>${JSON.stringify(payload, null, 2)}</pre>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
10
apps/web/src/features/explore/renderers/video-renderer.tsx
Normal file
10
apps/web/src/features/explore/renderers/video-renderer.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export function renderVideoRenderer(payload: {
|
||||||
|
path?: string;
|
||||||
|
}): string {
|
||||||
|
return `
|
||||||
|
<section data-view="video-renderer">
|
||||||
|
<h2>Video Renderer</h2>
|
||||||
|
<p>${payload.path ?? "No video path"}</p>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
43
apps/web/src/features/layout/app-shell.tsx
Normal file
43
apps/web/src/features/layout/app-shell.tsx
Normal file
@ -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 `<li ${active}>${item}</li>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div data-view="app-shell">
|
||||||
|
<header>
|
||||||
|
${renderWorkspaceSwitcher({ workspaceName: input.workspaceName })}
|
||||||
|
${renderProjectSelector({ projectName: input.projectName })}
|
||||||
|
<div data-slot="global-search">Search</div>
|
||||||
|
<div data-slot="run-notifications">Runs</div>
|
||||||
|
<div data-slot="user-menu">User</div>
|
||||||
|
</header>
|
||||||
|
<aside>
|
||||||
|
<nav>
|
||||||
|
<ul>${navigation}</ul>
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
<main>${input.content}</main>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
7
apps/web/src/features/projects/project-selector.tsx
Normal file
7
apps/web/src/features/projects/project-selector.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type ProjectSelectorInput = {
|
||||||
|
projectName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderProjectSelector(input: ProjectSelectorInput): string {
|
||||||
|
return `<div data-slot="project-selector"><label>Project</label><strong>${input.projectName}</strong></div>`;
|
||||||
|
}
|
||||||
22
apps/web/src/features/runs/components/run-graph-view.tsx
Normal file
22
apps/web/src/features/runs/components/run-graph-view.tsx
Normal file
@ -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) => `
|
||||||
|
<article data-task-id="${task.id}">
|
||||||
|
<strong>${task.nodeName}</strong>
|
||||||
|
<span data-status="${task.status}">${task.status}</span>
|
||||||
|
</article>
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<section data-view="run-graph-view">${nodes}</section>`;
|
||||||
|
}
|
||||||
20
apps/web/src/features/runs/components/task-log-panel.tsx
Normal file
20
apps/web/src/features/runs/components/task-log-panel.tsx
Normal file
@ -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 `<aside data-view="task-log-panel"><p>No task selected.</p></aside>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = task.logLines.map((line) => `<li>${line}</li>`).join("");
|
||||||
|
return `
|
||||||
|
<aside data-view="task-log-panel">
|
||||||
|
<h2>${task.nodeName}</h2>
|
||||||
|
<p>Status: ${task.status}</p>
|
||||||
|
<ul>${lines || "<li>No logs</li>"}</ul>
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
37
apps/web/src/features/runs/run-detail-page.tsx
Normal file
37
apps/web/src/features/runs/run-detail-page.tsx
Normal file
@ -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: `
|
||||||
|
<section data-view="run-detail-page">
|
||||||
|
<header>
|
||||||
|
<h1>${input.run.workflowName}</h1>
|
||||||
|
<p>Run ${input.run.id}</p>
|
||||||
|
<p>Status: ${input.run.status}</p>
|
||||||
|
</header>
|
||||||
|
${renderRunGraphView(input.tasks)}
|
||||||
|
${renderTaskLogPanel(input.tasks, input.selectedTaskId)}
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { WORKFLOW_NODE_DEFINITIONS } from "./node-library.tsx";
|
||||||
|
|
||||||
|
export function renderNodeConfigPanel(selectedNodeId?: string): string {
|
||||||
|
if (!selectedNodeId) {
|
||||||
|
return `
|
||||||
|
<aside data-view="node-config-panel">
|
||||||
|
<h2>Node Configuration</h2>
|
||||||
|
<p>Select a node to inspect config, schemas, executor, and runtime policy.</p>
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = WORKFLOW_NODE_DEFINITIONS.find((item) => item.id === selectedNodeId);
|
||||||
|
if (!node) {
|
||||||
|
return `
|
||||||
|
<aside data-view="node-config-panel">
|
||||||
|
<h2>Node Configuration</h2>
|
||||||
|
<p>Unknown node: ${selectedNodeId}</p>
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<aside data-view="node-config-panel">
|
||||||
|
<h2>Node Configuration</h2>
|
||||||
|
<h3>${node.name}</h3>
|
||||||
|
<p>${node.description}</p>
|
||||||
|
<p>Executor: ${node.executorType}</p>
|
||||||
|
<p>Input Schema: ${node.inputSchemaSummary}</p>
|
||||||
|
<p>Output Schema: ${node.outputSchemaSummary}</p>
|
||||||
|
<p>Code Hook: ${node.supportsCodeHook ? "enabled" : "not available"}</p>
|
||||||
|
</aside>
|
||||||
|
`;
|
||||||
|
}
|
||||||
76
apps/web/src/features/workflows/components/node-library.tsx
Normal file
76
apps/web/src/features/workflows/components/node-library.tsx
Normal file
@ -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) => `<li data-node-id="${item.id}">${item.name}</li>`)
|
||||||
|
.join("");
|
||||||
|
return `<section><h3>${category}</h3><ul>${nodes || "<li>none</li>"}</ul></section>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<aside data-view="node-library"><h2>Node Library</h2>${sections}</aside>`;
|
||||||
|
}
|
||||||
@ -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 `<div data-canvas-node="${node.id}" ${selected}>${node.name}</div>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `
|
||||||
|
<section data-view="workflow-canvas">
|
||||||
|
<header>
|
||||||
|
<button>Save Workflow Version</button>
|
||||||
|
<button>Trigger Workflow Run</button>
|
||||||
|
</header>
|
||||||
|
<div data-slot="canvas-surface">${nodes}</div>
|
||||||
|
<div data-slot="canvas-controls">Zoom | Pan | Mini-map</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@ -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/);
|
||||||
|
});
|
||||||
31
apps/web/src/features/workflows/workflow-editor-page.tsx
Normal file
31
apps/web/src/features/workflows/workflow-editor-page.tsx
Normal file
@ -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: `
|
||||||
|
<section data-view="workflow-editor-page">
|
||||||
|
<header><h1>${input.workflowName}</h1></header>
|
||||||
|
<div data-layout="editor">
|
||||||
|
${renderNodeLibrary()}
|
||||||
|
${renderWorkflowCanvas({ selectedNodeId: input.selectedNodeId })}
|
||||||
|
${renderNodeConfigPanel(input.selectedNodeId)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
25
apps/web/src/features/workflows/workflows-page.tsx
Normal file
25
apps/web/src/features/workflows/workflows-page.tsx
Normal file
@ -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) => `<li>${name}</li>`).join("");
|
||||||
|
return renderAppShell({
|
||||||
|
workspaceName: input.workspaceName,
|
||||||
|
projectName: input.projectName,
|
||||||
|
activeItem: "Workflows",
|
||||||
|
content: `
|
||||||
|
<section data-view="workflows-page">
|
||||||
|
<header>
|
||||||
|
<h1>Workflows</h1>
|
||||||
|
<button>Create Workflow</button>
|
||||||
|
</header>
|
||||||
|
<ul>${items || "<li>No workflows yet</li>"}</ul>
|
||||||
|
</section>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
}
|
||||||
9
apps/web/src/features/workspaces/workspace-switcher.tsx
Normal file
9
apps/web/src/features/workspaces/workspace-switcher.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export type WorkspaceSwitcherInput = {
|
||||||
|
workspaceName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function renderWorkspaceSwitcher(
|
||||||
|
input: WorkspaceSwitcherInput,
|
||||||
|
): string {
|
||||||
|
return `<div data-slot="workspace-switcher"><label>Workspace</label><strong>${input.workspaceName}</strong></div>`;
|
||||||
|
}
|
||||||
12
apps/web/src/main.tsx
Normal file
12
apps/web/src/main.tsx
Normal file
@ -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));
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "echo 'worker app scaffold pending'",
|
"dev": "tsx watch src/main.ts",
|
||||||
"test": "node --test --experimental-strip-types"
|
"test": "node --test --experimental-strip-types"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,3 +3,7 @@ export function bootstrapWorker() {
|
|||||||
status: "ready" as const,
|
status: "ready" as const,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
process.stdout.write(JSON.stringify(bootstrapWorker(), null, 2));
|
||||||
|
}
|
||||||
|
|||||||
@ -259,6 +259,28 @@ Each task must emit:
|
|||||||
- structured task summary
|
- structured task summary
|
||||||
- artifact refs
|
- 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:
|
The UI must allow:
|
||||||
|
|
||||||
- graph-level run status
|
- graph-level run status
|
||||||
|
|||||||
@ -22,6 +22,19 @@ The database must support:
|
|||||||
- Large, fast-growing arrays should be split into separate collections
|
- Large, fast-growing arrays should be split into separate collections
|
||||||
- Platform contracts should use references, not embedded file blobs
|
- 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
|
## Primary Collections
|
||||||
|
|
||||||
- `users`
|
- `users`
|
||||||
|
|||||||
@ -44,7 +44,7 @@ Do not defer the design update. Treat design edits as part of the implementation
|
|||||||
From the repo root:
|
From the repo root:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 scripts/check_doc_code_sync.py . --strict
|
make guardrails
|
||||||
```
|
```
|
||||||
|
|
||||||
Interpret warnings manually. The script is a guardrail, not a replacement for judgment.
|
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:
|
Install local hooks once per clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash scripts/install_hooks.sh
|
make bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
This enables:
|
This enables:
|
||||||
@ -63,6 +63,19 @@ This enables:
|
|||||||
- `pre-commit`: block staged code/config drift without doc updates
|
- `pre-commit`: block staged code/config drift without doc updates
|
||||||
- `pre-push`: run commit-message validation, doc/code sync checks, and repository tests
|
- `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
|
### 6. Close With Explicit Status
|
||||||
|
|
||||||
Every implementation summary should state one of:
|
Every implementation summary should state one of:
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
- `2026-03-26`: Tasks 1 and 2 are complete and committed.
|
- `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`: 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`: 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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,11 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"packageManager": "pnpm@9.12.3",
|
"packageManager": "pnpm@9.12.3",
|
||||||
"scripts": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
310
pnpm-lock.yaml
generated
310
pnpm-lock.yaml
generated
@ -6,7 +6,11 @@ settings:
|
|||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.: {}
|
.:
|
||||||
|
devDependencies:
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.21.0
|
||||||
|
version: 4.21.0
|
||||||
|
|
||||||
apps/api: {}
|
apps/api: {}
|
||||||
|
|
||||||
@ -15,3 +19,307 @@ importers:
|
|||||||
apps/worker: {}
|
apps/worker: {}
|
||||||
|
|
||||||
packages/contracts: {}
|
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
|
||||||
|
|||||||
39
tests/test_dev_commands.py
Normal file
39
tests/test_dev_commands.py
Normal file
@ -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()
|
||||||
Loading…
x
Reference in New Issue
Block a user