kld-sdd 2.7.8-2 → 2.7.8-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/README.md +2 -0
- package/USABILITY.md +52 -0
- package/lib/hook-install.js +75 -0
- package/lib/init.js +28 -148
- package/lib/managed-hook-hashes.json +24 -0
- package/package.json +2 -2
- package/skywalk-sdd/index.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +21 -17
- package/skywalk-sdd/lib/test-execution.cjs +20 -0
- package/templates/git-hooks/commit-msg +17 -99
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +61 -240
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/openspec/proposal.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +24 -22
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +1 -14
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@ KLD SDD OpenSpec 工程增强工具:一键初始化 AI 编辑器技能、语
|
|
|
4
4
|
|
|
5
5
|
本分支已调整使用方式:模式与测试策略自动选择,按已有授权连续处理,KB 断连时继续本地工作,发布时核对真实基线。新规则、设计依据与尚未完成的结构调整见 [使用摩擦分析](USABILITY.md)。旧安装副本需要更新后才会应用这些规则。
|
|
6
6
|
|
|
7
|
+
日常提交默认只保留本地关联 Hook;Apply 自动执行代码/Spec 检查、真实测试与交付验证。旧项目升级、备份回退、多仓和 CI 边界见 [Git Hook 调整说明](USABILITY.md#2026-09-10git-hook-收缩与交付验证)。
|
|
8
|
+
|
|
7
9
|
## 这是什么?
|
|
8
10
|
|
|
9
11
|
**SDD(Specification-Driven Development)** 是一种以文档链驱动 AI 编码的研发方法:先写清楚"要做什么",再让 AI 去实现,避免 AI 乱猜、反复返工。
|
package/USABILITY.md
CHANGED
|
@@ -102,3 +102,55 @@ SDD 应帮助团队留下三类东西:明确的业务约定、能够证明实
|
|
|
102
102
|
本机独立后端联调确认七种 Skill 的使用事件均成功写入,断网事件保留并恢复补报,服务端统计为 8 次、覆盖 7 阶段。使用统计是独立的 usage 事件,不等于测试通过、归档完成或 Spec 已入库;当前服务端按提交次数计数,网络响应丢失后的重复提交不提供 exactly-once 保证。
|
|
103
103
|
|
|
104
104
|
配置 Skill 与参考同步去除默认首目标、截短 UUID 和旧监控项目绑定步骤,修正 KB API 字段映射;切换查询目标不再引导改写团队项目身份,权限失败也不清空其他目标可能仍有效的密钥。
|
|
105
|
+
|
|
106
|
+
## 2026-09-10:Git Hook 收缩与交付验证
|
|
107
|
+
|
|
108
|
+
日常 commit 负责保存工作,Apply 收尾负责检查交付,Spec 入库负责发布知识。新安装只自动接入一个本地 `commit-msg`,它不请求 KB、不调用模型、不扫描整个工作区,也不重复执行文档 Check。
|
|
109
|
+
|
|
110
|
+
| 场景 | 当前行为 |
|
|
111
|
+
|---|---|
|
|
112
|
+
| 本次明确指定 Change | `KLD_SDD_CHANGE` 支持逗号分隔的多个 Change,校验目录后写关联 |
|
|
113
|
+
| 未显式指定 Change | 只复用能与暂存文件内容逐项匹配的检查关联;多个候选或没有证据时提示待关联 |
|
|
114
|
+
| Spec 仓尚未提交 | 允许保存代码,不写旧 HEAD 冒充当前 `Spec-Revision` |
|
|
115
|
+
| 独立 Spec 仓干净且远端关系匹配 | 写完整不可变 Spec SHA;单仓不写自指 Revision |
|
|
116
|
+
| 路径失效、Node 缺失或关联脚本超时 | 本次提交继续,显示待关联,不表示交付检查通过 |
|
|
117
|
+
| 实施收尾 | Agent 自动进行代码/Spec 检查、真实测试和 `delivery-check verify` |
|
|
118
|
+
|
|
119
|
+
关联 worker 设置 1.5 秒超时上限。这个上限不是性能测试结果;用户自有 Hook 的耗时不受 SDD 控制。
|
|
120
|
+
|
|
121
|
+
### 一个验证入口,四个内部步骤
|
|
122
|
+
|
|
123
|
+
入口是 `<spec-root>/skywalk-sdd/git-hooks/delivery-check.cjs`。Apply 自动串联,用户不需要记忆四条命令:
|
|
124
|
+
|
|
125
|
+
1. `prepare --project=<代码仓Git根> --change=<Change> --base=<实施前SHA>`:记录输入,返回本次需要检查的文件和是否可复用旧回执。
|
|
126
|
+
2. `record-review --project=... --change=... --input=<检查JSON>`:收录实际检查结果;输入改变、漏覆盖、失败、未检查都会明确显示。
|
|
127
|
+
3. `test --project=... --change=... --session-id=... --task-ids=... --command-file=<命令数组JSON>`:内部复用原 `test-run`,保留严格测试事件、命令退出码及原始输出。
|
|
128
|
+
4. `verify --project=... --change=...`:判断当前快照的证据是否完整。`ready` 与 `pending` 分开,不把 KB 入库或合并状态混入测试结果。
|
|
129
|
+
|
|
130
|
+
机器回执绑定代码仓身份、Spec 仓身份、明确 Change、代码和 Spec 内容、规则版本与配置。当前保守地纳入本仓依赖文件及全局规格;相同指纹可复用,变化后再定向取证。它尚未实现自动跨仓依赖图或逐规则的最小重查。mtime 不再作为内容一致的证据。UTF-8 文本统一 LF 以兼容 Git autocrlf,二进制和符号链接目标按原始字节检查。
|
|
131
|
+
|
|
132
|
+
检查结果分别记录接口、业务、校验和错误处理;`pass` 需要证据,`na` 需要理由,`not_checked` 与 `fail` 都不能算通过。回执记录 Agent 做过的检查及其输入,不单靠模型自报置信度判断正确性。
|
|
133
|
+
|
|
134
|
+
测试计数支持 Node TAP 与 Maven Surefire/Failsafe 的结果摘要;未知测试格式保留实际输出并显示待验证,不编造计数。只有无业务代码变更、四个维度都经检查判为不适用,且回执有明确测试不适用理由时,才允许 `testStatus=not_applicable`,不会显示“测试通过”。
|
|
135
|
+
|
|
136
|
+
### 多仓与正式提交
|
|
137
|
+
|
|
138
|
+
每个代码仓分别存回执,同一 Change 的多仓结果互不覆盖。Spec 可以先于代码完成并入库;“规格已发布”和“实现已验证”是两个状态。
|
|
139
|
+
|
|
140
|
+
需要核对已提交代码时使用 `verify --scope=commit --ref=<目标提交> --base=<实施前提交>`;独立 Spec 仓额外提供 `--spec-ref=<完整Spec SHA>`。命令读取指定 Git 对象,可以验证非当前 HEAD,不会用工作区替代。冲突解决改变内容后需重新检查和测试。
|
|
141
|
+
|
|
142
|
+
本次没有自动启用团队 CI。现有 CI 样例不等于已接入的合并门禁。团队接入时需要在受信任的执行环境生成或提供相应回执与原始测试日志;当前测试回执包含本地路径,复制到另一机器不能直接当作可移植的验证包。可以在目标 checkout 上重新执行同一入口。
|
|
143
|
+
|
|
144
|
+
### 已有项目升级与回退
|
|
145
|
+
|
|
146
|
+
重新运行安装器会更新运行时与 Skills,并迁移已识别的原版 SDD Hook。已退役或更新的 Hook 先备份到 Git 公共目录的 `kld-sdd-hook-backups/`;回退时把对应 `.bak` 复制回 hooks 同名路径并恢复执行权限。
|
|
147
|
+
|
|
148
|
+
只含 marker 不足以证明文件受管:追加过用户逻辑、Hook 符号链接、修改过插件列表、显式旧门禁设置都会保留并提示。`core.hooksPath`(包括 Husky)交给现有管理器维护;安装器不会接管其目录。普通 worktree 使用 Git 解析出的公共 hooks 目录。`hooks.config` 保留用户内容,重新安装不重置选项。
|
|
149
|
+
|
|
150
|
+
空目录首次安装声明正确的 mono 布局;此前生成的原样多仓占位配置可备份后自动迁移,真实团队配置不会被猜测改写。只有一个无效 `.git` 目录时明确提示 Hook 未安装,本地文档与运行时仍可部署。
|
|
151
|
+
|
|
152
|
+
### 上报与知识链路的边界
|
|
153
|
+
|
|
154
|
+
原 Skill 使用上报、严格测试事件和 KB 上传/查询 API 保留。默认移除旧 pre-commit 后,不再采集那条仅凭“本次提交有代码而没有文档”推断的 bypass 事件;这类旧统计与真实 Skill 使用量不能混为一谈。
|
|
155
|
+
|
|
156
|
+
`kb-upload --validate-only` 在未连接 KB 时可做本地语义校验,并明确说明未验证权限、发布基线及入库状态;正式上传仍严格检查项目与目标配置,连接或身份异常不能伪装为入库成功。UUID 继续完整保存,仅修正帮助中残留的 8 位说明。
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { execFileSync } = require('child_process');
|
|
6
|
+
const known = require('./managed-hook-hashes.json');
|
|
7
|
+
const hash = text => crypto.createHash('sha256').update(text).digest('hex');
|
|
8
|
+
|
|
9
|
+
function section(text, name) {
|
|
10
|
+
let active = false;
|
|
11
|
+
const result = [];
|
|
12
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
13
|
+
const line = raw.trim();
|
|
14
|
+
if (line.startsWith('[')) active = line === `[${name}]`;
|
|
15
|
+
else if (active && line && !line.startsWith('#')) result.push(line);
|
|
16
|
+
}
|
|
17
|
+
return result.join('\n');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function updateHook(root, name, pkgPath, configFile) {
|
|
21
|
+
const git = args => execFileSync('git', args, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
22
|
+
let configured = '';
|
|
23
|
+
try { configured = git(['config', '--get', 'core.hooksPath']); } catch { /* default path */ }
|
|
24
|
+
if (configured) {
|
|
25
|
+
console.log(` ℹ️ 保留 core.hooksPath=${configured};由现有 Hook 管理器接入,SDD 不覆盖它`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
let hookPath;
|
|
29
|
+
try { hookPath = path.resolve(root, git(['rev-parse', '--git-path', `hooks/${name}`])); }
|
|
30
|
+
catch { console.log(` ⚠️ ${root} 不是有效 Git 仓库,未安装 ${name} Hook`); return; }
|
|
31
|
+
const templateDir = path.join(pkgPath, 'templates/git-hooks');
|
|
32
|
+
const content = fs.readFileSync(path.join(templateDir, name));
|
|
33
|
+
const stat = fs.lstatSync(hookPath, { throwIfNoEntry: false });
|
|
34
|
+
if (stat && !stat.isFile()) { console.log(` ℹ️ 保留 Hook 链接或特殊文件:${hookPath}`); return; }
|
|
35
|
+
const existing = stat ? fs.readFileSync(hookPath) : null;
|
|
36
|
+
const customizedConfig = configFile && fs.existsSync(configFile)
|
|
37
|
+
&& section(fs.readFileSync(configFile, 'utf8'), name) !== section(fs.readFileSync(path.join(templateDir, 'hooks.config'), 'utf8'), name);
|
|
38
|
+
const explicitGate = name !== 'commit-msg' && configFile && fs.existsSync(configFile)
|
|
39
|
+
&& /^(?:strict|skip|warn_only)\s*=\s*(?:1|true|yes)$/m.test(section(fs.readFileSync(configFile, 'utf8'), 'consistency'));
|
|
40
|
+
if (existing && (explicitGate || customizedConfig ||
|
|
41
|
+
(!known[name].includes(hash(existing)) && hash(existing) !== hash(content)))) {
|
|
42
|
+
console.log(` ℹ️ 保留自定义 Hook:${hookPath};可审阅后自行移除旧 SDD 调用`);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (existing && hash(existing) === hash(content) && name === 'commit-msg') return;
|
|
46
|
+
if (existing) {
|
|
47
|
+
const common = path.resolve(root, git(['rev-parse', '--git-common-dir']));
|
|
48
|
+
const backup = path.join(common, 'kld-sdd-hook-backups', `${name}.${hash(existing)}.bak`);
|
|
49
|
+
fs.mkdirSync(path.dirname(backup), { recursive: true });
|
|
50
|
+
if (!fs.existsSync(backup)) fs.copyFileSync(hookPath, backup);
|
|
51
|
+
console.log(` ✓ 旧 Hook 已备份:${backup}`);
|
|
52
|
+
}
|
|
53
|
+
if (name !== 'commit-msg') {
|
|
54
|
+
if (existing) fs.unlinkSync(hookPath);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
58
|
+
fs.writeFileSync(hookPath, content, { mode: 0o755 });
|
|
59
|
+
fs.chmodSync(hookPath, 0o755);
|
|
60
|
+
console.log(' ✓ commit-msg 已更新为本地关联提示;交付验证由 Apply 收尾执行');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function deployRuntime(specRoot, pkgPath) {
|
|
64
|
+
const dir = path.join(specRoot, 'skywalk-sdd/git-hooks');
|
|
65
|
+
const source = path.join(pkgPath, 'templates/git-hooks');
|
|
66
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
67
|
+
for (const file of fs.readdirSync(source)) {
|
|
68
|
+
const target = path.join(dir, file);
|
|
69
|
+
const stat = fs.lstatSync(target, { throwIfNoEntry: false });
|
|
70
|
+
if (stat && !stat.isFile()) { console.log(` ℹ️ 保留运行时链接:${target}`); continue; }
|
|
71
|
+
if (file.endsWith('.cjs') || (file === 'hooks.config' && !stat)) fs.copyFileSync(path.join(source, file), target);
|
|
72
|
+
}
|
|
73
|
+
return path.join(dir, 'hooks.config');
|
|
74
|
+
}
|
|
75
|
+
module.exports = { updateHook, deployRuntime };
|
package/lib/init.js
CHANGED
|
@@ -1151,6 +1151,8 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1151
1151
|
console.log(' ⚠️ kb-sync-identity.cjs 源文件缺失,重新入库身份同步不可用');
|
|
1152
1152
|
}
|
|
1153
1153
|
|
|
1154
|
+
require('./hook-install.js').deployRuntime(cwd, pkgPath);
|
|
1155
|
+
|
|
1154
1156
|
console.log(` ✓ 部署 skywalk-sdd/ 运行时(events/state 数据目录 + ${deployedModules} 个模块)`);
|
|
1155
1157
|
vlog(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics|semantic-identity|semantic-reconcile|semantic-check');
|
|
1156
1158
|
vlog(' ✓ openspec: node skywalk-sdd/openspec-shim.cjs list(自动 cd 到 *-sdd-specs)');
|
|
@@ -1161,76 +1163,12 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
|
|
|
1161
1163
|
return true;
|
|
1162
1164
|
}
|
|
1163
1165
|
|
|
1164
|
-
/**
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
*
|
|
1168
|
-
* shell hook 运行时通过 git config sdd.specPath / .sdd-spec-root / 向上搜索
|
|
1169
|
-
* 定位 spec 仓的 skywalk-sdd/git-hooks/ 目录,兼容单仓/多仓/mono 布局。
|
|
1170
|
-
*
|
|
1171
|
-
* @param {string} cwd - Git 仓库根目录
|
|
1172
|
-
* @param {string} hookName - hook 名称(如 'pre-commit'、'pre-push')
|
|
1173
|
-
* @param {string} pkgPath - kld-sdd 包路径(用于读取模板)
|
|
1174
|
-
*/
|
|
1175
|
-
function installSddShellHook(cwd, hookName, pkgPath = getPackagePath()) {
|
|
1176
|
-
const hooksDir = path.join(cwd, '.git', 'hooks');
|
|
1177
|
-
if (!fs.existsSync(hooksDir)) {
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const templatePath = path.join(pkgPath, 'templates', 'git-hooks', hookName);
|
|
1182
|
-
if (!fs.existsSync(templatePath)) {
|
|
1183
|
-
console.log(` ⚠️ Hook 模板不存在: ${templatePath}`);
|
|
1184
|
-
return;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const hookPath = path.join(hooksDir, hookName);
|
|
1188
|
-
const marker = 'KLD SDD quality gate';
|
|
1189
|
-
const content = fs.readFileSync(templatePath, 'utf8');
|
|
1190
|
-
|
|
1191
|
-
if (fs.existsSync(hookPath)) {
|
|
1192
|
-
const existing = fs.readFileSync(hookPath, 'utf8');
|
|
1193
|
-
if (existing.includes(marker)) {
|
|
1194
|
-
// 已是 SDD hook:允许覆盖升级
|
|
1195
|
-
fs.writeFileSync(hookPath, content, 'utf8');
|
|
1196
|
-
try {
|
|
1197
|
-
// 0o700:仅属主可执行(git 以属主身份执行 hook),避免 S2612 对组/其他用户放权的告警
|
|
1198
|
-
fs.chmodSync(hookPath, 0o700);
|
|
1199
|
-
} catch {
|
|
1200
|
-
// ignore
|
|
1201
|
-
}
|
|
1202
|
-
console.log(` ✓ 更新 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
console.log(` ℹ️ .git/hooks/${hookName} 已存在且非 SDD 受管 hook,已保留不覆盖`);
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
fs.writeFileSync(hookPath, content, 'utf8');
|
|
1210
|
-
try {
|
|
1211
|
-
fs.chmodSync(hookPath, 0o755);
|
|
1212
|
-
} catch {
|
|
1213
|
-
// Windows 上 chmod 可能没有实际效果
|
|
1214
|
-
}
|
|
1215
|
-
console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁(多脚本)`);
|
|
1166
|
+
/** 安全升级 commit-msg、备份并退役受管旧门禁,保留用户 Hook 与管理器。 */
|
|
1167
|
+
function installSddShellHook(cwd, hookName, pkgPath = getPackagePath(), configFile) {
|
|
1168
|
+
return require('./hook-install.js').updateHook(cwd, hookName, pkgPath, configFile);
|
|
1216
1169
|
}
|
|
1217
1170
|
|
|
1218
|
-
/**
|
|
1219
|
-
* 部署代码仓全部 Git hooks(commit-msg + pre-commit + pre-push)。
|
|
1220
|
-
*
|
|
1221
|
-
* 统一入口,封装三种 hook 的部署逻辑。代码仓**不落 skywalk-sdd/**,
|
|
1222
|
-
* 只在 .git/hooks/ 安装 shell hook 模板,远程引用 spec 仓的 .cjs 脚本:
|
|
1223
|
-
*
|
|
1224
|
-
* - **spec 仓**:确保 skywalk-sdd/git-hooks/ 下有全部 .cjs 脚本
|
|
1225
|
-
* (commit-msg-sdd-trailer + pre-commit-* + pre-push-* + ontology)。
|
|
1226
|
-
* - **代码仓 .git/hooks/**:仅安装 3 个 shell hook 模板,运行时通过
|
|
1227
|
-
* git config sdd.specPath / .sdd-spec-root / 向上搜索定位 spec 仓的脚本。
|
|
1228
|
-
*
|
|
1229
|
-
* @param {string} codeRepoRoot - 代码仓根目录
|
|
1230
|
-
* @param {string} specRepoRoot - spec 包裹包根目录
|
|
1231
|
-
* @param {object} options - { pkgPath }
|
|
1232
|
-
* @returns {{ ok: boolean, message?: string }}
|
|
1233
|
-
*/
|
|
1171
|
+
/** 代码仓只安装 commit-msg;验证脚本集中部署在 Spec 包。 */
|
|
1234
1172
|
function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
|
|
1235
1173
|
const cwd = path.resolve(codeRepoRoot);
|
|
1236
1174
|
const specRoot = path.resolve(specRepoRoot);
|
|
@@ -1240,32 +1178,11 @@ function deployCodeRepoHooks(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1240
1178
|
return { ok: false, message: '不是 Git 仓库' };
|
|
1241
1179
|
}
|
|
1242
1180
|
|
|
1243
|
-
// ── 1. 确保 spec 仓有全部 hook 脚本 + ontology ──
|
|
1244
|
-
// commit-msg .cjs + ontology(ensureSpecCommitMsgScript 已处理)
|
|
1245
1181
|
ensureSpecCommitMsgScript(specRoot, pkgPath);
|
|
1246
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
fs.mkdirSync(specHooksDir, { recursive: true });
|
|
1251
|
-
}
|
|
1252
|
-
// 复制全部 .cjs 脚本 + hooks.config 配置文件
|
|
1253
|
-
const hookScripts = fs.existsSync(gitHooksTemplateDir)
|
|
1254
|
-
? fs.readdirSync(gitHooksTemplateDir).filter(f => f.endsWith('.cjs') || f === 'hooks.config')
|
|
1255
|
-
: [];
|
|
1256
|
-
for (const script of hookScripts) {
|
|
1257
|
-
const src = path.join(gitHooksTemplateDir, script);
|
|
1258
|
-
const dst = path.join(specHooksDir, script);
|
|
1259
|
-
// 始终覆盖,与 shell hook 的 marker 覆盖策略保持一致
|
|
1260
|
-
fs.copyFileSync(src, dst);
|
|
1261
|
-
}
|
|
1262
|
-
console.log(` ✓ 确保 spec 仓 skywalk-sdd/git-hooks/ 脚本齐全(${hookScripts.length} 个文件)`);
|
|
1263
|
-
|
|
1264
|
-
// ── 2. 安装三个 shell hooks 到代码仓 .git/hooks/ ──
|
|
1265
|
-
// 模板 hook 运行时自动定位 spec 仓的 .cjs 脚本
|
|
1266
|
-
installSddShellHook(cwd, 'commit-msg', pkgPath);
|
|
1267
|
-
installSddShellHook(cwd, 'pre-commit', pkgPath);
|
|
1268
|
-
installSddShellHook(cwd, 'pre-push', pkgPath);
|
|
1182
|
+
const configFile = require('./hook-install.js').deployRuntime(specRoot, pkgPath);
|
|
1183
|
+
for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
|
|
1184
|
+
installSddShellHook(cwd, name, pkgPath, configFile);
|
|
1185
|
+
}
|
|
1269
1186
|
|
|
1270
1187
|
return { ok: true };
|
|
1271
1188
|
}
|
|
@@ -1292,64 +1209,21 @@ function ensureSpecCommitMsgScript(specRepoRoot, pkgPath = getPackagePath()) {
|
|
|
1292
1209
|
return { ok: true, scriptPath, deployed: true };
|
|
1293
1210
|
}
|
|
1294
1211
|
|
|
1295
|
-
/**
|
|
1296
|
-
* 部署 Git hooks / CI 兜底模板
|
|
1297
|
-
* 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
|
|
1298
|
-
*
|
|
1299
|
-
* @param {string} [targetCwd]
|
|
1300
|
-
* @param {{ mode?: 'full'|'commit-msg-only' }} [options]
|
|
1301
|
-
* full: spec 包裹包(pre-commit/pre-push/commit-msg + CI)
|
|
1302
|
-
* commit-msg-only: 仅安装 commit-msg hook(跳过 pre-commit/pre-push)
|
|
1303
|
-
*/
|
|
1212
|
+
/** 部署验证运行时与轻量 Hook;不自动启用 CI。options 为旧调用兼容保留。 */
|
|
1304
1213
|
function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
|
|
1305
1214
|
const pkgPath = getPackagePath();
|
|
1306
1215
|
const cwd = path.resolve(targetCwd);
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
return false;
|
|
1216
|
+
const configFile = require('./hook-install.js').deployRuntime(cwd, pkgPath);
|
|
1217
|
+
if (!workspaceLayout.isGitRepo(cwd)) return false;
|
|
1218
|
+
for (const name of ['commit-msg', 'pre-commit', 'pre-push']) {
|
|
1219
|
+
installSddShellHook(cwd, name, pkgPath, configFile);
|
|
1312
1220
|
}
|
|
1313
|
-
|
|
1314
|
-
const dataDir = path.join(cwd, 'skywalk-sdd');
|
|
1315
|
-
const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
|
|
1316
|
-
const ciTemplateDir = path.join(pkgPath, 'templates', 'ci');
|
|
1317
|
-
const targetGitHooksDir = path.join(dataDir, 'git-hooks');
|
|
1318
|
-
const targetCiDir = path.join(dataDir, 'ci');
|
|
1319
|
-
|
|
1320
|
-
if (fs.existsSync(gitHooksTemplateDir)) {
|
|
1321
|
-
// 复制 .cjs 脚本 + hooks.config 到 skywalk-sdd/git-hooks/
|
|
1322
|
-
// shell hook 模板(commit-msg/pre-commit/pre-push)不落此目录
|
|
1323
|
-
if (!fs.existsSync(targetGitHooksDir)) {
|
|
1324
|
-
fs.mkdirSync(targetGitHooksDir, { recursive: true });
|
|
1325
|
-
}
|
|
1326
|
-
for (const file of fs.readdirSync(gitHooksTemplateDir)) {
|
|
1327
|
-
if (file.endsWith('.cjs') || file === 'hooks.config') {
|
|
1328
|
-
fs.copyFileSync(path.join(gitHooksTemplateDir, file), path.join(targetGitHooksDir, file));
|
|
1329
|
-
}
|
|
1330
|
-
}
|
|
1331
|
-
console.log(' ✓ 部署 skywalk-sdd/git-hooks/(.cjs 脚本 + hooks.config)');
|
|
1332
|
-
} else {
|
|
1333
|
-
console.log(` ⚠️ Git hooks 模板缺失: ${gitHooksTemplateDir}`);
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
if (mode === 'full' && fs.existsSync(ciTemplateDir)) {
|
|
1337
|
-
copyDir(ciTemplateDir, targetCiDir);
|
|
1338
|
-
console.log(' ✓ 部署 skywalk-sdd/ci/');
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
if (mode === 'full') {
|
|
1342
|
-
installSddShellHook(cwd, 'pre-commit', pkgPath);
|
|
1343
|
-
installSddShellHook(cwd, 'pre-push', pkgPath);
|
|
1344
|
-
}
|
|
1345
|
-
installSddShellHook(cwd, 'commit-msg', pkgPath);
|
|
1346
|
-
|
|
1347
|
-
console.log('✅ Git hooks 已就绪');
|
|
1221
|
+
// CI is an explicit team integration; installing SDD does not enable a merge gate.
|
|
1348
1222
|
return true;
|
|
1349
1223
|
}
|
|
1350
1224
|
|
|
1351
1225
|
/**
|
|
1352
|
-
* 代码仓轻量接入:写 .sdd.yaml + sdd.specPath +
|
|
1226
|
+
* 代码仓轻量接入:写 .sdd.yaml + sdd.specPath + 轻量 commit-msg。
|
|
1353
1227
|
* 代码仓不落 skywalk-sdd/,shell hook 远程引用 spec 仓 skywalk-sdd/git-hooks/ 下的 .cjs 脚本。
|
|
1354
1228
|
*/
|
|
1355
1229
|
function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
@@ -1364,7 +1238,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1364
1238
|
return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
|
|
1365
1239
|
}
|
|
1366
1240
|
|
|
1367
|
-
console.log(`🔗
|
|
1241
|
+
console.log(`🔗 轻量接入代码仓(本地关联 Hook): ${label}`);
|
|
1368
1242
|
|
|
1369
1243
|
const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
|
|
1370
1244
|
const sddYamlPath = path.join(cwd, '.sdd.yaml');
|
|
@@ -1390,7 +1264,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
|
|
|
1390
1264
|
}
|
|
1391
1265
|
console.log(` ✓ sdd.specPath = ${linked.path}`);
|
|
1392
1266
|
|
|
1393
|
-
//
|
|
1267
|
+
// 部署轻量关联 Hook,并迁移可识别的旧门禁
|
|
1394
1268
|
deployCodeRepoHooks(cwd, specRoot, options);
|
|
1395
1269
|
|
|
1396
1270
|
return { ok: true, repo: label, path: cwd, specPath: linked.path };
|
|
@@ -1470,7 +1344,13 @@ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
|
|
|
1470
1344
|
: '';
|
|
1471
1345
|
|
|
1472
1346
|
if (fs.existsSync(target)) {
|
|
1473
|
-
|
|
1347
|
+
let existing = fs.readFileSync(target, 'utf8');
|
|
1348
|
+
const placeholder = fs.readFileSync(path.join(getPackagePath(), 'templates/dot-sdd.yaml'), 'utf8');
|
|
1349
|
+
if (existing === placeholder && specRel) {
|
|
1350
|
+
fs.copyFileSync(target, target + '.pre-mono.bak');
|
|
1351
|
+
existing = `version: 1\nlayout: mono\n`;
|
|
1352
|
+
fs.writeFileSync(target, existing, 'utf8');
|
|
1353
|
+
}
|
|
1474
1354
|
if (/^layout:\s*mono\s*$/m.test(existing)) {
|
|
1475
1355
|
if (specRel && !new RegExp(`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(existing)) {
|
|
1476
1356
|
// 已有 mono 但缺/错 spec_path:补写相对路径
|
|
@@ -1927,7 +1807,7 @@ async function main() {
|
|
|
1927
1807
|
removeOuterSkywalk(workspaceRoot);
|
|
1928
1808
|
|
|
1929
1809
|
deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
|
|
1930
|
-
//
|
|
1810
|
+
// 部署轻量关联 Hook,并迁移可识别的旧门禁
|
|
1931
1811
|
// 单仓 mono:hook 通过 .sdd-spec-root 定位包裹包的 skywalk-sdd/git-hooks/
|
|
1932
1812
|
deployCodeRepoHooks(workspaceRoot, specPackage.abs);
|
|
1933
1813
|
const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
|
|
@@ -1947,7 +1827,7 @@ async function main() {
|
|
|
1947
1827
|
});
|
|
1948
1828
|
writeSddSpecRootHint(workspaceRoot, specPackage.abs);
|
|
1949
1829
|
removeOuterSkywalk(workspaceRoot);
|
|
1950
|
-
|
|
1830
|
+
deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
|
|
1951
1831
|
}
|
|
1952
1832
|
}
|
|
1953
1833
|
} else if (!skipOpenspec) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pre-commit": [
|
|
3
|
+
"497f012da027df5c26af3c501126e9a1c26276b038ee3b1edaaf5ab4401aa95c",
|
|
4
|
+
"7322d9aecafb497a5f40171a6bd7daeaadd7d2056b4a253cb0ee1a128faea36e",
|
|
5
|
+
"8fcdd74a3d2afaa823f9038c477c9dfd6fe53e2bccbe48cecd01535794862e4a",
|
|
6
|
+
"c8546c6c96adc3ecc32a15a689c59189543c2454f47998c46fbd5a0bf810cf30",
|
|
7
|
+
"e3b6776af51df76ff04b528152d0049a6c45d94d96a19bd2bc5ca2cc9786b22c"
|
|
8
|
+
],
|
|
9
|
+
"pre-push": [
|
|
10
|
+
"405debb5e6f9186e95b727e6013b06ef3e06c598b6d2b96678d34554d3530e8c",
|
|
11
|
+
"461abd16663757c5a092103ac2ecae8453891c0c817d1c763369e0cdd0a8ec7b",
|
|
12
|
+
"80f710ff647ce8b216fb1c081fb3e22f6ee8be25847b6d02921668ba83fb1965",
|
|
13
|
+
"d49eb8794fde4b0d7847a876cc7083d161b290a44990ff8a036c2ebb8a00ec21",
|
|
14
|
+
"e6e125cbf9d7424c01b249ce03197cef9e1923ed2974a53656084488a0de60c4",
|
|
15
|
+
"ebf628481ae93ac0c4c6649b204449f6825291dc6b6550a71563bebec1f3ad92"
|
|
16
|
+
],
|
|
17
|
+
"commit-msg": [
|
|
18
|
+
"15083d5165bff965ab2feba9a55dfedcecd54cb577c06fd11f4d1b9672d978b6",
|
|
19
|
+
"3b0ee15f5a126d159e14c5a3681cb7820cbf9c5e0c134ee6812053bc34330b85",
|
|
20
|
+
"6db7adbf17905bf14915d7e57a3c5ac46030ee3282fea5d5b211c80065406d48",
|
|
21
|
+
"9c5a86761b36d71b655f804a822eff675a244175799736c803e1f7379c638609",
|
|
22
|
+
"fc390776720393f100712fa02d5e143c2927fbb0d605a4a0242824ff7ea84574"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.7.8-
|
|
3
|
+
"version": "2.7.8-3",
|
|
4
4
|
"description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"bench:check": "node test/benchmark-check.cjs",
|
|
12
|
-
"test": "node --test test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs skywalk-sdd/usage-reporter.test.cjs skywalk-sdd/usage-reporting.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
|
|
12
|
+
"test": "node --test test/delivery-hooks.test.cjs test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs skywalk-sdd/usage-reporter.test.cjs skywalk-sdd/usage-reporting.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"kld",
|
package/skywalk-sdd/index.cjs
CHANGED
|
@@ -8610,7 +8610,7 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
8610
8610
|
check-task 扫描变更目录 tasks.md 并勾选指定 task_id
|
|
8611
8611
|
archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
|
|
8612
8612
|
kb-upload 知识库上传(文件夹自动打包 / zip 直传),支持随时入库
|
|
8613
|
-
semantic-identity 生成或复用实体/版本 ID
|
|
8613
|
+
semantic-identity 生成或复用实体/版本 ID(完整 UUID,界面可显示短标识);无需网络和中央 ID 服务
|
|
8614
8614
|
semantic-scan 只读解析并校验当前 Change
|
|
8615
8615
|
semantic-reconcile 全量对账文件与本地工作态本体实例
|
|
8616
8616
|
semantic-check 全量对账并在通过时标记 pending;--include-tasks 同时返回任务状态、评审输入清单和程序耗时
|
|
@@ -332,6 +332,7 @@ async function main() {
|
|
|
332
332
|
try {
|
|
333
333
|
// ── Zip 模式 ──
|
|
334
334
|
if (args.package) {
|
|
335
|
+
if (args['validate-only']) throw new Error('--validate-only 仅支持 --folder/--change 本地语义校验;未执行归档上传');
|
|
335
336
|
const zipPath = path.resolve(args.package);
|
|
336
337
|
if (!fs.existsSync(zipPath)) {
|
|
337
338
|
throw new Error(`zip 文件不存在: ${zipPath}`);
|
|
@@ -370,9 +371,12 @@ async function main() {
|
|
|
370
371
|
changeName = path.basename(folderPath);
|
|
371
372
|
}
|
|
372
373
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
374
|
+
const changesRoot = fs.realpathSync(path.resolve(specRoot, 'openspec', 'changes'));
|
|
375
|
+
const expectedPath = path.resolve(changesRoot, changeName);
|
|
376
|
+
const expectedFolder = fs.existsSync(expectedPath) ? fs.realpathSync(expectedPath) : expectedPath;
|
|
377
|
+
const suppliedFolder = fs.existsSync(folderPath) ? fs.realpathSync(folderPath) : path.resolve(folderPath);
|
|
378
|
+
if (suppliedFolder !== expectedFolder || !expectedFolder.startsWith(changesRoot + path.sep)) {
|
|
379
|
+
throw new Error('文件夹上传仅接受当前 Spec 仓 openspec/changes/<change>;已归档内容请用 --package 指定归档包');
|
|
376
380
|
}
|
|
377
381
|
|
|
378
382
|
if (!fs.existsSync(folderPath)) {
|
|
@@ -389,19 +393,6 @@ async function main() {
|
|
|
389
393
|
// 1. 校验必需文件
|
|
390
394
|
validateRequiredFiles(folderPath, minimumLevel);
|
|
391
395
|
|
|
392
|
-
// 2. 解析 project_id
|
|
393
|
-
const projectId = resolveProjectId(specRoot);
|
|
394
|
-
console.log(`✓ project_id: ${projectId}`);
|
|
395
|
-
|
|
396
|
-
// 3. 读取 KB 配置 + 预检
|
|
397
|
-
const state = readKbState(specRoot);
|
|
398
|
-
if (!state.apiKey) {
|
|
399
|
-
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
400
|
-
}
|
|
401
|
-
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
402
|
-
preCheckProjectId(projectId, state, target);
|
|
403
|
-
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
404
|
-
|
|
405
396
|
// 4. 仅校验模式(含语义校验)
|
|
406
397
|
if (args['validate-only']) {
|
|
407
398
|
const runtime = require('./ontology/runtime.cjs');
|
|
@@ -417,10 +408,23 @@ async function main() {
|
|
|
417
408
|
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
418
409
|
}
|
|
419
410
|
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
420
|
-
console.log('\n✓
|
|
411
|
+
console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
|
|
421
412
|
return;
|
|
422
413
|
}
|
|
423
414
|
|
|
415
|
+
// 2. 解析 project_id
|
|
416
|
+
const projectId = resolveProjectId(specRoot);
|
|
417
|
+
console.log(`✓ project_id: ${projectId}`);
|
|
418
|
+
|
|
419
|
+
// 3. 读取 KB 配置 + 预检
|
|
420
|
+
const state = readKbState(specRoot);
|
|
421
|
+
if (!state.apiKey) {
|
|
422
|
+
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
423
|
+
}
|
|
424
|
+
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
425
|
+
preCheckProjectId(projectId, state, target);
|
|
426
|
+
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
427
|
+
|
|
424
428
|
// 5. 构建 Archive Package
|
|
425
429
|
console.log('');
|
|
426
430
|
const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
|
|
@@ -17,6 +17,26 @@ function parseCounts(output) {
|
|
|
17
17
|
&& total === passed + failed + (skipped || 0) + (todo || 0)) {
|
|
18
18
|
return { counts_known: true, passed, failed, skipped: (skipped || 0) + (todo || 0) };
|
|
19
19
|
}
|
|
20
|
+
// Surefire/Failsafe print per-class counts and a Results section. Count only
|
|
21
|
+
// the latter, once per module/execution, to avoid double-counting classes.
|
|
22
|
+
const summaries = [];
|
|
23
|
+
let inResults = false;
|
|
24
|
+
for (const raw of output.replace(/\u001b\[[0-9;]*m/g, '').split(/\r?\n/)) {
|
|
25
|
+
const line = raw.replace(/^\[(?:INFO|WARNING|ERROR)\]\s*/, '').trim();
|
|
26
|
+
if (line === 'Results:') { inResults = true; continue; }
|
|
27
|
+
if (!inResults) continue;
|
|
28
|
+
const match = /^Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)\b/.exec(line);
|
|
29
|
+
if (!match) continue;
|
|
30
|
+
const [, count, failures, errors, ignored] = match.map(Number);
|
|
31
|
+
if (count < failures + errors + ignored) return { counts_known: false };
|
|
32
|
+
summaries.push({ passed: count - failures - errors - ignored, failed: failures + errors, skipped: ignored });
|
|
33
|
+
inResults = false;
|
|
34
|
+
}
|
|
35
|
+
if (summaries.length) {
|
|
36
|
+
return summaries.reduce((sum, item) => ({ counts_known: true, passed: sum.passed + item.passed,
|
|
37
|
+
failed: sum.failed + item.failed, skipped: sum.skipped + item.skipped }),
|
|
38
|
+
{ counts_known: true, passed: 0, failed: 0, skipped: 0 });
|
|
39
|
+
}
|
|
20
40
|
return { counts_known: false };
|
|
21
41
|
}
|
|
22
42
|
|