@tea-agent/loop-agent 0.38.1 → 0.38.3

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.
Files changed (180) hide show
  1. package/CHANGELOG.md +126 -12
  2. package/README.md +2 -0
  3. package/bin/loop-agent.js +7 -3
  4. package/dist/application/dag/generate-task-dag.js +9 -0
  5. package/dist/application/task-lifecycle/advance.js +4 -0
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/commands/init.js +2 -14
  8. package/dist/commands/task-advance.js +31 -0
  9. package/dist/executors/dag-pi-executor.js +99 -5
  10. package/dist/executors/pi-event-serializer.js +42 -1
  11. package/dist/executors/pi-executor.js +51 -0
  12. package/dist/executors/pi-extension-resolver.js +233 -0
  13. package/dist/executors/pi-sdk-executor.js +187 -59
  14. package/dist/executors/shell-executor.js +324 -16
  15. package/dist/executors/shell-write-guard.js +7 -0
  16. package/dist/governance/manifest-types.js +0 -2
  17. package/dist/shared/operator/capabilities.js +87 -22
  18. package/dist/task/config-types.js +22 -0
  19. package/dist/task/contract/constants.js +1 -0
  20. package/dist/task/contract/diff.js +26 -0
  21. package/dist/task/contract/project.js +5 -0
  22. package/dist/task/contract/schema.js +6 -1
  23. package/dist/task/source-prepare/build-draft.js +15 -0
  24. package/dist/worker/console/chat/deferred-turn.js +91 -0
  25. package/dist/worker/console/chat/pi-runtime.js +10 -5
  26. package/dist/worker/console/chat/routes.js +458 -47
  27. package/dist/worker/console/chat/semantic-activity.js +13 -13
  28. package/dist/worker/console/chat/session-store.js +146 -2
  29. package/dist/worker/console/harness-pi-model-matrix.js +2 -11
  30. package/dist/worker/console/operator-actions.js +5 -4
  31. package/dist/worker/console/pi-readiness.js +5 -4
  32. package/dist/worker/console/recovery-cta.js +4 -4
  33. package/dist/worker/console/recovery-error-copy.js +4 -4
  34. package/dist/worker/console/server.js +6 -1
  35. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-Hfsot6rP.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
  36. package/dist/worker/console/static/assets/{arc-YGwEASin.js → arc-Bj2M8iO5.js} +1 -1
  37. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CmOM0F92.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
  38. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-DDLuaHC5.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
  39. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B0Um43W4.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
  40. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
  41. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DvBN4JZc.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-B1UVgW4E.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Cf0GgXzX.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-DbNA24sj.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Bi_0Be06.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
  46. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-C8pPGvRd.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
  47. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BitQY8Z-.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
  48. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-U67GCiEo.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
  49. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-IoJwXPBA.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
  50. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-YDZ_UHgE.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
  51. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
  52. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
  53. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CUZgsiRC.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
  54. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-COmCh4pT.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
  55. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Ta6FSm68.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
  56. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UdcR-VW9.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
  57. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DybcO3Et.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
  58. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-wErDFWLw.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
  59. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DkVHZWqz.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
  60. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DdqWs7QN.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
  61. package/dist/worker/console/static/assets/{diagram-UB23O5K3-C9dMVmDo.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
  62. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-B2T2WcBK.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
  63. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt9rjDFq.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
  64. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-U8ic_tvi.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
  65. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BfQJdQoO.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
  66. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-CpMftH4O.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
  67. package/dist/worker/console/static/assets/index-DTZOKgAn.js +404 -0
  68. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
  69. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DzqWXNO_.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
  70. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-CJmGQ5Yp.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
  71. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-ChPtKFLz.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
  72. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-PRe7wB5u.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
  73. package/dist/worker/console/static/assets/{linear-DpKX6QNp.js → linear-DdnapKIH.js} +1 -1
  74. package/dist/worker/console/static/assets/{mermaid.core-Dnl76b7Y.js → mermaid.core-BVjAT9b8.js} +5 -5
  75. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-S9PCXc3J.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
  76. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Du8e-FSX.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
  77. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CY6OmHTc.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
  78. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-C65GPhpj.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
  79. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-U6IUiTCL.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
  80. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D-I_9Qhi.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
  81. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B57Fhed0.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
  82. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CJOtiw21.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
  83. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-FyX8zxSh.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
  84. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-eF-pbs9t.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
  85. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
  86. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CcfkA-Dq.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
  87. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
  88. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DH8FjnOK.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
  89. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-beoTagJg.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
  90. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CQSfw4cK.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
  91. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CQgzQ6jn.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
  92. package/dist/worker/console/static/index.html +2 -2
  93. package/dist/worker/console/static-src/app/console-types.js +0 -12
  94. package/dist/worker/console/static-src/app/useOperatorActions.js +1 -1
  95. package/dist/worker/console/static-src/app/useRecoveryActions.js +2 -2
  96. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +38 -1
  97. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  98. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +8 -1
  99. package/dist/worker/console/static-src/operator-chat/turn-submission.js +42 -0
  100. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +3 -0
  101. package/dist/worker/console/static-src/operator-chat/useChatStream.js +204 -11
  102. package/dist/worker/console/static-src/operator-chat/useChatThread.js +23 -1
  103. package/dist/worker/observe/node-input.js +72 -3
  104. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  105. package/dist/worker/observe/static/state.js +2 -2
  106. package/dist/worker/observe/static/views/dag-inspector.js +51 -0
  107. package/dist/worker/observe/static/views/session-timeline.js +18 -5
  108. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +809 -53
  109. package/dist/workflows/dag/backend-test-case-manifest.js +4 -0
  110. package/dist/workflows/dag/backend-test-execution-contract.js +18 -9
  111. package/dist/workflows/dag/backend-test-gap-fill.js +205 -0
  112. package/dist/workflows/dag/backend-test-intake-context.js +32 -0
  113. package/dist/workflows/dag/backend-test-layout.js +133 -0
  114. package/dist/workflows/dag/backend-test-markdown-workflow.js +167 -32
  115. package/dist/workflows/dag/backend-test-module-stem.js +5 -0
  116. package/dist/workflows/dag/backend-test-pytest-collection.js +368 -33
  117. package/dist/workflows/dag/backend-test-result-contract.js +1 -1
  118. package/dist/workflows/dag/backend-test-scenario-param.js +884 -140
  119. package/dist/workflows/dag/backend-test-scenario-partitions.js +321 -0
  120. package/dist/workflows/dag/backend-test-writer-completeness.js +100 -56
  121. package/dist/workflows/dag/contract-output-registry.js +15 -0
  122. package/dist/workflows/dag/contract-validator-registrations.js +2 -1
  123. package/dist/workflows/dag/convergence/controller.js +141 -0
  124. package/dist/workflows/dag/dynamic-runtime/map.js +24 -8
  125. package/dist/workflows/dag/dynamic-runtime/shared.js +1 -1
  126. package/dist/workflows/dag/failure-category.js +4 -0
  127. package/dist/workflows/dag/frontend-implementation-contract.js +355 -26
  128. package/dist/workflows/dag/frontend-prewrite-gate.js +96 -84
  129. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  130. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  131. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  132. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  133. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  134. package/dist/workflows/dag/init-hybrid.js +720 -177
  135. package/dist/workflows/dag/node-execution.js +164 -26
  136. package/dist/workflows/dag/prompt.js +60 -1
  137. package/dist/workflows/dag/rerun-feedback.js +212 -0
  138. package/dist/workflows/dag/rerun-plan.js +112 -27
  139. package/dist/workflows/dag/rerun-run.js +3 -0
  140. package/dist/workflows/dag/rerun-task.js +61 -2
  141. package/dist/workflows/dag/retry-policy.js +18 -0
  142. package/dist/workflows/dag/structured-output-repair.js +712 -0
  143. package/dist/workflows/dag/types.js +121 -0
  144. package/dist/workflows/dag/validate.js +41 -1
  145. package/docs/operations/local-development-environment.md +1 -5
  146. package/docs/skills/vetted-skill-registry.md +14 -0
  147. package/docs/templates/README.md +1 -1
  148. package/docs/templates/agent-dag.schema.json +44 -0
  149. package/docs/templates/backend-test-dag.json +60 -35
  150. package/docs/templates/frontend-test-dag.json +8 -10
  151. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  152. package/docs/templates/harness.schema.json +0 -8
  153. package/harness.json +1 -8
  154. package/package.json +8 -4
  155. package/skills/analyze-product-requirements/SKILL.md +8 -5
  156. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  157. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  158. package/skills/analyze-product-requirements/references/example.md +50 -0
  159. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  160. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  161. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  162. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  163. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  164. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  165. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  166. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  167. package/skills/codebase-scout/SKILL.md +1 -1
  168. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  169. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  170. package/skills/improve-codebase-architecture/deepening.md +37 -0
  171. package/skills/improve-codebase-architecture/html-report.md +123 -0
  172. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  173. package/skills/improve-codebase-architecture/language.md +53 -0
  174. package/dist/worker/console/static/assets/channel-Dp_2Aqf1.js +0 -1
  175. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL--CODy37j.js +0 -1
  176. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE--CODy37j.js +0 -1
  177. package/dist/worker/console/static/assets/index-BA5-N5ER.css +0 -1
  178. package/dist/worker/console/static/assets/index-DKPXBrdc.js +0 -325
  179. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B-mvnLEc.js +0 -1
  180. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-JtpBmIv4.js +0 -8
@@ -149,6 +149,10 @@ export const backendTestCaseManifestSchema = z
149
149
  crossCuttingTestPointCount: z.number().int().min(0),
150
150
  unclassifiedTestPointCount: z.number().int().min(0),
151
151
  duplicateBindingTestPointCount: z.number().int().min(0),
152
+ payloadSafeCount: z.number().int().min(0),
153
+ payloadUnsafeCount: z.number().int().min(0),
154
+ payloadUnavailableCount: z.number().int().min(0),
155
+ duplicateExecutionSignatureCount: z.number().int().min(0),
152
156
  }).strict().optional(),
