@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,191 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
currentBranch,
|
|
5
|
+
inboundLinks,
|
|
6
|
+
isProtectedBranch,
|
|
7
|
+
isTopicSlug,
|
|
8
|
+
legacyIdentity,
|
|
9
|
+
parsePrefixedName,
|
|
10
|
+
relativePath,
|
|
11
|
+
type Json,
|
|
12
|
+
} from "./migration_common";
|
|
13
|
+
|
|
14
|
+
function regularFile(path: string): boolean {
|
|
15
|
+
try {
|
|
16
|
+
return statSync(path).isFile();
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function baseResult(
|
|
23
|
+
repoRoot: string,
|
|
24
|
+
source: string,
|
|
25
|
+
sourceRelative: string,
|
|
26
|
+
topic: string,
|
|
27
|
+
branch: string,
|
|
28
|
+
): Json {
|
|
29
|
+
const targetRoot = `docs/specs/${topic}`;
|
|
30
|
+
return {
|
|
31
|
+
source: sourceRelative,
|
|
32
|
+
topic,
|
|
33
|
+
target_root: targetRoot,
|
|
34
|
+
target_spec: `${targetRoot}/SPEC.md`,
|
|
35
|
+
branch,
|
|
36
|
+
write_allowed: !isProtectedBranch(branch),
|
|
37
|
+
blocked_reason: null,
|
|
38
|
+
inbound_links: inboundLinks(repoRoot, source, sourceRelative),
|
|
39
|
+
pending_delete_approval: [],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withTopic(result: Json, topic: string): Json {
|
|
44
|
+
if (isTopicSlug(topic)) return result;
|
|
45
|
+
result.candidate_topic = topic;
|
|
46
|
+
result.target_root = null;
|
|
47
|
+
result.target_spec = null;
|
|
48
|
+
result.required_topic_slug = "lowercase-kebab-case";
|
|
49
|
+
result.blocked_reason = "topic_slug_required";
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function planSpecsMigration(
|
|
54
|
+
repoRootInput: string,
|
|
55
|
+
sourceInput: string,
|
|
56
|
+
options: { branch?: string; legacyId?: string; topic?: string } = {},
|
|
57
|
+
): Json {
|
|
58
|
+
const repoRoot = resolve(repoRootInput);
|
|
59
|
+
const source = resolve(sourceInput);
|
|
60
|
+
const sourceRelative = relativePath(repoRoot, source);
|
|
61
|
+
if (
|
|
62
|
+
!regularFile(source) ||
|
|
63
|
+
!sourceRelative.startsWith("docs/plan/") ||
|
|
64
|
+
!source.endsWith("/PLAN.md")
|
|
65
|
+
)
|
|
66
|
+
throw new Error("source must be an existing docs/plan/**/PLAN.md file");
|
|
67
|
+
const branch = options.branch ?? currentBranch(repoRoot);
|
|
68
|
+
const identity = legacyIdentity(
|
|
69
|
+
repoRoot,
|
|
70
|
+
source.split("/").at(-2) ?? "",
|
|
71
|
+
"docs/plan/README.md",
|
|
72
|
+
sourceRelative,
|
|
73
|
+
options.legacyId,
|
|
74
|
+
);
|
|
75
|
+
const topic =
|
|
76
|
+
options.topic ?? identity?.[1] ?? source.split("/").at(-2) ?? "";
|
|
77
|
+
const result = baseResult(repoRoot, source, sourceRelative, topic, branch);
|
|
78
|
+
result.legacy_id = identity?.[0] ?? null;
|
|
79
|
+
result.legacy_id_provenance = identity?.[2] ?? "none";
|
|
80
|
+
withTopic(result, topic);
|
|
81
|
+
if (result.blocked_reason) return result;
|
|
82
|
+
if (isProtectedBranch(branch)) {
|
|
83
|
+
result.blocked_reason = "protected_branch_read_only";
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
if (existsSync(resolve(repoRoot, String(result.target_root)))) {
|
|
87
|
+
result.blocked_reason = "slug_conflict";
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
result.action = "create_slug_spec";
|
|
91
|
+
result.pending_delete_approval = [sourceRelative];
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function planSpecsHygiene(
|
|
96
|
+
repoRootInput: string,
|
|
97
|
+
legacyRootInput: string,
|
|
98
|
+
options: {
|
|
99
|
+
branch?: string;
|
|
100
|
+
renameConfirmed?: boolean;
|
|
101
|
+
legacyId?: string;
|
|
102
|
+
topic?: string;
|
|
103
|
+
} = {},
|
|
104
|
+
): Json {
|
|
105
|
+
const repoRoot = resolve(repoRootInput);
|
|
106
|
+
const legacyRoot = resolve(legacyRootInput);
|
|
107
|
+
const sourceRelative = relativePath(repoRoot, legacyRoot);
|
|
108
|
+
if (!directoryExists(legacyRoot) || !sourceRelative.startsWith("docs/specs/"))
|
|
109
|
+
throw new Error(
|
|
110
|
+
"source must be an existing docs/specs/<legacy-id>-<topic> directory",
|
|
111
|
+
);
|
|
112
|
+
const sourceSpecRelative = `${sourceRelative}/SPEC.md`;
|
|
113
|
+
const identity = legacyIdentity(
|
|
114
|
+
repoRoot,
|
|
115
|
+
legacyRoot.split("/").at(-1) ?? "",
|
|
116
|
+
"docs/specs/README.md",
|
|
117
|
+
sourceSpecRelative,
|
|
118
|
+
options.legacyId,
|
|
119
|
+
);
|
|
120
|
+
const topic =
|
|
121
|
+
options.topic ?? identity?.[1] ?? legacyRoot.split("/").at(-1) ?? "";
|
|
122
|
+
const branch = options.branch ?? currentBranch(repoRoot);
|
|
123
|
+
const result = baseResult(
|
|
124
|
+
repoRoot,
|
|
125
|
+
legacyRoot,
|
|
126
|
+
sourceRelative,
|
|
127
|
+
topic,
|
|
128
|
+
branch,
|
|
129
|
+
);
|
|
130
|
+
result.target_spec = `docs/specs/${topic}/SPEC.md`;
|
|
131
|
+
result.legacy_root = sourceRelative;
|
|
132
|
+
result.legacy_id = identity?.[0] ?? null;
|
|
133
|
+
result.legacy_id_provenance = identity?.[2] ?? "none";
|
|
134
|
+
if (!identity) {
|
|
135
|
+
result.blocked_reason = parsePrefixedName(
|
|
136
|
+
legacyRoot.split("/").at(-1) ?? "",
|
|
137
|
+
)
|
|
138
|
+
? "legacy_id_proof_required"
|
|
139
|
+
: "not_legacy_id_directory";
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
withTopic(result, topic);
|
|
143
|
+
if (result.blocked_reason) return result;
|
|
144
|
+
if (isProtectedBranch(branch)) {
|
|
145
|
+
result.blocked_reason = "protected_branch_read_only";
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
if (existsSync(resolve(repoRoot, String(result.target_root)))) {
|
|
149
|
+
result.blocked_reason = "slug_conflict";
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
const conflicting: string[] = [];
|
|
153
|
+
const specsRoot = resolve(repoRoot, "docs/specs");
|
|
154
|
+
if (directoryExists(specsRoot)) {
|
|
155
|
+
for (const entry of readdirSync(specsRoot, { withFileTypes: true })) {
|
|
156
|
+
if (
|
|
157
|
+
!entry.isDirectory() ||
|
|
158
|
+
entry.name === (legacyRoot.split("/").at(-1) ?? "")
|
|
159
|
+
)
|
|
160
|
+
continue;
|
|
161
|
+
const sibling = resolve(specsRoot, entry.name);
|
|
162
|
+
const siblingRelative = relativePath(repoRoot, sibling);
|
|
163
|
+
const siblingIdentity = legacyIdentity(
|
|
164
|
+
repoRoot,
|
|
165
|
+
entry.name,
|
|
166
|
+
"docs/specs/README.md",
|
|
167
|
+
`${siblingRelative}/SPEC.md`,
|
|
168
|
+
);
|
|
169
|
+
if (siblingIdentity?.[1] === topic) conflicting.push(siblingRelative);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (conflicting.length) {
|
|
173
|
+
result.blocked_reason = "duplicate_slug_mapping";
|
|
174
|
+
result.conflicting_legacy_roots = conflicting.sort();
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
if (!options.renameConfirmed) {
|
|
178
|
+
result.blocked_reason = "rename_confirmation_required";
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
result.action = "git_mv";
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function directoryExists(path: string): boolean {
|
|
186
|
+
try {
|
|
187
|
+
return statSync(path).isDirectory();
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { docs } from "./compatibility_docs";
|
|
3
|
+
|
|
4
|
+
export async function runCompatibilityCli(name: string, argv: string[] = process.argv.slice(2)): Promise<number> {
|
|
5
|
+
const smoke = argv.includes("--compatibility-smoke");
|
|
6
|
+
const help = argv.includes("--help") || argv.includes("-h");
|
|
7
|
+
if (help && !smoke) { console.log(`Usage: bun ${name}.ts [options]`); return 0; }
|
|
8
|
+
const result = docs(basename(name).replace(/\.ts$/, ""), argv.filter(value => value !== "--compatibility-smoke"));
|
|
9
|
+
if (smoke && (result === 0 || help)) console.log(`SMOKE dispatched=${basename(name).replace(/\.ts$/, "")}`);
|
|
10
|
+
if (help) { if (result === 0) return 0; console.log(`Usage: bun ${name}.ts [options]`); return 0; }
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { verifyDocLocks } from "./modules/docs/hardware_locks";
|
|
2
|
+
import { ingestExtract } from "./modules/docs/hardware_ingest";
|
|
3
|
+
import { findNextPlan } from "./modules/docs/next_plan";
|
|
4
|
+
import { generateLegacySpecIds } from "./modules/docs/legacy_spec_id";
|
|
5
|
+
import { parseSpecDriftArgs, runSpecDrift } from "./modules/docs/spec_drift";
|
|
6
|
+
import {
|
|
7
|
+
planSpecsHygiene,
|
|
8
|
+
planSpecsMigration,
|
|
9
|
+
} from "./modules/docs/spec_migration";
|
|
10
|
+
import { json, option, rootFor } from "./compatibility_shared";
|
|
11
|
+
|
|
12
|
+
export function docs(name: string, argv: string[]): number {
|
|
13
|
+
if (name === "verify_doc_locks") {
|
|
14
|
+
const result = verifyDocLocks(rootFor(argv), {
|
|
15
|
+
strictPdf: argv.includes("--strict-pdf"),
|
|
16
|
+
checkPerms: !argv.includes("--no-check-perms"),
|
|
17
|
+
});
|
|
18
|
+
result.lines.forEach((line) => console.log(line));
|
|
19
|
+
return result.exitCode;
|
|
20
|
+
}
|
|
21
|
+
if (name === "find_next_plan") {
|
|
22
|
+
json(findNextPlan(option(argv, "repo", process.cwd())!));
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
if (name === "gen_legacy_spec_id") return legacyId(argv);
|
|
26
|
+
if (name === "specs_migrate_guard") return migrateGuard(argv);
|
|
27
|
+
if (name === "spec_drift_check") return drift(argv);
|
|
28
|
+
if (name === "ingest_mineru_extract") return ingest(argv);
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function legacyId(argv: string[]): number {
|
|
33
|
+
const args: Parameters<typeof generateLegacySpecIds>[0] = {
|
|
34
|
+
repoRoot: option(argv, "repo", process.cwd())!,
|
|
35
|
+
legacyCompat: argv.includes("--legacy-compat"),
|
|
36
|
+
count: Number(option(argv, "count", "1")),
|
|
37
|
+
length: Number(option(argv, "length", "5")),
|
|
38
|
+
};
|
|
39
|
+
const alphabet = option(argv, "alphabet");
|
|
40
|
+
if (alphabet !== undefined) args.alphabet = alphabet;
|
|
41
|
+
const result = generateLegacySpecIds(args);
|
|
42
|
+
process.stdout.write(result.stdout);
|
|
43
|
+
process.stderr.write(result.stderr);
|
|
44
|
+
return result.exitCode;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function migrateGuard(argv: string[]): number {
|
|
48
|
+
const repo = option(argv, "repo", process.cwd())!;
|
|
49
|
+
const source = option(argv, "source")!;
|
|
50
|
+
const branch = option(argv, "branch");
|
|
51
|
+
const legacyId = option(argv, "legacy-id");
|
|
52
|
+
const topic = option(argv, "topic");
|
|
53
|
+
const options = {
|
|
54
|
+
...(branch === undefined ? {} : { branch }),
|
|
55
|
+
...(legacyId === undefined ? {} : { legacyId }),
|
|
56
|
+
...(topic === undefined ? {} : { topic }),
|
|
57
|
+
};
|
|
58
|
+
const result =
|
|
59
|
+
option(argv, "mode") === "specs-hygiene"
|
|
60
|
+
? planSpecsHygiene(repo, source, {
|
|
61
|
+
...options,
|
|
62
|
+
renameConfirmed: argv.includes("--rename-confirmed"),
|
|
63
|
+
})
|
|
64
|
+
: planSpecsMigration(repo, source, options);
|
|
65
|
+
json(result);
|
|
66
|
+
return result.blocked_reason ? 1 : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function drift(argv: string[]): number {
|
|
70
|
+
const parsed = parseSpecDriftArgs(argv);
|
|
71
|
+
if (parsed.error) {
|
|
72
|
+
console.error(parsed.error);
|
|
73
|
+
return 2;
|
|
74
|
+
}
|
|
75
|
+
if (parsed.help) {
|
|
76
|
+
console.log(
|
|
77
|
+
"Usage: spec_drift_check.ts [--base-ref <ref>] [--spec-path <path>]...",
|
|
78
|
+
);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
const result = runSpecDrift(parsed.options);
|
|
82
|
+
json(result);
|
|
83
|
+
return result.exitCode;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function ingest(argv: string[]): number {
|
|
87
|
+
try {
|
|
88
|
+
const args: Parameters<typeof ingestExtract>[0] = {
|
|
89
|
+
extractPath: option(argv, "extract-path")!,
|
|
90
|
+
destDir: option(argv, "dest-dir")!,
|
|
91
|
+
slug: option(argv, "slug")!,
|
|
92
|
+
overwrite: argv.includes("--overwrite"),
|
|
93
|
+
addHeader: argv.includes("--add-header"),
|
|
94
|
+
keepMineruJson: argv.includes("--keep-mineru-json"),
|
|
95
|
+
writeLock: !argv.includes("--no-write-lock"),
|
|
96
|
+
lockGenerated: !argv.includes("--writable-generated"),
|
|
97
|
+
createReadme: !argv.includes("--no-readme"),
|
|
98
|
+
};
|
|
99
|
+
for (const [key, flag] of [
|
|
100
|
+
["docSubdir", "doc-subdir"],
|
|
101
|
+
["sourceUrl", "source-url"],
|
|
102
|
+
["docType", "doc-type"],
|
|
103
|
+
["vendor", "vendor"],
|
|
104
|
+
["part", "part"],
|
|
105
|
+
] as const) {
|
|
106
|
+
const value = option(argv, flag);
|
|
107
|
+
if (value !== undefined) args[key] = value;
|
|
108
|
+
}
|
|
109
|
+
ingestExtract(args).lines.forEach((line) => console.log(line));
|
|
110
|
+
return 0;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
parseReplayCases,
|
|
5
|
+
resolveCasePath,
|
|
6
|
+
runReplayCases,
|
|
7
|
+
} from "./modules/checks/replay";
|
|
8
|
+
|
|
9
|
+
export type Analyzer = (path: string) => {
|
|
10
|
+
findings: Array<{ code: string; line: number; message: string }>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function option(
|
|
14
|
+
argv: string[],
|
|
15
|
+
name: string,
|
|
16
|
+
fallback?: string,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const token = argv[index];
|
|
20
|
+
if (token === `--${name}`) return argv[index + 1] ?? fallback;
|
|
21
|
+
if (token?.startsWith(`--${name}=`)) return token.slice(name.length + 3);
|
|
22
|
+
}
|
|
23
|
+
return fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function values(argv: string[], name: string): string[] {
|
|
27
|
+
const result: string[] = [];
|
|
28
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
29
|
+
const token = argv[index];
|
|
30
|
+
if (token === `--${name}` && argv[index + 1]) result.push(argv[++index]!);
|
|
31
|
+
else if (token?.startsWith(`--${name}=`))
|
|
32
|
+
result.push(token.slice(name.length + 3));
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function groups(argv: string[], name: string, size: number): string[][] {
|
|
38
|
+
const result: string[][] = [];
|
|
39
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
40
|
+
if (argv[index] !== `--${name}`) continue;
|
|
41
|
+
const group = argv.slice(index + 1, index + 1 + size);
|
|
42
|
+
if (group.length === size) result.push(group);
|
|
43
|
+
index += size;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function positional(argv: string[]): string[] {
|
|
49
|
+
const skip = new Set([
|
|
50
|
+
"--repo-root",
|
|
51
|
+
"--cases",
|
|
52
|
+
"--path",
|
|
53
|
+
"--base-sha",
|
|
54
|
+
"--output-dir",
|
|
55
|
+
"--repo",
|
|
56
|
+
"--remote",
|
|
57
|
+
"--number",
|
|
58
|
+
"--sha",
|
|
59
|
+
"--method",
|
|
60
|
+
"--expected-base",
|
|
61
|
+
"--expected-head",
|
|
62
|
+
"--branch",
|
|
63
|
+
"--title",
|
|
64
|
+
"--body-file",
|
|
65
|
+
"--head",
|
|
66
|
+
"--base",
|
|
67
|
+
"--run-id",
|
|
68
|
+
"--expected-sha",
|
|
69
|
+
"--tag",
|
|
70
|
+
"--note-file",
|
|
71
|
+
"--target",
|
|
72
|
+
"--event",
|
|
73
|
+
"--text-file",
|
|
74
|
+
"--context",
|
|
75
|
+
]);
|
|
76
|
+
const result: string[] = [];
|
|
77
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
78
|
+
const token = argv[index]!;
|
|
79
|
+
if (skip.has(token)) {
|
|
80
|
+
index += 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!token.startsWith("--")) result.push(token);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function replay(
|
|
89
|
+
argv: string[],
|
|
90
|
+
analyzer: Analyzer,
|
|
91
|
+
expectation: "required" | "exact" = "required",
|
|
92
|
+
): number {
|
|
93
|
+
const casePaths = values(argv, "cases");
|
|
94
|
+
const paths = positional(argv);
|
|
95
|
+
if (!casePaths.length && !paths.length) {
|
|
96
|
+
console.error("at least one replay path is required");
|
|
97
|
+
return 2;
|
|
98
|
+
}
|
|
99
|
+
let exitCode = 0;
|
|
100
|
+
const summary = argv.includes("--summary");
|
|
101
|
+
for (const casesPath of casePaths) {
|
|
102
|
+
const failures = replayCases(casesPath, analyzer, expectation, summary);
|
|
103
|
+
if (failures.length) {
|
|
104
|
+
exitCode = 1;
|
|
105
|
+
for (const failure of failures)
|
|
106
|
+
console.error(formatCaseMismatch(failure, summary));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const path of paths) {
|
|
110
|
+
try {
|
|
111
|
+
const caseFailures = path.endsWith(".txt")
|
|
112
|
+
? replayCases(path, analyzer, expectation, summary)
|
|
113
|
+
: undefined;
|
|
114
|
+
if (caseFailures !== undefined) {
|
|
115
|
+
if (caseFailures.length) {
|
|
116
|
+
exitCode = 1;
|
|
117
|
+
for (const failure of caseFailures)
|
|
118
|
+
console.error(formatCaseMismatch(failure, summary));
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const findings = analyzer(path).findings;
|
|
123
|
+
if (findings.length) {
|
|
124
|
+
exitCode = 1;
|
|
125
|
+
if (summary)
|
|
126
|
+
console.log(`SUMMARY file=${path} ${replaySummary(findings)}`);
|
|
127
|
+
else
|
|
128
|
+
findings.forEach((finding) =>
|
|
129
|
+
console.log(
|
|
130
|
+
`FINDING file=${path} code=${finding.code} line=${finding.line} msg=${finding.message}`,
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
console.log(
|
|
135
|
+
summary
|
|
136
|
+
? `SUMMARY file=${path} status=PASS total=0`
|
|
137
|
+
: `PASS file=${path}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
exitCode = 1;
|
|
142
|
+
console.error(
|
|
143
|
+
`FAIL file=${path} ${error instanceof Error ? error.message : String(error)}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return exitCode;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatCaseMismatch(
|
|
151
|
+
failure: ReturnType<typeof runReplayCases>[number],
|
|
152
|
+
summary: boolean,
|
|
153
|
+
): string {
|
|
154
|
+
const actual = failure.actualCodes.join(",") || "PASS";
|
|
155
|
+
if (summary) {
|
|
156
|
+
return failure.case.expectFail
|
|
157
|
+
? `ERROR file=${failure.path} expected finding ${failure.case.expectedCodes.join(",") || "any"} but saw ${actual}`
|
|
158
|
+
: `ERROR file=${failure.path} expected PASS but saw findings ${actual}`;
|
|
159
|
+
}
|
|
160
|
+
return `CASE_MISMATCH file=${failure.path} line=${failure.case.line} expected=${failure.case.expectedCodes.join(",") || (failure.case.expectFail ? "finding" : "pass")} actual=${failure.actualCodes.join(",") || "pass"}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function replayCases(
|
|
164
|
+
casesPath: string,
|
|
165
|
+
analyzer: Analyzer,
|
|
166
|
+
expectation: "required" | "exact",
|
|
167
|
+
summary: boolean,
|
|
168
|
+
): ReturnType<typeof runReplayCases> {
|
|
169
|
+
const failures: ReturnType<typeof runReplayCases> = [];
|
|
170
|
+
for (const item of parseReplayCases(casesPath)) {
|
|
171
|
+
const localPath = resolveCasePath(dirname(casesPath), item.path);
|
|
172
|
+
const path = existsSync(localPath)
|
|
173
|
+
? localPath
|
|
174
|
+
: resolveCasePath(process.cwd(), item.path);
|
|
175
|
+
const actual = analyzer(path).findings;
|
|
176
|
+
const actualCodes = actual.map((finding) => finding.code).sort();
|
|
177
|
+
const actualCodeSet = [...new Set(actualCodes)];
|
|
178
|
+
const expectedCodes = [...new Set(item.expectedCodes)].sort();
|
|
179
|
+
const matches = item.expectFail
|
|
180
|
+
? expectation === "exact"
|
|
181
|
+
? actualCodes.length === expectedCodes.length &&
|
|
182
|
+
actualCodes.every((code, index) => code === expectedCodes[index])
|
|
183
|
+
: expectedCodes.every((code) => actualCodeSet.includes(code))
|
|
184
|
+
: actualCodes.length === 0;
|
|
185
|
+
if (summary) console.log(`SUMMARY file=${path} ${replaySummary(actual)}`);
|
|
186
|
+
else if (actual.length) {
|
|
187
|
+
for (const finding of actual)
|
|
188
|
+
console.log(
|
|
189
|
+
`FINDING file=${path} code=${finding.code} line=${finding.line} msg=${finding.message}`,
|
|
190
|
+
);
|
|
191
|
+
} else console.log(`PASS file=${path}`);
|
|
192
|
+
if (!matches) failures.push({ case: item, actualCodes, path });
|
|
193
|
+
}
|
|
194
|
+
return failures;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function replaySummary(
|
|
198
|
+
findings: Array<{ code: string; line: number }>,
|
|
199
|
+
): string {
|
|
200
|
+
if (!findings.length) return "status=PASS total=0";
|
|
201
|
+
const counts = new Map<string, { count: number; line: number }>();
|
|
202
|
+
for (const finding of findings) {
|
|
203
|
+
const previous = counts.get(finding.code);
|
|
204
|
+
counts.set(finding.code, {
|
|
205
|
+
count: (previous?.count ?? 0) + 1,
|
|
206
|
+
line: previous?.line ?? finding.line,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
const details = [...counts.entries()]
|
|
210
|
+
.sort(
|
|
211
|
+
(left, right) =>
|
|
212
|
+
left[1].line - right[1].line || left[0].localeCompare(right[0]),
|
|
213
|
+
)
|
|
214
|
+
.map(([code, detail]) => `${code}:${detail.count}@${detail.line}`)
|
|
215
|
+
.join(",");
|
|
216
|
+
return `status=FINDINGS total=${findings.length} codes=${details}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function rootFor(argv: string[]): string {
|
|
220
|
+
return resolve(option(argv, "repo-root", process.cwd())!);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function printList(valuesToPrint: string[], prefix = "FINDING"): number {
|
|
224
|
+
valuesToPrint.forEach((value) => console.log(`${prefix} ${value}`));
|
|
225
|
+
return valuesToPrint.length ? 1 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function json(value: unknown): void {
|
|
229
|
+
console.log(JSON.stringify(value, null, 2));
|
|
230
|
+
}
|