quantumbyteQB
§ · Nested detail page

需求 Harness 深度解析

Harness 把“应用能跑”提升为“业务要求可逐项验证”:需求可以自动派生,也允许人类锁定修改,结果则独立记录。

§ 00 · IMPORT CHAIN

导入链与契约

前后端之间的线格式由两个纯序列化函数钉住,Requirement 保留人工所有权标志,Result 暴露证据、验证 SHA、观察路径和过期状态,使 UI 不需要理解 Prisma 枚举 对应源码落点是 src/lib/harness.ts:8、src/lib/harness.ts:9、src/lib/harness.ts:10、src/lib/harness.ts:13、src/lib/harness.ts:15、src/lib/harness.ts:19、src/lib/harness.ts:20。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/lib/harness.ts · L8-29TypeScript · Python
   8  export function serializeRequirement(r: HarnessRequirement) {
   9    return {
  10      id: r.id,
  11      excerpt: r.excerpt,
  12      description: r.description,
  13      type: r.type.toLowerCase(),
  14      severity: r.severity.toLowerCase(),
  15      locked: r.locked,
  16    };
  17  }
  18  
  19  export function serializeResult(r: HarnessResult) {
  20    return {
  21      requirement_id: r.requirementId,
  22      result: r.result,
  23      evidence: r.evidence,
  24      run_status: r.runStatus,
  25      last_verified_sha: r.lastVerifiedSha,
  26      watched_paths: r.watchedPaths,
  27      run_at: r.runAt?.toISOString() ?? null,
  28      is_stale: r.isStale,
  29    };
§ 01 · CORE IMPL

核心实现,逐段拆

▸全量替换但不意外解锁

PUT 采用全量期望集合:缺失项删除,新项创建,已有项更新。输入先校验描述、类型和严重度,keptIds 决定删除边界,因此客户端必须提交完整集合 对应源码落点是 src/app/api/projects/[id]/harness/requirements/route.ts:26、src/app/api/projects/[id]/harness/requirements/route.ts:29、src/app/api/projects/[id]/harness/requirements/route.ts:39、src/app/api/projects/[id]/harness/requirements/route.ts:44、src/app/api/projects/[id]/harness/requirements/route.ts:51、src/app/api/projects/[id]/harness/requirements/route.ts:54、src/app/api/projects/[id]/harness/requirements/route.ts:62。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/app/api/projects/[id]/harness/requirements/route.ts · L62-96TypeScript · Python
  62    const keptIds = incoming.filter((r) => r.id).map((r) => r.id as string);
  63  
  64    const requirements = await prisma.$transaction(async (tx) => {
  65      await tx.harnessRequirement.deleteMany({
  66        where: { projectId: id, id: { notIn: keptIds } },
  67      });
  68      return Promise.all(
  69        incoming.map((r) => {
  70          const data = {
  71            excerpt: r.excerpt?.trim() || null,
  72            description: (r.description as string).trim(),
  73            type: (r.type?.toUpperCase() ?? 'EXPLICIT') as RequirementType,
  74            severity: (r.severity?.toUpperCase() ?? 'MEDIUM') as Severity,
  75          };
  76          // A create always claims manual ownership (the user typed it). An update
  77          // only changes lock state when the client explicitly sends one — true for an
  78          // edited row, false for a reset-to-auto; an omitted flag leaves the stored
  79          // value untouched, so a partial payload can never mass-lock the whole set.
  80          const lockedProvided = typeof r.locked === 'boolean';
  81          return r.id
  82            ? tx.harnessRequirement.update({
  83                where: { id: r.id },
  84                data: { ...data, ...(lockedProvided ? { locked: r.locked } : {}) },
  85              })
  86            : tx.harnessRequirement.create({
  87                data: { id: ulid(), projectId: id, ...data, locked: r.locked ?? true },
  88              });
  89        })
  90      );
  91    });
  92  
  93    return NextResponse.json({
  94      requirements: requirements.map(serializeRequirement),
  95    });
  96  }

▸发布门禁不是简单总通过

