@tea-agent/loop-agent 0.1.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 +121 -0
- package/CHANGELOG.md +37 -0
- package/README.md +144 -0
- package/bin/loop-agent.js +21 -0
- package/dist/adapters/aimax.js +91 -0
- package/dist/adapters/context.js +32 -0
- package/dist/adapters/index.js +28 -0
- package/dist/adapters/loop-agent.js +98 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/catalog.js +259 -0
- package/dist/cli/help.js +55 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/program.js +505 -0
- package/dist/cli.js +12 -0
- package/dist/commands/closeout.js +13 -0
- package/dist/commands/coverage-audit.js +14 -0
- package/dist/commands/cursor-prompt.js +222 -0
- package/dist/commands/cursor-worker.js +43 -0
- package/dist/commands/dag-approve.js +102 -0
- package/dist/commands/dag-final-verification.js +76 -0
- package/dist/commands/dag-init-hybrid.js +56 -0
- package/dist/commands/dag-reconcile-tasks.js +51 -0
- package/dist/commands/dag-reject.js +91 -0
- package/dist/commands/dag-report.js +177 -0
- package/dist/commands/dag-resume.js +34 -0
- package/dist/commands/dag-run-task.js +470 -0
- package/dist/commands/dag-validate.js +186 -0
- package/dist/commands/dag-workflow-compile.js +91 -0
- package/dist/commands/dag-workflow-plan.js +130 -0
- package/dist/commands/dag-workflow-validate.js +66 -0
- package/dist/commands/delegate.js +132 -0
- package/dist/commands/docs-archive.js +5 -0
- package/dist/commands/docs-audit.js +5 -0
- package/dist/commands/doctor.js +50 -0
- package/dist/commands/goal.js +92 -0
- package/dist/commands/handoff-check.js +5 -0
- package/dist/commands/harvest.js +44 -0
- package/dist/commands/inspect.js +11 -0
- package/dist/commands/instructions.js +195 -0
- package/dist/commands/knowledge.js +64 -0
- package/dist/commands/loop-benchmark.js +72 -0
- package/dist/commands/loop.js +241 -0
- package/dist/commands/new-task.js +5 -0
- package/dist/commands/pi-prompt.js +181 -0
- package/dist/commands/pi-reuse-benchmark.js +153 -0
- package/dist/commands/plan-list.js +5 -0
- package/dist/commands/promote-run.js +29 -0
- package/dist/commands/reference-index.js +16 -0
- package/dist/commands/run-dag.js +184 -0
- package/dist/commands/spine.js +38 -0
- package/dist/commands/stats.js +84 -0
- package/dist/commands/status.js +56 -0
- package/dist/commands/study-init.js +192 -0
- package/dist/commands/workflow.js +259 -0
- package/dist/commands/worktree-create.js +31 -0
- package/dist/commands/worktree-list.js +5 -0
- package/dist/commands/worktree-remove.js +26 -0
- package/dist/cursor-worker-entry.js +8 -0
- package/dist/executors/config-core.js +55 -0
- package/dist/executors/config.js +2 -0
- package/dist/executors/cursor-artifacts.js +33 -0
- package/dist/executors/cursor-execution-log.js +81 -0
- package/dist/executors/cursor-executor-artifacts.js +135 -0
- package/dist/executors/cursor-executor.js +468 -0
- package/dist/executors/cursor-run.js +115 -0
- package/dist/executors/cursor-tool.js +94 -0
- package/dist/executors/cursor-worker-client.js +213 -0
- package/dist/executors/cursor-worker-protocol.js +18 -0
- package/dist/executors/cursor-worker-server.js +54 -0
- package/dist/executors/cursor-worker.js +3 -0
- package/dist/executors/cursor.js +6 -0
- package/dist/executors/dag-cursor-executor.js +88 -0
- package/dist/executors/dag-pi-executor.js +322 -0
- package/dist/executors/dag-static-executor.js +45 -0
- package/dist/executors/dag.js +4 -0
- package/dist/executors/index.js +8 -0
- package/dist/executors/model-routing.js +60 -0
- package/dist/executors/pi-event-serializer.js +43 -0
- package/dist/executors/pi-executor.js +606 -0
- package/dist/executors/pi-reuse-benchmark.js +316 -0
- package/dist/executors/pi-runtime-reuse.js +29 -0
- package/dist/executors/pi-sdk-executor.js +255 -0
- package/dist/executors/pi-sdk.js +1 -0
- package/dist/executors/pi.js +3 -0
- package/dist/executors/shell-executor.js +300 -0
- package/dist/executors/shell-presets.js +47 -0
- package/dist/executors/shell-verification.js +251 -0
- package/dist/executors/shell-write-guard.js +126 -0
- package/dist/executors/shell.js +3 -0
- package/dist/executors/static.js +1 -0
- package/dist/governance/checks.js +434 -0
- package/dist/governance/harness.js +9 -0
- package/dist/governance/index.js +3 -0
- package/dist/governance/manifest-types.js +128 -0
- package/dist/governance/manifest.js +2 -0
- package/dist/governance/path-guard.js +69 -0
- package/dist/governance/path-guards.js +2 -0
- package/dist/governance/profiles.js +3 -0
- package/dist/governance/requirement-coverage.js +425 -0
- package/dist/governance/skill-safety.js +135 -0
- package/dist/governance/spine-audit.js +152 -0
- package/dist/records/closeout.js +2 -0
- package/dist/records/harvest.js +236 -0
- package/dist/records/index.js +3 -0
- package/dist/records/one-shot-runs.js +421 -0
- package/dist/records/promotion.js +199 -0
- package/dist/shared/artifacts-core.js +88 -0
- package/dist/shared/artifacts.js +2 -0
- package/dist/shared/context-files.js +32 -0
- package/dist/shared/context.js +2 -0
- package/dist/shared/copy-dir.js +17 -0
- package/dist/shared/git-progress.js +165 -0
- package/dist/shared/index.js +5 -0
- package/dist/shared/logger.js +23 -0
- package/dist/shared/one-shot-prompt-args.js +98 -0
- package/dist/shared/path-refs.js +31 -0
- package/dist/shared/prompts.js +26 -0
- package/dist/shared/reference-context.js +238 -0
- package/dist/shared/timeout-policy.js +19 -0
- package/dist/shared/timeout.js +1 -0
- package/dist/shared/types.js +5 -0
- package/dist/task/config-types.js +97 -0
- package/dist/task/config.js +2 -0
- package/dist/task/delegate.js +220 -0
- package/dist/task/goal-audit.js +51 -0
- package/dist/task/goal-policy.js +8 -0
- package/dist/task/goal.js +3 -0
- package/dist/task/ids.js +1 -0
- package/dist/task/index.js +9 -0
- package/dist/task/lifecycle.js +1 -0
- package/dist/task/paths.js +1 -0
- package/dist/task/read-model.js +149 -0
- package/dist/task/runtime.js +699 -0
- package/dist/task/source-state.js +1 -0
- package/dist/task/state.js +55 -0
- package/dist/task/subagent-guidance.js +1 -0
- package/dist/task/workflow-state-types.js +92 -0
- package/dist/task/worktree-cleanup.js +140 -0
- package/dist/task/worktree.js +171 -0
- package/dist/workflows/dag/authoring.js +8 -0
- package/dist/workflows/dag/authority-surface.js +138 -0
- package/dist/workflows/dag/canvas-observer.js +474 -0
- package/dist/workflows/dag/decision-envelope.js +502 -0
- package/dist/workflows/dag/decision-evidence.js +153 -0
- package/dist/workflows/dag/decision-gates.js +1 -0
- package/dist/workflows/dag/executor-registry.js +25 -0
- package/dist/workflows/dag/facts.js +4 -0
- package/dist/workflows/dag/failure-category.js +111 -0
- package/dist/workflows/dag/final-verification.js +180 -0
- package/dist/workflows/dag/governance-constants.js +5 -0
- package/dist/workflows/dag/governance-profile.js +405 -0
- package/dist/workflows/dag/index.js +6 -0
- package/dist/workflows/dag/init-hybrid.js +855 -0
- package/dist/workflows/dag/knowledge-curator.js +162 -0
- package/dist/workflows/dag/lifecycle.js +484 -0
- package/dist/workflows/dag/prompt-source.js +88 -0
- package/dist/workflows/dag/prompt.js +130 -0
- package/dist/workflows/dag/reconcile-tasks.js +404 -0
- package/dist/workflows/dag/recovery-recommendation.js +226 -0
- package/dist/workflows/dag/repair-artifact.js +136 -0
- package/dist/workflows/dag/report.js +1019 -0
- package/dist/workflows/dag/runner.js +1677 -0
- package/dist/workflows/dag/runtime.js +5 -0
- package/dist/workflows/dag/skill-instructions.js +471 -0
- package/dist/workflows/dag/skills.js +41 -0
- package/dist/workflows/dag/spec.js +3 -0
- package/dist/workflows/dag/topo.js +30 -0
- package/dist/workflows/dag/types.js +275 -0
- package/dist/workflows/dag/upstream-artifacts.js +95 -0
- package/dist/workflows/dag/validate.js +527 -0
- package/dist/workflows/dynamic/artifacts.js +65 -0
- package/dist/workflows/dynamic/compile.js +360 -0
- package/dist/workflows/dynamic/compileTypes.js +1 -0
- package/dist/workflows/dynamic/errors.js +5 -0
- package/dist/workflows/dynamic/index.js +7 -0
- package/dist/workflows/dynamic/profiles.js +156 -0
- package/dist/workflows/dynamic/spec.js +114 -0
- package/dist/workflows/dynamic/validate.js +275 -0
- package/dist/workflows/loop/actions.js +1334 -0
- package/dist/workflows/loop/benchmark.js +510 -0
- package/dist/workflows/loop/closeout.js +134 -0
- package/dist/workflows/loop/context.js +48 -0
- package/dist/workflows/loop/events.js +25 -0
- package/dist/workflows/loop/hash.js +32 -0
- package/dist/workflows/loop/index.js +8 -0
- package/dist/workflows/loop/paths.js +17 -0
- package/dist/workflows/loop/rounds.js +81 -0
- package/dist/workflows/loop/signals.js +55 -0
- package/dist/workflows/loop/state.js +116 -0
- package/dist/workflows/loop/templates.js +54 -0
- package/dist/workflows/loop/types.js +28 -0
- package/docs/README.md +62 -0
- package/docs/agent-dag-recovery-playbook.md +158 -0
- package/docs/agent-dag-runner.md +40 -0
- package/docs/cursor-executor-usage.md +25 -0
- package/docs/decisions/README.md +3 -0
- package/docs/design/README.md +36 -0
- package/docs/development-principles.md +71 -0
- package/docs/dynamic-workflow-dag-engine-roadmap.md +1749 -0
- package/docs/exec-plans/README.md +6 -0
- package/docs/exec-plans/active/README.md +5 -0
- package/docs/exec-plans/completed/README.md +5 -0
- package/docs/feature-workflow.md +184 -0
- package/docs/harness-methodology-debugging.md +153 -0
- package/docs/harness-methodology-tdd.md +130 -0
- package/docs/harness-methodology-verification.md +27 -0
- package/docs/loop-agent-harness.md +42 -0
- package/docs/progress/README.md +3 -0
- package/docs/reports/README.md +3 -0
- package/docs/templates/adr.md +60 -0
- package/docs/templates/agent-dag-authority-surface-audit.prompt.md +94 -0
- package/docs/templates/agent-dag-decision-envelope.schema.json +213 -0
- package/docs/templates/agent-dag-decision-gate-dogfood-report.md +117 -0
- package/docs/templates/agent-dag-decision-gate.prompt.md +246 -0
- package/docs/templates/agent-dag-process-supervisor.prompt.md +98 -0
- package/docs/templates/agent-dag-report.schema.json +423 -0
- package/docs/templates/agent-dag-review-verdict.prompt.md +68 -0
- package/docs/templates/agent-dag.base.json +195 -0
- package/docs/templates/agent-dag.final-verification.json +190 -0
- package/docs/templates/agent-dag.schema.json +316 -0
- package/docs/templates/agent-dag.supervised-implementation.json +500 -0
- package/docs/templates/exec-plan.md +64 -0
- package/docs/templates/feature-spec.md +53 -0
- package/docs/templates/hybrid-dag.json +193 -0
- package/docs/templates/progress-log.md +17 -0
- package/docs/templates/project-start-checklist.md +9 -0
- package/docs/templates/qa-report.md +42 -0
- package/docs/templates/sprint-contract.md +29 -0
- package/docs/verification-matrix.md +30 -0
- package/examples/decision-gate-agent-dag.json +123 -0
- package/examples/example-dag.json +51 -0
- package/examples/hybrid-loop-agent-dag.json +194 -0
- package/harness.json +92 -0
- package/package.json +61 -0
- package/skills/ai-engineering-context/SKILL.md +48 -0
- package/skills/loop-agent/SKILL.md +260 -0
- package/skills/loop-agent/references/README.md +63 -0
- package/skills/loop-agent/references/command-reference.md +315 -0
- package/skills/loop-agent/references/harness-policy.md +258 -0
- package/skills/loop-agent/references/hybrid-dag.md +216 -0
- package/skills/loop-agent/references/learned/README.md +21 -0
- package/skills/loop-agent/references/model-routing.md +36 -0
- package/skills/loop-agent/references/multi-worktree.md +54 -0
- package/skills/loop-agent/references/one-shot-runs.md +85 -0
- package/skills/loop-agent/references/orchestrator-and-interventions.md +169 -0
- package/skills/loop-agent/references/pi-prompt.md +23 -0
- package/skills/loop-agent/references/pi-subagent-assisted-mode.md +83 -0
- package/skills/loop-agent/references/post-implementation-and-patterns.md +44 -0
- package/skills/loop-agent/references/task-workflow.md +84 -0
- package/skills/loop-agent/references/verification-and-failure-handling.md +74 -0
- package/skills/requesting-code-review/SKILL.md +101 -0
- package/skills/requesting-code-review/code-reviewer.md +168 -0
- package/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/skills/systematic-debugging/SKILL.md +296 -0
- package/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/skills/systematic-debugging/find-polluter.sh +63 -0
- package/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/skills/systematic-debugging/test-academic.md +14 -0
- package/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/skills/verification-before-completion/SKILL.md +154 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const stepModelConfigSchema = z.object({
|
|
3
|
+
provider: z.string().optional(),
|
|
4
|
+
model: z.string().optional(),
|
|
5
|
+
thinking: z.string().optional(),
|
|
6
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
7
|
+
});
|
|
8
|
+
export const modelProfileSchema = stepModelConfigSchema.extend({
|
|
9
|
+
fallback: stepModelConfigSchema.optional(),
|
|
10
|
+
});
|
|
11
|
+
export const complexityRouteSchema = z.object({
|
|
12
|
+
small: z.string().optional(),
|
|
13
|
+
medium: z.string().optional(),
|
|
14
|
+
large: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
export const worktreeManifestConfigSchema = z.object({
|
|
17
|
+
rootRelativePath: z.string().optional().default(".worktrees"),
|
|
18
|
+
});
|
|
19
|
+
export const taskExecutorSchema = z.enum(["pi", "cursor"]);
|
|
20
|
+
export const executorManifestSchema = z.object({
|
|
21
|
+
description: z.string().optional(),
|
|
22
|
+
enabled: z.boolean().optional(),
|
|
23
|
+
defaultModel: z.string().optional().default("default"),
|
|
24
|
+
requiresApiKey: z.string().optional().default("CURSOR_API_KEY"),
|
|
25
|
+
});
|
|
26
|
+
export const workflowPolicyProfileNameSchema = z.enum([
|
|
27
|
+
"minimal",
|
|
28
|
+
"standard",
|
|
29
|
+
"reviewed",
|
|
30
|
+
"supervised",
|
|
31
|
+
]);
|
|
32
|
+
export const workflowPolicyDagTemplateSchema = z.enum([
|
|
33
|
+
"standard-dag",
|
|
34
|
+
"review-gated-dag",
|
|
35
|
+
"supervised-implementation",
|
|
36
|
+
]);
|
|
37
|
+
export const workflowPolicySchema = z
|
|
38
|
+
.object({
|
|
39
|
+
/** Preferred implementation workflow for recoverable loop-agent work. Declarative policy; callers may still require explicit CLI flags. */
|
|
40
|
+
defaultImplementationWorkflow: z
|
|
41
|
+
.enum(["agent-dag", "level-1"])
|
|
42
|
+
.optional()
|
|
43
|
+
.default("agent-dag"),
|
|
44
|
+
dag: z
|
|
45
|
+
.object({
|
|
46
|
+
defaultEntry: z
|
|
47
|
+
.enum(["dag run-task", "run-dag"])
|
|
48
|
+
.optional()
|
|
49
|
+
.default("dag run-task"),
|
|
50
|
+
profileRouting: z
|
|
51
|
+
.object({
|
|
52
|
+
minimal: workflowPolicyDagTemplateSchema
|
|
53
|
+
.optional()
|
|
54
|
+
.default("standard-dag"),
|
|
55
|
+
standard: workflowPolicyDagTemplateSchema
|
|
56
|
+
.optional()
|
|
57
|
+
.default("standard-dag"),
|
|
58
|
+
reviewed: workflowPolicyDagTemplateSchema
|
|
59
|
+
.optional()
|
|
60
|
+
.default("review-gated-dag"),
|
|
61
|
+
supervised: workflowPolicyDagTemplateSchema
|
|
62
|
+
.optional()
|
|
63
|
+
.default("supervised-implementation"),
|
|
64
|
+
})
|
|
65
|
+
.optional()
|
|
66
|
+
.default({}),
|
|
67
|
+
})
|
|
68
|
+
.optional()
|
|
69
|
+
.default({}),
|
|
70
|
+
humanGatePolicy: z
|
|
71
|
+
.object({
|
|
72
|
+
defaultMode: z
|
|
73
|
+
.enum(["record-only", "pause-on-human"])
|
|
74
|
+
.optional()
|
|
75
|
+
.default("record-only"),
|
|
76
|
+
pauseOn: z.array(z.string()).optional().default([]),
|
|
77
|
+
})
|
|
78
|
+
.optional()
|
|
79
|
+
.default({}),
|
|
80
|
+
})
|
|
81
|
+
.optional()
|
|
82
|
+
.default({});
|
|
83
|
+
export const harnessManifestSchema = z.object({
|
|
84
|
+
version: z.number(),
|
|
85
|
+
project: z.string(),
|
|
86
|
+
governanceRoot: z.string(),
|
|
87
|
+
features: z
|
|
88
|
+
.object({
|
|
89
|
+
goals: z.boolean().optional().default(true),
|
|
90
|
+
})
|
|
91
|
+
.optional()
|
|
92
|
+
.default({
|
|
93
|
+
goals: true,
|
|
94
|
+
}),
|
|
95
|
+
entrypoints: z.record(z.string(), z.string().nullable()),
|
|
96
|
+
artifacts: z.record(z.string(), z.string().nullable()),
|
|
97
|
+
scripts: z.record(z.string(), z.string().nullable()),
|
|
98
|
+
models: z
|
|
99
|
+
.object({
|
|
100
|
+
analyze: stepModelConfigSchema.optional(),
|
|
101
|
+
plan: stepModelConfigSchema.optional(),
|
|
102
|
+
implement: stepModelConfigSchema.optional(),
|
|
103
|
+
verify: stepModelConfigSchema.optional(),
|
|
104
|
+
retrospective: stepModelConfigSchema.optional(),
|
|
105
|
+
})
|
|
106
|
+
.optional()
|
|
107
|
+
.default({}),
|
|
108
|
+
modelProfiles: z
|
|
109
|
+
.record(z.string(), modelProfileSchema)
|
|
110
|
+
.optional()
|
|
111
|
+
.default({}),
|
|
112
|
+
modelRouting: z
|
|
113
|
+
.object({
|
|
114
|
+
analyze: complexityRouteSchema.optional(),
|
|
115
|
+
plan: complexityRouteSchema.optional(),
|
|
116
|
+
implement: complexityRouteSchema.optional(),
|
|
117
|
+
retrospective: complexityRouteSchema.optional(),
|
|
118
|
+
implementRetry: z.string().optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional()
|
|
121
|
+
.default({}),
|
|
122
|
+
workflowPolicy: workflowPolicySchema,
|
|
123
|
+
worktree: worktreeManifestConfigSchema.optional(),
|
|
124
|
+
executors: z
|
|
125
|
+
.record(z.string(), executorManifestSchema)
|
|
126
|
+
.optional()
|
|
127
|
+
.default({}),
|
|
128
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { pathMatchesPattern } from '../shared/git-progress.js';
|
|
2
|
+
function normalizePath(value) {
|
|
3
|
+
return value.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
4
|
+
}
|
|
5
|
+
function isTaskHarnessPath(filePath, taskId) {
|
|
6
|
+
const normalized = normalizePath(filePath);
|
|
7
|
+
if (!normalized.startsWith('.harness/tasks/'))
|
|
8
|
+
return false;
|
|
9
|
+
if (!taskId)
|
|
10
|
+
return true;
|
|
11
|
+
return normalized.startsWith(`.harness/tasks/${taskId}/`);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Validate git diff paths against task allowed/forbidden boundaries.
|
|
15
|
+
* Task harness paths are always allowed so artifacts/logs can be updated.
|
|
16
|
+
*/
|
|
17
|
+
export function validateDiffPathGuard(changedPaths, taskConfig) {
|
|
18
|
+
const forbiddenHits = [];
|
|
19
|
+
const outOfScopeHits = [];
|
|
20
|
+
for (const rawPath of changedPaths) {
|
|
21
|
+
const filePath = normalizePath(rawPath);
|
|
22
|
+
if (isTaskHarnessPath(filePath, taskConfig.taskId))
|
|
23
|
+
continue;
|
|
24
|
+
if (matchesForbidden(filePath, taskConfig.forbiddenPaths ?? [])) {
|
|
25
|
+
forbiddenHits.push(filePath);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const allowed = taskConfig.allowedPaths ?? [];
|
|
29
|
+
if (allowed.length === 0) {
|
|
30
|
+
outOfScopeHits.push(filePath);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const allowedMatch = allowed.some((pattern) => pathMatchesPattern(filePath, pattern));
|
|
34
|
+
if (!allowedMatch) {
|
|
35
|
+
outOfScopeHits.push(filePath);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const violations = [...forbiddenHits, ...outOfScopeHits];
|
|
39
|
+
return {
|
|
40
|
+
ok: violations.length === 0,
|
|
41
|
+
violations,
|
|
42
|
+
forbiddenHits,
|
|
43
|
+
outOfScopeHits,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function matchesForbidden(filePath, forbiddenPaths) {
|
|
47
|
+
return forbiddenPaths.some((pattern) => pathMatchesPattern(filePath, pattern));
|
|
48
|
+
}
|
|
49
|
+
export async function listDiffPaths(repoRoot, baseRef, headRef) {
|
|
50
|
+
const { spawn } = await import('node:child_process');
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const child = spawn('git', ['diff', '--name-only', '-z', `${baseRef}..${headRef}`], {
|
|
53
|
+
cwd: repoRoot,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
});
|
|
56
|
+
const out = [];
|
|
57
|
+
child.stdout?.on('data', (chunk) => out.push(chunk));
|
|
58
|
+
child.on('error', reject);
|
|
59
|
+
child.on('close', (code) => {
|
|
60
|
+
if (code !== 0) {
|
|
61
|
+
resolve([]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const raw = Buffer.concat(out).toString('utf-8');
|
|
65
|
+
const paths = raw.split('\0').map((line) => line.trim()).filter(Boolean);
|
|
66
|
+
resolve(Array.from(new Set(paths)));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { access, readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { extractGoalAuditScope } from '../task/goal-audit.js';
|
|
4
|
+
import { getTaskPaths, loadTaskConfig } from '../task/runtime.js';
|
|
5
|
+
export const COVERAGE_STATUSES = [
|
|
6
|
+
'covered_by_code',
|
|
7
|
+
'covered_by_test',
|
|
8
|
+
'covered_by_static_gate',
|
|
9
|
+
'covered_by_artifact',
|
|
10
|
+
'explicitly_out_of_scope',
|
|
11
|
+
];
|
|
12
|
+
const GOAL_SCOPE_START = /<!--\s*goal-scope\s*-->/i;
|
|
13
|
+
const GOAL_SCOPE_END = /<!--\s*\/goal-scope\s*-->/i;
|
|
14
|
+
const SECTION_HEADING = /^##\s+(.+?)\s*$/;
|
|
15
|
+
const CHECKLIST_LINE = /^-\s*\[( |x|X)\]\s+(.+)$/;
|
|
16
|
+
const NUMBERED_LINE = /^(\d+)\.\s+(.+)$/;
|
|
17
|
+
const REQ_ID = /\b(REQ-[A-Z0-9]+(?:-[A-Z0-9]+)*)\b/i;
|
|
18
|
+
const OUT_OF_SCOPE_SECTION = /非目标|out[- ]of[- ]scope/i;
|
|
19
|
+
const OUT_OF_SCOPE_INLINE = /\[(?:out-of-scope|explicitly_out_of_scope)\]|explicitly_out_of_scope\s*:/i;
|
|
20
|
+
const STATIC_GATE_KEYWORDS = [
|
|
21
|
+
{ pattern: /lint|typecheck|静态/i, tokens: ['lint', 'typecheck', 'tsc'] },
|
|
22
|
+
{ pattern: /test|单元测试|vitest/i, tokens: ['test', 'vitest', 'npm test'] },
|
|
23
|
+
{ pattern: /check-repo|仓库检查/i, tokens: ['check-repo', 'scripts/check-repo'] },
|
|
24
|
+
];
|
|
25
|
+
const TEXT_FILE_EXTENSIONS = new Set([
|
|
26
|
+
'.ts',
|
|
27
|
+
'.tsx',
|
|
28
|
+
'.js',
|
|
29
|
+
'.jsx',
|
|
30
|
+
'.mjs',
|
|
31
|
+
'.cjs',
|
|
32
|
+
'.md',
|
|
33
|
+
'.json',
|
|
34
|
+
'.sh',
|
|
35
|
+
]);
|
|
36
|
+
function extractReqId(text) {
|
|
37
|
+
const match = text.match(REQ_ID);
|
|
38
|
+
return match?.[1]?.toUpperCase();
|
|
39
|
+
}
|
|
40
|
+
function isOutOfScopeSectionHeading(heading) {
|
|
41
|
+
return OUT_OF_SCOPE_SECTION.test(heading.trim());
|
|
42
|
+
}
|
|
43
|
+
function isOutOfScopeLine(text) {
|
|
44
|
+
return OUT_OF_SCOPE_INLINE.test(text);
|
|
45
|
+
}
|
|
46
|
+
export function splitMarkdownSections(text) {
|
|
47
|
+
const lines = text.split('\n');
|
|
48
|
+
const sections = [];
|
|
49
|
+
let currentHeading = null;
|
|
50
|
+
let currentLines = [];
|
|
51
|
+
const flush = () => {
|
|
52
|
+
sections.push({
|
|
53
|
+
heading: currentHeading,
|
|
54
|
+
body: currentLines.join('\n'),
|
|
55
|
+
});
|
|
56
|
+
currentLines = [];
|
|
57
|
+
};
|
|
58
|
+
for (const line of lines) {
|
|
59
|
+
const headingMatch = line.match(SECTION_HEADING);
|
|
60
|
+
if (headingMatch) {
|
|
61
|
+
flush();
|
|
62
|
+
currentHeading = headingMatch[1]?.trim() ?? null;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
currentLines.push(line);
|
|
66
|
+
}
|
|
67
|
+
flush();
|
|
68
|
+
return sections;
|
|
69
|
+
}
|
|
70
|
+
export function extractRequirementsFromMarkdown(text) {
|
|
71
|
+
const scoped = extractGoalAuditScope(text);
|
|
72
|
+
const hasScopeMarkers = GOAL_SCOPE_START.test(text) && GOAL_SCOPE_END.test(text);
|
|
73
|
+
const sourceText = hasScopeMarkers ? scoped : text;
|
|
74
|
+
const sections = splitMarkdownSections(sourceText);
|
|
75
|
+
const requirements = [];
|
|
76
|
+
const outOfScope = [];
|
|
77
|
+
const seenIds = new Set();
|
|
78
|
+
let checklistCounter = 0;
|
|
79
|
+
let numberedCounter = 0;
|
|
80
|
+
const pushRequirement = (item, bucket) => {
|
|
81
|
+
const explicitId = extractReqId(item.label);
|
|
82
|
+
const id = explicitId ?? item.id ?? `${item.source}-${bucket.length + 1}`;
|
|
83
|
+
if (seenIds.has(id))
|
|
84
|
+
return;
|
|
85
|
+
seenIds.add(id);
|
|
86
|
+
bucket.push({
|
|
87
|
+
...item,
|
|
88
|
+
id,
|
|
89
|
+
label: item.label.trim(),
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
for (const section of sections) {
|
|
93
|
+
const inOutOfScopeSection = section.heading ? isOutOfScopeSectionHeading(section.heading) : false;
|
|
94
|
+
const lines = section.body.split('\n');
|
|
95
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
96
|
+
const lineNumber = index + 1;
|
|
97
|
+
const line = lines[index] ?? '';
|
|
98
|
+
const trimmed = line.trim();
|
|
99
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
100
|
+
continue;
|
|
101
|
+
const checklistMatch = trimmed.match(CHECKLIST_LINE);
|
|
102
|
+
if (checklistMatch) {
|
|
103
|
+
const checked = (checklistMatch[1] ?? '').toLowerCase() === 'x';
|
|
104
|
+
const label = checklistMatch[2]?.trim() ?? '';
|
|
105
|
+
if (!label)
|
|
106
|
+
continue;
|
|
107
|
+
checklistCounter += 1;
|
|
108
|
+
const bucket = inOutOfScopeSection || isOutOfScopeLine(label) ? outOfScope : requirements;
|
|
109
|
+
pushRequirement({
|
|
110
|
+
label,
|
|
111
|
+
source: 'checklist',
|
|
112
|
+
line: lineNumber,
|
|
113
|
+
section: section.heading ?? undefined,
|
|
114
|
+
checked,
|
|
115
|
+
id: extractReqId(label) ?? `checklist-${checklistCounter}`,
|
|
116
|
+
}, bucket);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const numberedMatch = trimmed.match(NUMBERED_LINE);
|
|
120
|
+
if (numberedMatch) {
|
|
121
|
+
const label = numberedMatch[2]?.trim() ?? '';
|
|
122
|
+
if (!label)
|
|
123
|
+
continue;
|
|
124
|
+
numberedCounter += 1;
|
|
125
|
+
const bucket = inOutOfScopeSection || isOutOfScopeLine(label) ? outOfScope : requirements;
|
|
126
|
+
pushRequirement({
|
|
127
|
+
label,
|
|
128
|
+
source: 'numbered',
|
|
129
|
+
line: lineNumber,
|
|
130
|
+
section: section.heading ?? undefined,
|
|
131
|
+
id: extractReqId(label) ?? `numbered-${numberedCounter}`,
|
|
132
|
+
}, bucket);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const explicitId = extractReqId(trimmed);
|
|
136
|
+
if (explicitId) {
|
|
137
|
+
const label = trimmed.replace(REQ_ID, '').replace(/^[-*:\s]+/, '').trim() || explicitId;
|
|
138
|
+
const bucket = inOutOfScopeSection || isOutOfScopeLine(trimmed) ? outOfScope : requirements;
|
|
139
|
+
pushRequirement({
|
|
140
|
+
id: explicitId,
|
|
141
|
+
label,
|
|
142
|
+
source: 'explicit-id',
|
|
143
|
+
line: lineNumber,
|
|
144
|
+
section: section.heading ?? undefined,
|
|
145
|
+
}, bucket);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { requirements, outOfScope };
|
|
150
|
+
}
|
|
151
|
+
function entryIsOutOfScope(entry) {
|
|
152
|
+
return entry.statuses.includes('explicitly_out_of_scope');
|
|
153
|
+
}
|
|
154
|
+
function entryIsCovered(entry) {
|
|
155
|
+
return entry.statuses.some((status) => status !== 'explicitly_out_of_scope');
|
|
156
|
+
}
|
|
157
|
+
export function buildRequirementCoverageAudit(taskId, requirements, outOfScope, evidenceByRequirementId) {
|
|
158
|
+
const outOfScopeIds = new Set(outOfScope.map((item) => item.id));
|
|
159
|
+
const entries = [];
|
|
160
|
+
for (const requirement of requirements) {
|
|
161
|
+
if (outOfScopeIds.has(requirement.id)) {
|
|
162
|
+
entries.push({
|
|
163
|
+
requirement,
|
|
164
|
+
statuses: ['explicitly_out_of_scope'],
|
|
165
|
+
evidence: ['source/需求.md'],
|
|
166
|
+
});
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const mapped = evidenceByRequirementId.get(requirement.id);
|
|
170
|
+
const statuses = new Set(mapped?.statuses ?? []);
|
|
171
|
+
const evidence = [...(mapped?.evidence ?? [])];
|
|
172
|
+
entries.push({
|
|
173
|
+
requirement,
|
|
174
|
+
statuses: Array.from(statuses),
|
|
175
|
+
evidence: Array.from(new Set(evidence)),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
for (const requirement of outOfScope) {
|
|
179
|
+
if (entries.some((entry) => entry.requirement.id === requirement.id))
|
|
180
|
+
continue;
|
|
181
|
+
entries.push({
|
|
182
|
+
requirement,
|
|
183
|
+
statuses: ['explicitly_out_of_scope'],
|
|
184
|
+
evidence: ['source/需求.md'],
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const uncovered = entries.filter((entry) => !entryIsOutOfScope(entry) && !entryIsCovered(entry));
|
|
188
|
+
const covered = entries.filter((entry) => entryIsCovered(entry));
|
|
189
|
+
const outOfScopeCount = entries.filter((entry) => entryIsOutOfScope(entry)).length;
|
|
190
|
+
return {
|
|
191
|
+
ok: uncovered.length === 0,
|
|
192
|
+
taskId,
|
|
193
|
+
extractedAt: new Date().toISOString(),
|
|
194
|
+
entries,
|
|
195
|
+
uncovered,
|
|
196
|
+
summary: {
|
|
197
|
+
total: entries.length,
|
|
198
|
+
covered: covered.length,
|
|
199
|
+
outOfScope: outOfScopeCount,
|
|
200
|
+
uncovered: uncovered.length,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async function exists(targetPath) {
|
|
205
|
+
try {
|
|
206
|
+
await access(targetPath);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function walkTextFiles(rootDir, files) {
|
|
214
|
+
let entries;
|
|
215
|
+
try {
|
|
216
|
+
entries = await readdir(rootDir, { withFileTypes: true });
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
for (const entry of entries) {
|
|
222
|
+
const absolutePath = path.join(rootDir, entry.name);
|
|
223
|
+
if (entry.isDirectory()) {
|
|
224
|
+
if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist')
|
|
225
|
+
continue;
|
|
226
|
+
await walkTextFiles(absolutePath, files);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
if (!entry.isFile())
|
|
230
|
+
continue;
|
|
231
|
+
const ext = path.extname(entry.name);
|
|
232
|
+
if (!TEXT_FILE_EXTENSIONS.has(ext))
|
|
233
|
+
continue;
|
|
234
|
+
files.push(absolutePath);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function classifyEvidencePath(relativePath) {
|
|
238
|
+
if (/(^|\/)test\//.test(relativePath) || relativePath.includes('.test.')) {
|
|
239
|
+
return 'covered_by_test';
|
|
240
|
+
}
|
|
241
|
+
if (relativePath.includes('artifacts/')) {
|
|
242
|
+
return 'covered_by_artifact';
|
|
243
|
+
}
|
|
244
|
+
return 'covered_by_code';
|
|
245
|
+
}
|
|
246
|
+
function detectStaticGateCoverage(label, verifyContent) {
|
|
247
|
+
const haystack = verifyContent.toLowerCase();
|
|
248
|
+
for (const rule of STATIC_GATE_KEYWORDS) {
|
|
249
|
+
if (!rule.pattern.test(label))
|
|
250
|
+
continue;
|
|
251
|
+
if (rule.tokens.some((token) => haystack.includes(token.toLowerCase()))) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
async function loadCoverageMapping(mappingPath) {
|
|
258
|
+
if (!await exists(mappingPath))
|
|
259
|
+
return null;
|
|
260
|
+
try {
|
|
261
|
+
const raw = JSON.parse(await readFile(mappingPath, 'utf-8'));
|
|
262
|
+
if (raw.version !== 1 || !Array.isArray(raw.entries))
|
|
263
|
+
return null;
|
|
264
|
+
return raw;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async function collectSearchRoots(repoRoot, allowedPaths) {
|
|
271
|
+
if (allowedPaths.length === 0) {
|
|
272
|
+
return [repoRoot];
|
|
273
|
+
}
|
|
274
|
+
return allowedPaths.map((entry) => path.resolve(repoRoot, entry.replace(/\/+$/, '')));
|
|
275
|
+
}
|
|
276
|
+
function pathMatchesAllowed(relativePath, allowedPaths) {
|
|
277
|
+
if (allowedPaths.length === 0)
|
|
278
|
+
return true;
|
|
279
|
+
const normalized = relativePath.replace(/\\/g, '/');
|
|
280
|
+
return allowedPaths.some((allowed) => {
|
|
281
|
+
const prefix = allowed.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
282
|
+
return normalized === prefix || normalized.startsWith(`${prefix}/`);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
async function scanRepoEvidence(repoRoot, requirements, allowedPaths, verifyContent, artifactContents) {
|
|
286
|
+
const evidenceById = new Map();
|
|
287
|
+
const addEvidence = (requirementId, status, evidencePath) => {
|
|
288
|
+
const current = evidenceById.get(requirementId) ?? { statuses: [], evidence: [] };
|
|
289
|
+
if (!current.statuses.includes(status))
|
|
290
|
+
current.statuses.push(status);
|
|
291
|
+
if (!current.evidence.includes(evidencePath))
|
|
292
|
+
current.evidence.push(evidencePath);
|
|
293
|
+
evidenceById.set(requirementId, current);
|
|
294
|
+
};
|
|
295
|
+
for (const requirement of requirements) {
|
|
296
|
+
if (detectStaticGateCoverage(requirement.label, verifyContent)) {
|
|
297
|
+
addEvidence(requirement.id, 'covered_by_static_gate', 'artifacts/验证结果.md');
|
|
298
|
+
}
|
|
299
|
+
for (const artifact of artifactContents) {
|
|
300
|
+
if (artifact.content.includes(requirement.id)
|
|
301
|
+
|| artifact.content.toLowerCase().includes(requirement.label.toLowerCase())) {
|
|
302
|
+
addEvidence(requirement.id, 'covered_by_artifact', artifact.relativePath);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const searchRoots = await collectSearchRoots(repoRoot, allowedPaths);
|
|
307
|
+
const files = [];
|
|
308
|
+
for (const root of searchRoots) {
|
|
309
|
+
if (!await exists(root))
|
|
310
|
+
continue;
|
|
311
|
+
const rootStat = await stat(root);
|
|
312
|
+
if (rootStat.isFile()) {
|
|
313
|
+
files.push(root);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (rootStat.isDirectory()) {
|
|
317
|
+
await walkTextFiles(root, files);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const absolutePath of files) {
|
|
321
|
+
const relativePath = path.relative(repoRoot, absolutePath).replace(/\\/g, '/');
|
|
322
|
+
if (!pathMatchesAllowed(relativePath, allowedPaths))
|
|
323
|
+
continue;
|
|
324
|
+
let content = '';
|
|
325
|
+
try {
|
|
326
|
+
content = await readFile(absolutePath, 'utf-8');
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
for (const requirement of requirements) {
|
|
332
|
+
if (!content.includes(requirement.id) && !content.toLowerCase().includes(requirement.label.toLowerCase())) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
addEvidence(requirement.id, classifyEvidencePath(relativePath), relativePath);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return evidenceById;
|
|
339
|
+
}
|
|
340
|
+
export async function runRequirementCoverageAudit(repoRoot, taskId) {
|
|
341
|
+
const taskPaths = getTaskPaths(repoRoot, taskId);
|
|
342
|
+
const requirementPath = path.join(taskPaths.sourceDir, '需求.md');
|
|
343
|
+
const taskConfig = await loadTaskConfig(repoRoot, taskId);
|
|
344
|
+
let requirementMarkdown = '';
|
|
345
|
+
try {
|
|
346
|
+
requirementMarkdown = await readFile(requirementPath, 'utf-8');
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return buildRequirementCoverageAudit(taskId, [], [], new Map());
|
|
350
|
+
}
|
|
351
|
+
const { requirements, outOfScope } = extractRequirementsFromMarkdown(requirementMarkdown);
|
|
352
|
+
const artifactDir = path.join(taskPaths.taskDir, 'artifacts');
|
|
353
|
+
const artifactContents = [];
|
|
354
|
+
let verifyContent = '';
|
|
355
|
+
if (await exists(artifactDir)) {
|
|
356
|
+
const artifactEntries = await readdir(artifactDir, { withFileTypes: true });
|
|
357
|
+
for (const entry of artifactEntries) {
|
|
358
|
+
if (!entry.isFile() || !entry.name.endsWith('.md'))
|
|
359
|
+
continue;
|
|
360
|
+
const absolutePath = path.join(artifactDir, entry.name);
|
|
361
|
+
const content = await readFile(absolutePath, 'utf-8');
|
|
362
|
+
const relativePath = path.relative(repoRoot, absolutePath).replace(/\\/g, '/');
|
|
363
|
+
artifactContents.push({ relativePath, content });
|
|
364
|
+
if (entry.name === '验证结果.md') {
|
|
365
|
+
verifyContent = content;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const mappingPath = path.join(artifactDir, 'requirement-coverage.json');
|
|
370
|
+
const mapping = await loadCoverageMapping(mappingPath);
|
|
371
|
+
const evidenceById = await scanRepoEvidence(repoRoot, requirements, taskConfig.allowedPaths, verifyContent, artifactContents);
|
|
372
|
+
if (mapping) {
|
|
373
|
+
for (const entry of mapping.entries) {
|
|
374
|
+
const current = evidenceById.get(entry.requirementId) ?? { statuses: [], evidence: [] };
|
|
375
|
+
for (const status of entry.statuses) {
|
|
376
|
+
if (!current.statuses.includes(status))
|
|
377
|
+
current.statuses.push(status);
|
|
378
|
+
}
|
|
379
|
+
for (const evidencePath of entry.evidence ?? []) {
|
|
380
|
+
if (!current.evidence.includes(evidencePath))
|
|
381
|
+
current.evidence.push(evidencePath);
|
|
382
|
+
}
|
|
383
|
+
evidenceById.set(entry.requirementId, current);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return buildRequirementCoverageAudit(taskId, requirements, outOfScope, evidenceById);
|
|
387
|
+
}
|
|
388
|
+
export function formatRequirementCoverageMarkdown(audit) {
|
|
389
|
+
const lines = [
|
|
390
|
+
'# Requirement Coverage Audit',
|
|
391
|
+
'',
|
|
392
|
+
`- task: \`${audit.taskId}\``,
|
|
393
|
+
`- extractedAt: ${audit.extractedAt}`,
|
|
394
|
+
`- ok: ${audit.ok ? 'yes' : 'no'}`,
|
|
395
|
+
`- total: ${audit.summary.total}`,
|
|
396
|
+
`- covered: ${audit.summary.covered}`,
|
|
397
|
+
`- outOfScope: ${audit.summary.outOfScope}`,
|
|
398
|
+
`- uncovered: ${audit.summary.uncovered}`,
|
|
399
|
+
'',
|
|
400
|
+
'## Requirements',
|
|
401
|
+
'',
|
|
402
|
+
'| ID | Label | Statuses | Evidence |',
|
|
403
|
+
'| --- | --- | --- | --- |',
|
|
404
|
+
];
|
|
405
|
+
for (const entry of audit.entries) {
|
|
406
|
+
const statuses = entry.statuses.length > 0 ? entry.statuses.join(', ') : 'uncovered';
|
|
407
|
+
const evidence = entry.evidence.length > 0 ? entry.evidence.join('; ') : '-';
|
|
408
|
+
lines.push(`| ${entry.requirement.id} | ${entry.requirement.label.replace(/\|/g, '\\|')} | ${statuses} | ${evidence} |`);
|
|
409
|
+
}
|
|
410
|
+
if (audit.uncovered.length > 0) {
|
|
411
|
+
lines.push('', '## Uncovered', '');
|
|
412
|
+
for (const entry of audit.uncovered) {
|
|
413
|
+
lines.push(`- ${entry.requirement.id}: ${entry.requirement.label}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return `${lines.join('\n')}\n`;
|
|
417
|
+
}
|
|
418
|
+
export function formatRequirementCoverageJson(audit) {
|
|
419
|
+
return `${JSON.stringify(audit, null, 2)}\n`;
|
|
420
|
+
}
|
|
421
|
+
export function requirementCoverageFailureReasons(audit) {
|
|
422
|
+
if (audit.ok)
|
|
423
|
+
return [];
|
|
424
|
+
return audit.uncovered.map((entry) => `uncovered requirement ${entry.requirement.id}: ${entry.requirement.label}`);
|
|
425
|
+
}
|