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/CHANGELOG.md +16 -0
- package/README.md +6 -4
- package/dist/cli.js +236 -40
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +5 -3
- package/templates/common/bundled-skills/ec-meta/references/platform-files/README.md +1 -1
- package/templates/common/skills/ec-analysis/SKILL.md +9 -1
- package/templates/common/skills/ec-config/SKILL.md +15 -1
- package/templates/common/skills/ec-tdd-init/SKILL.md +99 -0
- package/templates/common/skills/ec-verification/SKILL.md +9 -1
- package/templates/common/skills/ec-workflow/SKILL.md +11 -5
- package/templates/main-constraint/AGENTS.md.tpl +7 -4
- package/templates/main-constraint/CLAUDE.md.tpl +7 -4
- package/templates/runtime/tools/easy_coding_tdd_readiness.py +306 -0
- package/templates/shared-hooks/easy_coding_state.py +248 -4
package/package.json
CHANGED
|
@@ -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
|
|
89
|
-
`.easy-coding/tools/easy_coding_java_coverage.py`
|
|
90
|
-
|
|
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
|
|
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.
|
|
43
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
51
|
-
|
|
52
|
-
|
|
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())
|