easy-coding-harness 0.10.0-beta.1 → 0.10.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "0.10.0-beta.1",
3
+ "version": "0.10.0-beta.2",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -85,9 +85,11 @@ initialization standard. Check each item:
85
85
  and non-empty?
86
86
  - **Project profile**: `project.yaml` exists with `mode` and `test` fields (ec-init owns it;
87
87
  `config.yaml` is CLI-owned — not ec-init's concern)?
88
- - **TDD runtime contract**: CLI-owned `config.yaml` has schema 4 TDD defaults and
89
- `.easy-coding/tools/easy_coding_java_coverage.py` exists? Missing fields or tool mean
90
- `easy-coding upgrade` is required; ec-init must not repair them directly.
88
+ - **TDD runtime contract**: CLI-owned `config.yaml` has schema 5 TDD defaults and both
89
+ `.easy-coding/tools/easy_coding_java_coverage.py` and
90
+ `.easy-coding/tools/easy_coding_tdd_readiness.py` exist? Missing fields or tools mean
91
+ `easy-coding upgrade` is required; ec-init must not repair them directly. Project-specific
92
+ JaCoCo/GitLab readiness remains owned by `ec-tdd-init`, not `ec-init`.
91
93
  - **Platform hook config freshness**: for each installed platform in `.easy-coding/config.yaml`,
92
94
  read the platform hook config and verify managed Easy Coding hook commands use the portable
93
95
  relative hook launcher bound to this project root's `project.id`. If `project.id` is missing,
@@ -14,7 +14,7 @@ What lives in each platform directory and how the three platforms differ.
14
14
  | Sub-agent defs | `.claude/agents/*.md` | `.codex/agents/*.toml` | `.qoder/agents/*.md` |
15
15
  | CN variant | — | — | `.qodercn/` |
16
16
 
17
- Each skill is installed as `{skills-dir}/ec-{name}/SKILL.md`. The 14 skills are
17
+ Each skill is installed as `{skills-dir}/ec-{name}/SKILL.md`. The 15 skills are
18
18
  platform-agnostic — one template, resolved per platform at write time. The agent's native
19
19
  discovery surfaces them under `/ec-` or `$ec-`.
20
20
 
@@ -96,8 +96,14 @@ Code tasks require `test-strategy.md`; explicit `doc`, `analysis`, and `report`
96
96
  ## Optional Java TDD analysis
97
97
 
98
98
  Read `effective_tdd_enabled` and `effective_tdd_coverage_threshold` from the state snapshot.
99
+ For a `type=tdd-init` task, treat frozen TDD as off even if the project/session requests it. That
100
+ task is the sole exception allowed to inspect and plan build/CI coverage infrastructure while TDD
101
+ is off. Its scope is infrastructure only: never plan historical business-test backfill or a
102
+ repository-wide coverage target, and explicitly record `coverage scope: changed production lines`.
103
+
99
104
  When TDD is disabled, stop here: do not inspect GitLab CI or JaCoCo, do not add TDD fields or
100
- extra tests, and do not strengthen the selected Workflow Mode's ordinary acceptance depth.
105
+ extra tests, and do not strengthen the selected Workflow Mode's ordinary acceptance depth. This
106
+ zero-cost rule applies to ordinary tasks, not the explicit `tdd-init` infrastructure task above.
101
107
 
102
108
  When TDD is enabled for a Java code task, make `test-strategy.md` record:
103
109
 
@@ -110,6 +116,8 @@ When TDD is enabled for a Java code task, make `test-strategy.md` record:
110
116
  - `.gitlab-ci.yml` and included configuration, the TEST-stage job, JUnit/JaCoCo artifacts, and an
111
117
  equivalent changed-line gate. Reuse a gate only when its threshold is at least the configured
112
118
  value; otherwise include the CI change in the confirmed implementation scope.
119
+ - current `tdd_readiness_status=ready`; if missing or drifted, stop before IMPLEMENT and route to
120
+ `ec-tdd-init`. Never plan to initialize CI inside an already-enabled TDD feature task.
113
121
 
114
122
  The state API mechanically freezes current Git `HEAD` per repository into `task.tdd_baselines`
115
123
  when ANALYSIS advances to IMPLEMENT. Plan both local and GitLab commands with that exact SHA and
@@ -18,7 +18,15 @@ Call `snapshot` and show project, session, effective, and frozen task values for
18
18
 
19
19
  Use the returned fields directly, including `project_tdd_enabled`, `session_tdd_enabled`,
20
20
  `effective_tdd_enabled`, their threshold counterparts, `task_tdd_enabled`, and the task's
21
- per-repository `task_tdd_baselines` frozen state.
21
+ per-repository `task_tdd_baselines` frozen state. When `tdd_readiness_status=not_checked` because
22
+ TDD is off, explicitly run the read-only readiness command below before showing readiness:
23
+
24
+ ```bash
25
+ python3 .easy-coding/tools/easy_coding_tdd_readiness.py --cwd . check
26
+ ```
27
+
28
+ This explicit configuration-panel check is the only disabled-mode readiness scan; ordinary hooks
29
+ must not inspect build or CI files while TDD is off.
22
30
 
23
31
  Explain precedence as `session override > project config > defaults`. Defaults are Approval
24
32
  `guard`, Workflow `adaptive`, TDD disabled, and TDD changed-line coverage threshold 90%. An active
@@ -29,6 +37,7 @@ changes affect future tasks and ANALYSIS only.
29
37
 
30
38
  Use `easy-coding config` for project settings. The CLI confirms one atomic update of Approval,
31
39
  Workflow, TDD, and (when enabled) the threshold. The threshold must be an integer from 1 to 100.
40
+ Enabling TDD is rejected atomically unless `ec-tdd-init` readiness is currently `ready`.
32
41
 
33
42
  ## Session configuration
34
43
 
@@ -52,3 +61,8 @@ Turning TDD off must preserve the existing Fast/Standard/Strict test depth exact
52
61
  CI, request JaCoCo, add TDD artifacts, run coverage commands, or strengthen acceptance criteria.
53
62
  When TDD is on, explain that it applies only to Java code tasks and activates RED/GREEN/REFACTOR,
54
63
  TDD review, local changed-line JaCoCo coverage, and GitLab TEST-stage gate planning.
64
+
65
+ Before any project/session enable action, require `tdd_readiness_status=ready`. If it is not ready,
66
+ offer only `ec-tdd-init` or cancellation; never offer or persist "enable now, initialize later".
67
+ Readiness means infrastructure can measure future changed production lines. It does not certify
68
+ repository-wide coverage and does not require tests for unchanged historical code.
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: ec-tdd-init
3
+ description: Initialize or refresh Java changed-line TDD coverage infrastructure before TDD can be enabled.
4
+ ---
5
+
6
+ # ec-tdd-init — Java changed-line gate initialization
7
+
8
+ Communicate in the user's language. This skill owns TDD infrastructure readiness, not historical
9
+ test-debt cleanup. It must never bulk-generate tests for existing business code, require
10
+ repository-wide coverage, or modify production behavior merely to raise coverage.
11
+
12
+ ## Non-circular ordering
13
+
14
+ The only legal order is:
15
+
16
+ ```text
17
+ TDD off -> initialize infrastructure -> readiness ready -> user explicitly enables TDD
18
+ ```
19
+
20
+ Run this skill as a dedicated code task with `type=tdd-init`. The state API always freezes that
21
+ task with `tdd_enabled=false`, even when a legacy project/session setting or a suspended task has
22
+ TDD enabled. Never offer "enable now and initialize later". Never enable TDD automatically after
23
+ initialization.
24
+
25
+ ## Read-only preflight
26
+
27
+ First run:
28
+
29
+ ```bash
30
+ python3 .easy-coding/tools/easy_coding_tdd_readiness.py --cwd . check
31
+ ```
32
+
33
+ If it returns `ready`, report the recorded build/CI contract and stop without creating a task.
34
+ The user may then use `ec-config` or `easy-coding config` to enable TDD.
35
+
36
+ If it returns `needs_init`, inspect only the infrastructure needed to form a confirmed plan:
37
+
38
+ - Maven/Gradle files and the existing JUnit runner;
39
+ - JaCoCo XML generation configuration;
40
+ - `.gitlab-ci.yml` and its repository-local include chain;
41
+ - TEST-stage job, JUnit/JaCoCo artifacts, and invocation of
42
+ `.easy-coding/tools/easy_coding_java_coverage.py` with task-supplied baseline/threshold values.
43
+
44
+ Do not measure current whole-project coverage. A project with no historical business tests may
45
+ still become ready when the test runner, JaCoCo reporting, and parameterized changed-line gate
46
+ are functional.
47
+
48
+ ## Initialization task
49
+
50
+ After the user confirms the exact infrastructure scope, create one task and route it through the
51
+ ordinary workflow:
52
+
53
+ ```bash
54
+ {{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py create-task \
55
+ --task-id <safe-unique-id> --type tdd-init \
56
+ --title "Initialize Java changed-line TDD infrastructure" \
57
+ --agent <agent-id> --session-file <P>
58
+ ```
59
+
60
+ ANALYSIS may plan build files, GitLab CI files, common scripts, and the readiness receipt. It must
61
+ state `historical coverage required: no` and `coverage scope: changed production lines since each
62
+ future task baseline`. IMPLEMENT changes infrastructure only. It must not add tests whose sole
63
+ purpose is to cover unchanged production code.
64
+
65
+ The reusable GitLab job must consume a baseline SHA and threshold supplied for the future task;
66
+ do not hardcode the initialization commit or the default 90% threshold. The same Python coverage
67
+ tool must be used locally and remotely.
68
+
69
+ ## Readiness receipt and verification
70
+
71
+ At the end of IMPLEMENT, after the infrastructure files are stable, record their fingerprints.
72
+ The receipt is part of the implementation and must exist before REVIEW so review/verification
73
+ fingerprints do not change after review. The recorder automatically includes the harness-managed
74
+ `.easy-coding/tools/easy_coding_java_coverage.py` fingerprint:
75
+
76
+ ```bash
77
+ python3 .easy-coding/tools/easy_coding_tdd_readiness.py --cwd . record \
78
+ --build-file <pom.xml-or-build.gradle> [--build-file <included-build-file>]... \
79
+ --ci-file .gitlab-ci.yml [--ci-file <repository-local-include>]... \
80
+ --coverage-report <jacoco-xml-pattern> [--coverage-report <pattern>]... \
81
+ --gate-command "python3 .easy-coding/tools/easy_coding_java_coverage.py check --base \$EASY_CODING_TDD_BASE_SHA --threshold \$EASY_CODING_TDD_THRESHOLD" \
82
+ --agent <agent-id>
83
+ ```
84
+
85
+ REVIEW includes the receipt and its declared infrastructure boundary. VERIFICATION runs the
86
+ frozen Workflow Mode's applicable build/test/CI syntax checks, then performs only the read-only
87
+ readiness check:
88
+
89
+ ```bash
90
+ python3 .easy-coding/tools/easy_coding_tdd_readiness.py --cwd . check
91
+ ```
92
+
93
+ The `VERIFICATION -> MEMORY` gate requires the final check to return `ready`. If any recorded
94
+ build or CI file changes after the receipt was created, readiness becomes `needs_init`; return to
95
+ IMPLEMENT, refresh the receipt, and repeat REVIEW before verifying again. Rerun this skill when
96
+ the same drift occurs after task completion.
97
+
98
+ After completion, tell the user that TDD remains off and provide the explicit project/session
99
+ enable route. Do not treat readiness as consent to enable it.
@@ -26,7 +26,9 @@ Read-only tasks never enter this stage. Obtain fresh fingerprints before running
26
26
  - `strict`: run the project's full applicable lint, typecheck, test, and build gates.
27
27
 
28
28
  These rules remain unchanged when frozen TDD is off: do not discover JaCoCo reports, run the
29
- coverage tool, inspect GitLab, or add a coverage record.
29
+ coverage tool, inspect GitLab, or add a coverage record. The explicit `type=tdd-init` task is an
30
+ infrastructure exception: run its planned build/CI syntax checks and readiness tool, but do not
31
+ measure repository-wide coverage or append TDD coverage evidence for unchanged production code.
30
32
 
31
33
  When frozen TDD is on, first run the planned Java unit command and generate JaCoCo XML, then run
32
34
  the same deterministic gate intended for GitLab:
@@ -102,6 +104,12 @@ appended only after the remote job succeeds and must also include:
102
104
  Both records must preserve the same task-frozen baseline and threshold. A local-only result,
103
105
  pending/failed pipeline, missing job identity, or synthetic remote pass cannot satisfy MEMORY.
104
106
 
107
+ For `type=tdd-init`, the infrastructure receipt must already have been recorded during IMPLEMENT
108
+ and reviewed with the rest of the implementation. Run only `easy_coding_tdd_readiness.py check`
109
+ here. If it reports drift, return to IMPLEMENT to refresh the receipt and repeat REVIEW; never
110
+ rewrite it inside VERIFICATION. The state gate requires `ready` before MEMORY. This does not
111
+ enable TDD; report the explicit `ec-config`/`easy-coding config` next step.
112
+
105
113
  - Every must-test item has an executed check.
106
114
  - Bug fixes include a regression test when project infrastructure exists.
107
115
  - Present changed scope, commands, results, and unverified items.
@@ -39,8 +39,11 @@ selection and reasons, allows the user to change it within the risk floor, and f
39
39
  ANALYSIS -> IMPLEMENT is applied.
40
40
 
41
41
  TDD resolves with the same session-over-project precedence and freezes its enabled flag and
42
- threshold on ANALYSIS -> IMPLEMENT. When off, it must add no CI scan, artifacts, commands,
43
- coverage work, or stronger acceptance. Use `ec-config` for all mode configuration.
42
+ threshold on ANALYSIS -> IMPLEMENT. It may be enabled only after `ec-tdd-init` readiness passes;
43
+ there is no enabled-but-pending-initialization state. A dedicated `tdd-init` task always freezes
44
+ TDD off so it can create or repair the required infrastructure without circular gating. When off,
45
+ ordinary tasks add no CI scan, artifacts, commands, coverage work, or stronger acceptance. Use
46
+ `ec-config` for all mode configuration.
44
47
 
45
48
  `confirm` and `auto` do not hide the proposal: show it in the plan. Confirm waits for that one
46
49
  plan decision; Auto continues immediately. Both remove later waiting, not quality gates.
@@ -87,7 +90,10 @@ plan decision; Auto continues immediately. Both remove later waiting, not qualit
87
90
 
88
91
  When multiple selected tasks depend on the same target, disambiguate creation evidence with
89
92
  `<source-task-id>-><dependency-task-id>=<evidence>`.
90
- 4. Match the user's intent against `current_task` and the active task list before resuming.
93
+ 4. When the user explicitly invokes `ec-tdd-init`, let that skill own preflight and create a
94
+ `type=tdd-init` code task only after scope confirmation. Do not reinterpret it as an ordinary
95
+ TDD-enabled feature task and do not require readiness before creating it.
96
+ 5. Match the user's intent against `current_task` and the active task list before resuming.
91
97
  If the user names or clearly matches another task, confirm the switch and call
92
98
  `claim-task --task-id <id> --agent <agent-id> --session-file <P>`. Do not execute task A
93
99
  under task B's request.
@@ -99,10 +105,10 @@ plan decision; Auto continues immediately. Both remove later waiting, not qualit
99
105
  to the requested deliverable. Feature, bugfix, refactor, performance, and workflow changes
100
106
  are code tasks. Use `doc`, `analysis`, or `report` only when the user explicitly requested
101
107
  a no-code deliverable; never downgrade a code request to the read-only completion path.
102
- 5. Resume the matched/current task, then load only state-relevant assets. Do not read five full
108
+ 6. Resume the matched/current task, then load only state-relevant assets. Do not read five full
103
109
  memories at every startup; ANALYSIS searches memory metadata and opens relevant entries on
104
110
  demand.
105
- 6. If another Agent last owned the task, summarize the stored handoff before continuing.
111
+ 7. If another Agent last owned the task, summarize the stored handoff before continuing.
106
112
 
107
113
  ## Stage dispatch
108
114
 
@@ -33,7 +33,7 @@ Trigger Easy Coding skills with your platform prefix — Codex: `$ec-*`, Qoder:
33
33
  - `ec-brainstorming` — design exploration before building (hard design gate)
34
34
  - `ec-analysis` `ec-implementing` `ec-reviewing` `ec-verification` — workflow stages
35
35
  - `ec-memory` — short/long memory archive
36
- - `ec-task-management` — task lifecycle panel · `ec-config` — Approval/Workflow/TDD settings · `ec-task-close` — interrupt a task
36
+ - `ec-task-management` — task lifecycle panel · `ec-config` — Approval/Workflow/TDD settings · `ec-tdd-init` — Java changed-line gate initialization · `ec-task-close` — interrupt a task
37
37
  - `ec-no-harness` — bypass only Easy Coding for the current session
38
38
  - `ec-git` — git discipline · `ec-meta` — understand/customize the harness
39
39
 
@@ -49,9 +49,12 @@ First run `ec-init`; daily work goes through `ec-workflow`.
49
49
  evidence gates.
50
50
  - TDD is session override > project `behavior.tdd_enabled` > `false`; its changed-line threshold
51
51
  is session override > project `behavior.tdd_coverage_threshold` > `90`. ANALYSIS -> IMPLEMENT
52
- freezes both. Disabled TDD adds no CI scan, JaCoCo work, commands, artifacts, or stronger gates.
53
- Enabled TDD applies only to Java code tasks and requires lifecycle, review, local coverage, and
54
- GitLab TEST-stage gate evidence.
52
+ freezes both. TDD may be enabled only after `ec-tdd-init` records valid infrastructure readiness;
53
+ there is no enable-now/init-later state. The dedicated `tdd-init` task always freezes TDD off and
54
+ initializes only changed-line coverage infrastructure, never historical business-test coverage.
55
+ Disabled TDD adds no CI scan, JaCoCo work, commands, artifacts, or stronger gates. Enabled TDD
56
+ applies only to Java code tasks and requires lifecycle, review, local coverage, and GitLab
57
+ TEST-stage evidence for production lines changed since the task baseline.
55
58
  - Confirmation-required edges use `pending_transition`; automatic edges use the restricted
56
59
  `auto-transition` API. A read-only task creates no test-strategy.md, never enters REVIEW,
57
60
  VERIFICATION, or MEMORY, and writes no task memory.
@@ -31,7 +31,7 @@ platform prefixes such as `/` or `$`. If no status line is injected, do not inve
31
31
  - `/ec-brainstorming` — design exploration before building (hard design gate)
32
32
  - `/ec-analysis` `/ec-implementing` `/ec-reviewing` `/ec-verification` — workflow stages
33
33
  - `/ec-memory` — short/long memory archive
34
- - `/ec-task-management` — task lifecycle panel · `/ec-config` — Approval/Workflow/TDD settings · `/ec-task-close` — interrupt a task
34
+ - `/ec-task-management` — task lifecycle panel · `/ec-config` — Approval/Workflow/TDD settings · `/ec-tdd-init` — Java changed-line gate initialization · `/ec-task-close` — interrupt a task
35
35
  - `/ec-no-harness` — bypass only Easy Coding for the current session
36
36
  - `/ec-git` — git discipline · `/ec-meta` — understand/customize the harness
37
37
 
@@ -47,9 +47,12 @@ First run `/ec-init`; daily work goes through `/ec-workflow`.
47
47
  evidence gates.
48
48
  - TDD is session override > project `behavior.tdd_enabled` > `false`; its changed-line threshold
49
49
  is session override > project `behavior.tdd_coverage_threshold` > `90`. ANALYSIS -> IMPLEMENT
50
- freezes both. Disabled TDD adds no CI scan, JaCoCo work, commands, artifacts, or stronger gates.
51
- Enabled TDD applies only to Java code tasks and requires lifecycle, review, local coverage, and
52
- GitLab TEST-stage gate evidence.
50
+ freezes both. TDD may be enabled only after `ec-tdd-init` records valid infrastructure readiness;
51
+ there is no enable-now/init-later state. The dedicated `tdd-init` task always freezes TDD off and
52
+ initializes only changed-line coverage infrastructure, never historical business-test coverage.
53
+ Disabled TDD adds no CI scan, JaCoCo work, commands, artifacts, or stronger gates. Enabled TDD
54
+ applies only to Java code tasks and requires lifecycle, review, local coverage, and GitLab
55
+ TEST-stage evidence for production lines changed since the task baseline.
53
56
  - Confirmation-required edges use `pending_transition`; automatic edges use the restricted
54
57
  `auto-transition` API. A read-only task creates no test-strategy.md, never enters REVIEW,
55
58
  VERIFICATION, or MEMORY, and writes no task memory.
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env python3
2
+ """Record and verify Easy Coding Java TDD infrastructure readiness.
3
+
4
+ This tool validates infrastructure only. It never measures repository-wide coverage and never
5
+ creates business tests. Coverage acceptance remains scoped to production lines changed after a
6
+ task's frozen baseline.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import hashlib
13
+ import json
14
+ import os
15
+ import re
16
+ import shlex
17
+ import sys
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ SCHEMA = "easy-coding/tdd-readiness-v1"
22
+ COVERAGE_SCOPE = "changed-production-lines"
23
+ RECEIPT = Path(".easy-coding/tdd/readiness.json")
24
+ TDD_BASE_VARIABLE = "EASY_CODING_TDD_BASE_SHA"
25
+ TDD_THRESHOLD_VARIABLE = "EASY_CODING_TDD_THRESHOLD"
26
+ COVERAGE_TOOL_PATH = ".easy-coding/tools/easy_coding_java_coverage.py"
27
+ JAVA_BUILD_FILE_NAMES = {"pom.xml", "build.gradle", "build.gradle.kts"}
28
+ GITLAB_CI_ENTRY_FILES = {".gitlab-ci.yml", ".gitlab-ci.yaml"}
29
+
30
+
31
+ class ReadinessError(RuntimeError):
32
+ pass
33
+
34
+
35
+ def sha256(path: Path) -> str:
36
+ digest = hashlib.sha256()
37
+ with path.open("rb") as handle:
38
+ for chunk in iter(lambda: handle.read(65536), b""):
39
+ digest.update(chunk)
40
+ return digest.hexdigest()
41
+
42
+
43
+ def project_file(root: Path, value: str) -> tuple[str, Path]:
44
+ candidate = Path(value)
45
+ if candidate.is_absolute():
46
+ resolved = candidate.resolve()
47
+ else:
48
+ resolved = (root / candidate).resolve()
49
+ try:
50
+ relative = resolved.relative_to(root.resolve()).as_posix()
51
+ except ValueError as error:
52
+ raise ReadinessError(f"Path escapes project root: {value}") from error
53
+ if not resolved.is_file():
54
+ raise ReadinessError(f"Required file is missing: {relative}")
55
+ return relative, resolved
56
+
57
+
58
+ def file_record(root: Path, value: str) -> dict[str, str]:
59
+ relative, resolved = project_file(root, value)
60
+ return {"path": relative, "sha256": sha256(resolved)}
61
+
62
+
63
+ def safe_report_pattern(value: str) -> bool:
64
+ candidate = Path(value)
65
+ return bool(value.strip()) and not candidate.is_absolute() and ".." not in candidate.parts
66
+
67
+
68
+ def required_gate_variables(command: str) -> bool:
69
+ try:
70
+ tokens = shlex.split(command)
71
+ except ValueError:
72
+ return False
73
+ options: dict[str, str] = {}
74
+ for index, token in enumerate(tokens[:-1]):
75
+ if token in {"--base", "--threshold"}:
76
+ options[token] = tokens[index + 1]
77
+ return options.get("--base") in {
78
+ f"${TDD_BASE_VARIABLE}",
79
+ "$" + "{" + TDD_BASE_VARIABLE + "}",
80
+ } and options.get("--threshold") in {
81
+ f"${TDD_THRESHOLD_VARIABLE}",
82
+ "$" + "{" + TDD_THRESHOLD_VARIABLE + "}",
83
+ }
84
+
85
+
86
+ def ci_contract_reasons(contents: list[str]) -> list[str]:
87
+ combined = "\n".join(
88
+ re.sub(r"\s+#.*$", "", re.sub(r"^\s*#.*$", "", line))
89
+ for line in "\n".join(contents).splitlines()
90
+ )
91
+ lowered = combined.lower()
92
+ reasons: list[str] = []
93
+ for marker in (
94
+ "jacoco",
95
+ "artifacts",
96
+ COVERAGE_TOOL_PATH,
97
+ TDD_BASE_VARIABLE,
98
+ TDD_THRESHOLD_VARIABLE,
99
+ ):
100
+ if marker.lower() not in lowered:
101
+ reasons.append(f"CI files do not contain required marker: {marker}")
102
+ if not required_gate_variables(combined):
103
+ reasons.append(
104
+ "CI changed-line gate must use the task baseline and threshold variables"
105
+ )
106
+ if re.search(r"(?:^|\n)\s*stage\s*:\s*['\"]?test['\"]?\s*(?:#.*)?(?:\n|$)", combined, re.I) is None:
107
+ reasons.append("CI files do not declare a TEST-stage job")
108
+ return reasons
109
+
110
+
111
+ def parse_records(root: Path, value: object, field: str, reasons: list[str]) -> list[str]:
112
+ if not isinstance(value, list) or not value:
113
+ reasons.append(f"{field} must contain at least one file")
114
+ return []
115
+ contents: list[str] = []
116
+ for item in value:
117
+ if not isinstance(item, dict):
118
+ reasons.append(f"{field} contains an invalid record")
119
+ continue
120
+ file_name = item.get("path")
121
+ expected = item.get("sha256")
122
+ if not isinstance(file_name, str) or not isinstance(expected, str):
123
+ reasons.append(f"{field} contains an invalid path or SHA-256")
124
+ continue
125
+ try:
126
+ _, resolved = project_file(root, file_name)
127
+ if sha256(resolved) != expected:
128
+ reasons.append(f"readiness file changed: {file_name}")
129
+ contents.append(resolved.read_text(encoding="utf-8"))
130
+ except (OSError, UnicodeError, ReadinessError) as error:
131
+ reasons.append(str(error))
132
+ return contents
133
+
134
+
135
+ def inspect(root: Path) -> dict[str, object]:
136
+ receipt = root / RECEIPT
137
+ if not receipt.is_file():
138
+ return {
139
+ "status": "needs_init",
140
+ "coverage_scope": COVERAGE_SCOPE,
141
+ "reasons": ["TDD readiness receipt is missing"],
142
+ "receipt": RECEIPT.as_posix(),
143
+ }
144
+ try:
145
+ manifest = json.loads(receipt.read_text(encoding="utf-8"))
146
+ except (OSError, UnicodeError, json.JSONDecodeError):
147
+ return {
148
+ "status": "needs_init",
149
+ "coverage_scope": COVERAGE_SCOPE,
150
+ "reasons": ["TDD readiness receipt is invalid"],
151
+ "receipt": RECEIPT.as_posix(),
152
+ }
153
+
154
+ reasons: list[str] = []
155
+ if not isinstance(manifest, dict):
156
+ reasons.append("TDD readiness receipt must be a JSON object")
157
+ manifest = {}
158
+ if manifest.get("schema") != SCHEMA:
159
+ reasons.append("unsupported readiness schema")
160
+ if manifest.get("provider") != "gitlab":
161
+ reasons.append("readiness provider must be gitlab")
162
+ if manifest.get("coverage_scope") != COVERAGE_SCOPE:
163
+ reasons.append("coverage scope must be changed-production-lines")
164
+ if manifest.get("historical_coverage_required") is not False:
165
+ reasons.append("historical coverage must remain disabled")
166
+ patterns = manifest.get("coverage_report_patterns")
167
+ if not isinstance(patterns, list) or not patterns or not all(
168
+ isinstance(item, str) and safe_report_pattern(item) for item in patterns
169
+ ):
170
+ reasons.append(
171
+ "coverage_report_patterns must contain safe project-relative report patterns"
172
+ )
173
+ gate = manifest.get("changed_line_gate_command")
174
+ if not isinstance(gate, str) or COVERAGE_TOOL_PATH not in gate:
175
+ reasons.append("changed-line coverage gate command is missing")
176
+ elif not required_gate_variables(gate):
177
+ reasons.append(
178
+ "changed-line coverage gate must use the task baseline and threshold variables"
179
+ )
180
+
181
+ manifest_build_files = manifest.get("build_files")
182
+ manifest_ci_files = manifest.get("ci_files")
183
+ manifest_tool_files = manifest.get("tool_files")
184
+ build_contents = parse_records(root, manifest_build_files, "build_files", reasons)
185
+ ci_contents = parse_records(root, manifest_ci_files, "ci_files", reasons)
186
+ parse_records(root, manifest_tool_files, "tool_files", reasons)
187
+ build_paths = {
188
+ Path(item.get("path", "")).name
189
+ for item in manifest_build_files
190
+ if isinstance(item, dict) and isinstance(item.get("path"), str)
191
+ } if isinstance(manifest_build_files, list) else set()
192
+ ci_paths = {
193
+ item.get("path", "").replace("\\", "/")
194
+ for item in manifest_ci_files
195
+ if isinstance(item, dict) and isinstance(item.get("path"), str)
196
+ } if isinstance(manifest_ci_files, list) else set()
197
+ if not build_paths.intersection(JAVA_BUILD_FILE_NAMES):
198
+ reasons.append("build_files must include a Maven or Gradle Java build file")
199
+ if not ci_paths.intersection(GITLAB_CI_ENTRY_FILES):
200
+ reasons.append("ci_files must include the project-root GitLab CI entry file")
201
+ tool_paths = {
202
+ item.get("path", "").replace("\\", "/")
203
+ for item in manifest_tool_files
204
+ if isinstance(item, dict) and isinstance(item.get("path"), str)
205
+ } if isinstance(manifest_tool_files, list) else set()
206
+ if COVERAGE_TOOL_PATH not in tool_paths:
207
+ reasons.append(f"tool_files must include {COVERAGE_TOOL_PATH}")
208
+ if not any("jacoco" in content.lower() for content in build_contents):
209
+ reasons.append("build files do not configure JaCoCo")
210
+ reasons.extend(ci_contract_reasons(ci_contents))
211
+
212
+ return {
213
+ "status": "ready" if not reasons else "needs_init",
214
+ "coverage_scope": COVERAGE_SCOPE,
215
+ "reasons": list(dict.fromkeys(reasons)),
216
+ "receipt": RECEIPT.as_posix(),
217
+ }
218
+
219
+
220
+ def record(args: argparse.Namespace, root: Path) -> dict[str, object]:
221
+ if not args.build_file:
222
+ raise ReadinessError("At least one --build-file is required.")
223
+ if not args.ci_file:
224
+ raise ReadinessError("At least one --ci-file is required.")
225
+ if not args.coverage_report:
226
+ raise ReadinessError("At least one --coverage-report is required.")
227
+ if not all(safe_report_pattern(value) for value in args.coverage_report):
228
+ raise ReadinessError("--coverage-report values must be safe project-relative patterns.")
229
+ if COVERAGE_TOOL_PATH not in args.gate_command:
230
+ raise ReadinessError(f"--gate-command must invoke {COVERAGE_TOOL_PATH}.")
231
+ if not required_gate_variables(args.gate_command):
232
+ raise ReadinessError(
233
+ "--gate-command must use $EASY_CODING_TDD_BASE_SHA and "
234
+ "$EASY_CODING_TDD_THRESHOLD."
235
+ )
236
+
237
+ build_records = [file_record(root, value) for value in args.build_file]
238
+ ci_records = [file_record(root, value) for value in args.ci_file]
239
+ tool_records = [file_record(root, COVERAGE_TOOL_PATH)]
240
+ if not any(Path(item["path"]).name in JAVA_BUILD_FILE_NAMES for item in build_records):
241
+ raise ReadinessError("--build-file must include pom.xml, build.gradle, or build.gradle.kts.")
242
+ if not any(item["path"] in GITLAB_CI_ENTRY_FILES for item in ci_records):
243
+ raise ReadinessError(
244
+ "--ci-file must include the project-root .gitlab-ci.yml or .gitlab-ci.yaml."
245
+ )
246
+ build_contents = [
247
+ (root / record_item["path"]).read_text(encoding="utf-8")
248
+ for record_item in build_records
249
+ ]
250
+ ci_contents = [
251
+ (root / record_item["path"]).read_text(encoding="utf-8") for record_item in ci_records
252
+ ]
253
+ if not any("jacoco" in content.lower() for content in build_contents):
254
+ raise ReadinessError("Build files must configure JaCoCo before readiness can be recorded.")
255
+ ci_reasons = ci_contract_reasons(ci_contents)
256
+ if ci_reasons:
257
+ raise ReadinessError("; ".join(ci_reasons))
258
+
259
+ manifest = {
260
+ "schema": SCHEMA,
261
+ "provider": "gitlab",
262
+ "coverage_scope": COVERAGE_SCOPE,
263
+ "generated_at": datetime.now(timezone.utc).isoformat(),
264
+ "generated_by": args.agent,
265
+ "build_files": build_records,
266
+ "ci_files": ci_records,
267
+ "tool_files": tool_records,
268
+ "coverage_report_patterns": args.coverage_report,
269
+ "changed_line_gate_command": args.gate_command,
270
+ "historical_coverage_required": False,
271
+ }
272
+ receipt = root / RECEIPT
273
+ receipt.parent.mkdir(parents=True, exist_ok=True)
274
+ temporary = receipt.with_suffix(f".tmp-{os.getpid()}")
275
+ temporary.write_text(json.dumps(manifest, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
276
+ temporary.replace(receipt)
277
+ result = inspect(root)
278
+ if result["status"] != "ready":
279
+ raise ReadinessError("Recorded readiness receipt did not pass validation.")
280
+ return result
281
+
282
+
283
+ def main() -> int:
284
+ parser = argparse.ArgumentParser(description="Easy Coding Java TDD readiness tool")
285
+ parser.add_argument("--cwd", default=".")
286
+ subcommands = parser.add_subparsers(dest="command", required=True)
287
+ subcommands.add_parser("check")
288
+ record_parser = subcommands.add_parser("record")
289
+ record_parser.add_argument("--build-file", action="append", default=[])
290
+ record_parser.add_argument("--ci-file", action="append", default=[])
291
+ record_parser.add_argument("--coverage-report", action="append", default=[])
292
+ record_parser.add_argument("--gate-command", required=True)
293
+ record_parser.add_argument("--agent", required=True)
294
+ args = parser.parse_args()
295
+ root = Path(args.cwd).resolve()
296
+ try:
297
+ result = inspect(root) if args.command == "check" else record(args, root)
298
+ except (OSError, UnicodeError, ReadinessError) as error:
299
+ print(json.dumps({"status": "needs_init", "reasons": [str(error)]}, ensure_ascii=False))
300
+ return 2
301
+ print(json.dumps(result, ensure_ascii=False))
302
+ return 0 if result["status"] == "ready" else 1
303
+
304
+
305
+ if __name__ == "__main__":
306
+ raise SystemExit(main())