@dommaker/harness 1.2.2 → 1.3.0
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 +12 -0
- package/dist/cli/commands/definitions.js +1 -1
- package/dist/cli/commands/definitions.js.map +1 -1
- package/dist/cli/commands/failure.d.ts.map +1 -1
- package/dist/cli/commands/failure.js +2 -1
- package/dist/cli/commands/failure.js.map +1 -1
- package/dist/cli/commands/knowledge.d.ts.map +1 -1
- package/dist/cli/commands/knowledge.js +24 -1
- package/dist/cli/commands/knowledge.js.map +1 -1
- package/dist/cli/commands/release.d.ts.map +1 -1
- package/dist/cli/commands/release.js +6 -20
- package/dist/cli/commands/release.js.map +1 -1
- package/dist/cli/commands/sync-docs/agents-syncer.js +26 -14
- package/dist/cli/commands/sync-docs/agents-syncer.js.map +1 -1
- package/dist/completion-checkers/index.d.ts +3 -3
- package/dist/completion-checkers/index.js +3 -3
- package/dist/completion-checkers/phase-format.d.ts +1 -1
- package/dist/completion-checkers/phase-format.js +2 -2
- package/dist/completion-checkers/phase-format.js.map +1 -1
- package/dist/completion-checkers/tdd-chain.d.ts +1 -1
- package/dist/completion-checkers/tdd-chain.js +2 -2
- package/dist/completion-checkers/tdd-chain.js.map +1 -1
- package/dist/completion-checkers/types.d.ts +5 -5
- package/dist/completion-checkers/types.d.ts.map +1 -1
- package/dist/completion-checkers/types.js +2 -2
- package/dist/failure/recorder.d.ts +1 -1
- package/dist/failure/recorder.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/knowledge/index-generator.js +1 -1
- package/dist/knowledge/index-generator.js.map +1 -1
- package/dist/release/index.d.ts +3 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +11 -0
- package/dist/release/index.js.map +1 -0
- package/dist/release/integrity.d.ts +65 -0
- package/dist/release/integrity.d.ts.map +1 -0
- package/dist/release/integrity.js +151 -0
- package/dist/release/integrity.js.map +1 -0
- package/dist/test-setup/mkdtemp-cleanup.d.ts +9 -0
- package/dist/test-setup/mkdtemp-cleanup.d.ts.map +1 -0
- package/dist/test-setup/mkdtemp-cleanup.js +90 -0
- package/dist/test-setup/mkdtemp-cleanup.js.map +1 -0
- package/dist/types/failure.d.ts +7 -0
- package/dist/types/failure.d.ts.map +1 -1
- package/dist/types/failure.js +8 -1
- package/dist/types/failure.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/failure-recorder.test.ts +9 -0
- package/src/__tests__/public-exports.test.ts +3 -0
- package/src/cli/commands/__tests__/knowledge.test.ts +35 -3
- package/src/cli/commands/__tests__/release.test.ts +22 -1
- package/src/cli/commands/__tests__/sync-docs-agents.test.ts +33 -10
- package/src/cli/commands/definitions.ts +1 -1
- package/src/cli/commands/failure.ts +2 -1
- package/src/cli/commands/knowledge.ts +26 -1
- package/src/cli/commands/release.ts +6 -20
- package/src/cli/commands/sync-docs/agents-syncer.ts +26 -13
- package/src/completion-checkers/CONTEXT.md +1 -1
- package/src/completion-checkers/__tests__/contract-presence.test.ts +1 -1
- package/src/completion-checkers/__tests__/phase-format.test.ts +1 -1
- package/src/completion-checkers/__tests__/tdd-chain.test.ts +2 -2
- package/src/completion-checkers/index.ts +3 -3
- package/src/completion-checkers/phase-format.ts +2 -2
- package/src/completion-checkers/tdd-chain.ts +2 -2
- package/src/completion-checkers/types.ts +5 -5
- package/src/failure/recorder.ts +1 -1
- package/src/index.ts +11 -2
- package/src/knowledge/__tests__/index-generator.test.ts +2 -2
- package/src/knowledge/index-generator.ts +1 -1
- package/src/release/CONTEXT.md +23 -0
- package/src/release/__tests__/integrity.test.ts +157 -0
- package/src/release/index.ts +13 -0
- package/src/release/integrity.ts +141 -0
- package/src/test-setup/__tests__/mkdtemp-cleanup.test.ts +45 -0
- package/src/test-setup/mkdtemp-cleanup.ts +83 -0
- package/src/types/failure.ts +8 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 协议(写死为机制本体,不进配置):
|
|
5
5
|
* - 实现 commit 必须带 trailer `Tested-By: <sha>`
|
|
6
|
-
* - 被引 sha 须 a)
|
|
6
|
+
* - 被引 sha 须 a) 在本提交集内 b) 序列位置在本 commit 之前(比位置不比时间戳)c) 其文件清单命中 test_globs
|
|
7
7
|
* - 豁免:trailer `Tests: none` → waiver(放行记台账);纯非代码/纯测试 commit 天然免检
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -44,7 +44,7 @@ export function verifyTddChain(
|
|
|
44
44
|
const ref = m[1].toLowerCase();
|
|
45
45
|
const j = commits.findIndex((c) => c.sha.toLowerCase().startsWith(ref));
|
|
46
46
|
if (j === -1) {
|
|
47
|
-
return { sha: commit.sha, verdict: 'violation', reason: `Tested-By 引用 ${ref}
|
|
47
|
+
return { sha: commit.sha, verdict: 'violation', reason: `Tested-By 引用 ${ref} 不在本提交集内` };
|
|
48
48
|
}
|
|
49
49
|
if (j >= i) {
|
|
50
50
|
return { sha: commit.sha, verdict: 'violation', reason: `Tested-By 引用 ${ref} 位置不在本 commit 之前` };
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Completion Checkers
|
|
2
|
+
* Completion Checkers 类型定义
|
|
3
3
|
*
|
|
4
4
|
* 三个纯判定函数共享的输入/输出/配置类型。
|
|
5
5
|
* 与 ConstraintCheck 闭环注册表无关:直接 export,不注册、不碰 checkers/index.ts。
|
|
6
|
-
* 函数不碰文件系统与 git,commits 由调用方(
|
|
6
|
+
* 函数不碰文件系统与 git,commits 由调用方(git log)供给。
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** 提交集内的单个提交输入(有序,base..HEAD 升序) */
|
|
10
10
|
export interface CommitInput {
|
|
11
11
|
/** 完整 sha */
|
|
12
12
|
sha: string;
|
|
@@ -49,7 +49,7 @@ export interface PhaseFormatResult {
|
|
|
49
49
|
|
|
50
50
|
/** verifyContractPresence 上下文(类型→字段的判定方法映射是代码不是配置) */
|
|
51
51
|
export interface ContractPresenceContext {
|
|
52
|
-
/** review
|
|
52
|
+
/** review 类型契约:调用方已解析的 metadata.reviewReport */
|
|
53
53
|
reviewReport?: unknown;
|
|
54
54
|
[key: string]: unknown;
|
|
55
55
|
}
|
|
@@ -62,7 +62,7 @@ export interface ContractPresenceResult {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
* completion_checkers 配置(对应 yml 顶层键 `completion_checkers
|
|
65
|
+
* completion_checkers 配置(对应 yml 顶层键 `completion_checkers:`,消费方自解)。
|
|
66
66
|
* 只配开关/glob/契约类型清单;协议格式(Tested-By、Tests: none、phase 结构)写死为机制本体。
|
|
67
67
|
* 缺省 = 全开 + 默认 glob。
|
|
68
68
|
*/
|
package/src/failure/recorder.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface FailureRecorderConfig {
|
|
|
25
25
|
*
|
|
26
26
|
* 用法:
|
|
27
27
|
* ```typescript
|
|
28
|
-
* const recorder = new FailureRecorder({ logFile:
|
|
28
|
+
* const recorder = new FailureRecorder({ logFile: DEFAULT_FAILURE_LOG_FILE });
|
|
29
29
|
* await recorder.record({
|
|
30
30
|
* type: ErrorType.TEST_FAILED,
|
|
31
31
|
* level: FailureLevel.L1,
|
package/src/index.ts
CHANGED
|
@@ -235,6 +235,7 @@ export type { ContextAverages } from './monitoring';
|
|
|
235
235
|
export {
|
|
236
236
|
ErrorType,
|
|
237
237
|
FailureLevel,
|
|
238
|
+
DEFAULT_FAILURE_LOG_FILE,
|
|
238
239
|
DEFAULT_CLASSIFICATION_RULES,
|
|
239
240
|
DEFAULT_LEVEL_MAPPING,
|
|
240
241
|
ErrorClassifier,
|
|
@@ -339,8 +340,8 @@ export type {
|
|
|
339
340
|
} from './knowledge';
|
|
340
341
|
|
|
341
342
|
// ========================================
|
|
342
|
-
// Completion Checkers
|
|
343
|
-
//
|
|
343
|
+
// Completion Checkers
|
|
344
|
+
// 提交集收尾软观测三纯判定函数:tdd-chain / phase-format / contract-presence。
|
|
344
345
|
// 纯函数直接 export,不进 ConstraintCheck 闭环注册表。
|
|
345
346
|
// ========================================
|
|
346
347
|
export {
|
|
@@ -374,6 +375,14 @@ export type {
|
|
|
374
375
|
// ========================================
|
|
375
376
|
export { getRegistryPath, getToolsDir } from './tools';
|
|
376
377
|
|
|
378
|
+
// ========================================
|
|
379
|
+
// 发布物完整性自检(#75 N4 收编:harness 自己最知道自己发了什么)
|
|
380
|
+
// 清单 = 包声明面(package.json main/exports/bin)推导 + 运行时 extras 随源码维护;
|
|
381
|
+
// studio publishPackage 的 dist 校验改调本能力,不再硬编码 harness dist 内部清单。
|
|
382
|
+
// ========================================
|
|
383
|
+
export { getCriticalArtifacts, verifyReleaseArtifacts } from './release';
|
|
384
|
+
export type { ArtifactIntegrityResult } from './release';
|
|
385
|
+
|
|
377
386
|
// ========================================
|
|
378
387
|
// Hooks 管线
|
|
379
388
|
// ========================================
|
|
@@ -103,7 +103,7 @@ Extra text
|
|
|
103
103
|
it('handles files without frontmatter via best-effort', () => {
|
|
104
104
|
fs.writeFileSync(
|
|
105
105
|
path.join(tmpDir, 'agent-network-core-concepts.md'),
|
|
106
|
-
'#
|
|
106
|
+
'# 核心概念总览\n\n## 设计要点\nBody\n'
|
|
107
107
|
);
|
|
108
108
|
|
|
109
109
|
const gen = new KnowledgeIndexGenerator(tmpDir);
|
|
@@ -115,7 +115,7 @@ Extra text
|
|
|
115
115
|
// agent-network-* prefix maps to architecture
|
|
116
116
|
expect(fields[2]).toBe('architecture');
|
|
117
117
|
// H2 terms should still be extracted
|
|
118
|
-
expect(fields[6]).toContain('
|
|
118
|
+
expect(fields[6]).toContain('设计要点');
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
it('excludes .archive directory', () => {
|
|
@@ -59,7 +59,7 @@ export class KnowledgeIndexGenerator {
|
|
|
59
59
|
`# Types: ${Object.entries(byType).map(([t, n]) => `${t}:${n}`).join(' ')}`,
|
|
60
60
|
`#`,
|
|
61
61
|
`# Usage:`,
|
|
62
|
-
`# grep "<keyword>" ~/.
|
|
62
|
+
`# grep "<keyword>" ~/.harness/knowledge/_index.md`,
|
|
63
63
|
`# Then Read the matching file for full content.`,
|
|
64
64
|
`#`,
|
|
65
65
|
`# filename|id|type|title|maturity|tags|terms`,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# release/
|
|
2
|
+
|
|
3
|
+
## 职责
|
|
4
|
+
发布物完整性自检:harness 自己声明并校验自己发布了什么。关键发布物清单的单一来源,替代外部(studio publishPackage)与本仓 release 命令各自的 dist 文件硬编码(harness#77,#75 N4 收编)。
|
|
5
|
+
|
|
6
|
+
## 核心导出
|
|
7
|
+
- `deriveCriticalArtifacts(manifest)`(`integrity.ts`)——从包声明面(package.json `main`/`exports`/`bin`)推导脚本入口清单(.js/.cjs/.mjs/.d.ts,去 `./` 前缀、剔除非脚本叶子、去重排序);纯函数
|
|
8
|
+
- `EXTRA_CRITICAL_ARTIFACTS`——声明面推导之外的运行时关键发布物(bin/harness.js 引导期 require 的命令/门禁定义表、`dist/tools/definitions` 数据目录);与其描述的文件同仓同评审,删改须同步
|
|
9
|
+
- `resolvePackageRoot(fromDir?)`——从模块位置向上按包名解析本包根(源码仓与 node_modules 安装态均可达)
|
|
10
|
+
- `getCriticalArtifacts(pkgRoot?)` / `verifyReleaseArtifacts(pkgRoot?)`——公开 API;pkgRoot 缺省自动解析,外部消费者零参数即自检已安装的 harness
|
|
11
|
+
|
|
12
|
+
## 依赖关系
|
|
13
|
+
- 无内部依赖(只依赖 node fs/path)
|
|
14
|
+
|
|
15
|
+
## 约定
|
|
16
|
+
- 清单维护模型:声明面推导为主(目录重构动到 package.json 时清单自动跟随)+ extras 随源码维护;**不新增第三处硬编码清单**
|
|
17
|
+
- 覆盖口径(有意裁决,#77):深度内部文件(tsc 产出的 dist/knowledge/*、core/constraints/checker.js 等)**不在清单**——非公开契约,存在性由 tsc 构建成功保证;历史上外部硬编码它们正是重构误判(studio 6cf3c329)的根因。旧 studio 清单 checker.js/doctor.js、旧 release 清单 13 项均被本清单取代,覆盖收窄是裁决而非遗漏
|
|
18
|
+
- `__tests__/integrity.test.ts` 的「真实包根清单同步闸门」是重构同步闸门:动 exports/main/bin 或 extras 时该测试强制同步
|
|
19
|
+
- bin 新增引导期 require 的 dist 文件 → extras 加一条 + 测试同步
|
|
20
|
+
|
|
21
|
+
## 注意事项
|
|
22
|
+
- 挂载点:`harness release` 命令第 4 步(tsc 后校验 dist);studio publishPackage dist 校验改调本能力(studio#425 配套切换)
|
|
23
|
+
- 公开面只收 `getCriticalArtifacts` / `verifyReleaseArtifacts` + `ArtifactIntegrityResult`(ADR-0003);derive/resolve 属内部 seam
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布物完整性自检测试(harness#77,#75 N4 收编)
|
|
3
|
+
*
|
|
4
|
+
* 覆盖:声明面推导(main/exports/bin)、extras 清单守卫、包根向上解析、
|
|
5
|
+
* 存在性校验、真实包根清单同步闸门。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from '@jest/globals';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import {
|
|
13
|
+
HARNESS_PACKAGE_NAME,
|
|
14
|
+
EXTRA_CRITICAL_ARTIFACTS,
|
|
15
|
+
deriveCriticalArtifacts,
|
|
16
|
+
resolvePackageRoot,
|
|
17
|
+
getCriticalArtifacts,
|
|
18
|
+
verifyReleaseArtifacts,
|
|
19
|
+
} from '../integrity';
|
|
20
|
+
import type { PackagePublishManifest } from '../integrity';
|
|
21
|
+
|
|
22
|
+
/** 建临时 fixture 包根:写 package.json + 按清单造文件/目录 */
|
|
23
|
+
function makeFixture(manifest: PackagePublishManifest, artifacts: string[]): string {
|
|
24
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-release-integrity-'));
|
|
25
|
+
fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify(manifest));
|
|
26
|
+
for (const artifact of artifacts) {
|
|
27
|
+
const abs = path.join(root, artifact);
|
|
28
|
+
if (/\.[cm]?js$|\.d\.ts$/.test(artifact)) {
|
|
29
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
30
|
+
fs.writeFileSync(abs, '');
|
|
31
|
+
} else {
|
|
32
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('deriveCriticalArtifacts', () => {
|
|
39
|
+
it('收集 main/exports/bin 声明的脚本入口,去 ./ 前缀、去重、排序,非脚本叶子剔除', () => {
|
|
40
|
+
const manifest: PackagePublishManifest = {
|
|
41
|
+
name: HARNESS_PACKAGE_NAME,
|
|
42
|
+
main: './dist/index.js',
|
|
43
|
+
bin: { harness: './bin/harness.js' },
|
|
44
|
+
exports: {
|
|
45
|
+
'.': {
|
|
46
|
+
types: './dist/index.d.ts',
|
|
47
|
+
require: './dist/index.js',
|
|
48
|
+
import: './dist/index.js',
|
|
49
|
+
},
|
|
50
|
+
'./core': './dist/core/index.js',
|
|
51
|
+
'./package.json': './package.json',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
expect(deriveCriticalArtifacts(manifest)).toEqual([
|
|
55
|
+
'bin/harness.js',
|
|
56
|
+
'dist/core/index.js',
|
|
57
|
+
'dist/index.d.ts',
|
|
58
|
+
'dist/index.js',
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('exports 为字符串、bin 为字符串、null 条目(兜底禁用)安全', () => {
|
|
63
|
+
const manifest: PackagePublishManifest = {
|
|
64
|
+
name: HARNESS_PACKAGE_NAME,
|
|
65
|
+
main: 'dist/index.js',
|
|
66
|
+
bin: 'bin/harness.js',
|
|
67
|
+
exports: { '.': './dist/index.js', './forbidden': null },
|
|
68
|
+
};
|
|
69
|
+
expect(deriveCriticalArtifacts(manifest)).toEqual(['bin/harness.js', 'dist/index.js']);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('无声明字段返回空清单', () => {
|
|
73
|
+
expect(deriveCriticalArtifacts({ name: HARNESS_PACKAGE_NAME })).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('EXTRA_CRITICAL_ARTIFACTS', () => {
|
|
78
|
+
it('覆盖 bin 引导定义表与 tools 运行时数据目录(随源码维护,删改须同评审)', () => {
|
|
79
|
+
expect(EXTRA_CRITICAL_ARTIFACTS).toEqual([
|
|
80
|
+
'dist/cli/commands/definitions.js',
|
|
81
|
+
'dist/gates/definitions.js',
|
|
82
|
+
'dist/tools/definitions',
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('resolvePackageRoot', () => {
|
|
88
|
+
it('从源码位置向上解析到本仓包根', () => {
|
|
89
|
+
const root = resolvePackageRoot(__dirname);
|
|
90
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf-8'));
|
|
91
|
+
expect(pkg.name).toBe(HARNESS_PACKAGE_NAME);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('无匹配包根时抛错并提示显式传入 pkgRoot', () => {
|
|
95
|
+
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-no-pkg-root-'));
|
|
96
|
+
const nested = path.join(bare, 'deep', 'nested');
|
|
97
|
+
fs.mkdirSync(nested, { recursive: true });
|
|
98
|
+
expect(() => resolvePackageRoot(nested)).toThrow(/pkgRoot/);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('getCriticalArtifacts / verifyReleaseArtifacts(fixture 包根)', () => {
|
|
103
|
+
const FIXTURE_MANIFEST: PackagePublishManifest = {
|
|
104
|
+
name: HARNESS_PACKAGE_NAME,
|
|
105
|
+
main: './dist/index.js',
|
|
106
|
+
bin: './bin/harness.js',
|
|
107
|
+
exports: { '.': './dist/index.js' },
|
|
108
|
+
};
|
|
109
|
+
const FIXTURE_ARTIFACTS = [
|
|
110
|
+
'dist/index.js',
|
|
111
|
+
'bin/harness.js',
|
|
112
|
+
...EXTRA_CRITICAL_ARTIFACTS,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
it('清单 = 声明面推导 + extras', () => {
|
|
116
|
+
const root = makeFixture(FIXTURE_MANIFEST, FIXTURE_ARTIFACTS);
|
|
117
|
+
expect(getCriticalArtifacts(root)).toEqual([
|
|
118
|
+
'bin/harness.js',
|
|
119
|
+
'dist/cli/commands/definitions.js',
|
|
120
|
+
'dist/gates/definitions.js',
|
|
121
|
+
'dist/index.js',
|
|
122
|
+
'dist/tools/definitions',
|
|
123
|
+
]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('全部就位 → ok:true', () => {
|
|
127
|
+
const root = makeFixture(FIXTURE_MANIFEST, FIXTURE_ARTIFACTS);
|
|
128
|
+
const result = verifyReleaseArtifacts(root);
|
|
129
|
+
expect(result).toMatchObject({ ok: true, pkgRoot: root, missing: [] });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('缺文件 → ok:false 且 missing 指名', () => {
|
|
133
|
+
const root = makeFixture(FIXTURE_MANIFEST, FIXTURE_ARTIFACTS);
|
|
134
|
+
fs.rmSync(path.join(root, 'dist', 'tools', 'definitions'), { recursive: true, force: true });
|
|
135
|
+
const result = verifyReleaseArtifacts(root);
|
|
136
|
+
expect(result.ok).toBe(false);
|
|
137
|
+
expect(result.missing).toEqual(['dist/tools/definitions']);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('真实包根清单同步闸门', () => {
|
|
142
|
+
it('真实 package.json 推导覆盖全部声明入口 + extras(重构动声明面时本测试强制同步)', () => {
|
|
143
|
+
const root = resolvePackageRoot(__dirname);
|
|
144
|
+
const artifacts = getCriticalArtifacts(root);
|
|
145
|
+
for (const required of [
|
|
146
|
+
'dist/index.js',
|
|
147
|
+
'dist/core/index.js',
|
|
148
|
+
'dist/presets/index.js',
|
|
149
|
+
'dist/context/index.js',
|
|
150
|
+
'dist/gates/index.js',
|
|
151
|
+
'bin/harness.js',
|
|
152
|
+
...EXTRA_CRITICAL_ARTIFACTS,
|
|
153
|
+
]) {
|
|
154
|
+
expect(artifacts).toContain(required);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
HARNESS_PACKAGE_NAME,
|
|
3
|
+
EXTRA_CRITICAL_ARTIFACTS,
|
|
4
|
+
deriveCriticalArtifacts,
|
|
5
|
+
resolvePackageRoot,
|
|
6
|
+
getCriticalArtifacts,
|
|
7
|
+
verifyReleaseArtifacts,
|
|
8
|
+
} from './integrity';
|
|
9
|
+
export type {
|
|
10
|
+
PackagePublishManifest,
|
|
11
|
+
PackageExports,
|
|
12
|
+
ArtifactIntegrityResult,
|
|
13
|
+
} from './integrity';
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 发布物完整性自检(harness#77,#75 N4 收编)
|
|
3
|
+
*
|
|
4
|
+
* harness 自己最知道自己发了什么:关键发布物清单不再由外部(studio publishPackage)
|
|
5
|
+
* 或本仓 release 命令各自硬编码 dist 内部文件,而是从包声明面(package.json
|
|
6
|
+
* main/exports/bin)运行时推导 + 少量运行时 extras 随源码维护——目录重构动到
|
|
7
|
+
* 声明面时清单自动跟随,extras 与其描述的文件同仓同评审。
|
|
8
|
+
*
|
|
9
|
+
* 公开面(barrel):getCriticalArtifacts / verifyReleaseArtifacts。
|
|
10
|
+
* studio publishPackage 的 dist 校验改调本能力,替换硬编码清单(studio#425 配套切换)。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
|
|
16
|
+
/** 本包名(包根向上解析的身份锚点,同 bin 名同源) */
|
|
17
|
+
export const HARNESS_PACKAGE_NAME = '@dommaker/harness';
|
|
18
|
+
|
|
19
|
+
/** package.json 发布面形状(只关心与发布物定位相关字段) */
|
|
20
|
+
export interface PackagePublishManifest {
|
|
21
|
+
name?: string;
|
|
22
|
+
main?: string;
|
|
23
|
+
bin?: string | Record<string, string>;
|
|
24
|
+
exports?: PackageExports;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** exports 字段形状:字符串叶子 / null(兜底禁用)/ 条件对象递归 */
|
|
28
|
+
export type PackageExports = string | null | { [key: string]: PackageExports };
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 推导之外、随源码维护的运行时关键发布物。
|
|
32
|
+
* 声明面推导只覆盖入口文件;以下入口的运行时依赖由本清单兜底。
|
|
33
|
+
*/
|
|
34
|
+
export const EXTRA_CRITICAL_ARTIFACTS: readonly string[] = [
|
|
35
|
+
// bin/harness.js 引导期 require 的命令/门禁定义表
|
|
36
|
+
'dist/cli/commands/definitions.js',
|
|
37
|
+
'dist/gates/definitions.js',
|
|
38
|
+
// getToolsDir/getRegistryPath 运行时数据目录(build 脚本 cp -r,tsc 不产出)
|
|
39
|
+
'dist/tools/definitions',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const CRITICAL_SUFFIXES = ['.js', '.cjs', '.mjs', '.d.ts'];
|
|
43
|
+
|
|
44
|
+
function collectExportsLeaves(exportsField: PackageExports | undefined, out: string[]): void {
|
|
45
|
+
if (typeof exportsField === 'string') {
|
|
46
|
+
out.push(exportsField);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!exportsField || typeof exportsField !== 'object') return;
|
|
50
|
+
for (const value of Object.values(exportsField)) {
|
|
51
|
+
collectExportsLeaves(value, out);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 从包声明面推导关键发布物清单:main + bin + exports 全部脚本入口。
|
|
57
|
+
* 去 ./ 前缀,剔除非脚本叶子(如 ./package.json 自导出),去重排序。
|
|
58
|
+
*/
|
|
59
|
+
export function deriveCriticalArtifacts(manifest: PackagePublishManifest): string[] {
|
|
60
|
+
const collected: string[] = [];
|
|
61
|
+
if (manifest.main) collected.push(manifest.main);
|
|
62
|
+
if (typeof manifest.bin === 'string') collected.push(manifest.bin);
|
|
63
|
+
else if (manifest.bin && typeof manifest.bin === 'object') {
|
|
64
|
+
collected.push(...Object.values(manifest.bin));
|
|
65
|
+
}
|
|
66
|
+
collectExportsLeaves(manifest.exports, collected);
|
|
67
|
+
return [...new Set(
|
|
68
|
+
collected
|
|
69
|
+
.map(p => p.replace(/^\.\//, ''))
|
|
70
|
+
.filter(p => CRITICAL_SUFFIXES.some(suffix => p.endsWith(suffix)))
|
|
71
|
+
)].sort();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isHarnessPackageRoot(dir: string): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf-8'));
|
|
77
|
+
return pkg?.name === HARNESS_PACKAGE_NAME;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 从 fromDir(缺省本模块编译位置)向上解析本包根。
|
|
85
|
+
* 源码仓(src/release → 仓根)与发布包(node_modules/@dommaker/harness/dist/release
|
|
86
|
+
* → 包根)均可达;到文件系统根仍无匹配则抛错。
|
|
87
|
+
*/
|
|
88
|
+
export function resolvePackageRoot(fromDir: string = __dirname): string {
|
|
89
|
+
let dir = path.resolve(fromDir);
|
|
90
|
+
for (;;) {
|
|
91
|
+
if (isHarnessPackageRoot(dir)) return dir;
|
|
92
|
+
const parent = path.dirname(dir);
|
|
93
|
+
if (parent === dir) break;
|
|
94
|
+
dir = parent;
|
|
95
|
+
}
|
|
96
|
+
throw new Error(
|
|
97
|
+
`[harness] 无法从 ${fromDir} 向上定位 ${HARNESS_PACKAGE_NAME} 包根;请显式传入 pkgRoot。`
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function resolveRoot(pkgRoot?: string): string {
|
|
102
|
+
return pkgRoot ? path.resolve(pkgRoot) : resolvePackageRoot();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 关键发布物清单(相对包根路径,排序去重)。
|
|
107
|
+
* pkgRoot 缺省时自动解析本包根——外部消费者(studio)零参数即自检已安装的 harness。
|
|
108
|
+
*/
|
|
109
|
+
export function getCriticalArtifacts(pkgRoot?: string): string[] {
|
|
110
|
+
const root = resolveRoot(pkgRoot);
|
|
111
|
+
const manifest: PackagePublishManifest = JSON.parse(
|
|
112
|
+
fs.readFileSync(path.join(root, 'package.json'), 'utf-8')
|
|
113
|
+
);
|
|
114
|
+
return [...new Set([...deriveCriticalArtifacts(manifest), ...EXTRA_CRITICAL_ARTIFACTS])].sort();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ArtifactIntegrityResult {
|
|
118
|
+
ok: boolean;
|
|
119
|
+
pkgRoot: string;
|
|
120
|
+
/** 清单全量(相对包根路径) */
|
|
121
|
+
checked: string[];
|
|
122
|
+
/** 缺失项(相对包根路径) */
|
|
123
|
+
missing: string[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 发布物完整性自检:逐项存在性校验(目录/文件均可)。
|
|
128
|
+
*
|
|
129
|
+
* 覆盖口径(有意裁决,#77):清单只锚定声明面及其运行时依赖;深度内部文件
|
|
130
|
+
* (tsc 产出的 dist/knowledge/*、core/constraints/checker.js 等)不在清单——
|
|
131
|
+
* 它们非公开契约,存在性由 tsc 构建成功保证,历史上外部硬编码它们正是
|
|
132
|
+
* 重构误判(6cf3c329)的根因。
|
|
133
|
+
*
|
|
134
|
+
* 挂载点:harness release 命令第 4 步;studio publishPackage dist 校验(配套切换 studio#425)。
|
|
135
|
+
*/
|
|
136
|
+
export function verifyReleaseArtifacts(pkgRoot?: string): ArtifactIntegrityResult {
|
|
137
|
+
const root = resolveRoot(pkgRoot);
|
|
138
|
+
const checked = getCriticalArtifacts(root);
|
|
139
|
+
const missing = checked.filter(f => !fs.existsSync(path.join(root, f)));
|
|
140
|
+
return { ok: missing.length === 0, pkgRoot: root, checked, missing };
|
|
141
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mkdtemp 泄漏防护测试(2026-08-26 /tmp 3.9 万残留事故,与 studio 同款机制)。
|
|
3
|
+
* 正本见 ../mkdtemp-cleanup.ts。
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import {
|
|
8
|
+
patchMktempCleanup,
|
|
9
|
+
isRegisteredTmpDir,
|
|
10
|
+
sweepStaleMkdtempDirs,
|
|
11
|
+
} from '../mkdtemp-cleanup';
|
|
12
|
+
|
|
13
|
+
describe('mkdtemp-cleanup', () => {
|
|
14
|
+
it('patch 后 mkdtempSync 创建的目录进注册表(幂等)', () => {
|
|
15
|
+
patchMktempCleanup();
|
|
16
|
+
const d = fs.mkdtempSync(path.join('/tmp', 'harness-mkdtemp-cleanup-test-'));
|
|
17
|
+
expect(fs.existsSync(d)).toBe(true);
|
|
18
|
+
expect(isRegisteredTmpDir(d)).toBe(true);
|
|
19
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('sweep 只删超过年龄阈值且符合 mkdtemp 签名的目录', () => {
|
|
23
|
+
const base = fs.mkdtempSync(path.join('/tmp', 'harness-mkdtemp-cleanup-sweep-'));
|
|
24
|
+
const cases: Array<{ name: string; ageH: number; shouldRemove: boolean }> = [
|
|
25
|
+
{ name: 'fake-test-aB3xY9', ageH: 25, shouldRemove: true }, // 签名匹配 + 超龄 -> 删
|
|
26
|
+
{ name: 'fake-test-cD4zW0', ageH: 1, shouldRemove: false }, // 签名匹配但新鲜 -> 留
|
|
27
|
+
{ name: 'org.chromium.Chromium.RJ5vUP', ageH: 25, shouldRemove: false }, // 含点(chrome crashpad)-> 不匹配
|
|
28
|
+
{ name: 'fake-test-ab12', ageH: 25, shouldRemove: false }, // 后缀 4 字符 -> 不匹配
|
|
29
|
+
{ name: 'tsx-0', ageH: 25, shouldRemove: false }, // 后缀 1 字符 -> 不匹配
|
|
30
|
+
];
|
|
31
|
+
for (const c of cases) {
|
|
32
|
+
const p = path.join(base, c.name);
|
|
33
|
+
fs.mkdirSync(p);
|
|
34
|
+
const t = new Date(Date.now() - c.ageH * 3600 * 1000);
|
|
35
|
+
fs.utimesSync(p, t, t);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const removed = sweepStaleMkdtempDirs(base, 24 * 3600 * 1000);
|
|
39
|
+
expect(removed).toBe(1);
|
|
40
|
+
for (const c of cases) {
|
|
41
|
+
expect(fs.existsSync(path.join(base, c.name))).toBe(!c.shouldRemove);
|
|
42
|
+
}
|
|
43
|
+
fs.rmSync(base, { recursive: true, force: true });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest setupFilesAfterEnv - mkdtemp 泄漏防护(2026-08-26 /tmp 3.9 万残留事故根因修复)
|
|
3
|
+
*
|
|
4
|
+
* 事故:5 个测试文件 fs.mkdtempSync 后零清理(constraints-retire / check-drift /
|
|
5
|
+
* constraints-report / agent-prompt-renderer / injection-drift),日常 jest 运行累积。
|
|
6
|
+
*
|
|
7
|
+
* 机制(studio apps/api/tests/mkdtemp-cleanup.ts 同款,jest 版):
|
|
8
|
+
* 1) patch fs.mkdtempSync--本进程创建的临时目录全部进注册表,afterAll 统一 rmSync。
|
|
9
|
+
* jest worker 被 kill 时 process 'exit' 不触发,必须用 afterAll(setupFilesAfterEnv
|
|
10
|
+
* 里注册 = 文件级钩子)。
|
|
11
|
+
* 2) import 时清扫 tmpdir 里 >24h 且符合 mkdtemp 签名的目录,兜底 worker 被 kill 的场景。
|
|
12
|
+
*
|
|
13
|
+
* 签名正则只匹配「小写字母数字连字符前缀 + 恰好 6 位大小写字母数字后缀」:
|
|
14
|
+
* chrome crashpad(org.chromium.Chromium.XXXX,含点)、tsx-0 / jest_0 等不匹配。
|
|
15
|
+
*/
|
|
16
|
+
import fs from 'fs';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { afterAll } from '@jest/globals';
|
|
19
|
+
|
|
20
|
+
const MKDTEMP_SUFFIX_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*-[A-Za-z0-9]{6}$/;
|
|
21
|
+
|
|
22
|
+
let patched = false;
|
|
23
|
+
const registeredDirs = new Set<string>();
|
|
24
|
+
|
|
25
|
+
/** 幂等。setup 与测试文件重复调用安全。 */
|
|
26
|
+
export function patchMktempCleanup(): void {
|
|
27
|
+
if (patched) return;
|
|
28
|
+
patched = true;
|
|
29
|
+
const orig = fs.mkdtempSync as (prefix: string) => string;
|
|
30
|
+
fs.mkdtempSync = ((prefix: string) => {
|
|
31
|
+
const dir = orig(prefix);
|
|
32
|
+
registeredDirs.add(dir);
|
|
33
|
+
return dir;
|
|
34
|
+
}) as typeof fs.mkdtempSync;
|
|
35
|
+
|
|
36
|
+
afterAll(() => {
|
|
37
|
+
for (const dir of registeredDirs) {
|
|
38
|
+
try {
|
|
39
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
40
|
+
} catch {
|
|
41
|
+
// 清理失败不阻断测试
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
registeredDirs.clear();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isRegisteredTmpDir(dir: string): boolean {
|
|
49
|
+
return registeredDirs.has(dir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 清扫 tmpDir 中超龄且符合 mkdtemp 签名的目录。返回删除数。
|
|
54
|
+
* 单条失败(竞态被别进程先删等)不阻断。
|
|
55
|
+
*/
|
|
56
|
+
export function sweepStaleMkdtempDirs(tmpDir: string, ageMs: number): number {
|
|
57
|
+
let entries: string[];
|
|
58
|
+
try {
|
|
59
|
+
entries = fs.readdirSync(tmpDir);
|
|
60
|
+
} catch {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
let removed = 0;
|
|
65
|
+
for (const name of entries) {
|
|
66
|
+
if (!MKDTEMP_SUFFIX_RE.test(name)) continue;
|
|
67
|
+
try {
|
|
68
|
+
const p = path.join(tmpDir, name);
|
|
69
|
+
const st = fs.statSync(p);
|
|
70
|
+
if (st.isDirectory() && now - st.mtimeMs > ageMs) {
|
|
71
|
+
fs.rmSync(p, { recursive: true, force: true });
|
|
72
|
+
removed++;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// 单条竞态不阻断
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return removed;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// setupFilesAfterEnv 顶层执行:打补丁 + 清扫历史残留
|
|
82
|
+
patchMktempCleanup();
|
|
83
|
+
sweepStaleMkdtempDirs(process.env.TMPDIR || '/tmp', 24 * 60 * 60 * 1000);
|
package/src/types/failure.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* 不包含业务逻辑,只定义数据结构
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* 默认 failure 日志路径(相对项目根目录)
|
|
10
|
+
*
|
|
11
|
+
* 读写双方必须统一引用此常量(DEFAULT_TRACE_FILE 同款先例);
|
|
12
|
+
* 自定义路径时向 FailureRecorder 传入自定 logFile
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_FAILURE_LOG_FILE = '.harness/logs/failures.log';
|
|
15
|
+
|
|
8
16
|
/**
|
|
9
17
|
* 错误类型枚举
|
|
10
18
|
*/
|