@frankzhang2026/opencode-android-orchestrator 1.0.5 → 1.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/CHANGELOG.md +67 -0
- package/README.md +76 -15
- package/dist/config/queue-policy.d.ts.map +1 -1
- package/dist/config/queue-policy.js +1 -2
- package/dist/config/queue-policy.js.map +1 -1
- package/dist/doctor/index.d.ts.map +1 -1
- package/dist/doctor/index.js +14 -3
- package/dist/doctor/index.js.map +1 -1
- package/dist/doctor/installation.d.ts +1 -1
- package/dist/doctor/installation.d.ts.map +1 -1
- package/dist/doctor/installation.js +10 -1
- package/dist/doctor/installation.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/installer/adaptive-templates.d.ts +4 -1
- package/dist/installer/adaptive-templates.d.ts.map +1 -1
- package/dist/installer/adaptive-templates.js +38 -13
- package/dist/installer/adaptive-templates.js.map +1 -1
- package/dist/installer/android-project.d.ts +3 -1
- package/dist/installer/android-project.d.ts.map +1 -1
- package/dist/installer/android-project.js.map +1 -1
- package/dist/installer/capabilities-script.d.ts +2 -0
- package/dist/installer/capabilities-script.d.ts.map +1 -0
- package/dist/installer/capabilities-script.js +77 -0
- package/dist/installer/capabilities-script.js.map +1 -0
- package/dist/installer/gradle-verification.d.ts.map +1 -1
- package/dist/installer/gradle-verification.js +13 -6
- package/dist/installer/gradle-verification.js.map +1 -1
- package/dist/installer/init.d.ts.map +1 -1
- package/dist/installer/init.js +2 -1
- package/dist/installer/init.js.map +1 -1
- package/dist/installer/opencode-config.d.ts +3 -3
- package/dist/installer/opencode-config.js +1 -1
- package/dist/installer/project-capabilities.d.ts +35 -0
- package/dist/installer/project-capabilities.d.ts.map +1 -0
- package/dist/installer/project-capabilities.js +82 -0
- package/dist/installer/project-capabilities.js.map +1 -0
- package/dist/installer/upgrade.d.ts.map +1 -1
- package/dist/installer/upgrade.js +9 -1
- package/dist/installer/upgrade.js.map +1 -1
- package/dist/queue/cli.d.ts.map +1 -1
- package/dist/queue/cli.js +32 -5
- package/dist/queue/cli.js.map +1 -1
- package/dist/queue/executor.d.ts +1 -0
- package/dist/queue/executor.d.ts.map +1 -1
- package/dist/queue/executor.js +155 -11
- package/dist/queue/executor.js.map +1 -1
- package/dist/queue/process-ownership.d.ts +12 -0
- package/dist/queue/process-ownership.d.ts.map +1 -0
- package/dist/queue/process-ownership.js +58 -0
- package/dist/queue/process-ownership.js.map +1 -0
- package/dist/queue/queue.d.ts +25 -1
- package/dist/queue/queue.d.ts.map +1 -1
- package/dist/queue/queue.js +96 -11
- package/dist/queue/queue.js.map +1 -1
- package/dist/queue/service.d.ts.map +1 -1
- package/dist/queue/service.js +19 -2
- package/dist/queue/service.js.map +1 -1
- package/dist/queue/storage.d.ts +3 -1
- package/dist/queue/storage.d.ts.map +1 -1
- package/dist/queue/storage.js +25 -15
- package/dist/queue/storage.js.map +1 -1
- package/dist/queue/supervision.d.ts +38 -0
- package/dist/queue/supervision.d.ts.map +1 -0
- package/dist/queue/supervision.js +247 -0
- package/dist/queue/supervision.js.map +1 -0
- package/dist/queue/tools.d.ts.map +1 -1
- package/dist/queue/tools.js +4 -2
- package/dist/queue/tools.js.map +1 -1
- package/docs/MIGRATION.md +110 -9
- package/docs/QUEUE.md +350 -16
- package/docs/SECURITY.md +5 -2
- package/docs/TROUBLESHOOTING.md +82 -8
- package/package.json +1 -1
- package/templates/.opencode/agents/scheduled-coder.md +14 -1
- package/templates/.opencode/agents/scheduled-planner.md +64 -3
- package/templates/.opencode/skills/scheduled-quality-coder/SKILL.md +29 -3
- package/templates/.opencode/skills/scheduled-quality-orchestrator/SKILL.md +65 -4
- package/templates/.opencode/skills/scheduled-quality-reviewer/SKILL.md +19 -1
- package/templates/AGENTS.md.fragment +10 -4
- package/templates/README.md +3 -1
- package/templates/automation/config.schema.json +212 -22
- package/templates/automation/task-contract.schema.json +573 -49
- package/templates/automation/tasks/TASK-TEMPLATE.json.example +9 -4
- package/templates/automation/verification/collect.init.gradle +98 -0
- package/templates/automation/verification/contract.cjs +93 -0
- package/templates/automation/verification/inventory.cjs +433 -0
- package/templates/automation/verification/project.cjs +172 -0
- package/templates/automation/verification/recovery.cjs +426 -0
- package/templates/scripts/automation/acceptance-report.sh +17 -0
- package/templates/scripts/automation/claim-task.sh +35 -2
- package/templates/scripts/automation/integration-scope-gate.sh +3 -3
- package/templates/scripts/automation/lib.sh +61 -19
- package/templates/scripts/automation/orchestrate-task.sh +7 -1
- package/templates/scripts/automation/preflight.sh +1 -2
- package/templates/scripts/automation/quality-gate.sh +10 -1
- package/templates/scripts/automation/record-red.sh +13 -1
- package/templates/scripts/automation/resume-task.sh +3 -0
- package/templates/scripts/automation/scope-gate.sh +4 -4
- package/templates/scripts/automation/show-acceptance-review.sh +1 -0
- package/templates/scripts/automation/status.sh +32 -0
- package/templates/scripts/automation/submit-review.sh +10 -1
- package/templates/scripts/automation/validate-contract.sh +10 -1
- package/templates/scripts/automation/verify-integration.sh +3 -0
- package/templates/scripts/automation/verify-task.sh +3 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 4,
|
|
3
3
|
"id": "TASK-EXAMPLE-001",
|
|
4
4
|
"title": "Replace with one small, observable behavior change",
|
|
5
5
|
"designApproved": true,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
],
|
|
48
48
|
"targetTests": [
|
|
49
49
|
{
|
|
50
|
-
"gradleTask": "testDebugUnitTest",
|
|
50
|
+
"gradleTask": ":app:testDebugUnitTest",
|
|
51
51
|
"filter": "*ReplaceWithFocusedTest"
|
|
52
52
|
}
|
|
53
53
|
],
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"testPolicy": "required",
|
|
56
56
|
"testPolicyReason": "Behavior changes require a focused regression test",
|
|
57
57
|
"verification": {
|
|
58
|
-
"version":
|
|
58
|
+
"version": 2,
|
|
59
59
|
"maxPreparationFixes": 1,
|
|
60
60
|
"cases": [
|
|
61
61
|
{
|
|
@@ -75,6 +75,11 @@
|
|
|
75
75
|
"origin": "The assertion at the approved behavior call"
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
]
|
|
78
|
+
],
|
|
79
|
+
"inventory": {
|
|
80
|
+
"mode": "focusedBaseline",
|
|
81
|
+
"existingSkips": "reject",
|
|
82
|
+
"emptyBaseline": "reject"
|
|
83
|
+
}
|
|
79
84
|
}
|
|
80
85
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import groovy.json.JsonOutput
|
|
2
|
+
import groovy.json.JsonSlurper
|
|
3
|
+
import org.gradle.api.tasks.testing.Test
|
|
4
|
+
import org.gradle.api.tasks.testing.TestDescriptor
|
|
5
|
+
import org.gradle.api.tasks.testing.TestListener
|
|
6
|
+
import org.gradle.api.tasks.testing.TestResult
|
|
7
|
+
|
|
8
|
+
// All three phases use this collector. Never infer execution from existing XML.
|
|
9
|
+
def request = new JsonSlurper().parse(new File(System.getProperty('orchestrator.inventoryRequest')))
|
|
10
|
+
def outputFile = new File(request.output as String)
|
|
11
|
+
def append = { Map value ->
|
|
12
|
+
synchronized (outputFile) {
|
|
13
|
+
outputFile << JsonOutput.toJson(value + [runId: request.runId]) << '\n'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
append([kind: 'start', phase: request.phase, gradleVersion: gradle.gradleVersion])
|
|
17
|
+
def filtersByTask = request.targets.groupBy { it.gradleTask }.collectEntries { path, targets ->
|
|
18
|
+
[(path): targets.collect { it.filter }.unique().sort()]
|
|
19
|
+
}
|
|
20
|
+
gradle.taskGraph.whenReady { graph ->
|
|
21
|
+
filtersByTask.each { path, filters ->
|
|
22
|
+
def task = graph.allTasks.find { it.path == path }
|
|
23
|
+
if (!(task instanceof Test)) throw new GradleException("Inventory target must resolve to an exact Test task: ${path}")
|
|
24
|
+
def roots = [] as Set
|
|
25
|
+
def classes = task.testClassesDirs.files.collect { it.canonicalFile } as Set
|
|
26
|
+
def sourceSets = task.project.extensions.findByName('sourceSets')
|
|
27
|
+
sourceSets?.each { sourceSet ->
|
|
28
|
+
if (sourceSet.output.classesDirs.files.any { classes.contains(it.canonicalFile) }) {
|
|
29
|
+
roots.addAll(sourceSet.allSource.srcDirs)
|
|
30
|
+
roots.addAll(sourceSet.resources.srcDirs)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// AGP variants expose the source providers actually used by each unit test.
|
|
34
|
+
def android = task.project.extensions.findByName('android')
|
|
35
|
+
if (android != null && android.hasProperty('unitTestVariants')) {
|
|
36
|
+
android.unitTestVariants.each { variant ->
|
|
37
|
+
if (task.name == "test${variant.name.capitalize()}") {
|
|
38
|
+
variant.sourceSets.each { provider ->
|
|
39
|
+
roots.addAll(provider.javaDirectories)
|
|
40
|
+
roots.addAll(provider.resourcesDirectories)
|
|
41
|
+
if (provider.hasProperty('kotlinDirectories')) roots.addAll(provider.kotlinDirectories)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Modern AGP also exposes variant-specific test source sets through its DSL.
|
|
47
|
+
// Only unit-test providers are classified here, never production providers.
|
|
48
|
+
if (android != null && android.hasProperty('sourceSets')) {
|
|
49
|
+
android.sourceSets.findAll { it.name == 'test' || it.name.startsWith('test') && it.name.length() > 4 && Character.isUpperCase(it.name.charAt(4)) }.each { sourceSet ->
|
|
50
|
+
roots.addAll(sourceSet.java.srcDirs)
|
|
51
|
+
roots.addAll(sourceSet.resources.srcDirs)
|
|
52
|
+
if (sourceSet.hasProperty('kotlin')) roots.addAll(sourceSet.kotlin.srcDirs)
|
|
53
|
+
roots.addAll(sourceSet.assets.srcDirs)
|
|
54
|
+
roots.addAll(sourceSet.res.srcDirs)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
append([kind: 'task', taskPath: path, filters: filters,
|
|
58
|
+
sourceRoots: roots.collect { it.absoluteFile.toPath().normalize().toString() }.sort(),
|
|
59
|
+
testClassesDirs: classes.collect { it.toString() }.sort()])
|
|
60
|
+
task.outputs.upToDateWhen { false }
|
|
61
|
+
task.outputs.doNotCacheIf('Orchestrator inventory requires actual test execution') { true }
|
|
62
|
+
task.ignoreFailures = true
|
|
63
|
+
task.failFast = false
|
|
64
|
+
if (task.hasProperty('dryRun')) task.dryRun = false
|
|
65
|
+
// An approved new test class need not exist before Coder starts.
|
|
66
|
+
task.filter.failOnNoMatchingTests = request.phase != 'baseline'
|
|
67
|
+
if (task.hasProperty('failOnNoDiscoveredTests')) task.failOnNoDiscoveredTests = request.phase != 'baseline'
|
|
68
|
+
task.filter.setIncludePatterns(*(filters as String[]))
|
|
69
|
+
def reportDir = new File(outputFile.parentFile, 'reports/' + path.substring(1).replace(':', '/'))
|
|
70
|
+
task.reports.junitXml.required.set(true)
|
|
71
|
+
task.reports.junitXml.outputLocation.set(reportDir)
|
|
72
|
+
task.addTestListener(new TestListener() {
|
|
73
|
+
void beforeSuite(TestDescriptor descriptor) {}
|
|
74
|
+
void beforeTest(TestDescriptor descriptor) {}
|
|
75
|
+
void afterTest(TestDescriptor descriptor, TestResult result) {
|
|
76
|
+
def failures = result.exceptions.collect { failure ->
|
|
77
|
+
[type: failure.class.name, message: failure.message ?: '', stack: failure.stackTrace.collect { it.toString() }]
|
|
78
|
+
}
|
|
79
|
+
append([kind: 'case', taskPath: path, className: descriptor.className ?: '',
|
|
80
|
+
name: descriptor.name ?: '', displayName: descriptor.displayName,
|
|
81
|
+
result: result.resultType.toString(), failures: failures])
|
|
82
|
+
}
|
|
83
|
+
void afterSuite(TestDescriptor descriptor, TestResult result) {
|
|
84
|
+
if (descriptor.parent == null) append([kind: 'suite', taskPath: path,
|
|
85
|
+
tests: result.testCount, failures: result.failedTestCount, skipped: result.skippedTestCount])
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
gradle.taskGraph.afterTask { task, state ->
|
|
91
|
+
if (filtersByTask.containsKey(task.path)) append([kind: 'taskEnd', taskPath: task.path,
|
|
92
|
+
executed: state.executed, skipped: state.skipped, noSource: state.noSource,
|
|
93
|
+
upToDate: state.upToDate, skipMessage: state.skipMessage,
|
|
94
|
+
failure: state.failure == null ? null : state.failure.toString()])
|
|
95
|
+
}
|
|
96
|
+
gradle.buildFinished { result ->
|
|
97
|
+
append([kind: 'end', failure: result.failure == null ? null : result.failure.toString()])
|
|
98
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// One V4/V5 validator is used by both the queue and the installed shell entry.
|
|
4
|
+
const assert = (condition, message) => { if (!condition) throw new Error(message); };
|
|
5
|
+
const object = value => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
const text = value => typeof value === "string" && value.length > 0 && !/[\u0000\r\n]/.test(value);
|
|
7
|
+
const fields = (value, required, optional = []) => object(value) && required.every(key => Object.hasOwn(value, key)) &&
|
|
8
|
+
Object.keys(value).every(key => required.includes(key) || optional.includes(key));
|
|
9
|
+
const identity = value => JSON.stringify([value.taskPath, value.className, value.name]);
|
|
10
|
+
function matchesPath(pattern, path) {
|
|
11
|
+
const expression = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
12
|
+
.replace(/\*\*/g, "\u0001").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")
|
|
13
|
+
.replace(/\u0001\//g, "(?:.*/)?").replace(/\u0001/g, ".*");
|
|
14
|
+
return new RegExp(`^${expression}${pattern.endsWith("/") ? ".*" : ""}$`).test(path);
|
|
15
|
+
}
|
|
16
|
+
function validateContract(contract, config) {
|
|
17
|
+
assert(fields(contract, ["schemaVersion", "id", "title", "designApproved", "planPath", "ambiguityPolicy",
|
|
18
|
+
"maxFixLoops", "maxChangedFiles", "allowedPaths", "forbiddenPaths", "allowedWorkflowSkills",
|
|
19
|
+
"acceptanceCriteria", "nonGoals", "targetTests", "deviceTestsRequired", "testPolicy", "verification"],
|
|
20
|
+
contract.schemaVersion === 8 ? ["testPolicyReason", "recovery", "execution", "stageRecovery", "continuity"] : contract.schemaVersion === 7 ? ["testPolicyReason", "recovery", "execution", "stageRecovery"] : contract.schemaVersion === 6 ? ["testPolicyReason", "recovery", "execution"] : contract.schemaVersion === 5 ? ["testPolicyReason", "recovery"] : ["testPolicyReason"]), "Invalid inventory contract fields");
|
|
21
|
+
assert([4, 5, 6, 7, 8].includes(contract.schemaVersion) && /^TASK-[A-Z0-9-]+$/.test(contract.id), "Invalid inventory contract identity");
|
|
22
|
+
if (contract.schemaVersion >= 5) require("./recovery.cjs").validateRecoveryPolicy(contract.recovery);
|
|
23
|
+
if (contract.schemaVersion >= 6) require("./recovery.cjs").validateExecutionPolicy(contract.execution);
|
|
24
|
+
if (contract.schemaVersion >= 7) require("./recovery.cjs").validateStageRecoveryPolicy(contract.stageRecovery);
|
|
25
|
+
if (contract.schemaVersion === 8) {
|
|
26
|
+
const policy = contract.continuity;
|
|
27
|
+
assert(fields(policy, ["version", "isolatedAutoIntegration", "planningRefresh", "planningInputs"]) && policy.version === 1 &&
|
|
28
|
+
typeof policy.isolatedAutoIntegration === "boolean" && ["reject", "completedQueueTasks"].includes(policy.planningRefresh), "Invalid continuity policy");
|
|
29
|
+
assert(Array.isArray(policy.planningInputs) && policy.planningInputs.length > 0 && policy.planningInputs.length <= 128 &&
|
|
30
|
+
new Set(policy.planningInputs).size === policy.planningInputs.length && policy.planningInputs.every(p =>
|
|
31
|
+
typeof p === "string" && p.length <= 512 && /^[A-Za-z0-9._/?*-]+$/.test(p) &&
|
|
32
|
+
!p.startsWith("/") && !p.includes("..") && p.split("/").every(part => part && part !== "." && part !== ".git")), "Invalid planning inputs");
|
|
33
|
+
}
|
|
34
|
+
assert(text(contract.title) && contract.designApproved === true && contract.ambiguityPolicy === "BLOCKED", "An approved bounded design is required");
|
|
35
|
+
assert(contract.planPath === `docs/plans/${contract.id}.md`, "Invalid task plan path");
|
|
36
|
+
assert(Number.isInteger(contract.maxFixLoops) && contract.maxFixLoops >= 0 && contract.maxFixLoops <= 1, "Invalid fix-loop limit");
|
|
37
|
+
assert(Number.isInteger(contract.maxChangedFiles) && contract.maxChangedFiles >= 1 && contract.maxChangedFiles <= 12, "Invalid file limit");
|
|
38
|
+
for (const key of ["allowedPaths", "forbiddenPaths", "acceptanceCriteria", "nonGoals"])
|
|
39
|
+
assert(Array.isArray(contract[key]) && contract[key].length > 0 && contract[key].every(text), `Invalid ${key}`);
|
|
40
|
+
for (const path of [...contract.allowedPaths, ...contract.forbiddenPaths])
|
|
41
|
+
assert(/^[A-Za-z0-9._/?*-]+$/.test(path) && !path.startsWith("/") && !path.includes(".."), "Unsafe contract path");
|
|
42
|
+
for (const path of config.protectedPaths) {
|
|
43
|
+
assert(!contract.allowedPaths.some(pattern => matchesPath(pattern, path)), `Allowed paths overlap protected path: ${path}`);
|
|
44
|
+
assert(contract.forbiddenPaths.some(pattern => matchesPath(pattern, path)), `Forbidden paths must cover: ${path}`);
|
|
45
|
+
}
|
|
46
|
+
assert(JSON.stringify(contract.allowedWorkflowSkills) === JSON.stringify(["test-driven-development", "systematic-debugging", "verification-before-completion"].map(name => `android-orchestrator-${name}`)), "Invalid workflow skill allowlist");
|
|
47
|
+
assert(typeof contract.deviceTestsRequired === "boolean" && contract.testPolicy === "required", "Inventory verification requires tests");
|
|
48
|
+
assert(!contract.deviceTestsRequired || config.gradleVerification.deviceTestTasks?.length > 0,
|
|
49
|
+
"Device tests are required by this contract but no device test tasks are available");
|
|
50
|
+
assert(config.unitTestsEnabled !== false, "Inventory verification requires unitTestsEnabled=true");
|
|
51
|
+
assert(!Object.hasOwn(contract, "testPolicyReason") || typeof contract.testPolicyReason === "string", "Invalid test policy reason");
|
|
52
|
+
assert(Array.isArray(contract.targetTests) && contract.targetTests.length > 0, "Focused tests are required");
|
|
53
|
+
const targets = new Set();
|
|
54
|
+
for (const target of contract.targetTests) {
|
|
55
|
+
assert(fields(target, ["gradleTask", "filter"]) && /^(?::[A-Za-z0-9_.-]+)+$/.test(target.gradleTask), "Inventory verification requires a fully qualified Test task path");
|
|
56
|
+
assert(config.gradleVerification.focusedTestTasks.includes(target.gradleTask), "Focused test task is not configured");
|
|
57
|
+
assert(typeof target.filter === "string" && /^[A-Za-z0-9_.#$*-]+$/.test(target.filter), "Unsafe test filter");
|
|
58
|
+
const key = JSON.stringify([target.gradleTask, target.filter]);
|
|
59
|
+
assert(!targets.has(key), "Focused test targets must be unique"); targets.add(key);
|
|
60
|
+
}
|
|
61
|
+
const verification = contract.verification;
|
|
62
|
+
assert(fields(verification, ["version", "maxPreparationFixes", "cases", "inventory"]) && verification.version === 2, "Inventory verification requires verification version 2 with explicit inventory policy");
|
|
63
|
+
assert(Number.isInteger(verification.maxPreparationFixes) && verification.maxPreparationFixes >= 0 && verification.maxPreparationFixes <= 1, "Invalid preparation retry budget");
|
|
64
|
+
const policy = verification.inventory;
|
|
65
|
+
assert(fields(policy, ["mode", "existingSkips", "emptyBaseline"]) && policy.mode === "focusedBaseline" &&
|
|
66
|
+
["reject", "preserve"].includes(policy.existingSkips) && ["reject", "allow"].includes(policy.emptyBaseline), "Invalid inventory coverage, existing-skip or empty-baseline policy");
|
|
67
|
+
assert(Array.isArray(verification.cases) && verification.cases.length > 0, "Behavior cases are required");
|
|
68
|
+
const ids = new Set(), identities = new Set();
|
|
69
|
+
for (const item of verification.cases) {
|
|
70
|
+
assert(fields(item, ["id", "criterion", "intent", "before", "after", "source", "test"], ["expectedFailure"]), "Invalid behavior case fields");
|
|
71
|
+
assert(typeof item.id === "string" && /^[A-Z][A-Z0-9-]{2,63}$/.test(item.id) && !ids.has(item.id), "Behavior IDs must be unique and stable"); ids.add(item.id);
|
|
72
|
+
assert(Number.isInteger(item.criterion) && item.criterion >= 1 && item.criterion <= contract.acceptanceCriteria.length && item.after === "pass", "Invalid behavior criterion or GREEN expectation");
|
|
73
|
+
assert(fields(item.test, ["target", "className", "name"]) && Number.isInteger(item.test.target) && item.test.target >= 0 &&
|
|
74
|
+
item.test.target < contract.targetTests.length && text(item.test.className) && text(item.test.name), "Invalid exact test identity (include each parameter instance)");
|
|
75
|
+
const key = identity({ taskPath: contract.targetTests[item.test.target].gradleTask, ...item.test });
|
|
76
|
+
assert(!identities.has(key), "Behavior identities overlap across target filters"); identities.add(key);
|
|
77
|
+
if (item.intent === "preserve") assert(item.before === "pass" && ["existingTest", "baselineCapture", "measuredFact"].includes(item.source) && !Object.hasOwn(item, "expectedFailure"), "Preserved behavior must pass on a measured baseline");
|
|
78
|
+
else if (item.intent === "change") assert(item.before === "fail" && item.source === "userRequirement" && object(item.expectedFailure), "Changed behavior requires an approved expected failure");
|
|
79
|
+
else assert(item.intent === "observe" && item.before === "observe" && ["userRequirement", "existingTest", "baselineCapture", "measuredFact"].includes(item.source), "Invalid observed behavior");
|
|
80
|
+
if (Object.hasOwn(item, "expectedFailure")) {
|
|
81
|
+
const failure = item.expectedFailure;
|
|
82
|
+
assert(fields(failure, ["type", "origin"], ["messageIncludes"]) && text(failure.type) && text(failure.origin) && failure.origin.length >= 12 &&
|
|
83
|
+
(!Object.hasOwn(failure, "messageIncludes") || (text(failure.messageIncludes) && failure.messageIncludes.length >= 3)), "Invalid expected failure");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
assert(verification.cases.some(item => item.intent === "change"), "At least one changed behavior must provide genuine RED");
|
|
87
|
+
assert(!/replace with|TASK-EXAMPLE|\btodo\b|\btbd\b|placeholder/i.test(JSON.stringify(contract)), "Contract contains placeholders");
|
|
88
|
+
}
|
|
89
|
+
module.exports = { validateContract, identity, matchesPath, assert };
|
|
90
|
+
if (require.main === module) {
|
|
91
|
+
try { const fs = require("node:fs"); validateContract(JSON.parse(fs.readFileSync(process.argv[2], "utf8")), JSON.parse(fs.readFileSync(process.argv[3], "utf8"))); }
|
|
92
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
93
|
+
}
|