easy-coding-harness 1.1.0-beta.3 → 1.1.0-beta.4

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 CHANGED
@@ -6,6 +6,15 @@
6
6
  - `y`:常规功能升级
7
7
  - `z`:日常 bug 修复
8
8
 
9
+ ## 1.1.0-beta.4
10
+
11
+ - Hook 使用独立的会话存储与状态展示模块,避免加载完整工作流和 Canonical Spec 校验器;
12
+ 同次调用复用配置、任务与执行日志读取,保留会话迁移及状态提示。
13
+ - 定向检查区分生产依赖、选中测试与公共辅助输入,保留构建所需依赖,减少无关测试变化导致的重跑。
14
+ - 验收快照复用未变 Git 对象,只读取变化内容;自动验收共享本次漂移结果,保留全仓变化检测。
15
+ - Canonical 写回复用同次前置检查与写后确认,保留设计新鲜度、CAS 和幂等语义;批量准备、
16
+ 登记检查作为默认调用方式,减少重复状态查询。
17
+
9
18
  ## 1.1.0-beta.3
10
19
 
11
20
  - MEMORY 以可复用开发知识为正文,提炼业务规则、设计原因、修改入口、适用边界与真实踩坑经验;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-coding-harness",
3
- "version": "1.1.0-beta.3",
3
+ "version": "1.1.0-beta.4",
4
4
  "description": "CLI scaffold for installing Easy Coding harness files into agent-native directories.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -208,7 +208,11 @@ Acceptance: <observable outcome and minimum check>
208
208
  Record Unit `input_files` for the known additional direct inputs (an empty list means the
209
209
  Unit files are self-contained). Include shared helpers, fixtures, schemas and configuration
210
210
  actually consumed by its checks. Without a declared closure, checks cover the owning module.
211
- Build commands still include their module compilation inputs. Do not infer a whole-program call graph. Keep Unit contracts and test points in the existing execution plan. Do not duplicate them across
211
+ For targeted Vitest/Jest/Python/Node checks, name the selected test file in the command and declare
212
+ its helper/fixture inputs so unrelated test cases can retain their evidence. Module production
213
+ inputs and build configuration remain included. Maven/Gradle/tsc compilation still includes
214
+ neighboring sources/tests even with a test selector. Do not infer a whole-program call graph.
215
+ Keep Unit contracts and test points in the existing execution plan. Do not duplicate them across
212
216
  full template chapters or create a separate test strategy for this compact form. Canonical work
213
217
  consumes the selected source closure; it does not redesign unrelated selected tasks.
214
218
 
@@ -37,6 +37,9 @@ tracking metadata is handled once; do not reopen design or rewrite the complete
37
37
 
38
38
  When a lifecycle check is necessary, call `prepare-check` before running it and `record-check`
39
39
  afterward as documented in ec-quality. QUALITY reuses these input-bound results.
40
+ Batch independent checks at the same code state with the array forms of `--record` and `--result`.
41
+ Use each state operation's returned context and next action; query again only when required
42
+ information is absent or an intervening edit makes the returned context stale.
40
43
 
41
44
  ## Non-negotiable gates
42
45
 
@@ -22,24 +22,27 @@ Call `evidence-fingerprints` once to obtain the runtime-owned attempt and candid
22
22
  owns signatures and prior-evidence references. Never calculate historical fingerprints, import
23
23
  runtime internals to reconstruct old candidates, or ask a reviewer to audit workflow bookkeeping.
24
24
 
25
- For each distinct review or verification, prepare its actual inputs before executing it:
25
+ Prepare independent checks for the same unchanged candidate in one batch before executing them:
26
26
 
27
27
  ```bash
28
28
  {{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py prepare-check \
29
- --record '<review/verify JSON with unit_id, dimension or check/check_type/command>' \
29
+ --record '[<review/verify JSON with unit_id, dimension or check/check_type/command>, ...]' \
30
30
  --agent <agent-id> --session-file <P>
31
31
  ```
32
32
 
