feat(coding): add durable project identity core

This commit is contained in:
2026-08-26 17:59:44 +08:00
parent 7e54b8fbda
commit 43f58fc72b
12 changed files with 717 additions and 7 deletions

View File

@@ -9,6 +9,7 @@ import {
import type {
CodingProjectAgent,
CodingProjectConfig,
ProjectIdentityChoice,
} from '../../shared/coding-project-contracts';
import type {
ConversationModelState,
@@ -47,6 +48,24 @@ export interface CreateCodingProjectAgentInput {
pinned?: boolean;
}
const CANONICAL_PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
export function isCanonicalCodingProjectId(value: unknown): value is string {
return typeof value === 'string' && CANONICAL_PROJECT_ID_PATTERN.test(value);
}
export function normalizeProjectIdentityChoice(value: unknown): ProjectIdentityChoice {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error('Project identity choice is invalid');
}
const record = value as { kind?: unknown; projectId?: unknown };
if (record.kind === 'create') return { kind: 'create' };
if (record.kind === 'bind' && isCanonicalCodingProjectId(record.projectId)) {
return { kind: 'bind', projectId: record.projectId };
}
throw new Error('Project identity choice is invalid');
}
const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
const AVATAR_ID_PATTERN = /^avatar-(0[1-9]|1[0-6])$/;
const THINKING_LEVELS = new Set<ConversationThinkingLevel>([
@@ -182,6 +201,9 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon
const record = value as Partial<CodingProjectConfigV2>;
if (record.schemaVersion !== 2) throw new Error('Unsupported coding project config schema');
if (!isProjectType(record.projectType)) throw new Error('Invalid project type');
if (record.projectId !== undefined && !isCanonicalCodingProjectId(record.projectId)) {
throw new Error('Project identity is invalid');
}
if (typeof record.initialized !== 'boolean') throw new Error('Project initialized state is invalid');
if (record.knowledgeDirectory !== 'knowledge') throw new Error('Project knowledge directory is invalid');
if (!Array.isArray(record.agents)) throw new Error('Project Agents must be an array');
@@ -199,6 +221,7 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon
return {
schemaVersion: 2,
projectType: record.projectType,
...(record.projectId !== undefined ? { projectId: record.projectId } : {}),
initialized: record.initialized === true,
agents,
knowledgeDirectory: 'knowledge',
@@ -211,10 +234,15 @@ export function normalizeCodingProjectConfigV2(value: unknown): CodingProjectCon
export function createCodingProjectConfigV2(
now = new Date().toISOString(),
projectType: ProjectType = 'custom',
projectId?: string,
): CodingProjectConfigV2 {
if (projectId !== undefined && !isCanonicalCodingProjectId(projectId)) {
throw new Error('Project identity is invalid');
}
return {
schemaVersion: 2,
projectType,
...(projectId !== undefined ? { projectId } : {}),
initialized: false,
agents: [],
knowledgeDirectory: 'knowledge',
@@ -252,6 +280,7 @@ export async function createCodingProjectMetadata(
projectPath: string,
options: {
projectType?: ProjectType;
projectId?: string;
now?: string;
writer?: JsonFileWriter;
} = {},
@@ -262,7 +291,7 @@ export async function createCodingProjectMetadata(
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
}
const config = createCodingProjectConfigV2(options.now, options.projectType);
const config = createCodingProjectConfigV2(options.now, options.projectType, options.projectId);
await Promise.all([
mkdir(path.join(projectPath, '.niancode'), { recursive: true }),
mkdir(path.join(projectPath, 'knowledge'), { recursive: true }),