@tea-agent/loop-agent 0.26.0 → 0.26.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 +1056 -1020
- package/README.md +8 -3
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/generate-task-dag.js +33 -0
- package/dist/cli/command-definitions.js +25 -10
- package/dist/cli/help.js +4 -3
- package/dist/cli/program.js +43 -17
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/import-prd.js +7 -2
- package/dist/commands/init.js +7 -5
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/commands/task-source-prepare.js +474 -0
- package/dist/executors/dag-pi-executor.js +40 -5
- package/dist/executors/shell-executor.js +111 -0
- package/dist/executors/shell-presets.js +12 -4
- package/dist/executors/shell-write-guard.js +161 -25
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/config-types.js +6 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/project.js +8 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/frontend-preflight.js +131 -0
- package/dist/task/runtime.js +2 -4
- package/dist/task/source-prepare/build-draft.js +224 -0
- package/dist/task/source-prepare/completeness.js +195 -0
- package/dist/task/source-prepare/index.js +7 -0
- package/dist/task/source-prepare/parse-intent.js +373 -0
- package/dist/task/source-prepare/path-policy.js +197 -0
- package/dist/task/source-prepare/prepare.js +506 -0
- package/dist/task/source-prepare/reference-integrity.js +274 -0
- package/dist/task/source-prepare/types.js +7 -0
- package/dist/worker/observability/read-model.js +134 -0
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-layout.d.ts +31 -31
- package/dist/worker/observe/static/dag-layout.js +83 -83
- package/dist/worker/observe/static/dom.js +220 -220
- package/dist/worker/observe/static/relations.js +133 -133
- package/dist/worker/observe/static/router.js +93 -93
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/state.js +61 -0
- package/dist/worker/observe/static/styles.css +8 -0
- package/dist/worker/observe/static/views/batch.js +227 -227
- package/dist/worker/observe/static/views/dag-graph.js +248 -172
- package/dist/worker/observe/static/views/dag-inspector.js +374 -157
- package/dist/worker/observe/static/views/dag.js +4 -11
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -492
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/convergence/controller.js +110 -21
- package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
- package/dist/workflows/dag/frontend-review-context.js +7 -1
- package/dist/workflows/dag/frontend-verification-trace.js +14 -3
- package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
- package/dist/workflows/dag/init-hybrid.js +96 -34
- package/dist/workflows/dag/output-protocol.js +180 -7
- package/dist/workflows/dag/runner.js +141 -52
- package/dist/workflows/dag/types.js +4 -0
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/skills/README.md +7 -7
- package/docs/templates/adr.md +60 -60
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -94
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -213
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -246
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -98
- package/docs/templates/agent-dag-report.schema.json +473 -473
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -68
- package/docs/templates/backend-test-dag.json +100 -8
- package/docs/templates/backend-test-result.schema.json +99 -99
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -42
- package/docs/templates/frontend-eval/fixtures/failures/01-type-build-error.md +17 -17
- package/docs/templates/frontend-eval/fixtures/failures/02-unit-component-test-fail.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/03-fixture-schema-drift.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/04-missing-loading-empty-error-state.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/05-forbidden-write-writeset-expansion.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/06-unapproved-dependency-add.md +16 -16
- package/docs/templates/frontend-eval/fixtures/failures/07-mock-production-on.md +21 -21
- package/docs/templates/frontend-eval/fixtures/functional/01-simple-component-style.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/02-form-validation.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/03-list-detail-page.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/04-api-mock.md +29 -29
- package/docs/templates/frontend-eval/fixtures/functional/05-permission-auth-gated-ui.md +27 -27
- package/docs/templates/frontend-eval/fixtures/functional/06-ssr-server-client-boundary.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/07-shared-public-component-api.md +28 -28
- package/docs/templates/frontend-eval/fixtures/functional/08-pure-local-no-remote.md +27 -27
- package/docs/templates/frontend-eval/metrics.md +138 -138
- package/docs/templates/frontend-eval/smoke-targets.md +53 -53
- package/docs/templates/frontend-task-constraints.md +35 -35
- package/docs/templates/frontend-task-requirement.md +70 -70
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/init-managed-agents.md +10 -5
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -118
- package/docs/templates/knowledge-sync-dag.json +178 -178
- package/docs/templates/knowledge-sync-draft.schema.json +71 -71
- package/docs/templates/product-line/closeout.yaml +9 -9
- package/docs/templates/product-line/design.md +13 -13
- package/docs/templates/product-line/links.md +10 -10
- package/docs/templates/product-line/requirement.md +17 -17
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/project-start-checklist.md +9 -9
- package/docs/templates/qa-report.md +48 -48
- package/docs/templates/sprint-contract.md +29 -29
- package/docs/templates/worker-dogfood-evidence.md +80 -80
- package/docs/templates/worker-dogfood-setup.md +68 -68
- package/package.json +1 -1
- package/scripts/kb-bootstrap-init-skeleton.sh +0 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -386
- package/scripts/kb-graph-materialize.mjs +105 -105
- package/scripts/kb-graph-promote.mjs +164 -164
- package/scripts/kb-query.mjs +554 -554
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -67
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -4
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -30
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -28
- package/skills/analyze-product-dependencies/references/example.md +76 -76
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -35
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -11
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -61
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -267
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -101
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -142
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -76
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -146
- package/skills/analyze-product-requirements/SKILL.md +90 -90
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -4
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -91
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -56
- package/skills/analyze-product-requirements/references/example.md +86 -86
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -66
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -32
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -33
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -35
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -193
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -69
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -97
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -98
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -156
- package/skills/browser-tools/browser-content.js +103 -103
- package/skills/browser-tools/browser-cookies.js +35 -35
- package/skills/browser-tools/browser-eval.js +53 -53
- package/skills/browser-tools/browser-hn-scraper.js +108 -108
- package/skills/browser-tools/browser-nav.js +44 -44
- package/skills/browser-tools/browser-pick.js +162 -162
- package/skills/browser-tools/browser-screenshot.js +34 -34
- package/skills/browser-tools/browser-start.js +86 -86
- package/skills/browser-tools/package-lock.json +2556 -2556
- package/skills/browser-tools/package.json +19 -19
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/loop-agent/SKILL.md +5 -2
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +17 -15
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +3 -4
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/skills/loop-agent/references/learned/README.md +21 -21
- package/skills/loop-agent/references/long-running-loop.md +57 -57
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/pi-prompt.md +23 -23
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +84 -84
- package/skills/loop-agent/references/post-implementation-and-patterns.md +1 -1
- package/skills/loop-agent/references/source-and-plan-practice.md +3 -2
- package/skills/loop-agent/references/task-workflow.md +7 -5
- package/skills/playwright-cli/SKILL.md +420 -420
- package/skills/playwright-cli/references/element-attributes.md +23 -23
- package/skills/playwright-cli/references/playwright-tests.md +39 -39
- package/skills/playwright-cli/references/request-mocking.md +87 -87
- package/skills/playwright-cli/references/running-code.md +241 -241
- package/skills/playwright-cli/references/session-management.md +225 -225
- package/skills/playwright-cli/references/storage-state.md +275 -275
- package/skills/playwright-cli/references/test-generation.md +433 -433
- package/skills/playwright-cli/references/tracing.md +139 -139
- package/skills/playwright-cli/references/video-recording.md +143 -143
- package/skills/requesting-code-review/SKILL.md +101 -101
- package/skills/requesting-code-review/code-reviewer.md +168 -168
- package/skills/systematic-debugging/CREATION-LOG.md +119 -119
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -158
- package/skills/systematic-debugging/condition-based-waiting.md +115 -115
- package/skills/systematic-debugging/defense-in-depth.md +122 -122
- package/skills/systematic-debugging/find-polluter.sh +63 -63
- package/skills/systematic-debugging/root-cause-tracing.md +169 -169
- package/skills/systematic-debugging/test-academic.md +14 -14
- package/skills/systematic-debugging/test-pressure-1.md +58 -58
- package/skills/systematic-debugging/test-pressure-2.md +68 -68
- package/skills/systematic-debugging/test-pressure-3.md +69 -69
- package/skills/using-git-worktrees/SKILL.md +215 -215
- package/skills/verification-before-completion/SKILL.md +154 -154
- package/skills/webapp-testing/SKILL.md +19 -19
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell service locator removed in R5 boundary final (BND-02).
|
|
3
|
-
* Views import focused modules directly (shell-chrome, kpi, dag-helpers, run-processing).
|
|
4
|
-
* This file is retained only so historical bundle paths do not 404; do not reintroduce getShell.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export {};
|
|
1
|
+
/**
|
|
2
|
+
* Shell service locator removed in R5 boundary final (BND-02).
|
|
3
|
+
* Views import focused modules directly (shell-chrome, kpi, dag-helpers, run-processing).
|
|
4
|
+
* This file is retained only so historical bundle paths do not 404; do not reintroduce getShell.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {};
|
|
@@ -1,163 +1,163 @@
|
|
|
1
|
-
/** Observe UI view module (R5). */
|
|
2
|
-
import {
|
|
3
|
-
UI_TEXT,
|
|
4
|
-
LIVENESS_LABELS,
|
|
5
|
-
ARTIFACT_LABELS,
|
|
6
|
-
ARTIFACT_KEYS,
|
|
7
|
-
STEP_LABELS,
|
|
8
|
-
STEP_ORDER,
|
|
9
|
-
DAG_INSPECTOR_DEFAULT_WIDTH,
|
|
10
|
-
DAG_INSPECTOR_MIN_WIDTH,
|
|
11
|
-
DAG_INSPECTOR_MAX_WIDTH,
|
|
12
|
-
DAG_INSPECTOR_RIGHT_OFFSET,
|
|
13
|
-
DAG_INSPECTOR_VIEWPORT_MARGIN,
|
|
14
|
-
DAG_INSPECTOR_KEYBOARD_STEP,
|
|
15
|
-
TERMINAL_RUN_STATUSES,
|
|
16
|
-
DAG_EFFECTIVE_STATUS_LABELS,
|
|
17
|
-
STATUS_LABELS,
|
|
18
|
-
} from "../constants.js";
|
|
19
|
-
import { clearNode, el, metaGrid, buildTable } from "../dom.js";
|
|
20
|
-
import {
|
|
21
|
-
badge,
|
|
22
|
-
badgeClass,
|
|
23
|
-
statusLabel,
|
|
24
|
-
formatTs,
|
|
25
|
-
formatMs,
|
|
26
|
-
formatDuration,
|
|
27
|
-
formatAgo,
|
|
28
|
-
dagStatusBadge,
|
|
29
|
-
livenessBadge,
|
|
30
|
-
displayDurationMs,
|
|
31
|
-
derivedDurationMs,
|
|
32
|
-
summaryText,
|
|
33
|
-
truncateText,
|
|
34
|
-
repoBaseName,
|
|
35
|
-
parseMarkdownBlocks,
|
|
36
|
-
markdownLinkHref,
|
|
37
|
-
} from "../format.js";
|
|
38
|
-
import { parseRoute, navigate, buildHashPath } from "../router.js";
|
|
39
|
-
import {
|
|
40
|
-
fetchJson,
|
|
41
|
-
artifactUrl,
|
|
42
|
-
parseArtifactPreviewResponse,
|
|
43
|
-
} from "../api.js";
|
|
44
|
-
import {
|
|
45
|
-
objectRouteLink,
|
|
46
|
-
findFeatureInSnapshot,
|
|
47
|
-
featureLookupState,
|
|
48
|
-
buildObjectRelations,
|
|
49
|
-
renderObjectRelationBar,
|
|
50
|
-
} from "../relations.js";
|
|
51
|
-
import { copyText, renderRecommendedCommand } from "../copy.js";
|
|
52
|
-
import {
|
|
53
|
-
isPageVisible,
|
|
54
|
-
detailPollDelay,
|
|
55
|
-
dashboardPollDelay,
|
|
56
|
-
captureDagGraphViewportState,
|
|
57
|
-
updateDagGraphViewportState,
|
|
58
|
-
updateDagTimelineViewportState,
|
|
59
|
-
renderViewState,
|
|
60
|
-
mountViewState,
|
|
61
|
-
} from "../state.js";
|
|
62
|
-
import { uiState } from "../state.js";
|
|
63
|
-
import { sortPoolTasks } from "../format-pool.js";
|
|
64
|
-
import {
|
|
65
|
-
dagMetaVisibility,
|
|
66
|
-
isDagRunActive,
|
|
67
|
-
dashboardVisibleFeatures,
|
|
68
|
-
findDagForRun,
|
|
69
|
-
} from "../dag-model.js";
|
|
70
|
-
import { layoutDag } from "../dag-layout.js";
|
|
71
|
-
|
|
72
|
-
export async function mountBoundedEventTimeline(container, apiBase, title) {
|
|
73
|
-
clearNode(container);
|
|
74
|
-
container.appendChild(el("h3", "section-heading", title));
|
|
75
|
-
const note = el(
|
|
76
|
-
"p",
|
|
77
|
-
"process-timeline-note",
|
|
78
|
-
"有界历史(默认 50 / 最大 200);分页使用 cursor,与 SSE 行号 offset 分离。",
|
|
79
|
-
);
|
|
80
|
-
container.appendChild(note);
|
|
81
|
-
const scroll = el("div", "process-timeline-scroll event-history-scroll");
|
|
82
|
-
const listHost = el("div", "event-history-list");
|
|
83
|
-
scroll.appendChild(listHost);
|
|
84
|
-
container.appendChild(scroll);
|
|
85
|
-
const footer = el("div", "event-history-footer");
|
|
86
|
-
container.appendChild(footer);
|
|
87
|
-
|
|
88
|
-
let nextCursor = null;
|
|
89
|
-
let loading = false;
|
|
90
|
-
const accumulated = [];
|
|
91
|
-
|
|
92
|
-
const renderList = () => {
|
|
93
|
-
clearNode(listHost);
|
|
94
|
-
if (accumulated.length === 0) {
|
|
95
|
-
mountViewState(listHost, "empty", "暂无事件");
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const ul = el("ul", "process-timeline");
|
|
99
|
-
for (let index = 0; index < accumulated.length; index += 1) {
|
|
100
|
-
const event = accumulated[index];
|
|
101
|
-
const li = el(
|
|
102
|
-
"li",
|
|
103
|
-
`process-timeline-item process-timeline-event has-time${index === 0 ? " is-first" : ""}${index === accumulated.length - 1 ? " is-last" : ""}`,
|
|
104
|
-
);
|
|
105
|
-
const rail = el("span", "process-timeline-rail");
|
|
106
|
-
rail.appendChild(el("span", "process-timeline-dot"));
|
|
107
|
-
li.appendChild(rail);
|
|
108
|
-
li.appendChild(
|
|
109
|
-
el("span", "process-timeline-time", formatTs(event.at) || "—"),
|
|
110
|
-
);
|
|
111
|
-
const label = `${event.type ?? "event"}${event.taskId ? ` · ${event.taskId}` : ""}${event.workerRunId ? ` · ${event.workerRunId}` : ""}`;
|
|
112
|
-
li.appendChild(el("span", "process-timeline-label", label));
|
|
113
|
-
ul.appendChild(li);
|
|
114
|
-
}
|
|
115
|
-
listHost.appendChild(ul);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const loadPage = async (cursor) => {
|
|
119
|
-
if (loading) return;
|
|
120
|
-
loading = true;
|
|
121
|
-
if (accumulated.length === 0) {
|
|
122
|
-
mountViewState(listHost, "loading", "加载事件…");
|
|
123
|
-
}
|
|
124
|
-
const qs = new URLSearchParams();
|
|
125
|
-
qs.set("limit", "50");
|
|
126
|
-
if (cursor) qs.set("cursor", cursor);
|
|
127
|
-
const page = await fetchJson(`${apiBase}?${qs.toString()}`);
|
|
128
|
-
loading = false;
|
|
129
|
-
if (!page) {
|
|
130
|
-
mountViewState(listHost, "error", "无法加载事件历史。");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const events = page.events ?? [];
|
|
134
|
-
for (const event of events) accumulated.push(event);
|
|
135
|
-
nextCursor = page.nextCursor ?? null;
|
|
136
|
-
renderList();
|
|
137
|
-
clearNode(footer);
|
|
138
|
-
if (page.corruptLineCount > 0) {
|
|
139
|
-
footer.appendChild(
|
|
140
|
-
renderViewState(
|
|
141
|
-
"degraded",
|
|
142
|
-
`跳过损坏行 ${page.corruptLineCount} 条(failure-safe)。`,
|
|
143
|
-
),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
if (nextCursor) {
|
|
147
|
-
const btn = el(
|
|
148
|
-
"button",
|
|
149
|
-
"btn-secondary event-history-more",
|
|
150
|
-
"加载更早事件",
|
|
151
|
-
);
|
|
152
|
-
btn.type = "button";
|
|
153
|
-
btn.addEventListener("click", () => {
|
|
154
|
-
void loadPage(nextCursor);
|
|
155
|
-
});
|
|
156
|
-
footer.appendChild(btn);
|
|
157
|
-
} else if (accumulated.length > 0) {
|
|
158
|
-
footer.appendChild(el("p", "muted", "已到历史边界"));
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
await loadPage(null);
|
|
163
|
-
}
|
|
1
|
+
/** Observe UI view module (R5). */
|
|
2
|
+
import {
|
|
3
|
+
UI_TEXT,
|
|
4
|
+
LIVENESS_LABELS,
|
|
5
|
+
ARTIFACT_LABELS,
|
|
6
|
+
ARTIFACT_KEYS,
|
|
7
|
+
STEP_LABELS,
|
|
8
|
+
STEP_ORDER,
|
|
9
|
+
DAG_INSPECTOR_DEFAULT_WIDTH,
|
|
10
|
+
DAG_INSPECTOR_MIN_WIDTH,
|
|
11
|
+
DAG_INSPECTOR_MAX_WIDTH,
|
|
12
|
+
DAG_INSPECTOR_RIGHT_OFFSET,
|
|
13
|
+
DAG_INSPECTOR_VIEWPORT_MARGIN,
|
|
14
|
+
DAG_INSPECTOR_KEYBOARD_STEP,
|
|
15
|
+
TERMINAL_RUN_STATUSES,
|
|
16
|
+
DAG_EFFECTIVE_STATUS_LABELS,
|
|
17
|
+
STATUS_LABELS,
|
|
18
|
+
} from "../constants.js";
|
|
19
|
+
import { clearNode, el, metaGrid, buildTable } from "../dom.js";
|
|
20
|
+
import {
|
|
21
|
+
badge,
|
|
22
|
+
badgeClass,
|
|
23
|
+
statusLabel,
|
|
24
|
+
formatTs,
|
|
25
|
+
formatMs,
|
|
26
|
+
formatDuration,
|
|
27
|
+
formatAgo,
|
|
28
|
+
dagStatusBadge,
|
|
29
|
+
livenessBadge,
|
|
30
|
+
displayDurationMs,
|
|
31
|
+
derivedDurationMs,
|
|
32
|
+
summaryText,
|
|
33
|
+
truncateText,
|
|
34
|
+
repoBaseName,
|
|
35
|
+
parseMarkdownBlocks,
|
|
36
|
+
markdownLinkHref,
|
|
37
|
+
} from "../format.js";
|
|
38
|
+
import { parseRoute, navigate, buildHashPath } from "../router.js";
|
|
39
|
+
import {
|
|
40
|
+
fetchJson,
|
|
41
|
+
artifactUrl,
|
|
42
|
+
parseArtifactPreviewResponse,
|
|
43
|
+
} from "../api.js";
|
|
44
|
+
import {
|
|
45
|
+
objectRouteLink,
|
|
46
|
+
findFeatureInSnapshot,
|
|
47
|
+
featureLookupState,
|
|
48
|
+
buildObjectRelations,
|
|
49
|
+
renderObjectRelationBar,
|
|
50
|
+
} from "../relations.js";
|
|
51
|
+
import { copyText, renderRecommendedCommand } from "../copy.js";
|
|
52
|
+
import {
|
|
53
|
+
isPageVisible,
|
|
54
|
+
detailPollDelay,
|
|
55
|
+
dashboardPollDelay,
|
|
56
|
+
captureDagGraphViewportState,
|
|
57
|
+
updateDagGraphViewportState,
|
|
58
|
+
updateDagTimelineViewportState,
|
|
59
|
+
renderViewState,
|
|
60
|
+
mountViewState,
|
|
61
|
+
} from "../state.js";
|
|
62
|
+
import { uiState } from "../state.js";
|
|
63
|
+
import { sortPoolTasks } from "../format-pool.js";
|
|
64
|
+
import {
|
|
65
|
+
dagMetaVisibility,
|
|
66
|
+
isDagRunActive,
|
|
67
|
+
dashboardVisibleFeatures,
|
|
68
|
+
findDagForRun,
|
|
69
|
+
} from "../dag-model.js";
|
|
70
|
+
import { layoutDag } from "../dag-layout.js";
|
|
71
|
+
|
|
72
|
+
export async function mountBoundedEventTimeline(container, apiBase, title) {
|
|
73
|
+
clearNode(container);
|
|
74
|
+
container.appendChild(el("h3", "section-heading", title));
|
|
75
|
+
const note = el(
|
|
76
|
+
"p",
|
|
77
|
+
"process-timeline-note",
|
|
78
|
+
"有界历史(默认 50 / 最大 200);分页使用 cursor,与 SSE 行号 offset 分离。",
|
|
79
|
+
);
|
|
80
|
+
container.appendChild(note);
|
|
81
|
+
const scroll = el("div", "process-timeline-scroll event-history-scroll");
|
|
82
|
+
const listHost = el("div", "event-history-list");
|
|
83
|
+
scroll.appendChild(listHost);
|
|
84
|
+
container.appendChild(scroll);
|
|
85
|
+
const footer = el("div", "event-history-footer");
|
|
86
|
+
container.appendChild(footer);
|
|
87
|
+
|
|
88
|
+
let nextCursor = null;
|
|
89
|
+
let loading = false;
|
|
90
|
+
const accumulated = [];
|
|
91
|
+
|
|
92
|
+
const renderList = () => {
|
|
93
|
+
clearNode(listHost);
|
|
94
|
+
if (accumulated.length === 0) {
|
|
95
|
+
mountViewState(listHost, "empty", "暂无事件");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const ul = el("ul", "process-timeline");
|
|
99
|
+
for (let index = 0; index < accumulated.length; index += 1) {
|
|
100
|
+
const event = accumulated[index];
|
|
101
|
+
const li = el(
|
|
102
|
+
"li",
|
|
103
|
+
`process-timeline-item process-timeline-event has-time${index === 0 ? " is-first" : ""}${index === accumulated.length - 1 ? " is-last" : ""}`,
|
|
104
|
+
);
|
|
105
|
+
const rail = el("span", "process-timeline-rail");
|
|
106
|
+
rail.appendChild(el("span", "process-timeline-dot"));
|
|
107
|
+
li.appendChild(rail);
|
|
108
|
+
li.appendChild(
|
|
109
|
+
el("span", "process-timeline-time", formatTs(event.at) || "—"),
|
|
110
|
+
);
|
|
111
|
+
const label = `${event.type ?? "event"}${event.taskId ? ` · ${event.taskId}` : ""}${event.workerRunId ? ` · ${event.workerRunId}` : ""}`;
|
|
112
|
+
li.appendChild(el("span", "process-timeline-label", label));
|
|
113
|
+
ul.appendChild(li);
|
|
114
|
+
}
|
|
115
|
+
listHost.appendChild(ul);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const loadPage = async (cursor) => {
|
|
119
|
+
if (loading) return;
|
|
120
|
+
loading = true;
|
|
121
|
+
if (accumulated.length === 0) {
|
|
122
|
+
mountViewState(listHost, "loading", "加载事件…");
|
|
123
|
+
}
|
|
124
|
+
const qs = new URLSearchParams();
|
|
125
|
+
qs.set("limit", "50");
|
|
126
|
+
if (cursor) qs.set("cursor", cursor);
|
|
127
|
+
const page = await fetchJson(`${apiBase}?${qs.toString()}`);
|
|
128
|
+
loading = false;
|
|
129
|
+
if (!page) {
|
|
130
|
+
mountViewState(listHost, "error", "无法加载事件历史。");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const events = page.events ?? [];
|
|
134
|
+
for (const event of events) accumulated.push(event);
|
|
135
|
+
nextCursor = page.nextCursor ?? null;
|
|
136
|
+
renderList();
|
|
137
|
+
clearNode(footer);
|
|
138
|
+
if (page.corruptLineCount > 0) {
|
|
139
|
+
footer.appendChild(
|
|
140
|
+
renderViewState(
|
|
141
|
+
"degraded",
|
|
142
|
+
`跳过损坏行 ${page.corruptLineCount} 条(failure-safe)。`,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (nextCursor) {
|
|
147
|
+
const btn = el(
|
|
148
|
+
"button",
|
|
149
|
+
"btn-secondary event-history-more",
|
|
150
|
+
"加载更早事件",
|
|
151
|
+
);
|
|
152
|
+
btn.type = "button";
|
|
153
|
+
btn.addEventListener("click", () => {
|
|
154
|
+
void loadPage(nextCursor);
|
|
155
|
+
});
|
|
156
|
+
footer.appendChild(btn);
|
|
157
|
+
} else if (accumulated.length > 0) {
|
|
158
|
+
footer.appendChild(el("p", "muted", "已到历史边界"));
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
await loadPage(null);
|
|
163
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
6
|
+
const MAX_DIAGNOSTIC_CHARS = 12_000;
|
|
7
|
+
export const backendPytestCollectionFindingSchema = z.object({
|
|
8
|
+
kind: z.string().min(1),
|
|
9
|
+
classification: z.literal("test-asset-defect"),
|
|
10
|
+
repairability: z.enum(["repairable", "blocked"]),
|
|
11
|
+
detail: z.string().min(1),
|
|
12
|
+
}).strict();
|
|
13
|
+
export const backendPytestCollectionFactsSchema = z.object({
|
|
14
|
+
schemaId: z.literal("backend-test-pytest-collection-v1"),
|
|
15
|
+
phase: z.enum(["initial", "final", "effective"]),
|
|
16
|
+
status: z.enum(["PASS", "REPAIRABLE", "BLOCKED"]),
|
|
17
|
+
repairEligible: z.boolean(),
|
|
18
|
+
repairAttempt: z.number().int().min(0).max(1),
|
|
19
|
+
mappedScripts: z.array(z.string()).min(1),
|
|
20
|
+
assetFiles: z.array(z.string()).min(1),
|
|
21
|
+
inputHashes: z.record(z.string(), z.string().regex(SHA256)),
|
|
22
|
+
pytestExitCode: z.number().int(),
|
|
23
|
+
collectedItemCount: z.number().int().min(0),
|
|
24
|
+
collectedItemIds: z.array(z.string()),
|
|
25
|
+
findings: z.array(backendPytestCollectionFindingSchema),
|
|
26
|
+
stdoutExcerpt: z.string(),
|
|
27
|
+
stderrExcerpt: z.string(),
|
|
28
|
+
collectionSource: z.enum(["initial", "final"]).optional(),
|
|
29
|
+
}).strict();
|
|
30
|
+
function repoRef(workspaceRoot, absolutePath) {
|
|
31
|
+
return path.relative(workspaceRoot, absolutePath).replaceAll(path.sep, "/");
|
|
32
|
+
}
|
|
33
|
+
function sha256(bytes) {
|
|
34
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
35
|
+
}
|
|
36
|
+
async function walkPythonFiles(root) {
|
|
37
|
+
const results = [];
|
|
38
|
+
const visit = async (current) => {
|
|
39
|
+
let entries;
|
|
40
|
+
try {
|
|
41
|
+
entries = await readdir(current, { withFileTypes: true });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.name === "__pycache__" || entry.name === ".pytest_cache")
|
|
48
|
+
continue;
|
|
49
|
+
const absolute = path.join(current, entry.name);
|
|
50
|
+
if (entry.isDirectory())
|
|
51
|
+
await visit(absolute);
|
|
52
|
+
else if (entry.isFile() && entry.name.endsWith(".py"))
|
|
53
|
+
results.push(absolute);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
await visit(root);
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
export async function buildBackendPytestAssetInventory(workspaceRoot, mappedScripts) {
|
|
60
|
+
const normalizedScripts = [...new Set(mappedScripts.map((value) => value.replaceAll("\\", "/")))].sort();
|
|
61
|
+
if (normalizedScripts.length === 0)
|
|
62
|
+
throw new Error("backend pytest collection requires mapped scripts");
|
|
63
|
+
for (const script of normalizedScripts) {
|
|
64
|
+
if (!/^testcase(?:\/[A-Za-z0-9_.-]+)*\/test_[A-Za-z0-9_.-]+\.py$/.test(script) && !/^testcase\/test_[A-Za-z0-9_.-]+\.py$/.test(script)) {
|
|
65
|
+
throw new Error(`unsafe backend pytest mapped script: ${script}`);
|
|
66
|
+
}
|
|
67
|
+
const absolute = path.resolve(workspaceRoot, script);
|
|
68
|
+
const testcaseRoot = path.resolve(workspaceRoot, "testcase");
|
|
69
|
+
if (absolute !== testcaseRoot && !absolute.startsWith(`${testcaseRoot}${path.sep}`)) {
|
|
70
|
+
throw new Error(`backend pytest mapped script escapes testcase: ${script}`);
|
|
71
|
+
}
|
|
72
|
+
await readFile(absolute);
|
|
73
|
+
}
|
|
74
|
+
const pythonFiles = await walkPythonFiles(path.resolve(workspaceRoot, "testcase"));
|
|
75
|
+
const assetFiles = [...new Set(pythonFiles.map((file) => repoRef(workspaceRoot, file)))].sort();
|
|
76
|
+
const inputHashes = {};
|
|
77
|
+
for (const file of assetFiles)
|
|
78
|
+
inputHashes[file] = sha256(await readFile(path.resolve(workspaceRoot, file)));
|
|
79
|
+
return { mappedScripts: normalizedScripts, assetFiles, inputHashes };
|
|
80
|
+
}
|
|
81
|
+
function bounded(value) {
|
|
82
|
+
const normalized = value.replaceAll(/(Bearer|Basic)\s+[A-Za-z0-9._~+\/-]+/gi, "$1 [REDACTED]");
|
|
83
|
+
return normalized.length <= MAX_DIAGNOSTIC_CHARS
|
|
84
|
+
? normalized
|
|
85
|
+
: `${normalized.slice(0, MAX_DIAGNOSTIC_CHARS)}\n...[truncated]`;
|
|
86
|
+
}
|
|
87
|
+
function collectionItems(stdout) {
|
|
88
|
+
return [...new Set(stdout.split(/\r?\n/).map((line) => line.trim()).filter((line) => {
|
|
89
|
+
if (!line.includes("::"))
|
|
90
|
+
return false;
|
|
91
|
+
if (/^(ERROR|FAILED|E\s+)/.test(line))
|
|
92
|
+
return false;
|
|
93
|
+
return /^testcase\//.test(line.replaceAll("\\", "/"));
|
|
94
|
+
}).map((line) => line.replaceAll("\\", "/")))];
|
|
95
|
+
}
|
|
96
|
+
function classifyCollectionFailure(output) {
|
|
97
|
+
const normalized = output.replaceAll("\\", "/");
|
|
98
|
+
const blockedPatterns = [
|
|
99
|
+
[/No module named ['\"](?!testcase(?:\.|['\"]))/i, "missing-third-party-module"],
|
|
100
|
+
[/pytest(?:_|-)plugin|pluggy|unknown hook|unrecognized arguments/i, "pytest-plugin-or-config"],
|
|
101
|
+
[/(?:apps|src|server|api)\/[A-Za-z0-9_./-]+\.py/i, "production-module-import"],
|
|
102
|
+
[/permission denied|access is denied|timed? out|not recognized as an internal|no such file or directory.*python/i, "environment-or-spawn"],
|
|
103
|
+
];
|
|
104
|
+
for (const [pattern, kind] of blockedPatterns) {
|
|
105
|
+
if (pattern.test(normalized))
|
|
106
|
+
return { status: "BLOCKED", kind, detail: "collection failure is outside generated testcase asset repair scope" };
|
|
107
|
+
}
|
|
108
|
+
if (/ImportError:\s*cannot import name/i.test(normalized) && /(?:from ['\"]testcase\.|\/testcase\/)/i.test(normalized)) {
|
|
109
|
+
return { status: "REPAIRABLE", kind: "missing-local-import-symbol", detail: "generated testcase asset imports a missing local symbol" };
|
|
110
|
+
}
|
|
111
|
+
if (/ModuleNotFoundError:\s*No module named ['\"]testcase(?:\.|['\"])/i.test(normalized)) {
|
|
112
|
+
return { status: "REPAIRABLE", kind: "missing-local-testcase-module", detail: "generated testcase asset imports a missing local testcase module" };
|
|
113
|
+
}
|
|
114
|
+
if (/partially initialized module ['\"]testcase\.|circular import/i.test(normalized)) {
|
|
115
|
+
return { status: "REPAIRABLE", kind: "local-circular-import", detail: "generated testcase assets contain a local circular import" };
|
|
116
|
+
}
|
|
117
|
+
if (/SyntaxError|IndentationError|TabError/i.test(normalized) && /testcase\/[A-Za-z0-9_./-]+\.py/i.test(normalized)) {
|
|
118
|
+
return { status: "REPAIRABLE", kind: "generated-python-syntax", detail: "generated testcase Python source is not syntactically collectable" };
|
|
119
|
+
}
|
|
120
|
+
return { status: "BLOCKED", kind: "unclassified-collection-error", detail: "collection failure cannot be safely attributed to generated testcase assets" };
|
|
121
|
+
}
|
|
122
|
+
export function assessBackendPytestCollection(input) {
|
|
123
|
+
const items = collectionItems(input.stdout);
|
|
124
|
+
if (input.exitCode === 0) {
|
|
125
|
+
return backendPytestCollectionFactsSchema.parse({
|
|
126
|
+
schemaId: "backend-test-pytest-collection-v1",
|
|
127
|
+
phase: input.phase,
|
|
128
|
+
status: "PASS",
|
|
129
|
+
repairEligible: false,
|
|
130
|
+
repairAttempt: input.phase === "final" ? 1 : 0,
|
|
131
|
+
mappedScripts: input.inventory.mappedScripts,
|
|
132
|
+
assetFiles: input.inventory.assetFiles,
|
|
133
|
+
inputHashes: input.inventory.inputHashes,
|
|
134
|
+
pytestExitCode: input.exitCode,
|
|
135
|
+
collectedItemCount: items.length,
|
|
136
|
+
collectedItemIds: items,
|
|
137
|
+
findings: [],
|
|
138
|
+
stdoutExcerpt: bounded(input.stdout),
|
|
139
|
+
stderrExcerpt: bounded(input.stderr),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const classification = classifyCollectionFailure(`${input.stdout}\n${input.stderr}`);
|
|
143
|
+
return backendPytestCollectionFactsSchema.parse({
|
|
144
|
+
schemaId: "backend-test-pytest-collection-v1",
|
|
145
|
+
phase: input.phase,
|
|
146
|
+
status: classification.status,
|
|
147
|
+
repairEligible: input.phase === "initial" && classification.status === "REPAIRABLE",
|
|
148
|
+
repairAttempt: input.phase === "final" ? 1 : 0,
|
|
149
|
+
mappedScripts: input.inventory.mappedScripts,
|
|
150
|
+
assetFiles: input.inventory.assetFiles,
|
|
151
|
+
inputHashes: input.inventory.inputHashes,
|
|
152
|
+
pytestExitCode: input.exitCode,
|
|
153
|
+
collectedItemCount: items.length,
|
|
154
|
+
collectedItemIds: items,
|
|
155
|
+
findings: [{
|
|
156
|
+
kind: classification.kind,
|
|
157
|
+
classification: "test-asset-defect",
|
|
158
|
+
repairability: classification.status === "REPAIRABLE" ? "repairable" : "blocked",
|
|
159
|
+
detail: classification.detail,
|
|
160
|
+
}],
|
|
161
|
+
stdoutExcerpt: bounded(input.stdout),
|
|
162
|
+
stderrExcerpt: bounded(input.stderr),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
export function renderBackendPytestCollectionReport(facts) {
|
|
166
|
+
return [
|
|
167
|
+
`# Backend pytest Collection ${facts.phase}`,
|
|
168
|
+
"",
|
|
169
|
+
"## Status",
|
|
170
|
+
"",
|
|
171
|
+
facts.status,
|
|
172
|
+
"",
|
|
173
|
+
`- Repair eligible: ${facts.repairEligible}`,
|
|
174
|
+
`- Repair attempt: ${facts.repairAttempt}`,
|
|
175
|
+
`- Pytest exit code: ${facts.pytestExitCode}`,
|
|
176
|
+
`- Mapped scripts: ${facts.mappedScripts.length}`,
|
|
177
|
+
`- Bound Python assets: ${facts.assetFiles.length}`,
|
|
178
|
+
`- Collected items: ${facts.collectedItemCount}`,
|
|
179
|
+
"",
|
|
180
|
+
"## Findings",
|
|
181
|
+
"",
|
|
182
|
+
...(facts.findings.length ? facts.findings.map((finding) => `- ${finding.kind}: ${finding.detail}`) : ["- None"]),
|
|
183
|
+
"",
|
|
184
|
+
"## Collected Items",
|
|
185
|
+
"",
|
|
186
|
+
...(facts.collectedItemIds.length ? facts.collectedItemIds.map((item) => `- ${item}`) : ["- None"]),
|
|
187
|
+
"",
|
|
188
|
+
"## Bounded stdout",
|
|
189
|
+
"",
|
|
190
|
+
"```text",
|
|
191
|
+
facts.stdoutExcerpt,
|
|
192
|
+
"```",
|
|
193
|
+
"",
|
|
194
|
+
"## Bounded stderr",
|
|
195
|
+
"",
|
|
196
|
+
"```text",
|
|
197
|
+
facts.stderrExcerpt,
|
|
198
|
+
"```",
|
|
199
|
+
"",
|
|
200
|
+
].join("\n");
|
|
201
|
+
}
|
|
202
|
+
export async function writeBackendPytestCollectionArtifacts(input) {
|
|
203
|
+
const contractsDir = path.join(input.runDir, "contracts");
|
|
204
|
+
const reportsDir = path.join(input.runDir, "reports");
|
|
205
|
+
await mkdir(contractsDir, { recursive: true });
|
|
206
|
+
await mkdir(reportsDir, { recursive: true });
|
|
207
|
+
const factsPath = path.join(contractsDir, `backend-test-pytest-collection-${input.stem}.json`);
|
|
208
|
+
const reportPath = path.join(reportsDir, `backend-test-pytest-collection-${input.stem}.md`);
|
|
209
|
+
await writeFile(factsPath, `${JSON.stringify(input.facts, null, 2)}\n`, "utf8");
|
|
210
|
+
await writeFile(reportPath, renderBackendPytestCollectionReport(input.facts), "utf8");
|
|
211
|
+
return { factsPath, reportPath };
|
|
212
|
+
}
|
|
213
|
+
export async function readBackendPytestCollectionFacts(filePath) {
|
|
214
|
+
return backendPytestCollectionFactsSchema.parse(JSON.parse(await readFile(filePath, "utf8")));
|
|
215
|
+
}
|
|
216
|
+
async function assertBackendPytestRepairSafety(workspaceRoot, initial, final) {
|
|
217
|
+
if (JSON.stringify(initial.mappedScripts) !== JSON.stringify(final.mappedScripts)) {
|
|
218
|
+
throw new Error("backend pytest repair changed mapped script scope");
|
|
219
|
+
}
|
|
220
|
+
const forbidden = [
|
|
221
|
+
[/pytest\.mark\.(?:skip|skipif|xfail)\b|pytest\.(?:skip|xfail)\s*\(/, "skip/xfail"],
|
|
222
|
+
[/sys\.path\s*\.|PYTHONPATH/, "sys.path/PYTHONPATH mutation"],
|
|
223
|
+
[/except\s+ImportError\b/, "ImportError fallback"],
|
|
224
|
+
[/except\s+(?:Exception|BaseException)\b[\s\S]{0,160}?\bpass\b/, "broad exception swallowing"],
|
|
225
|
+
];
|
|
226
|
+
for (const file of final.assetFiles) {
|
|
227
|
+
const source = await readFile(path.resolve(workspaceRoot, file), "utf8");
|
|
228
|
+
for (const [pattern, label] of forbidden) {
|
|
229
|
+
if (pattern.test(source))
|
|
230
|
+
throw new Error(`backend pytest repair safety blocked ${label}: ${file}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function assertSameInventory(expected, actual) {
|
|
235
|
+
const expectedFiles = Object.keys(expected.inputHashes).sort();
|
|
236
|
+
const actualFiles = Object.keys(actual.inputHashes).sort();
|
|
237
|
+
if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles))
|
|
238
|
+
throw new Error("backend pytest collection asset inventory drift");
|
|
239
|
+
for (const file of expectedFiles) {
|
|
240
|
+
if (expected.inputHashes[file] !== actual.inputHashes[file])
|
|
241
|
+
throw new Error(`backend pytest collection hash drift: ${file}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export async function assertBackendPytestCollectionFresh(workspaceRoot, effective) {
|
|
245
|
+
if (effective.phase !== "effective" || effective.status !== "PASS") {
|
|
246
|
+
throw new Error("backend pytest execution requires effective collection PASS facts");
|
|
247
|
+
}
|
|
248
|
+
const current = await buildBackendPytestAssetInventory(workspaceRoot, effective.mappedScripts);
|
|
249
|
+
assertSameInventory(effective, current);
|
|
250
|
+
}
|
|
251
|
+
export async function materializeEffectiveBackendPytestCollection(input) {
|
|
252
|
+
if (input.initial.status === "PASS") {
|
|
253
|
+
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.initial.mappedScripts);
|
|
254
|
+
assertSameInventory(input.initial, current);
|
|
255
|
+
return backendPytestCollectionFactsSchema.parse({
|
|
256
|
+
...input.initial,
|
|
257
|
+
phase: "effective",
|
|
258
|
+
collectionSource: "initial",
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (input.initial.status === "BLOCKED")
|
|
262
|
+
throw new Error("backend pytest collection is blocked and not repairable");
|
|
263
|
+
if (!input.final)
|
|
264
|
+
throw new Error("backend pytest collection repair requires final collection facts");
|
|
265
|
+
if (input.final.status !== "PASS")
|
|
266
|
+
throw new Error("backend pytest collection still fails after bounded repair");
|
|
267
|
+
await assertBackendPytestRepairSafety(input.workspaceRoot, input.initial, input.final);
|
|
268
|
+
const current = await buildBackendPytestAssetInventory(input.workspaceRoot, input.final.mappedScripts);
|
|
269
|
+
assertSameInventory(input.final, current);
|
|
270
|
+
return backendPytestCollectionFactsSchema.parse({
|
|
271
|
+
...input.final,
|
|
272
|
+
phase: "effective",
|
|
273
|
+
repairEligible: false,
|
|
274
|
+
repairAttempt: 1,
|
|
275
|
+
collectionSource: "final",
|
|
276
|
+
});
|
|
277
|
+
}
|