Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,493 @@
import { describe, expect, it } from 'vitest';
import {
builtInProjectTemplates,
createProjectTemplateSnapshot,
getProjectTemplateRequiredSkillIds,
hasProjectCapability,
type ProjectTemplateCourseStage,
validateProjectTemplateSnapshot,
} from '../../shared/project-template';
import { isCourseProjectTemplate } from '../../src/lib/project-templates';
const LEGACY_YOUTH_DEPLOY_STAGE: ProjectTemplateCourseStage = {
id: 'deploy',
order: 6,
title: '发布部署',
agentName: '部署工程师',
agentRole: '发布部署',
goal: '确认作品可以通过 Docker 构建、启动、打包并通过作品广场上传接口提交。',
task: '创建或维护 Dockerfile 和 docker-compose.yml检查 Docker 构建状态,把当前项目打成 zip 包,通过作品广场上传接口提交,并记录发布目标、发布步骤、回滚说明和作品广场提交清单。',
deliverableTitle: '部署报告.md',
deliverableType: 'report',
deliverableSummary: 'Dockerfile、docker-compose.yml、发布目标、准备情况、Docker 执行命令、zip 包路径、作品广场上传接口结果、发布步骤、回滚步骤。',
acceptanceCriteria: [
'有明确发布目标',
'有 Dockerfile',
'有 docker-compose.yml',
'有 Docker 构建或检查结果',
'有 zip 包路径',
'有作品广场上传接口结果',
'有发布步骤',
'有回滚步骤',
'没有隐私或密钥风险',
],
handoffTarget: null,
};
const LEGACY_GAME_RELEASE_STAGE: ProjectTemplateCourseStage = {
id: 'game-release',
order: 4,
title: '发布打包',
agentName: '游戏发布官',
agentRole: '游戏发布官',
goal: '准备构建结果、试玩记录和发布检查清单。',
task: '记录如何运行游戏、试玩了什么,以及发布前后需要检查哪些项目。',
deliverableTitle: 'RELEASE_CHECKLIST.md',
deliverableType: 'report',
deliverableSummary: '构建结果、试玩检查、运行说明、发布检查项和已知风险。',
acceptanceCriteria: [
'包含构建命令和结果',
'包含试玩记录',
'包含运行说明',
'包含发布检查项',
'包含已知风险或问题',
],
handoffTarget: 'game-showcase',
};
function replacePublishStageWithLegacy(
snapshot: ReturnType<typeof createProjectTemplateSnapshot>,
legacyStage: ProjectTemplateCourseStage,
) {
if (!snapshot.capabilities.course.enabled) throw new Error('Expected course capability');
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.map((stage) => (
stage.id === legacyStage.id ? structuredClone(legacyStage) : stage
));
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.map((slot) => (
slot.id === legacyStage.id ? { ...slot, goal: legacyStage.goal } : slot
));
}
describe('project template snapshots', () => {
it('creates gate-free built-in snapshots with static course metadata and Chinese display text', () => {
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
expect(snapshot.schemaVersion).toBe(1);
expect(snapshot.template).toMatchObject({
id: 'youth-ai-course',
version: 1,
title: 'AI 伙伴课程项目',
createdAt: '2026-07-01T00:00:00.000Z',
});
expect(snapshot.capabilities.chat.enabled).toBe(true);
expect(snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
expect(snapshot.capabilities.course.enabled).toBe(true);
expect(snapshot.capabilities.publish.enabled).toBe(true);
if (snapshot.capabilities.course.enabled) {
expect(snapshot.capabilities.course.stages).toHaveLength(6);
expect(snapshot.capabilities.course.slots).toHaveLength(6);
expect(snapshot.capabilities.course).not.toHaveProperty('reviewer');
expect(snapshot.capabilities.course).not.toHaveProperty('partnerAgent');
expect(snapshot.capabilities.course).not.toHaveProperty('reviewPolicy');
expect(snapshot.capabilities.course).not.toHaveProperty('completionUnlock');
for (const stage of snapshot.capabilities.course.stages) {
const slot = snapshot.capabilities.course.slots.find((candidate) => candidate.id === stage.id);
const snapshotStage = snapshot.capabilities.course.stages.find((candidate) => candidate.id === stage.id);
expect(slot).toMatchObject({
id: stage.id,
title: stage.title,
agentName: stage.agentName,
agentRole: stage.agentRole,
recommendedAgentProfile: {
roleId: stage.id,
displayName: stage.agentName,
notes: [],
},
});
expect(snapshotStage).toMatchObject({
id: stage.id,
order: stage.order,
title: stage.title,
agentName: stage.agentName,
agentRole: stage.agentRole,
deliverableTitle: stage.deliverableTitle,
deliverableType: stage.deliverableType,
handoffTarget: stage.handoffTarget,
});
}
}
});
it('keeps standard development projects out of course and publish capabilities', () => {
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
expect(snapshot.template.title).toBe('普通开发项目');
expect(hasProjectCapability(snapshot, 'chat')).toBe(true);
expect(hasProjectCapability(snapshot, 'course')).toBe(false);
expect(hasProjectCapability(snapshot, 'publish')).toBe(false);
expect(hasProjectCapability(snapshot, 'gameDevelopment')).toBe(false);
});
it('normalizes legacy youth course snapshots that still point at the retired subagents route', () => {
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
snapshot.capabilities.chat.defaultRoute = '/subagents';
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
}
});
it('normalizes the exact legacy youth deploy contract after route and scaffold compatibility', () => {
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
snapshot.capabilities.chat.defaultRoute = '/subagents';
delete snapshot.capabilities.scaffold;
replacePublishStageWithLegacy(snapshot, LEGACY_YOUTH_DEPLOY_STAGE);
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
if (result.ok && result.snapshot.capabilities.course.enabled) {
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'deploy');
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
expect(result.snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
expect(result.snapshot.capabilities.scaffold).toEqual({ enabled: false });
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
}
});
it('normalizes the exact five-stage legacy game release delivery contract', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
replacePublishStageWithLegacy(snapshot, LEGACY_GAME_RELEASE_STAGE);
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
if (result.ok && result.snapshot.capabilities.course.enabled) {
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'game-release');
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
}
});
it('accepts legacy built-in snapshots without scaffold when the built-in scaffold is disabled', () => {
const standardSnapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
const youthSnapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
delete standardSnapshot.capabilities.scaffold;
delete youthSnapshot.capabilities.scaffold;
const standardResult = validateProjectTemplateSnapshot(standardSnapshot);
const youthResult = validateProjectTemplateSnapshot(youthSnapshot);
expect(standardResult.ok).toBe(true);
expect(youthResult.ok).toBe(true);
if (standardResult.ok) {
expect(standardResult.snapshot.capabilities.scaffold).toEqual({ enabled: false });
}
if (youthResult.ok) {
expect(youthResult.snapshot.capabilities.scaffold).toEqual({ enabled: false });
}
});
it('accepts built-in mini-game course snapshots with non-youth stage ids', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
});
it('normalizes legacy mini-game snapshots that predate the showcase stage', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
snapshot.template.version = 1;
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.filter((stage) => stage.id !== 'game-showcase');
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.filter((slot) => slot.id !== 'game-showcase');
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
if (!releaseStage) throw new Error('Expected release stage');
releaseStage.handoffTarget = null;
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
if (result.ok && result.snapshot.capabilities.course.enabled) {
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
expect(result.snapshot.capabilities.course.stages.map((stage) => stage.id)).toEqual([
'gameplay',
'assets',
'game-dev',
'game-release',
'game-showcase',
]);
expect(result.snapshot.capabilities.course.slots.map((slot) => slot.id)).toContain('game-showcase');
expect(result.snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release')).toMatchObject({
handoffTarget: 'game-showcase',
});
}
});
it('normalizes a four-stage mini-game snapshot with the frozen legacy release delivery', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
replacePublishStageWithLegacy(snapshot, { ...LEGACY_GAME_RELEASE_STAGE, handoffTarget: null });
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.filter((stage) => stage.id !== 'game-showcase');
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.filter((slot) => slot.id !== 'game-showcase');
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(true);
if (result.ok && result.snapshot.capabilities.course.enabled) {
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'game-release');
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
expect(stage?.handoffTarget).toBe('game-showcase');
expect(result.snapshot.capabilities.course.stages.map((item) => item.id)).toContain('game-showcase');
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
}
});
it('rejects a near-match legacy delivery snapshot with an unrelated stage tamper', () => {
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
snapshot.capabilities.chat.defaultRoute = '/subagents';
delete snapshot.capabilities.scaffold;
replacePublishStageWithLegacy(snapshot, LEGACY_YOUTH_DEPLOY_STAGE);
if (!snapshot.capabilities.course.enabled) throw new Error('Expected youth course capability');
const planStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'pm');
if (!planStage) throw new Error('Expected plan stage');
planStage.title = '被篡改的阶段';
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('built-in contract');
}
});
it('rejects unrelated tampering across every mini-game legacy matcher path', () => {
const fiveStageLegacy = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
replacePublishStageWithLegacy(fiveStageLegacy, LEGACY_GAME_RELEASE_STAGE);
const fourStageCurrent = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
if (!fourStageCurrent.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
fourStageCurrent.capabilities.course.stages = fourStageCurrent.capabilities.course.stages
.filter((stage) => stage.id !== 'game-showcase');
fourStageCurrent.capabilities.course.slots = fourStageCurrent.capabilities.course.slots
.filter((slot) => slot.id !== 'game-showcase');
const currentRelease = fourStageCurrent.capabilities.course.stages
.find((stage) => stage.id === 'game-release');
if (!currentRelease) throw new Error('Expected current release stage');
currentRelease.handoffTarget = null;
const fourStageLegacy = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
replacePublishStageWithLegacy(fourStageLegacy, { ...LEGACY_GAME_RELEASE_STAGE, handoffTarget: null });
if (!fourStageLegacy.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
fourStageLegacy.capabilities.course.stages = fourStageLegacy.capabilities.course.stages
.filter((stage) => stage.id !== 'game-showcase');
fourStageLegacy.capabilities.course.slots = fourStageLegacy.capabilities.course.slots
.filter((slot) => slot.id !== 'game-showcase');
for (const snapshot of [fiveStageLegacy, fourStageCurrent, fourStageLegacy]) {
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
const gameplayStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'gameplay');
if (!gameplayStage) throw new Error('Expected gameplay stage');
gameplayStage.title = '被篡改的玩法阶段';
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('built-in contract');
}
}
});
it('does not repair a non-string createdAt while matching a legacy delivery snapshot', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
replacePublishStageWithLegacy(snapshot, LEGACY_GAME_RELEASE_STAGE);
(snapshot.template as { createdAt: unknown }).createdAt = 123;
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('template.createdAt');
}
});
it('rejects built-in mini-game course snapshots when scaffold is missing', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
delete snapshot.capabilities.scaffold;
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('built-in contract web-mini-game-course@3');
}
});
it('rejects duplicate course stage ids within a template snapshot', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
if (snapshot.capabilities.course.enabled) {
snapshot.capabilities.course.slots[1] = {
...snapshot.capabilities.course.slots[1],
id: snapshot.capabilities.course.slots[0].id,
recommendedAgentProfile: {
...snapshot.capabilities.course.slots[1].recommendedAgentProfile,
roleId: snapshot.capabilities.course.slots[0].id,
},
};
}
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('capabilities.course.slots.id');
expect(result.error).toContain('capabilities.course.slotStageAlignment');
}
});
it('validates damaged snapshots with actionable errors', () => {
const result = validateProjectTemplateSnapshot({
schemaVersion: 1,
template: { id: '', version: 0, title: '', description: '' },
capabilities: { chat: { enabled: true }, course: { enabled: false }, publish: { enabled: false } },
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('template.id');
expect(result.error).toContain('template.version');
expect(result.error).toContain('template.title');
}
});
it('rejects structurally valid snapshots that do not match the built-in semantic contract', () => {
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
const courseSnapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
snapshot.capabilities.course = structuredClone(courseSnapshot.capabilities.course);
snapshot.capabilities.publish = {
enabled: true,
provider: 'works-square',
};
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('built-in contract');
expect(result.error).toContain('standard-dev@1');
}
});
it('exports the clean v3 mini-game template alongside the existing templates', () => {
expect(builtInProjectTemplates.map((template) => template.template.id)).toEqual([
'standard-dev',
'youth-ai-course',
'web-mini-game-course',
]);
});
it('includes the mini-game template scaffold contract and five course stages', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
expect(snapshot.template).toMatchObject({
version: 3,
title: 'Web 小游戏课程项目',
description: '启用结对式 Web 小游戏课程、游戏设计任务文档和 Phaser 3 脚手架支持。',
});
expect(snapshot.capabilities).toMatchObject({
scaffold: {
enabled: true,
kind: 'phaser-2d-casual',
entryFile: 'src/main.ts',
startCommand: 'pnpm dev',
packageManager: 'pnpm',
},
gameDevelopment: {
enabled: true,
engine: 'phaser3',
requiredSkillIds: ['nianxxgame-skill'],
requiredRoleIds: ['gameplay', 'game-dev', 'game-release'],
designDocument: 'GDD.md',
taskDocument: 'TASKS.md',
collaborationMode: 'pair',
},
});
expect(getProjectTemplateRequiredSkillIds(snapshot, 'game-dev')).toEqual(['nianxxgame-skill']);
expect(getProjectTemplateRequiredSkillIds(snapshot, 'assets')).toEqual([]);
if (snapshot.capabilities.course.enabled) {
expect(snapshot.capabilities.course.title).toBe('Web 小游戏制作路径');
expect(snapshot.capabilities.course.stages.map((stage) => stage.id)).toEqual([
'gameplay',
'assets',
'game-dev',
'game-release',
'game-showcase',
]);
expect(snapshot.capabilities.course.stages.map((stage) => stage.title)).toEqual([
'玩法规划',
'素材搜索',
'游戏开发',
'发布打包',
'宣传展示',
]);
const showcaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-showcase');
expect(showcaseStage).toMatchObject({
order: 5,
deliverableTitle: 'SHOWCASE_PACKAGE.md',
handoffTarget: null,
});
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
expect(releaseStage).toMatchObject({
order: 4,
handoffTarget: 'game-showcase',
});
}
});
it('names the complete Works Square package evidence in the game release starter prompt', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
if (!releaseStage) throw new Error('Expected game release stage');
const starterPrompt = [
releaseStage.deliverableTitle,
releaseStage.task,
releaseStage.deliverableSummary,
...releaseStage.acceptanceCriteria,
].join('\n');
expect(starterPrompt).toContain('RELEASE_CHECKLIST.md');
expect(starterPrompt).toContain('Compose 配置、构建、启动、动态端口、HTTP 冒烟和清理');
expect(starterPrompt).toContain('ZIP 根目录、大小、文件数、路径安全、重复项和符号链接');
expect(starterPrompt).toContain('works-publish.json');
expect(starterPrompt).toContain('发布与回滚');
});
it('treats every course-capable template as a course project template', () => {
expect(isCourseProjectTemplate('youth-ai-course')).toBe(true);
expect(isCourseProjectTemplate('web-mini-game-course')).toBe(true);
expect(isCourseProjectTemplate('standard-dev')).toBe(false);
});
it('rejects the removed v1 mini-game contract instead of migrating it', () => {
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
snapshot.template.version = 1;
const result = validateProjectTemplateSnapshot(snapshot);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toContain('built-in contract web-mini-game-course@1');
}
});
});