@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.
- package/README.md +244 -2
- package/artifacts/README.zh-CN.md +47 -0
- package/artifacts/SHA256SUMS +1 -0
- package/artifacts/asset-compound-0.1.0.tgz +0 -0
- package/dist/chunk-P2MVWNYS.js +1014 -0
- package/dist/chunk-P2MVWNYS.js.map +1 -0
- package/dist/cli.js +111 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/pipeline.d.ts +34 -0
- package/dist/types/schema.d.ts +186 -0
- package/dist/types/setup.d.ts +25 -0
- package/dist/types/types.d.ts +88 -0
- package/package.json +81 -4
- package/prompts/asset-analysis.zh-CN.md +59 -0
- package/protocol/README.zh-CN.md +49 -0
- package/schemas/analysis-input.schema.json +34 -0
- package/schemas/analysis-output.schema.json +117 -0
- package/schemas/read-state.schema.json +90 -0
- package/skill/asset-compounding/SKILL.md +65 -0
- package/templates/acceptance-report.md +23 -0
- package/templates/conflict-report.md +8 -0
|
@@ -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
|