@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
package/toolkit/skills/hardware-doc-ingest/scripts/_runtime/modules/docs/spec_drift_helpers.ts
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
3
|
+
import { gitText, runGit } from "./git";
|
|
4
|
+
import { checkSpec, type SpecIssue } from "./spec_contract";
|
|
5
|
+
import {
|
|
6
|
+
currentHash,
|
|
7
|
+
isImplementationPath,
|
|
8
|
+
parseNameStatus,
|
|
9
|
+
pathCountsAsSpecSync,
|
|
10
|
+
relatedAdrPaths,
|
|
11
|
+
type DriftPathContext,
|
|
12
|
+
type RenamePair,
|
|
13
|
+
} from "./spec_drift_paths";
|
|
14
|
+
import type { DriftResult } from "./spec_drift_types";
|
|
15
|
+
|
|
16
|
+
export interface ChangeSet {
|
|
17
|
+
changed: string[];
|
|
18
|
+
worktreeChanged: string[];
|
|
19
|
+
deleted: string[];
|
|
20
|
+
renames: RenamePair[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function unique(values: string[]): string[] {
|
|
24
|
+
return [...new Set(values.filter(Boolean))];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function result(
|
|
28
|
+
exitCode: 0 | 2 | 3,
|
|
29
|
+
status: DriftResult["status"],
|
|
30
|
+
drift: DriftResult["drift"],
|
|
31
|
+
evidence: string,
|
|
32
|
+
reason: string,
|
|
33
|
+
): DriftResult {
|
|
34
|
+
return {
|
|
35
|
+
exitCode,
|
|
36
|
+
status,
|
|
37
|
+
drift,
|
|
38
|
+
evidence,
|
|
39
|
+
reason,
|
|
40
|
+
stdout:
|
|
41
|
+
[
|
|
42
|
+
`Spec同步状态=${status}`,
|
|
43
|
+
`Spec漂移=${drift}`,
|
|
44
|
+
`Spec同步证据=${evidence}`,
|
|
45
|
+
`结束原因=${reason}`,
|
|
46
|
+
].join("\n") + "\n",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function commandLines(output: string | null): string[] {
|
|
51
|
+
return output?.split(/\r?\n/).filter(Boolean) ?? [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function addStatus(change: ChangeSet, line: string, worktree: boolean): void {
|
|
55
|
+
const parsed = parseNameStatus(line);
|
|
56
|
+
if (!parsed) return;
|
|
57
|
+
const paths = parsed.path2 ? [parsed.path1, parsed.path2] : [parsed.path1];
|
|
58
|
+
change.changed.push(...paths);
|
|
59
|
+
if (worktree) change.worktreeChanged.push(...paths);
|
|
60
|
+
if (parsed.status.startsWith("R")) {
|
|
61
|
+
change.deleted.push(parsed.path1);
|
|
62
|
+
change.renames.push({
|
|
63
|
+
oldPath: parsed.path1,
|
|
64
|
+
newPath: parsed.path2 as string,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (parsed.status.startsWith("D")) change.deleted.push(parsed.path1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function collectChanges(repoRoot: string, baseRef: string): ChangeSet {
|
|
71
|
+
const change: ChangeSet = {
|
|
72
|
+
changed: [],
|
|
73
|
+
worktreeChanged: [],
|
|
74
|
+
deleted: [],
|
|
75
|
+
renames: [],
|
|
76
|
+
};
|
|
77
|
+
for (const line of commandLines(
|
|
78
|
+
gitText(repoRoot, [
|
|
79
|
+
"-c",
|
|
80
|
+
"core.quotepath=false",
|
|
81
|
+
"diff",
|
|
82
|
+
"--name-status",
|
|
83
|
+
"-M",
|
|
84
|
+
`${baseRef}...HEAD`,
|
|
85
|
+
]),
|
|
86
|
+
))
|
|
87
|
+
addStatus(change, line, false);
|
|
88
|
+
for (const args of [
|
|
89
|
+
["-c", "core.quotepath=false", "diff", "--name-status", "-M"],
|
|
90
|
+
["-c", "core.quotepath=false", "diff", "--name-status", "-M", "--cached"],
|
|
91
|
+
])
|
|
92
|
+
for (const line of commandLines(gitText(repoRoot, args)))
|
|
93
|
+
addStatus(change, line, true);
|
|
94
|
+
for (const path of commandLines(
|
|
95
|
+
gitText(repoRoot, ["ls-files", "--others", "--exclude-standard"]),
|
|
96
|
+
)) {
|
|
97
|
+
if (
|
|
98
|
+
isImplementationPath(path) ||
|
|
99
|
+
path.startsWith("docs/specs/") ||
|
|
100
|
+
path.startsWith("docs/plan/")
|
|
101
|
+
) {
|
|
102
|
+
change.changed.push(path);
|
|
103
|
+
change.worktreeChanged.push(path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
change.changed = unique(change.changed);
|
|
107
|
+
change.worktreeChanged = unique(change.worktreeChanged);
|
|
108
|
+
change.deleted = unique(change.deleted);
|
|
109
|
+
return change;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function currentBranch(repoRoot: string): string {
|
|
113
|
+
return (
|
|
114
|
+
gitText(repoRoot, ["rev-parse", "--abbrev-ref", "HEAD"])?.trim() ?? "HEAD"
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolveBase(
|
|
119
|
+
repoRoot: string,
|
|
120
|
+
requested?: string,
|
|
121
|
+
): string | null {
|
|
122
|
+
if (requested)
|
|
123
|
+
return runGit(repoRoot, ["rev-parse", "--verify", requested]).exitCode === 0
|
|
124
|
+
? requested
|
|
125
|
+
: null;
|
|
126
|
+
const upstream = gitText(repoRoot, [
|
|
127
|
+
"rev-parse",
|
|
128
|
+
"--abbrev-ref",
|
|
129
|
+
"--symbolic-full-name",
|
|
130
|
+
"@{upstream}",
|
|
131
|
+
])?.trim();
|
|
132
|
+
const branch = currentBranch(repoRoot);
|
|
133
|
+
const remotes = commandLines(gitText(repoRoot, ["remote"]));
|
|
134
|
+
const remoteHeads = remotes
|
|
135
|
+
.map(
|
|
136
|
+
(remote) =>
|
|
137
|
+
gitText(repoRoot, [
|
|
138
|
+
"symbolic-ref",
|
|
139
|
+
"--quiet",
|
|
140
|
+
"--short",
|
|
141
|
+
`refs/remotes/${remote}/HEAD`,
|
|
142
|
+
])?.trim() ??
|
|
143
|
+
["main", "master", "trunk", "develop"]
|
|
144
|
+
.map((candidate) => `${remote}/${candidate}`)
|
|
145
|
+
.find(
|
|
146
|
+
(ref) =>
|
|
147
|
+
runGit(repoRoot, ["rev-parse", "--verify", `refs/remotes/${ref}`])
|
|
148
|
+
.exitCode === 0,
|
|
149
|
+
) ??
|
|
150
|
+
"",
|
|
151
|
+
)
|
|
152
|
+
.filter(Boolean);
|
|
153
|
+
const remoteHead = remoteHeads[0];
|
|
154
|
+
if (upstream && remoteHead && upstream === remoteHead) return upstream;
|
|
155
|
+
if (
|
|
156
|
+
upstream &&
|
|
157
|
+
/^(main|master|trunk|develop|release\/|hotfix\/|support\/)/.test(
|
|
158
|
+
upstream.split("/").slice(1).join("/"),
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
return upstream;
|
|
162
|
+
if (remoteHead && branch === remoteHead.split("/").slice(1).join("/"))
|
|
163
|
+
return remoteHead;
|
|
164
|
+
if (remoteHead) return remoteHead;
|
|
165
|
+
for (const candidate of ["main", "master", "trunk", "develop"])
|
|
166
|
+
if (runGit(repoRoot, ["rev-parse", "--verify", candidate]).exitCode === 0)
|
|
167
|
+
return candidate;
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function evidenceEntry(repoRoot: string, path: string): string {
|
|
172
|
+
return `${path}@${currentHash({ repoRoot, baseRef: "" } as DriftPathContext, path)}`;
|
|
173
|
+
}
|
|
174
|
+
export function evidence(repoRoot: string, paths: string[]): string {
|
|
175
|
+
return unique(paths)
|
|
176
|
+
.map((path) => evidenceEntry(repoRoot, path))
|
|
177
|
+
.join(",");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function normalizeRepoPath(
|
|
181
|
+
repoRoot: string,
|
|
182
|
+
path: string,
|
|
183
|
+
): string | null {
|
|
184
|
+
const candidate = path.replaceAll("\\", "/");
|
|
185
|
+
const absolute = resolve(repoRoot, candidate);
|
|
186
|
+
const relativePath = relative(resolve(repoRoot), absolute).replaceAll(
|
|
187
|
+
"\\",
|
|
188
|
+
"/",
|
|
189
|
+
);
|
|
190
|
+
if (relativePath === "" || relativePath === ".") return ".";
|
|
191
|
+
return relativePath === ".." || relativePath.startsWith("../")
|
|
192
|
+
? null
|
|
193
|
+
: relativePath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function specIssueText(issues: SpecIssue[]): string {
|
|
197
|
+
return issues
|
|
198
|
+
.map(
|
|
199
|
+
(issue) =>
|
|
200
|
+
`path=${issue.path} code=${issue.code} msg=${issue.message}${issue.line === undefined ? "" : ` line=${issue.line}`}`,
|
|
201
|
+
)
|
|
202
|
+
.join("; ");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function stagedText(repoRoot: string, path: string): string | null {
|
|
206
|
+
return gitText(repoRoot, ["show", `:${path}`]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isRegular(path: string): boolean {
|
|
210
|
+
try {
|
|
211
|
+
return lstatSync(path).isFile();
|
|
212
|
+
} catch {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function frozenAdrCheck(
|
|
218
|
+
context: DriftPathContext,
|
|
219
|
+
specs: string[],
|
|
220
|
+
raw: string | undefined,
|
|
221
|
+
): string | null {
|
|
222
|
+
if (raw === undefined || raw === "") return null;
|
|
223
|
+
if (
|
|
224
|
+
raw !== "none" &&
|
|
225
|
+
(raw.startsWith(",") || raw.endsWith(",") || raw.includes(",,"))
|
|
226
|
+
)
|
|
227
|
+
return "invalid frozen ADR refs: empty entry";
|
|
228
|
+
const frozen = raw === "none" ? [] : raw.split(",");
|
|
229
|
+
const paths: string[] = [];
|
|
230
|
+
let previous = "";
|
|
231
|
+
for (const entry of frozen) {
|
|
232
|
+
const match =
|
|
233
|
+
/^(docs\/adr\/[0-9]{4}-[a-z0-9]+(?:-[a-z0-9]+)*\.md)@([0-9a-f]{40})$/.exec(
|
|
234
|
+
entry,
|
|
235
|
+
);
|
|
236
|
+
if (!match?.[1] || !match[2]) return "invalid frozen ADR ref";
|
|
237
|
+
if (previous && match[1] <= previous)
|
|
238
|
+
return "invalid frozen ADR refs: paths must be unique and sorted";
|
|
239
|
+
previous = match[1];
|
|
240
|
+
paths.push(match[1]);
|
|
241
|
+
const path = resolve(context.repoRoot, match[1]);
|
|
242
|
+
if (!isRegular(path))
|
|
243
|
+
return `frozen ADR ref is not a regular candidate file: ${match[1]}`;
|
|
244
|
+
const index =
|
|
245
|
+
gitText(context.repoRoot, ["ls-files", "-s", "--", match[1]]) ?? "";
|
|
246
|
+
const indexSha = /^(?:100644|100755)\s+([0-9a-f]{40})\s+0\s/.exec(
|
|
247
|
+
index,
|
|
248
|
+
)?.[1];
|
|
249
|
+
if (!indexSha)
|
|
250
|
+
return `frozen ADR ref is not a regular index file: ${match[1]}`;
|
|
251
|
+
if (indexSha !== match[2])
|
|
252
|
+
return `frozen ADR ref drifted in index: ${match[1]}`;
|
|
253
|
+
if (currentHash(context, match[1]) !== match[2])
|
|
254
|
+
return `frozen ADR ref drifted: ${match[1]}`;
|
|
255
|
+
}
|
|
256
|
+
const candidateMembership = new Set<string>();
|
|
257
|
+
const indexMembership = new Set<string>();
|
|
258
|
+
for (const spec of specs) {
|
|
259
|
+
const current = resolve(context.repoRoot, spec);
|
|
260
|
+
if (!isRegular(current))
|
|
261
|
+
return `frozen ADR membership has no regular candidate Spec: ${spec}`;
|
|
262
|
+
const currentText = readFileSync(current, "utf8");
|
|
263
|
+
for (const path of relatedAdrPaths(currentText, false, false).paths)
|
|
264
|
+
candidateMembership.add(path);
|
|
265
|
+
const currentIndex =
|
|
266
|
+
gitText(context.repoRoot, ["ls-files", "-s", "--", spec]) ?? "";
|
|
267
|
+
if (
|
|
268
|
+
currentIndex &&
|
|
269
|
+
!/^(?:100644|100755)\s+[0-9a-f]{40}\s+0\s/.test(currentIndex)
|
|
270
|
+
)
|
|
271
|
+
return `frozen ADR membership has no regular index Spec: ${spec}`;
|
|
272
|
+
const staged = stagedText(context.repoRoot, spec);
|
|
273
|
+
if (currentIndex && staged === null)
|
|
274
|
+
return `frozen ADR membership cannot read index Spec: ${spec}`;
|
|
275
|
+
for (const path of relatedAdrPaths(staged ?? "", false, false).paths)
|
|
276
|
+
indexMembership.add(path);
|
|
277
|
+
}
|
|
278
|
+
if ([...candidateMembership].sort().join("\n") !== paths.join("\n"))
|
|
279
|
+
return "frozen ADR membership drifted across candidate Specs";
|
|
280
|
+
if ([...indexMembership].sort().join("\n") !== paths.join("\n"))
|
|
281
|
+
return "frozen ADR membership drifted in index across candidate Specs";
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function relatedAdrTargets(
|
|
286
|
+
repoRoot: string,
|
|
287
|
+
changed: string[],
|
|
288
|
+
): string[] {
|
|
289
|
+
const targets: string[] = [];
|
|
290
|
+
function visit(directory: string): void {
|
|
291
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
292
|
+
const path = resolve(directory, entry.name);
|
|
293
|
+
if (entry.isDirectory()) {
|
|
294
|
+
if (!entry.isSymbolicLink()) visit(path);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
!entry.isFile() ||
|
|
299
|
+
entry.name !== "SPEC.md" ||
|
|
300
|
+
!path.includes("/docs/specs/")
|
|
301
|
+
)
|
|
302
|
+
continue;
|
|
303
|
+
const text = readFileSync(path, "utf8");
|
|
304
|
+
for (const adr of changed)
|
|
305
|
+
if (text.includes(`../../${adr.slice("docs/".length)}`))
|
|
306
|
+
targets.push(path.slice(repoRoot.length + 1));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const root = resolve(repoRoot, "docs/specs");
|
|
310
|
+
if (existsSync(root)) visit(root);
|
|
311
|
+
return unique(targets);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function strictCheck(repoRoot: string, spec: string): string | null {
|
|
315
|
+
const issues = checkSpec(resolve(repoRoot, spec), repoRoot);
|
|
316
|
+
return issues.length ? specIssueText(issues) : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function commitCoverage(
|
|
320
|
+
context: DriftPathContext,
|
|
321
|
+
specs: string[],
|
|
322
|
+
worktreeSpec: Set<string>,
|
|
323
|
+
): string | null {
|
|
324
|
+
const commits = commandLines(
|
|
325
|
+
gitText(context.repoRoot, [
|
|
326
|
+
"rev-list",
|
|
327
|
+
"--topo-order",
|
|
328
|
+
`${context.baseRef}..HEAD`,
|
|
329
|
+
]),
|
|
330
|
+
);
|
|
331
|
+
if (!commits.length) return null;
|
|
332
|
+
const implCommits: string[] = [];
|
|
333
|
+
const specCommits = new Map<string, string[]>();
|
|
334
|
+
for (const spec of specs) specCommits.set(spec, []);
|
|
335
|
+
for (const commit of commits) {
|
|
336
|
+
const files = commandLines(
|
|
337
|
+
gitText(context.repoRoot, [
|
|
338
|
+
"show",
|
|
339
|
+
"--pretty=format:",
|
|
340
|
+
"--name-status",
|
|
341
|
+
"--first-parent",
|
|
342
|
+
commit,
|
|
343
|
+
]),
|
|
344
|
+
);
|
|
345
|
+
let implementation = false;
|
|
346
|
+
for (const line of files) {
|
|
347
|
+
const parsed = parseNameStatus(line);
|
|
348
|
+
if (!parsed) continue;
|
|
349
|
+
const paths = parsed.path2
|
|
350
|
+
? [parsed.path1, parsed.path2]
|
|
351
|
+
: [parsed.path1];
|
|
352
|
+
if (paths.some(isImplementationPath)) implementation = true;
|
|
353
|
+
for (const [index, spec] of specs.entries())
|
|
354
|
+
if (
|
|
355
|
+
paths.some((path) => pathCountsAsSpecSync(path, dirname(spec), spec))
|
|
356
|
+
)
|
|
357
|
+
specCommits.get(spec)?.push(commit);
|
|
358
|
+
}
|
|
359
|
+
if (implementation) implCommits.push(commit);
|
|
360
|
+
}
|
|
361
|
+
for (const spec of specs) {
|
|
362
|
+
if (worktreeSpec.has(spec)) continue;
|
|
363
|
+
const associated = specCommits.get(spec) ?? [];
|
|
364
|
+
if (implCommits.length && associated.length === 0)
|
|
365
|
+
return `no associated spec commit found after implementation commits (${spec})`;
|
|
366
|
+
for (const implementation of implCommits) {
|
|
367
|
+
const covered = associated.some(
|
|
368
|
+
(specCommit) =>
|
|
369
|
+
runGit(context.repoRoot, [
|
|
370
|
+
"merge-base",
|
|
371
|
+
"--is-ancestor",
|
|
372
|
+
implementation,
|
|
373
|
+
specCommit,
|
|
374
|
+
]).exitCode === 0,
|
|
375
|
+
);
|
|
376
|
+
if (!covered)
|
|
377
|
+
return `implementation changes are newer than associated spec commit (${spec})`;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|