33
- When `reusable:true`, use the returned evidence index and skip that check. Otherwise run the
34
- specified check once, then register its real result:
33
+ For each returned item with `reusable:true`, use its evidence index and skip that check. Run the
34
+ remaining checks once, then register their real results in one batch:
35
35
 
36
36
  ```bash
37
37
  {{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py record-check \
38
- --prepared-id <returned-id> --result '<JSON with passed, exit_code for verification, findings/reviewer for review>' \
38
+ --result '[{"prepared_id":"<returned-id>","result":<JSON with passed, exit_code for verification, findings/reviewer for review>}, ...]' \
39
39
  --agent <agent-id> --session-file <P>
40
40
  ```
41
41
 
42
42
  Preparation binds code/test inputs, module dependencies, build files and the actual command.
43
+ Batching preserves each check's result and evidence identity. A single check may still use
44
+ `--prepared-id` with one result object. Start a new preparation batch after a code change;
45
+ never reuse a pre-edit input snapshot for post-edit checks.
43
46
  Use the analyzed Unit `input_files` closure for additional helpers/fixtures/configuration, and record intentional
44
47
  environment overrides in the check descriptor. Production-only reviews may declare
45
48
  `review_scope:"production"`; test review still covers changed test behavior. A result is accepted
@@ -185,9 +188,9 @@ This returns other-Agent repairs to the coordinator for delta review/verificatio
185
188
  may pass while the repair is pending. Scope/contract changes must be resolved rather than silently
186
189
  included in the accepted bundle. Repeated starts/completions reuse the existing repair ID.
187
190
 
188
- `prepare-check --record` also accepts an array. `record-check --result` accepts an array of
189
- `{prepared_id,result}` to register the results of those prepared checks. Each check keeps its own
190
- inputs and real result; batching shares runtime reads, not unrelated evidence.
191
+ Use the same batch prepare/record flow for repair checks. Consume returned state and next-action
192
+ fields directly; do not follow a successful state operation with an unchanged `snapshot` or
193
+ Spec inspection merely to retrieve fields already returned.
191
194
 
192
195
  After repair, choose the minimum honest evidence refresh:
193
196
 
@@ -57,6 +57,9 @@ or `qoder`. Never use a display or source-author attribution such as `Codex with
57
57
 
58
58
  1. Read the injected state breadcrumbs or call:
59
59
 
60
+ Prefer the injected or most recent operation's state. Call `snapshot` only when that context
61
+ is missing or stale; do not repeat it after a successful operation that already returns state.
62
+
60
63
  ```bash
61
64
  {{PYTHON_CMD}} {{platform_config_dir}}/hooks/easy_coding_state.py snapshot --agent <agent-id> --session-file <P>
62
65
  ```
@@ -10,47 +10,16 @@ import stat
10
10
  import subprocess
11
11
  import sys
12
12
  import xml.etree.ElementTree as ET
13
- from contextlib import contextmanager
14
- from contextvars import ContextVar
13
+ from easy_coding_operation import evidence_operation, memo, cached_memo, invalidate_memo
15
14
  from pathlib import Path
16
15
 
17
16
 
18
- _operation = ContextVar("evidence_operation", default=None)
19
17
  BUILD_FILES = ("pom.xml", "package.json", "package-lock.json", "pnpm-lock.yaml",
20
18
  "yarn.lock", "tsconfig.json", "build.gradle", "build.gradle.kts",
21
19
  "settings.gradle", "gradle.properties", ".gitattributes", ".npmrc",
22
20
  "vitest.config.ts", "jest.config.js", "biome.json", "pytest.ini", "pyproject.toml")
23
21
 
24
22
 
25
- @contextmanager
26
- def evidence_operation():
27
- token = _operation.set({})
28
- try:
29
- yield
30
- finally:
31
- _operation.reset(token)
32
-
33
-
34
- def memo(key, compute):
35
- cache = _operation.get()
36
- if cache is None:
37
- return compute()
38
- if key not in cache:
39
- cache[key] = compute()
40
- return cache[key]
41
-
42
-
43
- def invalidate_memo(key):
44
- cache = _operation.get()
45
- if cache is not None:
46
- cache.pop(key, None)
47
-
48
-
49
- def cached_memo(key):
50
- cache = _operation.get()
51
- return cache.get(key) if cache is not None else None
52
-
53
-
54
23
  def command_identity(command):
55
24
  # Shell 展开和重定向受引号影响;只归一化普通 argv 命令,避免错误复用。
56
25
  if re.search(r"[\$`\\\n;|&<>*?\[\]{}~#]", command):
