artifact-graph 0.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.
@@ -0,0 +1,329 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * run-packet-prompt-audit.mjs
4
+ *
5
+ * Packet prompt audit 回归测试脚本。运行以下场景并输出中文摘要:
6
+ * 1. discover compact summary-only 全量(不写 prompt 文件)
7
+ * 2. discover limit 5(限制 target 数量)
8
+ * 3. targets-file 写 summary + prompt 文件
9
+ * 4. summary-only 写 summary 不写 prompt 文件
10
+ * 5. 负例:--discover 空 root(exit 1)
11
+ * 6. 负例:--limit -1(exit 1)
12
+ * 7. 负例:--discover 与 --targets-file 互斥(exit 1)
13
+ * 8. 负例:--summary-detail 非法值(exit 1)
14
+ *
15
+ * v1.13: 默认 outDirBase 改为每次唯一目录,避免旧结果干扰
16
+ *
17
+ * 用法: node scripts/run-packet-prompt-audit.mjs [--root <path>] [--out-dir <path>]
18
+ */
19
+ import { execFile } from 'node:child_process';
20
+ import { existsSync, readdirSync } from 'node:fs';
21
+ import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises';
22
+ import { tmpdir } from 'node:os';
23
+ import { dirname, join, resolve } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { promisify } from 'node:util';
26
+
27
+ const execFileAsync = promisify(execFile);
28
+
29
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
30
+ const CLI = resolve(__dirname, '../dist/cli.js');
31
+
32
+ // v1.13: 临时目录前缀,用于识别和清理
33
+ const TMP_PREFIX = 'packet-prompt-audit-regression-';
34
+ const MAX_KEPT_DIRS = 5;
35
+
36
+ /**
37
+ * 从脚本位置向上查找同时包含 artifact-graph.config.yaml 和 artifacts/ 的目录。
38
+ */
39
+ function discoverRoot(startDir) {
40
+ let dir = resolve(startDir);
41
+ for (let i = 0; i < 10; i++) {
42
+ if (
43
+ existsSync(join(dir, 'artifact-graph.config.yaml')) &&
44
+ existsSync(join(dir, 'artifacts'))
45
+ ) {
46
+ return dir;
47
+ }
48
+ const parent = dirname(dir);
49
+ if (parent === dir) break;
50
+ dir = parent;
51
+ }
52
+ return null;
53
+ }
54
+
55
+ /**
56
+ * v1.13: 清理旧的临时目录,保留最近 MAX_KEPT_DIRS 个
57
+ */
58
+ async function cleanupOldTmpDirs() {
59
+ try {
60
+ const tmpBase = tmpdir();
61
+ const entries = readdirSync(tmpBase, { withFileTypes: true })
62
+ .filter((e) => e.isDirectory() && e.name.startsWith(TMP_PREFIX))
63
+ .map((e) => ({
64
+ name: e.name,
65
+ path: join(tmpBase, e.name),
66
+ ts: parseInt(e.name.slice(TMP_PREFIX.length), 10) || 0,
67
+ }))
68
+ .sort((a, b) => b.ts - a.ts);
69
+
70
+ for (const dir of entries.slice(MAX_KEPT_DIRS)) {
71
+ await rm(dir.path, { recursive: true, force: true });
72
+ }
73
+ } catch {
74
+ // 忽略清理错误
75
+ }
76
+ }
77
+
78
+ async function run(args, opts = {}) {
79
+ try {
80
+ const { stdout, stderr } = await execFileAsync('node', [CLI, ...args], {
81
+ timeout: 120_000,
82
+ maxBuffer: 50 * 1024 * 1024,
83
+ ...opts,
84
+ });
85
+ return { code: 0, stdout, stderr };
86
+ } catch (err) {
87
+ return {
88
+ code: err.code ?? 1,
89
+ stdout: err.stdout ?? '',
90
+ stderr: err.stderr ?? '',
91
+ };
92
+ }
93
+ }
94
+
95
+ function log(msg) {
96
+ process.stdout.write(`[packet-prompt-audit-regression] ${msg}\n`);
97
+ }
98
+
99
+ function pass(msg) {
100
+ log(`✅ ${msg}`);
101
+ }
102
+
103
+ function fail(msg) {
104
+ log(`❌ ${msg}`);
105
+ }
106
+
107
+ async function main() {
108
+ const args = process.argv.slice(2);
109
+ let root = null;
110
+ let outDirBase = null;
111
+ let userSpecifiedOutDir = false;
112
+
113
+ for (let i = 0; i < args.length; i++) {
114
+ if (args[i] === '--root' && args[i + 1]) root = resolve(args[++i]);
115
+ else if (args[i] === '--out-dir' && args[i + 1]) {
116
+ outDirBase = resolve(args[++i]);
117
+ userSpecifiedOutDir = true;
118
+ }
119
+ }
120
+
121
+ // v1.13: 清理旧临时目录
122
+ await cleanupOldTmpDirs();
123
+
124
+ // v1.13: 默认使用唯一临时目录
125
+ if (!outDirBase) {
126
+ outDirBase = await mkdtemp(join(tmpdir(), TMP_PREFIX));
127
+ } else {
128
+ await mkdir(outDirBase, { recursive: true });
129
+ }
130
+
131
+ if (!root) {
132
+ root = discoverRoot(__dirname);
133
+ if (!root) {
134
+ log('错误:无法自动发现 root 目录(找不到 artifact-graph.config.yaml 和 artifacts/)');
135
+ log('请使用 --root <path> 显式指定');
136
+ process.exit(1);
137
+ }
138
+ }
139
+
140
+ const hasConfig = existsSync(join(root, 'artifact-graph.config.yaml'));
141
+ const hasArtifacts = existsSync(join(root, 'artifacts'));
142
+ if (!hasConfig || !hasArtifacts) {
143
+ log(`错误:root=${root} 缺少 ${!hasConfig ? 'artifact-graph.config.yaml' : ''}${!hasConfig && !hasArtifacts ? ' 和 ' : ''}${!hasArtifacts ? 'artifacts/' : ''}`);
144
+ process.exit(1);
145
+ }
146
+
147
+ let failures = 0;
148
+ const startTime = Date.now();
149
+
150
+ log(`根目录: ${root}`);
151
+ log(`输出目录: ${outDirBase}${userSpecifiedOutDir ? '(用户指定)' : '(自动创建)'}`);
152
+ log('─'.repeat(60));
153
+
154
+ // ── 场景 1: discover compact summary-only 全量 ──
155
+ {
156
+ log('场景 1: discover compact summary-only 全量(不写 prompt 文件)');
157
+ const res = await run([
158
+ 'packet-prompt-audit', '--root', root, '--discover', '--limit', '0',
159
+ '--summary-only', '--summary-detail', 'compact', '--format', 'json',
160
+ ]);
161
+ if (res.code !== 0) {
162
+ fail(`discover compact summary-only exit ${res.code},stderr: ${res.stderr.trim().slice(0, 200)}`);
163
+ failures++;
164
+ } else {
165
+ const summary = JSON.parse(res.stdout);
166
+ log(` 总计: ${summary.total} | 通过: ${summary.passed} | 失败: ${summary.failed} | 省略: ${summary.totalOmitted}`);
167
+ log(` schemaVersion: ${summary.schemaVersion} | countsByType: ${JSON.stringify(summary.countsByType)}`);
168
+ if (summary.total > 0 && summary.failed === 0 && summary.totalOmitted > 0) {
169
+ pass('discover compact summary-only 正常');
170
+ } else {
171
+ fail('discover compact summary-only 结果异常');
172
+ failures++;
173
+ }
174
+ }
175
+ }
176
+
177
+ // ── 场景 2: discover limit 5 ──
178
+ {
179
+ log('场景 2: discover limit 5');
180
+ const res = await run([
181
+ 'packet-prompt-audit', '--root', root, '--discover', '--limit', '5',
182
+ '--format', 'json',
183
+ ]);
184
+ if (res.code !== 0) {
185
+ fail(`discover limit 5 exit ${res.code},stderr: ${res.stderr.trim().slice(0, 200)}`);
186
+ failures++;
187
+ } else {
188
+ const summary = JSON.parse(res.stdout);
189
+ log(` 总计: ${summary.total} | 通过: ${summary.passed} | 失败: ${summary.failed}`);
190
+ if (summary.total <= 5 && summary.passed === summary.total) {
191
+ pass('discover limit 5 正常');
192
+ } else {
193
+ fail(`discover limit 5 结果异常: total=${summary.total}`);
194
+ failures++;
195
+ }
196
+ }
197
+ }
198
+
199
+ // ── 场景 3: targets-file 写 summary + prompt 文件 ──
200
+ {
201
+ log('场景 3: targets-file 写 summary + prompt 文件');
202
+ const outDir = join(outDirBase, 'targets-file-output');
203
+ await mkdir(outDir, { recursive: true });
204
+ const targetsFile = join(outDir, 'targets.txt');
205
+ await writeFile(targetsFile, 'feature:A1\nscenario:S-01\ndecision:D-ARCH-01\n');
206
+ const res = await run([
207
+ 'packet-prompt-audit', '--root', root, '--targets-file', targetsFile,
208
+ '--out-dir', outDir, '--format', 'json',
209
+ ]);
210
+ if (res.code !== 0) {
211
+ fail(`targets-file exit ${res.code},stderr: ${res.stderr.trim().slice(0, 200)}`);
212
+ failures++;
213
+ } else {
214
+ const summary = JSON.parse(res.stdout);
215
+ const files = await readdir(outDir);
216
+ const hasSummary = files.includes('prompt-audit-summary.json') && files.includes('prompt-audit-summary.md');
217
+ const hasPrompts = files.some((f) => f.startsWith('prompt-') && f.endsWith('.md') && f !== 'prompt-audit-summary.md');
218
+ log(` 文件数: ${files.length} | 有 summary: ${hasSummary} | 有 prompt 文件: ${hasPrompts}`);
219
+ if (summary.total === 3 && hasSummary && hasPrompts) {
220
+ pass('targets-file 写 summary + prompt 正常');
221
+ } else {
222
+ fail('targets-file 输出异常');
223
+ failures++;
224
+ }
225
+ }
226
+ }
227
+
228
+ // ── 场景 4: summary-only 写 summary 不写 prompt ──
229
+ {
230
+ log('场景 4: summary-only 写 summary 不写 prompt');
231
+ const outDir = join(outDirBase, 'summary-only-output');
232
+ await mkdir(outDir, { recursive: true });
233
+ const targetsFile = join(outDir, 'targets.txt');
234
+ await writeFile(targetsFile, 'feature:A1\nscenario:S-01\n');
235
+ const res = await run([
236
+ 'packet-prompt-audit', '--root', root, '--targets-file', targetsFile,
237
+ '--out-dir', outDir, '--summary-only', '--format', 'json',
238
+ ]);
239
+ if (res.code !== 0) {
240
+ fail(`summary-only exit ${res.code},stderr: ${res.stderr.trim().slice(0, 200)}`);
241
+ failures++;
242
+ } else {
243
+ const summary = JSON.parse(res.stdout);
244
+ const files = await readdir(outDir);
245
+ const hasSummary = files.includes('prompt-audit-summary.json') && files.includes('prompt-audit-summary.md');
246
+ const promptFiles = files.filter((f) => f.startsWith('prompt-') && f.endsWith('.md') && f !== 'prompt-audit-summary.md');
247
+ log(` 文件数: ${files.length} | 有 summary: ${hasSummary} | prompt 文件数: ${promptFiles.length}`);
248
+ if (summary.total === 2 && hasSummary && promptFiles.length === 0) {
249
+ pass('summary-only 写 summary 不写 prompt 正常');
250
+ } else {
251
+ fail('summary-only 输出异常');
252
+ failures++;
253
+ }
254
+ }
255
+ }
256
+
257
+ // ── 负例 5: --discover 空 root(exit 1)──
258
+ {
259
+ log('负例 5: --discover 空 root(应 exit 1)');
260
+ const tmpDir = await mkdtemp(join(tmpdir(), 'empty-root-'));
261
+ const res = await run([
262
+ 'packet-prompt-audit', '--root', tmpDir, '--discover', '--limit', '0', '--format', 'json',
263
+ ]);
264
+ await rm(tmpDir, { recursive: true, force: true });
265
+ if (res.code === 1) {
266
+ pass('空 root 正确 exit 1');
267
+ } else {
268
+ fail(`空 root exit ${res.code},期望 1,stderr: ${res.stderr.trim().slice(0, 200)}`);
269
+ failures++;
270
+ }
271
+ }
272
+
273
+ // ── 负例 6: --limit -1(exit 1)──
274
+ {
275
+ log('负例 6: --limit -1(应 exit 1)');
276
+ const res = await run([
277
+ 'packet-prompt-audit', '--root', root, '--discover', '--limit', '-1', '--format', 'json',
278
+ ]);
279
+ if (res.code === 1 && res.stderr.includes('Invalid --limit')) {
280
+ pass('--limit -1 正确 exit 1');
281
+ } else {
282
+ fail(`--limit -1 exit ${res.code},期望 1,stderr: ${res.stderr.trim().slice(0, 200)}`);
283
+ failures++;
284
+ }
285
+ }
286
+
287
+ // ── 负例 7: --discover 与 --targets-file 互斥(exit 1)──
288
+ {
289
+ log('负例 7: --discover 与 --targets-file 互斥(应 exit 1)');
290
+ const res = await run([
291
+ 'packet-prompt-audit', '--root', root, '--discover', '--targets-file', '/tmp/fake.txt', '--format', 'json',
292
+ ]);
293
+ if (res.code === 1 && res.stderr.includes('互斥')) {
294
+ pass('互斥参数正确 exit 1');
295
+ } else {
296
+ fail(`互斥参数 exit ${res.code},期望 1,stderr: ${res.stderr.trim().slice(0, 200)}`);
297
+ failures++;
298
+ }
299
+ }
300
+
301
+ // ── 负例 8: --summary-detail 非法值(exit 1)──
302
+ {
303
+ log('负例 8: --summary-detail 非法值(应 exit 1)');
304
+ const res = await run([
305
+ 'packet-prompt-audit', '--root', root, '--discover', '--summary-detail', 'verbose', '--format', 'json',
306
+ ]);
307
+ if (res.code === 1 && res.stderr.includes('Invalid --summary-detail')) {
308
+ pass('--summary-detail 非法值正确 exit 1');
309
+ } else {
310
+ fail(`--summary-detail 非法值 exit ${res.code},期望 1,stderr: ${res.stderr.trim().slice(0, 200)}`);
311
+ failures++;
312
+ }
313
+ }
314
+
315
+ // ── 总结 ──
316
+ log('─'.repeat(60));
317
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
318
+ if (failures === 0) {
319
+ log(`全部通过(${elapsed}s)`);
320
+ } else {
321
+ log(`${failures} 个场景失败(${elapsed}s)`);
322
+ process.exitCode = 1;
323
+ }
324
+ }
325
+
326
+ main().catch((err) => {
327
+ console.error(err);
328
+ process.exitCode = 1;
329
+ });
@@ -0,0 +1,26 @@
1
+ #!/bin/sh
2
+ set -u
3
+
4
+ artifact_graph() {
5
+ if [ -x ./node_modules/.bin/artifact-graph ]; then
6
+ ./node_modules/.bin/artifact-graph "$@"
7
+ return $?
8
+ fi
9
+ if command -v artifact-graph >/dev/null 2>&1; then
10
+ artifact-graph "$@"
11
+ return $?
12
+ fi
13
+ if [ -n "${ARTIFACT_GRAPH_LEGACY_CLI:-}" ] && [ -f "$ARTIFACT_GRAPH_LEGACY_CLI" ]; then
14
+ node "$ARTIFACT_GRAPH_LEGACY_CLI" "$@"
15
+ return $?
16
+ fi
17
+ echo "artifact-chain-assistant: artifact-graph CLI not found; install it in the project or PATH." >&2
18
+ return 127
19
+ }
20
+
21
+ artifact_graph version-lock refresh --changed-only --staged --format markdown || exit $?
22
+ if ! git diff --quiet -- artifacts/traceability-version-lock.json; then
23
+ echo "artifact-chain-assistant: version lock changed during pre-commit." >&2
24
+ echo "Please review and stage artifacts/traceability-version-lock.json, then commit again." >&2
25
+ exit 1
26
+ fi
@@ -0,0 +1,22 @@
1
+ #!/bin/sh
2
+ set -u
3
+
4
+ artifact_graph() {
5
+ if [ -x ./node_modules/.bin/artifact-graph ]; then
6
+ ./node_modules/.bin/artifact-graph "$@"
7
+ return $?
8
+ fi
9
+ if command -v artifact-graph >/dev/null 2>&1; then
10
+ artifact-graph "$@"
11
+ return $?
12
+ fi
13
+ if [ -n "${ARTIFACT_GRAPH_LEGACY_CLI:-}" ] && [ -f "$ARTIFACT_GRAPH_LEGACY_CLI" ]; then
14
+ node "$ARTIFACT_GRAPH_LEGACY_CLI" "$@"
15
+ return $?
16
+ fi
17
+ echo "artifact-chain-assistant: artifact-graph CLI not found; install it in the project or PATH." >&2
18
+ return 127
19
+ }
20
+
21
+ artifact_graph validate --warning-only || exit $?
22
+ artifact_graph version-lock audit --strict-missing-lock