@frankzhang2026/opencode-android-orchestrator 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/README.md +8 -8
- package/dist/installer/adaptive-templates.d.ts +1 -1
- package/dist/installer/adaptive-templates.d.ts.map +1 -1
- package/dist/installer/adaptive-templates.js +11 -0
- package/dist/installer/adaptive-templates.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +3 -3
- package/dist/installer/opencode-config.js +1 -1
- package/dist/queue/cli.js +1 -1
- package/dist/queue/cli.js.map +1 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/queue.js +57 -0
- package/dist/queue/queue.js.map +1 -1
- package/dist/queue/tools.js +1 -1
- package/dist/queue/tools.js.map +1 -1
- package/docs/MIGRATION.md +8 -8
- package/docs/QUEUE.md +35 -7
- package/docs/SECURITY.md +1 -1
- package/docs/TROUBLESHOOTING.md +8 -7
- package/package.json +1 -1
- package/templates/.opencode/agents/scheduled-coder.md +9 -0
- package/templates/.opencode/agents/scheduled-planner.md +14 -1
- package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +7 -1
- package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +3 -0
- package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +4 -0
- package/templates/automation/task-contract.schema.json +76 -4
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +25 -2
- package/templates/scripts/automation/acceptance-report.sh +12 -0
- package/templates/scripts/automation/lib.sh +108 -0
- package/templates/scripts/automation/record-red.sh +170 -32
- package/templates/scripts/automation/show-acceptance-review.sh +7 -0
- package/templates/scripts/automation/status.sh +6 -0
- package/templates/scripts/automation/validate-contract.sh +46 -2
- package/templates/scripts/automation/verify-task.sh +14 -0
|
@@ -6,51 +6,189 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
6
6
|
source "$SCRIPT_DIR/lib.sh"
|
|
7
7
|
|
|
8
8
|
task_id="${1:-}"
|
|
9
|
-
|
|
10
|
-
separator="${3:-}"
|
|
11
|
-
filter="${4:-}"
|
|
12
|
-
|
|
13
|
-
if [[ -z "$task_id" || -z "$expected" || "$separator" != "--" || -z "$filter" || "$#" -ne 4 ]]; then
|
|
14
|
-
printf 'Usage: %s TASK-ID EXPECTED-FAILURE-TEXT -- TEST-FILTER\n' "$0" >&2
|
|
15
|
-
exit 2
|
|
16
|
-
fi
|
|
17
|
-
|
|
9
|
+
[[ -n "$task_id" ]] || { printf 'Usage: %s TASK-ID [LEGACY-EXPECTED -- LEGACY-FILTER]\n' "$0" >&2; exit 2; }
|
|
18
10
|
automation_validate_task_id "$task_id"
|
|
19
11
|
automation_require_queue_execution "$task_id"
|
|
20
|
-
automation_validate_test_filter "$filter"
|
|
21
|
-
[[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
|
|
22
12
|
[[ "$(automation_read_state "$task_id")" == "CODING" ]] || automation_die "$task_id is not CODING"
|
|
23
13
|
"$SCRIPT_DIR/validate-contract.sh" "$task_id"
|
|
24
14
|
|
|
25
15
|
contract="$(automation_contract_path "$task_id")"
|
|
26
|
-
|
|
27
|
-
[[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
|
|
28
|
-
gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
|
|
29
|
-
|
|
16
|
+
schema_version="$(jq -er '.schemaVersion' "$contract")"
|
|
30
17
|
evidence_dir="$(automation_evidence_path "$task_id")"
|
|
31
18
|
mkdir -p "$evidence_dir"
|
|
32
|
-
red_log="$evidence_dir/red.log"
|
|
33
19
|
red_meta="$evidence_dir/red.json"
|
|
34
20
|
[[ ! -e "$red_meta" ]] || automation_die "RED evidence already exists for $task_id"
|
|
35
|
-
started_at="$(automation_now)"
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
# Preserve the exact V1/V2 behavior for already approved contracts.
|
|
23
|
+
if [[ "$schema_version" != "3" ]]; then
|
|
24
|
+
expected="${2:-}"
|
|
25
|
+
separator="${3:-}"
|
|
26
|
+
filter="${4:-}"
|
|
27
|
+
if [[ -z "$expected" || "$separator" != "--" || -z "$filter" || "$#" -ne 4 ]]; then
|
|
28
|
+
printf 'Usage: %s TASK-ID EXPECTED-FAILURE-TEXT -- TEST-FILTER\n' "$0" >&2
|
|
29
|
+
exit 2
|
|
30
|
+
fi
|
|
31
|
+
automation_validate_test_filter "$filter"
|
|
32
|
+
[[ ${#expected} -ge 3 ]] || automation_die "expected failure text is too short"
|
|
33
|
+
target_count="$(jq -er --arg filter "$filter" '[.targetTests[] | select(.filter == $filter)] | length' "$contract")"
|
|
34
|
+
[[ "$target_count" -eq 1 ]] || automation_die "test filter must identify exactly one declared contract target: $filter"
|
|
35
|
+
gradle_task="$(jq -er --arg filter "$filter" '.targetTests[] | select(.filter == $filter) | .gradleTask' "$contract")"
|
|
36
|
+
red_log="$evidence_dir/red.log"
|
|
37
|
+
started_at="$(automation_now)"
|
|
38
|
+
set +e
|
|
39
|
+
automation_run_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" 2>&1 | tee "$red_log"
|
|
40
|
+
red_status=${PIPESTATUS[0]}
|
|
41
|
+
set -e
|
|
42
|
+
[[ "$red_status" -ne 0 ]] || automation_die "RED capture failed: focused test passed before implementation"
|
|
43
|
+
rg -F "$expected" "$red_log" >/dev/null || automation_die "RED output does not contain the expected failure text"
|
|
44
|
+
jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$(automation_now)" \
|
|
45
|
+
--arg expectedFailure "$expected" --arg gradleTask "$gradle_task" --arg testFilter "$filter" \
|
|
46
|
+
--argjson exitCode "$red_status" \
|
|
47
|
+
'{taskId: $taskId, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
48
|
+
command: ["./gradlew", $gradleTask, "--tests", $testFilter],
|
|
49
|
+
expectedFailure: $expectedFailure, exitCode: $exitCode}' | automation_record_json "$red_meta"
|
|
50
|
+
automation_info "$task_id legacy RED evidence recorded"
|
|
51
|
+
exit 0
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
[[ "$#" -eq 1 ]] || { printf 'Schema V3 usage: %s TASK-ID\n' "$0" >&2; exit 2; }
|
|
55
|
+
"$SCRIPT_DIR/scope-gate.sh" "$task_id" >/dev/null
|
|
41
56
|
|
|
42
|
-
|
|
43
|
-
|
|
57
|
+
# RED must be captured before any production change. Planning artifacts are
|
|
58
|
+
# excluded; test sources and fixtures are the only permitted product changes.
|
|
59
|
+
while IFS= read -r changed_path; do
|
|
60
|
+
[[ -n "$changed_path" ]] || continue
|
|
61
|
+
if ! automation_array_matches_path "$AUTOMATION_CONFIG" '.androidProject.testPaths' "$changed_path"; then
|
|
62
|
+
automation_die "RED preflight requires unchanged production code; non-test path changed: $changed_path"
|
|
63
|
+
fi
|
|
64
|
+
done < <(automation_product_changed_paths_at "$task_id" "$AUTOMATION_ROOT")
|
|
65
|
+
|
|
66
|
+
previous_attempt=0
|
|
67
|
+
preflight_meta="$evidence_dir/test-preflight.json"
|
|
68
|
+
if [[ -f "$preflight_meta" ]]; then
|
|
69
|
+
previous_attempt="$(jq -er '.attempt' "$preflight_meta")"
|
|
70
|
+
fi
|
|
71
|
+
attempt=$((previous_attempt + 1))
|
|
72
|
+
max_fixes="$(jq -er '.verification.maxPreparationFixes' "$contract")"
|
|
73
|
+
if [[ "$attempt" -gt $((max_fixes + 1)) ]]; then
|
|
74
|
+
automation_die "test preparation retry budget exhausted; revise the contract or abort the task"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
attempt_id="$(printf '%03d' "$attempt")"
|
|
78
|
+
attempt_dir="$evidence_dir/attempts/red-preflight-$attempt_id"
|
|
79
|
+
mkdir -p "$attempt_dir"
|
|
80
|
+
observed_file="$attempt_dir/observed.jsonl"
|
|
81
|
+
: > "$observed_file"
|
|
82
|
+
execution_failures='[]'
|
|
83
|
+
started_at="$(automation_now)"
|
|
84
|
+
target_index=0
|
|
85
|
+
while IFS=$'\t' read -r gradle_task filter; do
|
|
86
|
+
result_file="$attempt_dir/target-$target_index.jsonl"
|
|
87
|
+
log_file="$attempt_dir/target-$target_index.log"
|
|
88
|
+
set +e
|
|
89
|
+
automation_run_classified_focused_test "$gradle_task" "$filter" "$AUTOMATION_ROOT" "$result_file" "$log_file"
|
|
90
|
+
gradle_status=$?
|
|
91
|
+
set -e
|
|
92
|
+
if [[ "$gradle_status" -ne 0 ]]; then
|
|
93
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
94
|
+
--arg task "$gradle_task" --arg filter "$filter" --argjson exitCode "$gradle_status" \
|
|
95
|
+
'$current + [{target: $target, gradleTask: $task, filter: $filter, exitCode: $exitCode}]')"
|
|
96
|
+
fi
|
|
97
|
+
if [[ -s "$result_file" ]]; then
|
|
98
|
+
while IFS= read -r result_line; do
|
|
99
|
+
if ! jq -e . >/dev/null 2>&1 <<< "$result_line"; then
|
|
100
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
101
|
+
'$current + [{target: $target, reason: "invalid structured test output"}]')"
|
|
102
|
+
continue
|
|
103
|
+
fi
|
|
104
|
+
jq -c --argjson target "$target_index" '. + {target: $target}' <<< "$result_line" >> "$observed_file"
|
|
105
|
+
done < "$result_file"
|
|
106
|
+
else
|
|
107
|
+
execution_failures="$(jq -nc --argjson current "$execution_failures" --argjson target "$target_index" \
|
|
108
|
+
'$current + [{target: $target, reason: "no structured test results"}]')"
|
|
109
|
+
fi
|
|
110
|
+
target_index=$((target_index + 1))
|
|
111
|
+
done < <(jq -r '.targetTests[] | [.gradleTask, .filter] | @tsv' "$contract")
|
|
112
|
+
|
|
113
|
+
contract_sha="$(automation_file_sha256 "$contract")"
|
|
114
|
+
baseline_head="$(jq -er '.head' "$evidence_dir/baseline.json")"
|
|
115
|
+
test_diff_sha="$(automation_test_diff_sha "$task_id" "$AUTOMATION_ROOT")"
|
|
116
|
+
finished_at="$(automation_now)"
|
|
44
117
|
|
|
45
118
|
jq -n \
|
|
119
|
+
--slurpfile contract "$contract" \
|
|
120
|
+
--slurpfile observed "$observed_file" \
|
|
46
121
|
--arg taskId "$task_id" \
|
|
47
122
|
--arg startedAt "$started_at" \
|
|
48
|
-
--arg finishedAt "$
|
|
49
|
-
--arg
|
|
50
|
-
--arg
|
|
51
|
-
--arg
|
|
52
|
-
--argjson
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
123
|
+
--arg finishedAt "$finished_at" \
|
|
124
|
+
--arg contractSha256 "$contract_sha" \
|
|
125
|
+
--arg baselineHead "$baseline_head" \
|
|
126
|
+
--arg testDiffSha256 "$test_diff_sha" \
|
|
127
|
+
--argjson attempt "$attempt" \
|
|
128
|
+
--argjson executionFailures "$execution_failures" '
|
|
129
|
+
def failure_matches($decl; $actual):
|
|
130
|
+
($actual.result == "FAILURE") and
|
|
131
|
+
($actual.exceptionType == $decl.expectedFailure.type) and
|
|
132
|
+
(($decl.expectedFailure.messageIncludes // "") as $fragment |
|
|
133
|
+
($fragment == "" or (($actual.exceptionMessage // "") | contains($fragment))));
|
|
134
|
+
($contract[0].verification.cases | map(
|
|
135
|
+
. as $decl |
|
|
136
|
+
[$observed[] | select(.kind == "case" and .target == $decl.test.target and
|
|
137
|
+
.className == $decl.test.className and .name == $decl.test.name)] as $matches |
|
|
138
|
+
(if ($matches | length) == 1 then $matches[0] else null end) as $actual |
|
|
139
|
+
(if ($matches | length) != 1 then false
|
|
140
|
+
elif $decl.before == "pass" then $actual.result == "SUCCESS"
|
|
141
|
+
elif $decl.before == "fail" then failure_matches($decl; $actual)
|
|
142
|
+
elif $actual.result == "SUCCESS" then true
|
|
143
|
+
elif ($decl | has("expectedFailure")) then failure_matches($decl; $actual)
|
|
144
|
+
else false end) as $valid |
|
|
145
|
+
{id: $decl.id, criterion: $decl.criterion, intent: $decl.intent,
|
|
146
|
+
expectedBefore: $decl.before, test: $decl.test, expectedFailure: ($decl.expectedFailure // null),
|
|
147
|
+
matches: ($matches | length), actual: $actual, valid: $valid}
|
|
148
|
+
)) as $cases |
|
|
149
|
+
[$observed[] | select(.kind == "case") as $actual |
|
|
150
|
+
select(any($contract[0].verification.cases[];
|
|
151
|
+
.test.target == $actual.target and .test.className == $actual.className and .test.name == $actual.name) | not)] as $undeclared |
|
|
152
|
+
[$observed[] | select(.kind == "suite")] as $suites |
|
|
153
|
+
($executionFailures | length == 0 and
|
|
154
|
+
($suites | length) >= ($contract[0].targetTests | length) and
|
|
155
|
+
($cases | all(.valid)) and ($undeclared | length == 0) and
|
|
156
|
+
($cases | any(.intent == "change" and .actual.result == "FAILURE"))) as $valid |
|
|
157
|
+
{taskId: $taskId, attempt: $attempt, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
158
|
+
contractSha256: $contractSha256, baselineHead: $baselineHead,
|
|
159
|
+
testDiffSha256: $testDiffSha256, valid: $valid,
|
|
160
|
+
reasonCode: (if $valid then "VALID_RED"
|
|
161
|
+
elif ($executionFailures | length) > 0 then "EXECUTION_FAILURE"
|
|
162
|
+
elif ($undeclared | length) > 0 then "UNDECLARED_TEST_RESULT"
|
|
163
|
+
else "CASE_EXPECTATION_MISMATCH" end),
|
|
164
|
+
summary: {declared: ($cases | length), valid: ([$cases[] | select(.valid)] | length),
|
|
165
|
+
invalid: ([$cases[] | select(.valid | not)] | length),
|
|
166
|
+
expectedRed: ([$cases[] | select(.intent == "change" and .actual.result == "FAILURE")] | length),
|
|
167
|
+
undeclared: ($undeclared | length)},
|
|
168
|
+
cases: $cases, undeclaredCases: $undeclared, suites: $suites,
|
|
169
|
+
executionFailures: $executionFailures}' | automation_record_json "$preflight_meta"
|
|
170
|
+
|
|
171
|
+
cp "$preflight_meta" "$attempt_dir/evaluation.json"
|
|
172
|
+
if [[ "$(jq -r '.valid' "$preflight_meta")" != "true" ]]; then
|
|
173
|
+
reason="$(jq -r '.reasonCode' "$preflight_meta")"
|
|
174
|
+
invalid="$(jq -r '[.cases[] | select(.valid | not) | .id] | join(", ")' "$preflight_meta")"
|
|
175
|
+
automation_die "RED preflight rejected ($reason); invalid cases: ${invalid:-none}; evidence: $preflight_meta"
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
jq '{taskId, contractSha256, baselineHead, testDiffSha256,
|
|
179
|
+
cases: [.cases[] | {id, criterion, intent, test, actual}]}' "$preflight_meta" \
|
|
180
|
+
| automation_record_json "$evidence_dir/test-manifest.json"
|
|
181
|
+
preflight_sha="$(automation_file_sha256 "$preflight_meta")"
|
|
182
|
+
manifest_sha="$(automation_file_sha256 "$evidence_dir/test-manifest.json")"
|
|
183
|
+
jq -n --arg taskId "$task_id" --arg startedAt "$started_at" --arg finishedAt "$finished_at" \
|
|
184
|
+
--arg contractSha256 "$contract_sha" --arg baselineHead "$baseline_head" \
|
|
185
|
+
--arg testDiffSha256 "$test_diff_sha" --arg preflightSha256 "$preflight_sha" \
|
|
186
|
+
--arg manifestSha256 "$manifest_sha" --arg attemptPath "attempts/red-preflight-$attempt_id" \
|
|
187
|
+
--argjson attempt "$attempt" \
|
|
188
|
+
'{taskId: $taskId, schemaVersion: 3, startedAt: $startedAt, finishedAt: $finishedAt,
|
|
189
|
+
contractSha256: $contractSha256, baselineHead: $baselineHead,
|
|
190
|
+
testDiffSha256: $testDiffSha256, preflightSha256: $preflightSha256,
|
|
191
|
+
manifestSha256: $manifestSha256, attempt: $attempt, attemptPath: $attemptPath,
|
|
192
|
+
structuredCasesVerified: true, exitCode: 1}' | automation_record_json "$red_meta"
|
|
193
|
+
|
|
194
|
+
automation_info "$task_id structured RED evidence recorded"
|
|
@@ -79,6 +79,13 @@ jq -r '.nonGoals[] | "- \(.)"' "$report_file"
|
|
|
79
79
|
|
|
80
80
|
printf '\n### P1 · 自动证据是否可信\n\n'
|
|
81
81
|
printf -- '- RED 退出码:`%s`(应为非 0,证明测试先真实失败)\n' "$(jq -r '.evidence.redExitCode' "$report_file")"
|
|
82
|
+
if [[ "$(jq -r '.evidence.structuredRed != null' "$report_file")" == "true" ]]; then
|
|
83
|
+
printf -- '- 结构化 RED:声明 `%s` 个,用例匹配 `%s` 个,额外用例 `%s` 个\n' \
|
|
84
|
+
"$(jq -r '.evidence.structuredRed.summary.declared' "$report_file")" \
|
|
85
|
+
"$(jq -r '.evidence.structuredRed.summary.valid' "$report_file")" \
|
|
86
|
+
"$(jq -r '.evidence.structuredRed.summary.undeclared' "$report_file")"
|
|
87
|
+
jq -r '.evidence.structuredRed.cases[] | " - `\(.id)` · \(.intent) · 修改前应 \(.expectedBefore) · 已核验 \(.valid)"' "$report_file"
|
|
88
|
+
fi
|
|
82
89
|
printf -- '- 质量门:`%s`,第 %s 个编码周期内共运行 %s 次\n' \
|
|
83
90
|
"$(jq -r '.evidence.qualityGate' "$report_file")" \
|
|
84
91
|
"$(jq -r '.evidence.codingCycle' "$report_file")" \
|
|
@@ -21,6 +21,8 @@ workspace_json="$(if [[ -f "$workspace_file" ]]; then jq -c . "$workspace_file";
|
|
|
21
21
|
origin_json="$(if [[ -f "$origin_file" ]]; then jq -c . "$origin_file"; else printf 'null'; fi)"
|
|
22
22
|
baseline_json="$(if [[ -f "$evidence_dir/baseline.json" ]]; then jq -c . "$evidence_dir/baseline.json"; else printf 'null'; fi)"
|
|
23
23
|
red_json="$(if [[ -f "$evidence_dir/red.json" ]]; then jq -c . "$evidence_dir/red.json"; else printf 'null'; fi)"
|
|
24
|
+
preflight_json="$(if [[ -f "$evidence_dir/test-preflight.json" ]]; then jq -c . "$evidence_dir/test-preflight.json"; else printf 'null'; fi)"
|
|
25
|
+
manifest_json="$(if [[ -f "$evidence_dir/test-manifest.json" ]]; then jq -c . "$evidence_dir/test-manifest.json"; else printf 'null'; fi)"
|
|
24
26
|
ready_json="$(if [[ -f "$evidence_dir/ready.json" ]]; then jq -c . "$evidence_dir/ready.json"; else printf 'null'; fi)"
|
|
25
27
|
review_json="$(if [[ -f "$evidence_dir/review.json" ]]; then jq -c . "$evidence_dir/review.json"; else printf 'null'; fi)"
|
|
26
28
|
gate_json=null
|
|
@@ -93,6 +95,8 @@ jq -n \
|
|
|
93
95
|
--argjson origin "$origin_json" \
|
|
94
96
|
--argjson baseline "$baseline_json" \
|
|
95
97
|
--argjson red "$red_json" \
|
|
98
|
+
--argjson testPreflight "$preflight_json" \
|
|
99
|
+
--argjson testManifest "$manifest_json" \
|
|
96
100
|
--argjson ready "$ready_json" \
|
|
97
101
|
--argjson gate "$gate_json" \
|
|
98
102
|
--argjson review "$review_json" \
|
|
@@ -118,6 +122,8 @@ jq -n \
|
|
|
118
122
|
directory: $evidence,
|
|
119
123
|
baseline: $baseline,
|
|
120
124
|
red: $red,
|
|
125
|
+
testPreflight: $testPreflight,
|
|
126
|
+
testManifest: $testManifest,
|
|
121
127
|
ready: $ready,
|
|
122
128
|
latestGate: $gate,
|
|
123
129
|
review: $review,
|
|
@@ -27,7 +27,7 @@ fi
|
|
|
27
27
|
jq -e . "$contract" >/dev/null || automation_die "contract is not valid JSON: $contract"
|
|
28
28
|
|
|
29
29
|
jq -e '
|
|
30
|
-
(.schemaVersion == 1 or .schemaVersion == 2) and
|
|
30
|
+
(.schemaVersion == 1 or .schemaVersion == 2 or .schemaVersion == 3) and
|
|
31
31
|
(.id | type == "string" and test("^TASK-[A-Z0-9-]+$")) and
|
|
32
32
|
(.title | type == "string" and length > 0) and
|
|
33
33
|
.designApproved == true and
|
|
@@ -53,7 +53,51 @@ jq -e '
|
|
|
53
53
|
([.targetTests[].filter] | length == (unique | length)) and
|
|
54
54
|
(.deviceTestsRequired | type == "boolean") and
|
|
55
55
|
(.testPolicy == "required" or .testPolicy == "not-required") and
|
|
56
|
-
(if .testPolicy == "not-required" then (.testPolicyReason | type == "string" and length >= 20) else true end)
|
|
56
|
+
(if .testPolicy == "not-required" then (.testPolicyReason | type == "string" and length >= 20) else true end) and
|
|
57
|
+
(if .schemaVersion == 3 then
|
|
58
|
+
(.acceptanceCriteria | length) as $criterionCount |
|
|
59
|
+
(.targetTests | length) as $targetCount |
|
|
60
|
+
(.verification | type == "object" and
|
|
61
|
+
keys == ["cases", "maxPreparationFixes", "version"] and
|
|
62
|
+
.version == 1 and
|
|
63
|
+
(.maxPreparationFixes | type == "number" and floor == . and . >= 0 and . <= 1) and
|
|
64
|
+
(.cases | type == "array" and length > 0) and
|
|
65
|
+
([.cases[].id] | length == (unique | length)) and
|
|
66
|
+
([.cases[] | [.test.target, .test.className, .test.name] | @json] | length == (unique | length)) and
|
|
67
|
+
(any(.cases[]; .intent == "change")) and
|
|
68
|
+
all(.cases[];
|
|
69
|
+
type == "object" and
|
|
70
|
+
([keys[]] - ["id", "criterion", "intent", "before", "after", "source", "test", "expectedFailure"] | length == 0) and
|
|
71
|
+
(.id | type == "string" and test("^[A-Z][A-Z0-9-]{2,63}$")) and
|
|
72
|
+
(.criterion | type == "number" and floor == . and . >= 1 and . <= $criterionCount) and
|
|
73
|
+
.after == "pass" and
|
|
74
|
+
(.test | type == "object" and keys == ["className", "name", "target"] and
|
|
75
|
+
(.target | type == "number" and floor == . and . >= 0 and . < $targetCount) and
|
|
76
|
+
(.className | type == "string" and length > 0) and
|
|
77
|
+
(.name | type == "string" and length > 0)) and
|
|
78
|
+
(if .intent == "preserve" then
|
|
79
|
+
.before == "pass" and
|
|
80
|
+
(.source == "existingTest" or .source == "baselineCapture" or .source == "measuredFact") and
|
|
81
|
+
(has("expectedFailure") | not)
|
|
82
|
+
elif .intent == "change" then
|
|
83
|
+
.before == "fail" and .source == "userRequirement" and
|
|
84
|
+
(.expectedFailure | type == "object" and
|
|
85
|
+
([keys[]] - ["type", "messageIncludes", "origin"] | length == 0) and
|
|
86
|
+
(.type | type == "string" and length > 0) and
|
|
87
|
+
(.origin | type == "string" and length >= 12) and
|
|
88
|
+
(if has("messageIncludes") then (.messageIncludes | type == "string" and length >= 3) else true end))
|
|
89
|
+
elif .intent == "observe" then
|
|
90
|
+
.before == "observe" and
|
|
91
|
+
(.source == "userRequirement" or .source == "existingTest" or .source == "baselineCapture" or .source == "measuredFact") and
|
|
92
|
+
(if has("expectedFailure") then
|
|
93
|
+
(.expectedFailure | type == "object" and
|
|
94
|
+
([keys[]] - ["type", "messageIncludes", "origin"] | length == 0) and
|
|
95
|
+
(.type | type == "string" and length > 0) and
|
|
96
|
+
(.origin | type == "string" and length >= 12) and
|
|
97
|
+
(if has("messageIncludes") then (.messageIncludes | type == "string" and length >= 3) else true end))
|
|
98
|
+
else true end)
|
|
99
|
+
else false end)))
|
|
100
|
+
else true end)
|
|
57
101
|
' "$contract" >/dev/null || automation_die "contract is missing required fields or violates limits"
|
|
58
102
|
|
|
59
103
|
task_id="$(jq -r '.id' "$contract")"
|
|
@@ -15,6 +15,20 @@ evidence_dir="$(automation_evidence_path "$task_id")"
|
|
|
15
15
|
[[ -f "$evidence_dir/baseline.json" ]] || { automation_die "baseline evidence is missing"; exit 41; }
|
|
16
16
|
[[ -f "$evidence_dir/red.json" ]] || { automation_die "RED evidence is missing"; exit 41; }
|
|
17
17
|
|
|
18
|
+
contract_schema="$(jq -er '.schemaVersion' "$contract")"
|
|
19
|
+
if [[ "$contract_schema" == "3" ]]; then
|
|
20
|
+
preflight_meta="$evidence_dir/test-preflight.json"
|
|
21
|
+
manifest_meta="$evidence_dir/test-manifest.json"
|
|
22
|
+
red_meta="$evidence_dir/red.json"
|
|
23
|
+
[[ -f "$preflight_meta" && -f "$manifest_meta" ]] || { automation_die "structured RED evidence is incomplete"; exit 41; }
|
|
24
|
+
[[ "$(jq -er '.valid' "$preflight_meta")" == "true" ]] || { automation_die "structured RED preflight was not valid"; exit 41; }
|
|
25
|
+
[[ "$(jq -er '.contractSha256' "$red_meta")" == "$(automation_file_sha256 "$contract")" ]] || { automation_die "RED contract binding changed"; exit 41; }
|
|
26
|
+
[[ "$(jq -er '.baselineHead' "$red_meta")" == "$(jq -er '.head' "$evidence_dir/baseline.json")" ]] || { automation_die "RED baseline binding changed"; exit 41; }
|
|
27
|
+
[[ "$(jq -er '.testDiffSha256' "$red_meta")" == "$(automation_test_diff_sha "$task_id" "$AUTOMATION_ROOT")" ]] || { automation_die "tests changed after structured RED"; exit 41; }
|
|
28
|
+
[[ "$(jq -er '.preflightSha256' "$red_meta")" == "$(automation_file_sha256 "$preflight_meta")" ]] || { automation_die "RED preflight evidence changed"; exit 41; }
|
|
29
|
+
[[ "$(jq -er '.manifestSha256' "$red_meta")" == "$(automation_file_sha256 "$manifest_meta")" ]] || { automation_die "RED test manifest changed"; exit 41; }
|
|
30
|
+
fi
|
|
31
|
+
|
|
18
32
|
"$SCRIPT_DIR/scope-gate.sh" "$task_id"
|
|
19
33
|
|
|
20
34
|
automation_run_configured_unit_tests "$contract" "$AUTOMATION_ROOT"
|