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
package/adapters/_jpa.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""JPA 模型解析 helper(框架知识的合法落点)。
|
|
3
|
+
|
|
4
|
+
跨文件解析 `@Entity` / `@MappedSuperclass` / `@Embeddable`,合并父类字段与
|
|
5
|
+
`@Embedded` 内嵌字段;不做 JPA 继承策略推断(只合并 mapped superclass),
|
|
6
|
+
不猜数据库命名策略(无 `@Table` 时表名 = 类名)。纯标准库。
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
CLASS_RE = re.compile(
|
|
15
|
+
r"\b(?:class|record)\s+([A-Za-z_]\w*)"
|
|
16
|
+
r"(?:\s+extends\s+([A-Za-z_]\w*))?[^{]*\{"
|
|
17
|
+
)
|
|
18
|
+
FIELD_RE = re.compile(
|
|
19
|
+
r"^\s*(?:private|protected|public)?\s*"
|
|
20
|
+
r"(?:static\s+|final\s+|volatile\s+|transient\s+)*"
|
|
21
|
+
r"([A-Za-z_][\w.<>, ?\[\]]*)\s+(\w+)\s*(?:=.*)?;\s*$"
|
|
22
|
+
)
|
|
23
|
+
ENUM_RE = re.compile(r"\benum\s+([A-Za-z_]\w*)\s*\{([^}]*)\}", re.S)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _strip_comments(text: str) -> str:
|
|
27
|
+
out: list[str] = []
|
|
28
|
+
i, n = 0, len(text)
|
|
29
|
+
quote = ""
|
|
30
|
+
in_line = False
|
|
31
|
+
in_block = False
|
|
32
|
+
while i < n:
|
|
33
|
+
if in_line:
|
|
34
|
+
if text[i] == "\n":
|
|
35
|
+
in_line = False
|
|
36
|
+
out.append("\n")
|
|
37
|
+
else:
|
|
38
|
+
out.append(" ")
|
|
39
|
+
i += 1
|
|
40
|
+
continue
|
|
41
|
+
if in_block:
|
|
42
|
+
if text.startswith("*/", i):
|
|
43
|
+
in_block = False
|
|
44
|
+
out.append(" ")
|
|
45
|
+
i += 2
|
|
46
|
+
else:
|
|
47
|
+
out.append("\n" if text[i] == "\n" else " ")
|
|
48
|
+
i += 1
|
|
49
|
+
continue
|
|
50
|
+
if quote:
|
|
51
|
+
out.append(text[i])
|
|
52
|
+
if text[i] == "\\" and i + 1 < n:
|
|
53
|
+
out.append(text[i + 1])
|
|
54
|
+
i += 2
|
|
55
|
+
continue
|
|
56
|
+
if text[i] == quote:
|
|
57
|
+
quote = ""
|
|
58
|
+
i += 1
|
|
59
|
+
continue
|
|
60
|
+
if text.startswith("//", i):
|
|
61
|
+
in_line = True
|
|
62
|
+
out.append(" ")
|
|
63
|
+
i += 2
|
|
64
|
+
continue
|
|
65
|
+
if text.startswith("/*", i):
|
|
66
|
+
in_block = True
|
|
67
|
+
out.append(" ")
|
|
68
|
+
i += 2
|
|
69
|
+
continue
|
|
70
|
+
if text[i] in "'\"":
|
|
71
|
+
quote = text[i]
|
|
72
|
+
out.append(text[i])
|
|
73
|
+
i += 1
|
|
74
|
+
return "".join(out)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _match_brace(text: str, open_pos: int) -> int | None:
|
|
78
|
+
depth = 0
|
|
79
|
+
i = open_pos
|
|
80
|
+
quote = ""
|
|
81
|
+
while i < len(text):
|
|
82
|
+
ch = text[i]
|
|
83
|
+
if quote:
|
|
84
|
+
if ch == "\\":
|
|
85
|
+
i += 2
|
|
86
|
+
continue
|
|
87
|
+
if ch == quote:
|
|
88
|
+
quote = ""
|
|
89
|
+
i += 1
|
|
90
|
+
continue
|
|
91
|
+
if ch in "'\"":
|
|
92
|
+
quote = ch
|
|
93
|
+
i += 1
|
|
94
|
+
continue
|
|
95
|
+
if ch == "{":
|
|
96
|
+
depth += 1
|
|
97
|
+
elif ch == "}":
|
|
98
|
+
depth -= 1
|
|
99
|
+
if depth == 0:
|
|
100
|
+
return i
|
|
101
|
+
i += 1
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _ann_value(annotations: str, name: str) -> str:
|
|
106
|
+
m = re.search(
|
|
107
|
+
r"@%s\s*\(\s*(?:[A-Za-z_]+\s*=\s*)?\"((?:\\.|[^\"\\])*)\"" % re.escape(name),
|
|
108
|
+
annotations,
|
|
109
|
+
)
|
|
110
|
+
return m.group(1) if m else ""
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _has(annotations: str, name: str) -> bool:
|
|
114
|
+
return bool(re.search(r"@%s\b" % re.escape(name), annotations))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _annotations_before(text: str, pos: int) -> str:
|
|
118
|
+
prefix = text[max(0, pos - 1200):pos]
|
|
119
|
+
cut = max(prefix.rfind("}"), prefix.rfind(";"))
|
|
120
|
+
tail = prefix[cut + 1:] if cut >= 0 else prefix
|
|
121
|
+
return "\n".join(ln.strip() for ln in tail.splitlines() if ln.strip().startswith("@"))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _fields(body: str) -> list[dict[str, Any]]:
|
|
125
|
+
rows: list[dict[str, Any]] = []
|
|
126
|
+
pending: list[str] = []
|
|
127
|
+
skip_depth = 0
|
|
128
|
+
for line in body.splitlines():
|
|
129
|
+
s = line.strip()
|
|
130
|
+
if not s:
|
|
131
|
+
continue
|
|
132
|
+
if skip_depth > 0:
|
|
133
|
+
skip_depth += line.count("{") - line.count("}")
|
|
134
|
+
continue
|
|
135
|
+
if re.search(r"\b(?:class|record|enum)\s+\w+[^{]*\{", s):
|
|
136
|
+
skip_depth = line.count("{") - line.count("}")
|
|
137
|
+
pending = []
|
|
138
|
+
continue
|
|
139
|
+
if s.startswith("@"):
|
|
140
|
+
pending.append(s)
|
|
141
|
+
continue
|
|
142
|
+
m = FIELD_RE.match(line)
|
|
143
|
+
if not m or "(" in line or re.search(r"\bstatic\b", line):
|
|
144
|
+
if s.endswith(";") or "}" in s:
|
|
145
|
+
pending = []
|
|
146
|
+
continue
|
|
147
|
+
ann = "\n".join(pending)
|
|
148
|
+
pending = []
|
|
149
|
+
if _has(ann, "Transient"):
|
|
150
|
+
continue
|
|
151
|
+
typ, field = m.group(1).strip(), m.group(2)
|
|
152
|
+
if typ in {"return", "throw", "new", "else", "case", "break", "continue",
|
|
153
|
+
"assert", "yield"} or re.search(r"\breturn\b", s):
|
|
154
|
+
continue
|
|
155
|
+
field = _ann_value(ann, "Column") or _ann_value(ann, "TableField") or field
|
|
156
|
+
flags: list[str] = []
|
|
157
|
+
if _has(ann, "Id") or _has(ann, "EmbeddedId") or _has(ann, "TableId"):
|
|
158
|
+
flags.append("主键")
|
|
159
|
+
if _has(ann, "GeneratedValue"):
|
|
160
|
+
flags.append("自增")
|
|
161
|
+
if re.search(r"@Column\b[^)]*nullable\s*=\s*false", ann):
|
|
162
|
+
flags.append("非空")
|
|
163
|
+
if re.search(r"@Column\b[^)]*unique\s*=\s*true", ann):
|
|
164
|
+
flags.append("唯一")
|
|
165
|
+
lm = re.search(r"@Column\b[^)]*length\s*=\s*(\d+)", ann)
|
|
166
|
+
if lm:
|
|
167
|
+
flags.append(f"长度={lm.group(1)}")
|
|
168
|
+
relation = None
|
|
169
|
+
if any(_has(ann, a) for a in ("ManyToOne", "OneToMany", "OneToOne", "ManyToMany")):
|
|
170
|
+
relation = typ
|
|
171
|
+
rows.append({
|
|
172
|
+
"field": field,
|
|
173
|
+
"type": typ,
|
|
174
|
+
"constraints": ",".join(flags) if flags else None,
|
|
175
|
+
"default": None,
|
|
176
|
+
"relation": relation,
|
|
177
|
+
"enum": typ if _has(ann, "Enumerated") else None,
|
|
178
|
+
"embedded": _has(ann, "Embedded") or _has(ann, "EmbeddedId"),
|
|
179
|
+
})
|
|
180
|
+
return rows
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def parse_classes(text: str, rel: str) -> list[dict[str, Any]]:
|
|
184
|
+
clean = _strip_comments(text)
|
|
185
|
+
defs: list[dict[str, Any]] = []
|
|
186
|
+
for m in CLASS_RE.finditer(clean):
|
|
187
|
+
ann = _annotations_before(clean, m.start())
|
|
188
|
+
kind = None
|
|
189
|
+
if _has(ann, "Entity"):
|
|
190
|
+
kind = "entity"
|
|
191
|
+
elif _has(ann, "MappedSuperclass"):
|
|
192
|
+
kind = "mapped"
|
|
193
|
+
elif _has(ann, "Embeddable"):
|
|
194
|
+
kind = "embeddable"
|
|
195
|
+
elif _has(ann, "TableName"):
|
|
196
|
+
kind = "mybatis"
|
|
197
|
+
else:
|
|
198
|
+
kind = "plain"
|
|
199
|
+
open_pos = m.end() - 1
|
|
200
|
+
close = _match_brace(clean, open_pos)
|
|
201
|
+
if close is None:
|
|
202
|
+
continue
|
|
203
|
+
defs.append({
|
|
204
|
+
"name": m.group(1),
|
|
205
|
+
"extends": m.group(2),
|
|
206
|
+
"kind": kind,
|
|
207
|
+
"table": _ann_value(ann, "Table") or _ann_value(ann, "TableName") or m.group(1),
|
|
208
|
+
"fields": _fields(clean[open_pos + 1:close]),
|
|
209
|
+
"rel": rel,
|
|
210
|
+
})
|
|
211
|
+
return defs
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _enum_map(texts: list[str]) -> dict[str, list[str]]:
|
|
215
|
+
out: dict[str, list[str]] = {}
|
|
216
|
+
for text in texts:
|
|
217
|
+
for m in ENUM_RE.finditer(_strip_comments(text)):
|
|
218
|
+
values = [v.strip().split()[0] for v in m.group(2).split(",")
|
|
219
|
+
if v.strip() and not v.strip().startswith("@")]
|
|
220
|
+
out[m.group(1)] = values
|
|
221
|
+
return out
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def parse_project(files: list[Path], root: Path) -> tuple[list[dict], list[dict], list[str]]:
|
|
225
|
+
defs: list[dict[str, Any]] = []
|
|
226
|
+
texts: list[str] = []
|
|
227
|
+
for f in files:
|
|
228
|
+
try:
|
|
229
|
+
text = f.read_text(encoding="utf-8")
|
|
230
|
+
except OSError:
|
|
231
|
+
continue
|
|
232
|
+
rel = f.relative_to(root).as_posix()
|
|
233
|
+
texts.append(text)
|
|
234
|
+
defs.extend(parse_classes(text, rel))
|
|
235
|
+
if not defs:
|
|
236
|
+
return [], [], []
|
|
237
|
+
by_name = {d["name"]: d for d in defs}
|
|
238
|
+
embeddables = {d["name"]: d for d in defs if d["kind"] == "embeddable"}
|
|
239
|
+
enums_map = _enum_map(texts)
|
|
240
|
+
|
|
241
|
+
def resolve(defn: dict[str, Any], stack: set[str], prefix: str = "",
|
|
242
|
+
merge_plain: bool = False) -> list[dict[str, Any]]:
|
|
243
|
+
out: list[dict[str, Any]] = []
|
|
244
|
+
index: dict[str, int] = {}
|
|
245
|
+
|
|
246
|
+
def add(row: dict[str, Any]) -> None:
|
|
247
|
+
name = row["field"]
|
|
248
|
+
if name in index:
|
|
249
|
+
out[index[name]] = row
|
|
250
|
+
else:
|
|
251
|
+
index[name] = len(out)
|
|
252
|
+
out.append(row)
|
|
253
|
+
|
|
254
|
+
base = by_name.get(defn.get("extends") or "")
|
|
255
|
+
merge = base is not None and base["name"] not in stack and (
|
|
256
|
+
base["kind"] == "mapped" or (merge_plain and base["kind"] in ("plain", "mybatis"))
|
|
257
|
+
)
|
|
258
|
+
if merge:
|
|
259
|
+
for row in resolve(base, stack | {defn["name"]}, prefix, merge_plain):
|
|
260
|
+
add(row)
|
|
261
|
+
for row in defn["fields"]:
|
|
262
|
+
name = prefix + row["field"]
|
|
263
|
+
if row["embedded"] and row["type"] in embeddables:
|
|
264
|
+
for nested in resolve(embeddables[row["type"]], stack | {defn["name"]}, name + "."):
|
|
265
|
+
add(nested)
|
|
266
|
+
continue
|
|
267
|
+
copy = dict(row)
|
|
268
|
+
copy["field"] = name
|
|
269
|
+
add(copy)
|
|
270
|
+
return out
|
|
271
|
+
|
|
272
|
+
items: list[dict] = []
|
|
273
|
+
enums: list[dict] = []
|
|
274
|
+
for d in defs:
|
|
275
|
+
if d["kind"] not in ("entity", "mybatis"):
|
|
276
|
+
continue
|
|
277
|
+
table = d["table"]
|
|
278
|
+
for row in resolve(d, set(), "", d["kind"] == "mybatis"):
|
|
279
|
+
row = dict(row)
|
|
280
|
+
row.pop("embedded", None)
|
|
281
|
+
row["table"] = table
|
|
282
|
+
row["source_file"] = d["rel"]
|
|
283
|
+
if row.get("enum") and row["type"] in enums_map:
|
|
284
|
+
enums.append({
|
|
285
|
+
"table": table,
|
|
286
|
+
"field": row["field"],
|
|
287
|
+
"values": [{"value": v, "meaning": ""} for v in enums_map[row["type"]]],
|
|
288
|
+
})
|
|
289
|
+
items.append(row)
|
|
290
|
+
sources = sorted({row["source_file"] for row in items})
|
|
291
|
+
return items, enums, sources
|
package/adapters/_lib.py
CHANGED
|
@@ -12,10 +12,15 @@ import re
|
|
|
12
12
|
import sys
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
|
|
15
|
+
if str(Path(__file__).resolve().parents[1]) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
17
|
+
from _support.layout import load_layout # noqa: E402
|
|
18
|
+
|
|
15
19
|
IGNORE_DIRS = {
|
|
16
20
|
".git", "node_modules", "__pycache__", "site-packages", "dist", "build",
|
|
17
21
|
".next", ".turbo", ".mypy_cache", ".pytest_cache", ".pyxle-build",
|
|
18
|
-
"coverage", ".coverage", ".venv", "venv",
|
|
22
|
+
"coverage", ".coverage", ".venv", "venv", ".scratch", "target",
|
|
23
|
+
".gradle", ".idea", ".atlas", ".atlas-run", ".trellis", ".adapter-out",
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
|
|
@@ -40,8 +45,8 @@ def iter_files(root: Path, suffixes: tuple[str, ...], app_path: str | None = Non
|
|
|
40
45
|
|
|
41
46
|
|
|
42
47
|
def load_apps(root: Path) -> list[dict]:
|
|
43
|
-
"""
|
|
44
|
-
p = root
|
|
48
|
+
"""读资产根/stack-profile.yaml 的 apps(name / path / role)。"""
|
|
49
|
+
p = load_layout(root).stack_profile
|
|
45
50
|
if not p.is_file():
|
|
46
51
|
return []
|
|
47
52
|
apps: list[dict] = []
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""atlas AC 提取器注册表(框架知识的合法落点)。
|
|
3
|
+
|
|
4
|
+
`validators/validate_ac_alignment.py` 只依赖本注册表返回的归一化记录;
|
|
5
|
+
具体框架的扫描/解析逻辑只允许出现在本目录的 adapter 内。
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
_ROOT = Path(__file__).resolve().parents[1]
|
|
16
|
+
if str(_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_ROOT))
|
|
18
|
+
from _support.layout import load_layout # noqa: E402
|
|
19
|
+
from _support.siblings import load_module # noqa: E402
|
|
20
|
+
|
|
21
|
+
_sp = load_module("stack_profile")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ExtractResult:
|
|
26
|
+
tests: list[dict[str, Any]] = field(default_factory=list)
|
|
27
|
+
parse_errors: list[str] = field(default_factory=list)
|
|
28
|
+
scan_face: list[str] = field(default_factory=list)
|
|
29
|
+
input_state: str = "absent" # "present" | "absent"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
DEFAULT = "pytest"
|
|
33
|
+
# name -> (模块名, Extractor 构造参数);具体框架知识只在本目录。
|
|
34
|
+
_BUILTINS = {
|
|
35
|
+
"pytest": ("pytest", None),
|
|
36
|
+
"vitest": ("js", "vitest"),
|
|
37
|
+
"jest": ("js", "jest"),
|
|
38
|
+
"junit5": ("junit", "junit5"),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def resolve(name: str | None = None):
|
|
43
|
+
"""按名字解析内置 adapter;未知返回 None。"""
|
|
44
|
+
key = (name or DEFAULT).strip()
|
|
45
|
+
spec = _BUILTINS.get(key)
|
|
46
|
+
if spec is None:
|
|
47
|
+
return None
|
|
48
|
+
module_name, label = spec
|
|
49
|
+
module = importlib.import_module(f".{module_name}", __name__)
|
|
50
|
+
return module.Extractor(label) if label else module.Extractor()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def available() -> list[str]:
|
|
54
|
+
return list(_BUILTINS)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def wire_project(target: Path) -> dict:
|
|
58
|
+
"""按项目声明的 extractor 调其项目级配置 hook;不支持则诚实跳过。"""
|
|
59
|
+
target = Path(target)
|
|
60
|
+
name = None
|
|
61
|
+
try:
|
|
62
|
+
lay = load_layout(target)
|
|
63
|
+
if lay.stack_profile.is_file():
|
|
64
|
+
profile = _sp.parse_profile(lay.stack_profile.read_text(encoding="utf-8"))
|
|
65
|
+
cfg = profile.get("backend_test") or {}
|
|
66
|
+
if isinstance(cfg, dict):
|
|
67
|
+
name = (cfg.get("extractor_command")
|
|
68
|
+
or cfg.get("extractor")
|
|
69
|
+
or cfg.get("framework"))
|
|
70
|
+
except Exception:
|
|
71
|
+
name = None
|
|
72
|
+
adapter = resolve(name)
|
|
73
|
+
if adapter is None:
|
|
74
|
+
return {"file": None, "status": "skipped",
|
|
75
|
+
"detail": f"extractor={name!r} 无项目级配置钩子(不写任何配置)"}
|
|
76
|
+
fn = getattr(adapter, "wire_project", None)
|
|
77
|
+
if fn is None:
|
|
78
|
+
return {"file": None, "status": "skipped",
|
|
79
|
+
"detail": "该 extractor 无项目级配置钩子(不写任何配置)"}
|
|
80
|
+
return fn(target, name)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""外部 AC extractor 运行器(通用协议,不含任何具体框架知识)。
|
|
3
|
+
|
|
4
|
+
协议:`extractor_command` 是 argv 字符串(`shlex.split`,禁止 shell),Atlas 追加
|
|
5
|
+
`--root <项目根> --test-dir <扫描根> --json`,命令向 stdout 输出:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"schema_version": 1,
|
|
10
|
+
"input_state": "present",
|
|
11
|
+
"tests": [ { "name": "...", "file": "...", "line": 1, "acs": [], "...": "..." } ],
|
|
12
|
+
"parse_errors": [],
|
|
13
|
+
"scan_face": []
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
失败(非 0 / 超时 / 非法 JSON / schema 不合法)一律抛 `ExtractError`,由 AC 门报
|
|
18
|
+
`[EXTRACTOR]` FAIL;不静默降级。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import shlex
|
|
24
|
+
import subprocess
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from . import ExtractResult
|
|
28
|
+
|
|
29
|
+
TIMEOUT_SECONDS = 120
|
|
30
|
+
MAX_OUTPUT_BYTES = 10_000_000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ExtractError(Exception):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _tail(text: str, n: int = 3) -> str:
|
|
38
|
+
lines = [ln for ln in (text or "").strip().splitlines() if ln.strip()]
|
|
39
|
+
return " | ".join(lines[-n:])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run(command: str, root: Path, test_dir: Path, timeout: int = TIMEOUT_SECONDS) -> ExtractResult:
|
|
43
|
+
argv = shlex.split(command)
|
|
44
|
+
if not argv:
|
|
45
|
+
raise ExtractError("extractor_command 为空")
|
|
46
|
+
argv = argv + ["--root", str(root), "--test-dir", str(test_dir), "--json"]
|
|
47
|
+
try:
|
|
48
|
+
proc = subprocess.run(
|
|
49
|
+
argv, cwd=str(root), capture_output=True, text=True,
|
|
50
|
+
timeout=timeout, shell=False,
|
|
51
|
+
)
|
|
52
|
+
except FileNotFoundError as exc:
|
|
53
|
+
raise ExtractError(f"命令不存在:{argv[0]}") from exc
|
|
54
|
+
except subprocess.TimeoutExpired as exc:
|
|
55
|
+
raise ExtractError(f"命令超时({timeout}s):{argv[0]}") from exc
|
|
56
|
+
except OSError as exc:
|
|
57
|
+
raise ExtractError(f"命令启动失败:{exc}") from exc
|
|
58
|
+
|
|
59
|
+
if len(proc.stdout.encode("utf-8", errors="ignore")) > MAX_OUTPUT_BYTES:
|
|
60
|
+
raise ExtractError("stdout 超过 10MB 上限")
|
|
61
|
+
if proc.returncode != 0:
|
|
62
|
+
raise ExtractError(f"退出码 {proc.returncode}:{_tail(proc.stderr or proc.stdout)}")
|
|
63
|
+
try:
|
|
64
|
+
payload = json.loads(proc.stdout)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
raise ExtractError(f"stdout 非合法 JSON:{exc}") from exc
|
|
67
|
+
if not isinstance(payload, dict):
|
|
68
|
+
raise ExtractError("JSON 顶层必须是对象")
|
|
69
|
+
if payload.get("schema_version") != 1:
|
|
70
|
+
raise ExtractError("schema_version 必须为 1")
|
|
71
|
+
tests = payload.get("tests")
|
|
72
|
+
if not isinstance(tests, list):
|
|
73
|
+
raise ExtractError("tests 必须是数组")
|
|
74
|
+
parse_errors = payload.get("parse_errors", [])
|
|
75
|
+
scan_face = payload.get("scan_face", [])
|
|
76
|
+
if not isinstance(parse_errors, list) or not all(isinstance(x, str) for x in parse_errors):
|
|
77
|
+
raise ExtractError("parse_errors 必须是字符串数组")
|
|
78
|
+
if not isinstance(scan_face, list) or not all(isinstance(x, str) for x in scan_face):
|
|
79
|
+
raise ExtractError("scan_face 必须是字符串数组")
|
|
80
|
+
input_state = payload.get("input_state", "present" if tests else "absent")
|
|
81
|
+
if input_state not in ("present", "absent"):
|
|
82
|
+
raise ExtractError("input_state 必须是 present 或 absent")
|
|
83
|
+
return ExtractResult(
|
|
84
|
+
tests=tests,
|
|
85
|
+
parse_errors=parse_errors,
|
|
86
|
+
scan_face=scan_face,
|
|
87
|
+
input_state=input_state,
|
|
88
|
+
)
|