@@ -153,7 +122,7 @@ def is_test(path):
153
122
 
154
123
  def is_test_case(path):
155
124
  name = Path(path).name
156
- return bool(re.search(r"(?:Test|Tests|IT)\.java$|\.(?:test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$", name))
125
+ return bool(re.search(r"(?:Test|Tests|IT)\.java$|\.(?:test|spec)\.[cm]?[jt]sx?$|^test_.*\.py$|_test\.py$", name))
157
126
 
158
127
 
159
128
  def command_tokens(command):
@@ -165,6 +134,36 @@ def command_tokens(command):
165
134
  return tokens
166
135
 
167
136
 
137
+ def targeted_test_files(command):
138
+ """只识别完整、无 shell 展开的文件选择调用;未知参数保持模块范围。"""
139
+ if not isinstance(command_identity(command), tuple):
140
+ return set()
141
+ tokens = command_tokens(command)
142
+ if not tokens:
143
+ return set()
144
+ if Path(tokens[0]).name == "npx":
145
+ tokens = tokens[1:]
146
+ elif tokens[:2] in (["pnpm", "exec"], ["npm", "exec"]):
147
+ tokens = tokens[2:]
148
+ if tokens[:1] == ["--"]:
149
+ tokens = tokens[1:]
150
+ if not tokens:
151
+ return set()
152
+ runner, arguments = Path(tokens[0]).name, tokens[1:]
153
+ if runner in {"python", "python3"} and arguments[:2] == ["-m", "pytest"]:
154
+ runner, arguments = "pytest", arguments[2:]
155
+ if runner == "vitest" and arguments[:1] == ["run"]:
156
+ arguments = arguments[1:]
157
+ elif runner == "node" and arguments[:1] == ["--test"]:
158
+ arguments = arguments[1:]
159
+ elif runner not in {"jest", "pytest", "python", "python3"}:
160
+ return set()
161
+ files = [value for value in arguments if value not in {"-q", "-v", "--runInBand", "--coverage"}]
162
+ if not files or any(value.startswith("-") or not is_test_case(value) for value in files):
163
+ return set()
164
+ return {str(Path(value)) for value in files}
165
+
166
+
168
167
  def toolchain_identity(command):
169
168
  tokens = command_tokens(command)
170
169
  executable = tokens[0] if tokens else ""
@@ -220,14 +219,25 @@ def input_spec(root, task, plan, check):
220
219
  tokens = command_tokens(command)
221
220
  executable = Path(tokens[0]).name if tokens else ""
222
221
  builds_module = check.get("type") == "verify" and executable in {
223
- "mvn", "mvnw", "gradle", "gradlew", "npm", "npx", "pnpm", "yarn", "tsc"
222
+ "mvn", "mvnw", "gradle", "gradlew", "npm", "npx", "pnpm", "yarn", "tsc",
223
+ "vitest", "jest", "pytest", "python", "python3", "node",
224
224
  }
225
+ selected_tests = targeted_test_files(command)
225
226
  repositories = {}
227
+ production_repositories = {}
226
228
  for unit in selected:
227
229
  repo_id = unit.get("repo_id") or "current"
228
230
  base = Path(task.get("repo_paths", {}).get(repo_id, root))
229
231
  base = (base if base.is_absolute() else root / base).resolve()
230
232
  paths = repositories.setdefault(str(base), set())
233
+ # 只有明确文件选择器和已声明依赖闭包的脚本测试才收窄测试输入。
234
+ # Maven/Gradle/tsc 的编译阶段仍消费模块内源文件,不能按测试名称裁剪。
235
+ test_files = {str(Path(name)) for name in [*unit.get("files", []), *unit.get("input_files", [])]
236
+ if is_test_case(name)}
237
+ targeted_script = (
238
+ check.get("type") == "verify" and "input_files" in unit
239
+ and bool(selected_tests) and selected_tests <= test_files
240
+ )
231
241
  for name in [*unit.get("files", []), *unit.get("input_files", [])]:
232
242
  absolute = Path(name) if Path(name).is_absolute() else base / name
233
243
  relative = absolute.relative_to(base).as_posix()
@@ -261,16 +271,18 @@ def input_spec(root, task, plan, check):
261
271
  pending.append(candidate)
262
272
  for module in involved:
263
273
  for folder in ("src/main", "src" if not (module / "pom.xml").exists() else "src/test"):
264
- if (builds_module or "input_files" not in unit) and (module / folder).is_dir():
265
- paths.add((module / folder).relative_to(base).as_posix())
274
+ if (builds_module or targeted_script or "input_files" not in unit) and (module / folder).is_dir():
275
+ destination = (production_repositories.setdefault(str(base), set())
276
+ if targeted_script else paths)
277
+ destination.add((module / folder).relative_to(base).as_posix())
266
278
  for parent in [module, *module.parents]:
267
279
  if not parent.is_relative_to(base):
268
280
  break
269
281
  for filename in BUILD_FILES:
270
282
  if (parent / filename).is_file():
271
283
  paths.add((parent / filename).relative_to(base).as_posix())
272
- for config in (".mvn", "gradle", "test" if builds_module else "test/fixtures",
273
- "tests" if builds_module else "tests/fixtures"):
284
+ for config in (".mvn", "gradle", "test" if builds_module and not targeted_script else "test/fixtures",
285
+ "tests" if builds_module and not targeted_script else "tests/fixtures"):
274
286
  if (base / config).is_dir():
275
287
  paths.add(config)
276
288
  if task.get("unit_test_mode") in {"ut", "tdd"}:
@@ -284,6 +296,8 @@ def input_spec(root, task, plan, check):
284
296
  return {
285
297
  "schema": 1,
286
298
  "repositories": {r: sorted(paths) for r, paths in sorted(repositories.items())},
299
+ **({"production_repositories": {r: sorted(paths) for r, paths in sorted(production_repositories.items())}}
300
+ if production_repositories else {}),
287
301
  "production_only": check.get("review_scope") == "production",
288
302
  "contract": [{"id": u["id"], "contracts": u.get("contracts", []),
289
303
  "acceptance_criteria": u.get("acceptance_criteria", [])} for u in owners]
@@ -299,6 +313,9 @@ def capture(spec):
299
313
  for root, scopes in spec["repositories"].items():
300
314
  repository = memo(("repository", root), lambda: RepositoryInputs(Path(root)))
301
315
  inputs[root] = repository.capture(scopes, spec["production_only"])
316
+ production_scopes = spec.get("production_repositories", {}).get(root, [])
317
+ if production_scopes:
318
+ inputs[root].update(repository.capture(production_scopes, production_only=True))
302
319
  return {"spec": spec, "files": inputs, "signature": digest([spec, inputs])}
303
320
 
304
321
 
@@ -0,0 +1,39 @@
1
+ """Operation-local caches shared by hooks and state commands."""
2
+
3
+ from contextlib import contextmanager
4
+ from contextvars import ContextVar
5
+
6
+
7
+ _operation = ContextVar("evidence_operation", default=None)
8
+
9
+
10
+ @contextmanager
11
+ def evidence_operation():
12
+ if _operation.get() is not None:
13
+ yield
14
+ return
15
+ token = _operation.set({})
16
+ try:
17
+ yield
18
+ finally:
19
+ _operation.reset(token)
20
+
21
+
22
+ def memo(key, compute):
23
+ cache = _operation.get()
24
+ if cache is None:
25
+ return compute()
26
+ if key not in cache:
27
+ cache[key] = compute()
28
+ return cache[key]
29
+
30
+
31
+ def invalidate_memo(key):
32
+ cache = _operation.get()
33
+ if cache is not None:
34
+ cache.pop(key, None)
35
+
36
+
37
+ def cached_memo(key):
38
+ cache = _operation.get()
39
+ return cache.get(key) if cache is not None else None