@vinsonw2/asset-compound 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1014 @@
1
+ // src/schema.ts
2
+ import { Ajv2020 } from "ajv/dist/2020.js";
3
+ import formatsPlugin from "ajv-formats";
4
+ var dimension = {
5
+ type: "object",
6
+ additionalProperties: false,
7
+ required: ["level", "evidence", "gaps", "recommendations"],
8
+ properties: {
9
+ level: {
10
+ enum: ["ready", "partial", "missing", "conflict", "high", "medium", "low"]
11
+ },
12
+ evidence: { type: "array", items: { type: "string" } },
13
+ gaps: { type: "array", items: { type: "string" } },
14
+ recommendations: { type: "array", items: { type: "string" } }
15
+ }
16
+ };
17
+ var assetSchema = {
18
+ $schema: "https://json-schema.org/draft/2020-12/schema",
19
+ type: "object",
20
+ additionalProperties: false,
21
+ required: [
22
+ "assetId",
23
+ "sourceType",
24
+ "sourceRef",
25
+ "sourceRevision",
26
+ "contentHash",
27
+ "title",
28
+ "summary",
29
+ "facts",
30
+ "decisions",
31
+ "actions",
32
+ "relations",
33
+ "analysis",
34
+ "writebackKind"
35
+ ],
36
+ properties: {
37
+ assetId: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" },
38
+ sourceType: { enum: ["requirement", "design", "ai-artifact", "git"] },
39
+ sourceRef: { type: "string", minLength: 1 },
40
+ sourceRevision: { type: "string", minLength: 1 },
41
+ contentHash: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" },
42
+ title: { type: "string", minLength: 1 },
43
+ summary: { type: "string" },
44
+ facts: { type: "array", items: { type: "string" } },
45
+ decisions: { type: "array", items: { type: "string" } },
46
+ actions: { type: "array", items: { type: "string" } },
47
+ relations: { type: "array", items: { type: "string" } },
48
+ analysis: {
49
+ type: "object",
50
+ additionalProperties: false,
51
+ required: ["completeness", "executability", "consistency", "reuseValue"],
52
+ properties: {
53
+ completeness: dimension,
54
+ executability: dimension,
55
+ consistency: dimension,
56
+ reuseValue: dimension
57
+ }
58
+ },
59
+ writebackKind: { enum: ["\u65B0\u589E", "\u589E\u91CF", "\u672A\u53D8"] }
60
+ }
61
+ };
62
+ var configSchema = {
63
+ type: "object",
64
+ additionalProperties: false,
65
+ required: ["outputRoot", "sourceSets"],
66
+ properties: {
67
+ outputRoot: { const: "docs/\u53EF\u8BFB\u5316" },
68
+ baseBranch: { type: "string", minLength: 1 },
69
+ sourceSets: {
70
+ type: "array",
71
+ minItems: 2,
72
+ items: {
73
+ type: "object",
74
+ additionalProperties: false,
75
+ required: ["type"],
76
+ properties: {
77
+ type: { enum: ["requirement", "design", "ai-artifact", "git"] },
78
+ include: { type: "array", items: { type: "string" } },
79
+ maxCommits: { type: "integer", minimum: 1, maximum: 1e3 }
80
+ }
81
+ }
82
+ },
83
+ exclude: { type: "array", items: { type: "string" } }
84
+ }
85
+ };
86
+ var stateSchema = {
87
+ type: "object",
88
+ additionalProperties: false,
89
+ required: [
90
+ "schemaVersion",
91
+ "selfHash",
92
+ "baseBranch",
93
+ "baseCommit",
94
+ "lastReadCommit",
95
+ "sources",
96
+ "assets",
97
+ "managedFiles"
98
+ ],
99
+ properties: {
100
+ schemaVersion: { const: 1 },
101
+ selfHash: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" },
102
+ baseBranch: { type: "string" },
103
+ baseCommit: { type: "string" },
104
+ lastReadCommit: { type: "string" },
105
+ sources: {
106
+ type: "object",
107
+ additionalProperties: {
108
+ type: "object",
109
+ additionalProperties: false,
110
+ required: ["contentHash", "sourceRevision", "assetIds", "status"],
111
+ properties: {
112
+ contentHash: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" },
113
+ sourceRevision: { type: "string", minLength: 1 },
114
+ assetIds: {
115
+ type: "array",
116
+ items: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" }
117
+ },
118
+ status: { enum: ["active", "stale"] }
119
+ }
120
+ }
121
+ },
122
+ assets: {
123
+ type: "object",
124
+ additionalProperties: {
125
+ type: "object",
126
+ additionalProperties: false,
127
+ required: ["canonicalContentHash", "outputPath", "lastGeneratedHash"],
128
+ properties: {
129
+ canonicalContentHash: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" },
130
+ outputPath: {
131
+ type: "string",
132
+ pattern: "^(?!.*(?:^|/)\\.\\.(?:/|$))docs/\u53EF\u8BFB\u5316/.+$"
133
+ },
134
+ lastGeneratedHash: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" }
135
+ }
136
+ }
137
+ },
138
+ managedFiles: {
139
+ type: "object",
140
+ propertyNames: {
141
+ pattern: "^(?!.*(?:^|/)\\.\\.(?:/|$))docs/\u53EF\u8BFB\u5316/.+$"
142
+ },
143
+ additionalProperties: { type: "string", pattern: "^sha256:[a-f0-9]{64}$" }
144
+ }
145
+ }
146
+ };
147
+ var ajv = new Ajv2020({ strict: true, allErrors: true });
148
+ formatsPlugin(ajv);
149
+ function compile(schema) {
150
+ return ajv.compile(schema);
151
+ }
152
+ var validateAsset = compile(assetSchema);
153
+ var validateConfig = compile(configSchema);
154
+ var validateState = compile(stateSchema);
155
+ function assertValid(validator, value, label) {
156
+ if (!validator(value)) {
157
+ const details = (validator.errors ?? []).map((error) => `${error.instancePath || "/"} ${error.message}`).sort().join("; ");
158
+ throw new Error(`${label} \u672A\u901A\u8FC7 schema \u6821\u9A8C\uFF1A${details}`);
159
+ }
160
+ }
161
+
162
+ // src/pipeline.ts
163
+ import { execFile } from "child_process";
164
+ import { createHash } from "crypto";
165
+ import { access, lstat, mkdir, readFile, realpath } from "fs/promises";
166
+ import path from "path";
167
+ import { promisify } from "util";
168
+ import fg from "fast-glob";
169
+ import writeFileAtomic from "write-file-atomic";
170
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
171
+ var execFileAsync = promisify(execFile);
172
+ var OUTPUT_ROOT = "docs/\u53EF\u8BFB\u5316";
173
+ var SOURCE_TYPE_LABEL = {
174
+ requirement: "\u9700\u6C42",
175
+ design: "\u8BBE\u8BA1",
176
+ "ai-artifact": "AI \u4EA7\u7269",
177
+ git: "Git \u5386\u53F2"
178
+ };
179
+ var LEVEL_LABEL = {
180
+ ready: "\u5C31\u7EEA",
181
+ partial: "\u90E8\u5206\u6EE1\u8DB3",
182
+ missing: "\u7F3A\u5931",
183
+ conflict: "\u51B2\u7A81",
184
+ high: "\u9AD8",
185
+ medium: "\u4E2D",
186
+ low: "\u4F4E"
187
+ };
188
+ function sha256(value) {
189
+ const normalized = typeof value === "string" ? value.replace(/\r\n?/g, "\n").normalize("NFC") : value;
190
+ return `sha256:${createHash("sha256").update(normalized).digest("hex")}`;
191
+ }
192
+ async function exists(file) {
193
+ try {
194
+ await access(file);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ async function git(repo, args) {
201
+ const { stdout } = await execFileAsync("git", args, {
202
+ cwd: repo,
203
+ encoding: "utf8",
204
+ maxBuffer: 20 * 1024 * 1024
205
+ });
206
+ return stdout.trimEnd();
207
+ }
208
+ async function assertOutputPath(repo, target) {
209
+ const root = await realpath(repo);
210
+ const absolute = path.resolve(root, target);
211
+ const relative = path.relative(root, absolute);
212
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`) || !(relative === OUTPUT_ROOT || relative.startsWith(`${OUTPUT_ROOT}${path.sep}`))) {
213
+ throw new Error(`\u62D2\u7EDD\u5199\u5165 ${OUTPUT_ROOT} \u4E4B\u5916\u7684\u8DEF\u5F84\uFF1A${target}`);
214
+ }
215
+ let cursor = root;
216
+ for (const segment of relative.split(path.sep)) {
217
+ cursor = path.join(cursor, segment);
218
+ if (await exists(cursor)) {
219
+ const stat2 = await lstat(cursor);
220
+ if (stat2.isSymbolicLink()) {
221
+ throw new Error(`\u62D2\u7EDD\u8F93\u51FA\u8DEF\u5F84\u4E2D\u7684\u7B26\u53F7\u94FE\u63A5\uFF1A${cursor}`);
222
+ }
223
+ }
224
+ }
225
+ return absolute;
226
+ }
227
+ async function atomicWriteIfChanged(repo, relativePath, content) {
228
+ const target = await assertOutputPath(repo, relativePath);
229
+ const normalized = `${content.replace(/\r\n?/g, "\n").trimEnd()}
230
+ `;
231
+ if (await exists(target)) {
232
+ const current = await readFile(target, "utf8");
233
+ if (current === normalized) return false;
234
+ }
235
+ await mkdir(path.dirname(target), { recursive: true });
236
+ await writeFileAtomic(target, normalized, { encoding: "utf8", fsync: true });
237
+ return true;
238
+ }
239
+ async function loadConfig(configPath) {
240
+ const value = parseYaml(await readFile(configPath, "utf8"));
241
+ assertValid(validateConfig, value, "\u8FD0\u884C\u914D\u7F6E");
242
+ const types = new Set(value.sourceSets.map((source) => source.type));
243
+ if (types.size < 2) {
244
+ throw new Error("V2 \u8981\u6C42\u81F3\u5C11\u4E24\u79CD\u4E0D\u540C\u7684\u6765\u6E90\u7C7B\u578B");
245
+ }
246
+ return value;
247
+ }
248
+ async function plan(repo, configPath) {
249
+ const repoRoot = await realpath(repo);
250
+ const config = await loadConfig(configPath);
251
+ const inside = await git(repoRoot, ["rev-parse", "--is-inside-work-tree"]);
252
+ if (inside !== "true") throw new Error("\u76EE\u6807\u8DEF\u5F84\u4E0D\u662F Git \u5DE5\u4F5C\u6811");
253
+ return {
254
+ repoRoot,
255
+ outputRoot: config.outputRoot,
256
+ sourceTypes: [...new Set(config.sourceSets.map((source) => source.type))].sort(),
257
+ baseCommit: await git(repoRoot, ["rev-parse", "HEAD"])
258
+ };
259
+ }
260
+ async function fileRevision(repo, file) {
261
+ try {
262
+ return await git(repo, ["log", "-1", "--format=%H", "--", file]) || "untracked";
263
+ } catch {
264
+ return "untracked";
265
+ }
266
+ }
267
+ async function scanFileSet(repo, source, excludes) {
268
+ if (!source.include?.length) {
269
+ throw new Error(`${SOURCE_TYPE_LABEL[source.type]}\u6765\u6E90\u5FC5\u987B\u914D\u7F6E include glob`);
270
+ }
271
+ const files = await fg(source.include, {
272
+ cwd: repo,
273
+ onlyFiles: true,
274
+ unique: true,
275
+ dot: false,
276
+ followSymbolicLinks: false,
277
+ ignore: excludes
278
+ });
279
+ files.sort();
280
+ if (files.length === 0) {
281
+ throw new Error(`${SOURCE_TYPE_LABEL[source.type]}\u6765\u6E90\u4E3A\u7A7A`);
282
+ }
283
+ return Promise.all(
284
+ files.map(async (file) => {
285
+ if (redactSensitive(file) !== file) {
286
+ throw new Error(`\u62D2\u7EDD\u5305\u542B\u654F\u611F\u4FE1\u606F\u7684\u6765\u6E90\u8DEF\u5F84\uFF1A${file}`);
287
+ }
288
+ const root = await realpath(repo);
289
+ const absolute = path.resolve(root, file);
290
+ const relative = path.relative(root, absolute);
291
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) {
292
+ throw new Error(`\u62D2\u7EDD\u8BFB\u53D6\u4ED3\u5E93\u4E4B\u5916\u7684\u6765\u6E90\uFF1A${file}`);
293
+ }
294
+ const stat2 = await lstat(absolute);
295
+ if (stat2.isSymbolicLink()) throw new Error(`\u62D2\u7EDD\u7B26\u53F7\u94FE\u63A5\u6765\u6E90\uFF1A${file}`);
296
+ const resolvedSource = await realpath(absolute);
297
+ if (resolvedSource !== absolute) {
298
+ throw new Error(`\u62D2\u7EDD\u901A\u8FC7\u7B26\u53F7\u94FE\u63A5\u9003\u9038\u4ED3\u5E93\u7684\u6765\u6E90\uFF1A${file}`);
299
+ }
300
+ const buffer = await readFile(absolute);
301
+ const text = buffer.toString("utf8");
302
+ const isText = /\.(?:md|mdx|txt|json|ya?ml|tsx?|jsx?|css|html)$/i.test(file);
303
+ return {
304
+ sourceType: source.type,
305
+ sourceRef: file.split(path.sep).join("/"),
306
+ sourceRevision: await fileRevision(repo, file),
307
+ contentHash: sha256(isText ? text : buffer),
308
+ content: text
309
+ };
310
+ })
311
+ );
312
+ }
313
+ async function scanGitSet(repo, source) {
314
+ const maxCommits = source.maxCommits ?? 20;
315
+ const output = await git(repo, [
316
+ "log",
317
+ `-${maxCommits}`,
318
+ "--format=%H%x1f%s%x1f%b%x1f%aI%x00"
319
+ ]);
320
+ const records = output.split("\0").map((record) => record.trim()).filter(Boolean).map((record) => {
321
+ const [commit = "", subject = "", body = "", authoredAt = ""] = record.split("");
322
+ const content = [subject, body, authoredAt].filter(Boolean).join("\n");
323
+ return {
324
+ sourceType: "git",
325
+ sourceRef: `git:${commit}`,
326
+ sourceRevision: commit,
327
+ contentHash: sha256(content),
328
+ content
329
+ };
330
+ });
331
+ if (records.length === 0) throw new Error("Git \u5386\u53F2\u6765\u6E90\u4E3A\u7A7A");
332
+ return records;
333
+ }
334
+ async function scan(repo, config) {
335
+ const excludes = [
336
+ `${OUTPUT_ROOT}/**`,
337
+ "node_modules/**",
338
+ "dist/**",
339
+ ...config.exclude ?? []
340
+ ];
341
+ const groups = await Promise.all(
342
+ config.sourceSets.map(
343
+ (source) => source.type === "git" ? scanGitSet(repo, source) : scanFileSet(repo, source, excludes)
344
+ )
345
+ );
346
+ const records = groups.flat().sort(
347
+ (a, b) => `${a.sourceType}:${a.sourceRef}`.localeCompare(`${b.sourceType}:${b.sourceRef}`)
348
+ );
349
+ const nonEmptyTypes = new Set(records.map((record) => record.sourceType));
350
+ if (nonEmptyTypes.size < 2) {
351
+ throw new Error("V2 \u8981\u6C42\u81F3\u5C11\u4E24\u7C7B\u771F\u5B9E\u4E14\u975E\u7A7A\u7684\u6765\u6E90");
352
+ }
353
+ return records;
354
+ }
355
+ function titleFor(record) {
356
+ const content = redactSensitive(record.content);
357
+ if (record.sourceType === "git") {
358
+ return content.split("\n")[0]?.trim() || record.sourceRevision.slice(0, 12);
359
+ }
360
+ const heading = content.match(/^#\s+(.+)$/m)?.[1]?.trim();
361
+ return heading || path.basename(record.sourceRef, path.extname(record.sourceRef));
362
+ }
363
+ function compactLines(content) {
364
+ return content.replace(/^#{1,6}\s+/gm, "").split(/\n+/).map((line) => line.trim()).filter(Boolean);
365
+ }
366
+ function redactSensitive(value) {
367
+ return value.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]").replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{20,}|AKIA[A-Z0-9]{16})\b/g, "[REDACTED_SECRET]").replace(
368
+ /\b(api[_-]?key|token|secret|password)\s*[:=]\s*["']?[^\s"'`]+/gi,
369
+ "$1=[REDACTED_SECRET]"
370
+ );
371
+ }
372
+ function prepare(records, previous) {
373
+ return records.map((record) => {
374
+ const sourceKey = `${record.sourceType}:${record.sourceRef}`;
375
+ const old = previous?.sources[sourceKey];
376
+ const writebackKind = old?.contentHash === record.contentHash ? "\u672A\u53D8" : old ? "\u589E\u91CF" : "\u65B0\u589E";
377
+ const lines = compactLines(redactSensitive(record.content));
378
+ const evidence = [`${record.sourceRef}@${record.sourceRevision}`];
379
+ const hasActions = lines.some((line) => /(^|\s)(must|should|todo|需要|必须|应当)/i.test(line));
380
+ const summary = lines.join(" ").slice(0, 280);
381
+ const asset = {
382
+ assetId: sha256(sourceKey),
383
+ sourceType: record.sourceType,
384
+ sourceRef: record.sourceRef,
385
+ sourceRevision: record.sourceRevision,
386
+ contentHash: record.contentHash,
387
+ title: titleFor(record),
388
+ summary,
389
+ facts: lines.slice(0, 5),
390
+ decisions: [],
391
+ actions: hasActions ? lines.filter((line) => /must|should|todo|需要|必须|应当/i.test(line)).slice(0, 5) : [],
392
+ relations: [],
393
+ analysis: {
394
+ completeness: {
395
+ level: summary.length >= 80 ? "partial" : "missing",
396
+ evidence,
397
+ gaps: ["\u9700\u8981\u4EBA\u5DE5\u786E\u8BA4\u8303\u56F4\u3001\u7EA6\u675F\u4E0E\u9A8C\u6536\u8FB9\u754C"],
398
+ recommendations: ["\u7531 Skill \u57FA\u4E8E\u4E1A\u52A1\u4E0A\u4E0B\u6587\u8865\u5168\u5B8C\u6574\u6027\u5206\u6790"]
399
+ },
400
+ executability: {
401
+ level: hasActions ? "partial" : "missing",
402
+ evidence,
403
+ gaps: hasActions ? ["\u884C\u52A8\u9879\u8D1F\u8D23\u4EBA\u548C\u5B8C\u6210\u6761\u4EF6\u5F85\u786E\u8BA4"] : ["\u672A\u8BC6\u522B\u5230\u660E\u786E\u884C\u52A8\u9879"],
404
+ recommendations: ["\u8865\u5145\u8D1F\u8D23\u4EBA\u3001\u524D\u7F6E\u6761\u4EF6\u548C\u53EF\u9A8C\u8BC1\u5B8C\u6210\u6807\u51C6"]
405
+ },
406
+ consistency: {
407
+ level: "partial",
408
+ evidence,
409
+ gaps: ["\u9700\u8981\u8DE8\u8D44\u4EA7\u5BF9\u7167\u540E\u786E\u8BA4\u4E00\u81F4\u6027"],
410
+ recommendations: ["\u5C06\u76F8\u5173\u9700\u6C42\u3001\u8BBE\u8BA1\u548C\u5386\u53F2\u51B3\u7B56\u8FDB\u884C\u4EA4\u53C9\u6838\u9A8C"]
411
+ },
412
+ reuseValue: {
413
+ level: lines.length >= 3 ? "medium" : "low",
414
+ evidence,
415
+ gaps: ["\u590D\u7528\u573A\u666F\u4E0E\u9002\u7528\u8FB9\u754C\u5F85\u786E\u8BA4"],
416
+ recommendations: ["\u63D0\u70BC\u53EF\u590D\u5236\u7684\u51B3\u7B56\u3001\u68C0\u67E5\u8868\u6216\u6A21\u677F"]
417
+ }
418
+ },
419
+ writebackKind
420
+ };
421
+ assertValid(validateAsset, asset, `\u8D44\u4EA7 ${asset.assetId}`);
422
+ return asset;
423
+ });
424
+ }
425
+ async function loadAssetsFile(file, records) {
426
+ const value = JSON.parse(await readFile(file, "utf8"));
427
+ if (!Array.isArray(value)) throw new Error("\u589E\u5F3A\u8D44\u4EA7\u8F93\u5165\u5FC5\u987B\u662F JSON \u6570\u7EC4");
428
+ const assets = value.map((candidate, index) => {
429
+ assertValid(validateAsset, candidate, `\u8D44\u4EA7\u8F93\u5165 ${index}`);
430
+ const serialized = JSON.stringify(candidate);
431
+ if (redactSensitive(serialized) !== serialized) {
432
+ throw new Error(`\u8D44\u4EA7\u8F93\u5165 ${index} \u5305\u542B\u654F\u611F\u6570\u636E`);
433
+ }
434
+ return candidate;
435
+ });
436
+ const sourceKeys = assets.map((asset) => `${asset.sourceType}:${asset.sourceRef}`);
437
+ if (new Set(sourceKeys).size !== assets.length || new Set(assets.map((asset) => asset.assetId)).size !== assets.length) {
438
+ throw new Error("\u589E\u5F3A\u8D44\u4EA7\u8F93\u5165\u5305\u542B\u91CD\u590D\u6765\u6E90\u6216\u91CD\u590D\u8D44\u4EA7 ID");
439
+ }
440
+ if (records) {
441
+ const bySource = new Map(
442
+ records.map((record) => [`${record.sourceType}:${record.sourceRef}`, record])
443
+ );
444
+ for (const asset of assets) {
445
+ const record = bySource.get(`${asset.sourceType}:${asset.sourceRef}`);
446
+ if (!record) throw new Error(`\u8D44\u4EA7\u6765\u6E90\u4E0D\u5728\u672C\u6B21\u626B\u63CF\u4E2D\uFF1A${asset.sourceRef}`);
447
+ if (asset.assetId !== sha256(`${record.sourceType}:${record.sourceRef}`) || asset.contentHash !== record.contentHash || asset.sourceRevision !== record.sourceRevision) {
448
+ throw new Error(`\u8D44\u4EA7\u6765\u6E90\u8FFD\u8E2A\u5B57\u6BB5\u88AB\u4FEE\u6539\uFF1A${asset.sourceRef}`);
449
+ }
450
+ }
451
+ if (assets.length !== records.length) {
452
+ throw new Error("\u589E\u5F3A\u8D44\u4EA7\u8F93\u5165\u5FC5\u987B\u4E3A\u6BCF\u4E2A\u626B\u63CF\u6765\u6E90\u63D0\u4F9B\u4E14\u4EC5\u63D0\u4F9B\u4E00\u6761\u8D44\u4EA7");
453
+ }
454
+ const expectedSources = new Set(bySource.keys());
455
+ if (sourceKeys.some((key) => !expectedSources.has(key))) {
456
+ throw new Error("\u589E\u5F3A\u8D44\u4EA7\u8F93\u5165\u4E0E\u672C\u6B21\u626B\u63CF\u4E0D\u5339\u914D");
457
+ }
458
+ }
459
+ return assets;
460
+ }
461
+ function renderDimension(label, finding) {
462
+ const bullets = (items) => items.length ? items.map((item) => `- ${redactSensitive(item)}`).join("\n") : "- \u65E0";
463
+ return `### ${label}\uFF1A${LEVEL_LABEL[finding.level]}
464
+
465
+ **\u8BC1\u636E**
466
+ ${bullets(finding.evidence)}
467
+
468
+ **\u7F3A\u53E3**
469
+ ${bullets(finding.gaps)}
470
+
471
+ **\u5EFA\u8BAE**
472
+ ${bullets(finding.recommendations)}`;
473
+ }
474
+ function renderAsset(asset) {
475
+ return `# ${redactSensitive(asset.title)}
476
+
477
+ - \u6807\u8BB0\uFF1A${asset.writebackKind}
478
+ - \u7C7B\u578B\uFF1A${SOURCE_TYPE_LABEL[asset.sourceType]}
479
+ - \u6765\u6E90\uFF1A\`${redactSensitive(asset.sourceRef)}\`
480
+ - \u6765\u6E90\u7248\u672C\uFF08sourceRevision\uFF09\uFF1A\`${asset.sourceRevision}\`
481
+ - \u5185\u5BB9\u6307\u7EB9\uFF08contentHash\uFF09\uFF1A\`${asset.contentHash}\`
482
+ - \u8D44\u4EA7 ID\uFF08assetId\uFF09\uFF1A\`${asset.assetId}\`
483
+
484
+ ## \u6458\u8981
485
+
486
+ ${redactSensitive(asset.summary) || "\u65E0\u53EF\u63D0\u53D6\u6458\u8981\u3002"}
487
+
488
+ ## \u56DB\u7EF4\u5206\u6790
489
+
490
+ ${renderDimension("\u5B8C\u6574\u6027", asset.analysis.completeness)}
491
+
492
+ ${renderDimension("\u53EF\u6267\u884C\u6027", asset.analysis.executability)}
493
+
494
+ ${renderDimension("\u4E00\u81F4\u6027", asset.analysis.consistency)}
495
+
496
+ ${renderDimension("\u590D\u7528\u4EF7\u503C", asset.analysis.reuseValue)}
497
+ `;
498
+ }
499
+ async function readState(repo) {
500
+ const statePath = path.join(repo, OUTPUT_ROOT, "state/read-state.json");
501
+ if (!await exists(statePath)) return null;
502
+ const value = JSON.parse(await readFile(statePath, "utf8"));
503
+ assertValid(validateState, value, "\u8BFB\u53D6\u72B6\u6001");
504
+ const expected = sha256(JSON.stringify({ ...value, selfHash: "" }, null, 2));
505
+ if (value.selfHash !== expected) {
506
+ throw new Error("\u8BFB\u53D6\u72B6\u6001\u7684 selfHash \u4E0D\u5339\u914D\uFF1B\u62D2\u7EDD\u8986\u76D6\u5DF2\u7F16\u8F91\u7684 state");
507
+ }
508
+ return value;
509
+ }
510
+ function renderIndex(assets) {
511
+ const rows = assets.map(
512
+ (asset) => `| [${redactSensitive(asset.title)}](assets/${asset.assetId.slice(7)}.md) | ${SOURCE_TYPE_LABEL[asset.sourceType]} | ${asset.writebackKind} | \`${redactSensitive(asset.sourceRef)}\` |`
513
+ ).join("\n");
514
+ return `# \u8D44\u4EA7\u590D\u5229\u7D22\u5F15
515
+
516
+ > \u672C\u76EE\u5F55\u7531 asset-compound \u7BA1\u7406\u3002\u793A\u4F8B fixture \u4E0D\u80FD\u4F5C\u4E3A\u771F\u5B9E\u9A8C\u6536\u6570\u636E\u3002
517
+
518
+ | \u8D44\u4EA7 | \u7C7B\u578B | \u6807\u8BB0 | \u6765\u6E90 |
519
+ |---|---|---|---|
520
+ ${rows}
521
+ `;
522
+ }
523
+ async function loadTemplate(name) {
524
+ return readFile(new URL(`../templates/${name}`, import.meta.url), "utf8");
525
+ }
526
+ async function renderConflicts(runId, conflicts) {
527
+ const details = conflicts.length === 0 ? "\u672C\u6B21\u68C0\u67E5\u53D1\u73B0 0 \u4E2A\u51B2\u7A81\u3002" : conflicts.map(
528
+ (conflict) => `## ${conflict.assetId}
529
+
530
+ - \u8DEF\u5F84\uFF1A\`${conflict.path}\`
531
+ - \u539F\u56E0\uFF1A${conflict.reason}
532
+ - \u671F\u671B\u5185\u5BB9\u6307\u7EB9\uFF08hash\uFF09\uFF1A\`${conflict.expectedHash ?? "\u65E0"}\`
533
+ - \u5F53\u524D\u5185\u5BB9\u6307\u7EB9\uFF08hash\uFF09\uFF1A\`${conflict.actualHash ?? "\u65E0"}\``
534
+ ).join("\n\n");
535
+ return (await loadTemplate("conflict-report.md")).replaceAll("{{run_id}}", runId).replaceAll("{{conflict_count}}", String(conflicts.length)).replaceAll("{{conflicts}}", details);
536
+ }
537
+ async function renderAcceptance(branch, base, head, result) {
538
+ const requirements = [
539
+ "| \u6761\u6B3E | \u7ED3\u679C |",
540
+ "|---|---|",
541
+ "| R1 \u8BC6\u522B\u8BFB\u53D6 | \u5DF2\u751F\u6210 manifest\uFF0C\u4ECD\u9700\u4EBA\u5DE5\u786E\u8BA4\u6750\u6599\u8303\u56F4 |",
542
+ "| R2 \u7ED3\u6784\u5316\u63D0\u53D6 | \u5DF2\u901A\u8FC7 JSON Schema |",
543
+ "| R3 \u56DB\u7EF4\u5206\u6790 | \u5DF2\u751F\u6210\u56DB\u7EF4\u57FA\u7EBF\uFF0C\u4E1A\u52A1\u7ED3\u8BBA\u9700\u4EBA\u5DE5\u62BD\u68C0 |",
544
+ "| R4 \u589E\u91CF\u56DE\u5199 | \u5DF2\u9650\u5236 docs/\u53EF\u8BFB\u5316 \u5E76\u8F93\u51FA\u51B2\u7A81\u62A5\u544A |",
545
+ "| R5 \u589E\u91CF\u72B6\u6001 | \u5DF2\u4FDD\u5B58\u6587\u4EF6 hash \u4E0E commit\uFF1B\u9700\u4E8C\u6B21\u8FD0\u884C\u9A8C\u8BC1 |",
546
+ "| R6 \u5168\u94FE\u8DEF\u7559\u75D5 | \u5DF2\u5EFA\u7ACB\u6765\u6E90\u3001revision\u3001asset \u4E0E\u8F93\u51FA\u6620\u5C04 |"
547
+ ].join("\n");
548
+ const verification = [
549
+ "| \u9A8C\u6536\u70B9 | \u7ED3\u679C |",
550
+ "|---|---|",
551
+ "| V1\u2013V4 | \u5DF2\u4EA7\u51FA\u673A\u5668\u8BC1\u636E\uFF1B\u771F\u5B9E\u6750\u6599\u4E0E\u56DB\u7EF4\u7ED3\u8BBA\u4ECD\u9700\u4EBA\u5DE5\u9A8C\u6536 |",
552
+ "| V5 | \u5DF2\u9A8C\u8BC1\u975E\u57FA\u7EBF\u5206\u652F\u4E0E Git scope\uFF1B\u63D0\u4EA4/PR \u5F85\u5916\u90E8\u6D41\u7A0B\u786E\u8BA4 |",
553
+ "| V6 | \u5F85\u76F8\u540C\u8F93\u5165\u4E8C\u6B21\u8FD0\u884C\u8BC1\u660E\u96F6\u4E1A\u52A1 diff |",
554
+ "| V7 | \u5DF2\u8BB0\u5F55 revision \u4E0E hash\uFF1B\u56DE\u5199 commit \u5F85\u63D0\u4EA4\u540E\u8865\u9F50 |",
555
+ "| V8 | \u5DF2\u751F\u6210\u7D22\u5F15\u4E0E\u8D44\u4EA7\u8BE6\u60C5\uFF1B\u53EF\u8BFB\u590D\u7528\u9700\u4EBA\u5DE5\u62BD\u68C0 |"
556
+ ].join("\n");
557
+ return (await loadTemplate("acceptance-report.md")).replaceAll("{{run_id}}", result.runId).replaceAll("{{branch}}", branch).replaceAll("{{base_sha}}", base).replaceAll("{{head_sha}}", head).replaceAll(
558
+ "{{source_type_summary}}",
559
+ [...new Set(result.assets.map((asset) => SOURCE_TYPE_LABEL[asset.sourceType]))].sort().join("\u3001")
560
+ ).replaceAll("{{new_count}}", String(result.counts.new)).replaceAll("{{updated_count}}", String(result.counts.updated)).replaceAll("{{unchanged_count}}", String(result.counts.unchanged)).replaceAll("{{conflict_count}}", String(result.counts.conflicts)).replaceAll("{{requirements_matrix}}", requirements).replaceAll("{{verification_matrix}}", verification).replaceAll(
561
+ "{{four_dimension_summary}}",
562
+ `${result.assets.length} \u6761\u8D44\u4EA7\u5747\u5305\u542B\u5B8C\u6574\u6027\u3001\u53EF\u6267\u884C\u6027\u3001\u4E00\u81F4\u6027\u3001\u590D\u7528\u4EF7\u503C\u56DB\u9879\u7ED3\u6784\u5316\u7ED3\u8BBA\u3002`
563
+ ).replaceAll(
564
+ "{{conflict_and_evidence_links}}",
565
+ `- [\u51B2\u7A81\u62A5\u544A](../conflicts/${result.runId}.md)
566
+ - [\u6765\u6E90\u6E05\u5355](../source-manifest.yaml)`
567
+ );
568
+ }
569
+ async function render(repo, config, records, assets, previous) {
570
+ const head = await git(repo, ["rev-parse", "HEAD"]);
571
+ const branch = await git(repo, ["branch", "--show-current"]);
572
+ const baseBranch = config.baseBranch ?? "main";
573
+ const baseCommit = await git(repo, ["merge-base", "HEAD", baseBranch]);
574
+ const changedAssets = assets.filter((asset) => asset.writebackKind !== "\u672A\u53D8");
575
+ const conflicts = [];
576
+ const contentByAsset = /* @__PURE__ */ new Map();
577
+ for (const asset of assets) {
578
+ const content = renderAsset(asset);
579
+ contentByAsset.set(asset.assetId, content);
580
+ const old = previous?.assets[asset.assetId];
581
+ const expectedPath = `${OUTPUT_ROOT}/assets/${asset.assetId.slice(7)}.md`;
582
+ if (old) {
583
+ const oldPath = await assertOutputPath(repo, old.outputPath);
584
+ if (await exists(oldPath)) {
585
+ const actualHash = sha256(await readFile(oldPath, "utf8"));
586
+ if (actualHash !== old.lastGeneratedHash) {
587
+ conflicts.push({
588
+ assetId: asset.assetId,
589
+ path: old.outputPath,
590
+ reason: "\u4EBA\u5DE5\u7F16\u8F91\u4E0E\u751F\u6210\u5185\u5BB9\u51B2\u7A81\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6",
591
+ expectedHash: old.lastGeneratedHash,
592
+ actualHash
593
+ });
594
+ }
595
+ } else {
596
+ conflicts.push({
597
+ assetId: asset.assetId,
598
+ path: old.outputPath,
599
+ reason: "state \u5F15\u7528\u7684\u751F\u6210\u6587\u4EF6\u7F3A\u5931\uFF0C\u5DF2\u62D2\u7EDD\u9759\u9ED8\u91CD\u5EFA",
600
+ expectedHash: old.lastGeneratedHash
601
+ });
602
+ }
603
+ } else if (await exists(path.join(repo, expectedPath))) {
604
+ conflicts.push({
605
+ assetId: asset.assetId,
606
+ path: expectedPath,
607
+ reason: "\u53D1\u73B0\u65E0 state \u5F52\u5C5E\u7684\u65E2\u6709\u8D44\u4EA7\u6587\u4EF6\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6",
608
+ actualHash: sha256(await readFile(path.join(repo, expectedPath), "utf8"))
609
+ });
610
+ }
611
+ }
612
+ for (const [managedPath, expectedHash] of Object.entries(previous?.managedFiles ?? {})) {
613
+ const absolute = await assertOutputPath(repo, managedPath);
614
+ if (!await exists(absolute)) {
615
+ conflicts.push({
616
+ assetId: `managed:${managedPath}`,
617
+ path: managedPath,
618
+ reason: "\u53D7\u7BA1\u8F93\u51FA\u6587\u4EF6\u7F3A\u5931\uFF0C\u5DF2\u62D2\u7EDD\u9759\u9ED8\u91CD\u5EFA",
619
+ expectedHash
620
+ });
621
+ continue;
622
+ }
623
+ const actualHash = sha256(await readFile(absolute));
624
+ if (actualHash !== expectedHash) {
625
+ conflicts.push({
626
+ assetId: `managed:${managedPath}`,
627
+ path: managedPath,
628
+ reason: "\u53D7\u7BA1\u8F93\u51FA\u88AB\u4EBA\u5DE5\u7F16\u8F91\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6",
629
+ expectedHash,
630
+ actualHash
631
+ });
632
+ }
633
+ }
634
+ const runId = sha256(
635
+ `${head}
636
+ ${records.map((record) => `${record.sourceRef}:${record.contentHash}`).join("\n")}`
637
+ ).slice(7, 23);
638
+ if (!previous) {
639
+ const firstRunManagedPaths = [
640
+ `${OUTPUT_ROOT}/README.md`,
641
+ `${OUTPUT_ROOT}/index.md`,
642
+ `${OUTPUT_ROOT}/source-manifest.yaml`,
643
+ `${OUTPUT_ROOT}/conflicts/${runId}.md`,
644
+ `${OUTPUT_ROOT}/evidence/acceptance-report.md`,
645
+ `${OUTPUT_ROOT}/runs/${runId}.md`,
646
+ `${OUTPUT_ROOT}/state/asset-index.jsonl`
647
+ ];
648
+ for (const managedPath of firstRunManagedPaths) {
649
+ const absolute = await assertOutputPath(repo, managedPath);
650
+ if (await exists(absolute)) {
651
+ conflicts.push({
652
+ assetId: `managed:${managedPath}`,
653
+ path: managedPath,
654
+ reason: "\u9996\u6B21\u8FD0\u884C\u53D1\u73B0\u65E0 state \u5F52\u5C5E\u7684\u65E2\u6709\u53D7\u7BA1\u6587\u4EF6\uFF0C\u5DF2\u62D2\u7EDD\u8986\u76D6",
655
+ actualHash: sha256(await readFile(absolute))
656
+ });
657
+ }
658
+ }
659
+ }
660
+ const conflictIds = new Set(conflicts.map((conflict) => conflict.assetId));
661
+ const writableAssets = changedAssets.filter((asset) => !conflictIds.has(asset.assetId));
662
+ const currentSourceKeys = new Set(
663
+ records.map((record) => `${record.sourceType}:${record.sourceRef}`)
664
+ );
665
+ const hasStaleTransition = Object.entries(previous?.sources ?? {}).some(
666
+ ([key, value]) => value.status === "active" && !currentSourceKeys.has(key)
667
+ );
668
+ const resultBase = {
669
+ runId,
670
+ counts: {
671
+ new: assets.filter((asset) => asset.writebackKind === "\u65B0\u589E").length,
672
+ updated: assets.filter((asset) => asset.writebackKind === "\u589E\u91CF").length,
673
+ unchanged: assets.filter((asset) => asset.writebackKind === "\u672A\u53D8").length,
674
+ conflicts: conflicts.length
675
+ },
676
+ assets,
677
+ conflicts,
678
+ noOp: changedAssets.length === 0 && conflicts.length === 0 && !hasStaleTransition
679
+ };
680
+ if (resultBase.noOp) {
681
+ return { ...resultBase, changedFiles: [], secondRunZeroDiff: true };
682
+ }
683
+ if (conflicts.length > 0) {
684
+ const conflictKey = sha256(JSON.stringify(conflicts)).slice(7, 19);
685
+ const conflictPath = `${OUTPUT_ROOT}/conflicts/${runId}-${conflictKey}.md`;
686
+ const changed = await atomicWriteIfChanged(
687
+ repo,
688
+ conflictPath,
689
+ await renderConflicts(runId, conflicts)
690
+ );
691
+ return {
692
+ ...resultBase,
693
+ changedFiles: changed ? [conflictPath] : []
694
+ };
695
+ }
696
+ const changedFiles = [];
697
+ const write = async (file, content) => {
698
+ if (await atomicWriteIfChanged(repo, file, content)) changedFiles.push(file);
699
+ };
700
+ for (const asset of writableAssets) {
701
+ const file = `${OUTPUT_ROOT}/assets/${asset.assetId.slice(7)}.md`;
702
+ await write(file, contentByAsset.get(asset.assetId) ?? renderAsset(asset));
703
+ }
704
+ const sources = {};
705
+ for (const [key, value] of Object.entries(previous?.sources ?? {})) {
706
+ sources[key] = { ...value, status: "stale" };
707
+ }
708
+ for (const record of records) {
709
+ const sourceKey = `${record.sourceType}:${record.sourceRef}`;
710
+ const asset = assets.find(
711
+ (candidate) => candidate.sourceType === record.sourceType && candidate.sourceRef === record.sourceRef
712
+ );
713
+ if (!asset || conflictIds.has(asset.assetId)) continue;
714
+ sources[sourceKey] = {
715
+ contentHash: record.contentHash,
716
+ sourceRevision: record.sourceRevision,
717
+ assetIds: [asset.assetId],
718
+ status: "active"
719
+ };
720
+ }
721
+ const stateAssets = { ...previous?.assets ?? {} };
722
+ for (const asset of writableAssets) {
723
+ const outputPath = `${OUTPUT_ROOT}/assets/${asset.assetId.slice(7)}.md`;
724
+ const content = contentByAsset.get(asset.assetId) ?? renderAsset(asset);
725
+ stateAssets[asset.assetId] = {
726
+ canonicalContentHash: asset.contentHash,
727
+ outputPath,
728
+ lastGeneratedHash: sha256(`${content.trimEnd()}
729
+ `)
730
+ };
731
+ }
732
+ const manifest = {
733
+ sourceTypes: [...new Set(records.map((record) => record.sourceType))].sort(),
734
+ sources: records.map(({ content: _content, ...record }) => record)
735
+ };
736
+ const managedContents = {
737
+ [`${OUTPUT_ROOT}/README.md`]: "# \u53EF\u8BFB\u5316\u8D44\u4EA7\n\n\u4ECE `index.md` \u5F00\u59CB\u9605\u8BFB\u3002\u6240\u6709\u5185\u5BB9\u5747\u4FDD\u7559\u6765\u6E90\u548C\u6765\u6E90\u7248\u672C\uFF08revision\uFF09\u3002\n",
738
+ [`${OUTPUT_ROOT}/index.md`]: renderIndex(assets),
739
+ [`${OUTPUT_ROOT}/source-manifest.yaml`]: stringifyYaml(manifest),
740
+ [`${OUTPUT_ROOT}/conflicts/${runId}.md`]: await renderConflicts(runId, conflicts),
741
+ [`${OUTPUT_ROOT}/evidence/acceptance-report.md`]: await renderAcceptance(
742
+ branch,
743
+ baseCommit,
744
+ head,
745
+ resultBase
746
+ ),
747
+ [`${OUTPUT_ROOT}/runs/${runId}.md`]: `# \u8FD0\u884C\u8BB0\u5F55 ${runId}
748
+
749
+ - \u5F53\u524D\u63D0\u4EA4\uFF1A\`${head}\`
750
+ - \u6765\u6E90\u6570\uFF1A${records.length}
751
+ - \u65B0\u589E\uFF1A${resultBase.counts.new}
752
+ - \u589E\u91CF\uFF1A${resultBase.counts.updated}
753
+ - \u672A\u53D8\uFF1A${resultBase.counts.unchanged}
754
+ - \u51B2\u7A81\uFF1A${conflicts.length}
755
+ `,
756
+ [`${OUTPUT_ROOT}/state/asset-index.jsonl`]: `${assets.map((asset) => JSON.stringify(asset)).join("\n")}
757
+ `
758
+ };
759
+ const managedFiles = {
760
+ ...previous?.managedFiles ?? {},
761
+ ...Object.fromEntries(
762
+ Object.entries(managedContents).map(([file, content]) => [
763
+ file,
764
+ sha256(`${content.trimEnd()}
765
+ `)
766
+ ])
767
+ )
768
+ };
769
+ const nextState = {
770
+ schemaVersion: 1,
771
+ selfHash: "",
772
+ baseBranch,
773
+ baseCommit: previous?.baseCommit ?? baseCommit,
774
+ lastReadCommit: head,
775
+ sources,
776
+ assets: stateAssets,
777
+ managedFiles
778
+ };
779
+ nextState.selfHash = sha256(JSON.stringify({ ...nextState, selfHash: "" }, null, 2));
780
+ assertValid(validateState, nextState, "\u4E0B\u4E00\u7248\u72B6\u6001");
781
+ for (const [file, content] of Object.entries(managedContents)) {
782
+ await write(file, content);
783
+ }
784
+ await write(`${OUTPUT_ROOT}/state/read-state.json`, JSON.stringify(nextState, null, 2));
785
+ changedFiles.sort();
786
+ return { ...resultBase, changedFiles };
787
+ }
788
+ async function verifyGitScope(repo) {
789
+ const output = await git(repo, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]);
790
+ const entries = output.split("\0").filter(Boolean);
791
+ const paths = [];
792
+ for (let index = 0; index < entries.length; index += 1) {
793
+ const entry = entries[index] ?? "";
794
+ paths.push(entry.slice(3));
795
+ const status = entry.slice(0, 2);
796
+ if (status.includes("R") || status.includes("C")) {
797
+ const original = entries[index + 1];
798
+ if (original) paths.push(original);
799
+ index += 1;
800
+ }
801
+ }
802
+ const outside = paths.filter(
803
+ (file) => file !== OUTPUT_ROOT && !file.startsWith(`${OUTPUT_ROOT}/`)
804
+ );
805
+ if (outside.length) {
806
+ throw new Error(`\u68C0\u6D4B\u5230 ${OUTPUT_ROOT} \u4E4B\u5916\u7684\u6539\u52A8\uFF1A${outside.join("\u3001")}`);
807
+ }
808
+ return paths.sort();
809
+ }
810
+ async function runPipeline(repoInput, configPath, assetsFile) {
811
+ const repo = await realpath(repoInput);
812
+ const config = await loadConfig(configPath);
813
+ const branch = await git(repo, ["branch", "--show-current"]);
814
+ const baseBranch = config.baseBranch ?? "main";
815
+ if (!branch || branch === baseBranch) {
816
+ throw new Error(`\u5199\u5165\u5FC5\u987B\u4F7F\u7528\u72EC\u7ACB\u5206\u652F\uFF1B\u5F53\u524D\u5206\u652F\u4E3A ${branch || "\uFF08\u5206\u79BB HEAD\uFF09"}`);
817
+ }
818
+ await verifyGitScope(repo);
819
+ const previous = await readState(repo);
820
+ const records = await scan(repo, config);
821
+ const baselineAssets = prepare(records, previous);
822
+ const assets = assetsFile ? (await loadAssetsFile(assetsFile, records)).map((asset) => ({
823
+ ...asset,
824
+ writebackKind: baselineAssets.find((baseline) => baseline.assetId === asset.assetId)?.writebackKind ?? asset.writebackKind
825
+ })) : baselineAssets;
826
+ const result = await render(repo, config, records, assets, previous);
827
+ await verifyGitScope(repo);
828
+ return result;
829
+ }
830
+ async function inspectPipeline(repoInput, configPath) {
831
+ const repo = await realpath(repoInput);
832
+ const config = await loadConfig(configPath);
833
+ const previous = await readState(repo);
834
+ const records = await scan(repo, config);
835
+ return { records, assets: prepare(records, previous) };
836
+ }
837
+ async function verifySecondRun(repoInput, configPath) {
838
+ const repo = await realpath(repoInput);
839
+ const config = await loadConfig(configPath);
840
+ const previous = await readState(repo);
841
+ if (!previous) throw new Error("\u4E8C\u6B21\u8FD0\u884C\u9A8C\u8BC1\u9700\u8981\u5DF2\u6709 state");
842
+ const records = await scan(repo, config);
843
+ const assets = prepare(records, previous);
844
+ const changed = assets.filter((asset) => asset.writebackKind !== "\u672A\u53D8");
845
+ if (changed.length) {
846
+ throw new Error(`\u7B2C\u4E8C\u6B21\u8FD0\u884C\u4ECD\u6709 ${changed.length} \u6761\u65B0\u589E\u6216\u589E\u91CF\u8D44\u4EA7`);
847
+ }
848
+ for (const asset of assets) {
849
+ const state = previous.assets[asset.assetId];
850
+ if (!state) throw new Error(`\u7F3A\u5C11\u8D44\u4EA7\u72B6\u6001\uFF1A${asset.assetId}`);
851
+ const target = await assertOutputPath(repo, state.outputPath);
852
+ if (!await exists(target) || sha256(await readFile(target)) !== state.lastGeneratedHash) {
853
+ throw new Error(`\u751F\u6210\u8D44\u4EA7\u4E0E state \u4E0D\u4E00\u81F4\uFF1A${state.outputPath}`);
854
+ }
855
+ }
856
+ for (const [managedPath, expectedHash] of Object.entries(previous.managedFiles)) {
857
+ const target = await assertOutputPath(repo, managedPath);
858
+ if (!await exists(target) || sha256(await readFile(target)) !== expectedHash) {
859
+ throw new Error(`\u53D7\u7BA1\u8F93\u51FA\u4E0E state \u4E0D\u4E00\u81F4\uFF1A${managedPath}`);
860
+ }
861
+ }
862
+ await verifyGitScope(repo);
863
+ return {
864
+ secondRunZeroDiff: true,
865
+ counts: { new: 0, updated: 0, unchanged: assets.length, conflicts: 0 },
866
+ stateCommit: previous.lastReadCommit
867
+ };
868
+ }
869
+
870
+ // src/setup.ts
871
+ import { mkdir as mkdir2, readFile as readFile2, stat } from "fs/promises";
872
+ import os from "os";
873
+ import path2 from "path";
874
+ import writeFileAtomic2 from "write-file-atomic";
875
+ var PACKAGE_ROOT = new URL("../", import.meta.url);
876
+ async function exists2(target) {
877
+ try {
878
+ await stat(target);
879
+ return true;
880
+ } catch {
881
+ return false;
882
+ }
883
+ }
884
+ async function packageText(relativePath) {
885
+ return readFile2(new URL(relativePath, PACKAGE_ROOT), "utf8");
886
+ }
887
+ async function writeGenerated(target, content, force) {
888
+ if (!force && await exists2(target)) {
889
+ throw new Error(`\u76EE\u6807\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1B\u5982\u9700\u8986\u76D6\u8BF7\u4F7F\u7528 --force\uFF1A${target}`);
890
+ }
891
+ await mkdir2(path2.dirname(target), { recursive: true });
892
+ await writeFileAtomic2(target, `${content.trimEnd()}
893
+ `, {
894
+ encoding: "utf8",
895
+ fsync: true
896
+ });
897
+ }
898
+ async function initGenericWorkspace(options = {}) {
899
+ const outputDir = path2.resolve(
900
+ options.output ?? path2.join(os.homedir(), ".asset-compound")
901
+ );
902
+ const baseBranch = options.baseBranch ?? "main";
903
+ const files = {
904
+ readme: path2.join(outputDir, "README.md"),
905
+ config: path2.join(outputDir, "run.yaml"),
906
+ prompt: path2.join(outputDir, "prompts", "asset-analysis.zh-CN.md"),
907
+ protocol: path2.join(outputDir, "protocol", "README.zh-CN.md"),
908
+ inputSchema: path2.join(outputDir, "schemas", "analysis-input.schema.json"),
909
+ outputSchema: path2.join(outputDir, "schemas", "analysis-output.schema.json")
910
+ };
911
+ const config = `outputRoot: docs/\u53EF\u8BFB\u5316
912
+ baseBranch: ${baseBranch}
913
+ sourceSets:
914
+ - type: requirement
915
+ include:
916
+ - docs/requirements/**/*.md
917
+ - type: design
918
+ include:
919
+ - docs/design/**/*.md
920
+ exclude:
921
+ - node_modules/**
922
+ - dist/**
923
+ - build/**
924
+ - coverage/**
925
+ `;
926
+ const readme = `# \u8D44\u4EA7\u590D\u5229\u901A\u7528\u5DE5\u4F5C\u533A
927
+
928
+ \u672C\u76EE\u5F55\u7531 \`asset-compound init\` \u751F\u6210\uFF0C\u4E0D\u4F9D\u8D56\u7279\u5B9A\u7F16\u8F91\u5668\u3002
929
+
930
+ 1. \u4FEE\u6539 \`run.yaml\`\uFF0C\u4F7F\u7528\u81F3\u5C11\u4E24\u7C7B\u771F\u5B9E\u975E\u7A7A\u6765\u6E90\u3002
931
+ 2. \u6267\u884C \`npx @vinsonw2/asset-compound prepare --repo <\u4ED3\u5E93> --config "${files.config}" > prepared.json\`\u3002
932
+ 3. \u8BA9\u4EFB\u610F AI \u7F16\u8F91\u5668\u6309 \`prompts/asset-analysis.zh-CN.md\` \u548C \`protocol/README.zh-CN.md\` \u751F\u6210 \`enriched.json\`\u3002
933
+ 4. \u6267\u884C \`npx @vinsonw2/asset-compound validate --input enriched.json\`\u3002
934
+ 5. \u5728\u72EC\u7ACB\u5206\u652F\u6267\u884C \`npx @vinsonw2/asset-compound finalize --repo <\u4ED3\u5E93> --config "${files.config}" --assets enriched.json\`\u3002
935
+
936
+ \u547D\u4EE4\u540D\u3001JSON \u5B57\u6BB5\u548C\u679A\u4E3E\u4FDD\u6301\u82F1\u6587\uFF1B\u4E2D\u6587\u542B\u4E49\u89C1\u63D0\u793A\u534F\u8BAE\u3002
937
+ `;
938
+ const force = options.force ?? false;
939
+ await writeGenerated(files.readme, readme, force);
940
+ await writeGenerated(files.config, config, force);
941
+ await writeGenerated(
942
+ files.prompt,
943
+ await packageText("prompts/asset-analysis.zh-CN.md"),
944
+ force
945
+ );
946
+ await writeGenerated(
947
+ files.protocol,
948
+ await packageText("protocol/README.zh-CN.md"),
949
+ force
950
+ );
951
+ await writeGenerated(
952
+ files.inputSchema,
953
+ await packageText("schemas/analysis-input.schema.json"),
954
+ force
955
+ );
956
+ await writeGenerated(
957
+ files.outputSchema,
958
+ await packageText("schemas/analysis-output.schema.json"),
959
+ force
960
+ );
961
+ return {
962
+ mode: "generic",
963
+ outputDir,
964
+ ...files
965
+ };
966
+ }
967
+ async function installEditorSkill(options) {
968
+ if (options.editor !== "cursor") {
969
+ throw new Error(
970
+ `\u5F53\u524D\u4EC5\u63D0\u4F9B Cursor \u9002\u914D\u5668\uFF1B\u901A\u7528\u6A21\u5F0F\u8BF7\u4F7F\u7528 init\u3002\u6536\u5230 editor=${options.editor}`
971
+ );
972
+ }
973
+ const target = path2.resolve(
974
+ options.target ?? path2.join(os.homedir(), ".cursor", "skills", "asset-compounding")
975
+ );
976
+ const skillFile = path2.join(target, "SKILL.md");
977
+ await writeGenerated(
978
+ skillFile,
979
+ await packageText("skill/asset-compounding/SKILL.md"),
980
+ options.force ?? false
981
+ );
982
+ return {
983
+ editor: "cursor",
984
+ target,
985
+ skillFile
986
+ };
987
+ }
988
+
989
+ export {
990
+ assetSchema,
991
+ validateAsset,
992
+ validateConfig,
993
+ validateState,
994
+ assertValid,
995
+ sha256,
996
+ git,
997
+ assertOutputPath,
998
+ atomicWriteIfChanged,
999
+ loadConfig,
1000
+ plan,
1001
+ scan,
1002
+ redactSensitive,
1003
+ prepare,
1004
+ loadAssetsFile,
1005
+ renderAsset,
1006
+ render,
1007
+ verifyGitScope,
1008
+ runPipeline,
1009
+ inspectPipeline,
1010
+ verifySecondRun,
1011
+ initGenericWorkspace,
1012
+ installEditorSkill
1013
+ };
1014
+ //# sourceMappingURL=chunk-P2MVWNYS.js.map