@tea-agent/loop-agent 0.16.1 → 0.16.2
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 +21 -0
- package/dist/executors/dag-pi-executor.js +4 -2
- package/dist/executors/pi-sdk-executor.js +66 -3
- package/dist/executors/shell-executor.js +212 -29
- package/dist/executors/shell-presets.js +12 -2
- package/dist/executors/shell-write-guard.js +20 -1
- package/dist/shared/git-progress.js +9 -2
- package/dist/worker/observability/read-model.js +56 -0
- package/dist/worker/observe/server.js +6 -3
- package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
- package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
- package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
- package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
- package/dist/workflows/dag/backend-test-result-contract.js +6 -4
- package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
- package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
- package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
- package/dist/workflows/dag/failure-routing.js +1 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +32 -16
- package/dist/workflows/dag/init-hybrid.js +591 -119
- package/dist/workflows/dag/lifecycle.js +33 -2
- package/dist/workflows/dag/scheduler.js +87 -17
- package/dist/workflows/dag/types.js +31 -0
- package/dist/workflows/dag/validate.js +20 -14
- package/docs/templates/agent-dag.schema.json +25 -2
- package/docs/templates/backend-test-analysis.schema.json +9 -16
- package/docs/templates/backend-test-dag.json +493 -197
- package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
- package/docs/templates/backend-test-execution.schema.json +6 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +4 -1
|
@@ -4,7 +4,7 @@ import { hostname as localHostname } from "node:os";
|
|
|
4
4
|
import { writeJsonAtomic, } from "../../infrastructure/harness/atomic-write.js";
|
|
5
5
|
import { parseDagSpec } from "./types.js";
|
|
6
6
|
import { normalizeDagFailureCategory, } from "./failure-category.js";
|
|
7
|
-
import { routeDagFailure } from "./failure-routing.js";
|
|
7
|
+
import { dagProductLineFailureCategoryValues, routeDagFailure, } from "./failure-routing.js";
|
|
8
8
|
const DAG_LIFECYCLE_SCAN_ORDER = [
|
|
9
9
|
"paused",
|
|
10
10
|
"active",
|
|
@@ -663,6 +663,36 @@ function findDoctorFailureNode(state) {
|
|
|
663
663
|
rawFailureCategory: selected[1].failureCategory,
|
|
664
664
|
};
|
|
665
665
|
}
|
|
666
|
+
async function readRunOwnedBackendTestClassification(runDir) {
|
|
667
|
+
try {
|
|
668
|
+
const raw = JSON.parse(await readFile(path.join(runDir, "classify-backend-test-result-pi.json"), "utf-8"));
|
|
669
|
+
const text = raw.assistantText ?? raw.stdout;
|
|
670
|
+
if (typeof text !== "string")
|
|
671
|
+
return undefined;
|
|
672
|
+
const trimmed = text.trim();
|
|
673
|
+
const fenced = trimmed.match(/^```json\s*([\s\S]*?)\s*```$/i);
|
|
674
|
+
const classification = JSON.parse(fenced?.[1] ?? trimmed);
|
|
675
|
+
return typeof classification.category === "string" &&
|
|
676
|
+
dagProductLineFailureCategoryValues.includes(classification.category)
|
|
677
|
+
? classification.category
|
|
678
|
+
: undefined;
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function resolveDoctorFailureRouting(input) {
|
|
685
|
+
const classifiedCategory = await readRunOwnedBackendTestClassification(input.runDir);
|
|
686
|
+
if (classifiedCategory) {
|
|
687
|
+
return routeDagFailure({
|
|
688
|
+
rawFailureCategory: classifiedCategory,
|
|
689
|
+
normalizedFailureCategory: "unknown",
|
|
690
|
+
nodeId: input.nodeId,
|
|
691
|
+
productLineFailureCategory: classifiedCategory,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
return routeDagFailure(input);
|
|
695
|
+
}
|
|
666
696
|
async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
667
697
|
const located = await locateDagRun(repoRoot, runId);
|
|
668
698
|
if (!located) {
|
|
@@ -675,7 +705,8 @@ async function formatDagDoctorMarkdown(repoRoot, runId) {
|
|
|
675
705
|
(state.status === "paused" ? "human-required" : state.failureCategory);
|
|
676
706
|
const failureStatus = state.status === "paused" ? "paused" : failure.status;
|
|
677
707
|
const normalizedCategory = normalizeDagFailureCategory(rawFailureCategory, failureStatus ?? state.status);
|
|
678
|
-
const routing =
|
|
708
|
+
const routing = await resolveDoctorFailureRouting({
|
|
709
|
+
runDir: located.runDir,
|
|
679
710
|
rawFailureCategory,
|
|
680
711
|
normalizedFailureCategory: normalizedCategory,
|
|
681
712
|
nodeId: failure.nodeId,
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
import { isPauseOnHumanDecisionGate } from "./decision-envelope.js";
|
|
2
|
+
import { evaluateConditionExpression } from "./dynamic-runtime/condition.js";
|
|
2
3
|
export function isConditionSkippedReason(reason) {
|
|
3
4
|
return Boolean(reason?.startsWith("condition "));
|
|
4
5
|
}
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Dependency readiness for PENDING nodes.
|
|
8
|
+
*
|
|
9
|
+
* Default (`dependsPolicy: all`): any ERROR/SKIPPED dependency → skip (cascades
|
|
10
|
+
* exclusive condition branches). Unsettled → wait. All FINISHED → run.
|
|
11
|
+
*
|
|
12
|
+
* `all-or-condition-skip`: exclusive-branch tips that were condition-SKIPPED are
|
|
13
|
+
* soft; the node runs when every dep is FINISHED or soft condition-skip and at
|
|
14
|
+
* least one is FINISHED (OR-join after condition). Opt-in only — never default.
|
|
15
|
+
*/
|
|
16
|
+
function dependencyReadiness(task, nodes) {
|
|
17
|
+
if (task.depends_on.length === 0)
|
|
18
|
+
return "run";
|
|
19
|
+
const softConditionJoin = task.dependsPolicy === "all-or-condition-skip";
|
|
20
|
+
let hasFinished = false;
|
|
21
|
+
let hasPendingUpstream = false;
|
|
22
|
+
let hasHardBlock = false;
|
|
23
|
+
for (const depId of task.depends_on) {
|
|
9
24
|
const dep = nodes[depId];
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
if (!dep) {
|
|
26
|
+
hasHardBlock = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (dep.status === "FINISHED") {
|
|
30
|
+
hasFinished = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (dep.status === "PENDING" || dep.status === "RUNNING") {
|
|
34
|
+
hasPendingUpstream = true;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (dep.status === "ERROR") {
|
|
38
|
+
hasHardBlock = true;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (dep.status === "SKIPPED") {
|
|
42
|
+
if (softConditionJoin &&
|
|
43
|
+
isConditionSkippedReason(dep.skippedReason)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
hasHardBlock = true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (hasPendingUpstream)
|
|
50
|
+
return "wait";
|
|
51
|
+
if (hasHardBlock)
|
|
52
|
+
return "skip";
|
|
53
|
+
if (hasFinished)
|
|
54
|
+
return "run";
|
|
55
|
+
return "skip";
|
|
12
56
|
}
|
|
13
57
|
function conditionSkippedByAncestor(task, nodes) {
|
|
14
58
|
return task.depends_on.some((depId) => isConditionSkippedReason(nodes[depId]?.skippedReason));
|
|
@@ -27,18 +71,44 @@ async function mapConcurrent(items, limit, fn) {
|
|
|
27
71
|
export async function executeDagRanksOnce(input) {
|
|
28
72
|
let pausedByNodeId;
|
|
29
73
|
for (const rank of input.ranks) {
|
|
30
|
-
const
|
|
74
|
+
const pending = rank.filter((id) => {
|
|
31
75
|
const node = input.state.nodes[id];
|
|
76
|
+
return node?.status === "PENDING";
|
|
77
|
+
});
|
|
78
|
+
const runnable = pending.filter((id) => {
|
|
32
79
|
const task = input.tasksById.get(id);
|
|
33
|
-
|
|
34
|
-
return false;
|
|
35
|
-
return !shouldSkipNode(node, task, input.state.nodes);
|
|
80
|
+
return dependencyReadiness(task, input.state.nodes) === "run";
|
|
36
81
|
});
|
|
37
|
-
const
|
|
38
|
-
|
|
82
|
+
const conditionSettled = [];
|
|
83
|
+
for (const id of runnable) {
|
|
84
|
+
const task = input.tasksById.get(id);
|
|
85
|
+
if (!task.runIf)
|
|
86
|
+
continue;
|
|
87
|
+
try {
|
|
88
|
+
if (!evaluateConditionExpression(task.runIf, input.state)) {
|
|
89
|
+
const node = input.state.nodes[id];
|
|
90
|
+
node.status = "SKIPPED";
|
|
91
|
+
node.skippedReason = `condition runIf did not match: ${task.runIf}`;
|
|
92
|
+
node.finishedAt = new Date().toISOString();
|
|
93
|
+
conditionSettled.push(id);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const node = input.state.nodes[id];
|
|
98
|
+
node.status = "ERROR";
|
|
99
|
+
node.failureCategory = "invalid-output";
|
|
100
|
+
node.stderr = error instanceof Error ? error.message : String(error);
|
|
101
|
+
node.finishedAt = new Date().toISOString();
|
|
102
|
+
conditionSettled.push(id);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (conditionSettled.length > 0)
|
|
106
|
+
await input.persistState();
|
|
107
|
+
const conditionSkippedSet = new Set(conditionSettled);
|
|
108
|
+
const actuallyRunnable = runnable.filter((id) => !conditionSkippedSet.has(id));
|
|
109
|
+
const blocked = pending.filter((id) => {
|
|
39
110
|
const task = input.tasksById.get(id);
|
|
40
|
-
return (
|
|
41
|
-
shouldSkipNode(node, task, input.state.nodes));
|
|
111
|
+
return dependencyReadiness(task, input.state.nodes) === "skip";
|
|
42
112
|
});
|
|
43
113
|
for (const id of blocked) {
|
|
44
114
|
const node = input.state.nodes[id];
|
|
@@ -50,12 +120,12 @@ export async function executeDagRanksOnce(input) {
|
|
|
50
120
|
if (blocked.length > 0) {
|
|
51
121
|
await input.persistState();
|
|
52
122
|
}
|
|
53
|
-
const pauseGateRunnable =
|
|
123
|
+
const pauseGateRunnable = actuallyRunnable.filter((id) => {
|
|
54
124
|
const task = input.tasksById.get(id);
|
|
55
125
|
return isPauseOnHumanDecisionGate(task);
|
|
56
126
|
});
|
|
57
|
-
const regularRunnable =
|
|
58
|
-
const rankWriterNodeIds =
|
|
127
|
+
const regularRunnable = actuallyRunnable.filter((id) => !pauseGateRunnable.includes(id));
|
|
128
|
+
const rankWriterNodeIds = actuallyRunnable.filter((id) => {
|
|
59
129
|
const task = input.tasksById.get(id);
|
|
60
130
|
return (task?.executor === "pi" &&
|
|
61
131
|
task.toolProfile === "write" &&
|
|
@@ -59,6 +59,14 @@ export const dagVerdictGateSchema = z.object({
|
|
|
59
59
|
fromNodeId: z
|
|
60
60
|
.string()
|
|
61
61
|
.regex(/^[a-z][a-z0-9-]*$/, "fromNodeId must be kebab-case"),
|
|
62
|
+
/**
|
|
63
|
+
* Optional fallback node ids when the primary `fromNodeId` JSON is absent
|
|
64
|
+
* (e.g. exclusive condition branch skip). Tried in order after primary.
|
|
65
|
+
* Prefer listing the post-revision / final reviewer first when both may exist.
|
|
66
|
+
*/
|
|
67
|
+
fallbackFromNodeIds: z
|
|
68
|
+
.array(z.string().regex(/^[a-z][a-z0-9-]*$/, "fallbackFromNodeIds must be kebab-case"))
|
|
69
|
+
.optional(),
|
|
62
70
|
accept: z.array(z.string().min(1)).min(1),
|
|
63
71
|
lineMode: z.enum(["first-non-empty", "first-verdict-line"]).optional(),
|
|
64
72
|
label: z.string().min(1).optional(),
|
|
@@ -76,8 +84,11 @@ export const dagRequirementCoverageGateSchema = z.object({
|
|
|
76
84
|
});
|
|
77
85
|
export const dagJsonArtifactSchemaIdSchema = z.enum([
|
|
78
86
|
"backend-test-analysis-v1",
|
|
87
|
+
"backend-test-analysis-v2",
|
|
79
88
|
"backend-test-execution-v1",
|
|
80
89
|
"backend-test-result-v1",
|
|
90
|
+
"backend-test-classification-v1",
|
|
91
|
+
"backend-test-semantic-review-v1",
|
|
81
92
|
"backend-test-case-manifest-v1",
|
|
82
93
|
"frontend-implementation-contract-v1",
|
|
83
94
|
]);
|
|
@@ -86,6 +97,7 @@ export const dagJsonArtifactGateSchema = z.object({
|
|
|
86
97
|
schemaId: dagJsonArtifactSchemaIdSchema,
|
|
87
98
|
artifactName: z.string().regex(/^[a-z0-9][a-z0-9._-]*\.json$/),
|
|
88
99
|
outputDir: z.string().regex(/^[a-z0-9][a-z0-9._-]*$/),
|
|
100
|
+
junitRelativePath: z.string().min(1).optional(),
|
|
89
101
|
});
|
|
90
102
|
export const ENV_VAR_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
|
|
91
103
|
export const dagVersionSchema = z
|
|
@@ -129,12 +141,22 @@ export const dagNodeStatusSchema = z.enum([
|
|
|
129
141
|
"ERROR",
|
|
130
142
|
"SKIPPED",
|
|
131
143
|
]);
|
|
144
|
+
export const dagBackendTestPipelineSchema = z.enum([
|
|
145
|
+
"contracts",
|
|
146
|
+
"semantic-initial",
|
|
147
|
+
"semantic-final",
|
|
148
|
+
"execute-parse-initial",
|
|
149
|
+
"classification-eligibility",
|
|
150
|
+
"repair-safety-traceability",
|
|
151
|
+
"finalize-effective-result",
|
|
152
|
+
]);
|
|
132
153
|
export const dagShellConfigSchema = z.object({
|
|
133
154
|
commands: z.array(z.string()).default([]),
|
|
134
155
|
preset: dagShellPresetSchema.optional(),
|
|
135
156
|
verdictGate: dagVerdictGateSchema.optional(),
|
|
136
157
|
requirementCoverageGate: dagRequirementCoverageGateSchema.optional(),
|
|
137
158
|
jsonArtifactGate: dagJsonArtifactGateSchema.optional(),
|
|
159
|
+
backendTestPipeline: dagBackendTestPipelineSchema.optional(),
|
|
138
160
|
verifyEvidence: dagShellVerifyEvidenceSchema.optional(),
|
|
139
161
|
repairArtifactGate: dagRepairArtifactGateSchema.optional(),
|
|
140
162
|
/** fail (default): any nonzero command fails the node. record: finish node FINISHED with failure facts for downstream assess/repair. */
|
|
@@ -255,6 +277,15 @@ export const dagConvergenceSpecSchema = z
|
|
|
255
277
|
export const dagTaskSchema = z.object({
|
|
256
278
|
id: z.string().regex(/^[a-z][a-z0-9-]*$/, "task id must be kebab-case"),
|
|
257
279
|
depends_on: z.array(z.string()).default([]),
|
|
280
|
+
/**
|
|
281
|
+
* How SKIPPED upstreams affect readiness.
|
|
282
|
+
* - all (default): any SKIPPED/ERROR dependency blocks (cascade exclusive branches)
|
|
283
|
+
* - all-or-condition-skip: condition-branch SKIPPED tips count as soft; node runs when
|
|
284
|
+
* every dep is FINISHED or soft condition-skip and at least one is FINISHED (OR-join)
|
|
285
|
+
*/
|
|
286
|
+
dependsPolicy: z.enum(["all", "all-or-condition-skip"]).optional(),
|
|
287
|
+
/** Execute only when this fail-closed condition resolves true after dependencies settle. */
|
|
288
|
+
runIf: z.string().min(1).optional(),
|
|
258
289
|
complexity: dagComplexitySchema,
|
|
259
290
|
subtask_prompt: z.string().min(1),
|
|
260
291
|
subtask_prompt_source: z
|
|
@@ -137,18 +137,24 @@ function validateVerdictGateConfig(task, spec, issues) {
|
|
|
137
137
|
return;
|
|
138
138
|
}
|
|
139
139
|
const upstreamIds = new Set(task.depends_on);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
140
|
+
const sourceNodeIds = [
|
|
141
|
+
verdictGate.fromNodeId,
|
|
142
|
+
...(verdictGate.fallbackFromNodeIds ?? []),
|
|
143
|
+
];
|
|
144
|
+
for (const sourceId of sourceNodeIds) {
|
|
145
|
+
if (!upstreamIds.has(sourceId)) {
|
|
146
|
+
issues.push({
|
|
147
|
+
type: "invalid-verdict-gate-config",
|
|
148
|
+
message: `task ${task.id} shell.verdictGate source "${sourceId}" must appear in depends_on`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const upstreamTask = spec.tasks.find((candidate) => candidate.id === sourceId);
|
|
152
|
+
if (upstreamTask && upstreamTask.executor === "shell") {
|
|
153
|
+
issues.push({
|
|
154
|
+
type: "invalid-verdict-gate-config",
|
|
155
|
+
message: `task ${task.id} shell.verdictGate source "${sourceId}" must reference a non-shell upstream verdict node`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
152
158
|
}
|
|
153
159
|
for (const accepted of verdictGate.accept) {
|
|
154
160
|
if (accepted.trim().length === 0) {
|
|
@@ -440,10 +446,10 @@ function validateShellTaskConfig(task, spec, issues) {
|
|
|
440
446
|
return;
|
|
441
447
|
}
|
|
442
448
|
const commands = resolveShellCommands(shell);
|
|
443
|
-
if (commands.length === 0 && !shell.jsonArtifactGate) {
|
|
449
|
+
if (commands.length === 0 && !shell.jsonArtifactGate && !shell.backendTestPipeline) {
|
|
444
450
|
issues.push({
|
|
445
451
|
type: "missing-shell-commands",
|
|
446
|
-
message: `shell task ${task.id} requires shell.preset, shell.verdictGate, and/or non-empty shell.commands`,
|
|
452
|
+
message: `shell task ${task.id} requires shell.preset, shell.verdictGate, shell.jsonArtifactGate, shell.backendTestPipeline, and/or non-empty shell.commands`,
|
|
447
453
|
});
|
|
448
454
|
}
|
|
449
455
|
if (commands.some((command) => command.trim().length === 0)) {
|
|
@@ -212,7 +212,15 @@
|
|
|
212
212
|
"fromNodeId": {
|
|
213
213
|
"type": "string",
|
|
214
214
|
"pattern": "^[a-z][a-z0-9-]*$",
|
|
215
|
-
"description": "
|
|
215
|
+
"description": "Primary upstream node id whose canonical JSON output in the runner-injected current run directory ($HARNESS_DAG_RUN_DIR/<fromNodeId>.json) supplies the verdict line."
|
|
216
|
+
},
|
|
217
|
+
"fallbackFromNodeIds": {
|
|
218
|
+
"type": "array",
|
|
219
|
+
"items": {
|
|
220
|
+
"type": "string",
|
|
221
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
222
|
+
},
|
|
223
|
+
"description": "Optional fallback upstream node ids tried in order when the primary JSON output is absent, for example when an exclusive condition branch was skipped. Each source must also appear in the gate task depends_on list."
|
|
216
224
|
},
|
|
217
225
|
"accept": {
|
|
218
226
|
"type": "array",
|
|
@@ -242,6 +250,9 @@
|
|
|
242
250
|
"label": { "type": "string", "minLength": 1 }
|
|
243
251
|
}
|
|
244
252
|
},
|
|
253
|
+
"backendTestPipeline": {
|
|
254
|
+
"enum": ["contracts", "semantic-initial", "semantic-final", "execute-parse-initial", "classification-eligibility", "repair-safety-traceability", "finalize-effective-result"]
|
|
255
|
+
},
|
|
245
256
|
"verifyEvidence": { "$ref": "#/$defs/shellVerifyEvidence" },
|
|
246
257
|
"repairArtifactGate": { "$ref": "#/$defs/repairArtifactGate" },
|
|
247
258
|
"envAllowlist": {
|
|
@@ -258,7 +269,8 @@
|
|
|
258
269
|
{ "required": ["commands"], "properties": { "commands": { "minItems": 1 } } },
|
|
259
270
|
{ "required": ["preset"] },
|
|
260
271
|
{ "required": ["verdictGate"] },
|
|
261
|
-
{ "required": ["requirementCoverageGate"] }
|
|
272
|
+
{ "required": ["requirementCoverageGate"] },
|
|
273
|
+
{ "required": ["backendTestPipeline"] }
|
|
262
274
|
]
|
|
263
275
|
},
|
|
264
276
|
"staticConfig": {
|
|
@@ -327,6 +339,17 @@
|
|
|
327
339
|
"items": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
|
|
328
340
|
"default": []
|
|
329
341
|
},
|
|
342
|
+
"dependsPolicy": {
|
|
343
|
+
"type": "string",
|
|
344
|
+
"enum": ["all", "all-or-condition-skip"],
|
|
345
|
+
"default": "all",
|
|
346
|
+
"description": "Dependency join policy. all is fail-closed on any skipped/error dependency. all-or-condition-skip treats only condition-branch skips as soft and runs after all dependencies settle when at least one dependency finished."
|
|
347
|
+
},
|
|
348
|
+
"runIf": {
|
|
349
|
+
"type": "string",
|
|
350
|
+
"minLength": 1,
|
|
351
|
+
"description": "Optional fail-closed node execution condition evaluated after dependencies settle. False records an explicit condition skip; malformed selectors fail the node."
|
|
352
|
+
},
|
|
330
353
|
"complexity": { "$ref": "#/$defs/complexity" },
|
|
331
354
|
"subtask_prompt": {
|
|
332
355
|
"type": "string",
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://tea-agent.dev/schemas/backend-test-analysis-
|
|
4
|
-
"title": "Backend Test Analysis
|
|
3
|
+
"$id": "https://tea-agent.dev/schemas/backend-test-analysis-v2.json",
|
|
4
|
+
"title": "Backend Test Analysis v2",
|
|
5
|
+
"description": "The runtime accepts v1 input for compatibility and normalizes it to this v2 artifact.",
|
|
5
6
|
"type": "object",
|
|
6
7
|
"additionalProperties": false,
|
|
7
8
|
"required": ["schemaVersion", "sourceBinding", "acceptanceCriteria", "endpoints", "dataModels", "businessRules", "stateTransitions", "boundaryConstraints", "externalDependencies", "risks", "evidenceGaps"],
|
|
8
9
|
"properties": {
|
|
9
|
-
"schemaVersion": { "const":
|
|
10
|
-
"sourceBinding": {
|
|
11
|
-
"type": "object", "additionalProperties": false,
|
|
12
|
-
"required": ["taskId", "requirementPath", "requirementSha256", "referencePaths", "requirementIds"],
|
|
13
|
-
"properties": {
|
|
14
|
-
"taskId": { "type": "string", "minLength": 1 },
|
|
15
|
-
"requirementPath": { "type": "string", "minLength": 1 },
|
|
16
|
-
"requirementSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" },
|
|
17
|
-
"referencePaths": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
18
|
-
"requirementIds": { "type": "array", "items": { "type": "string", "pattern": "^(REQ|BR|AC)-[A-Z0-9]+(-[A-Z0-9]+)*$" } }
|
|
19
|
-
}
|
|
20
|
-
},
|
|
10
|
+
"schemaVersion": { "const": 2 },
|
|
11
|
+
"sourceBinding": { "$ref": "#/$defs/sourceBinding" },
|
|
21
12
|
"acceptanceCriteria": { "type": "array", "items": { "$ref": "#/$defs/acceptanceCriterion" } },
|
|
22
13
|
"endpoints": { "type": "array", "items": { "$ref": "#/$defs/endpoint" } },
|
|
23
14
|
"dataModels": { "type": "array", "items": { "$ref": "#/$defs/evidencedDescription" } },
|
|
@@ -30,10 +21,12 @@
|
|
|
30
21
|
},
|
|
31
22
|
"$defs": {
|
|
32
23
|
"sourceRef": { "type": "string", "minLength": 1 },
|
|
33
|
-
"
|
|
24
|
+
"sourceBinding": { "type": "object", "additionalProperties": false, "required": ["taskId", "requirementPath", "requirementSha256", "referencePaths", "requirementIds"], "properties": { "taskId": { "type": "string", "minLength": 1 }, "requirementPath": { "type": "string", "minLength": 1 }, "requirementSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$" }, "referencePaths": { "type": "array", "items": { "type": "string", "minLength": 1 } }, "requirementIds": { "type": "array", "items": { "type": "string", "pattern": "^(REQ|BR|AC)-[A-Z0-9]+(-[A-Z0-9]+)*$" } } } },
|
|
25
|
+
"field": { "type": "object", "additionalProperties": false, "required": ["name", "sourceRefs"], "properties": { "name": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "required": { "type": "boolean" }, "description": { "type": "string" }, "format": { "type": "string", "minLength": 1 }, "comparison": { "enum": ["exact", "parseable-only", "semantic"] }, "precision": { "type": "string", "minLength": 1 }, "sourceRefs": { "type": "array", "items": { "$ref": "#/$defs/sourceRef" } } } },
|
|
26
|
+
"responseBody": { "type": "object", "additionalProperties": false, "required": ["kind", "ordering"], "properties": { "kind": { "enum": ["array", "object", "scalar", "empty", "unknown"] }, "itemSchemaRef": { "type": "string", "minLength": 1 }, "ordering": { "enum": ["specified", "unspecified", "not-applicable"] }, "description": { "type": "string", "minLength": 1 } } },
|
|
34
27
|
"errorCase": { "type": "object", "additionalProperties": false, "required": ["description"], "properties": { "status": { "type": "integer", "minimum": 400, "maximum": 599 }, "code": { "type": "string", "minLength": 1 }, "messageField": { "type": "string", "minLength": 1 }, "description": { "type": "string", "minLength": 1 } } },
|
|
35
28
|
"acceptanceCriterion": { "type": "object", "additionalProperties": false, "required": ["id", "text", "sourceRef"], "properties": { "id": { "type": "string", "pattern": "^AC-[A-Z0-9]+(-[A-Z0-9]+)*$" }, "text": { "type": "string", "minLength": 1 }, "sourceRef": { "$ref": "#/$defs/sourceRef" } } },
|
|
36
|
-
"endpoint": { "type": "object", "additionalProperties": false, "required": ["id", "method", "path", "requestFields", "responseFields", "successStatuses", "errorCases"], "properties": { "id": { "type": "string", "minLength": 1 }, "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "requestFields": { "type": "array", "items": { "$ref": "#/$defs/field" } }, "responseFields": { "type": "array", "items": { "$ref": "#/$defs/field" } }, "successStatuses": { "type": "array", "items": { "type": "integer", "minimum": 100, "maximum": 399 } }, "errorCases": { "type": "array", "items": { "$ref": "#/$defs/errorCase" } } } },
|
|
29
|
+
"endpoint": { "type": "object", "additionalProperties": false, "required": ["id", "method", "path", "requestFields", "responseFields", "responseBody", "successStatuses", "errorCases", "sourceRefs"], "properties": { "id": { "type": "string", "minLength": 1 }, "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "requestFields": { "type": "array", "items": { "$ref": "#/$defs/field" } }, "responseFields": { "type": "array", "items": { "$ref": "#/$defs/field" } }, "responseBody": { "$ref": "#/$defs/responseBody" }, "successStatuses": { "type": "array", "items": { "type": "integer", "minimum": 100, "maximum": 399 } }, "errorCases": { "type": "array", "items": { "$ref": "#/$defs/errorCase" } }, "sourceRefs": { "type": "array", "items": { "$ref": "#/$defs/sourceRef" } } } },
|
|
37
30
|
"evidencedDescription": { "type": "object", "additionalProperties": false, "required": ["id", "description", "sourceRef"], "properties": { "id": { "type": "string", "minLength": 1 }, "description": { "type": "string", "minLength": 1 }, "sourceRef": { "$ref": "#/$defs/sourceRef" } } },
|
|
38
31
|
"businessRule": { "type": "object", "additionalProperties": false, "required": ["id", "text", "sourceRef"], "properties": { "id": { "type": "string", "minLength": 1 }, "text": { "type": "string", "minLength": 1 }, "sourceRef": { "$ref": "#/$defs/sourceRef" } } },
|
|
39
32
|
"stateTransition": { "type": "object", "additionalProperties": false, "required": ["from", "to", "trigger", "sourceRef"], "properties": { "from": { "type": "string", "minLength": 1 }, "to": { "type": "string", "minLength": 1 }, "trigger": { "type": "string", "minLength": 1 }, "sourceRef": { "$ref": "#/$defs/sourceRef" } } },
|