UI 先把缺结果、INCONCLUSIVE、SUCCESS 和 FAIL 归一为四种 verdict,再按严重度计算 gate。Critical 失败直接禁止发布,普通失败或关键项未验证则带警告 对应源码落点是 src/components/blueprint/harness-slide.tsx:69、src/components/blueprint/harness-slide.tsx:70、src/components/blueprint/harness-slide.tsx:71、src/components/blueprint/harness-slide.tsx:75、src/components/blueprint/harness-slide.tsx:76、src/components/blueprint/harness-slide.tsx:84、src/components/blueprint/harness-slide.tsx:85。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

src/components/blueprint/harness-slide.tsx · L69-113TypeScript · Python
  69  function verdictOf(result: Result | undefined): Verdict {
  70    if (!result || !result.run_at) return 'unverified';
  71    if (result.run_status === 'INCONCLUSIVE') return 'inconclusive';
  72    return result.result === 'SUCCESS' ? 'pass' : 'fail';
  73  }
  74  
  75  function computeGate(requirements: Requirement[], verdicts: Map<string, Verdict>) {
  76    if (requirements.length === 0) {
  77      return {
  78        label: 'No requirements yet',
  79        reason: 'Add business requirements to gate this app on.',
  80        grad: 'from-slate-500 to-slate-700',
  81        Icon: Question,
  82      };
  83    }
  84    const criticals = requirements.filter((r) => r.severity === 'critical');
  85    const anyCriticalFail = criticals.some((r) => verdicts.get(r.id) === 'fail');
  86    const anyFail = requirements.some((r) => verdicts.get(r.id) === 'fail');
  87    const allCriticalPass = criticals.every((r) => verdicts.get(r.id) === 'pass');
  88  
  89    if (anyCriticalFail) {
  90      return {
  91        label: 'Do not ship',
  92        reason: 'A critical requirement is failing.',
  93        grad: 'from-red-500 to-rose-700',
  94        Icon: Prohibit,
  95      };
  96    }
  97    if (anyFail || !allCriticalPass) {
  98      return {
  99        label: 'Ship with warnings',
 100        reason: anyFail
 101          ? 'Non-critical requirements are failing.'
 102          : 'Critical requirements are not verified yet.',
 103        grad: 'from-amber-500 to-orange-600',
 104        Icon: Warning,
 105      };
 106    }
 107    return {
 108      label: 'Safe to ship',
 109      reason: 'Every critical requirement passes.',
 110      grad: 'from-emerald-500 to-green-700',
 111      Icon: RocketLaunch,
 112    };
 113  }
§ 02 · KEY FLOW

关键流程

auto-derived requirements + human edits → locked set
run request → read-only audit → result → gate

运行按钮只写持久请求标志并返回 202;如果已经运行就不重复请求。这让按钮动作能够跨 Worker 调度延迟生存,同时保持幂等,最终结果仍从共享 SSE 返回 对应源码落点是 src/app/api/projects/[id]/harness/run/route.ts:4、src/app/api/projects/[id]/harness/run/route.ts:7、src/app/api/projects/[id]/harness/run/route.ts:9、src/app/api/projects/[id]/harness/run/route.ts:14、src/app/api/projects/[id]/harness/run/route.ts:18、src/app/api/projects/[id]/harness/run/route.ts:22、src/app/api/projects/[id]/harness/run/route.ts:23。把这些相邻行放在同一条执行链里看,可以确认它不是文档里的愿景描述,而是已经落到请求、状态和界面反馈中的实现;同时也能辨认出失败时由哪一层收敛、下一次刷新从何处恢复,以及扩容后最先需要替换的边界。

§ 03 · EDGE CASES

边界 / 失败模式

  • 全量 PUT 要求客户端处理并发编辑,否则旧快照可能覆盖新修改。
  • INCONCLUSIVE 不等于 FAIL,门禁会保持未验证语义。
  • 人工 locked 需求只有显式 reset 才交回自动派生。
Sub-page · 2026-07-15 Source · QuantumByteOSS/quantumbyte ↑ quantumbyte 主页面