@tea-agent/loop-agent 0.16.1 → 0.16.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 (33) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/executors/dag-pi-executor.js +4 -2
  3. package/dist/executors/pi-sdk-executor.js +66 -3
  4. package/dist/executors/shell-executor.js +239 -29
  5. package/dist/executors/shell-presets.js +12 -2
  6. package/dist/executors/shell-write-guard.js +20 -1
  7. package/dist/shared/git-progress.js +9 -2
  8. package/dist/worker/observability/read-model.js +56 -0
  9. package/dist/worker/observe/server.js +6 -3
  10. package/dist/workflows/dag/backend-test-analysis-contract.js +87 -30
  11. package/dist/workflows/dag/backend-test-case-manifest.js +71 -8
  12. package/dist/workflows/dag/backend-test-execution-contract.js +63 -11
  13. package/dist/workflows/dag/backend-test-repair-contract.js +94 -0
  14. package/dist/workflows/dag/backend-test-result-contract.js +6 -4
  15. package/dist/workflows/dag/backend-test-semantic-review-contract.js +36 -0
  16. package/dist/workflows/dag/dynamic-runtime/condition.js +1 -1
  17. package/dist/workflows/dag/dynamic-runtime/shared.js +42 -0
  18. package/dist/workflows/dag/failure-routing.js +1 -1
  19. package/dist/workflows/dag/frontend-implementation-contract.js +32 -16
  20. package/dist/workflows/dag/frontend-worktree-diff.js +127 -0
  21. package/dist/workflows/dag/init-hybrid.js +616 -120
  22. package/dist/workflows/dag/lifecycle.js +33 -2
  23. package/dist/workflows/dag/scheduler.js +87 -17
  24. package/dist/workflows/dag/types.js +31 -0
  25. package/dist/workflows/dag/validate.js +20 -14
  26. package/docs/templates/agent-dag.schema.json +25 -2
  27. package/docs/templates/backend-test-analysis.schema.json +9 -16
  28. package/docs/templates/backend-test-dag.json +493 -197
  29. package/docs/templates/backend-test-dag.review-cases.prompt.md +10 -4
  30. package/docs/templates/backend-test-execution.schema.json +6 -1
  31. package/package.json +1 -1
  32. package/skills/frontend-review/SKILL.md +6 -2
  33. 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 = routeDagFailure({
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
- function shouldSkipNode(node, task, nodes) {
6
- if (!node || node.status !== "PENDING")
7
- return false;
8
- return task.depends_on.some((depId) => {
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
- return !dep || dep.status === "ERROR" || dep.status === "SKIPPED";
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 runnable = rank.filter((id) => {
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
- if (!node || node.status !== "PENDING")
34
- return false;
35
- return !shouldSkipNode(node, task, input.state.nodes);
80
+ return dependencyReadiness(task, input.state.nodes) === "run";
36
81
  });
37
- const blocked = rank.filter((id) => {
38
- const node = input.state.nodes[id];
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 (node?.status === "PENDING" &&
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 = runnable.filter((id) => {
123
+ const pauseGateRunnable = actuallyRunnable.filter((id) => {
54
124
  const task = input.tasksById.get(id);
55
125
  return isPauseOnHumanDecisionGate(task);
56
126
  });
57
- const regularRunnable = runnable.filter((id) => !pauseGateRunnable.includes(id));
58
- const rankWriterNodeIds = runnable.filter((id) => {
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
- if (!upstreamIds.has(verdictGate.fromNodeId)) {
141
- issues.push({
142
- type: "invalid-verdict-gate-config",
143
- message: `task ${task.id} shell.verdictGate.fromNodeId "${verdictGate.fromNodeId}" must appear in depends_on`,
144
- });
145
- }
146
- const upstreamTask = spec.tasks.find((candidate) => candidate.id === verdictGate.fromNodeId);
147
- if (upstreamTask && upstreamTask.executor === "shell") {
148
- issues.push({
149
- type: "invalid-verdict-gate-config",
150
- message: `task ${task.id} shell.verdictGate.fromNodeId "${verdictGate.fromNodeId}" must reference a non-shell upstream verdict node`,
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": "Upstream node id whose canonical JSON output in the runner-injected current run directory ($HARNESS_DAG_RUN_DIR/<fromNodeId>.json) supplies the verdict line."
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-v1.json",
4
- "title": "Backend Test Analysis v1",
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": 1 },
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
- "field": { "type": "object", "additionalProperties": false, "required": ["name"], "properties": { "name": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "required": { "type": "boolean" }, "description": { "type": "string" } } },
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" } } },