153
157
  artifactRefs: z.object({
154
158
  caseCoverageFacts: z.object({ path: z.string().min(1), sha256: z.string().regex(/^[a-f0-9]{64}$/).optional() }).strict(),
@@ -369,8 +369,13 @@ function seedLocalServerFixture(fixtures, managedStart) {
369
369
  },
370
370
  ];
371
371
  }
372
- /** Drop empty managedCommand strings; omit block when not managed-command. */
373
- export function sanitizeBackendTestExecutionInput(value) {
372
+ /**
373
+ * Drop empty managedCommand strings; omit block when not managed-command.
374
+ * With a configured backendTest layout, `allowedTestRoot` keeps the configured
375
+ * automation root stable instead of remapping every scout-chosen root to the
376
+ * frozen historical default.
377
+ */
378
+ export function sanitizeBackendTestExecutionInput(value, options = {}) {
374
379
  const record = asRecord(value);
375
380
  if (!record)
376
381
  return value;
@@ -404,17 +409,17 @@ export function sanitizeBackendTestExecutionInput(value) {
404
409
  normalizedRoot.includes("..") ||
405
410
  normalizedRoot.includes("\\") ||
406
411
  /^[A-Za-z]:/.test(normalizedRoot);
407
- if (!looksUnsafe &&
408
- normalizedRoot !== BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT) {
412
+ const frozenRoot = options.allowedTestRoot ?? BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT;
413
+ if (!looksUnsafe && normalizedRoot !== frozenRoot) {
409
414
  const gaps = Array.isArray(next.evidenceGaps)
410
415
  ? [...next.evidenceGaps]
411
416
  : [];
412
417
  gaps.push({
413
- description: `scout testRoot=${normalizedRoot} remapped to frozen adapter root ${BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT}`,
418
+ description: `scout testRoot=${normalizedRoot} remapped to frozen adapter root ${frozenRoot}`,
414
419
  sourceRef: "adapter:backend-test-execution",
415
420
  });
416
421
  next.evidenceGaps = gaps;
417
- next.testRoot = BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT;
422
+ next.testRoot = frozenRoot;
418
423
  }
419
424
  }
420
425
  // Local node/npm managed-command is not hosted by the clean-env pytest shell.
@@ -455,8 +460,8 @@ export function sanitizeBackendTestExecutionInput(value) {
455
460
  }
456
461
  return next;
457
462
  }
458
- export function coerceBackendTestExecutionInput(value) {
459
- const sanitized = sanitizeBackendTestExecutionInput(value);
463
+ export function coerceBackendTestExecutionInput(value, options = {}) {
464
+ const sanitized = sanitizeBackendTestExecutionInput(value, options);
460
465
  const direct = backendTestExecutionContractSchema.safeParse(sanitized);
461
466
  if (direct.success)
462
467
  return direct.data;
@@ -626,7 +631,11 @@ export async function materializeBackendTestExecutionContract(input) {
626
631
  catch (error) {
627
632
  throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
628
633
  }
629
- const candidates = [coerceBackendTestExecutionInput(parsed)];
634
+ const candidates = [
635
+ coerceBackendTestExecutionInput(parsed, {
636
+ allowedTestRoot: input.allowedTestRoot,
637
+ }),
638
+ ];
630
639
  let accepted = null;
631
640
  let lastSchemaError = "invalid execution contract";
632
641
  let lastSecretError = "";
@@ -0,0 +1,205 @@
1
+ import { z } from "zod";
2
+ import { createHash } from "node:crypto";
3
+ /**
4
+ * Plan D: deterministic gap-fill planning for backend-test.
5
+ *
6
+ * A `backend-test-gap-v1` document lists missing scenario slots/cases for an
7
+ * already-executed backend-test task. The deterministic ingest unions the
8
+ * document rows with the previous N6 coverage facts `missingSlots` and emits
9
+ * a `backend-test-gap-plan-v1.json` work order. Writers only patch the
10
+ * planned files; Case IDs are never renumbered and a second run over an
11
+ * already-satisfied plan returns alreadyPresent without touching assets.
12
+ */
13
+ export const BACKEND_TEST_GAP_DOC_SCHEMA_ID = "backend-test-gap-v1";
14
+ export const BACKEND_TEST_GAP_PLAN_SCHEMA_ID = "backend-test-gap-plan-v1";
15
+ const SLOT_ID = /^(?:TP-SP-[A-Z0-9._-]+|TP-[A-Z0-9._-]+)$/;
16
+ const CASE_ID = /^BE-[A-Z0-9]+(?:-[A-Z0-9]+)*-\d{3}$/;
17
+ export const backendTestGapDocSchema = z
18
+ .object({
19
+ schemaId: z.literal(BACKEND_TEST_GAP_DOC_SCHEMA_ID),
20
+ /** Must equal the original backend-test taskId. */
21
+ taskId: z.string().min(1),
22
+ /** sha256 of the original requirement document (identity check). */
23
+ requirementSha256: z.string().regex(/^[0-9a-f]{64}$/),
24
+ missingSlots: z
25
+ .array(z.object({
26
+ slotId: z.string().regex(SLOT_ID),
27
+ /** Markdown module stem that owns (or should own) the slot. */
28
+ moduleStem: z.string().min(1),
29
+ reason: z.string().min(1),
30
+ /** Optional: the Case to extend with the variant. */
31
+ caseId: z.string().regex(CASE_ID).optional(),
32
+ }))
33
+ .default([]),
34
+ })
35
+ .strict();
36
+ export const backendTestGapPlanSchema = z
37
+ .object({
38
+ schemaId: z.literal(BACKEND_TEST_GAP_PLAN_SCHEMA_ID),
39
+ taskId: z.string().min(1),
40
+ requirementSha256: z.string().regex(/^[0-9a-f]{64}$/),
41
+ status: z.enum(["planned", "already-satisfied", "error"]),
42
+ /** Module stems that need a Markdown/pytest patch. */
43
+ targetModules: z.array(z.string().min(1)),
44
+ /** Slot IDs to add (doc ∪ facts minus already-covered). */
45
+ newTestPoints: z.array(z.string()),
46
+ /** Shared setup refs already used by target modules (plan B). */
47
+ reuseSetupRefs: z.array(z.string()),
48
+ /** Doc rows conflicting with bound evidence; never written. */
49
+ conflicts: z.array(z.string()),
50
+ /** Doc rows whose slots are already covered; no file changes. */
51
+ alreadyPresent: z.array(z.string()),
52
+ /** Exact files writers may touch (README + module md + module py). */
53
+ targetPaths: z.array(z.string()),
54
+ issues: z.array(z.string()),
55
+ })
56
+ .strict();
57
+ function modulePaths(layout, stem) {
58
+ return [
59
+ `${layout.markdownDir}/${stem}.md`,
60
+ `${layout.scriptDir}/test_${stem}.py`,
61
+ ];
62
+ }
63
+ /**
64
+ * Deterministic ingest. Fail-closed rules:
65
+ * - taskId/requirement hash mismatch → status=error, no targetPaths.
66
+ * - slot referencing a module stem absent from the existing module index →
67
+ * conflict (gap-fill never adds new Module Index modules).
68
+ * - slot already covered per previous facts → alreadyPresent, not planned.
69
+ */
70
+ export function ingestBackendTestGap(input) {
71
+ const issues = [];
72
+ if (input.gapDoc.taskId !== input.taskId) {
73
+ return {
74
+ schemaId: BACKEND_TEST_GAP_PLAN_SCHEMA_ID,
75
+ taskId: input.taskId,
76
+ requirementSha256: input.requirementSha256,
77
+ status: "error",
78
+ targetModules: [],
79
+ newTestPoints: [],
80
+ reuseSetupRefs: [],
81
+ conflicts: [
82
+ `gap-doc taskId ${input.gapDoc.taskId} does not match task ${input.taskId}`,
83
+ ],
84
+ alreadyPresent: [],
85
+ targetPaths: [],
86
+ issues: ["identity mismatch: refusing to plan any writer work"],
87
+ };
88
+ }
89
+ if (input.gapDoc.requirementSha256 !== input.requirementSha256) {
90
+ return {
91
+ schemaId: BACKEND_TEST_GAP_PLAN_SCHEMA_ID,
92
+ taskId: input.taskId,
93
+ requirementSha256: input.requirementSha256,
94
+ status: "error",
95
+ targetModules: [],
96
+ newTestPoints: [],
97
+ reuseSetupRefs: [],
98
+ conflicts: [
99
+ "gap-doc requirementSha256 does not match the task requirement",
100
+ ],
101
+ alreadyPresent: [],
102
+ targetPaths: [],
103
+ issues: ["identity mismatch: refusing to plan any writer work"],
104
+ };
105
+ }
106
+ const covered = new Set(input.previousCoveredSlots ?? []);
107
+ const existing = new Set(input.existingModuleStems);
108
+ const docUnion = new Map();
109
+ const conflicts = [];
110
+ const alreadyPresent = [];
111
+ for (const row of input.gapDoc.missingSlots) {
112
+ if (covered.has(row.slotId)) {
113
+ alreadyPresent.push(row.slotId);
114
+ continue;
115
+ }
116
+ if (!existing.has(row.moduleStem)) {
117
+ conflicts.push(`${row.slotId}: module stem ${row.moduleStem} is not in the existing Module Index; gap-fill never adds new modules (run full mode for new business domains)`);
118
+ continue;
119
+ }
120
+ docUnion.set(row.slotId, {
121
+ moduleStem: row.moduleStem,
122
+ caseId: row.caseId,
123
+ source: "doc",
124
+ });
125
+ }
126
+ // facts-derived missing slots: attach to the partition's module when the
127
+ // slot token maps to an existing module stem heuristically; unknown owner
128
+ // stays a conflict rather than a guess.
129
+ for (const slotId of input.previousMissingSlots ?? []) {
130
+ if (docUnion.has(slotId) || covered.has(slotId))
131
+ continue;
132
+ const owner = inferSlotOwnerModule(slotId, existing);
133
+ if (!owner) {
134
+ conflicts.push(`${slotId}: previous facts report the slot missing but no existing module owns it; add the module stem to the gap doc`);
135
+ continue;
136
+ }
137
+ docUnion.set(slotId, { moduleStem: owner, source: "facts" });
138
+ }
139
+ const planned = [...docUnion.entries()].sort(([a], [b]) => a.localeCompare(b));
140
+ const targetModules = [...new Set(planned.map(([, v]) => v.moduleStem))].sort();
141
+ const targetPaths = targetModules.length
142
+ ? [
143
+ input.layout.readmePath,
144
+ ...targetModules.flatMap((stem) => modulePaths(input.layout, stem)),
145
+ ]
146
+ : [];
147
+ return {
148
+ schemaId: BACKEND_TEST_GAP_PLAN_SCHEMA_ID,
149
+ taskId: input.taskId,
150
+ requirementSha256: input.requirementSha256,
151
+ status: planned.length === 0 ? "already-satisfied" : "planned",
152
+ targetModules,
153
+ newTestPoints: planned.map(([slotId]) => slotId),
154
+ reuseSetupRefs: input.sharedSetupPath ? [input.sharedSetupPath] : [],
155
+ conflicts,
156
+ alreadyPresent: [...new Set(alreadyPresent)].sort(),
157
+ targetPaths,
158
+ issues,
159
+ };
160
+ }
161
+ /**
162
+ * Map a TP-SP-<OPERATION>-<AXIS>-... slot to an existing module stem. The
163
+ * heuristic is intentionally conservative: an exact stem match of the
164
+ * operation token against existing stems (e.g. slot TP-SP-RESOURCES-TYPE-X
165
+ * with stem "resources"). No match → null (conflict, not a guess).
166
+ */
167
+ const HTTP_METHOD_TOKENS = new Set([
168
+ "get",
169
+ "post",
170
+ "put",
171
+ "patch",
172
+ "delete",
173
+ "head",
174
+ "options",
175
+ ]);
176
+ export function inferSlotOwnerModule(slotId, existingStems) {
177
+ const match = /^TP-SP-([A-Z0-9-]+)-[A-Z0-9-]+$/.exec(slotId);
178
+ if (!match)
179
+ return null;
180
+ const tokens = match[1].split("-");
181
+ const starts = [0];
182
+ if (tokens[0] && HTTP_METHOD_TOKENS.has(tokens[0].toLowerCase())) {
183
+ starts.push(1);
184
+ if (tokens[1]?.toLowerCase() === "api")
185
+ starts.push(2);
186
+ }
187
+ else if (tokens[0]?.toLowerCase() === "api") {
188
+ starts.push(1);
189
+ }
190
+ // Prefer the LONGEST remaining prefix that matches an existing stem so
191
+ // "resource-notes" wins over a prefix-only accident.
192
+ for (const start of starts) {
193
+ for (let cut = tokens.length; cut > start; cut--) {
194
+ const candidate = tokens.slice(start, cut).join("-").toLowerCase();
195
+ for (const stem of existingStems) {
196
+ if (stem.toLowerCase().replace(/_/g, "-") === candidate)
197
+ return stem;
198
+ }
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ export function hashRequirement(markdown) {
204
+ return createHash("sha256").update(markdown, "utf8").digest("hex");
205
+ }
@@ -47,6 +47,33 @@ const DISCOVERY_CANDIDATES = {
47
47
  "src/server.ts",
48
48
  ],
49
49
  };
50
+ /**
51
+ * Resolve the single authoritative user shared-setup document.
52
+ * `backendTest.sharedSetupDoc` (task-relative, e.g. "source/shared-setup.md")
53
+ * wins when present; otherwise exactly one reference whose roleHints include
54
+ * `shared-setup` may bind. Multiple shared-setup references are fail-closed:
55
+ * the caller must pin one document explicitly.
56
+ */
57
+ export function resolveBackendTestSharedSetup(sources, referenceIndex, options = {}) {
58
+ const explicit = options.sharedSetupDoc?.trim();
59
+ if (explicit) {
60
+ const normalized = explicit.replaceAll("\\", "/");
61
+ const hit = referenceIndex.find((entry) => entry.path === normalized);
62
+ if (!hit) {
63
+ throw new Error(`backendTest.sharedSetupDoc "${explicit}" is not a bound reference document`);
64
+ }
65
+ return { path: hit.path, readPath: hit.readPath, sha256: hit.sha256 };
66
+ }
67
+ const hinted = referenceIndex.filter((entry) => entry.roleHints.includes("shared-setup"));
68
+ if (hinted.length > 1) {
69
+ throw new Error(`multiple shared-setup reference documents bound (${hinted
70
+ .map((entry) => entry.path)
71
+ .join(", ")}); pin one via backendTest.sharedSetupDoc`);
72
+ }
73
+ return hinted[0]
74
+ ? { path: hinted[0].path, readPath: hinted[0].readPath, sha256: hinted[0].sha256 }
75
+ : null;
76
+ }
50
77
  function taskRelative(sources, filePath) {
51
78
  return path.relative(sources.taskDir, filePath).replaceAll(path.sep, "/");
52
79
  }
@@ -81,6 +108,8 @@ function inferRoleHints(referencePath) {
81
108
  add("business-rule");
82
109
  if (/(test|pytest|测试)/.test(normalized))
83
110
  add("test-evidence");
111
+ if (/(公共前置|shared[-_. ]?setup|前置操作)/.test(normalized))
112
+ add("shared-setup");
84
113
  if (hints.length === 0)
85
114
  add("source-fact");
86
115
  return hints;
@@ -142,5 +171,8 @@ export async function buildBackendTestIntakeContext(sources) {
142
171
  boundedSourceContext: parts.join("\n\n"),
143
172
  referenceIndex,
144
173
  projectDiscoverySeed: await discoverCandidatePaths(sources.repoRoot),
174
+ sharedSetup: resolveBackendTestSharedSetup(sources, referenceIndex, {
175
+ sharedSetupDoc: sources.taskConfig?.backendTest?.sharedSetupDoc,
176
+ }),
145
177
  };
146
178
  }
@@ -0,0 +1,133 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Backend-test artifact layout (plan A: configurable test asset paths).
4
+ *
5
+ * The layout is resolved once from `task.backendTest` at DAG compile time and
6
+ * frozen for the whole run. Every generated path (README, module Markdown,
7
+ * pytest module script, pytest target) is derived from this single source so
8
+ * writers, gates, analyzers and the executor can never disagree about where
9
+ * backend-test assets live.
10
+ *
11
+ * Default layout keeps the historical frozen contract:
12
+ * testcase/md/README.md
13
+ * testcase/md/<stem>.md
14
+ * testcase/test_<stem>.py
15
+ * pytest target: testcase/
16
+ */
17
+ export const BACKEND_TEST_DEFAULT_TEST_ROOT = "testcase";
18
+ export const BACKEND_TEST_DEFAULT_MARKDOWN_REL = "md";
19
+ export const BACKEND_TEST_DEFAULT_SCRIPT_REL = ".";
20
+ /** Repository roots that must never be configured as backend-test test roots. */
21
+ const FORBIDDEN_TEST_ROOT_PREFIXES = [
22
+ "src",
23
+ "app",
24
+ "lib",
25
+ "apps",
26
+ "packages",
27
+ ".harness",
28
+ ".git",
29
+ "node_modules",
30
+ "dist",
31
+ "build",
32
+ "docs",
33
+ "website",
34
+ "scripts",
35
+ "test",
36
+ "smoke",
37
+ "knowledge",
38
+ "features",
39
+ "dogfood",
40
+ "examples",
41
+ "bin",
42
+ ];
43
+ function normalizeRelPosix(value, label) {
44
+ const trimmed = value.trim();
45
+ if (!trimmed)
46
+ throw new Error(`backendTest.${label} must not be empty`);
47
+ if (trimmed.includes("\\")) {
48
+ throw new Error(`backendTest.${label} must use forward slashes: ${value}`);
49
+ }
50
+ if (path.isAbsolute(trimmed) || /^[A-Za-z]:/.test(trimmed)) {
51
+ throw new Error(`backendTest.${label} must be a repository-relative path: ${value}`);
52
+ }
53
+ const segments = trimmed.split("/").filter((segment) => segment.length > 0);
54
+ if (segments.some((segment) => segment === ".." || segment === ".")) {
55
+ throw new Error(`backendTest.${label} must not contain "." or ".." segments: ${value}`);
56
+ }
57
+ if (segments.some((segment) => !/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(segment))) {
58
+ throw new Error(`backendTest.${label} contains unsafe path segments: ${value}`);
59
+ }
60
+ return segments.join("/");
61
+ }
62
+ function normalizeMarkdownRel(value) {
63
+ const trimmed = value.trim();
64
+ if (trimmed === "" || trimmed === ".") {
65
+ throw new Error("backendTest.markdownRel must be a named subdirectory (not '.'): testcase root itself is reserved for pytest scripts and run reports");
66
+ }
67
+ return normalizeRelPosix(value, "markdownRel");
68
+ }
69
+ function normalizeOptionalRel(value, fallback, label) {
70
+ if (value === undefined || value === null)
71
+ return fallback;
72
+ if (typeof value !== "string") {
73
+ throw new Error(`backendTest.${label} must be a string`);
74
+ }
75
+ if (value.trim() === "" || value.trim() === ".")
76
+ return ".";
77
+ return normalizeRelPosix(value, label);
78
+ }
79
+ function assertTestRootAllowed(testRoot) {
80
+ const topSegment = testRoot.split("/")[0];
81
+ if (FORBIDDEN_TEST_ROOT_PREFIXES.some((forbidden) => topSegment === forbidden || topSegment.startsWith(`${forbidden}.`))) {
82
+ throw new Error(`backendTest.testRoot must not target production/governance trees: ${testRoot}`);
83
+ }
84
+ if (topSegment.startsWith(".")) {
85
+ throw new Error(`backendTest.testRoot must not target hidden directories: ${testRoot}`);
86
+ }
87
+ }
88
+ /**
89
+ * Resolve the frozen backend-test artifact layout from task config.
90
+ * Throws on unsafe or ambiguous configuration (fail-closed).
91
+ */
92
+ export function resolveBackendTestLayout(config) {
93
+ const testRoot = config?.testRoot === undefined || config?.testRoot === null
94
+ ? BACKEND_TEST_DEFAULT_TEST_ROOT
95
+ : normalizeRelPosix(config.testRoot, "testRoot");
96
+ assertTestRootAllowed(testRoot);
97
+ const markdownRel = config?.markdownRel === undefined || config?.markdownRel === null
98
+ ? BACKEND_TEST_DEFAULT_MARKDOWN_REL
99
+ : normalizeMarkdownRel(config.markdownRel);
100
+ if (markdownRel === ".") {
101
+ throw new Error("backendTest.markdownRel must be a named subdirectory (not '.'): testcase root itself is reserved for pytest scripts and run reports");
102
+ }
103
+ const scriptRel = config?.scriptRel === undefined || config?.scriptRel === null
104
+ ? BACKEND_TEST_DEFAULT_SCRIPT_REL
105
+ : normalizeOptionalRel(config.scriptRel, ".", "scriptRel");
106
+ const markdownDir = `${testRoot}/${markdownRel}`;
107
+ const scriptDir = scriptRel === "." ? testRoot : `${testRoot}/${scriptRel}`;
108
+ const readmePath = `${markdownDir}/README.md`;
109
+ const isDefault = testRoot === BACKEND_TEST_DEFAULT_TEST_ROOT &&
110
+ markdownRel === BACKEND_TEST_DEFAULT_MARKDOWN_REL &&
111
+ scriptRel === BACKEND_TEST_DEFAULT_SCRIPT_REL;
112
+ return {
113
+ testRoot,
114
+ markdownRel,
115
+ scriptRel,
116
+ markdownDir,
117
+ scriptDir,
118
+ readmePath,
119
+ isDefault,
120
+ };
121
+ }
122
+ /** Markdown module path for a normalized module stem, e.g. "testcase/md/health.md". */
123
+ export function backendTestModuleMarkdownPath(layout, stem) {
124
+ return `${layout.markdownDir}/${stem}.md`;
125
+ }
126
+ /** Pytest module path for a normalized module stem, e.g. "testcase/test_health.py". */
127
+ export function backendTestModulePytestPath(layout, stem) {
128
+ return `${layout.scriptDir}/test_${stem}.py`;
129
+ }
130
+ /** pytest CLI target directory with trailing slash, e.g. "testcase/". */
131
+ export function backendTestPytestTarget(layout) {
132
+ return `${layout.testRoot}/`;
133
+ }