@tea-agent/loop-agent 0.17.2 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,56 +2,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [0.17.2] - 2026-07-22
6
-
7
- ### 改进
8
-
9
- - Operator Console Happy Path 补齐:主导航(任务/恢复/模型与认证/设置)、newTask / importPrd、grill-me Interview(一次一问 + assessment)、SSE 运行跟随;Pi readiness 可刷新。
10
- - Operator Console 页面交互文案改为中文(导航、向导步骤、按钮与状态说明);产品名与 CLI 命令仍保留英文术语。
11
- - Operator Console 界面与交互美化:暖色画布、胶囊导航、步骤进度条、统一按钮层级与状态条,运行面板元信息更清晰。
12
- - Operator Console Happy Path 增加「流程类型」选择(后端研发 / 前端研发 / 后端测试 / 前端测试),写入 `taskKind`;访谈枚举同步支持 `frontend-implementation` 与 `frontend-test`。
13
- - Console Happy Path 去掉单独的「创建任务」步骤:导入需求时根据 PRD 标题自动生成任务名称与 canonical taskId,并一键创建任务 + import-prd。
14
- - Operator Console 需求访谈可跳过:一键按 grill-me 推荐补齐草稿并签发评估,可直接进入契约校验。
15
- - Operator Console 将「校验契约 / 应用契约 / Spine 审计」合并为「契约落地」:自动确定性校验与差异,人工确认一次应用,成功后自动 Spine。
16
- - Observe 运行看板视觉对齐 Console:暖色渐变画布、胶囊导航、柔和面板阴影与状态点,减少高饱和色块;进一步统一 badge/表格/风险块/时间线/DAG 图与 inspector、对象关系条与推荐命令等视图组件。
5
+ - `backend-test` active runtime/template 改为 8 节点 Markdown-first 流程:环境 Shell 先检查 Python/pytest、配置/fixture/target 和 HTML renderer;随后生成并独立 Review Markdown 用例,经过确定性 Markdown/traceability gate 后只执行一次 pytest,同时生成 JUnit、self-contained HTML 和 Markdown facts,最终输出 Markdown 测试报告与 L-5。新流程不再要求模型生成 Analysis/Execution/Manifest/Classification/Result/L-5 业务 JSON,历史 JSON artifacts 仍保留读取兼容。
17
6
 
18
7
  ### 修复
19
8
 
