Add integration verification and validation phases, role templates, and static graph tests

- Implement Phase 4: Integration Verification to ensure skill package consistency.
- Implement Phase 5: Validation to verify quality and deliver the final skill package.
- Create role-template.md for generating per-role execution detail files.
- Create skill-router-template.md for generating SKILL.md with role-based routing.
- Add tests for static graph relationship writing during index build in test_static_graph_integration.py.
This commit is contained in:
catlog22
2026-02-13 12:35:31 +08:00
parent 6054a01b8f
commit a512564b5a
14 changed files with 2897 additions and 51 deletions

View File

@@ -10,47 +10,20 @@ import { Flowchart } from './Flowchart';
import { Badge } from '../ui/Badge';
import { Button } from '../ui/Button';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs';
import type { LiteTask, FlowControl } from '@/lib/api';
import type { NormalizedTask } from '@/lib/api';
import { buildFlowControl } from '@/lib/api';
import type { TaskData } from '@/types/store';
// ========== Types ==========
export interface TaskDrawerProps {
task: LiteTask | TaskData | null;
task: NormalizedTask | TaskData | null;
isOpen: boolean;
onClose: () => void;
}
type TabValue = 'overview' | 'flowchart' | 'files';
// ========== Helper: Unified Task Access ==========
/**
* Normalize task data to common interface
*/
function getTaskId(task: LiteTask | TaskData): string {
if ('task_id' in task && task.task_id) return task.task_id;
if ('id' in task) return task.id;
return 'N/A';
}
function getTaskTitle(task: LiteTask | TaskData): string {
return task.title || 'Untitled Task';
}
function getTaskDescription(task: LiteTask | TaskData): string | undefined {
return task.description;
}
function getTaskStatus(task: LiteTask | TaskData): string {
return task.status;
}
function getFlowControl(task: LiteTask | TaskData): FlowControl | undefined {
if ('flow_control' in task) return task.flow_control;
return undefined;
}
// Status configuration
const taskStatusConfig: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info' | null; icon: React.ComponentType<{ className?: string }> }> = {
pending: {
@@ -113,17 +86,28 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
return null;
}
const taskId = getTaskId(task);
const taskTitle = getTaskTitle(task);
const taskDescription = getTaskDescription(task);
const taskStatus = getTaskStatus(task);
const flowControl = getFlowControl(task);
// Use NormalizedTask fields (works for both old nested and new flat formats)
const nt = task as NormalizedTask;
const taskId = nt.task_id || 'N/A';
const taskTitle = nt.title || 'Untitled Task';
const taskDescription = nt.description;
const taskStatus = nt.status;
const flowControl = buildFlowControl(nt);
// Normalized flat fields
const acceptanceCriteria = nt.convergence?.criteria || [];
const focusPaths = nt.focus_paths || [];
const dependsOn = nt.depends_on || [];
const preAnalysis = nt.pre_analysis || flowControl?.pre_analysis || [];
const implSteps = nt.implementation || flowControl?.implementation_approach || [];
const taskFiles = nt.files || flowControl?.target_files || [];
const taskScope = nt.scope;
const statusConfig = taskStatusConfig[taskStatus] || taskStatusConfig.pending;
const StatusIcon = statusConfig.icon;
const hasFlowchart = !!flowControl?.implementation_approach && flowControl.implementation_approach.length > 0;
const hasFiles = !!flowControl?.target_files && flowControl.target_files.length > 0;
const hasFlowchart = implSteps.length > 0;
const hasFiles = taskFiles.length > 0;
return (
<>
@@ -205,27 +189,27 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
)}
{/* Scope Section */}
{(task as LiteTask).meta?.scope && (
{taskScope && (
<div className="p-4 bg-card rounded-lg border border-border">
<h3 className="text-sm font-semibold text-foreground mb-2 flex items-center gap-2">
<span>📁</span>
Scope
</h3>
<div className="pl-3 border-l-2 border-primary">
<code className="text-sm text-foreground">{(task as LiteTask).meta?.scope}</code>
<code className="text-sm text-foreground">{taskScope}</code>
</div>
</div>
)}
{/* Acceptance Criteria Section */}
{(task as LiteTask).context?.acceptance && (task as LiteTask).context!.acceptance!.length > 0 && (
{/* Acceptance / Convergence Criteria Section */}
{acceptanceCriteria.length > 0 && (
<div className="p-4 bg-card rounded-lg border border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span></span>
{formatMessage({ id: 'liteTasks.acceptanceCriteria' })}
</h3>
<div className="space-y-2">
{(task as LiteTask).context!.acceptance!.map((criterion, i) => (
{acceptanceCriteria.map((criterion, i) => (
<div key={i} className="flex items-start gap-2">
<span className="text-muted-foreground mt-0.5"></span>
<span className="text-sm text-foreground">{criterion}</span>
@@ -236,14 +220,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
)}
{/* Focus Paths / Reference Section */}
{(task as LiteTask).context?.focus_paths && (task as LiteTask).context!.focus_paths!.length > 0 && (
{focusPaths.length > 0 && (
<div className="p-4 bg-card rounded-lg border border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>📚</span>
{formatMessage({ id: 'liteTasks.focusPaths' })}
</h3>
<div className="space-y-1">
{(task as LiteTask).context!.focus_paths!.map((path, i) => (
{focusPaths.map((path, i) => (
<code key={i} className="block text-xs bg-muted px-3 py-1.5 rounded text-foreground font-mono">
{path}
</code>
@@ -253,14 +237,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
)}
{/* Dependencies Section */}
{(task as LiteTask).context?.depends_on && (task as LiteTask).context!.depends_on!.length > 0 && (
{dependsOn.length > 0 && (
<div className="p-4 bg-card rounded-lg border border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>🔗</span>
{formatMessage({ id: 'liteTasks.dependsOn' })}
</h3>
<div className="flex flex-wrap gap-2">
{(task as LiteTask).context!.depends_on!.map((dep, i) => (
{dependsOn.map((dep, i) => (
<Badge key={i} variant="secondary">{dep}</Badge>
))}
</div>
@@ -268,14 +252,14 @@ export function TaskDrawer({ task, isOpen, onClose }: TaskDrawerProps) {
)}
{/* Pre-analysis Steps */}
{flowControl?.pre_analysis && flowControl.pre_analysis.length > 0 && (
{preAnalysis.length > 0 && (
<div className="p-4 bg-card rounded-lg border border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
<span>🔍</span>
{formatMessage({ id: 'sessionDetail.taskDrawer.overview.preAnalysis' })}
</h3>
<div className="space-y-3">
{flowControl.pre_analysis.map((step, index) => (
{preAnalysis.map((step, index) => (
<div key={index} className="flex items-start gap-3">
<span className="flex-shrink-0 flex items-center justify-center w-6 h-6 rounded-full bg-primary text-primary-foreground text-xs font-medium">
{index + 1}

View File

@@ -312,7 +312,8 @@ function transformBackendSession(
has_review: backendData.hasReview,
review,
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
tasks: ((backendSession as unknown as { tasks?: TaskData[] }).tasks || [])
.map(t => normalizeTask(t as unknown as Record<string, unknown>)),
};
}
@@ -1986,6 +1987,139 @@ export interface LiteTask {
updated_at?: string;
}
// ========== Normalized Task (Unified Flat Format) ==========
/**
* Normalized task type that unifies both old 6-field nested format
* and new unified flat format into a single interface.
*
* Old format paths → New flat paths:
* - context.acceptance[] → convergence.criteria[]
* - context.focus_paths[] → focus_paths[]
* - context.depends_on[] → depends_on[]
* - context.requirements[] → description
* - flow_control.pre_analysis[] → pre_analysis[]
* - flow_control.implementation_approach[] → implementation[]
* - flow_control.target_files[] → files[]
*/
export interface NormalizedTask extends TaskData {
// Promoted from context
focus_paths?: string[];
convergence?: {
criteria?: string[];
verification?: string;
definition_of_done?: string;
};
// Promoted from flow_control
pre_analysis?: PreAnalysisStep[];
implementation?: (ImplementationStep | string)[];
files?: Array<{ path: string; name?: string }>;
// Promoted from meta
type?: string;
scope?: string;
action?: string;
// Original nested objects (preserved for long-term compat)
flow_control?: FlowControl;
context?: {
focus_paths?: string[];
acceptance?: string[];
depends_on?: string[];
requirements?: string[];
};
meta?: {
type?: string;
scope?: string;
[key: string]: unknown;
};
// Raw data reference for JSON viewer / debugging
_raw?: unknown;
}
/**
* Normalize a raw task object (old 6-field or new unified flat) into NormalizedTask.
* Reads new flat fields first, falls back to old nested paths.
* Long-term compatible: handles both formats permanently.
*/
export function normalizeTask(raw: Record<string, unknown>): NormalizedTask {
if (!raw || typeof raw !== 'object') {
return { task_id: 'N/A', status: 'pending', _raw: raw } as NormalizedTask;
}
// Type-safe access helpers
const rawContext = raw.context as LiteTask['context'] | undefined;
const rawFlowControl = raw.flow_control as FlowControl | undefined;
const rawMeta = raw.meta as LiteTask['meta'] | undefined;
const rawConvergence = raw.convergence as NormalizedTask['convergence'] | undefined;
// Description: new flat field first, then join old context.requirements
const rawRequirements = rawContext?.requirements;
const description = (raw.description as string | undefined)
|| (Array.isArray(rawRequirements) && rawRequirements.length > 0
? rawRequirements.join('; ')
: undefined);
return {
// Identity
task_id: (raw.task_id as string) || (raw.id as string) || 'N/A',
title: raw.title as string | undefined,
description,
status: (raw.status as NormalizedTask['status']) || 'pending',
priority: raw.priority as NormalizedTask['priority'],
created_at: raw.created_at as string | undefined,
updated_at: raw.updated_at as string | undefined,
has_summary: raw.has_summary as boolean | undefined,
estimated_complexity: raw.estimated_complexity as string | undefined,
// Promoted from context (new first, old fallback)
depends_on: (raw.depends_on as string[]) || rawContext?.depends_on || [],
focus_paths: (raw.focus_paths as string[]) || rawContext?.focus_paths || [],
convergence: rawConvergence || (rawContext?.acceptance?.length
? { criteria: rawContext.acceptance }
: undefined),
// Promoted from flow_control (new first, old fallback)
pre_analysis: (raw.pre_analysis as PreAnalysisStep[]) || rawFlowControl?.pre_analysis,
implementation: (raw.implementation as (ImplementationStep | string)[]) || rawFlowControl?.implementation_approach,
files: (raw.files as Array<{ path: string; name?: string }>) || rawFlowControl?.target_files,
// Promoted from meta (new first, old fallback)
type: (raw.type as string) || rawMeta?.type,
scope: (raw.scope as string) || rawMeta?.scope,
action: (raw.action as string) || (rawMeta as Record<string, unknown> | undefined)?.action as string | undefined,
// Preserve original nested objects for backward compat
flow_control: rawFlowControl,
context: rawContext,
meta: rawMeta,
// Raw reference
_raw: raw,
};
}
/**
* Build a FlowControl object from NormalizedTask for backward-compatible components (e.g. Flowchart).
*/
export function buildFlowControl(task: NormalizedTask): FlowControl | undefined {
const preAnalysis = task.pre_analysis;
const implementation = task.implementation;
const files = task.files;
if (!preAnalysis?.length && !implementation?.length && !files?.length) {
return task.flow_control; // Fall back to original if no flat fields
}
return {
pre_analysis: preAnalysis || task.flow_control?.pre_analysis,
implementation_approach: implementation || task.flow_control?.implementation_approach,
target_files: files || task.flow_control?.target_files,
};
}
export interface LiteTaskSession {
id: string;
session_id?: string;