feat: complete TASK-WP0-01 toolchain skeleton
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear />
|
||||||
|
</packageSources>
|
||||||
|
<fallbackPackageFolders>
|
||||||
|
<clear />
|
||||||
|
</fallbackPackageFolders>
|
||||||
|
</configuration>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "@dada/api",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@fastify/swagger": "9.8.1",
|
||||||
|
"@sinclair/typebox": "0.34.52",
|
||||||
|
"better-sqlite3": "13.0.1",
|
||||||
|
"drizzle-orm": "0.45.2",
|
||||||
|
"fastify": "5.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "7.6.13",
|
||||||
|
"@types/node": "24.13.3",
|
||||||
|
"typescript": "7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import swagger from "@fastify/swagger";
|
||||||
|
import Fastify from "fastify";
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = Fastify({ logger: false });
|
||||||
|
|
||||||
|
void app.register(swagger, {
|
||||||
|
openapi: {
|
||||||
|
info: {
|
||||||
|
title: "Dada P0-A",
|
||||||
|
version: "0.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { createApp } from "./app.js";
|
||||||
|
|
||||||
|
const app = createApp();
|
||||||
|
|
||||||
|
await app.listen({
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 43121,
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2024"],
|
||||||
|
"types": ["node"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dada</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "@dada/web",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vibrant/core": "4.0.4",
|
||||||
|
"@vibrant/quantizer-mmcq": "4.0.4",
|
||||||
|
"fabric": "7.4.0",
|
||||||
|
"react": "19.2.8",
|
||||||
|
"react-dom": "19.2.8"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "19.2.17",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"@vitejs/plugin-react": "6.0.4",
|
||||||
|
"typescript": "7.0.2",
|
||||||
|
"vite": "8.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
import { ToolchainProbe } from "./toolchain-probe.js";
|
||||||
|
|
||||||
|
const root = document.getElementById("root");
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
throw new Error("Dada web root element is missing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
createRoot(root).render(
|
||||||
|
<StrictMode>
|
||||||
|
<ToolchainProbe />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Rect, version as fabricVersion } from "fabric";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
|
export function ToolchainProbe() {
|
||||||
|
return <canvas aria-label="Dada toolchain probe" height={16} width={16} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderToolchainProbe() {
|
||||||
|
const reactMarkup = renderToStaticMarkup(<ToolchainProbe />);
|
||||||
|
const fabricSvg = new Rect({
|
||||||
|
fill: "#111111",
|
||||||
|
height: 16,
|
||||||
|
width: 16,
|
||||||
|
}).toSVG();
|
||||||
|
|
||||||
|
return {
|
||||||
|
fabricSvg,
|
||||||
|
fabricVersion,
|
||||||
|
reactMarkup,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
||||||
|
"types": ["vite/client"]
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@dada/worker",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "13.0.1",
|
||||||
|
"drizzle-orm": "0.45.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "7.6.13",
|
||||||
|
"@types/node": "24.13.3",
|
||||||
|
"typescript": "7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { parentPort } from "node:worker_threads";
|
||||||
|
|
||||||
|
export function handleWorkerProbe(message: unknown) {
|
||||||
|
return message === "ping" ? "pong" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workerPort = parentPort;
|
||||||
|
|
||||||
|
if (workerPort) {
|
||||||
|
workerPort.on("message", (message: unknown) => {
|
||||||
|
workerPort.postMessage(handleWorkerProbe(message));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2024"],
|
||||||
|
"types": ["node"],
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "dada-p0a",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@10.28.2",
|
||||||
|
"engines": {
|
||||||
|
"node": "24.13.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm -r --if-present build && dotnet build supervisor/Dada.Supervisor/Dada.Supervisor.csproj --configuration Release",
|
||||||
|
"typecheck": "pnpm -r --if-present typecheck",
|
||||||
|
"test:unit:contract": "node --test tests/toolchain/frozen-toolchain.test.mjs",
|
||||||
|
"test:unit": "pnpm run test:unit:contract && vitest run tests/unit",
|
||||||
|
"test:integration": "vitest run tests/integration",
|
||||||
|
"test:api": "vitest run tests/api",
|
||||||
|
"test:worker": "pnpm --filter @dada/worker build && node scripts/worker-smoke.mjs",
|
||||||
|
"test:e2e": "node scripts/validate-layer-scope.mjs E2E",
|
||||||
|
"test:visual": "node scripts/validate-layer-scope.mjs VISUAL",
|
||||||
|
"test:performance": "node scripts/validate-layer-scope.mjs PERFORMANCE",
|
||||||
|
"test:security": "node scripts/verify-frozen-dependencies.mjs && node scripts/redaction-scan.mjs",
|
||||||
|
"test:package": "pnpm run typecheck && pnpm --filter @dada/web build && pnpm --filter @dada/api build && pnpm --filter @dada/worker build && node scripts/package-smoke.mjs",
|
||||||
|
"validate:tdd-trace": "node scripts/validate-tdd-trace.mjs",
|
||||||
|
"validate:external": "node scripts/validate-external.mjs",
|
||||||
|
"test:all": "pnpm test:unit && pnpm test:integration && pnpm test:api && pnpm test:worker && pnpm test:e2e && pnpm test:visual && pnpm test:performance && pnpm test:security && pnpm test:package && pnpm validate:tdd-trace",
|
||||||
|
"test:wp0-01": "node scripts/run-wp0-01-validation.mjs"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "1.62.0",
|
||||||
|
"@types/better-sqlite3": "7.6.13",
|
||||||
|
"@types/node": "24.13.3",
|
||||||
|
"@types/react": "19.2.17",
|
||||||
|
"@types/react-dom": "19.2.3",
|
||||||
|
"typescript": "7.0.2",
|
||||||
|
"vite": "8.1.5",
|
||||||
|
"vitest": "4.1.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@dada/shared-contracts",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@sinclair/typebox": "0.34.52"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "7.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { Type } from "@sinclair/typebox";
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2024"],
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
Generated
+2424
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
|||||||
|
packages:
|
||||||
|
- apps/*
|
||||||
|
- packages/*
|
||||||
|
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- better-sqlite3
|
||||||
|
- esbuild
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
export const frozenPackages = {
|
||||||
|
"package.json": {
|
||||||
|
devDependencies: {
|
||||||
|
"@playwright/test": "1.62.0",
|
||||||
|
typescript: "7.0.2",
|
||||||
|
vite: "8.1.5",
|
||||||
|
vitest: "4.1.10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"apps/web/package.json": {
|
||||||
|
dependencies: {
|
||||||
|
"@vibrant/core": "4.0.4",
|
||||||
|
"@vibrant/quantizer-mmcq": "4.0.4",
|
||||||
|
fabric: "7.4.0",
|
||||||
|
react: "19.2.8",
|
||||||
|
"react-dom": "19.2.8",
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
"@vitejs/plugin-react": "6.0.4",
|
||||||
|
typescript: "7.0.2",
|
||||||
|
vite: "8.1.5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"apps/api/package.json": {
|
||||||
|
dependencies: {
|
||||||
|
"@fastify/swagger": "9.8.1",
|
||||||
|
"@sinclair/typebox": "0.34.52",
|
||||||
|
"better-sqlite3": "13.0.1",
|
||||||
|
"drizzle-orm": "0.45.2",
|
||||||
|
fastify: "5.10.0",
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
typescript: "7.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"apps/worker/package.json": {
|
||||||
|
dependencies: {
|
||||||
|
"better-sqlite3": "13.0.1",
|
||||||
|
"drizzle-orm": "0.45.2",
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
typescript: "7.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages/shared-contracts/package.json": {
|
||||||
|
dependencies: {
|
||||||
|
"@sinclair/typebox": "0.34.52",
|
||||||
|
},
|
||||||
|
devDependencies: {
|
||||||
|
typescript: "7.0.2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const frozenRuntime = {
|
||||||
|
arch: "x64",
|
||||||
|
dotnetTarget: "net8.0-windows",
|
||||||
|
node: "24.13.0",
|
||||||
|
os: "win32",
|
||||||
|
pnpm: "10.28.2",
|
||||||
|
};
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const expectedCounts = {
|
||||||
|
acceptanceCriteria: 52,
|
||||||
|
errorCategories: 9,
|
||||||
|
featureModules: 13,
|
||||||
|
parentFamilies: 89,
|
||||||
|
productContracts: 19,
|
||||||
|
requirements: 109,
|
||||||
|
tasks: 52,
|
||||||
|
testCases: 117,
|
||||||
|
uiPages: 22,
|
||||||
|
};
|
||||||
|
|
||||||
|
function readText(root, path) {
|
||||||
|
return readFileSync(resolve(root, path), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function sha256(root, path) {
|
||||||
|
return createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex").toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function duplicates(values) {
|
||||||
|
const counts = new Map();
|
||||||
|
for (const value of values) counts.set(value, (counts.get(value) ?? 0) + 1);
|
||||||
|
return [...counts.entries()].filter(([, count]) => count > 1).map(([value]) => value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function markdownTable(markdown, headingPattern) {
|
||||||
|
const lines = markdown.split(/\r?\n/);
|
||||||
|
const headingIndex = lines.findIndex((line) => headingPattern.test(line));
|
||||||
|
if (headingIndex < 0) return [];
|
||||||
|
|
||||||
|
const tableIndex = lines.findIndex((line, index) => index > headingIndex && line.trim().startsWith("|"));
|
||||||
|
if (tableIndex < 0) return [];
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (let index = tableIndex + 2; index < lines.length; index += 1) {
|
||||||
|
const line = lines[index].trim();
|
||||||
|
if (!line.startsWith("|")) break;
|
||||||
|
rows.push(line.split("|").slice(1, -1).map((cell) => cell.trim()));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectPenIds(node, ids, duplicateIds) {
|
||||||
|
if (node.id) {
|
||||||
|
if (ids.has(node.id)) duplicateIds.push(node.id);
|
||||||
|
ids.add(node.id);
|
||||||
|
}
|
||||||
|
for (const child of node.children ?? []) collectPenIds(child, ids, duplicateIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
function uiFrameIds(scope) {
|
||||||
|
if (!scope.frames) return [];
|
||||||
|
const frames = Array.isArray(scope.frames) ? scope.frames : [scope.frames];
|
||||||
|
return frames.map((frame) => frame.frame_id).filter((id) => id && id !== "UIDesign-only");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateTddTrace({ root = process.cwd(), manifestOverride } = {}) {
|
||||||
|
const errors = [];
|
||||||
|
const manifest = manifestOverride ?? JSON.parse(readText(root, "tasks.manifest.json"));
|
||||||
|
const prd = readText(root, "PRD.md");
|
||||||
|
const featureSummary = readText(root, "FeatureSummary.md");
|
||||||
|
const tdd = readText(root, "tdd.md");
|
||||||
|
const tasksMarkdown = readText(root, "tasks.md");
|
||||||
|
const pen = JSON.parse(readText(root, "Dada-P0A-LowFi.pen"));
|
||||||
|
|
||||||
|
for (const [path, expectedHash] of Object.entries(manifest.source_hashes ?? {})) {
|
||||||
|
const actualHash = sha256(root, path);
|
||||||
|
if (actualHash !== expectedHash.toUpperCase()) {
|
||||||
|
errors.push(`${path} SHA-256 mismatch: ${actualHash}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (manifest.tasks.length !== expectedCounts.tasks || manifest.task_count !== expectedCounts.tasks) {
|
||||||
|
errors.push(`task count must be ${expectedCounts.tasks}`);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
manifest.case_catalog.length !== expectedCounts.testCases ||
|
||||||
|
manifest.normative_case_count !== expectedCounts.testCases
|
||||||
|
) {
|
||||||
|
errors.push(`normative case count must be ${expectedCounts.testCases}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskIds = manifest.tasks.map((task) => task.task_id);
|
||||||
|
const familyAssignments = manifest.tasks.flatMap((task) => task.parent_families);
|
||||||
|
const caseAssignments = manifest.tasks.flatMap((task) => task.case_ids);
|
||||||
|
const catalogCaseIds = manifest.case_catalog.map((testCase) => testCase.case_id);
|
||||||
|
const catalogFamilies = [...new Set(manifest.case_catalog.map((testCase) => testCase.parent_family))];
|
||||||
|
|
||||||
|
for (const [label, values] of [
|
||||||
|
["task", taskIds],
|
||||||
|
["family assignment", familyAssignments],
|
||||||
|
["case assignment", caseAssignments],
|
||||||
|
["case catalog", catalogCaseIds],
|
||||||
|
]) {
|
||||||
|
const repeated = duplicates(values);
|
||||||
|
if (repeated.length > 0) errors.push(`duplicate ${label} IDs: ${repeated.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (familyAssignments.length !== expectedCounts.parentFamilies || new Set(familyAssignments).size !== expectedCounts.parentFamilies) {
|
||||||
|
errors.push(`parent family assignments must be unique and total ${expectedCounts.parentFamilies}`);
|
||||||
|
}
|
||||||
|
if (manifest.parent_family_count !== expectedCounts.parentFamilies || catalogFamilies.length !== expectedCounts.parentFamilies) {
|
||||||
|
errors.push(`parent family catalog count must be ${expectedCounts.parentFamilies}`);
|
||||||
|
}
|
||||||
|
if (caseAssignments.length !== expectedCounts.testCases || new Set(caseAssignments).size !== expectedCounts.testCases) {
|
||||||
|
errors.push(`case assignments must be unique and total ${expectedCounts.testCases}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignedFamilySet = new Set(familyAssignments);
|
||||||
|
const assignedCaseSet = new Set(caseAssignments);
|
||||||
|
for (const family of catalogFamilies) {
|
||||||
|
if (!assignedFamilySet.has(family)) errors.push(`unassigned parent family: ${family}`);
|
||||||
|
if (!tdd.includes(family)) errors.push(`parent family missing from tdd.md: ${family}`);
|
||||||
|
}
|
||||||
|
for (const caseId of catalogCaseIds) {
|
||||||
|
if (!assignedCaseSet.has(caseId)) errors.push(`unassigned normative case: ${caseId}`);
|
||||||
|
if (!tdd.includes(caseId)) errors.push(`normative case missing from tdd.md: ${caseId}`);
|
||||||
|
}
|
||||||
|
for (const assignedCase of assignedCaseSet) {
|
||||||
|
if (!catalogCaseIds.includes(assignedCase)) errors.push(`assigned case missing from catalog: ${assignedCase}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskIndex = new Map(taskIds.map((id, index) => [id, index]));
|
||||||
|
const knownWorkPackages = new Set(manifest.tasks.map((task) => task.work_package));
|
||||||
|
manifest.tasks.forEach((task, index) => {
|
||||||
|
if (!/^TASK-(?:WP\d+|FINAL)-\d{2}$/.test(task.task_id)) {
|
||||||
|
errors.push(`invalid task ID: ${task.task_id}`);
|
||||||
|
}
|
||||||
|
if (!tasksMarkdown.includes(task.task_id)) errors.push(`task missing from tasks.md: ${task.task_id}`);
|
||||||
|
|
||||||
|
for (const dependency of task.dependencies.task_ids) {
|
||||||
|
const dependencyIndex = taskIndex.get(dependency);
|
||||||
|
if (dependencyIndex === undefined) errors.push(`${task.task_id} has unknown dependency ${dependency}`);
|
||||||
|
else if (dependencyIndex >= index) errors.push(`${task.task_id} dependency is not earlier: ${dependency}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const dependency of task.dependencies.work_packages) {
|
||||||
|
if (!knownWorkPackages.has(dependency)) errors.push(`${task.task_id} has unknown work-package dependency ${dependency}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (task.validation_commands.length === 0) errors.push(`${task.task_id} has no validation command`);
|
||||||
|
if (task.evidence_directories.length === 0) errors.push(`${task.task_id} has no evidence directory`);
|
||||||
|
if (task.automation.length === 0) errors.push(`${task.task_id} has no automation classification`);
|
||||||
|
if (task.release_gate.length === 0) errors.push(`${task.task_id} has no release gate`);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const testCase of manifest.case_catalog) {
|
||||||
|
if (!/^TDD-[A-Z0-9-]+-[a-z0-9-]+$/.test(testCase.case_id)) {
|
||||||
|
errors.push(`invalid case ID: ${testCase.case_id}`);
|
||||||
|
}
|
||||||
|
if (!testCase.case_id.startsWith(`${testCase.parent_family}-`)) {
|
||||||
|
errors.push(`${testCase.case_id} does not belong to ${testCase.parent_family}`);
|
||||||
|
}
|
||||||
|
for (const field of [
|
||||||
|
"requirement_ac_source",
|
||||||
|
"fixture_and_preconditions",
|
||||||
|
"numbered_steps",
|
||||||
|
"expected_response_ui",
|
||||||
|
"expected_db_files",
|
||||||
|
"forbidden_side_effects",
|
||||||
|
"evidence_files",
|
||||||
|
]) {
|
||||||
|
if (!testCase[field]?.trim()) errors.push(`${testCase.case_id} has an empty ${field}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requirementIds = [...new Set(manifest.tasks.flatMap((task) => task.requirements))];
|
||||||
|
if (requirementIds.length !== expectedCounts.requirements) {
|
||||||
|
errors.push(`requirement coverage must total ${expectedCounts.requirements}`);
|
||||||
|
}
|
||||||
|
for (const id of requirementIds) {
|
||||||
|
if (!prd.includes(id)) errors.push(`requirement missing from PRD.md: ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const acceptanceCriteria = [...new Set(manifest.tasks.flatMap((task) => task.acceptance_criteria))];
|
||||||
|
const expectedAcceptanceCriteria = Array.from({ length: 56 }, (_, index) => index + 1)
|
||||||
|
.filter((number) => ![8, 26, 37, 54].includes(number))
|
||||||
|
.map((number) => `AC-${String(number).padStart(2, "0")}`);
|
||||||
|
if (
|
||||||
|
acceptanceCriteria.length !== expectedCounts.acceptanceCriteria ||
|
||||||
|
expectedAcceptanceCriteria.some((id) => !acceptanceCriteria.includes(id))
|
||||||
|
) {
|
||||||
|
errors.push(`current P0-A AC coverage must be the frozen ${expectedCounts.acceptanceCriteria}-item set`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const featureModules = markdownTable(featureSummary, /^## 5\./);
|
||||||
|
const errorCategories = markdownTable(featureSummary, /^### 6\.1\b/);
|
||||||
|
const productContracts = markdownTable(featureSummary, /^## 7\./);
|
||||||
|
for (const [label, rows, count] of [
|
||||||
|
["feature modules", featureModules, expectedCounts.featureModules],
|
||||||
|
["error categories", errorCategories, expectedCounts.errorCategories],
|
||||||
|
["product contracts", productContracts, expectedCounts.productContracts],
|
||||||
|
]) {
|
||||||
|
if (rows.length !== count) errors.push(`${label} table must contain ${count} rows`);
|
||||||
|
if (duplicates(rows.map((row) => row[0])).length > 0) errors.push(`${label} table has duplicate keys`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const uiScopes = manifest.tasks.flatMap((task) => task.ui_scope);
|
||||||
|
const pageIds = [...new Set(uiScopes.map((scope) => scope.page_id))];
|
||||||
|
if (pageIds.length !== expectedCounts.uiPages) errors.push(`UI page coverage must total ${expectedCounts.uiPages}`);
|
||||||
|
const uiRoleKeys = uiScopes.map((scope) => `${scope.page_id}:${scope.role}`);
|
||||||
|
for (const pageId of pageIds) {
|
||||||
|
for (const role of ["implementation", "final_evidence"]) {
|
||||||
|
if (!uiRoleKeys.includes(`${pageId}:${role}`)) errors.push(`${pageId} is missing UI role ${role}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const penIds = new Set();
|
||||||
|
const duplicatePenIds = [];
|
||||||
|
collectPenIds(pen, penIds, duplicatePenIds);
|
||||||
|
if (duplicatePenIds.length > 0) errors.push(`duplicate .pen IDs: ${duplicatePenIds.join(", ")}`);
|
||||||
|
if (pen.children.length !== 19 || pen.children.filter((frame) => frame.id !== "Z8p3I").length !== 18) {
|
||||||
|
errors.push("Dada-P0A-LowFi.pen must contain one index and 18 product frames");
|
||||||
|
}
|
||||||
|
for (const frameId of new Set(uiScopes.flatMap(uiFrameIds))) {
|
||||||
|
if (!penIds.has(frameId)) errors.push(`UI scope references an unknown .pen frame: ${frameId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredScripts = [
|
||||||
|
"test:unit",
|
||||||
|
"test:integration",
|
||||||
|
"test:api",
|
||||||
|
"test:worker",
|
||||||
|
"test:e2e",
|
||||||
|
"test:visual",
|
||||||
|
"test:performance",
|
||||||
|
"test:security",
|
||||||
|
"test:package",
|
||||||
|
"validate:tdd-trace",
|
||||||
|
"test:all",
|
||||||
|
"validate:external",
|
||||||
|
];
|
||||||
|
const packageJson = JSON.parse(readText(root, "package.json"));
|
||||||
|
for (const script of requiredScripts) {
|
||||||
|
if (!packageJson.scripts?.[script]) errors.push(`package.json is missing script ${script}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors,
|
||||||
|
status: errors.length === 0 ? "passed" : "failed",
|
||||||
|
summary: {
|
||||||
|
acceptanceCriteria: acceptanceCriteria.length,
|
||||||
|
errorCategories: errorCategories.length,
|
||||||
|
featureModules: featureModules.length,
|
||||||
|
parentFamilies: catalogFamilies.length,
|
||||||
|
penProductFrames: pen.children.filter((frame) => frame.id !== "Z8p3I").length,
|
||||||
|
productContracts: productContracts.length,
|
||||||
|
requirements: requirementIds.length,
|
||||||
|
tasks: manifest.tasks.length,
|
||||||
|
testCases: manifest.case_catalog.length,
|
||||||
|
uiPages: pageIds.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
import { pathToFileURL } from "node:url";
|
||||||
|
|
||||||
|
const requireFromApi = createRequire(pathToFileURL(resolve("apps/api/package.json")));
|
||||||
|
const Database = requireFromApi("better-sqlite3");
|
||||||
|
const sqlitePackage = requireFromApi("better-sqlite3/package.json");
|
||||||
|
|
||||||
|
const database = new Database(":memory:");
|
||||||
|
const row = database.prepare("select 1 as value").get();
|
||||||
|
database.close();
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
arch: process.arch,
|
||||||
|
betterSqlite3: sqlitePackage.version,
|
||||||
|
node: process.version.slice(1),
|
||||||
|
os: process.platform,
|
||||||
|
selectValue: row.value,
|
||||||
|
status: row.value === 1 ? "passed" : "failed",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result));
|
||||||
|
if (result.status !== "passed") process.exit(1);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { frozenRuntime } from "./frozen-versions.mjs";
|
||||||
|
|
||||||
|
if (
|
||||||
|
process.platform !== frozenRuntime.os ||
|
||||||
|
process.arch !== frozenRuntime.arch ||
|
||||||
|
process.version.slice(1) !== frozenRuntime.node
|
||||||
|
) {
|
||||||
|
throw new Error("The package smoke must run on the frozen win-x64 Node 24.13.0 runtime.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtimeDirectory = resolve(".build/runtime");
|
||||||
|
const runtimeNode = resolve(runtimeDirectory, "node.exe");
|
||||||
|
mkdirSync(runtimeDirectory, { recursive: true });
|
||||||
|
copyFileSync(process.execPath, runtimeNode);
|
||||||
|
|
||||||
|
const nativeSmoke = JSON.parse(
|
||||||
|
execFileSync(runtimeNode, ["scripts/native-smoke.mjs"], { encoding: "utf8" }).trim(),
|
||||||
|
);
|
||||||
|
const workerSmoke = JSON.parse(
|
||||||
|
execFileSync(runtimeNode, ["scripts/worker-smoke.mjs"], { encoding: "utf8" }).trim(),
|
||||||
|
);
|
||||||
|
|
||||||
|
execFileSync(
|
||||||
|
"dotnet",
|
||||||
|
[
|
||||||
|
"restore",
|
||||||
|
"supervisor/Dada.Supervisor/Dada.Supervisor.csproj",
|
||||||
|
"--configfile",
|
||||||
|
"NuGet.Config",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
execFileSync(
|
||||||
|
"dotnet",
|
||||||
|
[
|
||||||
|
"build",
|
||||||
|
"supervisor/Dada.Supervisor/Dada.Supervisor.csproj",
|
||||||
|
"--configuration",
|
||||||
|
"Release",
|
||||||
|
"--no-restore",
|
||||||
|
],
|
||||||
|
{ stdio: "inherit" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const supervisorExecutable = resolve(
|
||||||
|
"supervisor/Dada.Supervisor/bin/Release/net8.0-windows/Dada.Supervisor.exe",
|
||||||
|
);
|
||||||
|
if (!existsSync(supervisorExecutable)) {
|
||||||
|
throw new Error("The .NET 8 WinForms supervisor executable was not produced.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
schema_version: "1.0",
|
||||||
|
native: nativeSmoke,
|
||||||
|
packagedNodeCandidate: {
|
||||||
|
path: ".build/runtime/node.exe",
|
||||||
|
version: frozenRuntime.node,
|
||||||
|
},
|
||||||
|
status: "passed",
|
||||||
|
supervisor: {
|
||||||
|
build: "passed",
|
||||||
|
target: frozenRuntime.dotnetTarget,
|
||||||
|
},
|
||||||
|
worker: workerSmoke,
|
||||||
|
};
|
||||||
|
|
||||||
|
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR;
|
||||||
|
if (evidenceDirectory) {
|
||||||
|
mkdirSync(evidenceDirectory, { recursive: true });
|
||||||
|
writeFileSync(resolve(evidenceDirectory, "native-smoke.json"), `${JSON.stringify(result, null, 2)}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
|
import { extname, join, relative } from "node:path";
|
||||||
|
|
||||||
|
const scanRoots = ["apps", "packages", "scripts", "supervisor", "tests"];
|
||||||
|
const textExtensions = new Set([".cs", ".json", ".mjs", ".ts", ".tsx", ".yaml", ".yml"]);
|
||||||
|
const findings = [];
|
||||||
|
|
||||||
|
function visit(path) {
|
||||||
|
for (const name of readdirSync(path)) {
|
||||||
|
const child = join(path, name);
|
||||||
|
const relativePath = relative(process.cwd(), child).replaceAll("\\", "/");
|
||||||
|
if (["bin", "dist", "node_modules", "obj"].includes(name)) continue;
|
||||||
|
if (statSync(child).isDirectory()) {
|
||||||
|
visit(child);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!textExtensions.has(extname(name)) || relativePath === "scripts/redaction-scan.mjs") continue;
|
||||||
|
|
||||||
|
const content = readFileSync(child, "utf8");
|
||||||
|
const prohibited = [
|
||||||
|
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
|
||||||
|
/[A-Za-z]:\\Users\\[^\\\s]+/,
|
||||||
|
/(?:api[_-]?key|password|secret)\s*[:=]\s*["'][^"']{8,}["']/i,
|
||||||
|
];
|
||||||
|
if (prohibited.some((pattern) => pattern.test(content))) findings.push(relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const root of scanRoots) visit(root);
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ findings, status: findings.length === 0 ? "passed" : "failed" }, null, 2));
|
||||||
|
if (findings.length > 0) process.exit(1);
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { createHash } from "node:crypto";
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const runId = process.env.DADA_TDD_RUN_ID ?? `wp0-01-green-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
|
||||||
|
const caseId = "TDD-WP0-DEP-001-frozen-toolchain";
|
||||||
|
const evidenceDirectory = resolve("artifacts", "tdd", runId, "cases", caseId);
|
||||||
|
|
||||||
|
if (existsSync(evidenceDirectory)) {
|
||||||
|
throw new Error(`Evidence run already exists: ${runId}`);
|
||||||
|
}
|
||||||
|
mkdirSync(evidenceDirectory, { recursive: true });
|
||||||
|
|
||||||
|
const commandDefinitions = [
|
||||||
|
{ command: "pnpm install --frozen-lockfile --offline", args: ["install", "--frozen-lockfile", "--offline"] },
|
||||||
|
{ command: "pnpm test:unit", args: ["test:unit"] },
|
||||||
|
{ command: "pnpm test:security", args: ["test:security"] },
|
||||||
|
{ command: "pnpm test:package", args: ["test:package"] },
|
||||||
|
{ command: "pnpm validate:tdd-trace", args: ["validate:tdd-trace"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const startedAt = new Date().toISOString();
|
||||||
|
const commands = [];
|
||||||
|
for (const definition of commandDefinitions) {
|
||||||
|
const commandStartedAt = new Date().toISOString();
|
||||||
|
const executable = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "pnpm";
|
||||||
|
const args = process.platform === "win32"
|
||||||
|
? ["/d", "/s", "/c", `pnpm ${definition.args.join(" ")}`]
|
||||||
|
: definition.args;
|
||||||
|
const result = spawnSync(executable, args, {
|
||||||
|
env: { ...process.env, DADA_EVIDENCE_DIR: evidenceDirectory },
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
commands.push({
|
||||||
|
command: definition.command,
|
||||||
|
exit_code: result.status ?? 1,
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
started_at: commandStartedAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const allPassed = commands.every((command) => command.exit_code === 0);
|
||||||
|
const requiredEvidence = ["dependency-tree.json", "native-smoke.json"];
|
||||||
|
const missingEvidence = requiredEvidence.filter((path) => !existsSync(resolve(evidenceDirectory, path)));
|
||||||
|
const passed = allPassed && missingEvidence.length === 0;
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
resolve(evidenceDirectory, "commands.json"),
|
||||||
|
`${JSON.stringify({ schema_version: "1.0", run_id: runId, phase: "green", commands }, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const manifestBytes = readFileSync("tasks.manifest.json");
|
||||||
|
const result = {
|
||||||
|
schema_version: "1.0",
|
||||||
|
run_id: runId,
|
||||||
|
task_id: "TASK-WP0-01",
|
||||||
|
case_id: caseId,
|
||||||
|
parent_family: "TDD-WP0-DEP-001",
|
||||||
|
phase: "green",
|
||||||
|
status: passed ? "passed" : "failed",
|
||||||
|
manifest: {
|
||||||
|
path: "tasks.manifest.json",
|
||||||
|
sha256: createHash("sha256").update(manifestBytes).digest("hex").toUpperCase(),
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
arch: process.arch,
|
||||||
|
node: process.version.slice(1),
|
||||||
|
os: process.platform,
|
||||||
|
},
|
||||||
|
evidence_files: ["commands.json", ...requiredEvidence],
|
||||||
|
missing_evidence: missingEvidence,
|
||||||
|
started_at: startedAt,
|
||||||
|
finished_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
writeFileSync(resolve(evidenceDirectory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
if (!passed) process.exit(1);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const allowedServices = new Set(["ai", "resend", "amap"]);
|
||||||
|
|
||||||
|
function argument(name) {
|
||||||
|
const index = process.argv.indexOf(name);
|
||||||
|
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const service = argument("--service");
|
||||||
|
const runId = argument("--run-id");
|
||||||
|
|
||||||
|
if (!allowedServices.has(service) || !runId) {
|
||||||
|
console.error("Usage: pnpm validate:external -- --service <ai|resend|amap> --run-id <id>");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
mode: "mock",
|
||||||
|
run_id: runId,
|
||||||
|
service,
|
||||||
|
status: "not_applicable_for_TASK-WP0-01",
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const layer = process.argv[2];
|
||||||
|
if (!layer) {
|
||||||
|
console.error("A test layer is required.");
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifest = JSON.parse(readFileSync("tasks.manifest.json", "utf8"));
|
||||||
|
const task = manifest.tasks.find((item) => item.task_id === "TASK-WP0-01");
|
||||||
|
const applicable = task.layers.includes(layer);
|
||||||
|
|
||||||
|
if (applicable) {
|
||||||
|
console.error(`TASK-WP0-01 assigns ${layer}; a real layer runner is required.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify({
|
||||||
|
layer,
|
||||||
|
status: "not_applicable",
|
||||||
|
task_id: task.task_id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { validateTddTrace } from "./lib/tdd-trace.mjs";
|
||||||
|
|
||||||
|
const result = validateTddTrace();
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
if (result.errors.length > 0) process.exit(1);
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { dirname, resolve } from "node:path";
|
||||||
|
|
||||||
|
import { frozenPackages, frozenRuntime } from "./frozen-versions.mjs";
|
||||||
|
|
||||||
|
function readJson(path) {
|
||||||
|
return JSON.parse(readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
function installedPackageVersion(manifestPath, packageName) {
|
||||||
|
const packagePath = resolve(
|
||||||
|
dirname(manifestPath),
|
||||||
|
"node_modules",
|
||||||
|
...packageName.split("/"),
|
||||||
|
"package.json",
|
||||||
|
);
|
||||||
|
return readJson(packagePath).version;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyFrozenDependencies() {
|
||||||
|
const problems = [];
|
||||||
|
const packages = {};
|
||||||
|
|
||||||
|
for (const [manifestPath, sections] of Object.entries(frozenPackages)) {
|
||||||
|
const manifest = readJson(manifestPath);
|
||||||
|
packages[manifest.name] = {};
|
||||||
|
|
||||||
|
for (const [section, expected] of Object.entries(sections)) {
|
||||||
|
for (const [name, version] of Object.entries(expected)) {
|
||||||
|
const declared = manifest[section]?.[name];
|
||||||
|
if (declared !== version) {
|
||||||
|
problems.push(`${manifestPath} ${section}.${name}: declared ${declared ?? "missing"}, expected ${version}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = installedPackageVersion(manifestPath, name);
|
||||||
|
packages[manifest.name][name] = installed;
|
||||||
|
if (installed !== version) {
|
||||||
|
problems.push(`${manifestPath} ${name}: installed ${installed}, expected ${version}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageManager = readJson("package.json").packageManager;
|
||||||
|
const pnpmVersion = process.platform === "win32"
|
||||||
|
? execFileSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", "pnpm --version"], {
|
||||||
|
encoding: "utf8",
|
||||||
|
}).trim()
|
||||||
|
: execFileSync("pnpm", ["--version"], { encoding: "utf8" }).trim();
|
||||||
|
const runtimeChecks = {
|
||||||
|
arch: process.arch,
|
||||||
|
node: process.version.slice(1),
|
||||||
|
os: process.platform,
|
||||||
|
packageManager,
|
||||||
|
pnpm: pnpmVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of ["arch", "node", "os", "pnpm"]) {
|
||||||
|
if (runtimeChecks[key] !== frozenRuntime[key]) {
|
||||||
|
problems.push(`${key}: running ${runtimeChecks[key]}, expected ${frozenRuntime[key]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (packageManager !== `pnpm@${frozenRuntime.pnpm}`) {
|
||||||
|
problems.push(`packageManager: declared ${packageManager}, expected pnpm@${frozenRuntime.pnpm}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
packages,
|
||||||
|
problems,
|
||||||
|
runtime: runtimeChecks,
|
||||||
|
status: problems.length === 0 ? "passed" : "failed",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapshot = verifyFrozenDependencies();
|
||||||
|
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR;
|
||||||
|
|
||||||
|
if (evidenceDirectory) {
|
||||||
|
mkdirSync(evidenceDirectory, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
resolve(evidenceDirectory, "dependency-tree.json"),
|
||||||
|
`${JSON.stringify({ schema_version: "1.0", ...snapshot }, null, 2)}\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(snapshot, null, 2));
|
||||||
|
if (snapshot.problems.length > 0) process.exit(1);
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
import { Worker } from "node:worker_threads";
|
||||||
|
|
||||||
|
const worker = new Worker(resolve("apps/worker/dist/worker.js"));
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
void worker.terminate();
|
||||||
|
throw new Error("Worker probe timed out.");
|
||||||
|
}, 5_000);
|
||||||
|
|
||||||
|
const reply = await new Promise((resolveReply, reject) => {
|
||||||
|
worker.once("error", reject);
|
||||||
|
worker.once("message", resolveReply);
|
||||||
|
worker.postMessage("ping");
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
await worker.terminate();
|
||||||
|
|
||||||
|
if (reply !== "pong") {
|
||||||
|
throw new Error(`Unexpected Worker probe reply: ${String(reply)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify({ reply, status: "passed" }));
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Dada.Supervisor;
|
||||||
|
|
||||||
|
internal static class Program
|
||||||
|
{
|
||||||
|
[STAThread]
|
||||||
|
private static void Main()
|
||||||
|
{
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new SupervisorForm());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Dada.Supervisor;
|
||||||
|
|
||||||
|
internal sealed class SupervisorForm : Form
|
||||||
|
{
|
||||||
|
public SupervisorForm()
|
||||||
|
{
|
||||||
|
ClientSize = new Size(420, 160);
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
MaximizeBox = false;
|
||||||
|
StartPosition = FormStartPosition.CenterScreen;
|
||||||
|
Text = "Dada";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createApp } from "../../apps/api/src/app.js";
|
||||||
|
|
||||||
|
describe("Fastify skeleton", () => {
|
||||||
|
it("becomes ready without adding a product route", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await app.ready();
|
||||||
|
expect(app.printRoutes()).not.toContain("health");
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
const requireFromApi = createRequire(new URL("../../apps/api/package.json", import.meta.url));
|
||||||
|
const Database = requireFromApi("better-sqlite3");
|
||||||
|
|
||||||
|
describe("better-sqlite3 native module", () => {
|
||||||
|
it("executes an in-memory query without creating business data", () => {
|
||||||
|
const database = new Database(":memory:");
|
||||||
|
const row = database.prepare("select 1 as value").get();
|
||||||
|
database.close();
|
||||||
|
expect(row.value).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
import { frozenPackages } from "../../scripts/frozen-versions.mjs";
|
||||||
|
|
||||||
|
const expectedFiles = [
|
||||||
|
"NuGet.Config",
|
||||||
|
"pnpm-lock.yaml",
|
||||||
|
"pnpm-workspace.yaml",
|
||||||
|
"tsconfig.base.json",
|
||||||
|
"apps/web/package.json",
|
||||||
|
"apps/web/src/toolchain-probe.tsx",
|
||||||
|
"apps/api/package.json",
|
||||||
|
"apps/api/src/app.ts",
|
||||||
|
"apps/worker/package.json",
|
||||||
|
"apps/worker/src/worker.ts",
|
||||||
|
"packages/shared-contracts/package.json",
|
||||||
|
"packages/shared-contracts/src/index.ts",
|
||||||
|
"scripts/validate-tdd-trace.mjs",
|
||||||
|
"scripts/native-smoke.mjs",
|
||||||
|
"supervisor/Dada.Supervisor/Dada.Supervisor.csproj",
|
||||||
|
];
|
||||||
|
|
||||||
|
function readJson(path) {
|
||||||
|
return JSON.parse(readFileSync(path, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("TASK-WP0-01 has no unresolved task dependency", () => {
|
||||||
|
const manifest = readJson("tasks.manifest.json");
|
||||||
|
const task = manifest.tasks.find((item) => item.task_id === "TASK-WP0-01");
|
||||||
|
assert.ok(task, "TASK-WP0-01 must exist in tasks.manifest.json");
|
||||||
|
assert.deepEqual(task.dependencies.task_ids, []);
|
||||||
|
assert.deepEqual(task.dependencies.work_packages, []);
|
||||||
|
assert.equal(task.dependencies.source_text, "无");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("TDD-WP0-DEP-001 frozen toolchain contract is implemented", () => {
|
||||||
|
const problems = [];
|
||||||
|
|
||||||
|
for (const path of expectedFiles) {
|
||||||
|
if (!existsSync(path)) problems.push(`missing file: ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [path, sections] of Object.entries(frozenPackages)) {
|
||||||
|
if (!existsSync(path)) continue;
|
||||||
|
const packageJson = readJson(path);
|
||||||
|
for (const [section, packages] of Object.entries(sections)) {
|
||||||
|
for (const [name, version] of Object.entries(packages)) {
|
||||||
|
const actual = packageJson[section]?.[name];
|
||||||
|
if (actual !== version) {
|
||||||
|
problems.push(`${path} ${section}.${name}: expected ${version}, got ${actual ?? "missing"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.deepEqual(problems, []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { createApp } from "../../apps/api/src/app.js";
|
||||||
|
import { renderToolchainProbe } from "../../apps/web/src/toolchain-probe.js";
|
||||||
|
import { validateTddTrace } from "../../scripts/lib/tdd-trace.mjs";
|
||||||
|
|
||||||
|
function readManifest() {
|
||||||
|
return JSON.parse(readFileSync(new URL("../../tasks.manifest.json", import.meta.url), "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("TASK-WP0-01 minimum toolchain", () => {
|
||||||
|
it("renders through React and Fabric at the frozen versions", () => {
|
||||||
|
const probe = renderToolchainProbe();
|
||||||
|
|
||||||
|
expect(probe.reactMarkup).toContain("Dada toolchain probe");
|
||||||
|
expect(probe.fabricSvg).toContain("<rect");
|
||||||
|
expect(probe.fabricVersion).toBe("7.4.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("loads and closes Fastify with the frozen Swagger plugin", async () => {
|
||||||
|
const app = createApp();
|
||||||
|
await app.ready();
|
||||||
|
expect(app.hasPlugin("@fastify/swagger")).toBe(true);
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("validates the frozen TDD trace", () => {
|
||||||
|
const result = validateTddTrace();
|
||||||
|
expect(result.errors).toEqual([]);
|
||||||
|
expect(result.summary).toMatchObject({
|
||||||
|
acceptanceCriteria: 52,
|
||||||
|
parentFamilies: 89,
|
||||||
|
requirements: 109,
|
||||||
|
tasks: 52,
|
||||||
|
testCases: 117,
|
||||||
|
uiPages: 22,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a missing normative case", () => {
|
||||||
|
const manifest = readManifest();
|
||||||
|
manifest.case_catalog.pop();
|
||||||
|
const result = validateTddTrace({ manifestOverride: manifest });
|
||||||
|
expect(result.status).toBe("failed");
|
||||||
|
expect(result.errors.some((error) => error.includes("normative case count"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects a duplicated case assignment", () => {
|
||||||
|
const manifest = readManifest();
|
||||||
|
manifest.tasks[1].case_ids.push(manifest.tasks[0].case_ids[0]);
|
||||||
|
const result = validateTddTrace({ manifestOverride: manifest });
|
||||||
|
expect(result.status).toBe("failed");
|
||||||
|
expect(result.errors.some((error) => error.includes("duplicate case assignment"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2024",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user