@givolt/i3-composer-agent 2.2.2 → 2.2.3
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/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +28 -21
- package/bin/i3-composer-agent.cjs +5 -4
- package/bin/install.cjs +22 -10
- package/bin/native-command-validation.cjs +8 -13
- package/bin/native-layout.cjs +62 -28
- package/bin/native-migration.cjs +13 -7
- package/bin/native-runtime.cjs +10 -5
- package/docs/advanced-per-agent-configuration.md +78 -0
- package/docs/codebuddy-setup.md +3 -3
- package/docs/codex-setup.md +2 -2
- package/docs/workbuddy-setup.md +7 -7
- package/evals/README.md +26 -16
- package/package.json +37 -18
- package/plugin.json +1 -1
- package/scripts/floor-guard-reference-test.js +291 -0
- package/scripts/lib/skill-lint-test.js +650 -0
- package/scripts/run-evals-test.js +623 -0
- package/scripts/run-regressions.js +19 -0
- package/scripts/validate-artifact-paths-test.js +179 -0
- package/scripts/validate-commands-test.js +216 -0
- package/scripts/validate-reference-links-test.js +495 -0
- package/scripts/validate-versions-test.js +32 -0
- package/bin/native-version-validation.cjs +0 -8
package/docs/codebuddy-setup.md
CHANGED
|
@@ -11,7 +11,7 @@ npx @givolt/i3-composer-agent --codebuddy
|
|
|
11
11
|
向导预选 CodeBuddy,可增加其他工具,再选择项目或全局范围。
|
|
12
12
|
项目安装提供 `.codebuddy/skills/`(25 个技能)、`references/`、
|
|
13
13
|
`commands/`(9 个命令)和 `agents/`(4 个角色)。
|
|
14
|
-
同级 `evals/`、`hooks/`、`scripts/`、`docs/`、`references/`
|
|
14
|
+
同级 `i3-composer-agent/` 是配套工具根,集中提供 `package.json`、`evals/`、`hooks/`、`scripts/`、`docs/`、`references/` 和校验输入 `adapters/`,
|
|
15
15
|
只有一份实际技能、命令和角色,评测与宿主共用。
|
|
16
16
|
全局使用 `~/.codebuddy/` 的相同结构。默认中文说明和回复;技能/命令标识不改名。
|
|
17
17
|
安装成功清理旧内容,失败恢复;设置与自定义命令保留。
|
|
@@ -41,8 +41,8 @@ CodeBuddy 与 WorkBuddy 项目共用 `.codebuddy/`;同版本追加客户端会
|
|
|
41
41
|
|
|
42
42
|
## 配套检查
|
|
43
43
|
|
|
44
|
-
在业务项目运行 `node .codebuddy/
|
|
45
|
-
|
|
44
|
+
在业务项目运行 `node .codebuddy/i3-composer-agent/scripts/run-evals.js --min-rank1 95`,
|
|
45
|
+
可检查中英文触发,不调用模型;进入 `.codebuddy/i3-composer-agent/` 可运行 `npm test` 和 `node scripts/run-evals.js --locale zh-CN --min-rank1 95`。全局安装使用脚本的全局绝对路径。五项校验、行为评测和样例用法见
|
|
46
46
|
[评测说明](../evals/README.md)。Hook 文件已安装,需要按宿主协议单独启用。
|
|
47
47
|
|
|
48
48
|
## 通过 CodeBuddy Code 插件管理器安装
|
package/docs/codex-setup.md
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
npx @givolt/i3-composer-agent --codex
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
项目范围写入 `.agents/skills/`,全局写入 `~/.agents/skills/`,并安装同级 `
|
|
9
|
+
项目范围写入 `.agents/skills/`,全局写入 `~/.agents/skills/`,并安装同级 `i3-composer-agent/references/`。
|
|
10
10
|
提供全部 25 个 `i3-` 技能,中文选择说明与默认中文回复在每个入口独立生效。
|
|
11
11
|
同级 `evals/`、`hooks/`、`scripts/`、`docs/` 各自以 `i3-composer-agent/` 分组资料。
|
|
12
12
|
命令与角色文件也会提供,供按需阅读,其自动发现由宿主决定。
|
|
13
|
-
从项目目录执行 `node .agents/
|
|
13
|
+
从项目目录执行 `node .agents/i3-composer-agent/scripts/run-evals.js --min-rank1 95`
|
|
14
14
|
检查实际安装技能的中英文触发,不调用模型;五项结构校验详见
|
|
15
15
|
[评测说明](../evals/README.md)。原生调用使用技能 ID,Hook 文件需按宿主能力单独启用。
|
|
16
16
|
|
package/docs/workbuddy-setup.md
CHANGED
|
@@ -13,11 +13,11 @@ npx @givolt/i3-composer-agent --agents codebuddy,workbuddy --scope global --yes
|
|
|
13
13
|
- `~/.workbuddy/skills/`:25 个 `i3-` 技能,选择说明和默认回复中文。
|
|
14
14
|
- `~/.workbuddy/commands/`:9 个 `/i3-*` 短命令。
|
|
15
15
|
- `~/.workbuddy/agents/`:4 个专业角色。
|
|
16
|
-
- `~/.workbuddy/
|
|
17
|
-
- `~/.workbuddy/
|
|
18
|
-
- `~/.workbuddy/
|
|
19
|
-
- `~/.workbuddy/
|
|
20
|
-
- `~/.workbuddy/
|
|
16
|
+
- `~/.workbuddy/i3-composer-agent/references/`:共享参考。
|
|
17
|
+
- `~/.workbuddy/i3-composer-agent/docs/agents.md`:角色说明。
|
|
18
|
+
- `~/.workbuddy/i3-composer-agent/evals/`:中英文用例、样例与评测结果。
|
|
19
|
+
- `~/.workbuddy/i3-composer-agent/hooks/`:Hook 文件与说明。
|
|
20
|
+
- `~/.workbuddy/i3-composer-agent/scripts/`:评测、校验与依赖库。
|
|
21
21
|
- `~/.workbuddy/.i3-composer-agent.json`:安装内容所有权及客户端清单。
|
|
22
22
|
|
|
23
23
|
自定义 profile 可设置 `WORKBUDDY_CONFIG_DIR`,安装后打开对应 profile。
|
|
@@ -25,8 +25,8 @@ WorkBuddy 的目录选择使用自己的配置,不借用 `CODEBUDDY_CONFIG_DIR
|
|
|
25
25
|
|
|
26
26
|
## 配套检查
|
|
27
27
|
|
|
28
|
-
运行 `node ~/.workbuddy/
|
|
29
|
-
|
|
28
|
+
运行 `node ~/.workbuddy/i3-composer-agent/scripts/run-evals.js --min-rank1 95`,检查实际安装技能的中英文触发,
|
|
29
|
+
不会调用模型。进入 `~/.workbuddy/i3-composer-agent/` 运行 `npm test` 可检查完整内容、两种语言及全部脚本回归;原 `.js` 入口保留。项目安装将 `~/.workbuddy/` 换成 `.codebuddy/`。五项校验、行为评测用例、样例和
|
|
30
30
|
脚本用法见 [评测说明](../evals/README.md)。Hook 文件默认提供,启用需适配宿主事件协议。
|
|
31
31
|
|
|
32
32
|
## 发现和调用
|
package/evals/README.md
CHANGED
|
@@ -6,22 +6,31 @@
|
|
|
6
6
|
|
|
7
7
|
## 运行位置
|
|
8
8
|
|
|
9
|
-
原生安装的技能位于宿主基址的 `skills/`;评测资料位于 `
|
|
10
|
-
脚本位于 `
|
|
9
|
+
原生安装的技能位于宿主基址的 `skills/`;评测资料位于 `i3-composer-agent/evals/`,
|
|
10
|
+
脚本位于 `i3-composer-agent/scripts/`。安装清单记录预期技能和文件,评测只读取这份实际安装内容。
|
|
11
11
|
例如 CodeBuddy 项目基址是 `.codebuddy/`,WorkBuddy 全局基址是 `~/.workbuddy/`。
|
|
12
12
|
下面原生示例从业务项目根目录运行;从其他位置运行时,将脚本路径换成绝对路径即可。
|
|
13
|
-
Node.js 20 或以上,无需额外 npm
|
|
13
|
+
Node.js 20 或以上,无需额外 npm 依赖;配套根的独立 CommonJS 配置使 `.js` 入口兼容业务项目的 ESM/CommonJS 设置。
|
|
14
14
|
|
|
15
15
|
## 不调用模型的检查
|
|
16
16
|
|
|
17
|
+
进入 `.codebuddy/i3-composer-agent/`(全局安装进入对应宿主的配套根)即可沿用:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm test
|
|
21
|
+
node scripts/run-evals.js --locale zh-CN --min-rank1 95
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
第一条执行五项校验、中英文评测与全部脚本回归;第二条只运行中文评测。以下项目路径入口也可用。
|
|
25
|
+
|
|
17
26
|
```bash
|
|
18
|
-
node .codebuddy/
|
|
19
|
-
node .codebuddy/
|
|
20
|
-
node .codebuddy/
|
|
21
|
-
node .codebuddy/
|
|
22
|
-
node .codebuddy/
|
|
23
|
-
node .codebuddy/
|
|
24
|
-
node .codebuddy/
|
|
27
|
+
node .codebuddy/i3-composer-agent/scripts/validate-skills.js
|
|
28
|
+
node .codebuddy/i3-composer-agent/scripts/validate-versions.js
|
|
29
|
+
node .codebuddy/i3-composer-agent/scripts/validate-commands.js
|
|
30
|
+
node .codebuddy/i3-composer-agent/scripts/validate-reference-links.js
|
|
31
|
+
node .codebuddy/i3-composer-agent/scripts/validate-artifact-paths.js
|
|
32
|
+
node .codebuddy/i3-composer-agent/scripts/run-evals.js --locale zh-CN --min-rank1 95
|
|
33
|
+
node .codebuddy/i3-composer-agent/scripts/run-evals.js --locale en --min-rank1 95
|
|
25
34
|
```
|
|
26
35
|
|
|
27
36
|
五项校验分别检查技能格式、安装版本及登记文件、实际命令和角色格式、引用锚点、规格/计划产物路径。
|
|
@@ -37,8 +46,9 @@ npm run validate
|
|
|
37
46
|
node scripts/run-evals.js --locale zh-CN --min-rank1 95
|
|
38
47
|
```
|
|
39
48
|
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
原生配套与独立插件均保留全部原版脚本及回归测试。原生校验读取真实宿主命令,
|
|
50
|
+
并与 `adapters/` 中另外两种格式比较;同时检查五份插件清单、配套包及安装版本。
|
|
51
|
+
这些 adapter 输入不自动注册插件。在配套根运行 `npm test`,不修改业务项目配置。
|
|
42
52
|
|
|
43
53
|
触发评测使用词法评分检查正例、负例、描述冲突、用例结构及样例是否齐全。
|
|
44
54
|
中英文各自满足指定门槛;这不能证明模型在实际宿主中一定选择正确技能。
|
|
@@ -49,13 +59,13 @@ node scripts/run-evals.js --locale zh-CN --min-rank1 95
|
|
|
49
59
|
先预览,不调用模型、不生成评测结果:
|
|
50
60
|
|
|
51
61
|
```bash
|
|
52
|
-
node .codebuddy/
|
|
62
|
+
node .codebuddy/i3-composer-agent/scripts/run-evals.js --behavioral i3-test-driven-development --locale zh-CN --dry-run
|
|
53
63
|
```
|
|
54
64
|
|
|
55
65
|
确认需要后,移除 `--dry-run` 才会调用已登录的 Claude Code,并消耗该账户的模型额度:
|
|
56
66
|
|
|
57
67
|
```bash
|
|
58
|
-
node .codebuddy/
|
|
68
|
+
node .codebuddy/i3-composer-agent/scripts/run-evals.js --behavioral i3-test-driven-development --locale zh-CN
|
|
59
69
|
```
|
|
60
70
|
|
|
61
71
|
执行型用例需要 Git,并在临时 Git 项目中复制 `fixtures/` 的真实输入;
|
|
@@ -66,7 +76,7 @@ node .codebuddy/scripts/i3-composer-agent/run-evals.cjs --behavioral i3-test-dri
|
|
|
66
76
|
|
|
67
77
|
`--locale en` 运行英文用例,省略该参数运行两种语言。可通过 `I3_EVAL_MODEL`
|
|
68
78
|
和 `I3_EVAL_GRADER_MODEL` 指定执行/评分模型;报告同时记录宿主和模型信息。
|
|
69
|
-
原生结果写入 `
|
|
79
|
+
原生结果写入 `i3-composer-agent/evals/results/`;独立插件写入 `evals/results/`。
|
|
70
80
|
结果不会进入发行包和文件替换清单,升级保留;旧配套目录的结果会迁入新位置,同名异内容时停止迁移。
|
|
71
81
|
|
|
72
82
|
## 插件级评测
|
|
@@ -86,7 +96,7 @@ claude plugin eval . --no-publish
|
|
|
86
96
|
|
|
87
97
|
## Hooks
|
|
88
98
|
|
|
89
|
-
原生的 `
|
|
99
|
+
原生的 `i3-composer-agent/hooks/`(独立插件为 `hooks/`)默认交付会话注入、文档缓存、代码块保护脚本及说明。
|
|
90
100
|
安装文件不会自动注册事件。中文回复与普通技能调用不依赖启用 Hook。
|
|
91
101
|
会话注入仅适用于缺少原生技能路由的宿主;已有路由时不要重复注入。
|
|
92
102
|
缓存及保护的当前配置示例针对 Claude Code,详见
|
package/package.json
CHANGED
|
@@ -139,14 +139,8 @@
|
|
|
139
139
|
"evals/zh-CN/cases/i3-spec-driven-development.json",
|
|
140
140
|
"evals/zh-CN/cases/i3-test-driven-development.json",
|
|
141
141
|
"evals/zh-CN/cases/i3-using-agent-skills.json",
|
|
142
|
-
"scripts/run-evals.js",
|
|
143
|
-
"scripts/validate-skills.js",
|
|
144
|
-
"scripts/validate-versions.js",
|
|
145
|
-
"scripts/validate-commands.js",
|
|
146
|
-
"scripts/validate-reference-links.js",
|
|
147
|
-
"scripts/validate-artifact-paths.js",
|
|
148
|
-
"scripts/lib/skill-lint.js",
|
|
149
142
|
"docs/adoption-guide.md",
|
|
143
|
+
"docs/advanced-per-agent-configuration.md",
|
|
150
144
|
"docs/agents.md",
|
|
151
145
|
"docs/antigravity-setup.md",
|
|
152
146
|
"docs/codebuddy-setup.md",
|
|
@@ -163,12 +157,28 @@
|
|
|
163
157
|
"docs/workbuddy-setup.md",
|
|
164
158
|
"plugin.json",
|
|
165
159
|
"README.md",
|
|
166
|
-
"LICENSE"
|
|
160
|
+
"LICENSE",
|
|
161
|
+
"scripts/floor-guard-reference-test.js",
|
|
162
|
+
"scripts/lib/skill-lint-test.js",
|
|
163
|
+
"scripts/lib/skill-lint.js",
|
|
164
|
+
"scripts/run-evals-test.js",
|
|
165
|
+
"scripts/run-evals.js",
|
|
166
|
+
"scripts/run-regressions.js",
|
|
167
|
+
"scripts/validate-artifact-paths-test.js",
|
|
168
|
+
"scripts/validate-artifact-paths.js",
|
|
169
|
+
"scripts/validate-commands-test.js",
|
|
170
|
+
"scripts/validate-commands.js",
|
|
171
|
+
"scripts/validate-reference-links-test.js",
|
|
172
|
+
"scripts/validate-reference-links.js",
|
|
173
|
+
"scripts/validate-skills.js",
|
|
174
|
+
"scripts/validate-versions-test.js",
|
|
175
|
+
"scripts/validate-versions.js"
|
|
167
176
|
],
|
|
168
177
|
"scripts": {
|
|
169
178
|
"validate": "node scripts/validate-skills.js && node scripts/validate-versions.js && node scripts/validate-commands.js && node scripts/validate-reference-links.js && node scripts/validate-artifact-paths.js",
|
|
170
179
|
"eval": "node scripts/run-evals.js",
|
|
171
|
-
"test": "npm run validate && npm run eval -- --min-rank1 95",
|
|
180
|
+
"test": "npm run validate && npm run eval -- --min-rank1 95 && npm run test:regression",
|
|
181
|
+
"test:regression": "node scripts/run-regressions.js",
|
|
172
182
|
"prepack": "npm run validate"
|
|
173
183
|
},
|
|
174
184
|
"keywords": [
|
|
@@ -301,14 +311,8 @@
|
|
|
301
311
|
"evals/zh-CN/cases/i3-spec-driven-development.json",
|
|
302
312
|
"evals/zh-CN/cases/i3-test-driven-development.json",
|
|
303
313
|
"evals/zh-CN/cases/i3-using-agent-skills.json",
|
|
304
|
-
"scripts/run-evals.js",
|
|
305
|
-
"scripts/validate-skills.js",
|
|
306
|
-
"scripts/validate-versions.js",
|
|
307
|
-
"scripts/validate-commands.js",
|
|
308
|
-
"scripts/validate-reference-links.js",
|
|
309
|
-
"scripts/validate-artifact-paths.js",
|
|
310
|
-
"scripts/lib/skill-lint.js",
|
|
311
314
|
"docs/adoption-guide.md",
|
|
315
|
+
"docs/advanced-per-agent-configuration.md",
|
|
312
316
|
"docs/agents.md",
|
|
313
317
|
"docs/antigravity-setup.md",
|
|
314
318
|
"docs/codebuddy-setup.md",
|
|
@@ -326,10 +330,25 @@
|
|
|
326
330
|
"plugin.json",
|
|
327
331
|
"package.json",
|
|
328
332
|
"README.md",
|
|
329
|
-
"LICENSE"
|
|
333
|
+
"LICENSE",
|
|
334
|
+
"scripts/floor-guard-reference-test.js",
|
|
335
|
+
"scripts/lib/skill-lint-test.js",
|
|
336
|
+
"scripts/lib/skill-lint.js",
|
|
337
|
+
"scripts/run-evals-test.js",
|
|
338
|
+
"scripts/run-evals.js",
|
|
339
|
+
"scripts/run-regressions.js",
|
|
340
|
+
"scripts/validate-artifact-paths-test.js",
|
|
341
|
+
"scripts/validate-artifact-paths.js",
|
|
342
|
+
"scripts/validate-commands-test.js",
|
|
343
|
+
"scripts/validate-commands.js",
|
|
344
|
+
"scripts/validate-reference-links-test.js",
|
|
345
|
+
"scripts/validate-reference-links.js",
|
|
346
|
+
"scripts/validate-skills.js",
|
|
347
|
+
"scripts/validate-versions-test.js",
|
|
348
|
+
"scripts/validate-versions.js"
|
|
330
349
|
]
|
|
331
350
|
},
|
|
332
|
-
"version": "2.2.
|
|
351
|
+
"version": "2.2.3",
|
|
333
352
|
"repository": {
|
|
334
353
|
"type": "git",
|
|
335
354
|
"url": "https://github.com/49780522/i3-composer-agent.git"
|
package/plugin.json
CHANGED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// Contract test for skills/i3-constraint-driven-development/references/floor-guard.md.
|
|
6
|
+
// The fenced `js` block is extracted unchanged and run, with real git, against small
|
|
7
|
+
// fixtures: each is a fresh repository with one source file, one test file and a
|
|
8
|
+
// CONSTRAINTS.md carrying a floor bullet, a coverage minimum, a bundle maximum and an
|
|
9
|
+
// Exceptions table. Loosening moves must exit 1, tightening and no-op moves must exit 0.
|
|
10
|
+
|
|
11
|
+
const assert = require('node:assert/strict');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const { spawnSync } = require('node:child_process');
|
|
16
|
+
const { after, before, test } = require('node:test');
|
|
17
|
+
|
|
18
|
+
const REFERENCE = path.join(
|
|
19
|
+
__dirname, '..', 'skills', 'i3-constraint-driven-development', 'references', 'floor-guard.md',
|
|
20
|
+
);
|
|
21
|
+
const CONSTRAINTS = [
|
|
22
|
+
'# Constraints',
|
|
23
|
+
'',
|
|
24
|
+
'Last reviewed: 2026-08-08 by @owner',
|
|
25
|
+
'',
|
|
26
|
+
'## Floor',
|
|
27
|
+
'- No skipped tests.',
|
|
28
|
+
'- Latency: p95 under 200 ms and p99 under 900 ms.',
|
|
29
|
+
'',
|
|
30
|
+
'| Dimension | Rule |',
|
|
31
|
+
'|---|---|',
|
|
32
|
+
'| Coverage | >= 80% |',
|
|
33
|
+
'| Bundle | <= 200 KB |',
|
|
34
|
+
'',
|
|
35
|
+
'## Exceptions',
|
|
36
|
+
'',
|
|
37
|
+
'| ID | Rule | Owner | Expires |',
|
|
38
|
+
'|---|---|---|---|',
|
|
39
|
+
'| W1 | no-explicit-any | @owner | 2026-11-01 |',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
let scratch;
|
|
44
|
+
let guard;
|
|
45
|
+
const sandboxes = [];
|
|
46
|
+
|
|
47
|
+
before(() => {
|
|
48
|
+
scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-test-'));
|
|
49
|
+
const source = fs.readFileSync(REFERENCE, 'utf8');
|
|
50
|
+
const match = source.match(/```js\n([\s\S]*?)\n```/);
|
|
51
|
+
assert.ok(match, 'floor-guard.md must contain one fenced js block');
|
|
52
|
+
guard = path.join(scratch, 'floor-guard.mjs');
|
|
53
|
+
fs.writeFileSync(guard, match[1] + '\n');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
after(() => {
|
|
57
|
+
for (const dir of sandboxes) fs.rmSync(dir, { recursive: true, force: true });
|
|
58
|
+
if (scratch) fs.rmSync(scratch, { recursive: true, force: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
function git(cwd, ...args) {
|
|
62
|
+
const result = spawnSync('git', args, {
|
|
63
|
+
cwd,
|
|
64
|
+
encoding: 'utf8',
|
|
65
|
+
env: {
|
|
66
|
+
...process.env,
|
|
67
|
+
GIT_AUTHOR_NAME: 'Floor Guard Test',
|
|
68
|
+
GIT_AUTHOR_EMAIL: 'floor-guard@example.invalid',
|
|
69
|
+
GIT_COMMITTER_NAME: 'Floor Guard Test',
|
|
70
|
+
GIT_COMMITTER_EMAIL: 'floor-guard@example.invalid',
|
|
71
|
+
GIT_CONFIG_GLOBAL: '/dev/null',
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
assert.equal(result.status, 0, `git ${args.join(' ')} failed: ${result.stderr}`);
|
|
75
|
+
return result.stdout;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function makeRepo() {
|
|
79
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-fixture-'));
|
|
80
|
+
sandboxes.push(root);
|
|
81
|
+
git(root, 'init', '-q');
|
|
82
|
+
fs.writeFileSync(path.join(root, 'app.js'), 'module.exports = 1;\n');
|
|
83
|
+
fs.writeFileSync(
|
|
84
|
+
path.join(root, 'app.test.js'),
|
|
85
|
+
"const assert = require('assert');\nassert.equal(require('./app'), 1);\n",
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(path.join(root, 'CONSTRAINTS.md'), CONSTRAINTS);
|
|
88
|
+
git(root, 'add', '.');
|
|
89
|
+
git(root, 'commit', '-qm', 'baseline');
|
|
90
|
+
return root;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function editConstraints(root, from, to) {
|
|
94
|
+
const file = path.join(root, 'CONSTRAINTS.md');
|
|
95
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
96
|
+
assert.ok(text.includes(from), `fixture CONSTRAINTS.md must contain ${JSON.stringify(from)}`);
|
|
97
|
+
fs.writeFileSync(file, text.replace(from, to));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function commitFile(root, name, text) {
|
|
101
|
+
fs.writeFileSync(path.join(root, name), text);
|
|
102
|
+
git(root, 'add', name);
|
|
103
|
+
git(root, 'commit', '-qm', `add ${name}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runGuard(root, cwd = root) {
|
|
107
|
+
return spawnSync(process.execPath, [guard, '--base', 'HEAD'], { cwd, encoding: 'utf8' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const cases = [
|
|
111
|
+
['unchanged repository is clean', 0, () => {}],
|
|
112
|
+
['a suppression added to a tracked file is a violation', 1, (root) => {
|
|
113
|
+
fs.writeFileSync(path.join(root, 'app.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
114
|
+
}],
|
|
115
|
+
['a suppression in a new untracked file is a violation', 1, (root) => {
|
|
116
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
117
|
+
}],
|
|
118
|
+
['a skipped test in a new untracked file is a violation', 1, (root) => {
|
|
119
|
+
fs.writeFileSync(path.join(root, 'new.test.js'), "test.skip('regression', () => {});\n");
|
|
120
|
+
}],
|
|
121
|
+
['a suppression in a staged new file is a violation', 1, (root) => {
|
|
122
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
123
|
+
git(root, 'add', '-A');
|
|
124
|
+
}],
|
|
125
|
+
['a deleted test file is a violation', 1, (root) => {
|
|
126
|
+
fs.unlinkSync(path.join(root, 'app.test.js'));
|
|
127
|
+
}],
|
|
128
|
+
['a staged deleted test file is a violation', 1, (root) => {
|
|
129
|
+
fs.unlinkSync(path.join(root, 'app.test.js'));
|
|
130
|
+
git(root, 'add', '-A');
|
|
131
|
+
}],
|
|
132
|
+
['an assertion removed from a surviving test file is a violation', 1, (root) => {
|
|
133
|
+
fs.writeFileSync(path.join(root, 'app.test.js'), "const assert = require('assert');\n");
|
|
134
|
+
}],
|
|
135
|
+
['lowering a minimum is a violation', 1, (root) => {
|
|
136
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 60% |');
|
|
137
|
+
}],
|
|
138
|
+
['raising a minimum is silent', 0, (root) => {
|
|
139
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | >= 90% |');
|
|
140
|
+
}],
|
|
141
|
+
['raising a maximum is a violation', 1, (root) => {
|
|
142
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 300 KB |');
|
|
143
|
+
}],
|
|
144
|
+
['lowering a maximum is silent', 0, (root) => {
|
|
145
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 150 KB |');
|
|
146
|
+
}],
|
|
147
|
+
['deleting one threshold from a rule is a violation', 1, (root) => {
|
|
148
|
+
editConstraints(root, 'p95 under 200 ms and p99 under 900 ms.', 'p95 under 200 ms.');
|
|
149
|
+
}],
|
|
150
|
+
['a number inserted before a threshold is silent', 0, (root) => {
|
|
151
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | per ADR 7, >= 80% |');
|
|
152
|
+
}],
|
|
153
|
+
['adding a threshold to a rule is silent', 0, (root) => {
|
|
154
|
+
editConstraints(root, '| Bundle | <= 200 KB |', '| Bundle | <= 200 KB, <= 60 KB gzipped |');
|
|
155
|
+
}],
|
|
156
|
+
['a deleted floor rule is a violation', 1, (root) => {
|
|
157
|
+
editConstraints(root, '- No skipped tests.\n', '');
|
|
158
|
+
}],
|
|
159
|
+
['a new exception row is a violation', 1, (root) => {
|
|
160
|
+
const file = path.join(root, 'CONSTRAINTS.md');
|
|
161
|
+
fs.appendFileSync(file, '| W2 | temporary waiver | @owner | 2026-10-01 |\n');
|
|
162
|
+
}],
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
for (const [name, expectedExit, mutate] of cases) {
|
|
166
|
+
test(`floor guard: ${name}`, () => {
|
|
167
|
+
const root = makeRepo();
|
|
168
|
+
mutate(root);
|
|
169
|
+
const result = runGuard(root);
|
|
170
|
+
assert.equal(
|
|
171
|
+
result.status, expectedExit,
|
|
172
|
+
`expected exit ${expectedExit}, got ${result.status}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`,
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
test('floor guard: a removed exception row is silent', () => {
|
|
178
|
+
const root = makeRepo();
|
|
179
|
+
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |\n', '');
|
|
180
|
+
const result = runGuard(root);
|
|
181
|
+
assert.equal(result.status, 0, result.stderr);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('floor guard: a missing merge base exits 2, never 0', () => {
|
|
185
|
+
const root = makeRepo();
|
|
186
|
+
const result = spawnSync(process.execPath, [guard, '--base', 'no-such-ref'], {
|
|
187
|
+
cwd: root, encoding: 'utf8',
|
|
188
|
+
});
|
|
189
|
+
assert.equal(result.status, 2, result.stderr);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('floor guard: a threshold with no readable direction is reported when it changes', () => {
|
|
193
|
+
const root = makeRepo();
|
|
194
|
+
editConstraints(root, '| W1 | no-explicit-any | @owner | 2026-11-01 |', '| W1 | no-explicit-any | @owner | 2027-11-01 |');
|
|
195
|
+
const result = runGuard(root);
|
|
196
|
+
assert.equal(result.status, 1, result.stderr);
|
|
197
|
+
assert.match(result.stderr, /threshold-changed/);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('floor guard: a renamed rule is reported as removed, with a note that its label may have changed', () => {
|
|
201
|
+
const root = makeRepo();
|
|
202
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Code coverage | >= 80% |');
|
|
203
|
+
const result = runGuard(root);
|
|
204
|
+
assert.equal(result.status, 1, result.stderr);
|
|
205
|
+
assert.match(result.stderr, /\[rule-removed\]/);
|
|
206
|
+
assert.match(result.stderr, /label changed/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('floor guard: a threshold that loses its direction words is reported as removed, with a note saying so', () => {
|
|
210
|
+
const root = makeRepo();
|
|
211
|
+
editConstraints(root, '| Coverage | >= 80% |', '| Coverage | 80% |');
|
|
212
|
+
const result = runGuard(root);
|
|
213
|
+
assert.equal(result.status, 1, result.stderr);
|
|
214
|
+
assert.match(result.stderr, /\[threshold-removed\]/);
|
|
215
|
+
assert.match(result.stderr, /direction words/);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Inside a hunk every line is content. An added line that starts with `++` shows in the diff
|
|
219
|
+
// as `+++...`, and a removed line that starts with `--` as `---...`: neither is a file header.
|
|
220
|
+
test('floor guard: an added line that starts with ++ is still checked', () => {
|
|
221
|
+
const root = makeRepo();
|
|
222
|
+
fs.writeFileSync(
|
|
223
|
+
path.join(root, 'app.js'),
|
|
224
|
+
'let n = 0;\n++n; // eslint-disable-line no-plusplus\nmodule.exports = n;\n',
|
|
225
|
+
);
|
|
226
|
+
const result = runGuard(root);
|
|
227
|
+
assert.equal(result.status, 1, result.stderr);
|
|
228
|
+
assert.match(result.stderr, /\[silenced-checker\] app\.js:/);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('floor guard: an added line that starts with "++ " does not rename the file for later hunks', () => {
|
|
232
|
+
const root = makeRepo();
|
|
233
|
+
commitFile(root, 'counter.test.js', [
|
|
234
|
+
"const assert = require('assert');",
|
|
235
|
+
'let n = 0;',
|
|
236
|
+
'n += 1;',
|
|
237
|
+
'const m = 2;',
|
|
238
|
+
'assert.equal(n, 1);',
|
|
239
|
+
'assert.equal(m, 2);',
|
|
240
|
+
'',
|
|
241
|
+
].join('\n'));
|
|
242
|
+
const file = path.join(root, 'counter.test.js');
|
|
243
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
244
|
+
fs.writeFileSync(file, text.replace('n += 1;', '++ n;').replace('assert.equal(m, 2);\n', ''));
|
|
245
|
+
const result = runGuard(root);
|
|
246
|
+
assert.equal(result.status, 1, result.stderr);
|
|
247
|
+
assert.match(result.stderr, /\[assertion-removed\] counter\.test\.js:/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('floor guard: a removed line that starts with -- is still checked', () => {
|
|
251
|
+
const root = makeRepo();
|
|
252
|
+
commitFile(root, 'countdown.test.js', [
|
|
253
|
+
"const assert = require('assert');",
|
|
254
|
+
'let n = 1;',
|
|
255
|
+
'--n; assert.equal(n, 0);',
|
|
256
|
+
'',
|
|
257
|
+
].join('\n'));
|
|
258
|
+
const file = path.join(root, 'countdown.test.js');
|
|
259
|
+
fs.writeFileSync(file, fs.readFileSync(file, 'utf8').replace('--n; assert.equal(n, 0);\n', ''));
|
|
260
|
+
const result = runGuard(root);
|
|
261
|
+
assert.equal(result.status, 1, result.stderr);
|
|
262
|
+
assert.match(result.stderr, /\[assertion-removed\] countdown\.test\.js:/);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// `git ls-files` lists only the current directory's subtree, relative to it, while `git diff`
|
|
266
|
+
// covers the whole tree with paths from the top. Run from a subfolder, the guard has to see the
|
|
267
|
+
// same files, under the same names, as it does from the top.
|
|
268
|
+
test('floor guard: run from a subfolder, an untracked file outside it is still checked', () => {
|
|
269
|
+
const root = makeRepo();
|
|
270
|
+
fs.mkdirSync(path.join(root, 'sub'));
|
|
271
|
+
fs.writeFileSync(path.join(root, 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
272
|
+
const result = runGuard(root, path.join(root, 'sub'));
|
|
273
|
+
assert.equal(result.status, 1, result.stderr);
|
|
274
|
+
assert.match(result.stderr, /\[silenced-checker\] new\.js:/);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test('floor guard: run from a subfolder, an untracked file inside it is named from the top', () => {
|
|
278
|
+
const root = makeRepo();
|
|
279
|
+
fs.mkdirSync(path.join(root, 'sub'));
|
|
280
|
+
fs.writeFileSync(path.join(root, 'sub', 'new.js'), '// @ts-ignore\nmodule.exports = 1;\n');
|
|
281
|
+
const result = runGuard(root, path.join(root, 'sub'));
|
|
282
|
+
assert.equal(result.status, 1, result.stderr);
|
|
283
|
+
assert.match(result.stderr, /\[silenced-checker\] sub\/new\.js:/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('floor guard: outside a git work tree exits 2, never 0', () => {
|
|
287
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-floor-guard-no-repo-'));
|
|
288
|
+
sandboxes.push(dir);
|
|
289
|
+
const result = runGuard(dir);
|
|
290
|
+
assert.equal(result.status, 2, result.stderr);
|
|
291
|
+
});
|