@loomhelm/multi-task-flow 0.2.6 → 0.2.7
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 +3 -3
- package/package.json +1 -1
- package/toolkit/artifact-manifest.json +330 -26
- package/toolkit/package.json +1 -1
- package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_cli.ts +12 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_docs.ts +115 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/compatibility_shared.ts +230 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/checks/replay.ts +336 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/git.ts +54 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/migration_common.ts +291 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/next_plan.ts +199 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
- package/toolkit/skills/docs-migrate/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
- package/toolkit/skills/docs-migrate/scripts/gen_legacy_spec_id.ts +1 -1
- package/toolkit/skills/docs-migrate/scripts/specs_migrate_guard.ts +1 -1
- package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_cli.ts +12 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_docs.ts +115 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/compatibility_shared.ts +230 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/checks/replay.ts +336 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/git.ts +54 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/migration_common.ts +291 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/next_plan.ts +199 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
- package/toolkit/skills/find-next-plan/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
- package/toolkit/skills/find-next-plan/scripts/find_next_plan.ts +1 -1
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_cli.ts +12 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_docs.ts +115 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/compatibility_shared.ts +230 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/checks/replay.ts +336 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/git.ts +54 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/migration_common.ts +291 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/next_plan.ts +199 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
- package/toolkit/skills/hardware-doc-ingest/scripts/ingest_mineru_extract.ts +1 -1
- package/toolkit/skills/hardware-doc-ingest/scripts/verify_doc_locks.ts +1 -1
- package/toolkit/skills/multi-task-flow/dist/app.js +66 -66
- package/toolkit/skills/multi-task-flow/lib/view.ts +6 -2
- package/toolkit/skills/multi-task-flow/web/app.ts +2 -2
- package/toolkit/skills/multi-task-flow/web/graph-layout.ts +3 -3
- package/toolkit/skills/source-manifest.json +315 -11
- package/toolkit/skills/spec-sync/SKILL.md +1 -1
- package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_cli.ts +12 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_docs.ts +115 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/compatibility_shared.ts +230 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/checks/replay.ts +336 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/git.ts +54 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/hardware_ingest.ts +377 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/hardware_locks.ts +181 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/legacy_spec_id.ts +131 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/migration_common.ts +291 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/next_plan.ts +199 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/next_plan_support.ts +245 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_contract.ts +299 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift.ts +387 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_helpers.ts +381 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_legacy.ts +376 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_paths.ts +3 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_types.ts +27 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_drift_validation.ts +251 -0
- package/toolkit/skills/spec-sync/scripts/_runtime/modules/docs/spec_migration.ts +191 -0
- package/toolkit/skills/spec-sync/scripts/spec_drift_check.ts +1 -1
- package/toolkit/skills/support-manifest.json +77 -1
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type JsonValue =
|
|
5
|
+
string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
|
|
6
|
+
export type JsonRecord = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export interface Finding {
|
|
9
|
+
code: string;
|
|
10
|
+
line: number;
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ReplayResult {
|
|
15
|
+
findings: Finding[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ReplayCase {
|
|
19
|
+
path: string;
|
|
20
|
+
expectFail: boolean;
|
|
21
|
+
expectedCodes: string[];
|
|
22
|
+
line: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isRecord(value: unknown): value is JsonRecord {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectStrings(value: unknown): string[] {
|
|
30
|
+
if (typeof value === "string") return [value];
|
|
31
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
32
|
+
if (isRecord(value)) return Object.values(value).flatMap(collectStrings);
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const trackerCardFields = new Set([
|
|
37
|
+
"phase",
|
|
38
|
+
"tracking_phase",
|
|
39
|
+
"追踪阶段",
|
|
40
|
+
"tracking_target",
|
|
41
|
+
"追踪目标",
|
|
42
|
+
"bound_sha",
|
|
43
|
+
"checked_sha",
|
|
44
|
+
"追踪绑定sha",
|
|
45
|
+
"绑定sha",
|
|
46
|
+
"terminal_state",
|
|
47
|
+
"追踪终态",
|
|
48
|
+
"result",
|
|
49
|
+
"追踪结果",
|
|
50
|
+
"external_locator",
|
|
51
|
+
"外部定位",
|
|
52
|
+
]);
|
|
53
|
+
const routeResolutionFields = new Set([
|
|
54
|
+
"pre_merge_release_route",
|
|
55
|
+
"release_disposition",
|
|
56
|
+
"release_route",
|
|
57
|
+
"release_route_resolved",
|
|
58
|
+
"current_repository_facts",
|
|
59
|
+
"repository_facts",
|
|
60
|
+
"current_pr_facts",
|
|
61
|
+
"pr_facts",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
function pythonText(value: unknown): string {
|
|
65
|
+
if (value === null) return "None";
|
|
66
|
+
if (typeof value === "boolean") return value ? "True" : "False";
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function collectTrackerCardStrings(value: unknown): string[] {
|
|
71
|
+
if (Array.isArray(value)) return value.flatMap(collectTrackerCardStrings);
|
|
72
|
+
if (!isRecord(value)) return [];
|
|
73
|
+
const texts: string[] = [];
|
|
74
|
+
for (const [key, item] of Object.entries(value)) {
|
|
75
|
+
if (
|
|
76
|
+
trackerCardFields.has(key) &&
|
|
77
|
+
(typeof item === "string" ||
|
|
78
|
+
typeof item === "number" ||
|
|
79
|
+
typeof item === "boolean")
|
|
80
|
+
) {
|
|
81
|
+
texts.push(`${key}=${pythonText(item)}`);
|
|
82
|
+
} else if (routeResolutionFields.has(key)) {
|
|
83
|
+
if (Array.isArray(item) || isRecord(item)) {
|
|
84
|
+
texts.push(key);
|
|
85
|
+
texts.push(...collectTrackerCardStrings(item));
|
|
86
|
+
} else {
|
|
87
|
+
texts.push(`${key}=${pythonText(item)}`);
|
|
88
|
+
}
|
|
89
|
+
} else if (Array.isArray(item) || isRecord(item)) {
|
|
90
|
+
texts.push(...collectTrackerCardStrings(item));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return texts;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
type JsonScanResult = number | "duplicate" | undefined;
|
|
97
|
+
|
|
98
|
+
function skipJsonWhitespace(text: string, index: number): number {
|
|
99
|
+
while (index < text.length && /\s/.test(text[index]!)) index += 1;
|
|
100
|
+
return index;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function scanJsonString(
|
|
104
|
+
text: string,
|
|
105
|
+
start: number,
|
|
106
|
+
): { end: number; value: string } | undefined {
|
|
107
|
+
if (text[start] !== '"') return undefined;
|
|
108
|
+
let index = start + 1;
|
|
109
|
+
while (index < text.length) {
|
|
110
|
+
if (text[index] === "\\") {
|
|
111
|
+
index += 2;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (text[index] === '"') {
|
|
115
|
+
const raw = text.slice(start, index + 1);
|
|
116
|
+
try {
|
|
117
|
+
const value: unknown = JSON.parse(raw);
|
|
118
|
+
return typeof value === "string"
|
|
119
|
+
? { end: index + 1, value }
|
|
120
|
+
: undefined;
|
|
121
|
+
} catch {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
index += 1;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function scanJsonValue(text: string, start: number): JsonScanResult {
|
|
131
|
+
const index = skipJsonWhitespace(text, start);
|
|
132
|
+
if (text[index] === '"') return scanJsonString(text, index)?.end;
|
|
133
|
+
if (text[index] === "{") {
|
|
134
|
+
const keys = new Set<string>();
|
|
135
|
+
let cursor = skipJsonWhitespace(text, index + 1);
|
|
136
|
+
if (text[cursor] === "}") return cursor + 1;
|
|
137
|
+
while (cursor < text.length) {
|
|
138
|
+
const key = scanJsonString(text, cursor);
|
|
139
|
+
if (!key) return undefined;
|
|
140
|
+
if (keys.has(key.value)) return "duplicate";
|
|
141
|
+
keys.add(key.value);
|
|
142
|
+
cursor = skipJsonWhitespace(text, key.end);
|
|
143
|
+
if (text[cursor] !== ":") return undefined;
|
|
144
|
+
const value = scanJsonValue(text, cursor + 1);
|
|
145
|
+
if (value === "duplicate" || value === undefined) return value;
|
|
146
|
+
cursor = skipJsonWhitespace(text, value);
|
|
147
|
+
if (text[cursor] === "}") return cursor + 1;
|
|
148
|
+
if (text[cursor] !== ",") return undefined;
|
|
149
|
+
cursor = skipJsonWhitespace(text, cursor + 1);
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (text[index] === "[") {
|
|
154
|
+
let cursor = skipJsonWhitespace(text, index + 1);
|
|
155
|
+
if (text[cursor] === "]") return cursor + 1;
|
|
156
|
+
while (cursor < text.length) {
|
|
157
|
+
const value = scanJsonValue(text, cursor);
|
|
158
|
+
if (value === "duplicate" || value === undefined) return value;
|
|
159
|
+
cursor = skipJsonWhitespace(text, value);
|
|
160
|
+
if (text[cursor] === "]") return cursor + 1;
|
|
161
|
+
if (text[cursor] !== ",") return undefined;
|
|
162
|
+
cursor = skipJsonWhitespace(text, cursor + 1);
|
|
163
|
+
}
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
let cursor = index;
|
|
167
|
+
while (cursor < text.length && !/[\s,\]}]/.test(text[cursor]!)) cursor += 1;
|
|
168
|
+
return cursor > index ? cursor : undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hasDuplicateJsonKeys(text: string): boolean {
|
|
172
|
+
return scanJsonValue(text, 0) === "duplicate";
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseJsonArguments(raw: unknown): JsonRecord {
|
|
176
|
+
if (isRecord(raw)) return raw;
|
|
177
|
+
if (typeof raw !== "string") return {};
|
|
178
|
+
const parsed = parseJsonValue(raw);
|
|
179
|
+
return isRecord(parsed) ? parsed : {};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function parseJsonValue(raw: string): unknown | undefined {
|
|
183
|
+
if (hasDuplicateJsonKeys(raw)) return undefined;
|
|
184
|
+
try {
|
|
185
|
+
return JSON.parse(raw) as unknown;
|
|
186
|
+
} catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function eventTexts(record: JsonRecord): {
|
|
192
|
+
kind: string | undefined;
|
|
193
|
+
role: string | undefined;
|
|
194
|
+
texts: string[];
|
|
195
|
+
} {
|
|
196
|
+
const recordType = typeof record.type === "string" ? record.type : undefined;
|
|
197
|
+
const payload = isRecord(record.payload) ? record.payload : {};
|
|
198
|
+
if (recordType !== "response_item")
|
|
199
|
+
return {
|
|
200
|
+
kind: recordType,
|
|
201
|
+
role: undefined,
|
|
202
|
+
texts: collectStrings(payload),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const payloadType =
|
|
206
|
+
typeof payload.type === "string" ? payload.type : undefined;
|
|
207
|
+
if (payloadType === "message") {
|
|
208
|
+
const content = Array.isArray(payload.content) ? payload.content : [];
|
|
209
|
+
const texts = content.flatMap((item) => {
|
|
210
|
+
if (!isRecord(item) || typeof item.text !== "string") return [];
|
|
211
|
+
return [item.text];
|
|
212
|
+
});
|
|
213
|
+
return {
|
|
214
|
+
kind: "message",
|
|
215
|
+
role: typeof payload.role === "string" ? payload.role : undefined,
|
|
216
|
+
texts,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (payloadType === "function_call") {
|
|
220
|
+
const texts = [payload.name, payload.arguments].filter(
|
|
221
|
+
(item): item is string => typeof item === "string",
|
|
222
|
+
);
|
|
223
|
+
return { kind: "function_call", role: undefined, texts };
|
|
224
|
+
}
|
|
225
|
+
if (payloadType === "function_call_output")
|
|
226
|
+
return {
|
|
227
|
+
kind: "function_call_output",
|
|
228
|
+
role: undefined,
|
|
229
|
+
texts: [
|
|
230
|
+
...collectStrings(payload.output),
|
|
231
|
+
...(typeof payload.output === "object" && payload.output !== null
|
|
232
|
+
? [JSON.stringify(payload.output)]
|
|
233
|
+
: []),
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
return { kind: payloadType, role: undefined, texts: collectStrings(payload) };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export function eventText(record: JsonRecord): string {
|
|
240
|
+
return eventTexts(record).texts.join("\n");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function readJsonLines(
|
|
244
|
+
path: string,
|
|
245
|
+
): Array<{ line: number; value: JsonRecord }> {
|
|
246
|
+
const records: Array<{ line: number; value: JsonRecord }> = [];
|
|
247
|
+
for (const [index, raw] of readFileSync(path, "utf8")
|
|
248
|
+
.split(/\r?\n/)
|
|
249
|
+
.entries()) {
|
|
250
|
+
if (!raw.trim()) continue;
|
|
251
|
+
const parsed: unknown = JSON.parse(raw);
|
|
252
|
+
if (!isRecord(parsed))
|
|
253
|
+
throw new Error(`replay record at line ${index + 1} must be an object`);
|
|
254
|
+
records.push({ line: index + 1, value: parsed });
|
|
255
|
+
}
|
|
256
|
+
return records;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function parseReplayCases(path: string): ReplayCase[] {
|
|
260
|
+
const cases: ReplayCase[] = [];
|
|
261
|
+
for (const [index, raw] of readFileSync(path, "utf8")
|
|
262
|
+
.split(/\r?\n/)
|
|
263
|
+
.entries()) {
|
|
264
|
+
const line = raw.trim();
|
|
265
|
+
if (!line || line.startsWith("#")) continue;
|
|
266
|
+
const fields = line.split(/\s+/);
|
|
267
|
+
if (fields.length < 2) throw new Error(`invalid cases line ${index + 1}`);
|
|
268
|
+
if (fields[0] === "pass" || fields[0] === "fail") {
|
|
269
|
+
if (fields.length < 3)
|
|
270
|
+
throw new Error(`missing expectation at line ${index + 1}`);
|
|
271
|
+
cases.push({
|
|
272
|
+
path: fields[1]!,
|
|
273
|
+
expectFail: fields[0] === "fail",
|
|
274
|
+
expectedCodes: fields[2] === "-" ? [] : fields[2]!.split(","),
|
|
275
|
+
line: index + 1,
|
|
276
|
+
});
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
cases.push({
|
|
280
|
+
path: fields[0]!,
|
|
281
|
+
expectFail: fields[1] !== "pass",
|
|
282
|
+
expectedCodes: fields[1] === "pass" ? [] : fields[1]!.split(","),
|
|
283
|
+
line: index + 1,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return cases;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function resolveCasePath(baseDir: string, rawPath: string): string {
|
|
290
|
+
return isAbsolute(rawPath) ? rawPath : resolve(baseDir, rawPath);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type ReplayAnalyzer = (path: string) => ReplayResult;
|
|
294
|
+
|
|
295
|
+
export interface ReplayCaseFailure {
|
|
296
|
+
case: ReplayCase;
|
|
297
|
+
actualCodes: string[];
|
|
298
|
+
path: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export type ReplayExpectation = "required" | "exact";
|
|
302
|
+
|
|
303
|
+
export function runReplayCases(
|
|
304
|
+
casesPath: string,
|
|
305
|
+
analyzer: ReplayAnalyzer,
|
|
306
|
+
expectation: ReplayExpectation = "required",
|
|
307
|
+
): ReplayCaseFailure[] {
|
|
308
|
+
const failures: ReplayCaseFailure[] = [];
|
|
309
|
+
for (const item of parseReplayCases(casesPath)) {
|
|
310
|
+
const localPath = resolveCasePath(dirname(casesPath), item.path);
|
|
311
|
+
const path = existsSync(localPath)
|
|
312
|
+
? localPath
|
|
313
|
+
: resolveCasePath(process.cwd(), item.path);
|
|
314
|
+
const actualCodes = analyzer(path)
|
|
315
|
+
.findings.map((finding) => finding.code)
|
|
316
|
+
.sort();
|
|
317
|
+
const actualCodeSet = [...new Set(actualCodes)];
|
|
318
|
+
const expectedCodes = [...new Set(item.expectedCodes)].sort();
|
|
319
|
+
const equal = item.expectFail
|
|
320
|
+
? expectation === "exact"
|
|
321
|
+
? actualCodes.length === expectedCodes.length &&
|
|
322
|
+
actualCodes.every((code, index) => code === expectedCodes[index])
|
|
323
|
+
: expectedCodes.every((code) => actualCodeSet.includes(code))
|
|
324
|
+
: actualCodes.length === 0;
|
|
325
|
+
if (!equal) failures.push({ case: item, actualCodes, path });
|
|
326
|
+
}
|
|
327
|
+
return failures;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function result(findings: Finding[]): ReplayResult {
|
|
331
|
+
return { findings };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export function finding(code: string, line: number, message: string): Finding {
|
|
335
|
+
return { code, line, message };
|
|
336
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export interface GitCommandResult {
|
|
4
|
+
exitCode: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function text(value: unknown): string {
|
|
10
|
+
if (typeof value === "string") return value;
|
|
11
|
+
if (value instanceof Uint8Array) return new TextDecoder().decode(value);
|
|
12
|
+
return "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function runGit(repoRoot: string, args: string[]): GitCommandResult {
|
|
16
|
+
try {
|
|
17
|
+
const stdout = execFileSync("git", ["-C", repoRoot, ...args], {
|
|
18
|
+
encoding: "utf8",
|
|
19
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
20
|
+
});
|
|
21
|
+
return { exitCode: 0, stdout: text(stdout), stderr: "" };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const failure = error as {
|
|
24
|
+
status?: number;
|
|
25
|
+
stdout?: unknown;
|
|
26
|
+
stderr?: unknown;
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
exitCode: failure.status ?? 1,
|
|
30
|
+
stdout: text(failure.stdout),
|
|
31
|
+
stderr: text(failure.stderr),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function gitText(repoRoot: string, args: string[]): string | null {
|
|
37
|
+
const result = runGit(repoRoot, args);
|
|
38
|
+
return result.exitCode === 0 ? result.stdout : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function gitRefExists(repoRoot: string, ref: string): boolean {
|
|
42
|
+
return runGit(repoRoot, ["rev-parse", "--verify", ref]).exitCode === 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function gitFileText(
|
|
46
|
+
repoRoot: string,
|
|
47
|
+
ref: string,
|
|
48
|
+
path: string,
|
|
49
|
+
): string | null {
|
|
50
|
+
return gitText(repoRoot, [
|
|
51
|
+
"show",
|
|
52
|
+
ref === ":" ? `:${path}` : `${ref}:${path}`,
|
|
53
|
+
]);
|
|
54
|
+
}
|