@tea-agent/loop-agent 0.24.0 → 0.24.1
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 +43 -0
- package/dist/commands/init.js +2 -2
- package/dist/task/dag-source-paths.js +50 -0
- package/dist/task/frontend-project-capability.js +316 -0
- package/dist/task/task-demand-routing.js +432 -0
- package/dist/worker/console/chat/compaction-errors.js +64 -0
- package/dist/worker/console/chat/interview-adapter.js +17 -4
- package/dist/worker/console/chat/routes.js +6 -1
- package/dist/worker/console/interview/grill-me.js +28 -5
- package/dist/worker/console/static/assets/index-Cv68Ge5n.js +27 -0
- package/dist/worker/console/static/assets/index-Dp_2sKGk.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/observe/routes.js +8 -4
- package/dist/workflows/dag/backend-test-intake-context.js +10 -12
- package/dist/workflows/dag/backend-test-markdown-workflow.js +223 -42
- package/dist/workflows/dag/backend-test-result-contract.js +6 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +23 -17
- package/dist/workflows/dag/frontend-project-capability.js +1 -316
- package/dist/workflows/dag/frontend-worktree-diff.js +4 -1
- package/dist/workflows/dag/init-hybrid.js +44 -42
- package/dist/workflows/dag/rerun-plan.js +6 -10
- package/dist/workflows/dag/task-demand-routing.js +1 -383
- package/docs/templates/frontend-implementation-contract.schema.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/SKILL.md +7 -0
- package/skills/frontend-implementation/references/node-contracts.md +1 -1
- package/skills/frontend-verification/SKILL.md +4 -3
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
- package/dist/worker/console/static/assets/index-BTbrEHnO.css +0 -1
- package/dist/worker/console/static/assets/index-D9qLevoP.js +0 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 改进
|
|
6
|
+
|
|
7
|
+
- 夜间自动发布固定为 patch 通道(当前 `0.x.y` 上 +0.0.1,例如 `0.24.1 -> 0.24.2`),不再因 `feat:` 自动跳到 `0.x.0`。
|
|
8
|
+
- 新增人工主版本工作流 `manual-minor-release.yml`,仅在显式触发时强制 bump minor,产出 `v0.x.0`。
|
|
9
|
+
- 发布通道可将文档版本钉死/漂移降为告警(`LOOP_AGENT_DOC_VERSION_REFS=warn`),避免 nightly/prepublish 被安装文档旧精确串误阻塞;结构性文档治理仍 fail-closed。
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- 发布流程不再被合法的旧版本引用误阻塞:文档 catalog 现可用 `current`/`floating`/`minimum`/`range`/`historical` 五类语义声明版本引用,审查时基线等历史证据保留当时真实精确版本并通过检查,仅当前安装入口的精确钉死仍判为漂移。
|
|
14
|
+
- Nightly Release 现在在版本号与 CHANGELOG 准备完成之后、push/打 tag/发布 npm/创建 GitHub Release 之前运行最终 release tree 门禁(测试、构建、仓库治理、`npm pack` 干跑),避免在未通过最终门禁的发布树产生远端副作用。
|
|
15
|
+
- Nightly Release 的 commit/tag、npm publish、GitHub Release 三步现支持幂等恢复:重跑时探测目标对象是否存在,一致则跳过、冲突则 fail closed(不 force-push、不重写 tag、不重复发布),可补齐半发布状态。
|
|
16
|
+
- 文档版本治理的 `minimum`/`range` 声明现会校验文档正文实际出现的精确版本串与声明门槛/范围一致;仅元数据通过而正文钉死低于门槛或不满足范围的版本时仍判失败,避免安装入口借 minimum/range 声明悄悄钉死旧版本。
|
|
17
|
+
|
|
18
|
+
## [0.24.1] - 2026-07-27
|
|
19
|
+
|
|
20
|
+
### 重点更新
|
|
21
|
+
|
|
22
|
+
- 后端测试 HTML 报告全面升级,采用更清晰的可折叠用例卡片设计,直观展示脱敏的请求与响应详情。
|
|
23
|
+
- 彻底修复了 Windows 环境下因 cmd.exe 与 bash 嵌套引号转义不当,导致前端与初始化测试固件执行失败的问题。
|
|
24
|
+
- 前端实现 DAG 大幅优化,复用现有 run-owned canonical artifacts,有效精简了常规全绿路径的直接依赖节点数量。
|
|
25
|
+
|
|
26
|
+
### 新增
|
|
27
|
+
|
|
28
|
+
- 后端测试第 7 节点现使用 pytest-html 生成自包含报告,并新增解析器从其 JSON island 提取每条用例的结果与日志。
|
|
29
|
+
- 后端测试生成的 pytest 文件名现可与 Markdown 模块建立一对一清晰映射。
|
|
30
|
+
|
|
31
|
+
### 改进
|
|
32
|
+
|
|
33
|
+
- 后端测试 HTML 报告对齐 R04 模板样式:改用全 inline style 设计,包含渐变 banner、可折叠用例卡片、状态胶囊以及请求参数与响应结果分块展示。
|
|
34
|
+
- 前端 canonical implementation contract 允许纯逻辑修改使用空的 UI state,不再强制虚构,并会确定性移除不适用状态下的空输出。
|
|
35
|
+
- 前端实现 DAG 的多个节点(如 frontend-implement-pi、frontend-review-pi 等)直接依赖得到收敛,不再重复读取完整任务源或上游冗余文本。
|
|
36
|
+
|
|
37
|
+
### 修复
|
|
38
|
+
|
|
39
|
+
- 修复后端测试 HTML 报告中请求参数显示为空的问题:渲染器现已兼容 payload= 键或 JSON request_record 格式的请求摘要。
|
|
40
|
+
- 修复后端测试报告无法获取请求/响应日志的问题:兼容 HTTP_REQUEST 与 HTTP REQUEST 两种日志前缀,并在解析前正确解码 HTML 实体。
|
|
41
|
+
- 修复 shell-executor 在 Windows 环境下因反斜杠路径导致 runDir 损坏的问题,确保路径注入稳定。
|
|
42
|
+
- 修复前端 worktree baseline 解析 git status 时因提前 trim 导致未暂存文件路径丢失首字符的解析缺陷。
|
|
43
|
+
- 统一 DAG 任务源路径生成与运行时解析基准,解决 freshness、rerun 与预览共享不一致的问题。
|
|
44
|
+
- 修复前端项目在需求未显式提及 UI 时被误判保留为标准任务的问题,提升了基于工程证据与交互语义的任务类型推断准确度。
|
|
45
|
+
- 修复前端 lint baseline 在 Windows 下因路径解析逻辑缺陷,拒绝相对路径输出导致评估误报为不可用的问题。
|
|
46
|
+
- 修复多个测试固件在 Windows 下因嵌套 Shell 引号剥离导致命令执行输出为空或报错的问题(涉及 composite-shell 与 init-command)。
|
|
47
|
+
|
|
5
48
|
## [0.24.0] - 2026-07-27
|
|
6
49
|
|
|
7
50
|
### 新增
|
package/dist/commands/init.js
CHANGED
|
@@ -2049,8 +2049,8 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2049
2049
|
"",
|
|
2050
2050
|
"## Specialized Task Kinds",
|
|
2051
2051
|
"",
|
|
2052
|
-
"- standard tasks are classified conservatively
|
|
2053
|
-
"-
|
|
2052
|
+
"- standard tasks are classified conservatively by combining the task title, `source/需求.md`, structured `allowedPaths`, strong React/Next/Vue project evidence, and user-visible interaction/state semantics.",
|
|
2053
|
+
"- An implicit frontend demand selects the frontend DAG only when project or frontend-path evidence is paired with product-delivery semantics; framework evidence alone is insufficient. Backend, mixed, negated, documentation/test maintenance, and unknown demands keep the template selected by the normal governance profile.",
|
|
2054
2054
|
"- `frontend-mock-assess-pi` reads Mock/API/schema evidence and selects `native|browser-intercept|request-adapter|not-needed|blocked` before frontend planning; its deterministic gate rejects blocked or malformed output, while the existing frontend implementer remains the only writer.",
|
|
2055
2055
|
"- Mock-backed verification never proves real API integration. When the backend was not exercised, closeout must retain the gap and name `<task-id>-real-api-integration-verify`; that follow-up is explicitly created/run after backend readiness, never automatic.",
|
|
2056
2056
|
"- Automatic frontend classification never replaces a supervised template selected by an explicit profile, workflowPolicy, or the supervised quality gate.",
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getTaskPaths } from "./runtime.js";
|
|
3
|
+
function toPosixPath(filePath) {
|
|
4
|
+
return filePath.replaceAll(path.sep, "/");
|
|
5
|
+
}
|
|
6
|
+
function isOutside(basePath, candidatePath) {
|
|
7
|
+
const relative = path.relative(basePath, candidatePath);
|
|
8
|
+
return (relative === ".." ||
|
|
9
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
10
|
+
path.isAbsolute(relative));
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Serialize a task source path for DAG prompts and source bindings.
|
|
14
|
+
*
|
|
15
|
+
* Production generation always supplies repoRoot, so emitted paths are
|
|
16
|
+
* directly readable from the DAG executor's repository-root cwd. The
|
|
17
|
+
* task-relative fallback is retained only for in-memory builders/tests that
|
|
18
|
+
* intentionally do not have a repository root.
|
|
19
|
+
*/
|
|
20
|
+
export function serializeDagTaskSourcePath(input) {
|
|
21
|
+
const sourceRoot = path.resolve(input.taskDir, "source");
|
|
22
|
+
const basePath = path.resolve(input.repoRoot ?? input.taskDir);
|
|
23
|
+
const absolutePath = path.resolve(input.absolutePath);
|
|
24
|
+
if (absolutePath === sourceRoot ||
|
|
25
|
+
isOutside(sourceRoot, absolutePath) ||
|
|
26
|
+
isOutside(basePath, absolutePath)) {
|
|
27
|
+
throw new Error(`task source path is outside the current task source directory: ${input.absolutePath}`);
|
|
28
|
+
}
|
|
29
|
+
return toPosixPath(path.relative(basePath, absolutePath));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolve both current repository-relative bindings and legacy task-relative
|
|
33
|
+
* bindings, then require the result to remain inside the bound task's source
|
|
34
|
+
* directory. This is the single runtime path interpretation for DAG sources.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveDagTaskSourcePath(input) {
|
|
37
|
+
if (path.isAbsolute(input.sourcePath)) {
|
|
38
|
+
throw new Error(`absolute DAG task source path is forbidden: ${input.sourcePath}`);
|
|
39
|
+
}
|
|
40
|
+
const taskPaths = getTaskPaths(input.workspaceRoot, input.taskId);
|
|
41
|
+
const nativeSourcePath = input.sourcePath.replaceAll("/", path.sep);
|
|
42
|
+
const absolutePath = input.sourcePath.startsWith(".harness/")
|
|
43
|
+
? path.resolve(input.workspaceRoot, nativeSourcePath)
|
|
44
|
+
: path.resolve(taskPaths.taskDir, nativeSourcePath);
|
|
45
|
+
const sourceRoot = path.resolve(taskPaths.sourceDir);
|
|
46
|
+
if (absolutePath === sourceRoot || isOutside(sourceRoot, absolutePath)) {
|
|
47
|
+
throw new Error(`DAG task source path is outside task ${input.taskId} source directory: ${input.sourcePath}`);
|
|
48
|
+
}
|
|
49
|
+
return absolutePath;
|
|
50
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generation-time frontend project capability discovery (M5/M6).
|
|
3
|
+
* Strong evidence only: package.json deps, config files, source imports.
|
|
4
|
+
* Lockfile-only or directory-name-only must NOT activate adapters.
|
|
5
|
+
*/
|
|
6
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { OPENSPEC_SPEC_DIRS, OPENSPEC_SPEC_EXT_RE } from "../shared/openspec-spec.js";
|
|
9
|
+
async function exists(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
await stat(filePath);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async function readJson(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(filePath, "utf8"));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function allDeps(pkg) {
|
|
27
|
+
return {
|
|
28
|
+
...(pkg.dependencies ?? {}),
|
|
29
|
+
...(pkg.devDependencies ?? {}),
|
|
30
|
+
...(pkg.peerDependencies ?? {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function hasDep(deps, name) {
|
|
34
|
+
return Object.hasOwn(deps, name);
|
|
35
|
+
}
|
|
36
|
+
async function listOpenspec(repoRoot) {
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const specDir of OPENSPEC_SPEC_DIRS) {
|
|
39
|
+
const root = path.join(repoRoot, specDir);
|
|
40
|
+
if (!(await exists(root)))
|
|
41
|
+
continue;
|
|
42
|
+
await walk(root, 0);
|
|
43
|
+
}
|
|
44
|
+
return out.slice(0, 40);
|
|
45
|
+
async function walk(dir, depth) {
|
|
46
|
+
if (depth > 4)
|
|
47
|
+
return;
|
|
48
|
+
let entries = [];
|
|
49
|
+
try {
|
|
50
|
+
entries = await readdir(dir);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const name of entries) {
|
|
56
|
+
const full = path.join(dir, name);
|
|
57
|
+
try {
|
|
58
|
+
const info = await stat(full);
|
|
59
|
+
if (info.isDirectory())
|
|
60
|
+
await walk(full, depth + 1);
|
|
61
|
+
else if (OPENSPEC_SPEC_EXT_RE.test(name)) {
|
|
62
|
+
out.push(path.relative(repoRoot, full).replace(/\\/g, "/"));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// skip
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function buildAdapterGuidance(capability) {
|
|
72
|
+
const lines = [
|
|
73
|
+
"## Frontend project capability (generation-time)",
|
|
74
|
+
`Framework: ${capability.framework}${capability.frameworkVersion ? `@${capability.frameworkVersion}` : ""}`,
|
|
75
|
+
`Evidence: ${capability.evidencePaths.join(", ") || "(none)"}`,
|
|
76
|
+
"Rules: openspec specs and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
|
|
77
|
+
];
|
|
78
|
+
if (capability.framework === "react") {
|
|
79
|
+
lines.push("React adapter: prefer function components + hooks; reuse existing Testing Library / Vitest patterns; do not introduce new state libs without authorization.");
|
|
80
|
+
}
|
|
81
|
+
else if (capability.framework === "next") {
|
|
82
|
+
lines.push("Next.js adapter: respect App vs Pages router from evidence; keep server/client boundaries explicit; avoid guessing cache/navigation APIs across major versions.");
|
|
83
|
+
}
|
|
84
|
+
else if (capability.framework === "vue") {
|
|
85
|
+
lines.push("Vue adapter: prefer Composition API if project already uses it; reuse Pinia/Vue Router/Vue Query only when evidenced; Vitest + Vue Test Utils when present.");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push("Unknown framework: use generic frontend-implementation skill only; do not assume React/Vue/Next APIs.");
|
|
89
|
+
}
|
|
90
|
+
if (capability.a11y.status === "present") {
|
|
91
|
+
lines.push(`A11y tools present: ${capability.a11y.tools.join(", ")}. Report static/component-level evidence only; Browser a11y remains not-run.`);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
lines.push("A11y tools: absent/unknown — mark a11y checks unavailable/not-run; do not install tools.");
|
|
95
|
+
}
|
|
96
|
+
if (capability.designEvidence.normativePaths.length) {
|
|
97
|
+
lines.push(`Normative design paths: ${capability.designEvidence.normativePaths.slice(0, 8).join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
return lines.join("\n");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Discover frontend project capability from repo root.
|
|
103
|
+
* Strong evidence: package.json direct deps + config/bootstrap files.
|
|
104
|
+
*/
|
|
105
|
+
export async function discoverFrontendProjectCapability(repoRoot) {
|
|
106
|
+
const evidencePaths = [];
|
|
107
|
+
const reasons = [];
|
|
108
|
+
const pkgPath = path.join(repoRoot, "package.json");
|
|
109
|
+
const pkgRaw = await readJson(pkgPath);
|
|
110
|
+
if (!pkgRaw || typeof pkgRaw !== "object") {
|
|
111
|
+
const openspec = await listOpenspec(repoRoot);
|
|
112
|
+
const base = {
|
|
113
|
+
schemaVersion: 1,
|
|
114
|
+
framework: "unknown",
|
|
115
|
+
state: [],
|
|
116
|
+
dataFetching: [],
|
|
117
|
+
componentLibrary: [],
|
|
118
|
+
style: [],
|
|
119
|
+
testRunner: [],
|
|
120
|
+
mock: [],
|
|
121
|
+
a11y: { status: "unknown", tools: [], evidencePaths: [] },
|
|
122
|
+
evidencePaths: openspec.slice(0, 5),
|
|
123
|
+
reasons: ["package.json missing or unreadable"],
|
|
124
|
+
designEvidence: {
|
|
125
|
+
normativePaths: openspec,
|
|
126
|
+
advisoryPaths: [],
|
|
127
|
+
conflicts: [],
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
if (openspec.length) {
|
|
131
|
+
base.reasons.push(`openspec spec files discovered: ${openspec.slice(0, 5).join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
return { ...base, adapterGuidance: buildAdapterGuidance(base) };
|
|
134
|
+
}
|
|
135
|
+
evidencePaths.push("package.json");
|
|
136
|
+
const deps = allDeps(pkgRaw);
|
|
137
|
+
// Lockfile alone does not activate
|
|
138
|
+
for (const lock of ["package-lock.json", "pnpm-lock.yaml", "yarn.lock"]) {
|
|
139
|
+
if (await exists(path.join(repoRoot, lock))) {
|
|
140
|
+
reasons.push(`${lock} present but not used as sole activation evidence`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let framework = "unknown";
|
|
144
|
+
let frameworkVersion;
|
|
145
|
+
const configHits = [];
|
|
146
|
+
const nextConfig = ["next.config.js", "next.config.mjs", "next.config.ts"];
|
|
147
|
+
for (const name of nextConfig) {
|
|
148
|
+
if (await exists(path.join(repoRoot, name))) {
|
|
149
|
+
configHits.push(name);
|
|
150
|
+
evidencePaths.push(name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (hasDep(deps, "next") && configHits.some((c) => c.startsWith("next"))) {
|
|
154
|
+
framework = "next";
|
|
155
|
+
frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
|
|
156
|
+
reasons.push("next dependency + next.config*");
|
|
157
|
+
}
|
|
158
|
+
else if (hasDep(deps, "next") && configHits.length === 0) {
|
|
159
|
+
// dep without config: still weak if only transitive — require direct dep
|
|
160
|
+
framework = "next";
|
|
161
|
+
frameworkVersion = deps.next?.replace(/^[^\d]*/, "");
|
|
162
|
+
reasons.push("next direct dependency (config not found)");
|
|
163
|
+
evidencePaths.push("package.json#dependencies.next");
|
|
164
|
+
}
|
|
165
|
+
if (framework === "unknown") {
|
|
166
|
+
const vueConfig = ["vite.config.ts", "vite.config.js", "vue.config.js"];
|
|
167
|
+
let vueCfg = false;
|
|
168
|
+
for (const name of vueConfig) {
|
|
169
|
+
if (await exists(path.join(repoRoot, name))) {
|
|
170
|
+
vueCfg = true;
|
|
171
|
+
evidencePaths.push(name);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (hasDep(deps, "vue") && (vueCfg || hasDep(deps, "nuxt"))) {
|
|
175
|
+
framework = "vue";
|
|
176
|
+
frameworkVersion = deps.vue?.replace(/^[^\d]*/, "");
|
|
177
|
+
reasons.push("vue direct dependency + config/tooling");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (framework === "unknown" && hasDep(deps, "react")) {
|
|
181
|
+
// Need config or jsx tooling — not lockfile-only
|
|
182
|
+
const reactCfg = (await exists(path.join(repoRoot, "vite.config.ts"))) ||
|
|
183
|
+
(await exists(path.join(repoRoot, "vite.config.js"))) ||
|
|
184
|
+
(await exists(path.join(repoRoot, "webpack.config.js"))) ||
|
|
185
|
+
hasDep(deps, "react-scripts") ||
|
|
186
|
+
hasDep(deps, "vite");
|
|
187
|
+
if (reactCfg) {
|
|
188
|
+
framework = "react";
|
|
189
|
+
frameworkVersion = deps.react?.replace(/^[^\d]*/, "");
|
|
190
|
+
reasons.push("react direct dependency + bundler/tooling evidence");
|
|
191
|
+
evidencePaths.push("package.json#dependencies.react");
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
reasons.push("react listed but no strong config/bundler evidence — framework=unknown");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const state = [];
|
|
198
|
+
for (const lib of [
|
|
199
|
+
"redux",
|
|
200
|
+
"@reduxjs/toolkit",
|
|
201
|
+
"zustand",
|
|
202
|
+
"jotai",
|
|
203
|
+
"recoil",
|
|
204
|
+
"pinia",
|
|
205
|
+
"vuex",
|
|
206
|
+
]) {
|
|
207
|
+
if (hasDep(deps, lib))
|
|
208
|
+
state.push(lib);
|
|
209
|
+
}
|
|
210
|
+
const dataFetching = [];
|
|
211
|
+
for (const lib of [
|
|
212
|
+
"@tanstack/react-query",
|
|
213
|
+
"swr",
|
|
214
|
+
"axios",
|
|
215
|
+
"@tanstack/vue-query",
|
|
216
|
+
"ofetch",
|
|
217
|
+
]) {
|
|
218
|
+
if (hasDep(deps, lib))
|
|
219
|
+
dataFetching.push(lib);
|
|
220
|
+
}
|
|
221
|
+
const componentLibrary = [];
|
|
222
|
+
for (const lib of [
|
|
223
|
+
"antd",
|
|
224
|
+
"@mui/material",
|
|
225
|
+
"element-plus",
|
|
226
|
+
"vuetify",
|
|
227
|
+
"@chakra-ui/react",
|
|
228
|
+
]) {
|
|
229
|
+
if (hasDep(deps, lib))
|
|
230
|
+
componentLibrary.push(lib);
|
|
231
|
+
}
|
|
232
|
+
const style = [];
|
|
233
|
+
for (const lib of [
|
|
234
|
+
"tailwindcss",
|
|
235
|
+
"sass",
|
|
236
|
+
"less",
|
|
237
|
+
"styled-components",
|
|
238
|
+
"@emotion/react",
|
|
239
|
+
]) {
|
|
240
|
+
if (hasDep(deps, lib))
|
|
241
|
+
style.push(lib);
|
|
242
|
+
}
|
|
243
|
+
const testRunner = [];
|
|
244
|
+
for (const lib of [
|
|
245
|
+
"vitest",
|
|
246
|
+
"jest",
|
|
247
|
+
"@testing-library/react",
|
|
248
|
+
"@testing-library/vue",
|
|
249
|
+
"@playwright/test",
|
|
250
|
+
]) {
|
|
251
|
+
if (hasDep(deps, lib))
|
|
252
|
+
testRunner.push(lib);
|
|
253
|
+
}
|
|
254
|
+
const mock = [];
|
|
255
|
+
for (const lib of ["msw", "nock", "miragejs"]) {
|
|
256
|
+
if (hasDep(deps, lib))
|
|
257
|
+
mock.push(lib);
|
|
258
|
+
}
|
|
259
|
+
const a11yTools = [];
|
|
260
|
+
const a11yEvidence = [];
|
|
261
|
+
for (const lib of [
|
|
262
|
+
"eslint-plugin-jsx-a11y",
|
|
263
|
+
"@axe-core/react",
|
|
264
|
+
"jest-axe",
|
|
265
|
+
"vitest-axe",
|
|
266
|
+
"axe-core",
|
|
267
|
+
]) {
|
|
268
|
+
if (hasDep(deps, lib)) {
|
|
269
|
+
a11yTools.push(lib);
|
|
270
|
+
a11yEvidence.push(`package.json#${lib}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const a11y = {
|
|
274
|
+
status: (a11yTools.length > 0 ? "present" : "absent"),
|
|
275
|
+
tools: a11yTools,
|
|
276
|
+
evidencePaths: a11yEvidence,
|
|
277
|
+
};
|
|
278
|
+
let router;
|
|
279
|
+
if (hasDep(deps, "react-router-dom") || hasDep(deps, "react-router")) {
|
|
280
|
+
router = "react-router";
|
|
281
|
+
}
|
|
282
|
+
if (framework === "next")
|
|
283
|
+
router = router ?? "next-router";
|
|
284
|
+
if (hasDep(deps, "vue-router"))
|
|
285
|
+
router = "vue-router";
|
|
286
|
+
const openspec = await listOpenspec(repoRoot);
|
|
287
|
+
const designEvidence = {
|
|
288
|
+
normativePaths: openspec,
|
|
289
|
+
advisoryPaths: [],
|
|
290
|
+
conflicts: [],
|
|
291
|
+
};
|
|
292
|
+
if (openspec.length)
|
|
293
|
+
evidencePaths.push(...openspec.slice(0, 5));
|
|
294
|
+
const base = {
|
|
295
|
+
schemaVersion: 1,
|
|
296
|
+
framework,
|
|
297
|
+
frameworkVersion,
|
|
298
|
+
router,
|
|
299
|
+
state,
|
|
300
|
+
dataFetching,
|
|
301
|
+
componentLibrary,
|
|
302
|
+
style,
|
|
303
|
+
testRunner,
|
|
304
|
+
mock,
|
|
305
|
+
a11y,
|
|
306
|
+
packageManager: (await exists(path.join(repoRoot, "pnpm-lock.yaml")))
|
|
307
|
+
? "pnpm"
|
|
308
|
+
: (await exists(path.join(repoRoot, "yarn.lock")))
|
|
309
|
+
? "yarn"
|
|
310
|
+
: "npm",
|
|
311
|
+
evidencePaths: [...new Set(evidencePaths)],
|
|
312
|
+
reasons,
|
|
313
|
+
designEvidence,
|
|
314
|
+
};
|
|
315
|
+
return { ...base, adapterGuidance: buildAdapterGuidance(base) };
|
|
316
|
+
}
|