✨ 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
|
||||
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:
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
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",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "echo 'api app scaffold pending'",
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"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,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"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",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "echo 'worker app scaffold pending'",
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"test": "node --test --experimental-strip-types"
|
||||
}
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
310
pnpm-lock.yaml
generated
310
pnpm-lock.yaml
generated
@ -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
|
||||
|
||||
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