@tea-agent/loop-agent 0.12.0 → 0.13.0-beta.0
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/AGENTS.md +155 -153
- package/CHANGELOG.md +338 -265
- package/README.md +345 -298
- package/bin/agent-worker.js +22 -22
- package/bin/loop-agent.js +21 -21
- package/dist/application/dag/generate-task-dag.js +28 -28
- package/dist/application/evaluation/candidate-hash.js +75 -0
- package/dist/application/evaluation/candidate.js +52 -0
- package/dist/application/evaluation/replay.js +289 -0
- package/dist/application/evaluation/types.js +130 -0
- package/dist/cli/command-definitions.js +27 -7
- package/dist/cli/program.js +8 -4
- package/dist/commands/cursor-prompt.js +6 -6
- package/dist/commands/eval.js +235 -0
- package/dist/commands/init.js +544 -506
- package/dist/commands/knowledge.js +129 -31
- package/dist/commands/loop-benchmark.js +11 -11
- package/dist/commands/pi-reuse-benchmark.js +16 -16
- package/dist/executors/pi-sdk-executor.js +38 -24
- package/dist/executors/shell-executor.js +34 -2
- package/dist/executors/shell-presets.js +20 -0
- package/dist/executors/shell-verification.js +7 -0
- package/dist/governance/manifest-types.js +4 -0
- package/dist/infrastructure/evaluation/candidate-store.js +435 -0
- package/dist/infrastructure/evaluation/store.js +40 -0
- package/dist/sidecars/cursor-prompt/executor.js +1 -1
- package/dist/task/config-types.js +28 -1
- package/dist/task/runtime.js +27 -27
- package/dist/worker/cli.js +96 -1
- package/dist/worker/delivery/package.js +3 -3
- package/dist/worker/feature/decision-loader.js +37 -6
- package/dist/worker/feature/next-action.js +10 -2
- package/dist/worker/feature/ready-plan-projection.js +81 -0
- package/dist/worker/feature/reducer.js +2 -1
- package/dist/worker/feature/review.js +19 -2
- package/dist/worker/feature/run.js +27 -2
- package/dist/worker/follow-up/approve.js +5 -2
- package/dist/worker/follow-up/factory.js +1 -1
- package/dist/worker/observability/read-model.js +246 -41
- package/dist/worker/observe/routes.js +173 -15
- package/dist/worker/observe/spec-evidence.js +281 -0
- package/dist/worker/observe/static/api.js +46 -27
- package/dist/worker/observe/static/app.js +150 -150
- package/dist/worker/observe/static/constants.js +148 -148
- package/dist/worker/observe/static/copy.js +67 -67
- package/dist/worker/observe/static/dag-helpers.js +172 -172
- 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/dag-model.js +72 -72
- package/dist/worker/observe/static/dom.js +61 -61
- package/dist/worker/observe/static/format-pool.js +67 -67
- package/dist/worker/observe/static/format.js +292 -292
- package/dist/worker/observe/static/index.html +308 -308
- package/dist/worker/observe/static/kpi.js +94 -94
- package/dist/worker/observe/static/relations.js +133 -128
- package/dist/worker/observe/static/router.js +93 -85
- package/dist/worker/observe/static/run-processing.js +148 -148
- package/dist/worker/observe/static/shell-chrome.js +68 -68
- package/dist/worker/observe/static/state.js +253 -253
- package/dist/worker/observe/static/styles.css +1902 -1890
- package/dist/worker/observe/static/views/batch.js +227 -226
- package/dist/worker/observe/static/views/dag-graph.js +172 -172
- package/dist/worker/observe/static/views/dag-inspector.js +607 -477
- package/dist/worker/observe/static/views/dag.js +362 -362
- package/dist/worker/observe/static/views/dashboard.js +445 -442
- package/dist/worker/observe/static/views/failures.js +143 -143
- package/dist/worker/observe/static/views/feature.js +492 -453
- package/dist/worker/observe/static/views/pool.js +350 -347
- package/dist/worker/observe/static/views/run.js +453 -453
- package/dist/worker/observe/static/views/session-timeline.js +205 -205
- package/dist/worker/observe/static/views/shell.js +7 -7
- package/dist/worker/observe/static/views/task.js +314 -260
- package/dist/worker/observe/static/views/timeline.js +163 -163
- package/dist/worker/pool/doctor.js +165 -0
- package/dist/worker/pool/migrate-state.js +303 -0
- package/dist/worker/pool/run-store.js +205 -17
- package/dist/worker/pool/types.js +17 -1
- package/dist/worker/pool/validation.js +100 -15
- package/dist/worker/report/morning-report.js +12 -2
- package/dist/worker/runner/run-ready.js +41 -26
- package/dist/worker/task-graph/ready-planner.js +136 -0
- package/dist/workflows/dag/backend-test-analysis-contract.js +120 -0
- package/dist/workflows/dag/canvas-observer.js +275 -275
- package/dist/workflows/dag/convergence/controller.js +16 -8
- package/dist/workflows/dag/dynamic-runtime/map.js +90 -2
- package/dist/workflows/dag/failure-routing.js +12 -1
- package/dist/workflows/dag/init-hybrid.js +2404 -360
- package/dist/workflows/dag/node-execution.js +9 -0
- package/dist/workflows/dag/prompt.js +9 -0
- package/dist/workflows/dag/report.js +35 -1
- package/dist/workflows/dag/runner.js +28 -2
- package/dist/workflows/dag/task-demand-routing.js +383 -0
- package/dist/workflows/dag/types.js +51 -13
- package/dist/workflows/dag/upstream-artifacts.js +1 -0
- package/dist/workflows/dag/validate.js +59 -1
- package/docs/README.md +106 -104
- package/docs/agent-dag-recovery-playbook.md +195 -184
- package/docs/agent-dag-runner.md +67 -67
- package/docs/architecture/README.md +26 -26
- package/docs/architecture/dag-execution.md +140 -140
- package/docs/architecture/evolution.md +54 -53
- package/docs/architecture/facts-and-state.md +71 -58
- package/docs/architecture/runtime-boundaries.md +191 -191
- package/docs/architecture/system-overview.md +93 -93
- package/docs/architecture/worker-and-feature.md +85 -81
- package/docs/cursor-prompt-sidecar.md +36 -36
- package/docs/decisions/README.md +18 -15
- package/docs/design/README.md +167 -77
- package/docs/development-principles.md +73 -73
- package/docs/exec-plans/README.md +6 -6
- package/docs/exec-plans/active/README.md +15 -9
- package/docs/exec-plans/completed/README.md +85 -73
- package/docs/feature-workflow.md +389 -261
- package/docs/harness-methodology-debugging.md +153 -153
- package/docs/harness-methodology-tdd.md +130 -130
- package/docs/harness-methodology-verification.md +27 -27
- package/docs/init-surface.manifest.json +289 -280
- package/docs/loop-agent-harness.md +142 -130
- package/docs/production-readiness.md +96 -96
- package/docs/progress/README.md +64 -54
- package/docs/reports/README.md +117 -94
- package/docs/skills/README.md +7 -7
- package/docs/skills/vetted-skill-registry.md +29 -27
- 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-dogfood-report.md +117 -117
- 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/agent-dag.base.json +190 -190
- package/docs/templates/agent-dag.final-verification.json +185 -185
- package/docs/templates/agent-dag.schema.json +411 -383
- package/docs/templates/agent-dag.supervised-implementation.json +501 -501
- package/docs/templates/backend-test-analysis.schema.json +44 -0
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +202 -139
- package/docs/templates/backend-test-dag.json +311 -276
- package/docs/templates/backend-test-dag.retrospect.prompt.md +125 -125
- package/docs/templates/backend-test-dag.review-cases.prompt.md +81 -81
- package/docs/templates/exec-plan.md +64 -64
- package/docs/templates/feature-spec.md +53 -53
- package/docs/templates/frontend-design-contract.md +42 -33
- package/docs/templates/frontend-task-constraints.md +35 -25
- package/docs/templates/frontend-task-requirement.md +70 -61
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +5 -0
- package/docs/templates/frontend-test-dag.json +23 -0
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.retrospect.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-cases.prompt.md +3 -0
- package/docs/templates/frontend-test-dag.review-execution.prompt.md +3 -0
- package/docs/templates/harness.schema.json +221 -221
- package/docs/templates/hybrid-dag.json +188 -188
- package/docs/templates/init-evolution-review.md +35 -35
- package/docs/templates/interactive-ui-round2-experiment.md +66 -66
- package/docs/templates/knowledge-graph-bootstrap-dag.json +118 -0
- package/docs/templates/knowledge-sync-dag.json +178 -0
- package/docs/templates/knowledge-sync-draft.schema.json +71 -0
- package/docs/templates/product-line/AGENTS.md +8 -8
- package/docs/templates/product-line/README.md +9 -9
- package/docs/templates/product-line/acceptance.yaml +14 -14
- 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/task-graph.yaml +15 -15
- package/docs/templates/product-line/task.yaml +64 -64
- package/docs/templates/product-line/test-plan.md +7 -7
- package/docs/templates/production-readiness-checklist.md +57 -57
- package/docs/templates/progress-log.md +17 -17
- 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/docs/verification-matrix.md +70 -66
- package/examples/decision-gate-agent-dag.json +177 -177
- package/examples/example-dag.json +46 -46
- package/examples/hybrid-loop-agent-dag.json +189 -189
- package/harness.json +66 -66
- package/package.json +88 -46
- package/scripts/check-product-line-docs.sh +29 -29
- package/scripts/check-task-pool-root.sh +32 -32
- package/scripts/kb-bootstrap-init-skeleton.sh +240 -0
- package/scripts/kb-graph-incremental-prepare.mjs +386 -0
- package/scripts/kb-graph-incremental-prepare.sh +5 -0
- package/scripts/kb-graph-materialize.mjs +105 -0
- package/scripts/kb-graph-materialize.sh +4 -0
- package/scripts/kb-graph-promote.mjs +164 -0
- package/scripts/kb-graph-promote.sh +4 -0
- package/scripts/kb-query.mjs +554 -0
- package/scripts/kb-query.sh +5 -0
- package/skills/agent-worker/SKILL.md +39 -37
- package/skills/agent-worker/references/agent-worker-operator.md +60 -43
- package/skills/ai-engineering-context/SKILL.md +48 -48
- package/skills/analyze-product-dependencies/SKILL.md +67 -0
- package/skills/analyze-product-dependencies/agents/openai.yaml +4 -0
- package/skills/analyze-product-dependencies/references/api-documentation-schema.md +30 -0
- package/skills/analyze-product-dependencies/references/dependency-analysis-schema.md +28 -0
- package/skills/analyze-product-dependencies/references/example.md +76 -0
- package/skills/analyze-product-dependencies/references/forward-test-cases.md +35 -0
- package/skills/analyze-product-dependencies/references/input-contract.md +11 -0
- package/skills/analyze-product-dependencies/references/scouting-rules.md +61 -0
- package/skills/analyze-product-dependencies/scripts/test-validators.mjs +267 -0
- package/skills/analyze-product-dependencies/scripts/validate-api-documentation.mjs +101 -0
- package/skills/analyze-product-dependencies/scripts/validate-dependency-analysis.mjs +142 -0
- package/skills/analyze-product-dependencies/scripts/validate-product-requirement-input.mjs +76 -0
- package/skills/analyze-product-dependencies/scripts/validation-helpers.mjs +146 -0
- package/skills/analyze-product-requirements/SKILL.md +90 -0
- package/skills/analyze-product-requirements/agents/openai.yaml +4 -0
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +91 -0
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +56 -0
- package/skills/analyze-product-requirements/references/example.md +86 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +66 -0
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +32 -0
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +33 -0
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +35 -0
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +193 -0
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +69 -0
- package/skills/analyze-product-requirements/scripts/validate-product-requirement.mjs +97 -0
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +98 -0
- package/skills/analyze-product-requirements/scripts/validation-helpers.mjs +156 -0
- package/skills/code-review-core/SKILL.md +20 -20
- package/skills/codebase-scout/SKILL.md +19 -19
- package/skills/frontend-design-review/SKILL.md +66 -59
- package/skills/frontend-design-review/references/review-checklist.md +58 -37
- package/skills/frontend-implementation/SKILL.md +47 -51
- package/skills/frontend-implementation/references/code-standards.md +32 -34
- package/skills/frontend-implementation/references/design-spec.md +46 -46
- package/skills/frontend-implementation/references/node-contracts.md +76 -32
- package/skills/frontend-review/SKILL.md +59 -53
- package/skills/frontend-review/references/review-findings.md +47 -42
- package/skills/frontend-verification/SKILL.md +53 -40
- package/skills/frontend-verification/references/verification-checklist.md +68 -56
- package/skills/grill-me/SKILL.md +10 -10
- package/skills/grill-with-docs/SKILL.md +88 -88
- package/skills/grill-with-docs/adr-format.md +47 -47
- package/skills/grill-with-docs/context-format.md +60 -60
- package/skills/init-capability-evolution/SKILL.md +70 -70
- package/skills/loop-agent/SKILL.md +151 -151
- package/skills/loop-agent/references/README.md +67 -67
- package/skills/loop-agent/references/command-reference.md +505 -452
- package/skills/loop-agent/references/docs-converge.md +126 -126
- package/skills/loop-agent/references/harness-policy.md +263 -263
- package/skills/loop-agent/references/hybrid-dag.md +238 -233
- 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/model-routing.md +36 -36
- package/skills/loop-agent/references/multi-worktree.md +54 -54
- package/skills/loop-agent/references/one-shot-runs.md +85 -85
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -169
- 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 +44 -44
- package/skills/loop-agent/references/task-workflow.md +89 -89
- package/skills/loop-agent/references/verification-and-failure-handling.md +139 -139
- package/skills/playwright-cli/SKILL.md +420 -0
- package/skills/playwright-cli/references/element-attributes.md +23 -0
- package/skills/playwright-cli/references/playwright-tests.md +39 -0
- package/skills/playwright-cli/references/request-mocking.md +87 -0
- package/skills/playwright-cli/references/running-code.md +241 -0
- package/skills/playwright-cli/references/session-management.md +225 -0
- package/skills/playwright-cli/references/storage-state.md +275 -0
- package/skills/playwright-cli/references/test-generation.md +433 -0
- package/skills/playwright-cli/references/tracing.md +139 -0
- package/skills/playwright-cli/references/video-recording.md +143 -0
- package/skills/playwright-cli-case-generator/SKILL.md +74 -0
- 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/SKILL.md +296 -296
- 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/test-driven-development/SKILL.md +20 -20
- 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,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { access, readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { assertValidDagSpec } from "./validate.js";
|
|
@@ -14,6 +15,7 @@ import { getTaskPaths, loadTaskConfig } from "../../task/runtime.js";
|
|
|
14
15
|
import { materializeTaskReferenceDocs } from "../../task/source-references.js";
|
|
15
16
|
import { resolveVerifyPreset } from "../../executors/shell-verification.js";
|
|
16
17
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
18
|
+
import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "./task-demand-routing.js";
|
|
17
19
|
const REQUIREMENT_FILE = "需求.md";
|
|
18
20
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
19
21
|
const REFERENCE_DIRECTORY = "references";
|
|
@@ -148,6 +150,425 @@ const STANDARD_GLOBAL_CONSTRAINTS = [
|
|
|
148
150
|
"exclusive implementer nodes must use narrow, concrete writeSet paths; never keep ** or repo root",
|
|
149
151
|
`Replace ${IMPLEMENT_WRITESET_PLACEHOLDER} with concrete paths before executing the implementation writer`,
|
|
150
152
|
];
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// Frontend Mock capability discovery & mode resolution
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
/** Evidence-based check: does package.json contain a mock-related script? */
|
|
157
|
+
async function packageJsonHasMockScript(repoRoot) {
|
|
158
|
+
try {
|
|
159
|
+
const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
|
|
160
|
+
const pkg = JSON.parse(raw);
|
|
161
|
+
const scripts = pkg.scripts ?? {};
|
|
162
|
+
const mockScripts = Object.keys(scripts).filter((name) => name === "mock" ||
|
|
163
|
+
name.startsWith("mock:") ||
|
|
164
|
+
name.startsWith("dev:mock") ||
|
|
165
|
+
/mock/i.test(name));
|
|
166
|
+
const verifyCommands = mockScripts
|
|
167
|
+
.filter((name) => /(?:test|check|verify|contract)/i.test(name))
|
|
168
|
+
.map((name) => ({
|
|
169
|
+
label: `npm run ${name}`,
|
|
170
|
+
args: ["npm", "run", name],
|
|
171
|
+
cwd: repoRoot,
|
|
172
|
+
}));
|
|
173
|
+
return {
|
|
174
|
+
hasScript: mockScripts.length > 0,
|
|
175
|
+
scriptNames: mockScripts,
|
|
176
|
+
verifyCommands,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
return { hasScript: false, scriptNames: [], verifyCommands: [] };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/** Check whether a direct (non-transitive) dependency exists in package.json. */
|
|
184
|
+
async function hasDirectDependency(repoRoot, depName) {
|
|
185
|
+
try {
|
|
186
|
+
const raw = await readFile(path.join(repoRoot, "package.json"), "utf-8");
|
|
187
|
+
const pkg = JSON.parse(raw);
|
|
188
|
+
const deps = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
|
|
189
|
+
return depName in deps;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Check whether handler/fixture/bootstrap files exist for known mock frameworks. */
|
|
196
|
+
async function discoverMockHandlerFiles(repoRoot, serviceRoot) {
|
|
197
|
+
const exactCandidates = [
|
|
198
|
+
...[
|
|
199
|
+
"src/mocks/handlers.ts",
|
|
200
|
+
"src/mocks/handlers.js",
|
|
201
|
+
"src/mocks/browser.ts",
|
|
202
|
+
"src/mocks/browser.js",
|
|
203
|
+
"src/mocks/server.ts",
|
|
204
|
+
"src/mocks/server.js",
|
|
205
|
+
"mocks/handlers.ts",
|
|
206
|
+
"mocks/handlers.js",
|
|
207
|
+
"mocks/browser.ts",
|
|
208
|
+
"mocks/browser.js",
|
|
209
|
+
].map((candidatePath) => ({ framework: "msw", path: candidatePath })),
|
|
210
|
+
...["db.json", "mock/db.json", "src/mock/db.json"].map((candidatePath) => ({ framework: "json-server", path: candidatePath })),
|
|
211
|
+
];
|
|
212
|
+
const directoryCandidates = [
|
|
213
|
+
...(serviceRoot ? [{ path: serviceRoot }] : []),
|
|
214
|
+
{ framework: "mockjs", path: "src/mock" },
|
|
215
|
+
{ framework: "mockjs", path: "mock" },
|
|
216
|
+
{ framework: "msw", path: "src/mocks" },
|
|
217
|
+
{ framework: "msw", path: "mocks" },
|
|
218
|
+
{ framework: "mirage", path: "src/mirage" },
|
|
219
|
+
{ framework: "mirage", path: "mirage" },
|
|
220
|
+
];
|
|
221
|
+
const foundPaths = new Set();
|
|
222
|
+
let foundFramework;
|
|
223
|
+
for (const candidate of exactCandidates) {
|
|
224
|
+
try {
|
|
225
|
+
await access(path.join(repoRoot, candidate.path));
|
|
226
|
+
foundPaths.add(candidate.path);
|
|
227
|
+
foundFramework ??= candidate.framework;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Exact candidate does not exist.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function collectFiles(directory, relativeDirectory) {
|
|
234
|
+
let entries;
|
|
235
|
+
try {
|
|
236
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (foundPaths.size >= 24)
|
|
243
|
+
return;
|
|
244
|
+
const absoluteEntry = path.join(directory, entry.name);
|
|
245
|
+
const relativeEntry = path.posix.join(relativeDirectory.replace(/\\/g, "/"), entry.name);
|
|
246
|
+
if (entry.isDirectory()) {
|
|
247
|
+
await collectFiles(absoluteEntry, relativeEntry);
|
|
248
|
+
}
|
|
249
|
+
else if (entry.isFile()) {
|
|
250
|
+
foundPaths.add(relativeEntry);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const candidate of directoryCandidates) {
|
|
255
|
+
const before = foundPaths.size;
|
|
256
|
+
await collectFiles(path.join(repoRoot, candidate.path), candidate.path);
|
|
257
|
+
if (foundPaths.size > before && candidate.framework) {
|
|
258
|
+
foundFramework ??= candidate.framework;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { framework: foundFramework, paths: [...foundPaths].sort() };
|
|
262
|
+
}
|
|
263
|
+
async function discoverMockBootstrapImports(repoRoot) {
|
|
264
|
+
const entryCandidates = [
|
|
265
|
+
"src/main.ts",
|
|
266
|
+
"src/main.tsx",
|
|
267
|
+
"src/main.js",
|
|
268
|
+
"src/main.jsx",
|
|
269
|
+
"src/index.ts",
|
|
270
|
+
"src/index.tsx",
|
|
271
|
+
"src/index.js",
|
|
272
|
+
"src/index.jsx",
|
|
273
|
+
"src/setupTests.ts",
|
|
274
|
+
"src/setupTests.js",
|
|
275
|
+
"test/setup.ts",
|
|
276
|
+
"test/setup.js",
|
|
277
|
+
];
|
|
278
|
+
const imports = [];
|
|
279
|
+
for (const candidate of entryCandidates) {
|
|
280
|
+
try {
|
|
281
|
+
const content = await readFile(path.join(repoRoot, candidate), "utf-8");
|
|
282
|
+
if (/(?:from\s*|import\s*)["'][^"']*(?:mock|msw|mirage)[^"']*["']/i.test(content)) {
|
|
283
|
+
imports.push(candidate);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Candidate entry does not exist or is unreadable.
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return imports;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Deterministic frontend Mock capability discovery.
|
|
294
|
+
*
|
|
295
|
+
* Strong evidence (at least one must be hit to judge "present"):
|
|
296
|
+
* 1. package.json mock script + corresponding config/entry
|
|
297
|
+
* 2. Direct dependency (MSW, Mock.js, Mirage, json-server, Vite Mock plugin) + handler files
|
|
298
|
+
* 3. Application bootstrap imports project mock files
|
|
299
|
+
* 4. Project specs explicitly define mock service root, handler dir, and startup method
|
|
300
|
+
*
|
|
301
|
+
* Anti-evidence (cannot alone judge "present"):
|
|
302
|
+
* - lockfile-only or transitive dependency
|
|
303
|
+
* - test variable named "mock"
|
|
304
|
+
* - fixtures without service registration
|
|
305
|
+
* - neighboring project mock services
|
|
306
|
+
* - model-directory-name guessing
|
|
307
|
+
*/
|
|
308
|
+
export async function discoverFrontendMockCapability(repoRoot, taskConfig) {
|
|
309
|
+
const safetyViolation = await frontendMockServiceRootSafetyViolation(repoRoot, taskConfig);
|
|
310
|
+
if (safetyViolation) {
|
|
311
|
+
return {
|
|
312
|
+
status: "ambiguous",
|
|
313
|
+
serviceRoot: taskConfig.frontendMock?.serviceRoot,
|
|
314
|
+
safetyViolation,
|
|
315
|
+
evidencePaths: [],
|
|
316
|
+
verifyCommands: [],
|
|
317
|
+
reasons: [safetyViolation],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const evidencePaths = [];
|
|
321
|
+
const reasons = [];
|
|
322
|
+
let framework;
|
|
323
|
+
let serviceRoot;
|
|
324
|
+
let strongEvidenceCount = 0;
|
|
325
|
+
let ambiguousSignals = 0;
|
|
326
|
+
// 1. Check package.json mock scripts
|
|
327
|
+
const scriptResult = await packageJsonHasMockScript(repoRoot);
|
|
328
|
+
if (scriptResult.hasScript) {
|
|
329
|
+
reasons.push(`package.json has mock scripts: ${scriptResult.scriptNames.join(", ")}`);
|
|
330
|
+
evidencePaths.push("package.json");
|
|
331
|
+
// A script alone is not strong evidence unless we also find config/entry
|
|
332
|
+
}
|
|
333
|
+
// 2. Check for direct mock framework dependencies
|
|
334
|
+
const mockDeps = ["msw", "mockjs", "miragejs", "json-server", "vite-plugin-mock"];
|
|
335
|
+
const foundDeps = [];
|
|
336
|
+
for (const dep of mockDeps) {
|
|
337
|
+
if (await hasDirectDependency(repoRoot, dep)) {
|
|
338
|
+
foundDeps.push(dep);
|
|
339
|
+
evidencePaths.push(`package.json (${dep})`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// 3. Check for handler/fixture/bootstrap files
|
|
343
|
+
const serviceRootHint = taskConfig.frontendMock?.serviceRoot;
|
|
344
|
+
const handlerResult = await discoverMockHandlerFiles(repoRoot, serviceRootHint);
|
|
345
|
+
const bootstrapImports = await discoverMockBootstrapImports(repoRoot);
|
|
346
|
+
if (handlerResult.framework) {
|
|
347
|
+
framework = handlerResult.framework;
|
|
348
|
+
}
|
|
349
|
+
if (handlerResult.paths.length > 0) {
|
|
350
|
+
evidencePaths.push(...handlerResult.paths);
|
|
351
|
+
reasons.push(`Mock handler/fixture paths found: ${handlerResult.paths.join(", ")}`);
|
|
352
|
+
}
|
|
353
|
+
if (bootstrapImports.length > 0) {
|
|
354
|
+
evidencePaths.push(...bootstrapImports);
|
|
355
|
+
reasons.push(`Application/test bootstrap imports Mock code: ${bootstrapImports.join(", ")}`);
|
|
356
|
+
}
|
|
357
|
+
// Evaluate strong evidence
|
|
358
|
+
// Case: direct dep + handler files
|
|
359
|
+
if (foundDeps.length > 0 && handlerResult.paths.length > 0) {
|
|
360
|
+
strongEvidenceCount++;
|
|
361
|
+
reasons.push(`Direct mock dependency (${foundDeps.join(", ")}) with handler files`);
|
|
362
|
+
}
|
|
363
|
+
// Case: mock script in package.json + corresponding config/entry
|
|
364
|
+
if (scriptResult.hasScript && handlerResult.paths.length > 0) {
|
|
365
|
+
strongEvidenceCount++;
|
|
366
|
+
reasons.push("Mock scripts and handler files both present");
|
|
367
|
+
}
|
|
368
|
+
if (bootstrapImports.length > 0 && handlerResult.paths.length > 0) {
|
|
369
|
+
strongEvidenceCount++;
|
|
370
|
+
reasons.push("Application/test bootstrap and project Mock files both present");
|
|
371
|
+
}
|
|
372
|
+
// Case: project specs define mock service root
|
|
373
|
+
if (taskConfig.frontendMock?.serviceRoot && handlerResult.paths.length > 0) {
|
|
374
|
+
strongEvidenceCount++;
|
|
375
|
+
serviceRoot = taskConfig.frontendMock.serviceRoot;
|
|
376
|
+
reasons.push(`task config specifies serviceRoot=${serviceRoot}`);
|
|
377
|
+
}
|
|
378
|
+
// Handle ambiguous: some signals but not enough for "present"
|
|
379
|
+
if (strongEvidenceCount === 0 &&
|
|
380
|
+
(foundDeps.length > 0 ||
|
|
381
|
+
scriptResult.hasScript ||
|
|
382
|
+
handlerResult.paths.length > 0 ||
|
|
383
|
+
bootstrapImports.length > 0)) {
|
|
384
|
+
ambiguousSignals++;
|
|
385
|
+
if (foundDeps.length > 0 && handlerResult.paths.length === 0) {
|
|
386
|
+
reasons.push(`Mock dependency found (${foundDeps.join(", ")}) but no handler/bootstrap files detected`);
|
|
387
|
+
}
|
|
388
|
+
if (scriptResult.hasScript && handlerResult.paths.length === 0 && foundDeps.length === 0) {
|
|
389
|
+
reasons.push("Mock scripts exist but no handler files or direct mock dependencies found");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// Build verify commands from task config
|
|
393
|
+
const configuredVerifyCommands = (taskConfig.frontendMock?.verifyCommands ?? []).map((cmd) => ({
|
|
394
|
+
label: cmd.label,
|
|
395
|
+
args: ["bash", "-lc", cmd.command],
|
|
396
|
+
cwd: repoRoot,
|
|
397
|
+
timeoutMs: cmd.timeoutMs,
|
|
398
|
+
}));
|
|
399
|
+
const verifyCommands = [...configuredVerifyCommands, ...scriptResult.verifyCommands]
|
|
400
|
+
.filter((command, index, commands) => commands.findIndex((candidate) => candidate.label === command.label) === index);
|
|
401
|
+
if (strongEvidenceCount > 0) {
|
|
402
|
+
return {
|
|
403
|
+
status: "present",
|
|
404
|
+
framework,
|
|
405
|
+
serviceRoot: serviceRoot ?? taskConfig.frontendMock?.serviceRoot,
|
|
406
|
+
evidencePaths,
|
|
407
|
+
verifyCommands,
|
|
408
|
+
reasons,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (ambiguousSignals > 0) {
|
|
412
|
+
return {
|
|
413
|
+
status: "ambiguous",
|
|
414
|
+
framework,
|
|
415
|
+
serviceRoot: taskConfig.frontendMock?.serviceRoot,
|
|
416
|
+
evidencePaths,
|
|
417
|
+
verifyCommands,
|
|
418
|
+
reasons,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
status: "absent",
|
|
423
|
+
evidencePaths,
|
|
424
|
+
verifyCommands,
|
|
425
|
+
reasons: ["No mock service evidence found in project"],
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function patternStaticPrefix(pattern) {
|
|
429
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
430
|
+
const wildcard = normalized.search(/[?*]/);
|
|
431
|
+
return (wildcard >= 0 ? normalized.slice(0, wildcard) : normalized).replace(/\/+$/, "");
|
|
432
|
+
}
|
|
433
|
+
function frontendMockServiceRootAllowed(taskConfig) {
|
|
434
|
+
const serviceRoot = taskConfig.frontendMock?.serviceRoot;
|
|
435
|
+
if (!serviceRoot)
|
|
436
|
+
return true;
|
|
437
|
+
const normalized = serviceRoot.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
438
|
+
if (normalized === "." ||
|
|
439
|
+
normalized === ".." ||
|
|
440
|
+
normalized.startsWith("../") ||
|
|
441
|
+
path.isAbsolute(serviceRoot) ||
|
|
442
|
+
/[?*]/.test(normalized)) {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
const allowed = taskConfig.allowedPaths.some((allowedPath) => pathMatchesPattern(normalized, allowedPath) ||
|
|
446
|
+
pathMatchesPattern(`${normalized}/_probe_`, allowedPath));
|
|
447
|
+
if (!allowed)
|
|
448
|
+
return false;
|
|
449
|
+
return !mergeForbiddenPaths(taskConfig).some((forbiddenPath) => {
|
|
450
|
+
const forbiddenPrefix = patternStaticPrefix(forbiddenPath);
|
|
451
|
+
return (pathMatchesPattern(normalized, forbiddenPath) ||
|
|
452
|
+
(forbiddenPrefix.length > 0 &&
|
|
453
|
+
pathMatchesPattern(forbiddenPrefix, normalized)));
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
async function frontendMockServiceRootSafetyViolation(repoRoot, taskConfig) {
|
|
457
|
+
const serviceRoot = taskConfig.frontendMock?.serviceRoot;
|
|
458
|
+
if (!serviceRoot)
|
|
459
|
+
return undefined;
|
|
460
|
+
if (!frontendMockServiceRootAllowed(taskConfig)) {
|
|
461
|
+
return `frontendMock.serviceRoot is outside allowedPaths or overlaps forbiddenPaths: ${serviceRoot}`;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const [repoRealPath, serviceRealPath] = await Promise.all([
|
|
465
|
+
realpath(repoRoot),
|
|
466
|
+
realpath(path.resolve(repoRoot, serviceRoot)),
|
|
467
|
+
]);
|
|
468
|
+
const relativeRealPath = path.relative(repoRealPath, serviceRealPath);
|
|
469
|
+
if (relativeRealPath === ".." ||
|
|
470
|
+
relativeRealPath.startsWith(`..${path.sep}`) ||
|
|
471
|
+
path.isAbsolute(relativeRealPath)) {
|
|
472
|
+
return `frontendMock.serviceRoot resolves outside the repository: ${serviceRoot}`;
|
|
473
|
+
}
|
|
474
|
+
const normalizedRealPath = relativeRealPath.split(path.sep).join("/") || ".";
|
|
475
|
+
if (!frontendMockServiceRootAllowed({
|
|
476
|
+
...taskConfig,
|
|
477
|
+
frontendMock: {
|
|
478
|
+
...(taskConfig.frontendMock ?? { policy: "auto", verifyCommands: [] }),
|
|
479
|
+
serviceRoot: normalizedRealPath,
|
|
480
|
+
},
|
|
481
|
+
})) {
|
|
482
|
+
return `frontendMock.serviceRoot resolves outside its allowed boundary: ${serviceRoot}`;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
// A missing configured root is capability absence, not a path escape. The
|
|
487
|
+
// discovery pass below will report it without traversing another location.
|
|
488
|
+
if (error.code !== "ENOENT") {
|
|
489
|
+
return `frontendMock.serviceRoot safety could not be verified: ${serviceRoot}`;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return undefined;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Heuristic: does the task have interface/async data dependencies?
|
|
496
|
+
*
|
|
497
|
+
* Checks (in priority order):
|
|
498
|
+
* 1. taskConfig.frontendMock.policy === "required"
|
|
499
|
+
* 2. Requirement references API docs, schemas, endpoints
|
|
500
|
+
* 3. Acceptance criteria mention requests, async data, or service states
|
|
501
|
+
* 4. Contract/scout confirmed existing API call chain (not available at generation time)
|
|
502
|
+
*/
|
|
503
|
+
export function hasApiDependency(sources) {
|
|
504
|
+
if (sources.taskConfig.frontendMock?.policy === "required") {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
const requirement = normalizeTaskRequirementText(sources.requirementMarkdown)
|
|
508
|
+
.replace(/`[^`\n]*`/g, " ");
|
|
509
|
+
const dependencyPatterns = [
|
|
510
|
+
/(?:接口文档|接口定义|接口协议|后端接口|服务端接口|接口联调|请求|响应|远程数据|异步数据|数据获取|模拟接口|模拟数据)/,
|
|
511
|
+
/(?<![A-Za-z0-9_])API(?![A-Za-z0-9_])/i,
|
|
512
|
+
/\b(?:endpoint|request|response|fetch|axios|schema|mock|backend\s+api|server\s+api)\b/i,
|
|
513
|
+
];
|
|
514
|
+
const negationPatterns = [
|
|
515
|
+
/(?:不涉及|无需|不需要|不依赖|不调用|不请求|没有|禁止|不得).{0,16}(?:接口|后端|服务端|远程数据|异步数据|API)/i,
|
|
516
|
+
/\b(?:no|without|does\s+not|do\s+not|must\s+not)\b.{0,24}\b(?:api|endpoint|request|backend|server)\b/i,
|
|
517
|
+
];
|
|
518
|
+
return requirement
|
|
519
|
+
.split(/[。!?!?;;,,\r\n]+/)
|
|
520
|
+
.map((clause) => clause.trim())
|
|
521
|
+
.filter(Boolean)
|
|
522
|
+
.some((clause) => !negationPatterns.some((pattern) => pattern.test(clause)) &&
|
|
523
|
+
dependencyPatterns.some((pattern) => pattern.test(clause)));
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Resolve frontend Mock mode from capability seed, task config, and interface dependency analysis.
|
|
527
|
+
*
|
|
528
|
+
* Decision matrix (from docs/design/frontend-mock-data-workflow.md):
|
|
529
|
+
*
|
|
530
|
+
* | 接口/异步数据依赖 | 既有 Mock 服务 | policy | 结果 |
|
|
531
|
+
* |---|---|---|---|
|
|
532
|
+
* | 无 | 任意 | auto | not-required |
|
|
533
|
+
* | 有 | present | auto | required |
|
|
534
|
+
* | 有 | present/absent/ambiguous | auto | required (strategy chooses a safe mechanism) |
|
|
535
|
+
* | 任意 | present | required | required |
|
|
536
|
+
* | 任意 | absent/ambiguous | required | blocked |
|
|
537
|
+
* | 任意 | 任意 | disabled | not-required (if spec allows) else blocked |
|
|
538
|
+
*/
|
|
539
|
+
export function resolveFrontendMockMode(capability, taskConfig, hasApiDep) {
|
|
540
|
+
const policy = taskConfig.frontendMock?.policy ?? "auto";
|
|
541
|
+
const hasDeterministicMockVerification = capability.verifyCommands.length > 0;
|
|
542
|
+
if (capability.safetyViolation || !frontendMockServiceRootAllowed(taskConfig)) {
|
|
543
|
+
return "blocked";
|
|
544
|
+
}
|
|
545
|
+
// disabled policy: must respect project mock rules (can't override spec)
|
|
546
|
+
if (policy === "disabled") {
|
|
547
|
+
// When disabled but the project spec mandates mock, it's blocked
|
|
548
|
+
if (capability.status === "present") {
|
|
549
|
+
// Project has mock service; disabled is an explicit override that still allows not-required
|
|
550
|
+
return "not-required";
|
|
551
|
+
}
|
|
552
|
+
return "not-required";
|
|
553
|
+
}
|
|
554
|
+
// required policy
|
|
555
|
+
if (policy === "required") {
|
|
556
|
+
if (capability.status === "present" && hasDeterministicMockVerification) {
|
|
557
|
+
return "required";
|
|
558
|
+
}
|
|
559
|
+
return "blocked";
|
|
560
|
+
}
|
|
561
|
+
// auto policy
|
|
562
|
+
if (!hasApiDep) {
|
|
563
|
+
return "not-required";
|
|
564
|
+
}
|
|
565
|
+
// In auto mode, capability discovery is evidence for the strategy node, not
|
|
566
|
+
// a final mechanism decision. Projects without a native Mock service may use
|
|
567
|
+
// an existing browser interception harness or a reversible request adapter.
|
|
568
|
+
// The deterministic strategy gate blocks before the writer when none can be
|
|
569
|
+
// verified by the DAG's frozen static/behavior entrypoints.
|
|
570
|
+
return "required";
|
|
571
|
+
}
|
|
151
572
|
function mapTaskComplexity(complexity) {
|
|
152
573
|
if (complexity === "small")
|
|
153
574
|
return "LOW";
|
|
@@ -312,6 +733,10 @@ function markdownVerifyCommand(repoRoot, command) {
|
|
|
312
733
|
label: command,
|
|
313
734
|
};
|
|
314
735
|
}
|
|
736
|
+
function isSupportedMarkdownVerifyCommand(command) {
|
|
737
|
+
return (/^(npm|pnpm|yarn|bun)\s+(run\s+)?[a-z0-9:_-]+(?:\s.*)?$/i.test(command) ||
|
|
738
|
+
/^(npx|pnpm\s+exec|yarn\s+exec|bunx)\s+(vitest|jest|playwright|cypress|tsc|eslint)(?:\s.*)?$/i.test(command));
|
|
739
|
+
}
|
|
315
740
|
function extractFrontendVerifyCommandsFromMarkdown(input) {
|
|
316
741
|
if (!input.repoRoot)
|
|
317
742
|
return { staticCommands: [], behaviorCommands: [] };
|
|
@@ -328,8 +753,7 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
|
|
|
328
753
|
const codeSpanCommands = Array.from(bulletless.matchAll(/`([^`]+)`/g), (match) => match[1].trim());
|
|
329
754
|
const candidates = codeSpanCommands.length > 0 ? codeSpanCommands : [bulletless];
|
|
330
755
|
for (const candidate of candidates) {
|
|
331
|
-
if (
|
|
332
|
-
/^(npx|pnpm\s+exec|yarn\s+exec|bunx)\s+(vitest|jest|playwright|cypress|tsc|eslint)(?:\s.*)?$/i.test(candidate)) {
|
|
756
|
+
if (isSupportedMarkdownVerifyCommand(candidate)) {
|
|
333
757
|
commands.add(candidate);
|
|
334
758
|
}
|
|
335
759
|
}
|
|
@@ -350,6 +774,26 @@ function extractFrontendVerifyCommandsFromMarkdown(input) {
|
|
|
350
774
|
}
|
|
351
775
|
return { staticCommands, behaviorCommands };
|
|
352
776
|
}
|
|
777
|
+
function extractFrontendMockVerifyCommandsFromMarkdown(input) {
|
|
778
|
+
const commands = [];
|
|
779
|
+
const markdown = [
|
|
780
|
+
input.requirementMarkdown,
|
|
781
|
+
input.constraintMarkdown ?? "",
|
|
782
|
+
].join("\n");
|
|
783
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
784
|
+
if (!/(?:mock|模拟服务|接口桩)/i.test(line))
|
|
785
|
+
continue;
|
|
786
|
+
for (const match of line.matchAll(/`([^`]+)`/g)) {
|
|
787
|
+
const commandText = match[1].trim();
|
|
788
|
+
if (!isSupportedMarkdownVerifyCommand(commandText))
|
|
789
|
+
continue;
|
|
790
|
+
const command = markdownVerifyCommand(input.repoRoot, commandText);
|
|
791
|
+
if (command)
|
|
792
|
+
commands.push(command);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return commands.filter((command, index, all) => all.findIndex((candidate) => candidate.args.join("\0") === command.args.join("\0")) === index);
|
|
796
|
+
}
|
|
353
797
|
function chooseFrontendVerifyCommands(input) {
|
|
354
798
|
if (input.parsedCommands.length > 0) {
|
|
355
799
|
return { commands: input.parsedCommands, commandSource: "inline" };
|
|
@@ -411,6 +855,39 @@ function toTaskRelativeSourcePath(sources, absolutePath) {
|
|
|
411
855
|
.relative(sources.taskDir, absolutePath)
|
|
412
856
|
.replaceAll(path.sep, "/");
|
|
413
857
|
}
|
|
858
|
+
function extractExplicitRequirementIds(...markdownInputs) {
|
|
859
|
+
const ids = [];
|
|
860
|
+
const seen = new Set();
|
|
861
|
+
for (const markdown of markdownInputs) {
|
|
862
|
+
if (!markdown)
|
|
863
|
+
continue;
|
|
864
|
+
for (const match of markdown.matchAll(/\b(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi)) {
|
|
865
|
+
const id = match[0].toUpperCase();
|
|
866
|
+
if (!seen.has(id)) {
|
|
867
|
+
seen.add(id);
|
|
868
|
+
ids.push(id);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return ids;
|
|
873
|
+
}
|
|
874
|
+
function buildDagSourceBinding(sources) {
|
|
875
|
+
const sourceEntries = [
|
|
876
|
+
{ kind: "requirement", path: sources.requirementPath, markdown: sources.requirementMarkdown },
|
|
877
|
+
...(sources.constraintMarkdown ? [{ kind: "constraint", path: sources.constraintPath, markdown: sources.constraintMarkdown }] : []),
|
|
878
|
+
...(sources.referenceDocuments ?? []).map((reference) => ({ kind: "reference", path: reference.path, markdown: reference.markdown })),
|
|
879
|
+
];
|
|
880
|
+
return {
|
|
881
|
+
schemaVersion: 1,
|
|
882
|
+
taskId: sources.taskId,
|
|
883
|
+
sources: sourceEntries.map((source) => ({
|
|
884
|
+
kind: source.kind,
|
|
885
|
+
path: toTaskRelativeSourcePath(sources, source.path),
|
|
886
|
+
sha256: createHash("sha256").update(source.markdown, "utf8").digest("hex"),
|
|
887
|
+
})),
|
|
888
|
+
requirementIds: extractExplicitRequirementIds(sources.requirementMarkdown, sources.constraintMarkdown, ...(sources.referenceDocuments ?? []).map((reference) => reference.markdown)),
|
|
889
|
+
};
|
|
890
|
+
}
|
|
414
891
|
function buildSourceContextBlock(sources) {
|
|
415
892
|
const requirementRef = toTaskRelativeSourcePath(sources, sources.requirementPath);
|
|
416
893
|
const requirementExcerpt = excerptMarkdown(sources.requirementMarkdown, {
|
|
@@ -535,7 +1012,7 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
535
1012
|
catch (error) {
|
|
536
1013
|
throw new Error(`failed to load verification commands for task "${taskId}": ${error instanceof Error ? error.message : String(error)}`);
|
|
537
1014
|
}
|
|
538
|
-
|
|
1015
|
+
const sources = {
|
|
539
1016
|
taskId,
|
|
540
1017
|
repoRoot,
|
|
541
1018
|
taskDir: paths.taskDir,
|
|
@@ -551,6 +1028,26 @@ export async function loadTaskHybridSources(repoRoot, taskId) {
|
|
|
551
1028
|
verifyCommands,
|
|
552
1029
|
sddEmbeddedSkills: await probeRepoLocalSddSkills(repoRoot),
|
|
553
1030
|
};
|
|
1031
|
+
return sources;
|
|
1032
|
+
}
|
|
1033
|
+
async function prepareFrontendMockSources(sources) {
|
|
1034
|
+
const repoRoot = sources.repoRoot ?? process.cwd();
|
|
1035
|
+
const capability = await discoverFrontendMockCapability(repoRoot, sources.taskConfig);
|
|
1036
|
+
const sourceMockVerifyCommands = extractFrontendMockVerifyCommandsFromMarkdown({
|
|
1037
|
+
repoRoot,
|
|
1038
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
1039
|
+
constraintMarkdown: sources.constraintMarkdown,
|
|
1040
|
+
});
|
|
1041
|
+
for (const command of sourceMockVerifyCommands) {
|
|
1042
|
+
if (!capability.verifyCommands.some((existing) => existing.args.join("\0") === command.args.join("\0"))) {
|
|
1043
|
+
capability.verifyCommands.push(command);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return {
|
|
1047
|
+
...sources,
|
|
1048
|
+
frontendMockCapability: capability,
|
|
1049
|
+
frontendMockMode: resolveFrontendMockMode(capability, sources.taskConfig, hasApiDependency(sources)),
|
|
1050
|
+
};
|
|
554
1051
|
}
|
|
555
1052
|
function mergeFinalVerifyCommands(repoRoot, taskConfig, adapterCommands) {
|
|
556
1053
|
const taskCommands = taskConfig.verifyCommands.map((command) => ({
|
|
@@ -758,54 +1255,167 @@ export function buildStandardHybridDagFromTask(sources) {
|
|
|
758
1255
|
assertValidDagSpec(spec);
|
|
759
1256
|
return spec;
|
|
760
1257
|
}
|
|
761
|
-
function
|
|
1258
|
+
function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths) {
|
|
1259
|
+
return {
|
|
1260
|
+
id: "frontend-mock-assess-pi",
|
|
1261
|
+
depends_on: ["frontend-contract-pi", "frontend-scout-pi"],
|
|
1262
|
+
role: "planner",
|
|
1263
|
+
executor: "pi",
|
|
1264
|
+
complexity: "MED",
|
|
1265
|
+
writePolicy: "read-only",
|
|
1266
|
+
allowedPaths: readOnlyPaths,
|
|
1267
|
+
forbiddenPaths,
|
|
1268
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1269
|
+
outputContract: "Plain Markdown whose first non-empty line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
|
|
1270
|
+
subtask_prompt: [
|
|
1271
|
+
"Perform read-only Mock assessment and select one safe frontend data strategy.",
|
|
1272
|
+
"The first non-empty line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked.",
|
|
1273
|
+
"Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
|
|
1274
|
+
"Select not-needed only with positive evidence that no remote API is involved, a stable real backend will be exercised, or existing fixtures already cover the contract without changes. not-needed still requires the fixed behavior entrypoint to exercise applicable real or no-remote behavior verification. When configured policy is required, not-needed is forbidden.",
|
|
1275
|
+
"Configured policy disabled requests no Mock but cannot override project specifications; if an actually-read project rule requires Mock, select blocked.",
|
|
1276
|
+
"",
|
|
1277
|
+
"## Required Output Sections:",
|
|
1278
|
+
"- Mock Decision: required | not-required | blocked (with reasoning)",
|
|
1279
|
+
"- API Contract Evidence and Specification Evidence: actual Mock/API/schema specs read (paths + excerpts)",
|
|
1280
|
+
"- Service Evidence: detected Mock framework, service root, handler/fixture/bootstrap paths",
|
|
1281
|
+
"- Backend Readiness and Selection Evidence: why the selected mechanism is available and appropriate",
|
|
1282
|
+
"- Endpoint / Fixture Matrix: method/path, source, request, success, empty, error, permission, consumer, fixture/evidence",
|
|
1283
|
+
"- Activation and Target Files: explicit dev/test activation and authorized implementation paths",
|
|
1284
|
+
"- Production Safety: how Mock stays off and the real request remains default",
|
|
1285
|
+
"- Verification Plan: map the strategy to the fixed entrypoints below; do not propose replacement shell commands",
|
|
1286
|
+
"- Real Integration Gap: what remains unproved until the real backend is ready",
|
|
1287
|
+
"- Blocking Issues: any spec gaps, path violations, missing verify commands, or conflicts",
|
|
1288
|
+
"",
|
|
1289
|
+
"## Rules:",
|
|
1290
|
+
"- Read project Mock/API/schema specifications before making any judgment.",
|
|
1291
|
+
"- Do not infer Mock service from lockfile-only or transitive dependency evidence.",
|
|
1292
|
+
"- Output MOCK_STRATEGY: blocked if capability evidence conflicts, contract fields are missing/conflicting, paths or dependencies are unauthorized, specs were not actually read, sources conflict, production-default-off cannot be proven, the API layer is not writable for a new adapter, or the frozen entrypoints cannot verify the selected strategy.",
|
|
1293
|
+
"- Never comment out or replace the real request with inline data, hard-code Mock enablement, import test mocks from a production entrypoint, invent API fields, or place secrets/real user data in fixtures.",
|
|
1294
|
+
"- Mock-backed behavior evidence proves the documented frontend contract only; it never proves real API integration.",
|
|
1295
|
+
"",
|
|
1296
|
+
"Read-only: do not modify repository files.",
|
|
1297
|
+
fixedVerificationContext,
|
|
1298
|
+
sourceContext,
|
|
1299
|
+
mockContextBlock,
|
|
1300
|
+
].join("\n\n"),
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function buildFrontendMockContractGateNode(mockMode, configuredPolicy, readOnlyPaths, forbiddenPaths) {
|
|
1304
|
+
// A generation-time blocked decision is a hard fail-closed contract. Keep a
|
|
1305
|
+
// syntactically valid, impossible verdict so the shell gate can never pass
|
|
1306
|
+
// regardless of what the assessment model emits.
|
|
1307
|
+
const acceptedStrategies = mockMode === "blocked"
|
|
1308
|
+
? ["MOCK_STRATEGY: __blocked__"]
|
|
1309
|
+
: configuredPolicy === "disabled"
|
|
1310
|
+
? ["MOCK_STRATEGY: not-needed"]
|
|
1311
|
+
: [
|
|
1312
|
+
"MOCK_STRATEGY: native",
|
|
1313
|
+
"MOCK_STRATEGY: browser-intercept",
|
|
1314
|
+
"MOCK_STRATEGY: request-adapter",
|
|
1315
|
+
...(configuredPolicy !== "required"
|
|
1316
|
+
? ["MOCK_STRATEGY: not-needed"]
|
|
1317
|
+
: []),
|
|
1318
|
+
];
|
|
1319
|
+
return {
|
|
1320
|
+
id: "frontend-mock-contract-gate-shell",
|
|
1321
|
+
depends_on: ["frontend-mock-assess-pi"],
|
|
1322
|
+
role: "verifier",
|
|
1323
|
+
executor: "shell",
|
|
1324
|
+
complexity: "LOW",
|
|
1325
|
+
writePolicy: "read-only",
|
|
1326
|
+
allowedPaths: readOnlyPaths,
|
|
1327
|
+
forbiddenPaths,
|
|
1328
|
+
outputContract: "Deterministic Mock contract gate: exit 0 only when frontend-mock-assess-pi selects an allowed non-blocked strategy. Does not authorize code writes.",
|
|
1329
|
+
subtask_prompt: "Deterministic gate: block plan/design/implement when frontend-mock-assess-pi selected blocked, selected not-needed under explicit policy=required, or emitted malformed output. Failure route: ContractMismatch.",
|
|
1330
|
+
shell: {
|
|
1331
|
+
commands: [],
|
|
1332
|
+
verdictGate: {
|
|
1333
|
+
fromNodeId: "frontend-mock-assess-pi",
|
|
1334
|
+
accept: acceptedStrategies,
|
|
1335
|
+
label: "frontend mock contract",
|
|
1336
|
+
lineMode: "first-non-empty",
|
|
1337
|
+
},
|
|
1338
|
+
cwd: ".",
|
|
1339
|
+
timeoutMs: 60000,
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function buildFrontendMockVerifyNode(sources, implementId, readOnlyPaths, forbiddenPaths) {
|
|
1344
|
+
const capability = sources.frontendMockCapability;
|
|
1345
|
+
const taskConfig = sources.taskConfig;
|
|
1346
|
+
// Collect verify commands from task config, capability seed, and manifest
|
|
1347
|
+
const verifyCommands = [];
|
|
1348
|
+
// 1. Task config commands (highest priority)
|
|
1349
|
+
for (const cmd of taskConfig.frontendMock?.verifyCommands ?? []) {
|
|
1350
|
+
verifyCommands.push({
|
|
1351
|
+
label: cmd.label,
|
|
1352
|
+
args: ["bash", "-lc", cmd.command],
|
|
1353
|
+
cwd: sources.repoRoot ?? ".",
|
|
1354
|
+
timeoutMs: cmd.timeoutMs,
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
// 2. Capability seed commands (from discovery)
|
|
1358
|
+
if (capability) {
|
|
1359
|
+
for (const cmd of capability.verifyCommands) {
|
|
1360
|
+
if (!verifyCommands.some((existing) => existing.label === cmd.label)) {
|
|
1361
|
+
verifyCommands.push(cmd);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
// Fail closed: no commands = no verify shell
|
|
1366
|
+
const commands = verifyCommands.length > 0
|
|
1367
|
+
? buildVerifyShellCommands({
|
|
1368
|
+
repoRoot: sources.repoRoot ?? ".",
|
|
1369
|
+
commands: verifyCommands,
|
|
1370
|
+
fallbackCommands: [],
|
|
1371
|
+
})
|
|
1372
|
+
: [];
|
|
1373
|
+
return {
|
|
1374
|
+
id: "frontend-mock-verify-shell",
|
|
1375
|
+
depends_on: [implementId],
|
|
1376
|
+
role: "verifier",
|
|
1377
|
+
executor: "shell",
|
|
1378
|
+
complexity: "LOW",
|
|
1379
|
+
writePolicy: "read-only",
|
|
1380
|
+
allowedPaths: readOnlyPaths,
|
|
1381
|
+
forbiddenPaths,
|
|
1382
|
+
outputContract: "Archived shell stdout/stderr with exit codes for deterministic Mock-specific verification; no worktree writes.",
|
|
1383
|
+
subtask_prompt: "Run deterministic Mock-specific verification (handler loading, endpoint matrix, fixture consumption, production boundary). Commands are frozen from generation-time trusted sources only.",
|
|
1384
|
+
shell: {
|
|
1385
|
+
commands,
|
|
1386
|
+
verifyEvidence: buildVerifyEvidence({
|
|
1387
|
+
phase: "intermediate",
|
|
1388
|
+
quota: "full",
|
|
1389
|
+
commandSource: commands.length > 0 ? "inline" : "adapter",
|
|
1390
|
+
commands: verifyCommands.length > 0 ? verifyCommands : undefined,
|
|
1391
|
+
fallbackCommands: [],
|
|
1392
|
+
}),
|
|
1393
|
+
cwd: ".",
|
|
1394
|
+
timeoutMs: 300000,
|
|
1395
|
+
},
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function buildBlockedFrontendMockDag(sources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints) {
|
|
762
1399
|
const { taskConfig } = sources;
|
|
763
|
-
const
|
|
764
|
-
const implementPaths = resolveImplementPaths(taskConfig);
|
|
765
|
-
const implementId = frontendImplementationNodeId();
|
|
766
|
-
const sourceContext = buildSourceContextBlock(sources);
|
|
767
|
-
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
768
|
-
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
769
|
-
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
770
|
-
const globalConstraints = [
|
|
771
|
-
...taskConfig.hardConstraints,
|
|
772
|
-
...(sources.constraintMarkdown
|
|
773
|
-
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
774
|
-
: []),
|
|
775
|
-
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
776
|
-
"frontend-implementation DAGs must pass the design verdict gate before any write node executes.",
|
|
777
|
-
"frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
|
|
778
|
-
"frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
|
|
779
|
-
];
|
|
780
|
-
const staticFallbackCommands = ["npm run typecheck", "npm run build"];
|
|
781
|
-
const behaviorFallbackCommands = ["npm test"];
|
|
782
|
-
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
783
|
-
repoRoot: sources.repoRoot,
|
|
784
|
-
requirementMarkdown: sources.requirementMarkdown,
|
|
785
|
-
constraintMarkdown: sources.constraintMarkdown,
|
|
786
|
-
});
|
|
787
|
-
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
788
|
-
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
789
|
-
adapterCommands: sources.verifyCommands?.intermediate,
|
|
790
|
-
});
|
|
791
|
-
const behaviorVerifyCommands = chooseFrontendVerifyCommands({
|
|
792
|
-
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
793
|
-
adapterCommands: sources.verifyCommands?.final,
|
|
794
|
-
});
|
|
1400
|
+
const mockContextBlock = resolveFrontendMockContextBlock(sources);
|
|
795
1401
|
const spec = {
|
|
796
1402
|
version: 3,
|
|
797
|
-
title: `Frontend implementation DAG: ${taskConfig.title}`,
|
|
1403
|
+
title: `Frontend implementation DAG (BLOCKED Mock): ${taskConfig.title}`,
|
|
798
1404
|
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
1405
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
799
1406
|
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
800
1407
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
801
|
-
globalConstraints
|
|
1408
|
+
globalConstraints: [
|
|
1409
|
+
...globalConstraints,
|
|
1410
|
+
"Mock contract is BLOCKED: writer nodes must not be reachable. Resolve blocking issues and re-generate DAG.",
|
|
1411
|
+
"Do not execute any write, verify, or closeout nodes. The DAG ends at the Mock contract gate.",
|
|
1412
|
+
],
|
|
802
1413
|
defaults: {
|
|
803
1414
|
...FRONTEND_DEFAULTS,
|
|
804
1415
|
contextProfile: taskConfig.contextProfile,
|
|
805
1416
|
},
|
|
806
1417
|
skillsByRole: FRONTEND_SKILLS_BY_ROLE,
|
|
807
1418
|
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
808
|
-
verifyStrategy: resolveDagVerifyStrategy(taskConfig),
|
|
809
1419
|
tasks: [
|
|
810
1420
|
{
|
|
811
1421
|
id: "frontend-contract-pi",
|
|
@@ -843,60 +1453,376 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
843
1453
|
sourceContext,
|
|
844
1454
|
].join("\n\n"),
|
|
845
1455
|
},
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1456
|
+
buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, "No verification entrypoints were materialized because the generation-time Mock contract is blocked.", readOnlyPaths, forbiddenPaths),
|
|
1457
|
+
buildFrontendMockContractGateNode("blocked", taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
|
|
1458
|
+
],
|
|
1459
|
+
};
|
|
1460
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
1461
|
+
parseDagSpec(spec);
|
|
1462
|
+
assertValidDagSpec(spec);
|
|
1463
|
+
return spec;
|
|
1464
|
+
}
|
|
1465
|
+
function resolveFrontendMockContextBlock(sources) {
|
|
1466
|
+
const capability = sources.frontendMockCapability;
|
|
1467
|
+
const mode = sources.frontendMockMode ?? "not-required";
|
|
1468
|
+
if (!capability)
|
|
1469
|
+
return "";
|
|
1470
|
+
const parts = [
|
|
1471
|
+
"## Frontend Mock Context",
|
|
1472
|
+
"",
|
|
1473
|
+
`Configured Policy: ${sources.taskConfig.frontendMock?.policy ?? "auto"}`,
|
|
1474
|
+
`Mock Decision: ${mode}`,
|
|
1475
|
+
`Capability Status: ${capability.status}`,
|
|
1476
|
+
];
|
|
1477
|
+
if (capability.framework) {
|
|
1478
|
+
parts.push(`Detected Framework: ${capability.framework}`);
|
|
1479
|
+
}
|
|
1480
|
+
if (capability.serviceRoot) {
|
|
1481
|
+
parts.push(`Service Root: ${capability.serviceRoot}`);
|
|
1482
|
+
}
|
|
1483
|
+
if (capability.evidencePaths.length > 0) {
|
|
1484
|
+
parts.push(`Evidence Paths: ${capability.evidencePaths.join(", ")}`);
|
|
1485
|
+
}
|
|
1486
|
+
if (capability.verifyCommands.length > 0) {
|
|
1487
|
+
parts.push(`Frozen Mock Verify Commands: ${capability.verifyCommands
|
|
1488
|
+
.map((command) => command.label)
|
|
1489
|
+
.join(", ")}`);
|
|
1490
|
+
}
|
|
1491
|
+
if (capability.safetyViolation) {
|
|
1492
|
+
parts.push(`Safety Violation: ${capability.safetyViolation}`);
|
|
1493
|
+
}
|
|
1494
|
+
if (capability.reasons.length > 0) {
|
|
1495
|
+
parts.push(`Reasons: ${capability.reasons.join("; ")}`);
|
|
1496
|
+
}
|
|
1497
|
+
if (mode === "required") {
|
|
1498
|
+
parts.push("Mock-backed frontend verification is required. Prefer the detected native service; otherwise the assessment may select an existing browser interception harness or reversible request adapter. Any handler, fixture, adapter, and UI changes stay in the single frontend-implement-pi writeSet.");
|
|
1499
|
+
}
|
|
1500
|
+
if (mode === "not-required") {
|
|
1501
|
+
parts.push("Generation-time evidence does not require Mock. The assessment must still use contract/scout evidence: select not-needed only positively, or select a safe Mock strategy if an API dependency is confirmed.");
|
|
1502
|
+
}
|
|
1503
|
+
if (mode === "blocked") {
|
|
1504
|
+
parts.push("Mock contract is blocked. The DAG must stop before any write node executes.");
|
|
1505
|
+
}
|
|
1506
|
+
return parts.join("\n");
|
|
1507
|
+
}
|
|
1508
|
+
function buildFrontendHybridDagFromTask(sources) {
|
|
1509
|
+
const { taskConfig } = sources;
|
|
1510
|
+
const mockCapability = sources.frontendMockCapability ?? {
|
|
1511
|
+
status: "absent",
|
|
1512
|
+
evidencePaths: [],
|
|
1513
|
+
verifyCommands: [],
|
|
1514
|
+
reasons: ["Frontend Mock capability was not precomputed; assessment must verify repository evidence."],
|
|
1515
|
+
};
|
|
1516
|
+
const mockMode = sources.frontendMockMode ??
|
|
1517
|
+
resolveFrontendMockMode(mockCapability, taskConfig, hasApiDependency(sources));
|
|
1518
|
+
const frontendSources = {
|
|
1519
|
+
...sources,
|
|
1520
|
+
frontendMockCapability: mockCapability,
|
|
1521
|
+
frontendMockMode: mockMode,
|
|
1522
|
+
};
|
|
1523
|
+
const forbiddenPaths = mergeForbiddenPaths(taskConfig);
|
|
1524
|
+
const implementPaths = resolveImplementPaths(taskConfig);
|
|
1525
|
+
const implementId = frontendImplementationNodeId();
|
|
1526
|
+
const sourceContext = buildSourceContextBlock(sources);
|
|
1527
|
+
const mockContextBlock = resolveFrontendMockContextBlock(frontendSources);
|
|
1528
|
+
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
1529
|
+
mockCapability.verifyCommands.length > 0;
|
|
1530
|
+
const requirementIds = buildDagSourceBinding(sources).requirementIds;
|
|
1531
|
+
const requirementCoverageInstruction = requirementIds.length > 0
|
|
1532
|
+
? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
|
|
1533
|
+
: "";
|
|
1534
|
+
const strategy = resolveDagVerifyStrategy(taskConfig);
|
|
1535
|
+
const readOnlyPaths = taskConfig.allowedPaths.length > 0 ? taskConfig.allowedPaths : ["**"];
|
|
1536
|
+
const behaviorPaths = deriveFrontendBehaviorPaths(taskConfig);
|
|
1537
|
+
const globalConstraints = [
|
|
1538
|
+
...taskConfig.hardConstraints,
|
|
1539
|
+
...(sources.constraintMarkdown
|
|
1540
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
1541
|
+
: []),
|
|
1542
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
1543
|
+
"Frontend implementation DAGs must pass the final design verdict gate before any write node executes; the first design gate also accepts request-revision for plan revision only.",
|
|
1544
|
+
"Final design gate pass is the only authorization for frontend implementation writes.",
|
|
1545
|
+
"Plan revision remains read-only and never edits business code.",
|
|
1546
|
+
"Design revision failures route to replan-and-rerun, never dev-fix.",
|
|
1547
|
+
"Frontend planning must consume the read-only Mock assessment strategy produced after scouting; MOCK_STRATEGY: blocked must not pass the deterministic Mock contract gate.",
|
|
1548
|
+
"Mock implementations must preserve the real request path as the default, require explicit test/dev activation, and never rely on commenting out the real request.",
|
|
1549
|
+
"Mock-backed behavior evidence proves only the documented frontend contract, never real API integration.",
|
|
1550
|
+
"frontend-implementation DAGs must complete deterministic static verification and behavior verification before final review.",
|
|
1551
|
+
"frontend review must block closeout unless review verdict is exactly VERDICT: pass.",
|
|
1552
|
+
];
|
|
1553
|
+
// Guard: blocked mode — generate assessment-only DAG with no writer reachable
|
|
1554
|
+
if (mockMode === "blocked") {
|
|
1555
|
+
return buildBlockedFrontendMockDag(frontendSources, sourceContext, readOnlyPaths, forbiddenPaths, globalConstraints);
|
|
1556
|
+
}
|
|
1557
|
+
const staticFallbackCommands = ["npm run typecheck", "npm run build"];
|
|
1558
|
+
const behaviorFallbackCommands = ["npm test"];
|
|
1559
|
+
const parsedFrontendVerifyCommands = extractFrontendVerifyCommandsFromMarkdown({
|
|
1560
|
+
repoRoot: sources.repoRoot,
|
|
1561
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
1562
|
+
constraintMarkdown: sources.constraintMarkdown,
|
|
1563
|
+
});
|
|
1564
|
+
const staticVerifyCommands = chooseFrontendVerifyCommands({
|
|
1565
|
+
parsedCommands: parsedFrontendVerifyCommands.staticCommands,
|
|
1566
|
+
adapterCommands: sources.verifyCommands?.intermediate,
|
|
1567
|
+
});
|
|
1568
|
+
const behaviorVerifyCommands = chooseFrontendVerifyCommands({
|
|
1569
|
+
parsedCommands: parsedFrontendVerifyCommands.behaviorCommands,
|
|
1570
|
+
adapterCommands: sources.verifyCommands?.final,
|
|
1571
|
+
});
|
|
1572
|
+
const staticShellCommands = buildVerifyShellCommands({
|
|
1573
|
+
repoRoot: sources.repoRoot,
|
|
1574
|
+
commands: staticVerifyCommands.commands,
|
|
1575
|
+
fallbackCommands: staticFallbackCommands,
|
|
1576
|
+
});
|
|
1577
|
+
const behaviorShellCommands = buildVerifyShellCommands({
|
|
1578
|
+
repoRoot: sources.repoRoot,
|
|
1579
|
+
commands: behaviorVerifyCommands.commands,
|
|
1580
|
+
fallbackCommands: behaviorFallbackCommands,
|
|
1581
|
+
});
|
|
1582
|
+
const staticVerifyEvidence = buildVerifyEvidence({
|
|
1583
|
+
phase: "intermediate",
|
|
1584
|
+
quota: strategy.intermediateQuota ?? "full",
|
|
1585
|
+
commandSource: staticVerifyCommands.commandSource,
|
|
1586
|
+
commands: staticVerifyCommands.commands,
|
|
1587
|
+
fallbackCommands: staticFallbackCommands,
|
|
1588
|
+
});
|
|
1589
|
+
const behaviorVerifyEvidence = buildVerifyEvidence({
|
|
1590
|
+
phase: "final",
|
|
1591
|
+
quota: "full",
|
|
1592
|
+
commandSource: behaviorVerifyCommands.commandSource,
|
|
1593
|
+
commands: behaviorVerifyCommands.commands,
|
|
1594
|
+
fallbackCommands: behaviorFallbackCommands,
|
|
1595
|
+
finalFullRequired: true,
|
|
1596
|
+
});
|
|
1597
|
+
const fixedVerificationContext = [
|
|
1598
|
+
"## Fixed frontend verification entrypoints",
|
|
1599
|
+
"These shell entrypoints are fixed at DAG generation and are the only commands the static and behavior shell nodes execute. A strategy or plan may add tests behind an existing entrypoint inside writeSet, but must not invent or replace commands or assume subtask_prompt executes a command.",
|
|
1600
|
+
`- Static command source: ${staticVerifyEvidence.commandSource}`,
|
|
1601
|
+
...staticVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
1602
|
+
`- Behavior command source: ${behaviorVerifyEvidence.commandSource}`,
|
|
1603
|
+
...behaviorVerifyEvidence.commandLabels.map((command) => ` - ${JSON.stringify(command)}`),
|
|
1604
|
+
].join("\n");
|
|
1605
|
+
const spec = {
|
|
1606
|
+
version: 3,
|
|
1607
|
+
title: `Frontend implementation DAG: ${taskConfig.title}`,
|
|
1608
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
1609
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
1610
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
1611
|
+
globalConstraints,
|
|
1612
|
+
defaults: {
|
|
1613
|
+
...FRONTEND_DEFAULTS,
|
|
1614
|
+
contextProfile: taskConfig.contextProfile,
|
|
1615
|
+
},
|
|
1616
|
+
skillsByRole: FRONTEND_SKILLS_BY_ROLE,
|
|
1617
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
1618
|
+
verifyStrategy: resolveDagVerifyStrategy(taskConfig),
|
|
1619
|
+
tasks: [
|
|
1620
|
+
{
|
|
1621
|
+
id: "frontend-contract-pi",
|
|
1622
|
+
depends_on: [],
|
|
1623
|
+
role: "planner",
|
|
1624
|
+
executor: "pi",
|
|
1625
|
+
complexity: "MED",
|
|
1626
|
+
writePolicy: "read-only",
|
|
1627
|
+
allowedPaths: readOnlyPaths,
|
|
1628
|
+
forbiddenPaths,
|
|
1629
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1630
|
+
outputContract: "Markdown contract with Scope, Non-goals, Acceptance Criteria, UI States, Target Runtime Environment, Risks, and Verification Expectations. No file writes.",
|
|
1631
|
+
subtask_prompt: [
|
|
1632
|
+
"Read task source and produce a concise frontend implementation contract.",
|
|
1633
|
+
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations.",
|
|
1634
|
+
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
1635
|
+
sourceContext,
|
|
1636
|
+
].join("\n\n"),
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
id: "frontend-scout-pi",
|
|
1640
|
+
depends_on: ["frontend-contract-pi"],
|
|
1641
|
+
role: "scout",
|
|
1642
|
+
executor: "pi",
|
|
1643
|
+
complexity: mapTaskComplexity(taskConfig.complexity),
|
|
1644
|
+
writePolicy: "read-only",
|
|
1645
|
+
allowedPaths: readOnlyPaths,
|
|
1646
|
+
forbiddenPaths,
|
|
1647
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1648
|
+
outputContract: "Markdown scout report covering frontend stack, routes, components, styling system, existing design conventions, state/data flow, test entry points, reuse opportunities, and risks. No file writes.",
|
|
1649
|
+
subtask_prompt: [
|
|
1650
|
+
"Inspect frontend code, routing, components, styles, package scripts, and tests.",
|
|
1651
|
+
"Return code and design observations, existing reuse opportunities, and verification entry points.",
|
|
1652
|
+
"Read-only: do not modify repository files.",
|
|
1653
|
+
sourceContext,
|
|
1654
|
+
].join("\n\n"),
|
|
1655
|
+
},
|
|
1656
|
+
// Mock assessment is always read-only and runs before planning.
|
|
1657
|
+
buildFrontendMockAssessNode(frontendSources, sourceContext, mockContextBlock, fixedVerificationContext, readOnlyPaths, forbiddenPaths),
|
|
1658
|
+
buildFrontendMockContractGateNode(mockMode, taskConfig.frontendMock?.policy ?? "auto", readOnlyPaths, forbiddenPaths),
|
|
1659
|
+
{
|
|
1660
|
+
id: "frontend-plan-pi",
|
|
1661
|
+
depends_on: [
|
|
1662
|
+
"frontend-contract-pi",
|
|
1663
|
+
"frontend-scout-pi",
|
|
1664
|
+
"frontend-mock-assess-pi",
|
|
1665
|
+
"frontend-mock-contract-gate-shell",
|
|
1666
|
+
],
|
|
1667
|
+
role: "planner",
|
|
1668
|
+
executor: "pi",
|
|
1669
|
+
complexity: "MED",
|
|
1670
|
+
writePolicy: "read-only",
|
|
1671
|
+
allowedPaths: readOnlyPaths,
|
|
1672
|
+
forbiddenPaths,
|
|
1673
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1674
|
+
outputContract: "Markdown implementation plan with Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks. No file writes.",
|
|
1675
|
+
subtask_prompt: [
|
|
1676
|
+
"Based on frontend-contract-pi, frontend-scout-pi, and the gated frontend-mock-assess-pi strategy, return a minimal frontend implementation plan.",
|
|
1677
|
+
"Carry the selected Mock / API strategy, endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into the plan.",
|
|
1678
|
+
"Include ordered steps, target files, UI state handling, styling/component strategy, interaction notes, Mock/API strategy, dependency policy, deterministic verification entrypoints, and residual risks. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
1679
|
+
requirementCoverageInstruction,
|
|
1680
|
+
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
1681
|
+
fixedVerificationContext,
|
|
1682
|
+
sourceContext,
|
|
1683
|
+
mockContextBlock,
|
|
1684
|
+
].join("\n\n"),
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
id: "frontend-design-gate-pi",
|
|
1688
|
+
depends_on: ["frontend-plan-pi", "frontend-mock-assess-pi"],
|
|
1689
|
+
role: "reviewer",
|
|
1690
|
+
executor: "pi",
|
|
1691
|
+
complexity: "MED",
|
|
1692
|
+
writePolicy: "read-only",
|
|
1693
|
+
allowedPaths: readOnlyPaths,
|
|
1694
|
+
forbiddenPaths,
|
|
1695
|
+
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
1696
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
|
|
1697
|
+
subtask_prompt: [
|
|
1698
|
+
"Audit the frontend plan before implementation.",
|
|
1699
|
+
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
1700
|
+
"Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for the selected strategy. Mock strategies require Mock-backed evidence; not-needed requires applicable real or no-remote behavior evidence.",
|
|
1701
|
+
"Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
1702
|
+
"Read-only: do not modify repository files.",
|
|
1703
|
+
fixedVerificationContext,
|
|
1704
|
+
sourceContext,
|
|
1705
|
+
].join("\n\n"),
|
|
1706
|
+
},
|
|
1707
|
+
{
|
|
1708
|
+
id: "frontend-first-design-gate-shell",
|
|
1709
|
+
depends_on: ["frontend-design-gate-pi"],
|
|
886
1710
|
role: "verifier",
|
|
887
1711
|
executor: "shell",
|
|
888
1712
|
complexity: "LOW",
|
|
889
1713
|
writePolicy: "read-only",
|
|
890
1714
|
allowedPaths: readOnlyPaths,
|
|
891
1715
|
forbiddenPaths,
|
|
892
|
-
outputContract: "Deterministic
|
|
893
|
-
subtask_prompt: "Deterministic gate:
|
|
1716
|
+
outputContract: "Deterministic first design verdict gate: exit 0 when frontend-design-gate-pi emits VERDICT: pass or VERDICT: request-revision. Does not authorize code writes.",
|
|
1717
|
+
subtask_prompt: "Deterministic gate: validate frontend-design-gate-pi first-line VERDICT is pass or request-revision; block downstream only on malformed/unexpected verdict.",
|
|
894
1718
|
shell: {
|
|
895
1719
|
commands: [],
|
|
896
1720
|
verdictGate: {
|
|
897
1721
|
fromNodeId: "frontend-design-gate-pi",
|
|
1722
|
+
accept: ["VERDICT: pass", "VERDICT: request-revision"],
|
|
1723
|
+
label: "frontend first design gate",
|
|
1724
|
+
lineMode: "first-verdict-line",
|
|
1725
|
+
},
|
|
1726
|
+
cwd: ".",
|
|
1727
|
+
timeoutMs: 60000,
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
id: "frontend-plan-revision-pi",
|
|
1732
|
+
depends_on: [
|
|
1733
|
+
"frontend-first-design-gate-shell",
|
|
1734
|
+
"frontend-plan-pi",
|
|
1735
|
+
"frontend-design-gate-pi",
|
|
1736
|
+
"frontend-mock-assess-pi",
|
|
1737
|
+
],
|
|
1738
|
+
role: "planner",
|
|
1739
|
+
executor: "pi",
|
|
1740
|
+
complexity: "MED",
|
|
1741
|
+
writePolicy: "read-only",
|
|
1742
|
+
allowedPaths: readOnlyPaths,
|
|
1743
|
+
forbiddenPaths,
|
|
1744
|
+
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1745
|
+
outputContract: "Markdown revision plan (pass case: first line PASS_NO_REVISION_NEEDED with Requirement Coverage confirmation; request-revision case: complete revised implementation plan with corrections from design findings and Requirement Coverage). No file writes.",
|
|
1746
|
+
subtask_prompt: [
|
|
1747
|
+
"Consume frontend-plan-pi (original plan) and frontend-design-gate-pi (first design review findings).",
|
|
1748
|
+
"If the first design gate passed (VERDICT: pass from frontend-design-gate-pi), output exactly:",
|
|
1749
|
+
"PASS_NO_REVISION_NEEDED",
|
|
1750
|
+
"The original plan from frontend-plan-pi is confirmed and does not require changes.",
|
|
1751
|
+
"Then reproduce a complete Requirement Coverage section containing every explicit REQ-/BR-/AC- identifier from the authoritative task sources so this node is the single effective-plan evidence source for the deterministic coverage gate.",
|
|
1752
|
+
"",
|
|
1753
|
+
"If the first design gate requested revision (VERDICT: request-revision), produce a complete revised implementation plan that addresses every Required Plan Correction from the design findings.",
|
|
1754
|
+
"The revised plan must include Requirement Coverage, Implementation Steps, Target Files, UI State Handling, Styling / Component Strategy, Interaction Notes, Mock / API Strategy, Dependency Policy, Verification Plan, Real Integration Gap, and Residual Risks.",
|
|
1755
|
+
requirementCoverageInstruction,
|
|
1756
|
+
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
1757
|
+
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
1758
|
+
sourceContext,
|
|
1759
|
+
].join("\n\n"),
|
|
1760
|
+
},
|
|
1761
|
+
...(requirementIds.length > 0 ? [{
|
|
1762
|
+
id: "frontend-requirement-coverage-shell",
|
|
1763
|
+
depends_on: ["frontend-plan-revision-pi"],
|
|
1764
|
+
role: "verifier",
|
|
1765
|
+
executor: "shell",
|
|
1766
|
+
complexity: "LOW",
|
|
1767
|
+
writePolicy: "read-only",
|
|
1768
|
+
allowedPaths: readOnlyPaths,
|
|
1769
|
+
forbiddenPaths,
|
|
1770
|
+
outputContract: "Deterministic current-run evidence that the original or revised frontend plan retains every explicit REQ-/BR-/AC- identifier from the bound task sources.",
|
|
1771
|
+
subtask_prompt: "Block final design review when the current run's plan facts omit any explicit requirement identifier from the authoritative task sources.",
|
|
1772
|
+
shell: {
|
|
1773
|
+
commands: [],
|
|
1774
|
+
requirementCoverageGate: { fromNodeIds: ["frontend-plan-revision-pi"], requiredIds: requirementIds, label: "frontend requirement coverage" },
|
|
1775
|
+
cwd: ".",
|
|
1776
|
+
timeoutMs: 60000,
|
|
1777
|
+
},
|
|
1778
|
+
}] : []),
|
|
1779
|
+
{
|
|
1780
|
+
id: "frontend-final-design-review-pi",
|
|
1781
|
+
depends_on: [
|
|
1782
|
+
"frontend-plan-revision-pi",
|
|
1783
|
+
"frontend-plan-pi",
|
|
1784
|
+
"frontend-design-gate-pi",
|
|
1785
|
+
"frontend-mock-assess-pi",
|
|
1786
|
+
...(requirementIds.length > 0 ? ["frontend-requirement-coverage-shell"] : []),
|
|
1787
|
+
],
|
|
1788
|
+
role: "reviewer",
|
|
1789
|
+
executor: "pi",
|
|
1790
|
+
complexity: "MED",
|
|
1791
|
+
writePolicy: "read-only",
|
|
1792
|
+
allowedPaths: readOnlyPaths,
|
|
1793
|
+
forbiddenPaths,
|
|
1794
|
+
skills: FRONTEND_DESIGN_REVIEW_SKILLS,
|
|
1795
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
1796
|
+
subtask_prompt: [
|
|
1797
|
+
"Audit the revised (or confirmed) frontend plan from frontend-plan-revision-pi before implementation.",
|
|
1798
|
+
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
1799
|
+
"If frontend-plan-revision-pi returned PASS_NO_REVISION_NEEDED, confirm the original plan against all design constraints and task requirements. Re-verify that all applicable UI states are covered, dependencies are authorized, and deterministic verification commands are present.",
|
|
1800
|
+
"If frontend-plan-revision-pi revised the plan, verify that every Required Plan Correction from the first design review has been fully addressed.",
|
|
1801
|
+
"Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
|
|
1802
|
+
"The frontend requirement coverage gate has verified that every explicit REQ-/BR-/AC- identifier remains present in the current-run plan evidence; review the mapped behavior rather than accepting identifier presence alone.",
|
|
1803
|
+
"Request revision if any design gap remains, if corrections are incomplete, or if the revised plan introduces new unaddressed issues.",
|
|
1804
|
+
"Read-only: do not modify repository files.",
|
|
1805
|
+
fixedVerificationContext,
|
|
1806
|
+
sourceContext,
|
|
1807
|
+
].join("\n\n"),
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
id: "frontend-final-design-gate-shell",
|
|
1811
|
+
depends_on: ["frontend-final-design-review-pi"],
|
|
1812
|
+
role: "verifier",
|
|
1813
|
+
executor: "shell",
|
|
1814
|
+
complexity: "LOW",
|
|
1815
|
+
writePolicy: "read-only",
|
|
1816
|
+
allowedPaths: readOnlyPaths,
|
|
1817
|
+
forbiddenPaths,
|
|
1818
|
+
outputContract: "Deterministic final design verdict gate: exit 0 only when frontend-final-design-review-pi emits VERDICT: pass. This is the sole authorization for frontend implementation writes.",
|
|
1819
|
+
subtask_prompt: "Deterministic gate: block frontend implementation unless frontend-final-design-review-pi emitted VERDICT: pass.",
|
|
1820
|
+
shell: {
|
|
1821
|
+
commands: [],
|
|
1822
|
+
verdictGate: {
|
|
1823
|
+
fromNodeId: "frontend-final-design-review-pi",
|
|
898
1824
|
accept: ["VERDICT: pass"],
|
|
899
|
-
label: "frontend design gate",
|
|
1825
|
+
label: "frontend final design gate",
|
|
900
1826
|
lineMode: "first-verdict-line",
|
|
901
1827
|
},
|
|
902
1828
|
cwd: ".",
|
|
@@ -905,13 +1831,17 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
905
1831
|
},
|
|
906
1832
|
{
|
|
907
1833
|
id: implementId,
|
|
908
|
-
depends_on: [
|
|
1834
|
+
depends_on: [
|
|
1835
|
+
"frontend-final-design-gate-shell",
|
|
1836
|
+
"frontend-plan-revision-pi",
|
|
1837
|
+
"frontend-final-design-review-pi",
|
|
1838
|
+
"frontend-plan-pi",
|
|
1839
|
+
"frontend-mock-assess-pi",
|
|
1840
|
+
],
|
|
909
1841
|
role: "implementer",
|
|
910
1842
|
executor: "pi",
|
|
911
1843
|
toolProfile: "write",
|
|
912
|
-
complexity: taskConfig
|
|
913
|
-
? "HIGH"
|
|
914
|
-
: "MED",
|
|
1844
|
+
complexity: resolveWriterComplexity(taskConfig),
|
|
915
1845
|
writePolicy: "exclusive",
|
|
916
1846
|
writeSet: implementPaths.writeSet,
|
|
917
1847
|
allowedPaths: implementPaths.allowedPaths,
|
|
@@ -919,14 +1849,26 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
919
1849
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
920
1850
|
outputContract: "Markdown summary with Changed Files, Implemented Behavior, UI States Covered, Styling / Component Notes, Verification Attempted, and Residual Risks.",
|
|
921
1851
|
subtask_prompt: [
|
|
922
|
-
"Implement the approved frontend plan with minimal focused changes.",
|
|
923
|
-
"
|
|
1852
|
+
"Implement the final approved frontend plan (from frontend-plan-revision-pi) with minimal focused changes.",
|
|
1853
|
+
"Implement only the approved Mock strategy from frontend-mock-assess-pi as carried through the approved plan. Preserve the real request path as the default, require explicit test/dev activation, and never comment out or replace the real request with inline data.",
|
|
1854
|
+
"The frontend-final-design-review-pi verdict confirmed the plan is ready. Stay within writeSet and preserve unrelated files.",
|
|
1855
|
+
"For native, browser-intercept, or request-adapter, implement contract-aligned fixtures/states and a dev/test-only activation boundary in this same writer. For not-needed, do not add Mock files or a framework and state the positive reason.",
|
|
1856
|
+
"Do not write root artifacts/** unless explicitly included in writeSet.",
|
|
1857
|
+
writerDeliveryContract(taskConfig),
|
|
924
1858
|
sourceContext,
|
|
925
|
-
|
|
1859
|
+
mockContextBlock,
|
|
1860
|
+
].filter((value) => Boolean(value)).join("\n\n"),
|
|
926
1861
|
},
|
|
1862
|
+
// Optional dedicated Mock verification exists only when trusted commands
|
|
1863
|
+
// were frozen at generation time. Behavior verification remains required.
|
|
1864
|
+
...(mockMode === "required" && hasMockVerifyCommands
|
|
1865
|
+
? [buildFrontendMockVerifyNode(frontendSources, implementId, readOnlyPaths, forbiddenPaths)]
|
|
1866
|
+
: []),
|
|
927
1867
|
{
|
|
928
1868
|
id: "frontend-static-verify-shell",
|
|
929
|
-
depends_on:
|
|
1869
|
+
depends_on: mockMode === "required" && hasMockVerifyCommands
|
|
1870
|
+
? ["frontend-mock-verify-shell"]
|
|
1871
|
+
: [implementId],
|
|
930
1872
|
role: "verifier",
|
|
931
1873
|
executor: "shell",
|
|
932
1874
|
complexity: "LOW",
|
|
@@ -934,20 +1876,10 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
934
1876
|
allowedPaths: readOnlyPaths,
|
|
935
1877
|
forbiddenPaths,
|
|
936
1878
|
outputContract: "Archived shell stdout/stderr with exit codes for deterministic static verification; no worktree writes.",
|
|
937
|
-
subtask_prompt: "Run deterministic static verification for the frontend implementation.",
|
|
1879
|
+
subtask_prompt: "Run deterministic static verification for the frontend implementation, including a production/default-real-path build with Mock activation off when Mock applies. Report only what the commands actually exercise.",
|
|
938
1880
|
shell: {
|
|
939
|
-
commands:
|
|
940
|
-
|
|
941
|
-
commands: staticVerifyCommands.commands,
|
|
942
|
-
fallbackCommands: staticFallbackCommands,
|
|
943
|
-
}),
|
|
944
|
-
verifyEvidence: buildVerifyEvidence({
|
|
945
|
-
phase: "intermediate",
|
|
946
|
-
quota: strategy.intermediateQuota ?? "full",
|
|
947
|
-
commandSource: staticVerifyCommands.commandSource,
|
|
948
|
-
commands: staticVerifyCommands.commands,
|
|
949
|
-
fallbackCommands: staticFallbackCommands,
|
|
950
|
-
}),
|
|
1881
|
+
commands: staticShellCommands,
|
|
1882
|
+
verifyEvidence: staticVerifyEvidence,
|
|
951
1883
|
cwd: ".",
|
|
952
1884
|
timeoutMs: 300000,
|
|
953
1885
|
},
|
|
@@ -962,28 +1894,29 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
962
1894
|
allowedPaths: behaviorPaths,
|
|
963
1895
|
forbiddenPaths,
|
|
964
1896
|
outputContract: "Archived shell stdout/stderr with exit codes for deterministic behavior verification; no worktree writes.",
|
|
965
|
-
subtask_prompt: "Run deterministic behavior
|
|
1897
|
+
subtask_prompt: "Run the fixed deterministic behavior entrypoints for the selected strategy. For native, browser-intercept, or request-adapter, cover the approved Mock activation and applicable success/loading/empty/error states; for not-needed, exercise applicable real or no-remote behavior. Report only what the commands actually exercise.",
|
|
966
1898
|
shell: {
|
|
967
|
-
commands:
|
|
968
|
-
|
|
969
|
-
commands: behaviorVerifyCommands.commands,
|
|
970
|
-
fallbackCommands: behaviorFallbackCommands,
|
|
971
|
-
}),
|
|
972
|
-
verifyEvidence: buildVerifyEvidence({
|
|
973
|
-
phase: "final",
|
|
974
|
-
quota: "full",
|
|
975
|
-
commandSource: behaviorVerifyCommands.commandSource,
|
|
976
|
-
commands: behaviorVerifyCommands.commands,
|
|
977
|
-
fallbackCommands: behaviorFallbackCommands,
|
|
978
|
-
finalFullRequired: true,
|
|
979
|
-
}),
|
|
1899
|
+
commands: behaviorShellCommands,
|
|
1900
|
+
verifyEvidence: behaviorVerifyEvidence,
|
|
980
1901
|
cwd: ".",
|
|
981
1902
|
timeoutMs: 300000,
|
|
982
1903
|
},
|
|
983
1904
|
},
|
|
984
1905
|
{
|
|
985
1906
|
id: "frontend-review-pi",
|
|
986
|
-
depends_on: [
|
|
1907
|
+
depends_on: [
|
|
1908
|
+
"frontend-static-verify-shell",
|
|
1909
|
+
"frontend-behavior-verify-shell",
|
|
1910
|
+
implementId,
|
|
1911
|
+
"frontend-contract-pi",
|
|
1912
|
+
"frontend-plan-pi",
|
|
1913
|
+
"frontend-plan-revision-pi",
|
|
1914
|
+
"frontend-final-design-review-pi",
|
|
1915
|
+
"frontend-mock-assess-pi",
|
|
1916
|
+
...(mockMode === "required" && hasMockVerifyCommands
|
|
1917
|
+
? ["frontend-mock-verify-shell"]
|
|
1918
|
+
: []),
|
|
1919
|
+
],
|
|
987
1920
|
role: "reviewer",
|
|
988
1921
|
executor: "pi",
|
|
989
1922
|
complexity: "HIGH",
|
|
@@ -996,8 +1929,13 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
996
1929
|
"Review the frontend implementation and verification evidence.",
|
|
997
1930
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
998
1931
|
"Any Critical or Important finding must force VERDICT: request-revision.",
|
|
1932
|
+
"Use the direct contract, original plan, revision/no-op result, and final design review to reconstruct the approved plan and design verdict; do not infer them from the implementation summary.",
|
|
1933
|
+
"Treat a commented-out real request, default-enabled Mock, production entrypoint importing test mocks, API/fixture contract drift, unauthorized Mock dependency/path, or missing behavior evidence for the selected strategy as at least Important. Mock strategies require Mock-backed evidence; not-needed requires applicable real or no-remote behavior evidence. Verify that the real request remains the default when Mock activation is absent.",
|
|
1934
|
+
"Inspect the production/default-real-path static evidence directly and require Mock activation to be off for that check.",
|
|
1935
|
+
"Distinguish Mock-backed evidence from real API integration evidence and preserve the Real Integration Gap when the backend was not exercised.",
|
|
999
1936
|
"Review implementation quality, behavior/state coverage, verification evidence, and maintainability. Read-only: do not modify files.",
|
|
1000
1937
|
sourceContext,
|
|
1938
|
+
mockContextBlock,
|
|
1001
1939
|
].join("\n\n"),
|
|
1002
1940
|
},
|
|
1003
1941
|
{
|
|
@@ -1025,7 +1963,16 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1025
1963
|
},
|
|
1026
1964
|
{
|
|
1027
1965
|
id: "frontend-closeout-pi",
|
|
1028
|
-
depends_on: [
|
|
1966
|
+
depends_on: [
|
|
1967
|
+
"frontend-review-gate-shell",
|
|
1968
|
+
"frontend-review-pi",
|
|
1969
|
+
"frontend-static-verify-shell",
|
|
1970
|
+
"frontend-behavior-verify-shell",
|
|
1971
|
+
"frontend-mock-assess-pi",
|
|
1972
|
+
...(mockMode === "required" && hasMockVerifyCommands
|
|
1973
|
+
? ["frontend-mock-verify-shell"]
|
|
1974
|
+
: []),
|
|
1975
|
+
],
|
|
1029
1976
|
role: "closeout",
|
|
1030
1977
|
executor: "pi",
|
|
1031
1978
|
complexity: "MED",
|
|
@@ -1035,11 +1982,13 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1035
1982
|
: ["**", "docs/**"],
|
|
1036
1983
|
forbiddenPaths,
|
|
1037
1984
|
skills: FRONTEND_VERIFICATION_SKILLS,
|
|
1038
|
-
outputContract: "Markdown closeout summary with Changes, Verification Evidence, Review Result, Known Risks, and Follow-up. No file writes.",
|
|
1985
|
+
outputContract: "Markdown closeout summary with Changes, Mock Decision / Strategy / Files / Verification / Production Boundary, Verification Evidence, Review Result, Frontend Status, Real Integration Status, Known Risks, and Follow-up. No file writes.",
|
|
1039
1986
|
subtask_prompt: [
|
|
1040
|
-
"Return a frontend closeout summary covering changes, verification evidence, review result, known risks, and follow-up.",
|
|
1987
|
+
"Return a frontend closeout summary covering Mock decision/strategy/files/verification/production boundary, changes, verification evidence, review result, known risks, and follow-up.",
|
|
1988
|
+
`When only Mock-backed evidence passed, state exactly Frontend status: mock-validated and Real integration: pending, summarize the Real Integration Gap, and name ${taskConfig.taskId}-real-api-integration-verify as the explicit follow-up task to create/run after backend readiness. This follow-up is not auto-created or auto-executed. Never describe Mock evidence as real API integration.`,
|
|
1041
1989
|
"Read-only: do not modify code, docs, artifacts, or .harness/dag-runs/.",
|
|
1042
1990
|
sourceContext,
|
|
1991
|
+
mockContextBlock,
|
|
1043
1992
|
].join("\n\n"),
|
|
1044
1993
|
},
|
|
1045
1994
|
],
|
|
@@ -1062,53 +2011,48 @@ function buildAnalyzeInputsNode(sources) {
|
|
|
1062
2011
|
writePolicy: "read-only",
|
|
1063
2012
|
allowedPaths: commonReadOnlyPaths(sources),
|
|
1064
2013
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1065
|
-
outputContract: "
|
|
2014
|
+
outputContract: "Pure Backend Test Analysis v1 JSON object matching docs/templates/backend-test-analysis.schema.json. No Markdown prose and no file writes.",
|
|
1066
2015
|
subtask_prompt: [
|
|
1067
|
-
"Read the task source materials and
|
|
1068
|
-
"",
|
|
1069
|
-
"
|
|
1070
|
-
"",
|
|
1071
|
-
"
|
|
1072
|
-
"
|
|
1073
|
-
"",
|
|
1074
|
-
"### 2. Data Model",
|
|
1075
|
-
"For each table/collection: fields, types, constraints, descriptions.",
|
|
1076
|
-
"",
|
|
1077
|
-
"### 3. Business Logic",
|
|
1078
|
-
"Core business rules, validation rules, calculation formulas.",
|
|
1079
|
-
"",
|
|
1080
|
-
"### 4. State Transitions",
|
|
1081
|
-
"State machines (e.g. order status: pending → paid → shipped → completed).",
|
|
1082
|
-
"",
|
|
1083
|
-
"### 5. Error Scenarios & Error Codes",
|
|
1084
|
-
"All error codes, error messages, and when they occur.",
|
|
1085
|
-
"",
|
|
1086
|
-
"### 6. External Dependencies",
|
|
1087
|
-
"Third-party services, databases, message queues. Include timeout settings if documented.",
|
|
1088
|
-
"",
|
|
1089
|
-
"### 7. Acceptance Criteria",
|
|
1090
|
-
"Extract ALL acceptance criteria from 需求.md. Number them AC-001, AC-002, etc. If not explicitly listed, derive from functional requirements.",
|
|
1091
|
-
"",
|
|
1092
|
-
"### 8. Risk Areas",
|
|
1093
|
-
"High-risk areas requiring extra test coverage.",
|
|
1094
|
-
"",
|
|
1095
|
-
"## Conditional Sections (include ONLY if mentioned in requirements):",
|
|
1096
|
-
"- Authentication & Authorization: include ONLY if requirements mention auth mechanism (JWT, OAuth2, API Key, etc.)",
|
|
1097
|
-
"- Timeout Handling: include ONLY if requirements mention timeout configuration or degradation strategy",
|
|
1098
|
-
"- Concurrency & Idempotency: include ONLY if requirements mention concurrency, idempotency rules, or locking mechanisms",
|
|
1099
|
-
"- State Transitions: include ONLY if requirements mention business state machines",
|
|
1100
|
-
"- If not mentioned in requirements, do NOT include these sections",
|
|
1101
|
-
"",
|
|
1102
|
-
"This output will be used directly by downstream nodes. Be thorough and structured.",
|
|
2016
|
+
"Read the task source materials and return exactly one JSON object matching Backend Test Analysis v1.",
|
|
2017
|
+
"Do not wrap it in explanatory prose. A single fenced json block is tolerated, but pure JSON is preferred.",
|
|
2018
|
+
"Copy taskId, requirementPath, requirementSha256, referencePaths, and requirementIds exactly from the DAG source binding shown below.",
|
|
2019
|
+
"Preserve existing AC IDs. Do not invent endpoint methods, paths, fields, errors, boundaries, or business rules; record unknowns in evidenceGaps.",
|
|
2020
|
+
"Use empty arrays for categories not documented. Never include credentials, tokens, private keys, or secret values.",
|
|
2021
|
+
"Required top-level keys: schemaVersion, sourceBinding, acceptanceCriteria, endpoints, dataModels, businessRules, stateTransitions, boundaryConstraints, externalDependencies, risks, evidenceGaps.",
|
|
1103
2022
|
"Read-only: do not modify code, docs, artifacts, or repository files.",
|
|
1104
2023
|
buildSourceContextBlock(sources),
|
|
1105
2024
|
].join("\n\n"),
|
|
1106
2025
|
};
|
|
1107
2026
|
}
|
|
2027
|
+
function buildBackendTestAnalysisContractGateNode(sources) {
|
|
2028
|
+
return {
|
|
2029
|
+
id: "backend-test-analysis-contract-shell",
|
|
2030
|
+
depends_on: ["analyze-inputs-pi"],
|
|
2031
|
+
role: "verifier",
|
|
2032
|
+
executor: "shell",
|
|
2033
|
+
complexity: "LOW",
|
|
2034
|
+
writePolicy: "read-only",
|
|
2035
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2036
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2037
|
+
outputContract: "Validated run-owned Backend Test Analysis v1 artifact pointer, schema ID, and SHA-256.",
|
|
2038
|
+
subtask_prompt: "Materialize and validate the backend-test analysis contract under the current DAG run.",
|
|
2039
|
+
shell: {
|
|
2040
|
+
commands: [],
|
|
2041
|
+
jsonArtifactGate: {
|
|
2042
|
+
fromNodeId: "analyze-inputs-pi",
|
|
2043
|
+
schemaId: "backend-test-analysis-v1",
|
|
2044
|
+
artifactName: "backend-test-analysis.json",
|
|
2045
|
+
outputDir: "contracts",
|
|
2046
|
+
},
|
|
2047
|
+
cwd: ".",
|
|
2048
|
+
timeoutMs: 60000,
|
|
2049
|
+
},
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
1108
2052
|
function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
1109
2053
|
return {
|
|
1110
2054
|
id: "generate-backend-functional-cases-pi",
|
|
1111
|
-
depends_on: ["
|
|
2055
|
+
depends_on: ["backend-test-analysis-contract-shell"],
|
|
1112
2056
|
role: "implementer",
|
|
1113
2057
|
executor: "pi",
|
|
1114
2058
|
toolProfile: "write",
|
|
@@ -1120,7 +2064,7 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
|
1120
2064
|
// 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
|
|
1121
2065
|
// 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
|
|
1122
2066
|
subtask_prompt: [
|
|
1123
|
-
"
|
|
2067
|
+
"Read the validated structured artifact pointer from backend-test-analysis-contract-shell and generate cases only from that JSON contract.", ,
|
|
1124
2068
|
"",
|
|
1125
2069
|
"## Output Steps (do in order):",
|
|
1126
2070
|
"1. First, output a brief summary: how many modules, how many cases planned per module",
|
|
@@ -1135,9 +2079,9 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
|
1135
2079
|
"## Coverage Requirements:",
|
|
1136
2080
|
"- Positive paths: happy path for each acceptance criterion",
|
|
1137
2081
|
"- Negative paths: error scenarios (invalid input, not found, state violations)",
|
|
1138
|
-
"- Boundary conditions: empty input, max length, edge values",
|
|
1139
2082
|
"",
|
|
1140
2083
|
"## Conditional Coverage (include ONLY if mentioned in upstream analysis):",
|
|
2084
|
+
"- Boundary conditions: include ONLY if upstream analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
|
|
1141
2085
|
"- State transitions: include ONLY if upstream analyze-inputs-pi mentions state machine",
|
|
1142
2086
|
"- Authentication scenarios: include ONLY if upstream analyze-inputs-pi mentions auth mechanism",
|
|
1143
2087
|
"- Timeout scenarios: include ONLY if upstream analyze-inputs-pi mentions timeout handling",
|
|
@@ -1146,279 +2090,1367 @@ function buildGenerateBackendFunctionalCasesNode(sources) {
|
|
|
1146
2090
|
"",
|
|
1147
2091
|
"## Constraints:",
|
|
1148
2092
|
"- Stay within writeSet: testcase/md/**",
|
|
1149
|
-
"- Do NOT re-read source documents — use the
|
|
2093
|
+
"- Do NOT re-read source documents or fall back to free-form analysis — use the validated structured artifact only", ,
|
|
1150
2094
|
"- Do not write root artifacts/**",
|
|
1151
2095
|
].join("\n\n"),
|
|
1152
2096
|
};
|
|
1153
2097
|
}
|
|
1154
|
-
function buildReviewBackendCasesNode(sources) {
|
|
2098
|
+
function buildReviewBackendCasesNode(sources) {
|
|
2099
|
+
return {
|
|
2100
|
+
id: "review-backend-cases-pi",
|
|
2101
|
+
depends_on: ["generate-backend-functional-cases-pi", "backend-test-analysis-contract-shell"],
|
|
2102
|
+
role: "reviewer",
|
|
2103
|
+
executor: "pi",
|
|
2104
|
+
complexity: "HIGH",
|
|
2105
|
+
writePolicy: "read-only",
|
|
2106
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2107
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2108
|
+
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision; followed by Findings and Coverage Assessment. No file writes.",
|
|
2109
|
+
subtask_prompt: [
|
|
2110
|
+
"Review the generated backend functional test cases under testcase/md/.",
|
|
2111
|
+
"",
|
|
2112
|
+
"## Mandatory First Line:",
|
|
2113
|
+
"First non-empty line must be exactly: VERDICT: pass or VERDICT: request-revision",
|
|
2114
|
+
"",
|
|
2115
|
+
"## Review Checklist:",
|
|
2116
|
+
"- ID format: every case uses BE-<MODULE>-<NNN>",
|
|
2117
|
+
"- Positive coverage: each acceptance criterion (AC-xxx) has happy-path case",
|
|
2118
|
+
"- Negative coverage: error scenarios (invalid input, not found, state violations)",
|
|
2119
|
+
"- Traceability: each AC maps to at least one case ID",
|
|
2120
|
+
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
2121
|
+
"- No duplicate IDs across files",
|
|
2122
|
+
"",
|
|
2123
|
+
"## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
|
|
2124
|
+
"- Boundary coverage: check ONLY if analyze-inputs-pi mentions value ranges, length limits, numeric bounds, or format constraints",
|
|
2125
|
+
"- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
|
|
2126
|
+
"- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
|
|
2127
|
+
"- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
|
|
2128
|
+
"- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
|
|
2129
|
+
"- If not mentioned, do NOT flag as missing",
|
|
2130
|
+
"",
|
|
2131
|
+
"## Verdict Rules:",
|
|
2132
|
+
"- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
|
|
2133
|
+
"- Any Critical fails OR Important > 2 → VERDICT: request-revision",
|
|
2134
|
+
"",
|
|
2135
|
+
"## Output After Verdict:",
|
|
2136
|
+
"1. Coverage Assessment table (AC → case IDs)",
|
|
2137
|
+
"2. Findings list (Critical/Important/Informational)",
|
|
2138
|
+
"3. Statistics (total cases, positive/negative/boundary breakdown)",
|
|
2139
|
+
"",
|
|
2140
|
+
"## Constraints:",
|
|
2141
|
+
"- Read-only: do not modify files",
|
|
2142
|
+
"- Read the validated backend-test analysis artifact pointer from upstream and use it for AC/source-binding coverage checks", ,
|
|
2143
|
+
"- Use testcase/md/ files for case review",
|
|
2144
|
+
].join("\n\n"),
|
|
2145
|
+
};
|
|
2146
|
+
}
|
|
2147
|
+
function buildReviewBackendCasesGateNode(sources) {
|
|
2148
|
+
return {
|
|
2149
|
+
id: "review-backend-cases-gate-shell",
|
|
2150
|
+
depends_on: ["review-backend-cases-pi"],
|
|
2151
|
+
role: "verifier",
|
|
2152
|
+
executor: "shell",
|
|
2153
|
+
complexity: "LOW",
|
|
2154
|
+
writePolicy: "read-only",
|
|
2155
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2156
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2157
|
+
outputContract: "Deterministic backend case review gate: exit 0 only when review-backend-cases-pi emits VERDICT: pass.",
|
|
2158
|
+
subtask_prompt: "Deterministic gate: block pytest generation unless backend case review emitted VERDICT: pass.",
|
|
2159
|
+
shell: {
|
|
2160
|
+
commands: [],
|
|
2161
|
+
verdictGate: {
|
|
2162
|
+
fromNodeId: "review-backend-cases-pi",
|
|
2163
|
+
accept: ["VERDICT: pass"],
|
|
2164
|
+
label: "backend case review",
|
|
2165
|
+
lineMode: "first-verdict-line",
|
|
2166
|
+
},
|
|
2167
|
+
cwd: ".",
|
|
2168
|
+
timeoutMs: 60000,
|
|
2169
|
+
},
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
function buildGenerateBackendPytestNode(sources) {
|
|
2173
|
+
return {
|
|
2174
|
+
id: "generate-backend-pytest-pi",
|
|
2175
|
+
depends_on: ["review-backend-cases-gate-shell"],
|
|
2176
|
+
role: "implementer",
|
|
2177
|
+
executor: "pi",
|
|
2178
|
+
toolProfile: "write",
|
|
2179
|
+
complexity: "HIGH",
|
|
2180
|
+
writePolicy: "exclusive",
|
|
2181
|
+
// test_*.py plus optional helpers/factories under testcase/ (not conftest/config)
|
|
2182
|
+
writeSet: [
|
|
2183
|
+
"testcase/**/test_*.py",
|
|
2184
|
+
"testcase/**/helpers/**",
|
|
2185
|
+
"testcase/**/factories/**",
|
|
2186
|
+
],
|
|
2187
|
+
// Union task allowedPaths with testcase/** so writeSet stays in scope even when
|
|
2188
|
+
// task.json only lists product paths (e.g. ./src/**). Writes still gated by writeSet.
|
|
2189
|
+
allowedPaths: Array.from(new Set([...commonReadOnlyPaths(sources), "testcase/**"])),
|
|
2190
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2191
|
+
// 注意:Pi 节点超时由 executor 层控制(默认 30 分钟)
|
|
2192
|
+
// 如需调整,在 harness.json 的 executors.pi 中配置 modelConfig.timeoutMs
|
|
2193
|
+
subtask_prompt: [
|
|
2194
|
+
"Convert the reviewed test cases under testcase/md/ into pytest automation code.",
|
|
2195
|
+
"",
|
|
2196
|
+
"## Output Steps (do in order):",
|
|
2197
|
+
"1. First, output a brief summary: how many files, how many test functions planned",
|
|
2198
|
+
"2. Then write each test file under testcase/",
|
|
2199
|
+
"",
|
|
2200
|
+
"## Format Rules:",
|
|
2201
|
+
"- File prefix: test_<module>.py",
|
|
2202
|
+
"- Function name: test_BE_<MODULE>_<NNN>_<description>",
|
|
2203
|
+
"- Docstring first line: BE-<MODULE>-<NNN>: <Case Title>",
|
|
2204
|
+
"- 1:1 mapping: each functional case → one pytest function",
|
|
2205
|
+
"",
|
|
2206
|
+
"## Implementation Rules:",
|
|
2207
|
+
"- Use assert statements, not unittest assertions",
|
|
2208
|
+
"- Use @pytest.mark.parametrize for boundary cases when the case defines edge values",
|
|
2209
|
+
"- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
|
|
2210
|
+
"",
|
|
2211
|
+
"## Test Data Preparation Rules (MUST follow):",
|
|
2212
|
+
"",
|
|
2213
|
+
"### When Setup is Needed",
|
|
2214
|
+
"Setup phase is REQUIRED only when test cases need pre-existing data:",
|
|
2215
|
+
"- Query/Read APIs: need data to exist before querying",
|
|
2216
|
+
"- Update/Delete APIs: need data to exist before modifying",
|
|
2217
|
+
"- State transition tests: need data in specific state",
|
|
2218
|
+
"",
|
|
2219
|
+
"Setup phase is NOT needed for:",
|
|
2220
|
+
"- Create APIs: testing the creation itself",
|
|
2221
|
+
"- Validation tests: testing input validation with invalid data",
|
|
2222
|
+
"",
|
|
2223
|
+
"### Data Setup Strategy",
|
|
2224
|
+
"When setup is needed:",
|
|
2225
|
+
"1. Prefer function-scoped fixtures for isolation; use module/session scope only when cases explicitly share immutable fixtures",
|
|
2226
|
+
"2. Prefer API-based setup from the upstream analyze-inputs-pi API list and reviewed cases",
|
|
2227
|
+
"3. If a required helper/factory is missing, create NEW files only under testcase/**/helpers/** or testcase/**/factories/**",
|
|
2228
|
+
"",
|
|
2229
|
+
"### Data Construction Priority",
|
|
2230
|
+
"1. API-first: construct data via documented APIs from analyze-inputs-pi / reviewed cases",
|
|
2231
|
+
"2. Reuse existing conftest fixtures when present (read-only)",
|
|
2232
|
+
"3. Direct DB writes are LAST RESORT and only if conftest already exposes a safe test DB fixture with rollback/isolation",
|
|
2233
|
+
"4. If neither API nor safe DB fixture exists, skip the case with an explicit gap note — do NOT invent production DB credentials or write live data",
|
|
2234
|
+
"",
|
|
2235
|
+
"### API Data Construction",
|
|
2236
|
+
"- Prefer the analyze-inputs-pi API Endpoints section and reviewed cases for method/path/fields",
|
|
2237
|
+
"- Chain API calls only when cases document multi-step preconditions",
|
|
2238
|
+
"- Store created resource IDs in fixtures for reuse",
|
|
2239
|
+
"- Do NOT broadly search host route/controller trees for secrets, .env, private keys, or production configs",
|
|
2240
|
+
"- Read host API definitions only when needed to resolve a field name already referenced by reviewed cases; stay out of credential/config paths",
|
|
2241
|
+
"",
|
|
2242
|
+
"### Database Data Construction (restricted)",
|
|
2243
|
+
"- Allowed only via existing conftest test-DB fixtures with transaction rollback or equivalent isolation",
|
|
2244
|
+
"- Never hardcode connection strings, passwords, tokens, or cloud credentials",
|
|
2245
|
+
"- Never target production/shared non-test databases",
|
|
2246
|
+
"- If isolation is unclear, report the gap instead of writing DB rows",
|
|
2247
|
+
"",
|
|
2248
|
+
"## Assertion Rules (MUST follow):",
|
|
2249
|
+
"",
|
|
2250
|
+
"### Positive Path",
|
|
2251
|
+
"MUST assert ALL of the following:",
|
|
2252
|
+
"1. HTTP status code: as defined in API spec (e.g. 200, 201)",
|
|
2253
|
+
"2. Response structure: key fields exist in response body",
|
|
2254
|
+
"3. Specific values: each field equals expected value from test case",
|
|
2255
|
+
"4. Data type: each field is correct type",
|
|
2256
|
+
"",
|
|
2257
|
+
"### Negative Path",
|
|
2258
|
+
"MUST assert ALL of the following:",
|
|
2259
|
+
"1. HTTP status code: as defined in API spec (e.g. 400, 404, 500)",
|
|
2260
|
+
"2. Error code field: field name from API spec (e.g. code, error_code, errcode, ret)",
|
|
2261
|
+
"3. Error message field: field name from API spec (e.g. message, msg, errmsg, error)",
|
|
2262
|
+
"",
|
|
2263
|
+
"### Field Name Resolution",
|
|
2264
|
+
"Field names MUST come from the upstream analyze-inputs-pi output (API Endpoints section) or reviewed cases, NOT guessed. For example:",
|
|
2265
|
+
"- If API spec defines {\"ret\": 0, \"msg\": \"success\"}, assert response.json()['ret'] and response.json()['msg']",
|
|
2266
|
+
"- If API spec defines {\"code\": 4001, \"message\": \"error\"}, assert response.json()['code'] and response.json()['message']",
|
|
2267
|
+
"",
|
|
2268
|
+
"## Conditional Implementation (include ONLY if test cases exist):",
|
|
2269
|
+
"- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
|
|
2270
|
+
"- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
|
|
2271
|
+
"- Boundary tests: implement ONLY when cases define value ranges, length limits, or format constraints",
|
|
2272
|
+
"- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
|
|
2273
|
+
"- If no such cases exist, do NOT add these tests",
|
|
2274
|
+
"",
|
|
2275
|
+
"## Constraints:",
|
|
2276
|
+
"- Only create NEW files under writeSet: testcase/**/test_*.py, testcase/**/helpers/**, testcase/**/factories/**",
|
|
2277
|
+
"- Do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml, setup.cfg, __init__.py)",
|
|
2278
|
+
"- If a test filename exists, add suffix: test_order.py → test_order_01.py",
|
|
2279
|
+
"- Do NOT re-read source documents — use reviewed cases under testcase/md/ and upstream analyze-inputs-pi output only",
|
|
2280
|
+
"- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
|
|
2281
|
+
].join("\n\n"),
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
function buildExecuteBackendPytestNode(sources) {
|
|
2285
|
+
// Keep the target worktree read-only: JUnit is runner-owned evidence under
|
|
2286
|
+
// the current DAG run and moves with active → completed/paused lifecycle.
|
|
2287
|
+
const pytestCommand = [
|
|
2288
|
+
'test -n "${HARNESS_DAG_RUN_DIR:-}" || { echo "missing HARNESS_DAG_RUN_DIR for backend pytest report" >&2; exit 2; }',
|
|
2289
|
+
'REPORT="${HARNESS_DAG_RUN_DIR}/reports/backend-test-junit.xml"',
|
|
2290
|
+
'mkdir -p "$(dirname "${REPORT}")"',
|
|
2291
|
+
'PYTHONDONTWRITEBYTECODE=1 python -m pytest testcase/ -v -p no:cacheprovider --junitxml="${REPORT}"',
|
|
2292
|
+
'STATUS=$?',
|
|
2293
|
+
'printf "JUnit report: %s\\n" "${REPORT}"',
|
|
2294
|
+
'exit "${STATUS}"',
|
|
2295
|
+
].join("; ");
|
|
2296
|
+
return {
|
|
2297
|
+
id: "execute-backend-pytest-shell",
|
|
2298
|
+
depends_on: ["generate-backend-pytest-pi"],
|
|
2299
|
+
role: "verifier",
|
|
2300
|
+
executor: "shell",
|
|
2301
|
+
complexity: "LOW",
|
|
2302
|
+
writePolicy: "read-only",
|
|
2303
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2304
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2305
|
+
outputContract: "Archived pytest stdout/stderr with exit codes; JUnit XML is runner-owned evidence at $HARNESS_DAG_RUN_DIR/reports/backend-test-junit.xml. Must not modify worktree files, testcase sources, production code, or assertions.",
|
|
2306
|
+
subtask_prompt: "Run pytest for the backend test suite; write JUnit evidence only under the current HARNESS_DAG_RUN_DIR/reports/.",
|
|
2307
|
+
shell: {
|
|
2308
|
+
commands: [pytestCommand],
|
|
2309
|
+
verifyEvidence: buildVerifyEvidence({
|
|
2310
|
+
phase: "final",
|
|
2311
|
+
quota: "full",
|
|
2312
|
+
commandSource: "inline",
|
|
2313
|
+
fallbackCommands: [pytestCommand],
|
|
2314
|
+
finalFullRequired: true,
|
|
2315
|
+
}),
|
|
2316
|
+
cwd: ".",
|
|
2317
|
+
timeoutMs: 300000,
|
|
2318
|
+
},
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
function buildTestRetrospectNode(sources) {
|
|
2322
|
+
return {
|
|
2323
|
+
id: "test-retrospect-pi",
|
|
2324
|
+
depends_on: ["execute-backend-pytest-shell"],
|
|
2325
|
+
role: "closeout",
|
|
2326
|
+
executor: "pi",
|
|
2327
|
+
toolProfile: "write",
|
|
2328
|
+
complexity: "MED",
|
|
2329
|
+
writePolicy: "exclusive",
|
|
2330
|
+
writeSet: ["docs/test-reports/**"],
|
|
2331
|
+
allowedPaths: ["docs/test-reports/**"],
|
|
2332
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2333
|
+
subtask_prompt: [
|
|
2334
|
+
"Read upstream outputs (review report + pytest results) and generate a test retrospective report.",
|
|
2335
|
+
"",
|
|
2336
|
+
"## Output Steps (do in order):",
|
|
2337
|
+
"1. First, output the maturity rating on the first line: Rating: A/B/C/D",
|
|
2338
|
+
"2. Then write the full report under docs/test-reports/",
|
|
2339
|
+
"",
|
|
2340
|
+
"## Report Structure:",
|
|
2341
|
+
"1. Maturity Rating with rationale",
|
|
2342
|
+
"2. Test Coverage Summary (total cases, pass rate, failed case analysis)",
|
|
2343
|
+
"3. Review Findings and resolution status",
|
|
2344
|
+
"4. Failed Test Analysis (if any)",
|
|
2345
|
+
"5. Recommendations for improvement",
|
|
2346
|
+
"",
|
|
2347
|
+
"## Rating Criteria:",
|
|
2348
|
+
"- A: 100% acceptance criteria covered + 100% pytest pass + no Critical findings",
|
|
2349
|
+
"- B: ≥80% coverage + ≥90% pass + Low findings only",
|
|
2350
|
+
"- C: ≥60% coverage + ≥70% pass + no Critical findings",
|
|
2351
|
+
"- D: below C thresholds",
|
|
2352
|
+
"",
|
|
2353
|
+
"## Constraints:",
|
|
2354
|
+
"- Stay within writeSet: docs/test-reports/**",
|
|
2355
|
+
"- Do NOT re-read source documents — use upstream outputs only",
|
|
2356
|
+
"- Do not write root artifacts/**",
|
|
2357
|
+
].join("\n\n"),
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
const BACKEND_TEST_DEFAULTS = {
|
|
2361
|
+
...HYBRID_DEFAULTS,
|
|
2362
|
+
writePolicy: "read-only",
|
|
2363
|
+
};
|
|
2364
|
+
const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
2365
|
+
planner: ["loop-agent"],
|
|
2366
|
+
scout: [],
|
|
2367
|
+
implementer: ["test-driven-development", "verification-before-completion"],
|
|
2368
|
+
reviewer: ["requesting-code-review", "code-review-core"],
|
|
2369
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
2370
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
2371
|
+
};
|
|
2372
|
+
function buildBackendTestHybridDag(sources) {
|
|
2373
|
+
const { taskConfig } = sources;
|
|
2374
|
+
const sourceContext = buildSourceContextBlock(sources);
|
|
2375
|
+
const readOnlyPaths = commonReadOnlyPaths(sources);
|
|
2376
|
+
const forbiddenPaths = commonForbiddenPaths(sources);
|
|
2377
|
+
const globalConstraints = [
|
|
2378
|
+
...taskConfig.hardConstraints,
|
|
2379
|
+
...(sources.constraintMarkdown
|
|
2380
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2381
|
+
: []),
|
|
2382
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2383
|
+
"backend-test-dag nodes must maintain traceability from requirements to functional cases to pytest automation.",
|
|
2384
|
+
"Functional test case IDs must use BE-<MODULE>-<NNN> format.",
|
|
2385
|
+
"pytest execution must keep the target worktree read-only and write machine-readable results only under the current HARNESS_DAG_RUN_DIR/reports/** (e.g. JUnit XML).",
|
|
2386
|
+
"pytest automation scripts must use test_ filename prefix for pytest discovery.",
|
|
2387
|
+
"generate-backend-pytest-pi may create only new files under testcase/**/test_*.py, testcase/**/helpers/**, and testcase/**/factories/**; modifying conftest.py, pytest.ini, pyproject.toml, or production code is forbidden.",
|
|
2388
|
+
"review-backend-cases-gate-shell must block pytest generation unless the review verdict is exactly VERDICT: pass.",
|
|
2389
|
+
"If a target test filename already exists under testcase/, add a numeric suffix (_01, _02, ...); never overwrite or append to existing files.",
|
|
2390
|
+
"execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
|
|
2391
|
+
];
|
|
2392
|
+
const spec = {
|
|
2393
|
+
version: 3,
|
|
2394
|
+
title: `Backend test DAG: ${taskConfig.title}`,
|
|
2395
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
2396
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
2397
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
2398
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
2399
|
+
globalConstraints,
|
|
2400
|
+
// No convergence loop: review gate is fail-closed. request-revision stops
|
|
2401
|
+
// the DAG; regenerate after fixing cases. Controller still keys off
|
|
2402
|
+
// hard-verify-shell, which this template does not include.
|
|
2403
|
+
defaults: {
|
|
2404
|
+
...BACKEND_TEST_DEFAULTS,
|
|
2405
|
+
contextProfile: taskConfig.contextProfile,
|
|
2406
|
+
},
|
|
2407
|
+
skillsByRole: BACKEND_TEST_SKILLS_BY_ROLE,
|
|
2408
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
2409
|
+
tasks: [
|
|
2410
|
+
buildAnalyzeInputsNode(sources),
|
|
2411
|
+
buildBackendTestAnalysisContractGateNode(sources),
|
|
2412
|
+
buildGenerateBackendFunctionalCasesNode(sources),
|
|
2413
|
+
buildReviewBackendCasesNode(sources),
|
|
2414
|
+
buildReviewBackendCasesGateNode(sources),
|
|
2415
|
+
buildGenerateBackendPytestNode(sources),
|
|
2416
|
+
buildExecuteBackendPytestNode(sources),
|
|
2417
|
+
buildTestRetrospectNode(sources),
|
|
2418
|
+
],
|
|
2419
|
+
};
|
|
2420
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
2421
|
+
parseDagSpec(spec);
|
|
2422
|
+
assertValidDagSpec(spec);
|
|
2423
|
+
return spec;
|
|
2424
|
+
}
|
|
2425
|
+
// ---------------------------------------------------------------------------
|
|
2426
|
+
// Frontend browser-test RAG DAG template
|
|
2427
|
+
// ---------------------------------------------------------------------------
|
|
2428
|
+
function buildFrontendTestHybridDag(sources) {
|
|
2429
|
+
const config = sources.taskConfig.frontendTest ?? { maxCasesPerBatch: 20 };
|
|
2430
|
+
const hasFrontendTestWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "testcase/frontend/**" || pattern === "testcase/**" || pattern === "**");
|
|
2431
|
+
const hasReportWriteScope = sources.taskConfig.allowedPaths.some((pattern) => pattern === "docs/test-reports/**" || pattern === "docs/**" || pattern === "**");
|
|
2432
|
+
if (!hasFrontendTestWriteScope || !hasReportWriteScope) {
|
|
2433
|
+
throw new Error('frontend-test requires task.json allowedPaths to include both "testcase/frontend/**" and "docs/test-reports/**" (or explicit containing globs).');
|
|
2434
|
+
}
|
|
2435
|
+
const forbidden = commonForbiddenPaths(sources);
|
|
2436
|
+
const ragWriteSet = ["testcase/frontend/rag/**"];
|
|
2437
|
+
const casesWriteSet = ["testcase/frontend/cases/**"];
|
|
2438
|
+
const evidenceRoot = "testcase/frontend/evidence";
|
|
2439
|
+
const manifestValidation = [
|
|
2440
|
+
"node -e",
|
|
2441
|
+
JSON.stringify([
|
|
2442
|
+
"const fs=require('fs'),path=require('path');",
|
|
2443
|
+
"const file='testcase/frontend/cases/manifest.json'; if(!fs.existsSync(file)) throw new Error('missing '+file);",
|
|
2444
|
+
"const manifest=JSON.parse(fs.readFileSync(file,'utf8')); if(manifest.schemaVersion!==1||!Array.isArray(manifest.cases)) throw new Error('invalid frontend case manifest');",
|
|
2445
|
+
"const dims=new Set(['core','boundary','flow','backend']);",
|
|
2446
|
+
"const seen=new Set(); const seenCasePath=new Set(); const seenEvidenceDir=new Set();",
|
|
2447
|
+
"for(const c of manifest.cases){",
|
|
2448
|
+
" if(!c||typeof c.caseId!=='string'||!/^FE-[A-Za-z0-9][A-Za-z0-9-]*$/.test(c.caseId)||seen.has(c.caseId)) throw new Error('invalid or duplicate caseId');",
|
|
2449
|
+
" seen.add(c.caseId);",
|
|
2450
|
+
" if(typeof c.dimension!=='string'||!dims.has(c.dimension)) throw new Error('invalid dimension');",
|
|
2451
|
+
" if(!Array.isArray(c.acIds)||c.acIds.length===0||c.acIds.some(a=>typeof a!=='string'||!a.trim())) throw new Error('invalid acIds');",
|
|
2452
|
+
" for(const k of ['casePath','evidenceDir']){ const v=c[k]; if(typeof v!=='string'||path.isAbsolute(v)||v.includes('..')) throw new Error('unsafe '+k); }",
|
|
2453
|
+
" if(c.casePath!=='testcase/frontend/cases/'+c.caseId+'.md') throw new Error('casePath must match caseId');",
|
|
2454
|
+
" if(!c.evidenceDir.startsWith('testcase/frontend/evidence/'+c.caseId+'/')) throw new Error('case path escapes frontend test roots');",
|
|
2455
|
+
" if(seenCasePath.has(c.casePath)) throw new Error('duplicate casePath'); seenCasePath.add(c.casePath);",
|
|
2456
|
+
" if(seenEvidenceDir.has(c.evidenceDir)) throw new Error('duplicate evidenceDir'); seenEvidenceDir.add(c.evidenceDir);",
|
|
2457
|
+
"}",
|
|
2458
|
+
"process.stdout.write(JSON.stringify({cases:manifest.cases}));",
|
|
2459
|
+
].join("")),
|
|
2460
|
+
].join(" ");
|
|
2461
|
+
const spec = {
|
|
2462
|
+
version: 3,
|
|
2463
|
+
title: `Frontend test DAG: ${sources.taskConfig.title}`,
|
|
2464
|
+
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
2465
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
2466
|
+
objective: extractObjective(sources.requirementMarkdown, sources.taskConfig.title),
|
|
2467
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
2468
|
+
globalConstraints: [
|
|
2469
|
+
...sources.taskConfig.hardConstraints,
|
|
2470
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2471
|
+
"frontend-test-dag generates Markdown cases and browser evidence only; it must not generate pytest or Playwright test source code.",
|
|
2472
|
+
"Each browser case runs serially in a fresh Pi execution boundary. Persist its evidence before starting the next case.",
|
|
2473
|
+
"Use only the declared isolated test environment. Production URLs, real credentials, and unauthorized data are blocked.",
|
|
2474
|
+
"Browser startup for generated cases must be playwright-cli open --browser=chrome --headed <base-url>.",
|
|
2475
|
+
"Token settings are post-case stop thresholds, never a hard provider token cap. Unstarted cases after a threshold are blocked: token-budget-exhausted.",
|
|
2476
|
+
],
|
|
2477
|
+
defaults: { ...HYBRID_DEFAULTS, writePolicy: "read-only", contextProfile: sources.taskConfig.contextProfile },
|
|
2478
|
+
skillsByRole: {
|
|
2479
|
+
planner: ["loop-agent"], scout: ["playwright-cli"], implementer: ["playwright-cli-case-generator", "playwright-cli", "webapp-testing"], reviewer: ["requesting-code-review"], verifier: ["playwright-cli", "webapp-testing"], closeout: ["loop-agent", "verification-before-completion"],
|
|
2480
|
+
},
|
|
2481
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
2482
|
+
tasks: [
|
|
2483
|
+
{
|
|
2484
|
+
id: "retrieve-frontend-test-context-pi", depends_on: [], role: "planner", executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive", writeSet: ragWriteSet, allowedPaths: [...commonReadOnlyPaths(sources), ...ragWriteSet], forbiddenPaths: forbidden,
|
|
2485
|
+
outputContract: "Write testcase/frontend/rag/context.md and coverage-map.md with traceable UI/API/test-environment facts.",
|
|
2486
|
+
subtask_prompt: ["Build the frontend test RAG package.", "Read task source, relevant routes/components/API or Mock facts, existing tests, and execution contract. Write only testcase/frontend/rag/context.md and coverage-map.md.", "Record AC IDs, source paths, routes, states, roles, fixture/data prerequisites, API mapping status, risks, and isolated execution contract. Do not guess unavailable facts.", buildSourceContextBlock(sources)].join("\n\n"),
|
|
2487
|
+
},
|
|
2488
|
+
{
|
|
2489
|
+
id: "generate-frontend-functional-cases-pi", depends_on: ["retrieve-frontend-test-context-pi"], role: "implementer", executor: "pi", toolProfile: "write", complexity: "HIGH", writePolicy: "exclusive", writeSet: casesWriteSet, allowedPaths: [...ragWriteSet, ...casesWriteSet], forbiddenPaths: forbidden,
|
|
2490
|
+
outputContract: "Write executable Markdown frontend cases, index.md, and manifest.json schemaVersion 1; no test source code.",
|
|
2491
|
+
subtask_prompt: ["Use skill playwright-cli-case-generator.", "Read only testcase/frontend/rag/context.md, testcase/frontend/rag/coverage-map.md, and existing testcase/frontend/cases/. Write only testcase/frontend/cases/**.", "Generate Markdown cases, index.md and manifest.json (schemaVersion 1; cases[] with caseId, casePath, dimension, acIds, evidenceDir). IDs use FE-<FEATURE>-<NNN>-<dimension>; dimensions core|boundary|flow|backend.", "Never infer API fields, constraints, SLA, credentials, or unrecorded test data. Do not create pytest or Playwright source. Every browser start command is: playwright-cli open --browser=chrome --headed <base-url>.", "Each case must be independent, declare its session/preconditions/data cleanup, UI assertions, evidence paths under testcase/frontend/evidence/<case-id>/, and mark unsafe/missing dependencies blocked."].join("\n\n"),
|
|
2492
|
+
},
|
|
2493
|
+
{
|
|
2494
|
+
id: "review-frontend-cases-pi", depends_on: ["generate-frontend-functional-cases-pi"], role: "reviewer", executor: "pi", complexity: "HIGH", writePolicy: "read-only", allowedPaths: [...ragWriteSet, ...casesWriteSet], forbiddenPaths: forbidden,
|
|
2495
|
+
outputContract: "First line VERDICT: pass or VERDICT: request-revision, followed by AC-to-case coverage and execution risk findings; no writes.", subtask_prompt: "Review only the RAG package and frontend Markdown cases. Verify traceability, independent execution, safe data/environment handling, manifest correctness, and evidence requirements. The verdict is advisory and does not block case execution.",
|
|
2496
|
+
},
|
|
2497
|
+
{
|
|
2498
|
+
id: "materialize-frontend-case-manifest-shell", depends_on: ["review-frontend-cases-pi"], role: "verifier", executor: "shell", complexity: "LOW", writePolicy: "read-only", allowedPaths: casesWriteSet, forbiddenPaths: forbidden,
|
|
2499
|
+
outputContract: "stdout is exactly JSON { cases: [...] } after deterministic frontend manifest validation.", subtask_prompt: "Validate and materialize the generated frontend case manifest.", shell: { commands: [manifestValidation], cwd: ".", timeoutMs: 120000 },
|
|
2500
|
+
},
|
|
2501
|
+
{
|
|
2502
|
+
id: "execute-frontend-cases-map", depends_on: ["materialize-frontend-case-manifest-shell"], role: "verifier", executor: "static", complexity: "LOW", writePolicy: "none", allowedPaths: [], forbiddenPaths: forbidden,
|
|
2503
|
+
outputContract: "Serial aggregate of case execution summaries, evidence paths, tokens, and token-budget blocked cases.", subtask_prompt: "Expand and execute the validated frontend case manifest serially.", static: { resultMarkdown: "Frontend case map expansion barrier." },
|
|
2504
|
+
dynamicExpansion: { type: "map_agent", workflowNodeId: "execute-frontend-cases-map", itemsFrom: "$.nodes['materialize-frontend-case-manifest-shell'].output.cases", itemName: "case", maxItems: config.maxCasesPerBatch, maxExpandedNodes: config.maxCasesPerBatch, childIdPrefix: "execute-frontend-case", tokenBudget: { maxTokensPerCase: config.maxTokensPerCase, maxTotalTokens: config.maxTotalTokens }, childTask: {
|
|
2505
|
+
executor: "pi", role: "verifier", skills: ["playwright-cli", "webapp-testing"], toolProfile: "write", complexity: "MED", writePolicy: "exclusive", allowedPaths: ["testcase/frontend/cases/{{case.caseId}}.md", "testcase/frontend/rag/context.md", "testcase/frontend/rag/coverage-map.md", `${evidenceRoot}/{{case.caseId}}/**`], forbiddenPaths: forbidden, writeSet: [`${evidenceRoot}/{{case.caseId}}/**`], outputContract: "Compact JSON <=1200 characters with case status, evidence paths, error summary, and tokens.", subtaskPromptTemplate: ["Execute exactly case {{case.caseId}} from {{case.casePath}} using playwright-cli and webapp-testing. This is a fresh Pi session; do not use /new.", "Use only the declared isolated test environment. If CLI/browser/base URL/credentials/fixture isolation is missing, record blocked rather than installing tools or guessing.", "Use playwright-cli open --browser=chrome --headed <base-url>. Persist execution.md, case-result.json, screenshots/trace/video/logs under {{case.evidenceDir}} before returning.", "A business failed or blocked case is a recorded result, not a node failure. Close the session and return only compact JSON (<=1200 chars): {caseId,status,evidencePaths,errorSummary,tokens}."].join("\n\n")
|
|
2506
|
+
} },
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
id: "review-frontend-execution-pi", depends_on: ["execute-frontend-cases-map"], role: "reviewer", executor: "pi", complexity: "HIGH", writePolicy: "read-only", allowedPaths: ["testcase/frontend/**"], forbiddenPaths: forbidden,
|
|
2510
|
+
outputContract: "Read-only AC-to-case-to-browser-evidence review, including failed, blocked and token-budget-exhausted cases.", subtask_prompt: "Review the frontend case aggregate and on-disk case/evidence artifacts. A passed case requires assertion plus screenshot or equivalent browser evidence; failed/blocked cases require reasons. Do not replace browser evidence with model conclusions.",
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
id: "frontend-test-retrospect-pi", depends_on: ["review-frontend-execution-pi"], role: "closeout", executor: "pi", toolProfile: "write", complexity: "MED", writePolicy: "exclusive", writeSet: ["docs/test-reports/**"], allowedPaths: ["testcase/frontend/**", "docs/test-reports/**"], forbiddenPaths: forbidden,
|
|
2514
|
+
outputContract: "Write frontend-test-retrospect-<date>.md with coverage, pass/fail/blocked, risks, findings, and A/B/C/D rating.", subtask_prompt: "Write the frontend test retrospective under docs/test-reports/. Summarize coverage, passed/failed/blocked cases (including token-budget-exhausted), review findings, browser anomalies, residual risks, and A/B/C/D rating. Blocked cases never count as passed.",
|
|
2515
|
+
},
|
|
2516
|
+
],
|
|
2517
|
+
};
|
|
2518
|
+
applyDefaultReadOnlyRetryPolicy(spec);
|
|
2519
|
+
parseDagSpec(spec);
|
|
2520
|
+
assertValidDagSpec(spec);
|
|
2521
|
+
return spec;
|
|
2522
|
+
}
|
|
2523
|
+
// ---------------------------------------------------------------------------
|
|
2524
|
+
// Knowledge-sync DAG template
|
|
2525
|
+
// ---------------------------------------------------------------------------
|
|
2526
|
+
const KNOWLEDGE_SYNC_DEFAULTS = {
|
|
2527
|
+
...HYBRID_DEFAULTS,
|
|
2528
|
+
writePolicy: "read-only",
|
|
2529
|
+
};
|
|
2530
|
+
const KNOWLEDGE_SYNC_SKILLS_BY_ROLE = {
|
|
2531
|
+
planner: ["loop-agent"],
|
|
2532
|
+
scout: [],
|
|
2533
|
+
implementer: ["verification-before-completion"],
|
|
2534
|
+
reviewer: ["requesting-code-review"],
|
|
2535
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
2536
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
2537
|
+
};
|
|
2538
|
+
/**
|
|
2539
|
+
* Deterministic aggregate gate: all listed review nodes must emit VERDICT: pass
|
|
2540
|
+
* (first VERDICT: line in assistantText/stdout). Uses HARNESS_DAG_RUN_DIR JSON artifacts.
|
|
2541
|
+
*/
|
|
2542
|
+
export function buildMultiPerspectiveReviewAggregateScript(fromNodeIds, label) {
|
|
2543
|
+
if (fromNodeIds.length === 0) {
|
|
2544
|
+
throw new Error("multi-perspective aggregate requires at least one review node id");
|
|
2545
|
+
}
|
|
2546
|
+
const idsLiteral = JSON.stringify([...fromNodeIds]);
|
|
2547
|
+
const labelLiteral = JSON.stringify(label);
|
|
2548
|
+
return [
|
|
2549
|
+
"node",
|
|
2550
|
+
"-e",
|
|
2551
|
+
JSON.stringify([
|
|
2552
|
+
"const fs=require('fs');",
|
|
2553
|
+
"const path=require('path');",
|
|
2554
|
+
`const ids=${idsLiteral};`,
|
|
2555
|
+
`const label=${labelLiteral};`,
|
|
2556
|
+
"const runDir=process.env.HARNESS_DAG_RUN_DIR;",
|
|
2557
|
+
"if(!runDir){ console.error(label+': missing HARNESS_DAG_RUN_DIR'); process.exit(1); }",
|
|
2558
|
+
"function normalize(line){",
|
|
2559
|
+
" const t=String(line).trim();",
|
|
2560
|
+
" const m=t.match(/^\\*{1,3}\\s*(VERDICT:[^*]+?)\\s*\\*{1,3}$/);",
|
|
2561
|
+
" return (m?m[1]:t).trim();",
|
|
2562
|
+
"}",
|
|
2563
|
+
"function firstVerdict(text){",
|
|
2564
|
+
" for (const line of String(text||'').split(/\\r?\\n/)) {",
|
|
2565
|
+
" const n=normalize(line);",
|
|
2566
|
+
" if(/^VERDICT:/.test(n)) return n;",
|
|
2567
|
+
" }",
|
|
2568
|
+
" return '';",
|
|
2569
|
+
"}",
|
|
2570
|
+
"const failures=[];",
|
|
2571
|
+
"for (const id of ids) {",
|
|
2572
|
+
" const file=path.join(runDir, id+'.json');",
|
|
2573
|
+
" if(!fs.existsSync(file)){ failures.push(id+': missing JSON '+file); continue; }",
|
|
2574
|
+
" let raw; try { raw=JSON.parse(fs.readFileSync(file,'utf8')); } catch(e){ failures.push(id+': invalid JSON'); continue; }",
|
|
2575
|
+
" const verdict=firstVerdict(raw.assistantText ?? raw.stdout ?? '');",
|
|
2576
|
+
" if(verdict!=='VERDICT: pass') failures.push(id+': '+(verdict||'missing VERDICT line'));",
|
|
2577
|
+
" else console.log(id+': VERDICT: pass');",
|
|
2578
|
+
"}",
|
|
2579
|
+
"if(failures.length){ console.error(label+' blocked:\\n'+failures.join('\\n')); process.exit(1); }",
|
|
2580
|
+
"console.log(label+': all perspectives VERDICT: pass ('+ids.length+')');",
|
|
2581
|
+
].join("")),
|
|
2582
|
+
].join(" ");
|
|
2583
|
+
}
|
|
2584
|
+
function buildMultiPerspectiveReviewNodes(input) {
|
|
2585
|
+
const reviewNodes = input.perspectives.map((p) => ({
|
|
2586
|
+
id: `${input.nodePrefix}${p.id}-pi`,
|
|
2587
|
+
depends_on: [...input.dependsOn],
|
|
2588
|
+
role: "reviewer",
|
|
2589
|
+
executor: "pi",
|
|
2590
|
+
complexity: "HIGH",
|
|
2591
|
+
writePolicy: "read-only",
|
|
2592
|
+
allowedPaths: input.allowedPaths,
|
|
2593
|
+
forbiddenPaths: commonForbiddenPaths(input.sources),
|
|
2594
|
+
outputContract: `Plain Markdown; first non-empty line is VERDICT: pass or VERDICT: request-revision. Perspective: ${p.perspective}. No file writes.`,
|
|
2595
|
+
subtask_prompt: [
|
|
2596
|
+
`You are the **${p.perspective}** reviewer in a multi-perspective review panel.`,
|
|
2597
|
+
"Other perspectives run in parallel; do not assume their conclusions. Stay in your role.",
|
|
2598
|
+
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2599
|
+
"Any Critical or Important finding in your domain must force VERDICT: request-revision.",
|
|
2600
|
+
"Structure: VERDICT line, then Findings (Critical/Important/Minor), then Checked Items, then Residual Risks.",
|
|
2601
|
+
"Cite concrete paths/ids as evidence. Read-only: do not modify files.",
|
|
2602
|
+
...input.sharedBrief,
|
|
2603
|
+
"Focus for this perspective:",
|
|
2604
|
+
...p.focus.map((line) => `- ${line}`),
|
|
2605
|
+
buildSourceContextBlock(input.sources),
|
|
2606
|
+
].join("\n\n"),
|
|
2607
|
+
}));
|
|
2608
|
+
const reviewIds = reviewNodes.map((n) => n.id);
|
|
2609
|
+
const aggregateScript = buildMultiPerspectiveReviewAggregateScript(reviewIds, input.gateLabel);
|
|
2610
|
+
const gateNode = {
|
|
2611
|
+
id: input.gateId,
|
|
2612
|
+
depends_on: reviewIds,
|
|
2613
|
+
role: "verifier",
|
|
2614
|
+
executor: "shell",
|
|
2615
|
+
complexity: "LOW",
|
|
2616
|
+
writePolicy: "read-only",
|
|
2617
|
+
allowedPaths: commonReadOnlyPaths(input.sources),
|
|
2618
|
+
forbiddenPaths: commonForbiddenPaths(input.sources),
|
|
2619
|
+
outputContract: `Deterministic multi-perspective gate: exit 0 only when every review node among ${reviewIds.join(", ")} emits VERDICT: pass.`,
|
|
2620
|
+
subtask_prompt: `Aggregate gate for ${input.gateLabel}: all perspectives must pass before downstream apply/promote.`,
|
|
2621
|
+
shell: {
|
|
2622
|
+
commands: [aggregateScript],
|
|
2623
|
+
verifyEvidence: buildVerifyEvidence({
|
|
2624
|
+
phase: "final",
|
|
2625
|
+
quota: "full",
|
|
2626
|
+
commandSource: "inline",
|
|
2627
|
+
fallbackCommands: [aggregateScript],
|
|
2628
|
+
finalFullRequired: true,
|
|
2629
|
+
}),
|
|
2630
|
+
cwd: ".",
|
|
2631
|
+
timeoutMs: 60000,
|
|
2632
|
+
},
|
|
2633
|
+
};
|
|
2634
|
+
return [...reviewNodes, gateNode];
|
|
2635
|
+
}
|
|
2636
|
+
/** Safe Feature directory id: F-… without path separators. */
|
|
2637
|
+
const KNOWLEDGE_SYNC_FEATURE_ID_RE = /^F-[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
2638
|
+
export function assertSafeKnowledgeSyncFeatureId(featureId) {
|
|
2639
|
+
const trimmed = featureId.trim();
|
|
2640
|
+
if (!KNOWLEDGE_SYNC_FEATURE_ID_RE.test(trimmed)) {
|
|
2641
|
+
throw new Error(`knowledge-sync featureId must match F-<id> (letters/digits/._- only); got ${JSON.stringify(featureId)}`);
|
|
2642
|
+
}
|
|
2643
|
+
if (trimmed.includes("..") || trimmed.includes("/") || trimmed.includes("\\")) {
|
|
2644
|
+
throw new Error(`knowledge-sync featureId must not contain path segments: ${featureId}`);
|
|
2645
|
+
}
|
|
2646
|
+
return trimmed;
|
|
2647
|
+
}
|
|
2648
|
+
/**
|
|
2649
|
+
* Resolve featureId for knowledge-sync writeSet binding (fail-closed).
|
|
2650
|
+
* Order: task.json featureId → hardConstraints key → requirement body F-YYYY-… → taskId if F-*.
|
|
2651
|
+
*/
|
|
2652
|
+
export function resolveKnowledgeSyncFeatureId(sources) {
|
|
2653
|
+
const candidates = [];
|
|
2654
|
+
const fromConfig = sources.taskConfig.featureId?.trim();
|
|
2655
|
+
if (fromConfig)
|
|
2656
|
+
candidates.push(fromConfig);
|
|
2657
|
+
for (const line of sources.taskConfig.hardConstraints ?? []) {
|
|
2658
|
+
const match = line.match(/(?:featureId|feature_id)\s*[=:]\s*(F-[A-Za-z0-9][A-Za-z0-9._-]*)/i);
|
|
2659
|
+
if (match?.[1])
|
|
2660
|
+
candidates.push(match[1]);
|
|
2661
|
+
}
|
|
2662
|
+
const fromRequirement = sources.requirementMarkdown?.match(/\b(F-\d{4}-\d{2,})\b/);
|
|
2663
|
+
if (fromRequirement?.[1])
|
|
2664
|
+
candidates.push(fromRequirement[1]);
|
|
2665
|
+
if (KNOWLEDGE_SYNC_FEATURE_ID_RE.test(sources.taskId)) {
|
|
2666
|
+
candidates.push(sources.taskId);
|
|
2667
|
+
}
|
|
2668
|
+
for (const candidate of candidates) {
|
|
2669
|
+
try {
|
|
2670
|
+
return assertSafeKnowledgeSyncFeatureId(candidate);
|
|
2671
|
+
}
|
|
2672
|
+
catch {
|
|
2673
|
+
// try next candidate
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
throw new Error([
|
|
2677
|
+
"knowledge-sync requires an explicit featureId bound to features/<featureId>/.",
|
|
2678
|
+
'Set task.json "featureId": "F-YYYY-NNN", or hardConstraints entry featureId=F-…,',
|
|
2679
|
+
"or put F-YYYY-NNN in 需求.md, or use a taskId that is already an F-* id.",
|
|
2680
|
+
].join(" "));
|
|
2681
|
+
}
|
|
2682
|
+
function knowledgeSyncWriteSet(featureId) {
|
|
2683
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
2684
|
+
return [
|
|
2685
|
+
`features/${id}/testing/**`,
|
|
2686
|
+
`features/${id}/requirement-delta.md`,
|
|
2687
|
+
"docs/test-reports/**",
|
|
2688
|
+
];
|
|
2689
|
+
}
|
|
2690
|
+
function knowledgeSyncPendingWriteSet(featureId) {
|
|
2691
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
2692
|
+
return [`features/${id}/testing/sync/pending/**`];
|
|
2693
|
+
}
|
|
2694
|
+
function knowledgeSyncPointerWriteSet(featureId) {
|
|
2695
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
2696
|
+
return [
|
|
2697
|
+
`features/${id}/testing/runs/**`,
|
|
2698
|
+
`features/${id}/testing/sync/applied/**`,
|
|
2699
|
+
];
|
|
2700
|
+
}
|
|
2701
|
+
function knowledgeSyncDraftRelPath(featureId) {
|
|
2702
|
+
const id = assertSafeKnowledgeSyncFeatureId(featureId);
|
|
2703
|
+
return `features/${id}/testing/sync/pending/knowledge-sync-draft.json`;
|
|
2704
|
+
}
|
|
2705
|
+
function buildKnowledgeSyncCollectNode(sources, featureId) {
|
|
2706
|
+
return {
|
|
2707
|
+
id: "knowledge-sync-collect-pi",
|
|
2708
|
+
depends_on: [],
|
|
2709
|
+
role: "planner",
|
|
2710
|
+
executor: "pi",
|
|
2711
|
+
complexity: "MED",
|
|
2712
|
+
writePolicy: "read-only",
|
|
2713
|
+
allowedPaths: commonReadOnlyPaths(sources),
|
|
2714
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2715
|
+
outputContract: "Plain Markdown inventory of final-verification evidence, acceptance criteria, cases, defects, and requirement-delta candidates; no file writes.",
|
|
2716
|
+
subtask_prompt: [
|
|
2717
|
+
"Collect inputs for knowledge-sync after final verification.",
|
|
2718
|
+
`Bound featureId for this run: ${featureId} (directory features/${featureId}/).`,
|
|
2719
|
+
`Read task source (需求.md, 执行约束.md, references), features/${featureId}/ testing assets when present, and upstream final verification / pytest / retrospect evidence paths.`,
|
|
2720
|
+
"Produce a concise inventory covering: featureId/taskId/runId/headSha, AC list, reviewed cases, automation bindings, open defects, requirement-delta risk, and missing evidence.",
|
|
2721
|
+
"Do not claim final verification passed without shell evidence. Read-only: do not modify repository files.",
|
|
2722
|
+
buildSourceContextBlock(sources),
|
|
2723
|
+
].join("\n\n"),
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
function buildKnowledgeSyncDraftNode(sources, featureId) {
|
|
2727
|
+
const pending = knowledgeSyncPendingWriteSet(featureId);
|
|
2728
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
2729
|
+
return {
|
|
2730
|
+
id: "knowledge-sync-draft-pi",
|
|
2731
|
+
depends_on: ["knowledge-sync-collect-pi"],
|
|
2732
|
+
role: "implementer",
|
|
2733
|
+
executor: "pi",
|
|
2734
|
+
toolProfile: "write",
|
|
2735
|
+
complexity: "HIGH",
|
|
2736
|
+
writePolicy: "exclusive",
|
|
2737
|
+
writeSet: pending,
|
|
2738
|
+
allowedPaths: pending,
|
|
2739
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2740
|
+
subtask_prompt: [
|
|
2741
|
+
"Using knowledge-sync-collect-pi inventory and final verification evidence, write the pending knowledge-sync draft for this Feature only.",
|
|
2742
|
+
`featureId MUST be exactly ${featureId}. Write only under features/${featureId}/testing/sync/pending/.`,
|
|
2743
|
+
`Create ${draftPath} (schemaVersion 1) and features/${featureId}/testing/sync/pending/knowledge-sync-draft.md human summary.`,
|
|
2744
|
+
"Draft payload must include: acceptanceVerdict, coverageMatrix, caseIndex, automationMap, defects, requirementDelta (or null), retrospectiveRef, evidencePointers, and operations[] with op/target/risk/reason.",
|
|
2745
|
+
`Every operations[].target path must stay under features/${featureId}/ or docs/test-reports/.`,
|
|
2746
|
+
"Mark requirement/acceptance body edits as risk=high. Keep raw shell logs as path pointers only.",
|
|
2747
|
+
"If final verification is not pass, still write draft but set gates.blockIfFinalVerificationNotPass accordingly and do not invent pass results.",
|
|
2748
|
+
"Stay within writeSet. Do not write other features/**, src/**, .harness/**, or knowledge/testing/standards/**.",
|
|
2749
|
+
buildSourceContextBlock(sources),
|
|
2750
|
+
].join("\n\n"),
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function buildKnowledgeSyncValidateNode(sources, featureId) {
|
|
2754
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
2755
|
+
const validateScript = [
|
|
2756
|
+
"node",
|
|
2757
|
+
"-e",
|
|
2758
|
+
JSON.stringify([
|
|
2759
|
+
"const fs=require('fs');",
|
|
2760
|
+
"const path=require('path');",
|
|
2761
|
+
`const featureId=${JSON.stringify(featureId)};`,
|
|
2762
|
+
`const file=${JSON.stringify(draftPath)};`,
|
|
2763
|
+
"if(!fs.existsSync(file)){ console.error('knowledge-sync validate: missing draft at '+file); process.exit(1); }",
|
|
2764
|
+
"let draft; try { draft=JSON.parse(fs.readFileSync(file,'utf8')); } catch(e){ console.error('invalid JSON',file,e.message); process.exit(1); }",
|
|
2765
|
+
"let failed=false;",
|
|
2766
|
+
"if(draft.schemaVersion!==1){ console.error(file+': schemaVersion must be 1'); failed=true; }",
|
|
2767
|
+
"for (const key of ['featureId','taskId','runId','finalVerification','operations','payload','gates']) {",
|
|
2768
|
+
" if(draft[key]===undefined||draft[key]===null){ console.error(file+': missing '+key); failed=true; }",
|
|
2769
|
+
"}",
|
|
2770
|
+
"if(draft.featureId!==featureId){ console.error(file+': featureId must be '+featureId+' got '+draft.featureId); failed=true; }",
|
|
2771
|
+
"if(!Array.isArray(draft.operations)||draft.operations.length===0){ console.error(file+': operations must be non-empty array'); failed=true; }",
|
|
2772
|
+
"const prefix='features/'+featureId+'/';",
|
|
2773
|
+
"for (const op of draft.operations||[]) {",
|
|
2774
|
+
" const t=op&&op.target; if(typeof t!=='string'){ console.error('op missing target'); failed=true; continue; }",
|
|
2775
|
+
" const ok=t===prefix.slice(0,-1)||t.startsWith(prefix)||t.startsWith('docs/test-reports/');",
|
|
2776
|
+
" if(!ok){ console.error('op.target outside feature bind: '+t); failed=true; }",
|
|
2777
|
+
"}",
|
|
2778
|
+
"const highRisk=(draft.operations||[]).some((op)=>op && op.risk==='high');",
|
|
2779
|
+
"if(draft.gates && draft.gates.blockIfFinalVerificationNotPass && draft.finalVerification!=='pass' && draft.finalVerification!=='pass-with-waivers'){",
|
|
2780
|
+
" console.error(file+': finalVerification is not pass'); failed=true;",
|
|
2781
|
+
"}",
|
|
2782
|
+
"if(highRisk && draft.gates && draft.gates.requireHumanIfHighRisk){",
|
|
2783
|
+
" console.log(file+': high-risk ops present; human approval required before/while apply');",
|
|
2784
|
+
"}",
|
|
2785
|
+
"if(failed) process.exit(1);",
|
|
2786
|
+
"console.log('knowledge-sync validate ok '+file);",
|
|
2787
|
+
].join("")),
|
|
2788
|
+
].join(" ");
|
|
2789
|
+
return {
|
|
2790
|
+
id: "knowledge-sync-validate-shell",
|
|
2791
|
+
depends_on: ["knowledge-sync-draft-pi"],
|
|
2792
|
+
role: "verifier",
|
|
2793
|
+
executor: "shell",
|
|
2794
|
+
complexity: "LOW",
|
|
2795
|
+
writePolicy: "read-only",
|
|
2796
|
+
allowedPaths: [
|
|
2797
|
+
...commonReadOnlyPaths(sources),
|
|
2798
|
+
`features/${featureId}/testing/sync/pending/**`,
|
|
2799
|
+
],
|
|
2800
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2801
|
+
outputContract: `Deterministic validation of ${draftPath} (schema, featureId bind, operation targets, finalVerification gate); no worktree writes.`,
|
|
2802
|
+
subtask_prompt: `Validate only ${draftPath} for featureId=${featureId} before apply.`,
|
|
2803
|
+
shell: {
|
|
2804
|
+
commands: [validateScript],
|
|
2805
|
+
verifyEvidence: buildVerifyEvidence({
|
|
2806
|
+
phase: "final",
|
|
2807
|
+
quota: "full",
|
|
2808
|
+
commandSource: "inline",
|
|
2809
|
+
fallbackCommands: [validateScript],
|
|
2810
|
+
finalFullRequired: true,
|
|
2811
|
+
}),
|
|
2812
|
+
cwd: ".",
|
|
2813
|
+
timeoutMs: 120000,
|
|
2814
|
+
},
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
const KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES = [
|
|
2818
|
+
{
|
|
2819
|
+
id: "qa",
|
|
2820
|
+
perspective: "QA / acceptance",
|
|
2821
|
+
focus: [
|
|
2822
|
+
"acceptanceVerdict and AC coverage vs evidencePointers",
|
|
2823
|
+
"caseIndex completeness and non-invented pass results",
|
|
2824
|
+
"defects registry consistency with open issues",
|
|
2825
|
+
],
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
id: "domain",
|
|
2829
|
+
perspective: "domain / product",
|
|
2830
|
+
focus: [
|
|
2831
|
+
"requirement-delta risk and silent requirement rewrites",
|
|
2832
|
+
"operations[] targets stay under the bound featureId",
|
|
2833
|
+
"business meaning of coverage/matrix changes",
|
|
2834
|
+
],
|
|
2835
|
+
},
|
|
2836
|
+
{
|
|
2837
|
+
id: "evidence",
|
|
2838
|
+
perspective: "evidence / audit",
|
|
2839
|
+
focus: [
|
|
2840
|
+
"finalVerification authority is shell evidence, not prose",
|
|
2841
|
+
"high-risk ops and gates.requireHumanIfHighRisk",
|
|
2842
|
+
"draft schema fields and pointer-only log policy",
|
|
2843
|
+
],
|
|
2844
|
+
},
|
|
2845
|
+
];
|
|
2846
|
+
function buildKnowledgeSyncMultiReviewNodes(sources, featureId) {
|
|
2847
|
+
return buildMultiPerspectiveReviewNodes({
|
|
2848
|
+
sources,
|
|
2849
|
+
dependsOn: ["knowledge-sync-validate-shell"],
|
|
2850
|
+
nodePrefix: "knowledge-sync-review-",
|
|
2851
|
+
gateId: "knowledge-sync-multi-review-gate-shell",
|
|
2852
|
+
gateLabel: "knowledge-sync multi-perspective review",
|
|
2853
|
+
perspectives: KNOWLEDGE_SYNC_MULTI_REVIEW_PERSPECTIVES,
|
|
2854
|
+
allowedPaths: [
|
|
2855
|
+
...commonReadOnlyPaths(sources),
|
|
2856
|
+
`features/${featureId}/**`,
|
|
2857
|
+
"docs/test-reports/**",
|
|
2858
|
+
],
|
|
2859
|
+
sharedBrief: [
|
|
2860
|
+
`Bound featureId: ${featureId}. Only review draft/ops for this Feature.`,
|
|
2861
|
+
`Primary draft path: ${knowledgeSyncDraftRelPath(featureId)}.`,
|
|
2862
|
+
"Apply is blocked until all perspectives pass. Do not approve fabricated verification pass.",
|
|
2863
|
+
],
|
|
2864
|
+
});
|
|
2865
|
+
}
|
|
2866
|
+
function buildKnowledgeSyncApplyNode(sources, featureId) {
|
|
2867
|
+
const writeSet = knowledgeSyncWriteSet(featureId);
|
|
2868
|
+
const draftPath = knowledgeSyncDraftRelPath(featureId);
|
|
2869
|
+
return {
|
|
2870
|
+
id: "knowledge-sync-apply-pi",
|
|
2871
|
+
depends_on: ["knowledge-sync-multi-review-gate-shell"],
|
|
2872
|
+
role: "implementer",
|
|
2873
|
+
executor: "pi",
|
|
2874
|
+
toolProfile: "write",
|
|
2875
|
+
complexity: "HIGH",
|
|
2876
|
+
writePolicy: "exclusive",
|
|
2877
|
+
writeSet,
|
|
2878
|
+
allowedPaths: writeSet,
|
|
2879
|
+
forbiddenPaths: [
|
|
2880
|
+
...commonForbiddenPaths(sources),
|
|
2881
|
+
"src/**",
|
|
2882
|
+
"knowledge/testing/standards/**",
|
|
2883
|
+
"conftest.py",
|
|
2884
|
+
"pytest.ini",
|
|
2885
|
+
"pyproject.toml",
|
|
2886
|
+
],
|
|
2887
|
+
subtask_prompt: [
|
|
2888
|
+
"Apply the validated knowledge-sync draft into the Feature testing knowledge base (L1).",
|
|
2889
|
+
`Bound featureId: ${featureId}. Only touch features/${featureId}/ and docs/test-reports/** (writeSet is exclusive).`,
|
|
2890
|
+
`From ${draftPath}, upsert under features/${featureId}/:`,
|
|
2891
|
+
"- testing/acceptance-verdict.yaml",
|
|
2892
|
+
"- testing/coverage-matrix.yaml",
|
|
2893
|
+
"- testing/cases/** and index.yaml when payload includes caseIndex",
|
|
2894
|
+
"- testing/automation-map.yaml",
|
|
2895
|
+
"- testing/defects/registry.yaml and open.md",
|
|
2896
|
+
"- testing/closeout-testing.md and retrospectives when provided",
|
|
2897
|
+
`- features/${featureId}/requirement-delta.md only as draft/proposal unless operations mark approved product change`,
|
|
2898
|
+
"Never modify other features/**, knowledge/testing/standards/**, src/**, or pytest framework files.",
|
|
2899
|
+
"Do not invent AC pass without evidencePointers. Keep raw logs as pointers only.",
|
|
2900
|
+
"If high-risk requirement changes lack approval, write requirement-delta.md as draft and do not merge into requirement.md/acceptance.yaml.",
|
|
2901
|
+
"Stay within writeSet.",
|
|
2902
|
+
buildSourceContextBlock(sources),
|
|
2903
|
+
].join("\n\n"),
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
function buildKnowledgeSyncPointerNode(sources, featureId) {
|
|
2907
|
+
const writeSet = knowledgeSyncPointerWriteSet(featureId);
|
|
2908
|
+
return {
|
|
2909
|
+
id: "knowledge-sync-pointer-pi",
|
|
2910
|
+
depends_on: ["knowledge-sync-apply-pi"],
|
|
2911
|
+
role: "closeout",
|
|
2912
|
+
executor: "pi",
|
|
2913
|
+
toolProfile: "write",
|
|
2914
|
+
complexity: "MED",
|
|
2915
|
+
writePolicy: "exclusive",
|
|
2916
|
+
writeSet,
|
|
2917
|
+
allowedPaths: writeSet,
|
|
2918
|
+
forbiddenPaths: commonForbiddenPaths(sources),
|
|
2919
|
+
subtask_prompt: [
|
|
2920
|
+
"Write knowledge-sync audit pointers after apply.",
|
|
2921
|
+
`Update features/${featureId}/testing/runs/latest.md with runId, headSha, finalVerification, maturity, evidence paths, and applied artifact refs.`,
|
|
2922
|
+
`Write features/${featureId}/testing/sync/applied/KS-<timestamp>.json summarizing applied operations and source draft hash/path.`,
|
|
2923
|
+
"Do not rewrite acceptance-verdict or cases here unless only adding pointer fields already applied.",
|
|
2924
|
+
"Stay within writeSet. Do not touch other features. Read-only regarding src/** and .harness/**.",
|
|
2925
|
+
buildSourceContextBlock(sources),
|
|
2926
|
+
].join("\n\n"),
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
function buildKnowledgeSyncHybridDag(sources) {
|
|
2930
|
+
const { taskConfig } = sources;
|
|
2931
|
+
const featureId = resolveKnowledgeSyncFeatureId(sources);
|
|
2932
|
+
const globalConstraints = [
|
|
2933
|
+
...taskConfig.hardConstraints,
|
|
2934
|
+
...(sources.constraintMarkdown
|
|
2935
|
+
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
2936
|
+
: []),
|
|
2937
|
+
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
2938
|
+
`knowledge-sync-dag is bound to featureId=${featureId}; writes only features/${featureId}/testing/**, features/${featureId}/requirement-delta.md, and docs/test-reports/**.`,
|
|
2939
|
+
"knowledge-sync must not modify other features/**, src/**, .harness/**, knowledge/testing/standards/**, or pytest framework files.",
|
|
2940
|
+
"Apply is blocked unless knowledge-sync-validate-shell and multi-perspective review gate pass; final verification evidence remains the completion authority.",
|
|
2941
|
+
"Multi-perspective review: QA/acceptance, domain/product, and evidence/audit must each emit VERDICT: pass before apply.",
|
|
2942
|
+
"High-risk requirement/acceptance body changes require human approval via requirement-delta; do not silently rewrite requirement.md.",
|
|
2943
|
+
"Raw shell logs stay as path pointers; knowledge base stores stable facts only.",
|
|
2944
|
+
"Prefer structured YAML/Markdown L1 knowledge pack over vector-store-only writes.",
|
|
2945
|
+
];
|
|
2946
|
+
const multiReview = buildKnowledgeSyncMultiReviewNodes(sources, featureId);
|
|
2947
|
+
const spec = {
|
|
2948
|
+
version: 2,
|
|
2949
|
+
title: `Knowledge-sync DAG (${featureId}): ${taskConfig.title}`,
|
|
2950
|
+
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
2951
|
+
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
2952
|
+
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
2953
|
+
globalConstraints,
|
|
2954
|
+
defaults: {
|
|
2955
|
+
...KNOWLEDGE_SYNC_DEFAULTS,
|
|
2956
|
+
contextProfile: taskConfig.contextProfile,
|
|
2957
|
+
},
|
|
2958
|
+
skillsByRole: KNOWLEDGE_SYNC_SKILLS_BY_ROLE,
|
|
2959
|
+
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
2960
|
+
tasks: [
|
|
2961
|
+
buildKnowledgeSyncCollectNode(sources, featureId),
|
|
2962
|
+
buildKnowledgeSyncDraftNode(sources, featureId),
|
|
2963
|
+
buildKnowledgeSyncValidateNode(sources, featureId),
|
|
2964
|
+
...multiReview,
|
|
2965
|
+
buildKnowledgeSyncApplyNode(sources, featureId),
|
|
2966
|
+
buildKnowledgeSyncPointerNode(sources, featureId),
|
|
2967
|
+
],
|
|
2968
|
+
};
|
|
2969
|
+
parseDagSpec(spec);
|
|
2970
|
+
assertValidDagSpec(spec);
|
|
2971
|
+
return spec;
|
|
2972
|
+
}
|
|
2973
|
+
// ---------------------------------------------------------------------------
|
|
2974
|
+
// Knowledge-graph bootstrap DAG template (AI-assisted graph initialization)
|
|
2975
|
+
// ---------------------------------------------------------------------------
|
|
2976
|
+
const KG_BOOTSTRAP_DEFAULTS = {
|
|
2977
|
+
...HYBRID_DEFAULTS,
|
|
2978
|
+
writePolicy: "read-only",
|
|
2979
|
+
};
|
|
2980
|
+
const KG_BOOTSTRAP_SKILLS_BY_ROLE = {
|
|
2981
|
+
planner: ["loop-agent"],
|
|
2982
|
+
scout: ["codebase-scout"],
|
|
2983
|
+
implementer: ["verification-before-completion"],
|
|
2984
|
+
reviewer: ["requesting-code-review"],
|
|
2985
|
+
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
2986
|
+
closeout: ["loop-agent", "verification-before-completion"],
|
|
2987
|
+
};
|
|
2988
|
+
function buildKgBootstrapInlineNodeScript(lines) {
|
|
2989
|
+
return ["node", "-e", JSON.stringify(lines.join(""))].join(" ");
|
|
2990
|
+
}
|
|
2991
|
+
function buildKgBootstrapPreflightNode(sources) {
|
|
2992
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
2993
|
+
"const fs=require('fs');",
|
|
2994
|
+
"const path=require('path');",
|
|
2995
|
+
"const root=process.cwd();",
|
|
2996
|
+
"const scope=path.join(root,'knowledge','bootstrap','scope.yaml');",
|
|
2997
|
+
"const status=path.join(root,'knowledge','bootstrap','status.yaml');",
|
|
2998
|
+
"const staging=path.join(root,'knowledge','bootstrap','staging');",
|
|
2999
|
+
"function ensureDir(p){fs.mkdirSync(p,{recursive:true});}",
|
|
3000
|
+
"ensureDir(path.join(root,'knowledge','bootstrap','runs'));",
|
|
3001
|
+
"ensureDir(staging);",
|
|
3002
|
+
"ensureDir(path.join(root,'knowledge','graph'));",
|
|
3003
|
+
"if(!fs.existsSync(scope)){",
|
|
3004
|
+
" console.error('kg-bootstrap preflight: missing knowledge/bootstrap/scope.yaml — complete B0/B1 skeleton first');",
|
|
3005
|
+
" process.exit(1);",
|
|
3006
|
+
"}",
|
|
3007
|
+
"if(!fs.existsSync(status)){",
|
|
3008
|
+
" console.error('kg-bootstrap preflight: missing knowledge/bootstrap/status.yaml');",
|
|
3009
|
+
" process.exit(1);",
|
|
3010
|
+
"}",
|
|
3011
|
+
"console.log('kg-bootstrap preflight ok');",
|
|
3012
|
+
]);
|
|
1155
3013
|
return {
|
|
1156
|
-
id: "
|
|
1157
|
-
depends_on: [
|
|
1158
|
-
role: "
|
|
1159
|
-
executor: "
|
|
1160
|
-
complexity: "
|
|
3014
|
+
id: "kg-bootstrap-preflight-shell",
|
|
3015
|
+
depends_on: [],
|
|
3016
|
+
role: "verifier",
|
|
3017
|
+
executor: "shell",
|
|
3018
|
+
complexity: "LOW",
|
|
1161
3019
|
writePolicy: "read-only",
|
|
1162
|
-
allowedPaths:
|
|
3020
|
+
allowedPaths: ["knowledge/bootstrap/**", "knowledge/graph/**"],
|
|
1163
3021
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1164
|
-
outputContract: "
|
|
1165
|
-
subtask_prompt:
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
"- Case structure: ID, Title, Precondition, Steps, Expected Result",
|
|
1178
|
-
"- No duplicate IDs across files",
|
|
1179
|
-
"",
|
|
1180
|
-
"## Conditional Coverage (check ONLY if mentioned in upstream analysis):",
|
|
1181
|
-
"- State transition coverage: check ONLY if analyze-inputs-pi mentions state machine",
|
|
1182
|
-
"- Authentication coverage: check ONLY if analyze-inputs-pi mentions auth mechanism",
|
|
1183
|
-
"- Timeout coverage: check ONLY if analyze-inputs-pi mentions timeout handling",
|
|
1184
|
-
"- Concurrency coverage: check ONLY if analyze-inputs-pi mentions concurrency/idempotency rules",
|
|
1185
|
-
"- If not mentioned, do NOT flag as missing",
|
|
1186
|
-
"",
|
|
1187
|
-
"## Verdict Rules:",
|
|
1188
|
-
"- All Critical checks pass + Important findings ≤ 2 → VERDICT: pass",
|
|
1189
|
-
"- Any Critical fails OR Important > 2 → VERDICT: request-revision",
|
|
1190
|
-
"",
|
|
1191
|
-
"## Output After Verdict:",
|
|
1192
|
-
"1. Coverage Assessment table (AC → case IDs)",
|
|
1193
|
-
"2. Findings list (Critical/Important/Informational)",
|
|
1194
|
-
"3. Statistics (total cases, positive/negative/boundary breakdown)",
|
|
1195
|
-
"",
|
|
1196
|
-
"## Constraints:",
|
|
1197
|
-
"- Read-only: do not modify files",
|
|
1198
|
-
"- Do NOT re-read source documents — use upstream analyze-inputs-pi output for acceptance criteria",
|
|
1199
|
-
"- Use testcase/md/ files for case review",
|
|
1200
|
-
].join("\n\n"),
|
|
3022
|
+
outputContract: "Preflight: require knowledge/bootstrap/scope.yaml and status.yaml; ensure staging/runs/graph dirs exist.",
|
|
3023
|
+
subtask_prompt: "Fail if bootstrap skeleton is missing. Do not invent business entities.",
|
|
3024
|
+
shell: {
|
|
3025
|
+
commands: [script],
|
|
3026
|
+
verifyEvidence: buildVerifyEvidence({
|
|
3027
|
+
phase: "intermediate",
|
|
3028
|
+
quota: "full",
|
|
3029
|
+
commandSource: "inline",
|
|
3030
|
+
fallbackCommands: [script],
|
|
3031
|
+
}),
|
|
3032
|
+
cwd: ".",
|
|
3033
|
+
timeoutMs: 60000,
|
|
3034
|
+
},
|
|
1201
3035
|
};
|
|
1202
3036
|
}
|
|
1203
|
-
function
|
|
3037
|
+
function buildKgBootstrapInventoryNode(sources) {
|
|
3038
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
3039
|
+
"const fs=require('fs');",
|
|
3040
|
+
"const path=require('path');",
|
|
3041
|
+
"const root=process.cwd();",
|
|
3042
|
+
"const out=path.join(root,'knowledge','bootstrap','inventory.json');",
|
|
3043
|
+
"function listDir(rel){const p=path.join(root,rel);if(!fs.existsSync(p))return[];return fs.readdirSync(p,{withFileTypes:true}).map(e=>({name:e.name,dir:e.isDirectory()}));}",
|
|
3044
|
+
"const top=listDir('.').filter(e=>e.dir).map(e=>e.name).filter(n=>!['node_modules','.git','dist','.harness'].includes(n));",
|
|
3045
|
+
"const features=listDir('features').filter(e=>e.dir&&/^F-/.test(e.name)).map(e=>e.name);",
|
|
3046
|
+
"const services=[];",
|
|
3047
|
+
"for (const base of ['services','apps','packages']) {",
|
|
3048
|
+
" for (const e of listDir(base)) if(e.dir) services.push({candidateName:e.name,paths:[base+'/'+e.name+'/**'],evidence:[base+'/'+e.name]});",
|
|
3049
|
+
"}",
|
|
3050
|
+
"const manifests=['package.json','go.mod','pyproject.toml','pom.xml','Cargo.toml'].filter(f=>fs.existsSync(path.join(root,f)));",
|
|
3051
|
+
"const inventory={schemaVersion:1,generatedAt:new Date().toISOString(),signals:{packageManifests:manifests,topLevelDirs:top,featureIds:features,likelyServices:services,docHits:[],openApiOrProto:[],existingKnowledge:{hasDomains:fs.existsSync(path.join(root,'knowledge','domains')),hasServices:fs.existsSync(path.join(root,'knowledge','services')),hasGraph:fs.existsSync(path.join(root,'knowledge','graph','edges.yaml'))}},limits:{maxFilesScanned:0,truncated:false}};",
|
|
3052
|
+
"fs.mkdirSync(path.dirname(out),{recursive:true});",
|
|
3053
|
+
"fs.writeFileSync(out,JSON.stringify(inventory,null,2)+'\\n');",
|
|
3054
|
+
"console.log('wrote '+out+' features='+features.length+' serviceCandidates='+services.length);",
|
|
3055
|
+
]);
|
|
1204
3056
|
return {
|
|
1205
|
-
id: "
|
|
1206
|
-
depends_on: ["
|
|
3057
|
+
id: "kg-bootstrap-inventory-shell",
|
|
3058
|
+
depends_on: ["kg-bootstrap-preflight-shell"],
|
|
1207
3059
|
role: "verifier",
|
|
1208
3060
|
executor: "shell",
|
|
1209
3061
|
complexity: "LOW",
|
|
1210
|
-
writePolicy: "
|
|
1211
|
-
|
|
3062
|
+
writePolicy: "exclusive",
|
|
3063
|
+
writeSet: ["knowledge/bootstrap/inventory.json", "knowledge/bootstrap/status.yaml"],
|
|
3064
|
+
allowedPaths: ["knowledge/bootstrap/**", "./**"],
|
|
1212
3065
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1213
|
-
outputContract: "Deterministic
|
|
1214
|
-
subtask_prompt: "
|
|
3066
|
+
outputContract: "Deterministic inventory.json under knowledge/bootstrap/ from directory and feature signals.",
|
|
3067
|
+
subtask_prompt: "Scan repository structure into knowledge/bootstrap/inventory.json (B2).",
|
|
1215
3068
|
shell: {
|
|
1216
|
-
commands: [],
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
},
|
|
3069
|
+
commands: [script],
|
|
3070
|
+
verifyEvidence: buildVerifyEvidence({
|
|
3071
|
+
phase: "intermediate",
|
|
3072
|
+
quota: "full",
|
|
3073
|
+
commandSource: "inline",
|
|
3074
|
+
fallbackCommands: [script],
|
|
3075
|
+
}),
|
|
1223
3076
|
cwd: ".",
|
|
1224
|
-
timeoutMs:
|
|
3077
|
+
timeoutMs: 120000,
|
|
1225
3078
|
},
|
|
1226
3079
|
};
|
|
1227
3080
|
}
|
|
1228
|
-
function
|
|
3081
|
+
function buildKgBootstrapProposeNode(sources) {
|
|
1229
3082
|
return {
|
|
1230
|
-
id: "
|
|
1231
|
-
depends_on: ["
|
|
3083
|
+
id: "kg-bootstrap-propose-pi",
|
|
3084
|
+
depends_on: ["kg-bootstrap-inventory-shell"],
|
|
1232
3085
|
role: "implementer",
|
|
1233
3086
|
executor: "pi",
|
|
1234
3087
|
toolProfile: "write",
|
|
1235
3088
|
complexity: "HIGH",
|
|
1236
3089
|
writePolicy: "exclusive",
|
|
1237
|
-
writeSet: [
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
3090
|
+
writeSet: [
|
|
3091
|
+
"knowledge/bootstrap/staging/**",
|
|
3092
|
+
"knowledge/bootstrap/runs/**",
|
|
3093
|
+
],
|
|
3094
|
+
allowedPaths: [
|
|
3095
|
+
"knowledge/bootstrap/**",
|
|
3096
|
+
"knowledge/**",
|
|
3097
|
+
"features/**",
|
|
3098
|
+
"docs/**",
|
|
3099
|
+
"README.md",
|
|
3100
|
+
"services/**",
|
|
3101
|
+
"apps/**",
|
|
3102
|
+
"packages/**",
|
|
3103
|
+
"src/**",
|
|
3104
|
+
],
|
|
3105
|
+
forbiddenPaths: [
|
|
3106
|
+
...commonForbiddenPaths(sources),
|
|
3107
|
+
"knowledge/domains/**",
|
|
3108
|
+
"knowledge/services/**",
|
|
3109
|
+
"knowledge/modules/**",
|
|
3110
|
+
"knowledge/graph/edges.yaml",
|
|
3111
|
+
"knowledge/graph/entities-index.yaml",
|
|
3112
|
+
],
|
|
1242
3113
|
subtask_prompt: [
|
|
1243
|
-
"
|
|
1244
|
-
"",
|
|
1245
|
-
"
|
|
1246
|
-
"
|
|
1247
|
-
"
|
|
1248
|
-
"",
|
|
1249
|
-
"
|
|
1250
|
-
"-
|
|
1251
|
-
"
|
|
1252
|
-
"
|
|
1253
|
-
"
|
|
1254
|
-
"",
|
|
1255
|
-
|
|
1256
|
-
"- Use assert statements, not unittest assertions",
|
|
1257
|
-
"- Assert specific values, not just 'no exception'",
|
|
1258
|
-
"- Use @pytest.mark.parametrize for boundary cases",
|
|
1259
|
-
"- Use markers: @pytest.mark.positive, @pytest.mark.negative, @pytest.mark.boundary",
|
|
1260
|
-
"",
|
|
1261
|
-
"## Conditional Implementation (include ONLY if test cases exist):",
|
|
1262
|
-
"- Authentication tests: implement ONLY if testcase/md/ contains auth-related cases",
|
|
1263
|
-
"- Timeout tests: implement ONLY if testcase/md/ contains timeout-related cases",
|
|
1264
|
-
"- Use @pytest.mark.auth for auth tests, @pytest.mark.timeout for timeout tests",
|
|
1265
|
-
"- If no such cases exist, do NOT add these tests",
|
|
1266
|
-
"",
|
|
1267
|
-
"## Constraints:",
|
|
1268
|
-
"- Only create NEW files, do NOT modify existing framework files (conftest.py, pytest.ini, pyproject.toml)",
|
|
1269
|
-
"- If filename exists, add suffix: test_order.py → test_order_01.py",
|
|
1270
|
-
"- Stay within writeSet: testcase/**/test_*.py",
|
|
1271
|
-
"- Do NOT re-read source documents — use the reviewed cases under testcase/md/ only",
|
|
1272
|
-
"- Read existing conftest.py/pytest.ini to understand conventions, but do NOT modify them",
|
|
3114
|
+
"B3: Generate AI proposals for the business knowledge graph ONLY under knowledge/bootstrap/staging/**.",
|
|
3115
|
+
"Read knowledge/bootstrap/scope.yaml and knowledge/bootstrap/inventory.json first.",
|
|
3116
|
+
"If scope.yaml has update_mode: incremental (or status.yaml update_mode: incremental), treat this as a narrow incremental update:",
|
|
3117
|
+
"- Prefer seed_features / seed_services / seed_domains and include_paths only; do not re-propose the entire monorepo.",
|
|
3118
|
+
"- Do not invent entities outside the incremental scope; note skipped inventory candidates in coverage-notes.md.",
|
|
3119
|
+
"Create domain/service/module drafts with meta.yaml + overview.md (and interfaces/dependencies/pitfalls when evidence exists).",
|
|
3120
|
+
"Write staging/edges.proposed.yaml and optional staging/features-links.proposed/<F-id>.yaml for seed features.",
|
|
3121
|
+
"Write staging/coverage-notes.md explaining which inventory candidates were covered or skipped.",
|
|
3122
|
+
"Every entity/edge MUST include evidence paths. confidence may be inferred|proposed only — NEVER asserted.",
|
|
3123
|
+
"Do not invent production API details without file evidence; use unknown/TODO instead.",
|
|
3124
|
+
"Do NOT write knowledge/domains|services formal trees or graph indexes — promotion is a later node.",
|
|
3125
|
+
"Stay within writeSet.",
|
|
3126
|
+
buildSourceContextBlock(sources),
|
|
1273
3127
|
].join("\n\n"),
|
|
1274
3128
|
};
|
|
1275
3129
|
}
|
|
1276
|
-
function
|
|
3130
|
+
function buildKgBootstrapValidateNode(sources) {
|
|
3131
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
3132
|
+
"const fs=require('fs');",
|
|
3133
|
+
"const path=require('path');",
|
|
3134
|
+
"const staging=path.join(process.cwd(),'knowledge','bootstrap','staging');",
|
|
3135
|
+
"if(!fs.existsSync(staging)){console.error('missing staging');process.exit(1);}",
|
|
3136
|
+
"function walk(dir,acc=[]){if(!fs.existsSync(dir))return acc;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p,acc);else acc.push(p);}return acc;}",
|
|
3137
|
+
"const files=walk(staging);",
|
|
3138
|
+
"if(files.length===0){console.error('staging empty');process.exit(1);}",
|
|
3139
|
+
"const ids=new Set(); let failed=false;",
|
|
3140
|
+
"for(const f of files){",
|
|
3141
|
+
" if(!f.endsWith('meta.yaml')&&!f.endsWith('edges.proposed.yaml')) continue;",
|
|
3142
|
+
" const text=fs.readFileSync(f,'utf8');",
|
|
3143
|
+
" if(/confidence:\\s*asserted/i.test(text)&&f.includes('staging')){console.error('agent must not self-assert: '+f);failed=true;}",
|
|
3144
|
+
" const m=text.match(/^id:\\s*([A-Za-z0-9_-]+)/m);",
|
|
3145
|
+
" if(m){ if(ids.has(m[1])){console.error('duplicate id '+m[1]);failed=true;} ids.add(m[1]); }",
|
|
3146
|
+
"}",
|
|
3147
|
+
"const edgesPath=path.join(staging,'edges.proposed.yaml');",
|
|
3148
|
+
"if(!fs.existsSync(edgesPath)){console.error('missing staging/edges.proposed.yaml');failed=true;}",
|
|
3149
|
+
"if(!fs.existsSync(path.join(staging,'coverage-notes.md'))){console.error('missing staging/coverage-notes.md');failed=true;}",
|
|
3150
|
+
"if(failed) process.exit(1);",
|
|
3151
|
+
"console.log('kg-bootstrap validate ok files='+files.length+' ids='+ids.size);",
|
|
3152
|
+
]);
|
|
1277
3153
|
return {
|
|
1278
|
-
id: "
|
|
1279
|
-
depends_on: ["
|
|
3154
|
+
id: "kg-bootstrap-validate-shell",
|
|
3155
|
+
depends_on: ["kg-bootstrap-propose-pi"],
|
|
1280
3156
|
role: "verifier",
|
|
1281
3157
|
executor: "shell",
|
|
1282
3158
|
complexity: "LOW",
|
|
1283
3159
|
writePolicy: "read-only",
|
|
1284
|
-
allowedPaths:
|
|
3160
|
+
allowedPaths: ["knowledge/bootstrap/staging/**"],
|
|
1285
3161
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1286
|
-
outputContract: "
|
|
1287
|
-
subtask_prompt: "
|
|
3162
|
+
outputContract: "Validate staging proposals: non-empty, edges.proposed + coverage-notes present, no self-asserted confidence, unique ids.",
|
|
3163
|
+
subtask_prompt: "B4 deterministic validation of knowledge/bootstrap/staging.",
|
|
1288
3164
|
shell: {
|
|
1289
|
-
commands: [
|
|
1290
|
-
"python -m pytest testcase/ --html=reports/backend-test-report.html -v",
|
|
1291
|
-
],
|
|
3165
|
+
commands: [script],
|
|
1292
3166
|
verifyEvidence: buildVerifyEvidence({
|
|
1293
3167
|
phase: "final",
|
|
1294
3168
|
quota: "full",
|
|
1295
3169
|
commandSource: "inline",
|
|
1296
|
-
fallbackCommands: [
|
|
1297
|
-
"python -m pytest testcase/ --html=reports/backend-test-report.html -v",
|
|
1298
|
-
],
|
|
3170
|
+
fallbackCommands: [script],
|
|
1299
3171
|
finalFullRequired: true,
|
|
1300
3172
|
}),
|
|
1301
3173
|
cwd: ".",
|
|
1302
|
-
timeoutMs:
|
|
3174
|
+
timeoutMs: 120000,
|
|
1303
3175
|
},
|
|
1304
3176
|
};
|
|
1305
3177
|
}
|
|
1306
|
-
|
|
3178
|
+
const KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES = [
|
|
3179
|
+
{
|
|
3180
|
+
id: "structure",
|
|
3181
|
+
perspective: "architecture / structure",
|
|
3182
|
+
focus: [
|
|
3183
|
+
"domain/service/module partition vs inventory candidates",
|
|
3184
|
+
"id uniqueness and naming conventions",
|
|
3185
|
+
"edges that create impossible or circular dependencies",
|
|
3186
|
+
],
|
|
3187
|
+
},
|
|
3188
|
+
{
|
|
3189
|
+
id: "evidence",
|
|
3190
|
+
perspective: "evidence / anti-hallucination",
|
|
3191
|
+
focus: [
|
|
3192
|
+
"every entity/edge has concrete evidence paths",
|
|
3193
|
+
"no confidence: asserted in staging",
|
|
3194
|
+
"no invented APIs or production details without files",
|
|
3195
|
+
],
|
|
3196
|
+
},
|
|
3197
|
+
{
|
|
3198
|
+
id: "safety",
|
|
3199
|
+
perspective: "write-boundary / promote safety",
|
|
3200
|
+
focus: [
|
|
3201
|
+
"writes stayed in knowledge/bootstrap/staging/** (and runs/**)",
|
|
3202
|
+
"no formal knowledge/domains|services trees or graph indexes written by AI",
|
|
3203
|
+
"incremental scope respected when update_mode: incremental",
|
|
3204
|
+
],
|
|
3205
|
+
},
|
|
3206
|
+
];
|
|
3207
|
+
function buildKgBootstrapMultiReviewNodes(sources) {
|
|
3208
|
+
return buildMultiPerspectiveReviewNodes({
|
|
3209
|
+
sources,
|
|
3210
|
+
dependsOn: ["kg-bootstrap-validate-shell"],
|
|
3211
|
+
nodePrefix: "kg-bootstrap-review-",
|
|
3212
|
+
gateId: "kg-bootstrap-multi-review-gate-shell",
|
|
3213
|
+
gateLabel: "kg-bootstrap multi-perspective review",
|
|
3214
|
+
perspectives: KG_BOOTSTRAP_MULTI_REVIEW_PERSPECTIVES,
|
|
3215
|
+
allowedPaths: [
|
|
3216
|
+
"knowledge/bootstrap/**",
|
|
3217
|
+
"knowledge/**",
|
|
3218
|
+
"features/**",
|
|
3219
|
+
"docs/**",
|
|
3220
|
+
],
|
|
3221
|
+
sharedBrief: [
|
|
3222
|
+
"Review staging knowledge-graph proposals before promote.",
|
|
3223
|
+
"pass means ready for promote consideration — it does NOT mark entities asserted.",
|
|
3224
|
+
"request-revision if critical entities lack evidence, ids collide, or formal trees were written outside staging.",
|
|
3225
|
+
],
|
|
3226
|
+
});
|
|
3227
|
+
}
|
|
3228
|
+
function buildKgBootstrapPromoteNode(sources) {
|
|
3229
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
3230
|
+
"const fs=require('fs');",
|
|
3231
|
+
"const path=require('path');",
|
|
3232
|
+
"const root=process.cwd();",
|
|
3233
|
+
"const staging=path.join(root,'knowledge','bootstrap','staging');",
|
|
3234
|
+
"function copyDir(src,dest){",
|
|
3235
|
+
" if(!fs.existsSync(src)) return 0;",
|
|
3236
|
+
" let n=0;",
|
|
3237
|
+
" fs.mkdirSync(dest,{recursive:true});",
|
|
3238
|
+
" for(const ent of fs.readdirSync(src,{withFileTypes:true})){",
|
|
3239
|
+
" const s=path.join(src,ent.name), d=path.join(dest,ent.name);",
|
|
3240
|
+
" if(ent.isDirectory()) n+=copyDir(s,d);",
|
|
3241
|
+
" else { if(!fs.existsSync(d)){ fs.mkdirSync(path.dirname(d),{recursive:true}); fs.copyFileSync(s,d); n++; } }",
|
|
3242
|
+
" }",
|
|
3243
|
+
" return n;",
|
|
3244
|
+
"}",
|
|
3245
|
+
"let copied=0;",
|
|
3246
|
+
"copied+=copyDir(path.join(staging,'domains'), path.join(root,'knowledge','domains'));",
|
|
3247
|
+
"copied+=copyDir(path.join(staging,'services'), path.join(root,'knowledge','services'));",
|
|
3248
|
+
"copied+=copyDir(path.join(staging,'modules'), path.join(root,'knowledge','modules'));",
|
|
3249
|
+
"const edgesSrc=path.join(staging,'edges.proposed.yaml');",
|
|
3250
|
+
"const edgesManual=path.join(root,'knowledge','graph','edges.manual.yaml');",
|
|
3251
|
+
"if(fs.existsSync(edgesSrc)){ fs.mkdirSync(path.dirname(edgesManual),{recursive:true}); if(!fs.existsSync(edgesManual)) fs.copyFileSync(edgesSrc,edgesManual); }",
|
|
3252
|
+
"const linksDir=path.join(staging,'features-links.proposed');",
|
|
3253
|
+
"if(fs.existsSync(linksDir)){",
|
|
3254
|
+
" for(const f of fs.readdirSync(linksDir)){",
|
|
3255
|
+
" if(!f.endsWith('.yaml')&&!f.endsWith('.yml')) continue;",
|
|
3256
|
+
" if(!/^F-[A-Za-z0-9][A-Za-z0-9._-]*\\.ya?ml$/.test(f)){ console.error('invalid feature link proposal filename: '+f); process.exit(1); }",
|
|
3257
|
+
" const id=f.replace(/\\.ya?ml$/,'');",
|
|
3258
|
+
" if(id.includes('..')){ console.error('invalid feature link proposal filename: '+f); process.exit(1); }",
|
|
3259
|
+
" const dest=path.join(root,'features',id,'knowledge-links.yaml');",
|
|
3260
|
+
" if(!fs.existsSync(path.join(root,'features',id))) continue;",
|
|
3261
|
+
" if(!fs.existsSync(dest)){ fs.mkdirSync(path.dirname(dest),{recursive:true}); fs.copyFileSync(path.join(linksDir,f),dest); copied++; }",
|
|
3262
|
+
" }",
|
|
3263
|
+
"}",
|
|
3264
|
+
"console.log('kg-bootstrap promote copied_new_files='+copied+' (existing asserted targets skipped)');",
|
|
3265
|
+
]);
|
|
1307
3266
|
return {
|
|
1308
|
-
id: "
|
|
1309
|
-
depends_on: ["
|
|
3267
|
+
id: "kg-bootstrap-promote-shell",
|
|
3268
|
+
depends_on: ["kg-bootstrap-multi-review-gate-shell"],
|
|
3269
|
+
role: "verifier",
|
|
3270
|
+
executor: "shell",
|
|
3271
|
+
complexity: "LOW",
|
|
3272
|
+
writePolicy: "exclusive",
|
|
3273
|
+
writeSet: [
|
|
3274
|
+
"knowledge/domains/**",
|
|
3275
|
+
"knowledge/services/**",
|
|
3276
|
+
"knowledge/modules/**",
|
|
3277
|
+
"knowledge/graph/edges.manual.yaml",
|
|
3278
|
+
"features/**/knowledge-links.yaml",
|
|
3279
|
+
],
|
|
3280
|
+
allowedPaths: [
|
|
3281
|
+
"knowledge/bootstrap/staging/**",
|
|
3282
|
+
"knowledge/domains/**",
|
|
3283
|
+
"knowledge/services/**",
|
|
3284
|
+
"knowledge/modules/**",
|
|
3285
|
+
"knowledge/graph/**",
|
|
3286
|
+
"features/**",
|
|
3287
|
+
],
|
|
3288
|
+
forbiddenPaths: [
|
|
3289
|
+
...commonForbiddenPaths(sources),
|
|
3290
|
+
"src/**",
|
|
3291
|
+
"testcase/**",
|
|
3292
|
+
],
|
|
3293
|
+
outputContract: "Promote staging → formal knowledge trees without overwriting existing files; copy edges.manual.yaml if absent.",
|
|
3294
|
+
subtask_prompt: "B5 promote: merge new files only (no overwrite of existing asserted content).",
|
|
3295
|
+
shell: {
|
|
3296
|
+
commands: [script],
|
|
3297
|
+
verifyEvidence: buildVerifyEvidence({
|
|
3298
|
+
phase: "final",
|
|
3299
|
+
quota: "full",
|
|
3300
|
+
commandSource: "inline",
|
|
3301
|
+
fallbackCommands: [script],
|
|
3302
|
+
finalFullRequired: true,
|
|
3303
|
+
}),
|
|
3304
|
+
cwd: ".",
|
|
3305
|
+
timeoutMs: 120000,
|
|
3306
|
+
},
|
|
3307
|
+
};
|
|
3308
|
+
}
|
|
3309
|
+
function buildKgBootstrapMaterializeNode(sources) {
|
|
3310
|
+
const script = buildKgBootstrapInlineNodeScript([
|
|
3311
|
+
"const fs=require('fs');",
|
|
3312
|
+
"const path=require('path');",
|
|
3313
|
+
"const root=process.cwd();",
|
|
3314
|
+
"const entities=[];",
|
|
3315
|
+
"function walk(dir,acc=[]){if(!fs.existsSync(dir))return acc;for(const ent of fs.readdirSync(dir,{withFileTypes:true})){const p=path.join(dir,ent.name);if(ent.isDirectory())walk(p,acc);else acc.push(p);}return acc;}",
|
|
3316
|
+
"for (const base of ['knowledge/domains','knowledge/services','knowledge/modules']) {",
|
|
3317
|
+
" for (const f of walk(path.join(root,base))) {",
|
|
3318
|
+
" if(!f.endsWith('meta.yaml')&&!f.endsWith('.md')) continue;",
|
|
3319
|
+
" const rel=path.relative(root,f).split(path.sep).join('/');",
|
|
3320
|
+
" const text=fs.existsSync(f)&&f.endsWith('meta.yaml')?fs.readFileSync(f,'utf8'):'';",
|
|
3321
|
+
" const id=(text.match(/^id:\\s*([A-Za-z0-9_-]+)/m)||[])[1];",
|
|
3322
|
+
" const kind=(text.match(/^kind:\\s*([A-Za-z0-9_-]+)/m)||[])[1];",
|
|
3323
|
+
" if(id) entities.push({kind:kind||'unknown',id,path:rel});",
|
|
3324
|
+
" }",
|
|
3325
|
+
"}",
|
|
3326
|
+
"const featRoot=path.join(root,'features');",
|
|
3327
|
+
"if(fs.existsSync(featRoot)){",
|
|
3328
|
+
" for(const name of fs.readdirSync(featRoot)){",
|
|
3329
|
+
" if(!/^F-/.test(name)) continue;",
|
|
3330
|
+
" entities.push({kind:'feature',id:name,path:'features/'+name+'/'} );",
|
|
3331
|
+
" const acc=path.join(featRoot,name,'acceptance.yaml');",
|
|
3332
|
+
" if(fs.existsSync(acc)) entities.push({kind:'ac-file',id:name+':acceptance',path:'features/'+name+'/acceptance.yaml',feature_id:name});",
|
|
3333
|
+
" }",
|
|
3334
|
+
"}",
|
|
3335
|
+
"const graphDir=path.join(root,'knowledge','graph');",
|
|
3336
|
+
"fs.mkdirSync(graphDir,{recursive:true});",
|
|
3337
|
+
"const index={schema_version:1,generated_at:new Date().toISOString(),entities};",
|
|
3338
|
+
"fs.writeFileSync(path.join(graphDir,'entities-index.yaml'),'# generated by kg-bootstrap-materialize\\nschema_version: 1\\ngenerated_at: '+index.generated_at+'\\nentities:\\n'+entities.map(e=>' - kind: '+e.kind+'\\n id: '+e.id+'\\n path: '+e.path+'\\n').join(''));",
|
|
3339
|
+
"const manual=path.join(graphDir,'edges.manual.yaml');",
|
|
3340
|
+
"const edgesOut=path.join(graphDir,'edges.yaml');",
|
|
3341
|
+
"if(fs.existsSync(manual)) fs.copyFileSync(manual,edgesOut);",
|
|
3342
|
+
"else fs.writeFileSync(edgesOut,'schema_version: 1\\nupdated_at: '+index.generated_at+'\\nedges: []\\n');",
|
|
3343
|
+
"console.log('materialized entities='+entities.length);",
|
|
3344
|
+
]);
|
|
3345
|
+
return {
|
|
3346
|
+
id: "kg-bootstrap-materialize-shell",
|
|
3347
|
+
depends_on: ["kg-bootstrap-promote-shell"],
|
|
1310
3348
|
role: "closeout",
|
|
1311
|
-
executor: "
|
|
1312
|
-
|
|
1313
|
-
complexity: "MED",
|
|
3349
|
+
executor: "shell",
|
|
3350
|
+
complexity: "LOW",
|
|
1314
3351
|
writePolicy: "exclusive",
|
|
1315
|
-
writeSet: [
|
|
1316
|
-
|
|
3352
|
+
writeSet: [
|
|
3353
|
+
"knowledge/graph/entities-index.yaml",
|
|
3354
|
+
"knowledge/graph/edges.yaml",
|
|
3355
|
+
],
|
|
3356
|
+
allowedPaths: ["knowledge/**", "features/**"],
|
|
1317
3357
|
forbiddenPaths: commonForbiddenPaths(sources),
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
"
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
"## Rating Criteria:",
|
|
1333
|
-
"- A: 100% acceptance criteria covered + 100% pytest pass + no Critical findings",
|
|
1334
|
-
"- B: ≥80% coverage + ≥90% pass + Low findings only",
|
|
1335
|
-
"- C: ≥60% coverage + ≥70% pass + no Critical findings",
|
|
1336
|
-
"- D: below C thresholds",
|
|
1337
|
-
"",
|
|
1338
|
-
"## Constraints:",
|
|
1339
|
-
"- Stay within writeSet: docs/test-reports/**",
|
|
1340
|
-
"- Do NOT re-read source documents — use upstream outputs only",
|
|
1341
|
-
"- Do not write root artifacts/**",
|
|
1342
|
-
].join("\n\n"),
|
|
3358
|
+
outputContract: "Write knowledge/graph/entities-index.yaml and edges.yaml from formal trees + edges.manual.yaml.",
|
|
3359
|
+
subtask_prompt: "B6 materialize graph indexes for kb-query.",
|
|
3360
|
+
shell: {
|
|
3361
|
+
commands: [script],
|
|
3362
|
+
verifyEvidence: buildVerifyEvidence({
|
|
3363
|
+
phase: "final",
|
|
3364
|
+
quota: "full",
|
|
3365
|
+
commandSource: "inline",
|
|
3366
|
+
fallbackCommands: [script],
|
|
3367
|
+
finalFullRequired: true,
|
|
3368
|
+
}),
|
|
3369
|
+
cwd: ".",
|
|
3370
|
+
timeoutMs: 120000,
|
|
3371
|
+
},
|
|
1343
3372
|
};
|
|
1344
3373
|
}
|
|
1345
|
-
|
|
1346
|
-
...HYBRID_DEFAULTS,
|
|
1347
|
-
writePolicy: "read-only",
|
|
1348
|
-
};
|
|
1349
|
-
const BACKEND_TEST_SKILLS_BY_ROLE = {
|
|
1350
|
-
planner: ["loop-agent"],
|
|
1351
|
-
scout: [],
|
|
1352
|
-
implementer: ["test-driven-development", "verification-before-completion"],
|
|
1353
|
-
reviewer: ["requesting-code-review", "code-review-core"],
|
|
1354
|
-
verifier: ["verification-before-completion", "systematic-debugging"],
|
|
1355
|
-
closeout: ["loop-agent", "verification-before-completion"],
|
|
1356
|
-
};
|
|
1357
|
-
function buildBackendTestHybridDag(sources) {
|
|
3374
|
+
function buildKnowledgeGraphBootstrapHybridDag(sources) {
|
|
1358
3375
|
const { taskConfig } = sources;
|
|
1359
|
-
const sourceContext = buildSourceContextBlock(sources);
|
|
1360
|
-
const readOnlyPaths = commonReadOnlyPaths(sources);
|
|
1361
|
-
const forbiddenPaths = commonForbiddenPaths(sources);
|
|
1362
3376
|
const globalConstraints = [
|
|
1363
3377
|
...taskConfig.hardConstraints,
|
|
1364
3378
|
...(sources.constraintMarkdown
|
|
1365
3379
|
? [`See 执行约束.md in task source (${sources.taskId})`]
|
|
1366
3380
|
: []),
|
|
1367
3381
|
...STANDARD_GLOBAL_CONSTRAINTS,
|
|
1368
|
-
"
|
|
1369
|
-
"
|
|
1370
|
-
"
|
|
1371
|
-
"
|
|
1372
|
-
"
|
|
1373
|
-
"
|
|
1374
|
-
"
|
|
1375
|
-
"execute-backend-pytest-shell must not modify test assertions or production code to make tests pass; test failures indicate potential implementation issues and must be reported honestly.",
|
|
3382
|
+
"knowledge-graph-bootstrap-dag: AI may write only knowledge/bootstrap/staging/** and runs/** until promote.",
|
|
3383
|
+
"AI must never set confidence: asserted; only human promotion marks formal knowledge.",
|
|
3384
|
+
"Do not modify src/**, testcase/**, or Feature testing verdict/cases via bootstrap.",
|
|
3385
|
+
"Promote must not overwrite existing formal files (merge-new-only).",
|
|
3386
|
+
"Require knowledge/bootstrap/scope.yaml before propose (B0/B1 skeleton).",
|
|
3387
|
+
"Graph indexes are written only by materialize-shell, not by propose-pi.",
|
|
3388
|
+
"Multi-perspective review (structure, evidence, safety) must all VERDICT: pass before promote.",
|
|
1376
3389
|
];
|
|
3390
|
+
const multiReview = buildKgBootstrapMultiReviewNodes(sources);
|
|
1377
3391
|
const spec = {
|
|
1378
|
-
version:
|
|
1379
|
-
title: `
|
|
1380
|
-
runtimeContract: GENERATED_DAG_RUNTIME_CONTRACT,
|
|
3392
|
+
version: 2,
|
|
3393
|
+
title: `Knowledge-graph bootstrap DAG: ${taskConfig.title}`,
|
|
1381
3394
|
outputLanguage: sources.outputLanguage ?? DEFAULT_DAG_OUTPUT_LANGUAGE,
|
|
1382
3395
|
objective: extractObjective(sources.requirementMarkdown, taskConfig.title),
|
|
1383
3396
|
successCriteria: extractSuccessCriteria(sources.requirementMarkdown, sources.taskId),
|
|
1384
3397
|
globalConstraints,
|
|
1385
3398
|
defaults: {
|
|
1386
|
-
...
|
|
3399
|
+
...KG_BOOTSTRAP_DEFAULTS,
|
|
1387
3400
|
contextProfile: taskConfig.contextProfile,
|
|
1388
3401
|
},
|
|
1389
|
-
skillsByRole:
|
|
3402
|
+
skillsByRole: KG_BOOTSTRAP_SKILLS_BY_ROLE,
|
|
1390
3403
|
executorModels: sources.executorModelMatrix ?? DEFAULT_DAG_EXECUTOR_MODELS,
|
|
1391
3404
|
tasks: [
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
3405
|
+
buildKgBootstrapPreflightNode(sources),
|
|
3406
|
+
buildKgBootstrapInventoryNode(sources),
|
|
3407
|
+
buildKgBootstrapProposeNode(sources),
|
|
3408
|
+
buildKgBootstrapValidateNode(sources),
|
|
3409
|
+
...multiReview,
|
|
3410
|
+
buildKgBootstrapPromoteNode(sources),
|
|
3411
|
+
buildKgBootstrapMaterializeNode(sources),
|
|
1399
3412
|
],
|
|
1400
3413
|
};
|
|
1401
|
-
|
|
3414
|
+
parseDagSpec(spec);
|
|
3415
|
+
assertValidDagSpec(spec);
|
|
3416
|
+
return spec;
|
|
3417
|
+
}
|
|
3418
|
+
function buildHybridDagForTemplate(sources, template) {
|
|
3419
|
+
let spec;
|
|
3420
|
+
if (template === "frontend-implementation") {
|
|
3421
|
+
spec = buildFrontendHybridDagFromTask(sources);
|
|
3422
|
+
}
|
|
3423
|
+
else if (template === "frontend-test-dag")
|
|
3424
|
+
spec = buildFrontendTestHybridDag(sources);
|
|
3425
|
+
else if (template === "backend-test-dag")
|
|
3426
|
+
spec = buildBackendTestHybridDag(sources);
|
|
3427
|
+
else if (template === "knowledge-sync-dag")
|
|
3428
|
+
spec = buildKnowledgeSyncHybridDag(sources);
|
|
3429
|
+
else if (template === "knowledge-graph-bootstrap-dag")
|
|
3430
|
+
spec = buildKnowledgeGraphBootstrapHybridDag(sources);
|
|
3431
|
+
else {
|
|
3432
|
+
const standard = buildStandardHybridDagFromTask(sources);
|
|
3433
|
+
if (template === "standard-dag")
|
|
3434
|
+
spec = standard;
|
|
3435
|
+
else if (template === "review-gated-dag")
|
|
3436
|
+
spec = buildReviewGatedHybridDag(standard, sources);
|
|
3437
|
+
else
|
|
3438
|
+
spec = buildSupervisedHybridDag(standard, sources);
|
|
3439
|
+
}
|
|
3440
|
+
spec.sourceBinding = buildDagSourceBinding(sources);
|
|
1402
3441
|
parseDagSpec(spec);
|
|
1403
3442
|
assertValidDagSpec(spec);
|
|
1404
3443
|
return spec;
|
|
1405
3444
|
}
|
|
1406
3445
|
export function buildHybridDagFromTask(sources, options = {}) {
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
options.template
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
const standard = buildStandardHybridDagFromTask(sources);
|
|
1416
|
-
const template = options.template ?? "standard-dag";
|
|
1417
|
-
if (template === "standard-dag")
|
|
1418
|
-
return standard;
|
|
1419
|
-
if (template === "review-gated-dag")
|
|
1420
|
-
return buildReviewGatedHybridDag(standard, sources);
|
|
1421
|
-
return buildSupervisedHybridDag(standard, sources);
|
|
3446
|
+
const selection = resolveTaskDagTemplateSelection({
|
|
3447
|
+
taskKind: sources.taskConfig.taskKind,
|
|
3448
|
+
title: sources.taskConfig.title,
|
|
3449
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
3450
|
+
allowedPaths: sources.taskConfig.allowedPaths,
|
|
3451
|
+
requestedTemplate: options.template,
|
|
3452
|
+
});
|
|
3453
|
+
return buildHybridDagForTemplate(sources, selection.template);
|
|
1422
3454
|
}
|
|
1423
3455
|
function cloneTask(task, patch = {}) {
|
|
1424
3456
|
return { ...task, ...patch };
|
|
@@ -1806,8 +3838,19 @@ export function defaultHybridDagOutputPath(taskId) {
|
|
|
1806
3838
|
return path.join(os.tmpdir(), `${taskId}-hybrid-dag.json`);
|
|
1807
3839
|
}
|
|
1808
3840
|
export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
1809
|
-
const
|
|
1810
|
-
|
|
3841
|
+
const templateSelection = resolveTaskDagTemplateSelection({
|
|
3842
|
+
taskKind: sources.taskConfig.taskKind,
|
|
3843
|
+
title: sources.taskConfig.title,
|
|
3844
|
+
requirementMarkdown: sources.requirementMarkdown,
|
|
3845
|
+
allowedPaths: sources.taskConfig.allowedPaths,
|
|
3846
|
+
requestedTemplate: options.template,
|
|
3847
|
+
});
|
|
3848
|
+
const template = templateSelection.template;
|
|
3849
|
+
assertTaskAllowedPathsPreflight(sources.taskConfig);
|
|
3850
|
+
const preparedSources = template === "frontend-implementation"
|
|
3851
|
+
? await prepareFrontendMockSources(sources)
|
|
3852
|
+
: sources;
|
|
3853
|
+
const spec = buildHybridDagForTemplate(preparedSources, template);
|
|
1811
3854
|
await writeFile(outputPath, `${JSON.stringify(spec, null, 2)}\n`, "utf-8");
|
|
1812
3855
|
return {
|
|
1813
3856
|
taskId: sources.taskId,
|
|
@@ -1815,6 +3858,7 @@ export async function writeHybridDagDraft(sources, outputPath, options = {}) {
|
|
|
1815
3858
|
taskCount: spec.tasks.length,
|
|
1816
3859
|
nodeIds: spec.tasks.map((task) => task.id),
|
|
1817
3860
|
template,
|
|
3861
|
+
templateSelection,
|
|
1818
3862
|
};
|
|
1819
3863
|
}
|
|
1820
3864
|
export async function initHybridDagFromTask(repoRoot, taskId, options = {}) {
|