atlas-project-harness 0.2.0-beta.2 → 0.2.0-beta.21
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 +267 -0
- package/DESIGN.md +46 -3
- package/README.md +24 -14
- package/_support/__pycache__/ac_coverage.cpython-312.pyc +0 -0
- package/_support/__pycache__/case_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/layout.cpython-312.pyc +0 -0
- package/_support/__pycache__/manual_block.cpython-312.pyc +0 -0
- package/_support/__pycache__/stack_profile.cpython-312.pyc +0 -0
- package/_support/ac_coverage.py +120 -0
- package/_support/case_block.py +3 -3
- package/_support/layout.py +411 -0
- package/_support/manual_block.py +67 -0
- package/_support/stack_profile.py +3 -1
- package/adapters/__pycache__/_graph.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_jpa.cpython-312.pyc +0 -0
- package/adapters/__pycache__/_lib.cpython-312.pyc +0 -0
- package/adapters/__pycache__/api.cpython-312.pyc +0 -0
- package/adapters/__pycache__/models.cpython-312.pyc +0 -0
- package/adapters/__pycache__/tests.cpython-312.pyc +0 -0
- package/adapters/_graph.py +7 -2
- package/adapters/_jpa.py +291 -0
- package/adapters/_lib.py +8 -3
- package/adapters/ac/__init__.py +80 -0
- package/adapters/ac/__pycache__/__init__.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/command.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/js.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/junit.cpython-312.pyc +0 -0
- package/adapters/ac/__pycache__/pytest.cpython-312.pyc +0 -0
- package/adapters/ac/command.py +88 -0
- package/adapters/ac/js.py +334 -0
- package/adapters/ac/junit.py +266 -0
- package/adapters/ac/pytest.py +380 -0
- package/adapters/api.py +331 -8
- package/adapters/models.py +126 -2
- package/adapters/routes.py +7 -3
- package/adapters/tests.py +43 -5
- package/apply/reference.md +6 -3
- package/bin/atlas.js +66 -28
- package/extensions/pi.ts +39 -8
- package/install.sh +179 -19
- package/package.json +1 -1
- package/patches/project-wiring/README.md +4 -3
- package/patches/project-wiring/__pycache__/apply-wiring.cpython-312.pyc +0 -0
- package/patches/project-wiring/agent-pointer.md +1 -1
- package/patches/project-wiring/apply-wiring.py +146 -153
- package/patches/workflow-plan-apply/1.6-plan-apply.insert.md +1 -1
- package/rings/e2e/reference.md +37 -20
- package/rings/prd/reference.md +26 -8
- package/rings/structure/reference.md +3 -2
- package/scripts/__pycache__/atlas_check.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_report.cpython-312.pyc +0 -0
- package/scripts/__pycache__/atlas_verify.cpython-312.pyc +0 -0
- package/scripts/__pycache__/closeout_derive.cpython-312.pyc +0 -0
- package/scripts/__pycache__/e2e_console.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_e2e_scripts.cpython-312.pyc +0 -0
- package/scripts/__pycache__/gen_testid_stub.cpython-312.pyc +0 -0
- package/scripts/__pycache__/make_review_inputs.cpython-312.pyc +0 -0
- package/scripts/__pycache__/pkg_digest.cpython-312.pyc +0 -0
- package/scripts/__pycache__/refresh_structure.cpython-312.pyc +0 -0
- package/scripts/__pycache__/release.cpython-312.pyc +0 -0
- package/scripts/__pycache__/write_claim.cpython-312.pyc +0 -0
- package/scripts/atlas_check.py +329 -86
- package/scripts/atlas_report.py +10 -2
- package/scripts/atlas_verify.py +435 -0
- package/scripts/closeout_derive.py +296 -0
- package/scripts/e2e_console.py +46 -31
- package/scripts/gen_e2e_scripts.py +424 -66
- package/scripts/gen_testid_stub.py +8 -3
- package/scripts/make_review_inputs.py +20 -6
- package/scripts/pkg_digest.py +42 -5
- package/scripts/refresh_structure.py +29 -8
- package/scripts/release.py +238 -7
- package/scripts/write_claim.py +204 -0
- package/shared/closeout.md +7 -0
- package/shared/gates.md +24 -19
- package/shared/global-rules.md +8 -1
- package/shared/independent-review.md +4 -3
- package/shared/layout.md +28 -3
- package/shared/single-source.md +2 -0
- package/shared/stack-profile.md +19 -10
- package/skills/atlas-apply/SKILL.md +1 -0
- package/skills/atlas-design-review/SKILL.md +3 -3
- package/skills/atlas-e2e/SKILL.md +1 -0
- package/skills/atlas-prd/SKILL.md +1 -0
- package/skills/atlas-release/SKILL.md +9 -3
- package/skills/atlas-structure/SKILL.md +1 -0
- package/skills/frontend-design/SKILL.md +2 -2
- package/skills/frontend-design/references/tuning-verbs.md +1 -1
- package/templates/e2e/case-shard.md +1 -1
- package/templates/e2e/console/app.js +25 -4
- package/templates/e2e/console/console.css +2 -0
- package/templates/e2e/console/index.html +2 -2
- package/templates/prd/domain.md +14 -12
- package/templates/prd/global.md +2 -2
- package/templates/stack-profile.yaml +12 -11
- package/tests/__pycache__/test_ac_adapters.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_api.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_adapter_models.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_apply_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_check.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_atlas_verify_playground.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_canonical_consistency.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_closeout_derive.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_duty_bug_families.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_e2e_console.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_gen_e2e_scripts.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_install_selfcheck.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_layout.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_map_anchors.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_no_framework_names.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_project_wiring.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_refresh_facts.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_shared_kernel.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_tools.cpython-312.pyc +0 -0
- package/tests/__pycache__/test_validate_ac_alignment.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_adr.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validate_walkthrough.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_validators.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/__pycache__/test_write_claim.cpython-312-pytest-9.1.1.pyc +0 -0
- package/tests/fixtures/e2e_coverage_dims_golden.json +12 -0
- package/tests/fixtures/verify_playground/product/e2e/cases/home.md +40 -0
- package/tests/fixtures/verify_playground/product/e2e/e2e-index.md +18 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/README.md +3 -0
- package/tests/fixtures/verify_playground/product/e2e/reviews/2026-01-01-fixture/review.json +17 -0
- package/tests/fixtures/verify_playground/product/e2e/scripts/atlas-prepare.mjs +4 -0
- package/tests/fixtures/verify_playground/product/prd/README.md +5 -0
- package/tests/fixtures/verify_playground/product/prd/home-prd.md +62 -0
- package/tests/fixtures/verify_playground/product/prd/prd.md +52 -0
- package/tests/fixtures/verify_playground/product/stack-profile.yaml.tpl +18 -0
- package/tests/fixtures/verify_playground/site/index.html +6 -0
- package/tests/test_ac_adapters.py +312 -0
- package/tests/test_adapter_api.py +253 -0
- package/tests/test_adapter_models.py +236 -0
- package/tests/test_apply_wiring.py +136 -23
- package/tests/test_atlas_check.py +371 -0
- package/tests/test_atlas_verify.py +335 -0
- package/tests/test_atlas_verify_playground.py +162 -0
- package/tests/test_canonical_consistency.py +5 -2
- package/tests/test_closeout_derive.py +264 -0
- package/tests/test_duty_bug_families.py +355 -3
- package/tests/test_e2e_console.py +49 -0
- package/tests/test_gen_e2e_scripts.py +650 -17
- package/tests/test_install_selfcheck.py +76 -0
- package/tests/test_layout.py +252 -0
- package/tests/test_map_anchors.py +475 -0
- package/tests/test_no_framework_names.py +86 -0
- package/tests/test_project_wiring.py +12 -4
- package/tests/test_refresh_facts.py +18 -3
- package/tests/test_shared_kernel.py +50 -0
- package/tests/test_tools.py +257 -1
- package/tests/test_validate_ac_alignment.py +1081 -0
- package/tests/test_validate_adr.py +24 -0
- package/tests/test_validate_walkthrough.py +313 -0
- package/tests/test_validators.py +499 -1
- package/tests/test_write_claim.py +236 -0
- package/validators/__pycache__/validate_ac_alignment.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_adr.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_design_exemptions.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_index.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_e2e_mocks.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_prd.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_stack_profile.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_structure.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_testids.cpython-312.pyc +0 -0
- package/validators/__pycache__/validate_walkthrough.cpython-312.pyc +0 -0
- package/validators/validate_ac_alignment.py +439 -183
- package/validators/validate_adr.py +15 -5
- package/validators/validate_design_exemptions.py +15 -6
- package/validators/validate_e2e_index.py +109 -38
- package/validators/validate_e2e_mocks.py +12 -5
- package/validators/validate_prd.py +109 -14
- package/validators/validate_stack_profile.py +62 -8
- package/validators/validate_structure.py +9 -2
- package/validators/validate_testids.py +36 -23
- package/validators/validate_walkthrough.py +300 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| # | 接线 | 落点 | 作用 |
|
|
8
8
|
|---|---|---|---|
|
|
9
9
|
| A | **AGENTS.md 指针** | `<项目根>/AGENTS.md` 的 `<!-- TRELLIS:END -->` **之后**(`<!-- ATLAS:START/END -->` 区块) | 位于 Trellis 区块外 ⇒ 不被 `trellis update` 覆盖;给出契约路径、skills、需求级 1.6 步、收口门 |
|
|
10
|
-
| B | **`
|
|
10
|
+
| B | **`after_archive` hook + `session_auto_commit: false`** | `<项目根>/.trellis/config.yaml` | 每个任务归档(`task.py archive` / `/trellis-finish-work`)后跑 `refresh_structure.py --chain` 刷结构事实(决策 D159;旧 `after_finish` 形态自动迁移) |
|
|
11
11
|
|
|
12
12
|
## 内容
|
|
13
13
|
|
|
@@ -28,7 +28,8 @@ python3 apply-wiring.py --target <项目根> --apply # 写入
|
|
|
28
28
|
## 幂等与可重放
|
|
29
29
|
|
|
30
30
|
- AGENTS.md:区块已存在则**按源整段替换**(不新建重复段);文件不存在则创建;标记单侧存在 → 报 `error` 且**不改文件**。
|
|
31
|
-
- `config.yaml`:hook 命令已在 →
|
|
31
|
+
- `config.yaml`:hook 命令已在 `after_archive` → 跳过该步;有 `hooks:` 块则并入 `after_archive`,无则末尾新增 `hooks:` 块;存量 `after_finish` 中的同一条命令自动迁移。
|
|
32
|
+
- `session_auto_commit`:默认写 `false`(D160,提交由人 review);用户显式设置的键尊重不动(含 `true`)。
|
|
32
33
|
- `.yaml` 写入前做**轻量结构护栏**(不依赖 PyYAML):顶层 `hooks:` 必须唯一、hook 命令必须存在,否则报 `error` 且不写。
|
|
33
34
|
- **可重放**:`trellis update` 冲掉 `config.yaml` 的本地 hook 或 AGENTS.md 指针后,重跑 `install.sh`(或本接线器 `--apply`)即恢复。
|
|
34
35
|
|
|
@@ -38,4 +39,4 @@ python3 apply-wiring.py --target <项目根> --apply # 写入
|
|
|
38
39
|
|
|
39
40
|
## 回归测试
|
|
40
41
|
|
|
41
|
-
`atlas/tests/test_project_wiring.py`:验证「TRELLIS:END 之后插入 / 幂等 / 缺文件创建 / 区块更新 / config 三种落位(无 hooks、有 hooks 无
|
|
42
|
+
`atlas/tests/test_project_wiring.py`:验证「TRELLIS:END 之后插入 / 幂等 / 缺文件创建 / 区块更新 / config 三种落位(无 hooks、有 hooks 无 after_archive、有 after_archive)/ dry-run 不写 / 标记不成对报错不改」。
|
|
Binary file
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
- **先分清「装配」与「开户」(第一步就问这个)**:`atlas init` 只装配线束。**若 `product/prd/` 与 `.trellis/spec/structure/` 仍为空 ⇒ 本产品尚未开户** —— 用户说「接入 Atlas 工作流 / 把项目接进来」时走 `atlas-apply adopt`(存量:只登记既有基线 + 开户最小集,不重写老 PRD、不猜老用例),**不得**因为 `.atlas/` / `product/` 已存在就回「已接入」或反问是不是接过了。开户按产物存在与否**断点续跑**,不重来。
|
|
13
13
|
- skills:`atlas-structure` / `atlas-prd` / `atlas-e2e` / `atlas-apply`
|
|
14
14
|
- 需求级流程走 `.trellis/workflow.md` 的 `#### 1.6 项目级资产更新`(= `atlas apply <需求ID>`),**实现在其后**
|
|
15
|
-
- 收口 = 相关 E2E 全 `green` + **用户验收走查确认**(`blocked` ⇒ 域标待验收、不判收口)+ 结构事实刷新(`.trellis/config.yaml` 的 `
|
|
15
|
+
- 收口 = 相关 E2E 全 `green` + **用户验收走查确认**(`blocked` ⇒ 域标待验收、不判收口)+ 结构事实刷新(`.trellis/config.yaml` 的 `after_archive` hook)
|
|
16
16
|
- **撞到「规则本身不合业务」不许静默绕过**:注释规则 / 放宽判定 / `--no-verify` / 假装完成都算绕过——按契约 §15 提炼**设计问题**(缺陷本体 · 抽象触发形态 · 建议方向;**不带产品名 / 业务名词 / 真实数据**)后用 `atlas report` 上报,并告知用户 + 给当前阶段最优临时推进方案(`.atlas/shared/global-rules.md` §15)。用户说「这不合理」是同一类。
|
|
17
17
|
|
|
18
18
|
> 本段由 `atlas/install.sh` 幂等维护(区块可重放)。请勿手改;改契约请改 atlas 源包后重跑 `install.sh`。
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
因此不被 `trellis update` 覆盖。已存在则整段按源更新(不新建重复段)。
|
|
12
12
|
文件不存在时创建。
|
|
13
13
|
|
|
14
|
-
2. `<项目根>/.trellis/config.yaml`:登记 `
|
|
15
|
-
`refresh_structure.py`(结构事实刷新,决策
|
|
16
|
-
|
|
14
|
+
2. `<项目根>/.trellis/config.yaml`:登记 `after_archive` hook 跑
|
|
15
|
+
`refresh_structure.py`(结构事实刷新,决策 D159:归档才是任务收口;旧形态
|
|
16
|
+
`after_finish` 会被迁移);已有 `hooks:` 块则并入,否则在文件末尾新增。
|
|
17
|
+
同一步把 `session_auto_commit` 默认写为 `false`(D160:提交由人 review,
|
|
18
|
+
不让归档自动 commit;用户显式设置的键尊重不动)。
|
|
17
19
|
|
|
18
|
-
3.
|
|
19
|
-
|
|
20
|
+
3. `<项目根>/<测试配置>`(仅 extractor 声明的栈):由 `adapters/ac/` 的 extractor
|
|
21
|
+
提供项目级配置 hook;无 hook 的栈跳过,不写任何配置。
|
|
20
22
|
|
|
21
23
|
4. `<项目根>/.pi/`(仅 pi 宿主):把官方扩展落到 `.pi/extensions/atlas/index.ts`,
|
|
22
24
|
并在 `.pi/settings.json` 的 `extensions[]` 里幂等登记一行(2026-10-15 补)。
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
不只图谱用户。判据:文件**不存在 ⇒ 不首创**(跳过并在报告里给一行建议);
|
|
28
30
|
内容已含 `.adapter-out` 字样 ⇒ 幂等跳过;出现 `!.adapter-out` 负模式 ⇒ 跳过,
|
|
29
31
|
不与用户既有规则打架。**本触点无退役对称**:`.gitignore` 不是本项目创建的文件,
|
|
30
|
-
追加后永不回收(与
|
|
32
|
+
追加后永不回收(与 extractor 配置的「逐字节相等才清」不同——追加是单向动作,
|
|
31
33
|
由报告行当刻告知用户)。
|
|
32
34
|
|
|
33
35
|
设计要点:
|
|
@@ -47,6 +49,11 @@ import re
|
|
|
47
49
|
import sys
|
|
48
50
|
from pathlib import Path
|
|
49
51
|
|
|
52
|
+
if str(Path(__file__).resolve().parents[2]) not in sys.path:
|
|
53
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
54
|
+
from _support.layout import LayoutError, load_layout # noqa: E402
|
|
55
|
+
from adapters.ac import wire_project # noqa: E402
|
|
56
|
+
|
|
50
57
|
PKG_ROOT = Path(__file__).resolve().parent
|
|
51
58
|
POINTER_SNIPPET = PKG_ROOT / "agent-pointer.md"
|
|
52
59
|
|
|
@@ -60,6 +67,8 @@ HOOK_CMD = "python3 .atlas/scripts/refresh_structure.py --root . --chain"
|
|
|
60
67
|
# 子串等于旧命令会漏识别 `--apply` 旧形态 ⇒ 双 hook 常驻、refresh 跑两遍。
|
|
61
68
|
HOOK_PREFIX = "python3 .atlas/scripts/refresh_structure.py --root ."
|
|
62
69
|
CONFIG_REL = ".trellis/config.yaml"
|
|
70
|
+
EVENT_ARCHIVE = "after_archive"
|
|
71
|
+
EVENT_ORDER = ["after_finish", "after_start", "after_create", "after_archive"]
|
|
63
72
|
|
|
64
73
|
RE_HOOKS = re.compile(r"^hooks:\s*(#.*)?$")
|
|
65
74
|
RE_AFTER_FINISH = re.compile(r"^ after_finish:\s*(#.*)?$")
|
|
@@ -118,59 +127,136 @@ def wire_agents(target: Path) -> dict:
|
|
|
118
127
|
|
|
119
128
|
# ----------------------------------------------------------------- .trellis/config.yaml
|
|
120
129
|
|
|
130
|
+
RE_AUTO_COMMIT = re.compile(r"^session_auto_commit:\s*(.*)$")
|
|
131
|
+
RE_AUTO_COMMIT_COMMENTED = re.compile(r"^#\s*session_auto_commit:.*$")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _wire_auto_commit(lines: list[str]) -> str:
|
|
135
|
+
"""默认关闭 Trellis 的自动提交(D160)。
|
|
136
|
+
|
|
137
|
+
自动提交会把 journal / task 归档直接 commit 进用户仓库,绕过人工 review;
|
|
138
|
+
atlas 的接线口径是「提交由人决定」,故新项目默认写 `session_auto_commit: false`。
|
|
139
|
+
用户显式写了该键 ⇒ 尊重(true/false 都不改),只在报告里说明。
|
|
140
|
+
"""
|
|
141
|
+
for ln in lines:
|
|
142
|
+
m = RE_AUTO_COMMIT.match(ln)
|
|
143
|
+
if not m:
|
|
144
|
+
continue
|
|
145
|
+
value = m.group(1).split("#")[0].strip().lower()
|
|
146
|
+
return "respected (already false)" if value in ("false", "no", "0", "off") else "respected (user override)"
|
|
147
|
+
for i, ln in enumerate(lines):
|
|
148
|
+
if RE_AUTO_COMMIT_COMMENTED.match(ln):
|
|
149
|
+
lines[i] = "# atlas 接线:默认关闭自动提交,提交由人 review 后手工执行(D160)\nsession_auto_commit: false\n"
|
|
150
|
+
return "applied (enabled-off at template comment)"
|
|
151
|
+
if lines and not lines[-1].endswith("\n"):
|
|
152
|
+
lines[-1] += "\n"
|
|
153
|
+
lines.append("\n# atlas 接线:默认关闭自动提交,提交由人 review 后手工执行(D160)\nsession_auto_commit: false\n")
|
|
154
|
+
return "applied (enabled-off appended)"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _hooks_range(lines: list[str]) -> tuple[int, int] | None:
|
|
158
|
+
"""返回 hooks: 子块的行区间 [start, end)(end 为第一个顶层行)。"""
|
|
159
|
+
hi = next((i for i, ln in enumerate(lines) if RE_HOOKS.match(ln)), None)
|
|
160
|
+
if hi is None:
|
|
161
|
+
return None
|
|
162
|
+
end = len(lines)
|
|
163
|
+
for j in range(hi + 1, len(lines)):
|
|
164
|
+
if RE_TOP_LEVEL.match(lines[j]):
|
|
165
|
+
end = j
|
|
166
|
+
break
|
|
167
|
+
return (hi + 1, end)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _event_lines(lines: list[str], rng: tuple[int, int], event: str) -> tuple[int, list[int]] | None:
|
|
171
|
+
"""返回某事件子块的 (键行下标, 命令行下标列表);不存在返回 None。"""
|
|
172
|
+
start, end = rng
|
|
173
|
+
key_re = re.compile(rf"^ {re.escape(event)}:\s*(#.*)?$")
|
|
174
|
+
for i in range(start, end):
|
|
175
|
+
if not key_re.match(lines[i]):
|
|
176
|
+
continue
|
|
177
|
+
cmd_idx = []
|
|
178
|
+
j = i + 1
|
|
179
|
+
while j < end:
|
|
180
|
+
ln = lines[j]
|
|
181
|
+
if ln.strip() and not ln.startswith(" "):
|
|
182
|
+
break
|
|
183
|
+
if ln.lstrip().startswith("- "):
|
|
184
|
+
cmd_idx.append(j)
|
|
185
|
+
j += 1
|
|
186
|
+
return (i, cmd_idx)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
121
190
|
def wire_config(target: Path) -> dict:
|
|
191
|
+
"""接线 `.trellis/config.yaml` 的两件事(各自幂等):
|
|
192
|
+
|
|
193
|
+
1. refresh hook 登记到 `after_archive`(D159)。归档(`task.py archive`,收尾命令
|
|
194
|
+
`/trellis-finish-work` 走的也是它)才是任务收口;`after_finish` 只清活动任务指针,
|
|
195
|
+
用它会在真收口时漏刷新。存量 `after_finish` 里的同一条命令会被**迁移**。
|
|
196
|
+
2. `session_auto_commit` 默认写 `false`(D160):自动提交会把 journal / 任务归档直接
|
|
197
|
+
commit 进用户仓库,绕过人工 review;用户显式写了该键则尊重不动。
|
|
198
|
+
"""
|
|
122
199
|
path = target / CONFIG_REL
|
|
123
200
|
if not path.exists():
|
|
124
201
|
return {"file": CONFIG_REL, "status": "skipped",
|
|
125
202
|
"detail": "config.yaml 不存在(Trellis 未初始化?)"}
|
|
126
203
|
|
|
127
204
|
text = path.read_text(encoding="utf-8")
|
|
128
|
-
if HOOK_CMD in text:
|
|
129
|
-
return {"file": CONFIG_REL, "status": "skipped", "detail": "after_finish hook 已登记"}
|
|
130
|
-
|
|
131
|
-
# C1 迁移分支(R7):识别旧形态 `HOOK_PREFIX … --apply`(引号/裸命令皆),**原位替换**
|
|
132
|
-
# 为 --chain,而不是插入新行——否则旧+新双 hook 常驻,每次 finish 刷新跑两遍。
|
|
133
|
-
if HOOK_PREFIX in text and HOOK_CMD not in text:
|
|
134
|
-
tails = [m.group(1) for m in re.finditer(re.escape(HOOK_PREFIX) + r"([^\n\"\']*)", text)]
|
|
135
|
-
if any(t.strip() == "--apply" for t in tails):
|
|
136
|
-
new_text = re.sub(re.escape(HOOK_PREFIX) + r"\s+--apply", HOOK_CMD, text)
|
|
137
|
-
# 护栏:替换后同一文件内仍有 `HOOK_PREFIX --apply`(多 hook 场景不期)则拒绝
|
|
138
|
-
if "--apply" in new_text and HOOK_PREFIX in new_text:
|
|
139
|
-
return {"file": CONFIG_REL, "status": "error",
|
|
140
|
-
"detail": "迁移护栏:替换后仍存在旧形态 --apply hook,不修改"}
|
|
141
|
-
return {"file": CONFIG_REL, "status": "applied",
|
|
142
|
-
"detail": "hook upgraded in-place (--apply → --chain)", "_write": (path, new_text)}
|
|
143
|
-
if all(t.strip() not in ("--apply", "--chain") for t in tails):
|
|
144
|
-
# 前缀在但命令尾是自定义形态 ⇒ 尊重用户自定 hook,不插第二行(防双 hook 常驻)
|
|
145
|
-
return {"file": CONFIG_REL, "status": "skipped",
|
|
146
|
-
"detail": "存在自定义 refresh hook(yellow 尾非 --apply/--chain),未改"}
|
|
147
|
-
|
|
148
205
|
lines = text.splitlines(keepends=True)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if
|
|
161
|
-
|
|
162
|
-
|
|
206
|
+
rng = _hooks_range(lines)
|
|
207
|
+
|
|
208
|
+
hook_ready = False
|
|
209
|
+
if rng is not None:
|
|
210
|
+
archive = _event_lines(lines, rng, EVENT_ARCHIVE)
|
|
211
|
+
hook_ready = archive is not None and any(HOOK_CMD in lines[i] for i in archive[1])
|
|
212
|
+
|
|
213
|
+
detail = "after_archive hook already registered"
|
|
214
|
+
if not hook_ready:
|
|
215
|
+
# 找到别处(旧形态 after_finish / 其它事件)的同一条命令,准备迁移
|
|
216
|
+
legacy: list[tuple[str, int]] = []
|
|
217
|
+
if rng is not None:
|
|
218
|
+
for event in EVENT_ORDER:
|
|
219
|
+
found = _event_lines(lines, rng, event)
|
|
220
|
+
if found is None:
|
|
221
|
+
continue
|
|
222
|
+
for i in found[1]:
|
|
223
|
+
if HOOK_PREFIX in lines[i]:
|
|
224
|
+
tail = lines[i].split(HOOK_PREFIX, 1)[1].strip().strip('"').strip("'").strip()
|
|
225
|
+
if tail not in ("--apply", "--chain"):
|
|
226
|
+
return {"file": CONFIG_REL, "status": "skipped",
|
|
227
|
+
"detail": "存在自定义 refresh hook(尾参非 --apply/--chain),未改"}
|
|
228
|
+
legacy.append((event, i))
|
|
229
|
+
|
|
230
|
+
# 迁移:删除旧命令行;旧事件子块被删空时连键行一起去掉
|
|
231
|
+
for event, i in sorted(legacy, key=lambda x: -x[1]):
|
|
232
|
+
del lines[i]
|
|
233
|
+
found = _event_lines(lines, _hooks_range(lines) or (0, 0), event)
|
|
234
|
+
if found is not None and not found[1]:
|
|
235
|
+
del lines[found[0]]
|
|
236
|
+
|
|
237
|
+
rng = _hooks_range(lines)
|
|
238
|
+
if rng is None:
|
|
239
|
+
if text and not text.endswith("\n"):
|
|
240
|
+
lines.append("\n")
|
|
241
|
+
lines.append("\n# atlas structure refresh(D159):任务归档后刷新结构事实指针层\n")
|
|
242
|
+
lines.append("hooks:\n")
|
|
243
|
+
lines.append(" after_archive:\n")
|
|
244
|
+
lines.append(f' - "{HOOK_CMD}"\n')
|
|
245
|
+
detail = "applied (new hooks block at EOF)"
|
|
163
246
|
else:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
247
|
+
archive = _event_lines(lines, rng, EVENT_ARCHIVE)
|
|
248
|
+
if archive is None:
|
|
249
|
+
lines.insert(rng[1], f' after_archive:\n - "{HOOK_CMD}"\n')
|
|
250
|
+
detail = "applied (after_archive added to existing hooks)"
|
|
251
|
+
else:
|
|
252
|
+
lines.insert(archive[0] + 1, f' - "{HOOK_CMD}"\n')
|
|
253
|
+
detail = "applied (hook added to existing after_archive)"
|
|
254
|
+
if legacy:
|
|
255
|
+
detail = detail.replace("applied", "applied (migrated from after_finish)")
|
|
256
|
+
|
|
257
|
+
commit_state = _wire_auto_commit(lines)
|
|
258
|
+
if commit_state.startswith("applied"):
|
|
259
|
+
detail = f"{detail}; session_auto_commit off"
|
|
174
260
|
|
|
175
261
|
new_text = "".join(lines)
|
|
176
262
|
|
|
@@ -182,114 +268,16 @@ def wire_config(target: Path) -> dict:
|
|
|
182
268
|
if HOOK_CMD not in new_text:
|
|
183
269
|
return {"file": CONFIG_REL, "status": "error",
|
|
184
270
|
"detail": "YAML 护栏:hook 命令未写入,不修改"}
|
|
271
|
+
active_commit_keys = [ln for ln in new_text.splitlines() if RE_AUTO_COMMIT.match(ln)]
|
|
272
|
+
if len(active_commit_keys) > 1:
|
|
273
|
+
return {"file": CONFIG_REL, "status": "error",
|
|
274
|
+
"detail": f"YAML 护栏:session_auto_commit 出现 {len(active_commit_keys)} 次(期望 ≤1),不修改"}
|
|
185
275
|
|
|
186
276
|
if new_text == text:
|
|
187
277
|
return {"file": CONFIG_REL, "status": "skipped", "detail": "already current"}
|
|
188
278
|
return {"file": CONFIG_REL, "status": "applied", "detail": detail, "_write": (path, new_text)}
|
|
189
279
|
|
|
190
280
|
|
|
191
|
-
# ------------------------------------------------------------------- pytest 配置兜底(R6.2 对账定稿,2026-10-15)
|
|
192
|
-
|
|
193
|
-
# 注释剥离(2026-10-15 事故):出厂模板的 `backend_test:` 段是**注释示例**,逐字匹配 raw
|
|
194
|
-
# 文本会把**每个**按模板开户的项目判成 Python 工程 ⇒ 纯前端仓库里凭空多出一个 pytest.ini
|
|
195
|
-
# (存量实测:pnpm + React 仓库,`atlas init` 当刻即中招)。⇒ 只认**声明项**。
|
|
196
|
-
RE_COMMENT = re.compile(r"(?:^|\s+)#.*$")
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def is_python_project(target: Path) -> bool:
|
|
200
|
-
"""探测是否为 Python 工程(stack-profile **声明**或根目录 Python 特征文件)。
|
|
201
|
-
|
|
202
|
-
只看**声明**:注释里出现的关键字不算(见上面 RE_COMMENT 的事故注)——出厂模板的
|
|
203
|
-
`# framework: pytest` 是注释,不能当证据。声明面三种任一生效:
|
|
204
|
-
`python` 字样 / `framework: pytest` / `backend_test:` 段(最后一个是本兜底所服务的
|
|
205
|
-
AC 标记特性的**契约 opt-in 段**,`shared/stack-profile.md` §2)。
|
|
206
|
-
"""
|
|
207
|
-
prof_path = target / "product" / "stack-profile.yaml"
|
|
208
|
-
if prof_path.is_file():
|
|
209
|
-
text = prof_path.read_text(encoding="utf-8", errors="ignore")
|
|
210
|
-
declared = "\n".join(RE_COMMENT.sub("", ln) for ln in text.splitlines()).lower()
|
|
211
|
-
if "python" in declared or "framework: pytest" in declared or "backend_test:" in declared:
|
|
212
|
-
return True
|
|
213
|
-
for marker in ("requirements.txt", "pyproject.toml", "setup.py", "Pipfile", ".venv"):
|
|
214
|
-
if (target / marker).exists():
|
|
215
|
-
return True
|
|
216
|
-
return False
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
MARKERS_INI = (
|
|
220
|
-
"[pytest]\n"
|
|
221
|
-
"markers =\n"
|
|
222
|
-
" ac: mark test with AC ID for traceability\n"
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
MARKERS_PYPROJECT = (
|
|
226
|
-
"[tool.pytest.ini_options]\n"
|
|
227
|
-
"markers = [\n"
|
|
228
|
-
" \"ac: mark test with AC ID for traceability\",\n"
|
|
229
|
-
"]\n"
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _has_pytest_section(pyproject: Path) -> bool:
|
|
234
|
-
try:
|
|
235
|
-
return "[tool.pytest" in pyproject.read_text(encoding="utf-8", errors="ignore")
|
|
236
|
-
except OSError:
|
|
237
|
-
return False
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def wire_python_scaffold(target: Path) -> dict:
|
|
241
|
-
"""AC 标记注册兜底——**载体优先级:已有 pytest 配置文件 > 既有 pytest.ini > 新建 pytest.ini**。
|
|
242
|
-
|
|
243
|
-
R6.2 对账定稿(2026-10-15):本兜底只承担 **`ac` marker 注册**这一项正当元数据;
|
|
244
|
-
`pythonpath = .` 环境补丁已随 F23(适配器输出 `python3 -m pytest`,cwd 自动入
|
|
245
|
-
`sys.path`)退役——线束不再替业务打环境补丁(假桩③病理)。
|
|
246
|
-
|
|
247
|
-
优先级(防「复活被清掉的补丁」):
|
|
248
|
-
1. pyproject.toml / setup.cfg 已承载 pytest 配置 ⇒ skip;
|
|
249
|
-
2. 既有 pytest.ini ⇒ 只补缺失的 marker 注册(pythonpath 不再注入);
|
|
250
|
-
3. 什么都没有 ⇒ 新建 pytest.ini(仅 marker 注册)。
|
|
251
|
-
|
|
252
|
-
**退役对称(2026-10-15 补)**:非 Python 工程里若躺着一份**逐字节等于我们兜底内容**
|
|
253
|
-
的 pytest.ini,那是本脚本早先误建的(判据事故见 `RE_COMMENT`)⇒ 清掉。用户改过 ⇒
|
|
254
|
-
一字不动(只跳过)——「只删自己写的、且没被动过的东西」。
|
|
255
|
-
"""
|
|
256
|
-
ini_path = target / "pytest.ini"
|
|
257
|
-
if not is_python_project(target):
|
|
258
|
-
if ini_path.is_file() and ini_path.read_text(encoding="utf-8") == MARKERS_INI:
|
|
259
|
-
return {"file": "pytest.ini", "status": "removed",
|
|
260
|
-
"detail": "非 Python 工程:清掉本兜底误建的 pytest.ini"
|
|
261
|
-
"(仅当内容逐字节等于兜底内容;用户改过则保留)",
|
|
262
|
-
"_remove": ini_path}
|
|
263
|
-
return {"file": "pytest.ini", "status": "skipped", "detail": "非 Python 工程,跳过"}
|
|
264
|
-
|
|
265
|
-
pyproject = target / "pyproject.toml"
|
|
266
|
-
setup_cfg = target / "setup.cfg"
|
|
267
|
-
|
|
268
|
-
# 1. pyproject.toml / setup.cfg 已承载 pytest 配置 ⇒ 尊重,不另立 pytest.ini
|
|
269
|
-
if pyproject.is_file() and _has_pytest_section(pyproject):
|
|
270
|
-
return {"file": "pytest.ini", "status": "skipped", "detail": "已由 pyproject.toml 承载 pytest 配置"}
|
|
271
|
-
if setup_cfg.is_file():
|
|
272
|
-
text_cfg = setup_cfg.read_text(encoding="utf-8", errors="ignore")
|
|
273
|
-
if "[tool:pytest]" in text_cfg:
|
|
274
|
-
return {"file": "pytest.ini", "status": "skipped", "detail": "已由 setup.cfg 承载 pytest 配置"}
|
|
275
|
-
|
|
276
|
-
if not ini_path.is_file():
|
|
277
|
-
# 3. 全无 ⇒ 新建 pytest.ini(只承载 marker 注册;pythonpath 补丁已随 F23 退役)
|
|
278
|
-
return {"file": "pytest.ini", "status": "applied",
|
|
279
|
-
"detail": "created pytest.ini with ac marker registration (pythonpath retired with F23)",
|
|
280
|
-
"_write": (ini_path, MARKERS_INI)}
|
|
281
|
-
|
|
282
|
-
# 2. 既有 pytest.ini ⇒ 只补 marker 注册(幂等)
|
|
283
|
-
content = ini_path.read_text(encoding="utf-8")
|
|
284
|
-
if "markers" in content:
|
|
285
|
-
return {"file": "pytest.ini", "status": "skipped", "detail": "pytest.ini 已包含 marker 注册"}
|
|
286
|
-
|
|
287
|
-
new_content = content.rstrip("\n") + "\nmarkers =\n ac: mark test with AC ID for traceability\n"
|
|
288
|
-
return {"file": "pytest.ini", "status": "applied",
|
|
289
|
-
"detail": "updated (appended ac marker registration)",
|
|
290
|
-
"_write": (ini_path, new_content)}
|
|
291
|
-
|
|
292
|
-
|
|
293
281
|
# ----------------------------------------------------------------- .gitignore 兜底(2026-10-15 补,第 5 个触点)
|
|
294
282
|
|
|
295
283
|
GITIGNORE_REL = ".gitignore"
|
|
@@ -427,8 +415,13 @@ def main() -> int:
|
|
|
427
415
|
|
|
428
416
|
write = bool(args.apply) and not bool(args.dry_run)
|
|
429
417
|
target = Path(args.target).resolve()
|
|
418
|
+
try:
|
|
419
|
+
load_layout(target)
|
|
420
|
+
except LayoutError as exc:
|
|
421
|
+
print(f"[FAIL] {exc}", file=sys.stderr)
|
|
422
|
+
return 2
|
|
430
423
|
|
|
431
|
-
results = [wire_agents(target), wire_config(target),
|
|
424
|
+
results = [wire_agents(target), wire_config(target), wire_project(target),
|
|
432
425
|
wire_gitignore(target)]
|
|
433
426
|
results += wire_pi_extension(target)
|
|
434
427
|
errors = sum(1 for r in results if r["status"] == "error")
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- **独立审查门**:`e2e` 用例产出后、生成脚本前跑 **E2E 环独立审查**(`rings/e2e/reference.md` §12;有 `Critical` 不得生成脚本)。
|
|
10
10
|
- 应然 UI 的 testid 落点 = 用例分片 `testid:`(应然源);不改 UI 的复用既有 testid(实然 = 前端源码)。
|
|
11
11
|
- **影响面无法确定 / 门控态 / 任一环失败**:如实报告并**停在 planning**,**不得进入实现**——除非用户显式放弃或降级,且结论已写入 `atlas-apply.md`。
|
|
12
|
-
- 结构事实由 `
|
|
12
|
+
- 结构事实由 `after_archive` hook 在实现后刷新,本步不写结构事实(唯一例外:本次改了域清单 ⇒ 末尾追一次 structure re-key)。
|
|
13
13
|
- 三问门的**判定 + 依据**必须写进 `atlas-apply.md`(机器判不了,靠留痕)。
|
|
14
14
|
- 并发时先声明写面:同一项目级文件**单写者**(`apply/reference.md` §5.1)。
|
|
15
15
|
- 幂等:重跑只处理受影响增量。
|