20
- - Console Draft 在 validate/diff/apply 前会投影为 CLI 严格 schema:补齐 `constraints.invariants`,并去掉仅 Console 使用的 `updatedAt`,避免误报 draft schema validation failed。
21
- - 修复访谈完成后绑定 `interviewAssessmentRef` 会立刻让 assessment 过期的问题:`draftSha256` 不再纳入该绑定字段;finalize 时先清空 `openQuestions` 再签发 assessment。
22
- - 修复 Console `contractApply` 默认传入 `expectedObservedHash=unknown` 导致 OBSERVED_HASH_CONFLICT / 页面只见 `exit 1`:改为先 `task contract show` 再传真实 revision/hash;异步操作失败时展示 CLI 错误信息。
23
- - 修复应用契约后界面卡住:轮询操作结果时误读 `{ ok, operation }` 响应,一直等到超时且无法进入 Spine
24
- - 修复 DAG 确认准备失败:不再提交空 stub DAG;`prepareDagConfirmation` 会经 `dag run-task` 生成真实 DAG;`dag validate` 接受 `--json` 兼容 Console
25
- - 修复 Console `runDag` `run-dag --json` 未识别而失败:CLI 现接受该旗标(输出本就为 JSON)。
26
- - 运行 DAG 不再用短超时阻塞整页:`runDag` 改为后台接受 + SSE 跟随;Observe 按钮与任务 busy 解耦,运行中可打开。
27
- - Console 打开 Observe 时,若只给 taskId,会解析该任务最新 DAG run 并跳转 `#/dag/<runId>`,不再误开 `#/task/<taskId>`。
28
-
29
- ## [0.17.1] - 2026-07-22
30
-
31
- ### 修复
32
-
33
- - TaskSpec materialize 在写入 task.json、需求和约束后会自动 adopt 或刷新 managed Task Contract,避免生成 v4 DAG 后立刻因 binding drift 无法 validate/run。
34
- - Operator Console 的 Pi readiness 改为真实探测 SDK、认证和可用模型;本机已配置 Pi 时不再误报 `setup-required`,界面同步对齐 Observe 暖白画布。
35
-
36
- ## [0.17.0] - 2026-07-22
37
-
38
- ### 新增
39
-
40
- - streamline report-first workflow (#53)
41
- - add operator console, task contract, and harden bindings (#51)
42
- - condition DAG design and repair branches
43
-
44
- ### 修复
45
-
46
- - synchronize console frontend lockfile (#52)
47
- - tolerate mock strategy format drift
48
- - bound executor output
49
-
50
- ## [Unreleased]
51
-
52
- - 将 `backend-test` runtime 与打包模板收敛为报告优先的 12 节点 DAG:case review 改为 advisory evidence,移除 pytest semantic review/gates 与最终 outcome gate,保留 pytest 前确定性 traceability hard gate、单次 pytest、canonical context/retrospective/L-5;Analysis v2 对 string evidence gap 与 `id` alias 做窄 canonicalize,同时继续严格拒绝未知键、错类型、secret 与多 JSON envelope。
53
-
54
- ### 修复
9
+ - `backend-test` Markdown 用例生成与独立审查现在默认采用简体中文:用例标题、策略、前置条件、测试数据、步骤、预期结果、自动化说明、表头和证据缺口优先中文表达;Case/AC/REQ/BR ID、固定章节标题、HTTP 方法、路径、字段名、枚举值、命令和代码符号继续保持精确机器契约,不做无意义双语翻译。
10
+ - `backend-test` traceability 现在识别带 `-> None` 等返回类型 annotation 以及可选 `async` pytest 函数,不再把合法 typed test function 全部误报为缺失映射。
11
+ - `backend-test` Markdown gate 只要求 case-level AC;pytest 执行、MD↔pytest 追溯、JUnit/HTML/stdout/stderr 和失败分类等下游证据 AC 交给 traceability、execute report 节点,不再被错误要求写入单条 Markdown case。
12
+ - `backend-test` Markdown gate `Steps` 仍要求编号列表;`Expected Results` 现在接受编号列表或 Markdown bullet 列表,只要保持结构化可断言,不再误拒绝模型生成的合法 bullet 断言;普通散文仍 fail-closed
13
+ - `backend-test` pre-run Spine 审计不再要求尚未执行的 `artifacts/requirement-coverage.json`:未覆盖需求在该 taskKind 下记为 warning,并交给运行内 Markdown validation traceability gate 确定性阻断;普通实现任务仍保持 error/fail-closed
14
+ - `backend-test` Markdown 用例门禁现在从 DAG `sourceBinding` 精确识别 `Source References`,支持反引号以及中英文括号/标点,不再将说明文字吞入路径;未绑定引用、路径前缀伪装和绑定文件缺失仍然 fail-closed。
15
+ - `backend-test` Markdown writer/reviewer 的 reference index 现在同时提供 task-relative 引用路径与 repo-relative `readPath`,Pi 会直接只读 `.harness/tasks/<taskId>/source/**` 的绑定文件,不再把 `source/references/**` 误读为仓库根路径,也不会在读取失败后回退到 `docs/**` 替代文件。
16
+ - `backend-test` Markdown 用例门禁会通过 DAG `sourceBinding` `source/需求.md`、`source/执行约束.md` `source/references/**` 解析到对应任务目录,不再错误地相对仓库根查找,也不会接受未绑定的同名仓库文件。
17
+ - `backend-test` traceability 门禁现在优先用 Markdown 中已知 Case ID 匹配 `test_BE_<MODULE>_<NNN>_` 函数前缀,不再把函数描述中的 `100`、`400`、`404` 等三位数字误识别为 Case ID。
18
+ - `backend-test` traceability 门禁允许仅包裹 cleanup 调用的 `except requests.RequestException: pass`,避免 best-effort 资源清理被误报;bare/宽异常、`AssertionError` 及非 cleanup 主体仍然 fail-closed。
55
19
 
56
20
  - DAG runtime 的 task-contract 启动门禁现在只将 `writePolicy: "exclusive"` 视为 writer execution:历史只读/static/shell DAG 可继续执行,legacy writer DAG 仍必须升级为 DagSpec v4 并绑定 managed Task Contract;真实 DAG 生成会冻结当前 managed binding。测试执行 fixture、Worker materializer 和 Windows npm/tmp 路径也已同步适配该契约。
57
21
  - Pi CLI / SDK executor 不再把单个节点的完整 stdout 累积为一个 JS 字符串;长 `frontend-plan-pi` 或大段结构化输出会保留有界预览并继续流式解析最终 assistant 事件,避免触发 V8 `Invalid string length` 或卡死。
@@ -60,6 +24,8 @@
60
24
 
61
25
  ### 改进
62
26
 
27
+ - `backend-test` 的 source intake 能力已并入 Markdown 用例生成/Review:派生 `需求.md` / `执行约束.md` 有界内联,reference 使用 path/SHA-256/角色线索索引并按需精确读取;pytest writer 不再读取 `source/**`。
28
+
63
29
  - **Console Phase 3(Observe 协作)**:
64
30
  - Observe `GET /api/health` 升级为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package name/version、由 ROUTES 派生的 `routeCapabilities`)。
65
31
  - Console `observeLink` fail-closed:仅 health **match**(同仓 fingerprint + schema + 目标 kind 所需 capability)时返回深链;offline / mismatch 返回诊断与 `agent-worker observe serve --repo . --port 8787`,不打开错误 URL。
@@ -91,6 +57,8 @@
91
57
 
92
58
  ### 修复
93
59
 
60
+ - Init evolution 高影响校验现在覆盖升级后的 init surface notifier、eligibility policy、runtime activity gate 与 CLI entry 接线;这些专用路径单独变化时会触发 `model-review`,不再以 `no trigger matched` 静默跳过审查,同时不扩大到通用 CLI program/command registry 文件。
61
+
94
62
  - Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
95
63
  - Observe 节点检查器的“规范证据”现在会按任务目录解析 `sourceBinding` 中的相对路径,`source/需求.md` 等已绑定文件可正常打开;路径成员校验、目录穿越、绝对路径、二进制文件和符号链接逃逸防护保持不变。
96
64
  - Decision Gate 的 Pi 执行指令不再与输出合同冲突:启用 `decisionGate` 的只读节点会明确输出唯一的 `DECISION_ENVELOPE_JSON` fenced block,普通只读节点仍保持原有无代码围栏约束,严格解析规则不放宽。
@@ -115,6 +83,83 @@
115
83
  - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
116
84
  - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为唯一 JUnit/initial Result、canonical Result、traceability 与 Observe 投影保留结构化运行证据。
117
85
 
86
+ ## [0.18.0] - 2026-07-22
87
+
88
+ ### 重点更新
89
+
90
+ - 后端测试全面采用 Markdown 优先工作流,整合环境预检、独立审查与确定性门禁,单次执行即可生成完整测试报告。
91
+ - 前端实现与测试流程大幅优化,按需执行计划修订与验证,常规全绿路径执行节点数量显著减少。
92
+ - 修复多项严格校验导致的误中断问题,大幅提升 Markdown 用例生成、追踪映射与异常清理的兼容性。
93
+ - 优化 Pi CLI 与 SDK 执行器,彻底解决长文本输出导致的内存溢出与进程卡死问题。
94
+
95
+ ### 新增
96
+
97
+ - 引入 Markdown 优先的后端测试工作流,支持环境预检、独立审查用例并生成自包含的 HTML 与 Markdown 报告。
98
+ - 新增升级后的初始化面通知器,在检测到版本不一致时自动提示安全更新或引导人工处理。
99
+ - 新增统一的初始化协调命令,支持安全应用更新并智能处理各类阻断情况。
100
+ - 测试报告新增 L-5 指标消费与统一的代码覆盖率报告入口,支持 Python 与 Java 测试数据。
101
+
102
+ ### 改进
103
+
104
+ - 前端实现绿色路径改为按需分支执行,常规无错路径可少执行多个节点,提升执行效率。
105
+ - 前端测试用例生成现可自动解析明确的 baseUrl,不再保留占位符,避免执行异常。
106
+ - 前端测试复盘报告写入路径调整,支持安全禁止整个 docs 目录的写入权限。
107
+ - 夜间自动发布机制优化为双基线检测,手动补丁不再抹除同周期的功能信号。
108
+ - GitHub Actions CI 优化执行策略,合并任务并跳过纯文档变更,有效降低私有仓库分钟消耗。
109
+
110
+ ### 修复
111
+
112
+ - 修复 Markdown 用例追踪映射将 HTTP 状态码等数字误识别为用例 ID,以及无法正确识别带返回类型标注的测试函数的问题。
113
+ - 修复 Markdown 用例门禁误拒绝合法的列表断言,以及无法正确解析相对路径绑定文件的问题。
114
+ - 修复 Pi CLI 执行器将单个节点完整标准输出累积为单一字符串,导致长文本输出时内存溢出或卡死的问题。
115
+ - 修复 Windows 环境下因清理环境变量导致 Python 和 pytest 无法正确发现用户级依赖的问题。
116
+ - 修复初始化演进的高影响校验未覆盖专用路径,导致必要审查被静默跳过的问题。
117
+
118
+ ## [0.17.2] - 2026-07-22
119
+
120
+ ### 改进
121
+
122
+ - Operator Console Happy Path 补齐:主导航(任务/恢复/模型与认证/设置)、newTask / importPrd、grill-me Interview(一次一问 + assessment)、SSE 运行跟随;Pi readiness 可刷新。
123
+ - Operator Console 页面交互文案改为中文(导航、向导步骤、按钮与状态说明);产品名与 CLI 命令仍保留英文术语。
124
+ - Operator Console 界面与交互美化:暖色画布、胶囊导航、步骤进度条、统一按钮层级与状态条,运行面板元信息更清晰。
125
+ - Operator Console Happy Path 增加「流程类型」选择(后端研发 / 前端研发 / 后端测试 / 前端测试),写入 `taskKind`;访谈枚举同步支持 `frontend-implementation` 与 `frontend-test`。
126
+ - Console Happy Path 去掉单独的「创建任务」步骤:导入需求时根据 PRD 标题自动生成任务名称与 canonical taskId,并一键创建任务 + import-prd。
127
+ - Operator Console 需求访谈可跳过:一键按 grill-me 推荐补齐草稿并签发评估,可直接进入契约校验。
128
+ - Operator Console 将「校验契约 / 应用契约 / Spine 审计」合并为「契约落地」:自动确定性校验与差异,人工确认一次应用,成功后自动 Spine。
129
+ - Observe 运行看板视觉对齐 Console:暖色渐变画布、胶囊导航、柔和面板阴影与状态点,减少高饱和色块;进一步统一 badge/表格/风险块/时间线/DAG 图与 inspector、对象关系条与推荐命令等视图组件。
130
+
131
+ ### 修复
132
+
133
+ - Console Draft 在 validate/diff/apply 前会投影为 CLI 严格 schema:补齐 `constraints.invariants`,并去掉仅 Console 使用的 `updatedAt`,避免误报 draft schema validation failed。
134
+ - 修复访谈完成后绑定 `interviewAssessmentRef` 会立刻让 assessment 过期的问题:`draftSha256` 不再纳入该绑定字段;finalize 时先清空 `openQuestions` 再签发 assessment。
135
+ - 修复 Console `contractApply` 默认传入 `expectedObservedHash=unknown` 导致 OBSERVED_HASH_CONFLICT / 页面只见 `exit 1`:改为先 `task contract show` 再传真实 revision/hash;异步操作失败时展示 CLI 错误信息。
136
+ - 修复应用契约后界面卡住:轮询操作结果时误读 `{ ok, operation }` 响应,一直等到超时且无法进入 Spine。
137
+ - 修复 DAG 确认准备失败:不再提交空 stub DAG;`prepareDagConfirmation` 会经 `dag run-task` 生成真实 DAG;`dag validate` 接受 `--json` 兼容 Console。
138
+ - 修复 Console `runDag` 因 `run-dag --json` 未识别而失败:CLI 现接受该旗标(输出本就为 JSON)。
139
+ - 运行 DAG 不再用短超时阻塞整页:`runDag` 改为后台接受 + SSE 跟随;Observe 按钮与任务 busy 解耦,运行中可打开。
140
+ - Console 打开 Observe 时,若只给 taskId,会解析该任务最新 DAG run 并跳转 `#/dag/<runId>`,不再误开 `#/task/<taskId>`。
141
+
142
+ ## [0.17.1] - 2026-07-22
143
+
144
+ ### 修复
145
+
146
+ - TaskSpec materialize 在写入 task.json、需求和约束后会自动 adopt 或刷新 managed Task Contract,避免生成 v4 DAG 后立刻因 binding drift 无法 validate/run。
147
+ - Operator Console 的 Pi readiness 改为真实探测 SDK、认证和可用模型;本机已配置 Pi 时不再误报 `setup-required`,界面同步对齐 Observe 暖白画布。
148
+
149
+ ## [0.17.0] - 2026-07-22
150
+
151
+ ### 新增
152
+
153
+ - streamline report-first workflow (#53)
154
+ - add operator console, task contract, and harden bindings (#51)
155
+ - condition DAG design and repair branches
156
+
157
+ ### 修复
158
+
159
+ - synchronize console frontend lockfile (#52)
160
+ - tolerate mock strategy format drift
161
+ - bound executor output
162
+
118
163
  ## [0.16.26] - 2026-07-22
119
164
 
120
165
  ### 修复
@@ -14,7 +14,8 @@ import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "
14
14
  import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
15
15
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
16
16
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
17
- import { materializeBackendTestResultFromRunDir } from "../workflows/dag/backend-test-result-contract.js";
17
+ import { materializeBackendTestResultFromRunDir, parseJunitXml } from "../workflows/dag/backend-test-result-contract.js";
18
+ import { inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
18
19
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
19
20
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
20
21
  import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
@@ -308,7 +309,66 @@ async function executeBackendTestPipeline(input, meta) {
308
309
  if (!pipeline)
309
310
  throw new Error("missing backend-test pipeline id");
310
311
  const outputs = [];
311
- if (pipeline === "contracts") {
312
+ if (pipeline === "markdown-environment") {
313
+ const results = await executePipelineCommands(input, meta);
314
+ if (!results.every((result) => result.ok)) {
315
+ const failure = results.find((result) => !result.ok);
316
+ return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
317
+ }
318
+ const environment = await inspectBackendTestEnvironment({
319
+ workspaceRoot: input.cwd,
320
+ pythonVersion: results[0]?.stdout ?? "unknown",
321
+ pytestVersion: results[1]?.stdout ?? "unknown",
322
+ pytestHelp: results[2]?.stdout ?? "",
323
+ });
324
+ const reportPath = await writeRunReport(meta.runDir, "backend-test-environment.md", environment.markdown);
325
+ outputs.push(`environment=${reportPath}`, environment.markdown);
326
+ }
327
+ else if (pipeline === "markdown-cases") {
328
+ const environmentMarkdown = await readFile(path.join(meta.runDir, "reports", "backend-test-environment.md"), "utf8");
329
+ const requiredRequirementIds = requiredBackendMarkdownCaseAcIds([
330
+ meta.spec.objective ?? "",
331
+ ...(meta.spec.successCriteria ?? []),
332
+ ]);
333
+ const report = await validateBackendMarkdownCases({
334
+ workspaceRoot: input.cwd,
335
+ environmentMarkdown,
336
+ requiredRequirementIds,
337
+ sourceBinding: meta.spec.sourceBinding,
338
+ });
339
+ const reportPath = await writeRunReport(meta.runDir, "backend-md-case-validation.md", report);
340
+ outputs.push(`caseValidation=${reportPath}`, report);
341
+ }
342
+ else if (pipeline === "markdown-traceability") {
343
+ const report = await validateBackendMarkdownTraceability(input.cwd);
344
+ const reportPath = await writeRunReport(meta.runDir, "backend-test-traceability.md", report);
345
+ outputs.push(`traceability=${reportPath}`, report);
346
+ }
347
+ else if (pipeline === "markdown-execute-html") {
348
+ const results = await executePipelineCommands(input, meta);
349
+ if (!results.every((result) => result.ok)) {
350
+ const failure = results.find((result) => !result.ok);
351
+ return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
352
+ }
353
+ const reportsDir = path.join(meta.runDir, "reports");
354
+ const junitContent = await readFile(path.join(reportsDir, "backend-test.junit.xml"), "utf8");
355
+ const pytestExitCode = Number.parseInt((await readFile(path.join(reportsDir, "backend-test-pytest-exit.txt"), "utf8")).trim(), 10);
356
+ if (![0, 1].includes(pytestExitCode))
357
+ throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
358
+ const parsed = parseJunitXml(junitContent);
359
+ const htmlContent = renderBackendTestHtml({
360
+ title: meta.spec.title,
361
+ parsed,
362
+ environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
363
+ traceabilitySummary: await readFile(path.join(reportsDir, "backend-test-traceability.md"), "utf8"),
364
+ });
365
+ const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
366
+ const facts = renderBackendTestFacts({ parsed, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
367
+ const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
368
+ const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
369
+ outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `facts=${factsPath}`, facts);
370
+ }
371
+ else if (pipeline === "contracts") {
312
372
  const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
313
373
  const wrapper = JSON.parse(await readFile(wrapperPath, "utf8"));
314
374
  const raw = wrapper.assistantText?.trim() || wrapper.stdout?.trim() || "";
@@ -85,10 +85,13 @@ export async function runSpineAudit(repoRoot, taskId) {
85
85
  }
86
86
  const requirementCoverage = await runRequirementCoverageAudit(repoRoot, taskId);
87
87
  for (const reason of requirementCoverageFailureReasons(requirementCoverage)) {
88
+ const backendTestDeferred = config.taskKind === "backend-test";
88
89
  findings.push({
89
- severity: "error",
90
+ severity: backendTestDeferred ? "warning" : "error",
90
91
  type: "uncovered-requirement",
91
- message: reason,
92
+ message: backendTestDeferred
93
+ ? `${reason}; deferred to backend-test Markdown validation and traceability gates`
94
+ : reason,
92
95
  });
93
96
  }
94
97
  const adapter = await resolveAdapter(repoRoot);
@@ -0,0 +1,148 @@
1
+ import { createHash } from "node:crypto";
2
+ import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const MAX_SOURCE_EXCERPT_CHARS = 2000;
5
+ export const BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES = [
6
+ ".git",
7
+ ".harness",
8
+ ".env",
9
+ "node_modules",
10
+ "vendor",
11
+ "dist",
12
+ "build",
13
+ "coverage",
14
+ ".venv",
15
+ "venv",
16
+ "__pycache__",
17
+ ];
18
+ const DISCOVERY_CANDIDATES = {
19
+ pytestConfig: ["pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini"],
20
+ conftest: [
21
+ "conftest.py",
22
+ "tests/conftest.py",
23
+ "test/conftest.py",
24
+ "testcase/conftest.py",
25
+ ],
26
+ testRoot: ["tests", "test", "testcase", "src/tests", "app/tests"],
27
+ package: [
28
+ "package.json",
29
+ "pyproject.toml",
30
+ "requirements.txt",
31
+ "requirements-dev.txt",
32
+ "Pipfile",
33
+ "poetry.lock",
34
+ ],
35
+ serverEntry: [
36
+ "server.py",
37
+ "app.py",
38
+ "main.py",
39
+ "manage.py",
40
+ "src/server.py",
41
+ "src/main.py",
42
+ "app/main.py",
43
+ "server.js",
44
+ "server.ts",
45
+ "src/server.js",
46
+ "src/server.ts",
47
+ ],
48
+ };
49
+ function taskRelative(sources, filePath) {
50
+ return path.relative(sources.taskDir, filePath).replaceAll(path.sep, "/");
51
+ }
52
+ function repoRelativeReadPath(sources, filePath) {
53
+ if (!sources.repoRoot) {
54
+ return filePath.replaceAll(path.sep, "/");
55
+ }
56
+ const relative = path.relative(sources.repoRoot, filePath);
57
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
58
+ throw new Error(`backend-test reference escapes repository root: ${filePath}`);
59
+ }
60
+ return relative.replaceAll(path.sep, "/");
61
+ }
62
+ function boundedExcerpt(markdown, sourcePath) {
63
+ const trimmed = markdown.trim();
64
+ if (trimmed.length <= MAX_SOURCE_EXCERPT_CHARS)
65
+ return trimmed;
66
+ const omitted = trimmed.length - MAX_SOURCE_EXCERPT_CHARS;
67
+ return `${trimmed.slice(0, MAX_SOURCE_EXCERPT_CHARS)}\n\n...[truncated for backend-test intake: showing first ${MAX_SOURCE_EXCERPT_CHARS} of ${trimmed.length} chars; omitted ${omitted} chars]\nFull derived source: ${sourcePath}`;
68
+ }
69
+ function inferRoleHints(referencePath) {
70
+ const normalized = referencePath.toLowerCase();
71
+ const hints = [];
72
+ const add = (hint) => {
73
+ if (!hints.includes(hint))
74
+ hints.push(hint);
75
+ };
76
+ if (/(acceptance|验收|criteria|ac[._-])/.test(normalized))
77
+ add("acceptance-criteria");
78
+ if (/(api|openapi|swagger|endpoint|接口)/.test(normalized))
79
+ add("api-contract");
80
+ if (/(schema|model|field|字段|数据)/.test(normalized))
81
+ add("data-contract");
82
+ if (/(rule|constraint|policy|规则|约束)/.test(normalized))
83
+ add("business-rule");
84
+ if (/(test|pytest|测试)/.test(normalized))
85
+ add("test-evidence");
86
+ if (hints.length === 0)
87
+ add("source-fact");
88
+ return hints;
89
+ }
90
+ async function discoverCandidatePaths(repoRoot) {
91
+ const candidates = {
92
+ pytestConfig: [],
93
+ conftest: [],
94
+ testRoot: [],
95
+ package: [],
96
+ serverEntry: [],
97
+ };
98
+ if (!repoRoot) {
99
+ return {
100
+ mode: "deterministic-fallback",
101
+ candidates,
102
+ excludedDirectories: [...BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES],
103
+ };
104
+ }
105
+ for (const [kind, paths] of Object.entries(DISCOVERY_CANDIDATES)) {
106
+ for (const candidate of paths) {
107
+ try {
108
+ await access(path.join(repoRoot, candidate));
109
+ candidates[kind].push(candidate);
110
+ }
111
+ catch {
112
+ // A missing bounded candidate is not evidence and is omitted.
113
+ }
114
+ }
115
+ }
116
+ return {
117
+ mode: "filesystem",
118
+ candidates,
119
+ excludedDirectories: [...BACKEND_TEST_DISCOVERY_EXCLUDED_DIRECTORIES],
120
+ };
121
+ }
122
+ export async function buildBackendTestIntakeContext(sources) {
123
+ const requirementPath = taskRelative(sources, sources.requirementPath);
124
+ const constraintPath = taskRelative(sources, sources.constraintPath);
125
+ const parts = [
126
+ "## Derived task contract: 需求.md",
127
+ boundedExcerpt(sources.requirementMarkdown, requirementPath),
128
+ ];
129
+ if (sources.constraintMarkdown) {
130
+ parts.push("## Derived execution constraints: 执行约束.md", boundedExcerpt(sources.constraintMarkdown, constraintPath));
131
+ }
132
+ const referenceIndex = (sources.referenceDocuments ?? [])
133
+ .map((reference) => {
134
+ const relativePath = taskRelative(sources, reference.path);
135
+ return {
136
+ path: relativePath,
137
+ readPath: repoRelativeReadPath(sources, reference.path),
138
+ sha256: createHash("sha256").update(reference.markdown, "utf8").digest("hex"),
139
+ roleHints: inferRoleHints(relativePath),
140
+ };
141
+ })
142
+ .sort((left, right) => left.path.localeCompare(right.path));
143
+ return {
144
+ boundedSourceContext: parts.join("\n\n"),
145
+ referenceIndex,
146
+ projectDiscoverySeed: await discoverCandidatePaths(sources.repoRoot),
147
+ };
148
+ }