@tea-agent/loop-agent 0.33.7-beta.0 → 0.34.1
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 +67 -16
- package/dist/application/task-lifecycle/advance.js +309 -9
- package/dist/application/task-lifecycle/gates.js +50 -0
- package/dist/application/task-lifecycle/observe.js +11 -2
- package/dist/application/task-lifecycle/plan-transitions.js +13 -21
- package/dist/commands/init-upgrade.js +32 -1
- package/dist/commands/init.js +94 -3
- package/dist/executors/dag-pi-executor.js +18 -3
- package/dist/executors/shell-executor.js +4 -91
- package/dist/executors/shell-write-guard.js +26 -8
- package/dist/shared/operator/capabilities.js +98 -44
- package/dist/shared/resilient-git.js +133 -0
- package/dist/task/source-prepare/artifact-meta.js +137 -0
- package/dist/task/source-prepare/index.js +2 -0
- package/dist/task/source-prepare/parse-intent.js +58 -10
- package/dist/task/source-prepare/prepare.js +229 -18
- package/dist/task/source-prepare/reference-integrity.js +18 -2
- package/dist/task/source-prepare/semantic-intake.js +404 -0
- package/dist/worker/console/app-data.js +2 -0
- package/dist/worker/console/chat/chat-event-store.js +190 -25
- package/dist/worker/console/chat/model-resolver.js +17 -0
- package/dist/worker/console/chat/pi-console-config.js +250 -32
- package/dist/worker/console/chat/pi-runtime.js +1007 -188
- package/dist/worker/console/chat/resource-loader.js +5 -4
- package/dist/worker/console/chat/routes.js +495 -157
- package/dist/worker/console/chat/runtime-context.js +48 -12
- package/dist/worker/console/chat/runtime-selection.js +59 -0
- package/dist/worker/console/chat/session-store.js +39 -0
- package/dist/worker/console/chat/shortcuts.js +1 -0
- package/dist/worker/console/chat/tool-adapter.js +9 -3
- package/dist/worker/console/chat/tools.js +5 -1
- package/dist/worker/console/dag-execution-receipt.js +380 -0
- package/dist/worker/console/interview/grill-me.js +7 -6
- package/dist/worker/console/operator-actions.js +785 -85
- package/dist/worker/console/prd-intake-bridge.js +393 -0
- package/dist/worker/console/recovery-cta.js +35 -6
- package/dist/worker/console/server.js +8 -15
- package/dist/worker/console/static/assets/index-BQkhJpV8.css +1 -0
- package/dist/worker/console/static/assets/index-BpuHmlSP.js +29 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +1 -0
- package/dist/worker/console/static-src/app/usePrdImport.js +2 -1
- package/dist/worker/console/static-src/app/useRecoveryActions.js +24 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +19 -1
- package/dist/worker/console/static-src/app/useTaskWizard.js +57 -2
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +45 -8
- package/dist/worker/console/static-src/operator-chat/landing-density.js +23 -0
- package/dist/worker/console/static-src/operator-chat/refs.js +9 -0
- package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +257 -0
- package/dist/worker/console/static-src/operator-chat/session-title-watcher.js +128 -0
- package/dist/worker/console/static-src/operator-chat/sidebar-split.js +90 -0
- package/dist/worker/console/static-src/operator-chat/spatial-overlay.js +37 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +125 -22
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +215 -184
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +49 -5
- package/dist/worker/console/static-src/operator-chat/useComposer.js +17 -0
- package/dist/worker/console/static-src/operator-chat/useOverlayFocus.js +84 -0
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +225 -74
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +196 -0
- package/dist/worker/console/static-src/operator-chat/useWorkspaceLayout.js +58 -0
- package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +31 -0
- package/dist/worker/delivery/final-verification.js +13 -5
- package/dist/worker/delivery/package.js +31 -19
- package/dist/worker/delivery/verification-bundle.js +6 -4
- package/dist/worker/observability/read-model.js +3 -0
- package/dist/worker/observe/static/operator-chrome.css +5 -2
- package/dist/worker/observe/static/operator-chrome.js +6 -1
- package/dist/worker/observe/static/styles.css +39 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +33 -462
- package/dist/workflows/dag/backend-test-case-manifest.js +0 -4
- package/dist/workflows/dag/backend-test-markdown-workflow.js +1 -25
- package/dist/workflows/dag/backend-test-module-stem.js +0 -5
- package/dist/workflows/dag/backend-test-pytest-collection.js +24 -345
- package/dist/workflows/dag/backend-test-scenario-param.js +82 -269
- package/dist/workflows/dag/backend-test-writer-completeness.js +16 -47
- package/dist/workflows/dag/dynamic-runtime/map.js +8 -24
- package/dist/workflows/dag/failure-category.js +3 -0
- package/dist/workflows/dag/frontend-worktree-diff.js +12 -27
- package/dist/workflows/dag/init-hybrid.js +49 -55
- package/dist/workflows/dag/node-execution.js +3 -2
- package/dist/workflows/dag/retry-policy.js +44 -11
- package/dist/workflows/dag/runner.js +6 -0
- package/dist/workflows/dag/scheduler.js +49 -1
- package/dist/workflows/dag/types.js +0 -7
- package/dist/workflows/dag/validate.js +16 -2
- package/dist/workflows/dag/workspace-checkpoint.js +8 -27
- package/docs/templates/agent-dag.schema.json +4 -4
- package/docs/templates/backend-test-dag.json +29 -32
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/local-jacoco-coverage/SKILL.md +281 -0
- package/skills/local-jacoco-coverage/references/requirement-to-source-mapping.md +85 -0
- package/skills/local-jacoco-coverage/references/runtime-alignment.md +106 -0
- package/skills/local-jacoco-coverage/scripts/run-coverage-analysis.sh +148 -0
- package/skills/local-jacoco-coverage/scripts/start-jacoco-agent.sh +110 -0
- package/skills/loop-agent/references/command-reference.md +3 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +13 -0
- package/skills/loop-agent/references/task-workflow.md +4 -0
- package/dist/worker/console/chat/instruction-skills.js +0 -217
- package/dist/worker/console/static/assets/index-CnUXAqxG.css +0 -1
- package/dist/worker/console/static/assets/index-CteJFFL2.js +0 -29
|
@@ -98,16 +98,10 @@ async function listMarkdownCases(workspaceRoot) {
|
|
|
98
98
|
const allPoints = orderedUnique(body.match(/\bTP-[A-Z0-9-]+\b/g) ?? []);
|
|
99
99
|
const variantLabel = body.match(/(?:Variant Test Points|\u53d8\u4f53\u6d4b\u8bd5\u70b9)\s*[::]\s*([^\n]+)/i)?.[1] ??
|
|
100
100
|
"";
|
|
101
|
-
const variantLabelDeclared = variantLabel.trim().length > 0;
|
|
102
|
-
const variantLabelIsNone = /^(?:none|无|n\/a|not-applicable)\b/i.test(variantLabel.trim());
|
|
103
101
|
const variantFromLabel = orderedUnique((variantLabel.match(/\bTP-[A-Z0-9-]+\b/g) ?? []).filter((item) => item.toUpperCase() !== "NONE"));
|
|
104
|
-
const variantTestPoints =
|
|
105
|
-
?
|
|
106
|
-
:
|
|
107
|
-
? variantFromLabel
|
|
108
|
-
: variantLabelDeclared
|
|
109
|
-
? []
|
|
110
|
-
: allPoints.filter((tp) => /(EMPTY|MISSING|NULL|MIN|MAX|INVALID|WRONG|ENUM|PATTERN|NOMINAL|BOUNDARY)/i.test(tp));
|
|
102
|
+
const variantTestPoints = variantFromLabel.length > 0
|
|
103
|
+
? variantFromLabel
|
|
104
|
+
: allPoints.filter((tp) => /(EMPTY|MISSING|NULL|MIN|MAX|INVALID|WRONG|ENUM|PATTERN|NOMINAL|BOUNDARY)/i.test(tp));
|
|
111
105
|
const scriptMatch = body.match(/`?(testcase\/[A-Za-z0-9_./-]*test_[A-Za-z0-9_.-]*\.py)`?/i);
|
|
112
106
|
cases.push({
|
|
113
107
|
caseId,
|
|
@@ -121,123 +115,23 @@ async function listMarkdownCases(workspaceRoot) {
|
|
|
121
115
|
}
|
|
122
116
|
return cases;
|
|
123
117
|
}
|
|
124
|
-
function parseScenarioIntentLine(line) {
|
|
125
|
-
// Target may contain commas (body.title,body.status). Intent may contain `|` / spaces.
|
|
126
|
-
// Stop field values only at known trailing keys or end-of-line.
|
|
127
|
-
const precise = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;]\s*operation\s*=\s*([^;\n]+?)\s*[|;]\s*target\s*=\s*([^;\n]+?)\s*[|;]\s*intent\s*=\s*(.+?)(?:\s*[|;]\s*bound\s*=\s*(\d+))?(?:\s*[|;]\s*example\s*=\s*([^;\n]+))?(?:\s*[|;]\s*expectedCode\s*=\s*[^;\n]+)?\s*$/i.exec(line.trim());
|
|
128
|
-
if (precise) {
|
|
129
|
-
return {
|
|
130
|
-
tpId: precise[1],
|
|
131
|
-
rawIntent: (precise[4] ?? "unknown").trim(),
|
|
132
|
-
target: precise[3]?.trim(),
|
|
133
|
-
bound: precise[5] ? Number(precise[5]) : undefined,
|
|
134
|
-
example: precise[6]?.trim(),
|
|
135
|
-
legacy: false,
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
const legacy = /场景意图\s*[::]\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*field\s*=\s*([A-Za-z0-9_.]+))?(?:\s*[|;,,]\s*bound\s*=\s*(\d+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(line.trim());
|
|
139
|
-
if (legacy) {
|
|
140
|
-
return {
|
|
141
|
-
rawIntent: (legacy[1] ?? "unknown").trim(),
|
|
142
|
-
field: legacy[2],
|
|
143
|
-
bound: legacy[3] ? Number(legacy[3]) : undefined,
|
|
144
|
-
example: legacy[4]?.trim(),
|
|
145
|
-
legacy: true,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
|
-
function resolveScenarioIntentField(target, field) {
|
|
151
|
-
if (field)
|
|
152
|
-
return field;
|
|
153
|
-
if (!target || target === "request")
|
|
154
|
-
return undefined;
|
|
155
|
-
const bodyTargets = target
|
|
156
|
-
.split(/\s*,\s*/)
|
|
157
|
-
.map((item) => item.trim())
|
|
158
|
-
.filter((item) => /^(?:body|json|payload)\./i.test(item));
|
|
159
|
-
const preferred = bodyTargets.find((item) => /\.status$/i.test(item)) ??
|
|
160
|
-
bodyTargets[0] ??
|
|
161
|
-
target.split(/\s*,\s*/)[0];
|
|
162
|
-
return preferred?.split(".").at(-1);
|
|
163
|
-
}
|
|
164
|
-
function resolveCustomLiteralIntent(rawIntent, field) {
|
|
165
|
-
const lower = rawIntent.toLowerCase();
|
|
166
|
-
if (!lower.startsWith("custom-literal:")) {
|
|
167
|
-
return CLOSED_SET.has(lower) ? lower : "unknown";
|
|
168
|
-
}
|
|
169
|
-
// Preserve literal case: enum values such as ACTIVE must not be lowercased.
|
|
170
|
-
const prefixLength = rawIntent.toLowerCase().startsWith("custom-literal:")
|
|
171
|
-
? rawIntent.indexOf(":") + 1
|
|
172
|
-
: "custom-literal:".length;
|
|
173
|
-
const payload = rawIntent.slice(prefixLength);
|
|
174
|
-
if (!payload.includes("|") && !payload.includes(",")) {
|
|
175
|
-
return `custom-literal:${payload}`;
|
|
176
|
-
}
|
|
177
|
-
const parts = payload.split(/\s*[|,]\s*/).map((item) => item.trim()).filter(Boolean);
|
|
178
|
-
if (field) {
|
|
179
|
-
const byField = parts.find((part) => part.toLowerCase().startsWith(`${field.toLowerCase()}=`));
|
|
180
|
-
if (byField) {
|
|
181
|
-
return `custom-literal:${byField.slice(field.length + 1)}`;
|
|
182
|
-
}
|
|
183
|
-
if (/^status$/i.test(field)) {
|
|
184
|
-
const enumLike = parts.find((part) => /^(?:ACTIVE|ARCHIVED|INACTIVE|DISABLED|ENABLED)$/i.test(part));
|
|
185
|
-
if (enumLike)
|
|
186
|
-
return `custom-literal:${enumLike}`;
|
|
187
|
-
}
|
|
188
|
-
if (/^title$/i.test(field)) {
|
|
189
|
-
const titleLike = parts.find((part) => !/^(?:ACTIVE|ARCHIVED|INACTIVE|DISABLED|ENABLED)$/i.test(part));
|
|
190
|
-
if (titleLike)
|
|
191
|
-
return `custom-literal:${titleLike}`;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
return parts.length === 1
|
|
195
|
-
? `custom-literal:${parts[0]}`
|
|
196
|
-
: "unknown";
|
|
197
|
-
}
|
|
198
118
|
export function inferScenarioParamIntent(input) {
|
|
199
119
|
const { tpId, caseBody } = input;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
(
|
|
213
|
-
? {
|
|
214
|
-
rawIntent: fallbackLine[1] ?? "unknown",
|
|
215
|
-
legacy: true,
|
|
216
|
-
}
|
|
217
|
-
: undefined);
|
|
218
|
-
if (machine) {
|
|
219
|
-
const field = resolveScenarioIntentField(machine.target, machine.field);
|
|
220
|
-
const rawIntent = machine.rawIntent.trim();
|
|
221
|
-
const example = machine.example;
|
|
222
|
-
const lower = rawIntent.toLowerCase();
|
|
223
|
-
const normalizedIntent = lower === "nominal-operation" ? "nominal" : lower;
|
|
224
|
-
const dataSection = sectionBody(caseBody, ["测试数据", "Test Data", "操作步骤", "Steps"]);
|
|
225
|
-
const fieldScopedBound = field
|
|
226
|
-
? new RegExp(`${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]{0,80}?(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})`, "i").exec(`${caseBody}\n${dataSection}`) ??
|
|
227
|
-
new RegExp(`(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i").exec(`${caseBody}\n${dataSection}`)
|
|
228
|
-
: undefined;
|
|
229
|
-
const boundFromBody = fieldScopedBound ??
|
|
230
|
-
/(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
|
|
231
|
-
/(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
|
|
232
|
-
const bound = machine.bound && machine.bound > 0
|
|
233
|
-
? machine.bound
|
|
234
|
-
: boundFromBody
|
|
235
|
-
? Number(boundFromBody[1])
|
|
236
|
-
: machine.bound;
|
|
237
|
-
if (rawIntent.toLowerCase().startsWith("custom-literal:")) {
|
|
238
|
-
const intent = resolveCustomLiteralIntent(rawIntent, field);
|
|
120
|
+
const preciseLine = /场景意图\s*[::]\s*(TP-[A-Z0-9-]+)\s*[|;,,]\s*operation\s*=\s*([^|;,,\n]+)\s*[|;,,]\s*target\s*=\s*([A-Za-z0-9_.-]+)\s*[|;,,]\s*intent\s*=\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*bound\s*=\s*(\d+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(caseBody);
|
|
121
|
+
const legacyLine = /场景意图\s*[::]\s*([a-z0-9+._:-]+)(?:\s*[|;,,]\s*field\s*=\s*([A-Za-z0-9_.]+))?(?:\s*[|;,,]\s*bound\s*=\s*(\d+))?(?:\s*[|;,,]\s*example\s*=\s*([^|\n]+))?/i.exec(caseBody);
|
|
122
|
+
const fallbackLine = new RegExp(`${tpId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]{0,160}?intent\\s*[=:]\\s*([a-z0-9+._:-]+)`, "i").exec(caseBody);
|
|
123
|
+
const machineLine = preciseLine ?? legacyLine ?? fallbackLine;
|
|
124
|
+
if (machineLine) {
|
|
125
|
+
const rawIntent = (preciseLine ? preciseLine[4] : machineLine[1] ?? "unknown").toLowerCase();
|
|
126
|
+
const target = preciseLine?.[3];
|
|
127
|
+
const field = target && target !== "request" ? target.split(".").at(-1) : legacyLine?.[2];
|
|
128
|
+
const boundRaw = preciseLine?.[5] ?? legacyLine?.[3];
|
|
129
|
+
const example = (preciseLine?.[6] ?? legacyLine?.[4])?.trim();
|
|
130
|
+
const bound = boundRaw ? Number(boundRaw) : undefined;
|
|
131
|
+
const normalizedIntent = rawIntent === "nominal-operation" ? "nominal" : rawIntent;
|
|
132
|
+
if (normalizedIntent.startsWith("custom-literal:")) {
|
|
239
133
|
return {
|
|
240
|
-
intent,
|
|
134
|
+
intent: normalizedIntent,
|
|
241
135
|
field,
|
|
242
136
|
bound,
|
|
243
137
|
example,
|
|
@@ -281,13 +175,7 @@ export function inferScenarioParamIntent(input) {
|
|
|
281
175
|
? fieldFromTp.charAt(0).toLowerCase() + fieldFromTp.slice(1)
|
|
282
176
|
: undefined;
|
|
283
177
|
const dataSection = sectionBody(caseBody, ["测试数据", "Test Data", "操作步骤", "Steps"]);
|
|
284
|
-
const
|
|
285
|
-
? new RegExp(`${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[^\\n]{0,80}?(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})`, "i").exec(`${caseBody}\n${dataSection}`) ??
|
|
286
|
-
new RegExp(`(?:maxLength|max(?:imum)?|上限|最大长度)\\s*[=::]?\\s*(\\d{1,4})[^\\n]{0,80}?${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i").exec(`${caseBody}\n${dataSection}`)
|
|
287
|
-
: undefined;
|
|
288
|
-
const boundMatch = fieldScopedBound ??
|
|
289
|
-
/(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ??
|
|
290
|
-
/(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
|
|
178
|
+
const boundMatch = /(?:maxLength|max(?:imum)?|上限|最大长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`) ?? /(?:minLength|min(?:imum)?|下限|最小长度)\s*[=::]?\s*(\d{1,4})/i.exec(`${caseBody}\n${dataSection}`);
|
|
291
179
|
const bound = boundMatch ? Number(boundMatch[1]) : undefined;
|
|
292
180
|
const exampleMatch = /(?:反例|invalid example|example)\s*[=::]\s*[`"]?([^`"\n]+)[`"]?/i.exec(caseBody);
|
|
293
181
|
const example = exampleMatch?.[1]?.trim();
|
|
@@ -360,27 +248,18 @@ export function observeParamFeatures(block, field) {
|
|
|
360
248
|
if (/\bFaker\b|\bfake\b|\brandom\b|\buuid4\b|\bfactory\b/i.test(block)) {
|
|
361
249
|
return { kind: "call", text: "dynamic-or-faker" };
|
|
362
250
|
}
|
|
363
|
-
// Prefer dict-like payload extraction
|
|
364
|
-
// Writers often build the payload in the test body and only pass scalar intent
|
|
365
|
-
// values in pytest.param; in that case fall through to positional observation.
|
|
251
|
+
// Prefer dict-like payload extraction.
|
|
366
252
|
const dictMatch = /\{\s*([\s\S]*?)\s*\}/.exec(block) ??
|
|
367
253
|
/(?:payload|body|data|json)\s*=\s*(\{[\s\S]*?\})/.exec(block);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
? new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:`)
|
|
371
|
-
: undefined;
|
|
372
|
-
const dictHasField = Boolean(fieldPattern && fieldPattern.test(dictText));
|
|
373
|
-
const dictLooksLikePayload = Boolean(dictMatch) &&
|
|
374
|
-
(/["'][A-Za-z_][A-Za-z0-9_]*["']\s*:/.test(dictText) ||
|
|
375
|
-
/(?:payload|body|data|json)\s*=/.test(block));
|
|
376
|
-
if (dictLooksLikePayload && !field) {
|
|
377
|
-
return { kind: "dict", text: dictText.replace(/\s+/g, " ").slice(0, 160) };
|
|
254
|
+
if (dictMatch && !field) {
|
|
255
|
+
return { kind: "dict", text: (dictMatch[0] ?? dictMatch[1] ?? "{}").replace(/\s+/g, " ").slice(0, 160) };
|
|
378
256
|
}
|
|
379
|
-
if (
|
|
380
|
-
|
|
257
|
+
if (dictMatch && field) {
|
|
258
|
+
const dict = dictMatch[0] ?? dictMatch[1] ?? "";
|
|
259
|
+
const fieldPattern = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:`);
|
|
260
|
+
if (!fieldPattern.test(dict)) {
|
|
381
261
|
return { kind: "missing-key", text: `missing:${field}` };
|
|
382
262
|
}
|
|
383
|
-
const dict = dictText;
|
|
384
263
|
const valueMatch = new RegExp(`["']${field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']\\s*:\\s*([^,}\\n]+)`).exec(dict);
|
|
385
264
|
const raw = valueMatch?.[1]?.trim() ?? "";
|
|
386
265
|
if (raw === "None" || raw === "null") {
|
|
@@ -389,19 +268,6 @@ export function observeParamFeatures(block, field) {
|
|
|
389
268
|
if (raw === '""' || raw === "''") {
|
|
390
269
|
return { kind: "empty-string", text: '""', length: 0, literal: "" };
|
|
391
270
|
}
|
|
392
|
-
const repeatedValue = /^(["'])([^"'\\])\1\s*\*\s*(\d{1,5})$/.exec(raw);
|
|
393
|
-
if (repeatedValue) {
|
|
394
|
-
const ch = repeatedValue[2] ?? "x";
|
|
395
|
-
const length = Number(repeatedValue[3] ?? "0");
|
|
396
|
-
const value = ch.repeat(Math.max(0, length));
|
|
397
|
-
return {
|
|
398
|
-
kind: length === 0 ? "empty-string" : "string",
|
|
399
|
-
text: JSON.stringify(value),
|
|
400
|
-
length,
|
|
401
|
-
hasUppercase: /[A-Z]/.test(value),
|
|
402
|
-
literal: value,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
271
|
const str = /^["']([\s\S]*)["']$/.exec(raw);
|
|
406
272
|
if (str) {
|
|
407
273
|
const value = str[1] ?? "";
|
|
@@ -430,83 +296,31 @@ export function observeParamFeatures(block, field) {
|
|
|
430
296
|
}
|
|
431
297
|
return { kind: "unknown", text: raw || compact.slice(0, 120) };
|
|
432
298
|
}
|
|
433
|
-
//
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
kind: length === 0 ? "empty-string" : "string",
|
|
442
|
-
text: JSON.stringify(value),
|
|
443
|
-
length,
|
|
444
|
-
hasUppercase: /[A-Z]/.test(value),
|
|
445
|
-
literal: value,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
// Positional args: skip a leading TP-id label when writers emit
|
|
449
|
-
// pytest.param("TP-...", actual_value, id="TP-...").
|
|
450
|
-
const paramOpen = /pytest\.param\s*\(/.exec(block);
|
|
451
|
-
if (paramOpen) {
|
|
452
|
-
const afterOpen = block.slice((paramOpen.index ?? 0) + paramOpen[0].length);
|
|
453
|
-
const positionalLiterals = [];
|
|
454
|
-
const literalRe = /\s*(None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*(?:,|$)/g;
|
|
455
|
-
let match;
|
|
456
|
-
while ((match = literalRe.exec(afterOpen)) !== null) {
|
|
457
|
-
// Stop once keyword args begin (id=/marks=) after a comma segment without a pure literal.
|
|
458
|
-
const between = afterOpen.slice(0, match.index);
|
|
459
|
-
if (/(?:^|,)\s*(?:id|marks)\s*=/.test(between + match[0]))
|
|
460
|
-
break;
|
|
461
|
-
positionalLiterals.push(match[1]);
|
|
462
|
-
if (positionalLiterals.length >= 4)
|
|
463
|
-
break;
|
|
299
|
+
// Positional first arg string/None.
|
|
300
|
+
const firstArg = /pytest\.param\s*\(\s*(None|True|False|-?\d+(?:\.\d+)?|"""[\s\S]*?"""|'''[\s\S]*?'''|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')/.exec(block);
|
|
301
|
+
if (firstArg) {
|
|
302
|
+
const raw = firstArg[1];
|
|
303
|
+
if (raw === "None")
|
|
304
|
+
return { kind: "none", text: "None", literal: "None" };
|
|
305
|
+
if (raw === '""' || raw === "''") {
|
|
306
|
+
return { kind: "empty-string", text: '""', length: 0, literal: "" };
|
|
464
307
|
}
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
const candidates = positionalLiterals.filter((raw) => {
|
|
468
|
-
const str = /^["']([\s\S]*)["']$/.exec(raw) ?? /^("""|''')([\s\S]*)\1$/.exec(raw);
|
|
469
|
-
if (!str)
|
|
470
|
-
return true;
|
|
308
|
+
const str = /^["']([\s\S]*)["']$/.exec(raw) ?? /^("""|''')([\s\S]*)\1$/.exec(raw);
|
|
309
|
+
if (str) {
|
|
471
310
|
const value = (str[2] ?? str[1] ?? "").replace(/^["']|["']$/g, "");
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
if (raw) {
|
|
480
|
-
if (raw === "None")
|
|
481
|
-
return { kind: "none", text: "None", literal: "None" };
|
|
482
|
-
if (raw === '""' || raw === "''") {
|
|
483
|
-
return { kind: "empty-string", text: '""', length: 0, literal: "" };
|
|
484
|
-
}
|
|
485
|
-
const str = /^["']([\s\S]*)["']$/.exec(raw) ?? /^("""|''')([\s\S]*)\1$/.exec(raw);
|
|
486
|
-
if (str) {
|
|
487
|
-
const value = (str[2] ?? str[1] ?? "").replace(/^["']|["']$/g, "");
|
|
488
|
-
return {
|
|
489
|
-
kind: value.length === 0
|
|
490
|
-
? "empty-string"
|
|
491
|
-
: "string",
|
|
492
|
-
text: JSON.stringify(value),
|
|
493
|
-
length: value.length,
|
|
494
|
-
hasUppercase: /[A-Z]/.test(value),
|
|
495
|
-
literal: value,
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
if (/^-?\d+(?:\.\d+)?$/.test(raw)) {
|
|
499
|
-
return { kind: "number", text: raw, literal: raw };
|
|
500
|
-
}
|
|
501
|
-
if (raw === "True" || raw === "False") {
|
|
502
|
-
return { kind: "boolean", text: raw, literal: raw };
|
|
503
|
-
}
|
|
504
|
-
return { kind: "unknown", text: raw };
|
|
311
|
+
return {
|
|
312
|
+
kind: "string",
|
|
313
|
+
text: JSON.stringify(value),
|
|
314
|
+
length: value.length,
|
|
315
|
+
hasUppercase: /[A-Z]/.test(value),
|
|
316
|
+
literal: value,
|
|
317
|
+
};
|
|
505
318
|
}
|
|
319
|
+
return { kind: "unknown", text: raw };
|
|
506
320
|
}
|
|
507
321
|
return { kind: "unknown", text: compact.slice(0, 160) };
|
|
508
322
|
}
|
|
509
|
-
function compareIntent(intent, observed, bound, example
|
|
323
|
+
function compareIntent(intent, observed, bound, example) {
|
|
510
324
|
if (intent === "unknown")
|
|
511
325
|
return "UNDETERMINED";
|
|
512
326
|
if (observed.kind === "call" || observed.kind === "name") {
|
|
@@ -515,10 +329,7 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
515
329
|
switch (intent) {
|
|
516
330
|
case "empty":
|
|
517
331
|
return observed.kind === "empty-string" ||
|
|
518
|
-
(observed.kind === "string" &&
|
|
519
|
-
(observed.length === 0 ||
|
|
520
|
-
(typeof observed.literal === "string" &&
|
|
521
|
-
/^\s+$/.test(observed.literal))))
|
|
332
|
+
(observed.kind === "string" && observed.length === 0)
|
|
522
333
|
? "MATCH"
|
|
523
334
|
: observed.kind === "unknown"
|
|
524
335
|
? "UNDETERMINED"
|
|
@@ -536,39 +347,45 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
536
347
|
? "UNDETERMINED"
|
|
537
348
|
: "MISMATCH";
|
|
538
349
|
case "max":
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
350
|
+
return bound !== undefined &&
|
|
351
|
+
observed.kind === "string" &&
|
|
352
|
+
observed.length === bound
|
|
353
|
+
? "MATCH"
|
|
354
|
+
: bound === undefined
|
|
355
|
+
? "UNDETERMINED"
|
|
356
|
+
: observed.kind === "string"
|
|
357
|
+
? "MISMATCH"
|
|
358
|
+
: "UNDETERMINED";
|
|
547
359
|
case "max+1":
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
360
|
+
return bound !== undefined &&
|
|
361
|
+
observed.kind === "string" &&
|
|
362
|
+
observed.length === bound + 1
|
|
363
|
+
? "MATCH"
|
|
364
|
+
: bound === undefined
|
|
365
|
+
? "UNDETERMINED"
|
|
366
|
+
: observed.kind === "string"
|
|
367
|
+
? "MISMATCH"
|
|
368
|
+
: "UNDETERMINED";
|
|
555
369
|
case "min":
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
370
|
+
return bound !== undefined &&
|
|
371
|
+
observed.kind === "string" &&
|
|
372
|
+
observed.length === bound
|
|
373
|
+
? "MATCH"
|
|
374
|
+
: bound === undefined
|
|
375
|
+
? "UNDETERMINED"
|
|
376
|
+
: observed.kind === "string"
|
|
377
|
+
? "MISMATCH"
|
|
378
|
+
: "UNDETERMINED";
|
|
563
379
|
case "min-1":
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
380
|
+
return bound !== undefined &&
|
|
381
|
+
observed.kind === "string" &&
|
|
382
|
+
observed.length === Math.max(0, bound - 1)
|
|
383
|
+
? "MATCH"
|
|
384
|
+
: bound === undefined
|
|
385
|
+
? "UNDETERMINED"
|
|
386
|
+
: observed.kind === "string"
|
|
387
|
+
? "MISMATCH"
|
|
388
|
+
: "UNDETERMINED";
|
|
572
389
|
case "pattern-invalid":
|
|
573
390
|
if (observed.kind === "string" && example !== undefined) {
|
|
574
391
|
return observed.literal === example ? "MATCH" : "MISMATCH";
|
|
@@ -588,10 +405,6 @@ function compareIntent(intent, observed, bound, example, field) {
|
|
|
588
405
|
? "UNDETERMINED"
|
|
589
406
|
: "UNDETERMINED";
|
|
590
407
|
case "nominal":
|
|
591
|
-
// Request-level nominal (no body field) may legitimately pass None/empty query filters.
|
|
592
|
-
if (!field && (observed.kind === "none" || observed.kind === "missing-key")) {
|
|
593
|
-
return "MATCH";
|
|
594
|
-
}
|
|
595
408
|
return observed.kind === "string" ||
|
|
596
409
|
observed.kind === "number" ||
|
|
597
410
|
observed.kind === "boolean" ||
|
|
@@ -699,7 +512,7 @@ export async function assessBackendScenarioParamConsistency(input) {
|
|
|
699
512
|
const observed = observeParamFeatures(block, inferred.field);
|
|
700
513
|
const status = !source || fallbackSourced
|
|
701
514
|
? "UNDETERMINED"
|
|
702
|
-
: compareIntent(inferred.intent, observed, inferred.bound, inferred.example
|
|
515
|
+
: compareIntent(inferred.intent, observed, inferred.bound, inferred.example);
|
|
703
516
|
const repairability = repairabilityFor(status, inferred.intent, observed, inferred.example);
|
|
704
517
|
entries.push({
|
|
705
518
|
caseId: testCase.caseId,
|
|
@@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { isPriorityOnlyBackendTestModuleStem } from "./backend-test-module-stem.js";
|
|
6
6
|
/**
|
|
7
7
|
* Maps a backend-test generation writer task id to its writer-progress role.
|
|
8
8
|
* Returns undefined for nodes that are not completeness-gated generators.
|
|
@@ -103,8 +103,6 @@ function moduleStemInvalidReason(raw) {
|
|
|
103
103
|
const stem = normalizeBackendTestModuleStem(raw);
|
|
104
104
|
if (isPriorityOnlyBackendTestModuleStem(stem))
|
|
105
105
|
return "priority-only-module-stem";
|
|
106
|
-
if (isOpaqueHashBackendTestModuleStem(stem))
|
|
107
|
-
return "opaque-hash-module-stem";
|
|
108
106
|
if (stem.toLowerCase() === "readme")
|
|
109
107
|
return "reserved-module-stem";
|
|
110
108
|
if (!/^[a-z][a-z0-9_]*$/.test(stem))
|
|
@@ -116,40 +114,28 @@ function moduleStemInvalidReason(raw) {
|
|
|
116
114
|
function looksLikeValidModuleStem(raw) {
|
|
117
115
|
return moduleStemInvalidReason(raw) === undefined;
|
|
118
116
|
}
|
|
119
|
-
|
|
120
|
-
const lines = readme
|
|
121
|
-
.replaceAll("\r\n", "\n")
|
|
122
|
-
.replaceAll("\r", "\n")
|
|
123
|
-
.split("\n");
|
|
124
|
-
const headings = lines.flatMap((line, index) => line.trim() === "## Module Index" ? [index] : []);
|
|
125
|
-
if (headings.length === 0) {
|
|
126
|
-
return { section: "", structureIssues: ["missing-module-index"] };
|
|
127
|
-
}
|
|
128
|
-
if (headings.length > 1) {
|
|
129
|
-
return { section: "", structureIssues: ["duplicate-module-index"] };
|
|
130
|
-
}
|
|
131
|
-
const start = headings[0] + 1;
|
|
132
|
-
const relativeEnd = lines.slice(start).findIndex((line) => /^##\s+\S/.test(line.trim()));
|
|
133
|
-
const end = relativeEnd < 0 ? lines.length : start + relativeEnd;
|
|
134
|
-
return { section: lines.slice(start, end).join("\n"), structureIssues: [] };
|
|
135
|
-
}
|
|
136
|
-
/** Inspect only candidates inside the unique exact `## Module Index` section. */
|
|
117
|
+
/** Inspect only authoritative README module-index candidates. */
|
|
137
118
|
export function inspectModuleStemsFromReadme(readme) {
|
|
138
|
-
const extracted = extractCanonicalModuleIndexSection(readme);
|
|
139
119
|
const candidates = [];
|
|
140
|
-
const tableRowLines =
|
|
120
|
+
const tableRowLines = readme
|
|
121
|
+
.replaceAll("\r\n", "\n")
|
|
122
|
+
.replaceAll("\r", "\n")
|
|
123
|
+
.split("\n")
|
|
124
|
+
.filter((line) => line.includes("|"));
|
|
141
125
|
for (const line of tableRowLines) {
|
|
142
126
|
for (const match of line.matchAll(/`?testcase\/md\/([A-Za-z0-9_.-]+)\.md`?/g)) {
|
|
143
127
|
candidates.push(match[1]);
|
|
144
128
|
}
|
|
145
129
|
for (const match of line.matchAll(/\|\s*`?([A-Za-z0-9_.-]+)`?\s*\|\s*`?testcase\/test_/g)) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
// This compatibility shape can also capture a numeric count column.
|
|
131
|
+
// Keep valid stems and the priority-only defect we diagnose, while
|
|
132
|
+
// ignoring non-stem incidental cells such as `1`.
|
|
133
|
+
if (looksLikeValidModuleStem(match[1]) || isPriorityOnlyBackendTestModuleStem(match[1])) {
|
|
134
|
+
candidates.push(match[1]);
|
|
149
135
|
}
|
|
150
136
|
}
|
|
151
137
|
}
|
|
152
|
-
for (const match of
|
|
138
|
+
for (const match of readme.matchAll(/\[[^\]]+\]\(\.\/([A-Za-z0-9_.-]+)\.md\)/g)) {
|
|
153
139
|
candidates.push(match[1]);
|
|
154
140
|
}
|
|
155
141
|
const uniqueCandidates = orderedUnique(candidates);
|
|
@@ -159,18 +145,12 @@ export function inspectModuleStemsFromReadme(readme) {
|
|
|
159
145
|
? [{ raw, normalized: normalizeBackendTestModuleStem(raw), reasonCode }]
|
|
160
146
|
: [];
|
|
161
147
|
});
|
|
162
|
-
const validStems = orderedUnique(uniqueCandidates
|
|
163
|
-
.filter(looksLikeValidModuleStem)
|
|
164
|
-
.map((stem) => normalizeBackendTestModuleStem(stem)));
|
|
165
|
-
const structureIssues = [...extracted.structureIssues];
|
|
166
|
-
if (validStems.length > BACKEND_TEST_MAX_MODULE_COUNT) {
|
|
167
|
-
structureIssues.push(`excessive-module-count: ${validStems.length} > ${BACKEND_TEST_MAX_MODULE_COUNT}`);
|
|
168
|
-
}
|
|
169
148
|
return {
|
|
170
149
|
candidates: uniqueCandidates,
|
|
171
|
-
validStems
|
|
150
|
+
validStems: orderedUnique(uniqueCandidates
|
|
151
|
+
.filter(looksLikeValidModuleStem)
|
|
152
|
+
.map((stem) => normalizeBackendTestModuleStem(stem))),
|
|
172
153
|
invalid,
|
|
173
|
-
structureIssues,
|
|
174
154
|
};
|
|
175
155
|
}
|
|
176
156
|
/**
|
|
@@ -937,17 +917,6 @@ export async function assessBackendTestMdPlanCompleteness(workspaceRoot) {
|
|
|
937
917
|
// are produced by map_agent children and are NOT required here.
|
|
938
918
|
const stemInspection = inspectModuleStemsFromReadme(readme);
|
|
939
919
|
const indexedStems = stemInspection.validStems;
|
|
940
|
-
for (const structureIssue of stemInspection.structureIssues) {
|
|
941
|
-
if (!brokenPaths.includes("testcase/md/README.md")) {
|
|
942
|
-
brokenPaths.push("testcase/md/README.md");
|
|
943
|
-
}
|
|
944
|
-
issues.push({
|
|
945
|
-
code: "T5",
|
|
946
|
-
path: "testcase/md/README.md",
|
|
947
|
-
detail: structureIssue,
|
|
948
|
-
recoverable: true,
|
|
949
|
-
});
|
|
950
|
-
}
|
|
951
920
|
for (const invalid of stemInspection.invalid) {
|
|
952
921
|
if (!brokenPaths.includes("testcase/md/README.md")) {
|
|
953
922
|
brokenPaths.push("testcase/md/README.md");
|
|
@@ -367,26 +367,19 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
369
|
const budgetBlockedIds = new Set(blockedChildren.map((entry) => entry.nodeId));
|
|
370
|
-
const failOnTokenBudgetExhaustion = input.expansion.failOnTokenBudgetExhaustion === true;
|
|
371
370
|
const failedChildren = tolerateChildFailures
|
|
372
371
|
? []
|
|
373
372
|
: childNodeIds.filter((nodeId) => {
|
|
374
373
|
const record = input.state.nodes[nodeId];
|
|
375
374
|
if (!record || record.status === "FINISHED")
|
|
376
375
|
return false;
|
|
377
|
-
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
378
|
-
return
|
|
379
|
-
}
|
|
376
|
+
if (budgetBlockedIds.has(nodeId) || isBudgetBlockedRecord(record))
|
|
377
|
+
return false;
|
|
380
378
|
return true;
|
|
381
379
|
});
|
|
382
|
-
const finishedItemCount = childNodeIds.filter((nodeId) => input.state.nodes[nodeId]?.status === "FINISHED").length;
|
|
383
|
-
const blockedItemCount = budgetBlockedIds.size;
|
|
384
380
|
const aggregate = {
|
|
385
381
|
workflowNodeId: input.expansion.workflowNodeId,
|
|
386
382
|
itemCount: items.length,
|
|
387
|
-
expectedItemCount: items.length,
|
|
388
|
-
finishedItemCount,
|
|
389
|
-
blockedItemCount,
|
|
390
383
|
children: childNodeIds.map((nodeId, index) => ({
|
|
391
384
|
nodeId,
|
|
392
385
|
item: items[index],
|
|
@@ -406,30 +399,21 @@ export async function executeDynamicMapExpansion(input) {
|
|
|
406
399
|
?.reason,
|
|
407
400
|
})),
|
|
408
401
|
tokensUsed: totalTokensUsed,
|
|
409
|
-
maxTotalTokens: tokenBudget?.maxTotalTokens,
|
|
410
402
|
caseOutcomeNotes: caseOutcomeNotes.length > 0 ? caseOutcomeNotes : undefined,
|
|
411
403
|
};
|
|
412
|
-
const tokenBudgetFailure = failOnTokenBudgetExhaustion && blockedItemCount > 0;
|
|
413
404
|
return {
|
|
414
|
-
// With tolerateChildFailures: barrier succeeds
|
|
415
|
-
// case evidence.
|
|
416
|
-
|
|
417
|
-
ok: failedChildren.length === 0 && !tokenBudgetFailure,
|
|
405
|
+
// With tolerateChildFailures: barrier always succeeds; product outcomes live in
|
|
406
|
+
// case evidence + result materialization. Without it: real child failures fail-close.
|
|
407
|
+
ok: failedChildren.length === 0,
|
|
418
408
|
stdout: JSON.stringify(aggregate),
|
|
419
409
|
stderr: caseOutcomeNotes.length > 0
|
|
420
410
|
? `map children recorded as case outcomes: ${caseOutcomeNotes
|
|
421
411
|
.map((entry) => `${entry.nodeId}=${entry.status}:${entry.reason}`)
|
|
422
412
|
.join(", ")}`
|
|
423
|
-
: tokenBudgetFailure
|
|
424
|
-
? `dynamic map token budget exhausted: expected=${items.length} finished=${finishedItemCount} blocked=${blockedItemCount} tokensUsed=${totalTokensUsed} maxTotalTokens=${tokenBudget?.maxTotalTokens ?? "unbounded"}`
|
|
425
|
-
: failedChildren.length > 0
|
|
426
|
-
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
427
|
-
: "",
|
|
428
|
-
failureCategory: tokenBudgetFailure
|
|
429
|
-
? "dynamic-expansion-token-budget-exhausted"
|
|
430
413
|
: failedChildren.length > 0
|
|
431
|
-
?
|
|
432
|
-
: "
|
|
414
|
+
? `dynamic map children failed: ${failedChildren.join(", ")}`
|
|
415
|
+
: "",
|
|
416
|
+
failureCategory: failedChildren.length > 0 ? "dynamic-expansion-child-failed" : "success",
|
|
433
417
|
durationMs: Date.now() - started,
|
|
434
418
|
};
|
|
435
419
|
}
|
|
@@ -19,6 +19,9 @@ const RAW_TO_NORMALIZED = {
|
|
|
19
19
|
success: "success",
|
|
20
20
|
"write-guard": "write-guard",
|
|
21
21
|
timeout: "timeout",
|
|
22
|
+
// Clean implementer stall with zero writes — still a timeout class for report,
|
|
23
|
+
// but retryable under WRITER_TRANSPORT_RETRY_POLICY.
|
|
24
|
+
"writer-clean-timeout": "timeout",
|
|
22
25
|
"missing-api-key": "auth",
|
|
23
26
|
auth: "auth",
|
|
24
27
|
"human-rejected": "human-rejected",
|