artifact-graph 0.9.3 → 0.10.0
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/CHANGELOG.md +28 -0
- package/dist/cli.js +1071 -75
- package/dist/index.cjs +351 -60
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +331 -41
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2649,11 +2649,215 @@ var init_versioned_traceability = __esm({
|
|
|
2649
2649
|
}
|
|
2650
2650
|
});
|
|
2651
2651
|
|
|
2652
|
+
// src/native-binding-diagnostics.ts
|
|
2653
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
2654
|
+
import { createRequire } from "module";
|
|
2655
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
2656
|
+
import { fileURLToPath } from "url";
|
|
2657
|
+
function parsePnpmVersion(stdout) {
|
|
2658
|
+
const trimmed = stdout.trim();
|
|
2659
|
+
const segments = trimmed.split(".");
|
|
2660
|
+
if (!/^\d+$/.test(segments[0] ?? "")) return void 0;
|
|
2661
|
+
const major = Number(segments[0]);
|
|
2662
|
+
const minorSegment = segments.length > 1 ? segments[1] : void 0;
|
|
2663
|
+
const minor = minorSegment !== void 0 && /^\d+$/.test(minorSegment) ? Number(minorSegment) : void 0;
|
|
2664
|
+
return minor === void 0 ? { major } : { major, minor };
|
|
2665
|
+
}
|
|
2666
|
+
function pnpmUsesAllowBuilds(version) {
|
|
2667
|
+
if (!version) return false;
|
|
2668
|
+
return version.major > 10 || version.major === 10 && (version.minor ?? 0) >= 26;
|
|
2669
|
+
}
|
|
2670
|
+
function createInstallBoundBindingLoader(installBase) {
|
|
2671
|
+
return () => createRequire(installBase)("better-sqlite3");
|
|
2672
|
+
}
|
|
2673
|
+
function defaultBindingLoaderImpl() {
|
|
2674
|
+
const base = typeof __filename === "string" ? __filename : import.meta.url;
|
|
2675
|
+
return createRequire(base)("better-sqlite3");
|
|
2676
|
+
}
|
|
2677
|
+
function defaultBindingLoader() {
|
|
2678
|
+
return defaultBindingLoaderImpl();
|
|
2679
|
+
}
|
|
2680
|
+
function classifyNativeBindingError(error, context = {}) {
|
|
2681
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2682
|
+
const code = error?.code;
|
|
2683
|
+
if (/was compiled against a different Node\.js version|NODE_MODULE_VERSION|different ABI|Incompatible ABI/i.test(
|
|
2684
|
+
message
|
|
2685
|
+
)) {
|
|
2686
|
+
return { cause: "ABI_MISMATCH", detail: message };
|
|
2687
|
+
}
|
|
2688
|
+
if (code === "MODULE_NOT_FOUND" || /Cannot find module|Cannot find package/i.test(message)) {
|
|
2689
|
+
if (context.packagePresent) {
|
|
2690
|
+
return {
|
|
2691
|
+
cause: "BUILD_DISABLED",
|
|
2692
|
+
detail: `${message}\uFF08better-sqlite3 \u5305\u76EE\u5F55\u5B58\u5728\u4F46\u539F\u751F\u4EA7\u7269\u7F3A\u5931\uFF0C\u7B26\u5408 lifecycle scripts \u88AB\u7981\u7684\u7279\u5F81\uFF09`
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
return { cause: "MISSING", detail: message };
|
|
2696
|
+
}
|
|
2697
|
+
if (/Could not locate the bindings file/i.test(message)) {
|
|
2698
|
+
return {
|
|
2699
|
+
cause: "BUILD_DISABLED",
|
|
2700
|
+
detail: `${message}\uFF08better-sqlite3 \u5305\u5B58\u5728\u4F46\u539F\u751F .node \u4EA7\u7269\u672A\u6784\u5EFA\uFF0C\u7B26\u5408 lifecycle scripts \u88AB\u7981\u6216\u672A\u6784\u5EFA\u7684\u7279\u5F81\uFF09`
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
if (/invalid ELF|broken binary|failed to load native/i.test(message)) {
|
|
2704
|
+
return { cause: "LOAD_ERROR", detail: message };
|
|
2705
|
+
}
|
|
2706
|
+
return { cause: "LOAD_ERROR", detail: message };
|
|
2707
|
+
}
|
|
2708
|
+
function buildNativeBindingSuggestion(cause, pnpmVersion) {
|
|
2709
|
+
const allowlist = pnpmUsesAllowBuilds(pnpmVersion) ? `pnpm 10.26+ \u5728\u9879\u76EE\u6839 pnpm-workspace.yaml \u6DFB\u52A0 allowBuilds \u952E\uFF08${PNPM_ALLOW_BUILDS_SNIPPET.replace(/\n/g, "\uFF1B")}\uFF09` : `pnpm 10.0\u201310.25 \u5728 package.json \u914D\u7F6E pnpm.onlyBuiltDependencies: ["better-sqlite3"]\uFF08pnpm 10.26+ \u6539\u7528\u9879\u76EE\u6839 pnpm-workspace.yaml \u7684 allowBuilds \u952E\uFF09`;
|
|
2710
|
+
switch (cause) {
|
|
2711
|
+
case "MISSING":
|
|
2712
|
+
return `better-sqlite3 \u539F\u751F\u4EA7\u7269\u7F3A\u5931\u3002\u6309 INSTALL.md \u58F0\u660E\u6784\u5EFA\u5141\u8BB8\u6E05\u5355\u540E\u91CD\u65B0\u5B89\u88C5\uFF1A${allowlist}\uFF0C\u7136\u540E pnpm install\u3002`;
|
|
2713
|
+
case "ABI_MISMATCH":
|
|
2714
|
+
return `\u5F53\u524D Node ABI \u4E0E better-sqlite3 prebuilt \u76EE\u6807\u4E0D\u4E00\u81F4\uFF08\u5F53\u524D process.versions.modules=${process.versions.modules}\uFF09\u3002\u5207\u6362\u5230\u4E0E\u5DF2\u5B89\u88C5 prebuilt \u5339\u914D\u7684 Node \u7248\u672C\uFF0C\u6216\u5BF9\u5F53\u524D Node \u91CD\u5EFA\uFF1Apnpm rebuild better-sqlite3\u3002\u4E0D\u8981\u624B\u5DE5\u7F16\u8BD1\u3002`;
|
|
2715
|
+
case "BUILD_DISABLED":
|
|
2716
|
+
return `pnpm lifecycle scripts \u88AB\u7981\u5BFC\u81F4 better-sqlite3 \u672A\u6784\u5EFA\u3002${allowlist}\uFF0C\u7136\u540E pnpm install \u91CD\u5EFA\u539F\u751F\u4EA7\u7269\u3002\u4E0D\u8981\u624B\u5DE5\u7F16\u8BD1\u3002`;
|
|
2717
|
+
default:
|
|
2718
|
+
return "better-sqlite3 \u52A0\u8F7D\u5931\u8D25\uFF08\u672A\u77E5\u539F\u751F\u9519\u8BEF\uFF09\u3002\u5148\u8FD0\u884C artifact-graph doctor --format json \u67E5\u770B\u63A2\u9488\u8BE6\u60C5\uFF0C\u518D\u6309 INSTALL.md \u91CD\u5EFA\u5F53\u524D\u9009\u4E2D\u8FD0\u884C\u65F6\u7684\u539F\u751F\u4EA7\u7269\u3002";
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
function probeNativeBinding(context = {}) {
|
|
2722
|
+
const loader = context.loader ?? defaultBindingLoader;
|
|
2723
|
+
const presenceRoot = context.installRoot ?? context.root;
|
|
2724
|
+
const packagePresent = presenceRoot ? betterSqlite3PackagePresent(presenceRoot) : void 0;
|
|
2725
|
+
const classify = (error) => classifyNativeBindingError(error, { packagePresent });
|
|
2726
|
+
const fail = (failedStage, error) => {
|
|
2727
|
+
const { cause, detail } = classify(error);
|
|
2728
|
+
return { ok: false, failedStage, cause, detail, suggestion: buildNativeBindingSuggestion(cause, context.pnpmVersion) };
|
|
2729
|
+
};
|
|
2730
|
+
let DatabaseCtor;
|
|
2731
|
+
try {
|
|
2732
|
+
DatabaseCtor = loader();
|
|
2733
|
+
} catch (error) {
|
|
2734
|
+
return fail("load", error);
|
|
2735
|
+
}
|
|
2736
|
+
const Database = DatabaseCtor;
|
|
2737
|
+
let db;
|
|
2738
|
+
try {
|
|
2739
|
+
db = new Database(":memory:");
|
|
2740
|
+
} catch (error) {
|
|
2741
|
+
return fail("open", error);
|
|
2742
|
+
}
|
|
2743
|
+
try {
|
|
2744
|
+
db.exec("CREATE TABLE probe (id INTEGER PRIMARY KEY, value TEXT)");
|
|
2745
|
+
db.prepare("INSERT INTO probe (id, value) VALUES (?, ?)").run(1, "ok");
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
safeClose(db);
|
|
2748
|
+
return fail("write", error);
|
|
2749
|
+
}
|
|
2750
|
+
try {
|
|
2751
|
+
const row = db.prepare("SELECT value FROM probe WHERE id = ?").get(1);
|
|
2752
|
+
if (!row || row.value !== "ok") {
|
|
2753
|
+
safeClose(db);
|
|
2754
|
+
return {
|
|
2755
|
+
ok: false,
|
|
2756
|
+
failedStage: "read",
|
|
2757
|
+
cause: "LOAD_ERROR",
|
|
2758
|
+
detail: "probe \u56DE\u8BFB\u65AD\u8A00\u5931\u8D25\uFF1ASELECT \u7ED3\u679C\u4E0E\u5199\u5165\u503C\u4E0D\u4E00\u81F4\u3002",
|
|
2759
|
+
suggestion: buildNativeBindingSuggestion("LOAD_ERROR", context.pnpmVersion)
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
} catch (error) {
|
|
2763
|
+
safeClose(db);
|
|
2764
|
+
return fail("read", error);
|
|
2765
|
+
}
|
|
2766
|
+
try {
|
|
2767
|
+
db.close();
|
|
2768
|
+
} catch (error) {
|
|
2769
|
+
return fail("close", error);
|
|
2770
|
+
}
|
|
2771
|
+
return { ok: true };
|
|
2772
|
+
}
|
|
2773
|
+
function buildNativeBindingDiagnostic(failure, probeContext) {
|
|
2774
|
+
return {
|
|
2775
|
+
selectedCli: probeContext.selectedCli,
|
|
2776
|
+
runtime: `${process.execPath} (v${process.versions.node})`,
|
|
2777
|
+
abi: `process.versions.modules=${process.versions.modules}${abiPrebuiltHint(failure.detail)}`,
|
|
2778
|
+
installSource: probeContext.installSource,
|
|
2779
|
+
probedFrom: probeContext.probedFrom,
|
|
2780
|
+
failedStage: failure.failedStage,
|
|
2781
|
+
cause: failure.cause,
|
|
2782
|
+
suggestion: failure.suggestion
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
function buildNativeBindingSuccessDiagnostic(probeContext) {
|
|
2786
|
+
return {
|
|
2787
|
+
selectedCli: probeContext.selectedCli,
|
|
2788
|
+
runtime: `${process.execPath} (v${process.versions.node})`,
|
|
2789
|
+
installSource: probeContext.installSource,
|
|
2790
|
+
probedFrom: probeContext.probedFrom
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function findPackageRoot(anchorFile) {
|
|
2794
|
+
let dir = dirname2(anchorFile);
|
|
2795
|
+
for (; ; ) {
|
|
2796
|
+
if (existsSync2(join4(dir, "package.json"))) return dir;
|
|
2797
|
+
const parent = dirname2(dir);
|
|
2798
|
+
if (parent === dir) return void 0;
|
|
2799
|
+
dir = parent;
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
function runningArtifactGraphInstallRoot() {
|
|
2803
|
+
const anchor = typeof __filename === "string" ? __filename : fileURLToPath(import.meta.url);
|
|
2804
|
+
return findPackageRoot(anchor);
|
|
2805
|
+
}
|
|
2806
|
+
function structuredNativeBindingError(failure, context = {}) {
|
|
2807
|
+
return {
|
|
2808
|
+
error: "native_binding_unavailable",
|
|
2809
|
+
stage: failure.failedStage,
|
|
2810
|
+
cause: failure.cause,
|
|
2811
|
+
detail: failure.detail,
|
|
2812
|
+
suggestion: failure.suggestion,
|
|
2813
|
+
doctorHint: "artifact-graph doctor --format json",
|
|
2814
|
+
// F-10-P01/F-10-P03:机器读取错误包络也暴露实际探测来源,使 scan/query
|
|
2815
|
+
// 与 doctor 的分类依据(同一运行安装根)可被自动测试断言。
|
|
2816
|
+
probedFrom: context.probedFrom ?? "(current artifact-graph package)"
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
function renderStructuredNativeBindingError(failure, context = {}) {
|
|
2820
|
+
return `${JSON.stringify(structuredNativeBindingError(failure, context), null, 2)}
|
|
2821
|
+
`;
|
|
2822
|
+
}
|
|
2823
|
+
function safeClose(db) {
|
|
2824
|
+
try {
|
|
2825
|
+
db?.close();
|
|
2826
|
+
} catch {
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function abiPrebuiltHint(detail) {
|
|
2830
|
+
const match = /using NODE_MODULE_VERSION (\d+)/.exec(detail);
|
|
2831
|
+
return match ? `\uFF1Bprebuilt \u76EE\u6807 NODE_MODULE_VERSION=${match[1]}` : "";
|
|
2832
|
+
}
|
|
2833
|
+
function betterSqlite3PackagePresent(root) {
|
|
2834
|
+
const direct = join4(root, "node_modules", "better-sqlite3", "package.json");
|
|
2835
|
+
if (existsSync2(direct)) return true;
|
|
2836
|
+
const pnpmDir = join4(root, "node_modules", ".pnpm");
|
|
2837
|
+
let entries;
|
|
2838
|
+
try {
|
|
2839
|
+
entries = readdirSync(pnpmDir);
|
|
2840
|
+
} catch {
|
|
2841
|
+
return false;
|
|
2842
|
+
}
|
|
2843
|
+
return entries.some((entry) => {
|
|
2844
|
+
if (!entry.startsWith("better-sqlite3@")) return false;
|
|
2845
|
+
return existsSync2(join4(pnpmDir, entry, "node_modules", "better-sqlite3", "package.json"));
|
|
2846
|
+
});
|
|
2847
|
+
}
|
|
2848
|
+
var PNPM_ALLOW_BUILDS_SNIPPET;
|
|
2849
|
+
var init_native_binding_diagnostics = __esm({
|
|
2850
|
+
"src/native-binding-diagnostics.ts"() {
|
|
2851
|
+
"use strict";
|
|
2852
|
+
PNPM_ALLOW_BUILDS_SNIPPET = "pnpm-workspace.yaml:\nallowBuilds:\n better-sqlite3: true";
|
|
2853
|
+
}
|
|
2854
|
+
});
|
|
2855
|
+
|
|
2652
2856
|
// src/cli-resolver.ts
|
|
2653
2857
|
import { execFile } from "child_process";
|
|
2654
|
-
import { constants } from "fs";
|
|
2858
|
+
import { constants, readFileSync, realpathSync } from "fs";
|
|
2655
2859
|
import { access } from "fs/promises";
|
|
2656
|
-
import { isAbsolute, join as
|
|
2860
|
+
import { basename, dirname as dirname3, isAbsolute, join as join5, resolve } from "path";
|
|
2657
2861
|
import { promisify } from "util";
|
|
2658
2862
|
async function resolveArtifactGraphCli(root, options = {}) {
|
|
2659
2863
|
const pathCli = await findCommandOnPath("artifact-graph");
|
|
@@ -2661,7 +2865,7 @@ async function resolveArtifactGraphCli(root, options = {}) {
|
|
|
2661
2865
|
const candidates = [
|
|
2662
2866
|
{
|
|
2663
2867
|
source: "node_modules",
|
|
2664
|
-
path:
|
|
2868
|
+
path: join5(root, "node_modules/.bin/artifact-graph"),
|
|
2665
2869
|
exists: false
|
|
2666
2870
|
},
|
|
2667
2871
|
{
|
|
@@ -2694,13 +2898,33 @@ async function resolveArtifactGraphCli(root, options = {}) {
|
|
|
2694
2898
|
}
|
|
2695
2899
|
async function doctorArtifactChain(root, options = {}) {
|
|
2696
2900
|
const cli = await resolveArtifactGraphCli(root, options);
|
|
2697
|
-
const configPath =
|
|
2698
|
-
const lockPath =
|
|
2901
|
+
const configPath = join5(root, "artifact-graph.config.yaml");
|
|
2902
|
+
const lockPath = join5(root, VERSION_LOCK_PATH);
|
|
2699
2903
|
const supportedCommands = cli.path ? await detectSupportedCommands(cli.path) : [];
|
|
2700
2904
|
const nodeCompatible = isNodeCompatible(process.versions.node);
|
|
2905
|
+
const pnpmVersion = await detectPnpmVersion();
|
|
2906
|
+
const installBase = bindingInstallBase(cli, root);
|
|
2907
|
+
const installRoot = bindingInstallRoot(cli, root);
|
|
2908
|
+
const loader = options.bindingLoader ?? (installBase ? createInstallBoundBindingLoader(installBase) : void 0);
|
|
2909
|
+
const probe = probeNativeBinding({
|
|
2910
|
+
root,
|
|
2911
|
+
...installRoot ? { installRoot } : {},
|
|
2912
|
+
pnpmVersion,
|
|
2913
|
+
...loader ? { loader } : {}
|
|
2914
|
+
});
|
|
2915
|
+
const probedFrom = installBase ?? "(current artifact-graph package)";
|
|
2916
|
+
const probeContext = {
|
|
2917
|
+
selectedCli: cli.path ? `${cli.path} (${cli.source})` : "not found",
|
|
2918
|
+
installSource: cli.source ?? "unknown",
|
|
2919
|
+
probedFrom
|
|
2920
|
+
};
|
|
2921
|
+
const nativeBinding = probe.ok ? { ok: true, ...buildNativeBindingSuccessDiagnostic(probeContext) } : { ok: false, ...buildNativeBindingDiagnostic(probe, probeContext) };
|
|
2701
2922
|
const warnings = [
|
|
2702
2923
|
...cli.warnings,
|
|
2703
|
-
...nodeCompatible ? [] : [`Node.js ${process.versions.node} does not satisfy >=22.0.0.`]
|
|
2924
|
+
...nodeCompatible ? [] : [`Node.js ${process.versions.node} does not satisfy >=22.0.0.`],
|
|
2925
|
+
...probe.ok ? [] : [
|
|
2926
|
+
`better-sqlite3 binding probe FAILED at stage "${probe.failedStage}" (cause ${probe.cause}): ${probe.suggestion}`
|
|
2927
|
+
]
|
|
2704
2928
|
];
|
|
2705
2929
|
return {
|
|
2706
2930
|
schemaVersion: "1.0",
|
|
@@ -2719,6 +2943,7 @@ async function doctorArtifactChain(root, options = {}) {
|
|
|
2719
2943
|
path: lockPath,
|
|
2720
2944
|
exists: await pathExists(lockPath)
|
|
2721
2945
|
},
|
|
2946
|
+
nativeBinding,
|
|
2722
2947
|
supportedCommands,
|
|
2723
2948
|
warnings
|
|
2724
2949
|
};
|
|
@@ -2734,6 +2959,31 @@ function renderDoctorMarkdown(report) {
|
|
|
2734
2959
|
`Lock: \`${report.lock.path}\` ${report.lock.exists ? "found" : "missing"}`,
|
|
2735
2960
|
""
|
|
2736
2961
|
];
|
|
2962
|
+
lines.push("## Native Binding Probe");
|
|
2963
|
+
if (report.nativeBinding.ok) {
|
|
2964
|
+
const binding = report.nativeBinding;
|
|
2965
|
+
lines.push(
|
|
2966
|
+
"- better-sqlite3 probe: PASS",
|
|
2967
|
+
` - selectedCli: \`${binding.selectedCli}\``,
|
|
2968
|
+
` - runtime: \`${binding.runtime}\``,
|
|
2969
|
+
` - installSource: \`${binding.installSource}\``,
|
|
2970
|
+
` - probedFrom: \`${binding.probedFrom}\``
|
|
2971
|
+
);
|
|
2972
|
+
} else {
|
|
2973
|
+
const binding = report.nativeBinding;
|
|
2974
|
+
lines.push(
|
|
2975
|
+
"- better-sqlite3 probe: **FAIL**",
|
|
2976
|
+
` - failedStage: \`${binding.failedStage}\``,
|
|
2977
|
+
` - cause: \`${binding.cause}\``,
|
|
2978
|
+
` - selectedCli: \`${binding.selectedCli}\``,
|
|
2979
|
+
` - runtime: \`${binding.runtime}\``,
|
|
2980
|
+
` - abi: \`${binding.abi}\``,
|
|
2981
|
+
` - installSource: \`${binding.installSource}\``,
|
|
2982
|
+
` - probedFrom: \`${binding.probedFrom}\``,
|
|
2983
|
+
` - suggestion: ${binding.suggestion}`
|
|
2984
|
+
);
|
|
2985
|
+
}
|
|
2986
|
+
lines.push("");
|
|
2737
2987
|
if (report.supportedCommands.length > 0) {
|
|
2738
2988
|
lines.push("## Supported Commands");
|
|
2739
2989
|
for (const command of report.supportedCommands) {
|
|
@@ -2790,11 +3040,71 @@ function isNodeCompatible(version) {
|
|
|
2790
3040
|
const major = Number(version.split(".")[0]);
|
|
2791
3041
|
return Number.isFinite(major) && major >= 22;
|
|
2792
3042
|
}
|
|
3043
|
+
async function detectPnpmVersion() {
|
|
3044
|
+
try {
|
|
3045
|
+
const result = await execFileAsync("pnpm", ["--version"]);
|
|
3046
|
+
return parsePnpmVersion(result.stdout);
|
|
3047
|
+
} catch {
|
|
3048
|
+
return void 0;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
function bindingInstallBase(cli, root) {
|
|
3052
|
+
if (!cli.path || !cli.source) return void 0;
|
|
3053
|
+
switch (cli.source) {
|
|
3054
|
+
case "node_modules":
|
|
3055
|
+
return join5(root, "package.json");
|
|
3056
|
+
case "plugin-bundled":
|
|
3057
|
+
case "path":
|
|
3058
|
+
case "legacy":
|
|
3059
|
+
return resolveCliInstallAnchor(cli.path);
|
|
3060
|
+
default:
|
|
3061
|
+
return void 0;
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
function bindingInstallRoot(cli, root) {
|
|
3065
|
+
if (!cli.path || !cli.source) return void 0;
|
|
3066
|
+
switch (cli.source) {
|
|
3067
|
+
case "node_modules":
|
|
3068
|
+
return root;
|
|
3069
|
+
case "plugin-bundled":
|
|
3070
|
+
case "path":
|
|
3071
|
+
case "legacy":
|
|
3072
|
+
return findPackageRoot(resolveCliInstallAnchor(cli.path));
|
|
3073
|
+
default:
|
|
3074
|
+
return void 0;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function resolveCliInstallAnchor(cliPath) {
|
|
3078
|
+
const real = safeRealpath(cliPath);
|
|
3079
|
+
if (basename(dirname3(real)) !== ".bin") return real;
|
|
3080
|
+
const shimTarget = readBinShimTarget(real);
|
|
3081
|
+
return shimTarget ? safeRealpath(shimTarget) : real;
|
|
3082
|
+
}
|
|
3083
|
+
function readBinShimTarget(shimPath) {
|
|
3084
|
+
let content;
|
|
3085
|
+
try {
|
|
3086
|
+
content = readFileSync(shimPath, "utf-8");
|
|
3087
|
+
} catch {
|
|
3088
|
+
return void 0;
|
|
3089
|
+
}
|
|
3090
|
+
if (!content.startsWith("#!")) return void 0;
|
|
3091
|
+
const match = /\$basedir\/(\.\.(?:\/[^"'\s]+)+\.js)/.exec(content);
|
|
3092
|
+
if (!match) return void 0;
|
|
3093
|
+
return resolve(dirname3(shimPath), match[1]);
|
|
3094
|
+
}
|
|
3095
|
+
function safeRealpath(path) {
|
|
3096
|
+
try {
|
|
3097
|
+
return realpathSync(path);
|
|
3098
|
+
} catch {
|
|
3099
|
+
return path;
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
2793
3102
|
var execFileAsync, KNOWN_COMMANDS;
|
|
2794
3103
|
var init_cli_resolver = __esm({
|
|
2795
3104
|
"src/cli-resolver.ts"() {
|
|
2796
3105
|
"use strict";
|
|
2797
3106
|
init_versioned_traceability();
|
|
3107
|
+
init_native_binding_diagnostics();
|
|
2798
3108
|
execFileAsync = promisify(execFile);
|
|
2799
3109
|
KNOWN_COMMANDS = [
|
|
2800
3110
|
"init",
|
|
@@ -2813,6 +3123,7 @@ var init_cli_resolver = __esm({
|
|
|
2813
3123
|
"version-lock refresh",
|
|
2814
3124
|
"trace-version",
|
|
2815
3125
|
"next-id",
|
|
3126
|
+
"refactor-id",
|
|
2816
3127
|
"render",
|
|
2817
3128
|
"doctor"
|
|
2818
3129
|
];
|
|
@@ -2920,7 +3231,7 @@ var init_git_hook_path = __esm({
|
|
|
2920
3231
|
import { constants as constants2 } from "fs";
|
|
2921
3232
|
import { randomUUID } from "crypto";
|
|
2922
3233
|
import { lstat, mkdir as mkdir3, open, readlink, rename, unlink } from "fs/promises";
|
|
2923
|
-
import { basename, dirname as
|
|
3234
|
+
import { basename as basename2, dirname as dirname4, join as join6 } from "path";
|
|
2924
3235
|
function detectHookInterpreter(content) {
|
|
2925
3236
|
if (content.trim().length === 0) {
|
|
2926
3237
|
return "empty";
|
|
@@ -3227,8 +3538,8 @@ async function removeHookAtomically(hookPath, snapshot) {
|
|
|
3227
3538
|
}
|
|
3228
3539
|
}
|
|
3229
3540
|
async function writeHookAtomically(hookPath, content, snapshot, mode) {
|
|
3230
|
-
await mkdir3(
|
|
3231
|
-
const temporaryPath =
|
|
3541
|
+
await mkdir3(dirname4(hookPath), { recursive: true });
|
|
3542
|
+
const temporaryPath = join6(dirname4(hookPath), `.${basename2(hookPath)}.${randomUUID()}.tmp`);
|
|
3232
3543
|
let temporaryExists = false;
|
|
3233
3544
|
try {
|
|
3234
3545
|
const temporary = await open(temporaryPath, "wx", mode);
|
|
@@ -3694,7 +4005,7 @@ __export(contract_kernel_exports, {
|
|
|
3694
4005
|
});
|
|
3695
4006
|
import { createHash as createHash2 } from "crypto";
|
|
3696
4007
|
import { readFile as readFile2, readdir as readdir2 } from "fs/promises";
|
|
3697
|
-
import { join as
|
|
4008
|
+
import { join as join7 } from "path";
|
|
3698
4009
|
import _AjvModule from "ajv";
|
|
3699
4010
|
function isOfficialNamespace(namespace) {
|
|
3700
4011
|
return OFFICIAL_NAMESPACE_PATTERN.test(namespace);
|
|
@@ -4107,7 +4418,7 @@ async function loadContractsFromDirectory(contractsDir, options) {
|
|
|
4107
4418
|
const entries = await readdir2(contractsDir, { withFileTypes: true });
|
|
4108
4419
|
for (const entry of entries) {
|
|
4109
4420
|
if (entry.isDirectory()) {
|
|
4110
|
-
const schemaPath =
|
|
4421
|
+
const schemaPath = join7(contractsDir, entry.name, "schema.json");
|
|
4111
4422
|
const contract = await loadContract(schemaPath, options);
|
|
4112
4423
|
contracts.push(contract);
|
|
4113
4424
|
}
|
|
@@ -4595,12 +4906,11 @@ var init_contract_kernel = __esm({
|
|
|
4595
4906
|
});
|
|
4596
4907
|
|
|
4597
4908
|
// src/index.ts
|
|
4598
|
-
import Database from "better-sqlite3";
|
|
4599
4909
|
import matter from "gray-matter";
|
|
4600
4910
|
import yaml from "js-yaml";
|
|
4601
|
-
import { accessSync, constants as fsConstants, existsSync as
|
|
4911
|
+
import { accessSync, constants as fsConstants, existsSync as existsSync3, statSync } from "fs";
|
|
4602
4912
|
import { mkdir as mkdir4, readFile as readFile3, readdir as readdir3, writeFile as writeFile3 } from "fs/promises";
|
|
4603
|
-
import { basename as
|
|
4913
|
+
import { basename as basename4, dirname as dirname5, extname, isAbsolute as isAbsolute3, join as join8, relative as relative3, resolve as resolve3 } from "path";
|
|
4604
4914
|
function isTargetArtifactType(type) {
|
|
4605
4915
|
return isPacketTargetType(type);
|
|
4606
4916
|
}
|
|
@@ -4642,7 +4952,7 @@ function resolveArtifactTypeName(schema, token) {
|
|
|
4642
4952
|
return void 0;
|
|
4643
4953
|
}
|
|
4644
4954
|
async function loadConfig(root) {
|
|
4645
|
-
const configPath =
|
|
4955
|
+
const configPath = join8(root, "artifact-graph.config.yaml");
|
|
4646
4956
|
let parsed = {};
|
|
4647
4957
|
try {
|
|
4648
4958
|
const raw = await readFile3(configPath, "utf-8");
|
|
@@ -4819,7 +5129,7 @@ async function scanArtifacts(root, schema) {
|
|
|
4819
5129
|
continue;
|
|
4820
5130
|
}
|
|
4821
5131
|
scannedFiles.set(file, type);
|
|
4822
|
-
const raw = await readFile3(
|
|
5132
|
+
const raw = await readFile3(join8(root, file), "utf-8");
|
|
4823
5133
|
const parsed = parseFile(type, file, raw, config);
|
|
4824
5134
|
nodes.push(...parsed.nodes);
|
|
4825
5135
|
edges.push(...parsed.edges);
|
|
@@ -5132,7 +5442,7 @@ async function validateScenarioPrdLinkIndex(root, graph) {
|
|
|
5132
5442
|
const indexPath = "artifacts/prd/feature-index.md";
|
|
5133
5443
|
let raw = "";
|
|
5134
5444
|
try {
|
|
5135
|
-
raw = await readFile3(
|
|
5445
|
+
raw = await readFile3(join8(root, indexPath), "utf-8");
|
|
5136
5446
|
} catch (error) {
|
|
5137
5447
|
if (error.code === "ENOENT") {
|
|
5138
5448
|
return [];
|
|
@@ -5322,11 +5632,12 @@ function nextId(graph, schema, type, rangeName) {
|
|
|
5322
5632
|
throw new Error(`ID range ${type}.${rangeName} is exhausted`);
|
|
5323
5633
|
}
|
|
5324
5634
|
async function writeGraphCache(root, graph) {
|
|
5325
|
-
const cacheDir =
|
|
5635
|
+
const cacheDir = join8(root, ".artifact-graph");
|
|
5326
5636
|
await mkdir4(cacheDir, { recursive: true });
|
|
5327
|
-
await writeFile3(
|
|
5637
|
+
await writeFile3(join8(cacheDir, "index.json"), `${JSON.stringify(graph, null, 2)}
|
|
5328
5638
|
`);
|
|
5329
|
-
const
|
|
5639
|
+
const { default: Database } = await import("better-sqlite3");
|
|
5640
|
+
const db = new Database(join8(cacheDir, "graph.sqlite"));
|
|
5330
5641
|
try {
|
|
5331
5642
|
db.exec(`
|
|
5332
5643
|
DROP TABLE IF EXISTS nodes;
|
|
@@ -5680,7 +5991,7 @@ function parseDecisions(path, raw) {
|
|
|
5680
5991
|
}
|
|
5681
5992
|
function isTestFile(filePath) {
|
|
5682
5993
|
const normalized = filePath.replace(/\\/g, "/");
|
|
5683
|
-
const name =
|
|
5994
|
+
const name = basename4(normalized);
|
|
5684
5995
|
if (/\.(test|spec)\.[^.]+$/.test(name)) return true;
|
|
5685
5996
|
if (/(^|\/)(tests|test|__tests__)\//.test(normalized)) return true;
|
|
5686
5997
|
if (/\w+Tests?\.java$/.test(name)) return true;
|
|
@@ -6001,7 +6312,7 @@ function parseE2eTest(path, raw) {
|
|
|
6001
6312
|
});
|
|
6002
6313
|
const nodes = [];
|
|
6003
6314
|
const edges = [];
|
|
6004
|
-
const batch = String(data.test_batch ??
|
|
6315
|
+
const batch = String(data.test_batch ?? basename4(path, extname(path))).trim();
|
|
6005
6316
|
const frontmatterScenarios = toArray(data.related_scenarios).map((value) => String(value).trim()).filter(Boolean);
|
|
6006
6317
|
const scopeFeatures = extractCodes(String(data.scope ?? ""), "feature");
|
|
6007
6318
|
const frontmatterFeatures = [.../* @__PURE__ */ new Set([...Object.keys(asRecord(data.ac_coverage)), ...scopeFeatures])];
|
|
@@ -6074,7 +6385,7 @@ function parseE2eRegistry(path, raw) {
|
|
|
6074
6385
|
data = { parseError: error.message };
|
|
6075
6386
|
}
|
|
6076
6387
|
return {
|
|
6077
|
-
nodes: [{ type: "e2e_registry", code:
|
|
6388
|
+
nodes: [{ type: "e2e_registry", code: basename4(path, extname(path)), title: "E2E Test Registry", path, line: 1, attrs: data }],
|
|
6078
6389
|
edges: []
|
|
6079
6390
|
};
|
|
6080
6391
|
}
|
|
@@ -7100,10 +7411,10 @@ function validateE2eRegistry(graph) {
|
|
|
7100
7411
|
async function validateExecutableTraceability(root, config) {
|
|
7101
7412
|
const issues = [];
|
|
7102
7413
|
const schema = config ?? await loadConfig(root);
|
|
7103
|
-
const e2eDir =
|
|
7414
|
+
const e2eDir = join8(root, "artifacts", "tests", "e2e");
|
|
7104
7415
|
let e2eFiles;
|
|
7105
7416
|
try {
|
|
7106
|
-
e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) =>
|
|
7417
|
+
e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join8(e2eDir, name));
|
|
7107
7418
|
} catch {
|
|
7108
7419
|
return [];
|
|
7109
7420
|
}
|
|
@@ -7116,7 +7427,7 @@ async function validateExecutableTraceability(root, config) {
|
|
|
7116
7427
|
const relPath = relative3(root, filePath).split("\\").join("/");
|
|
7117
7428
|
const parsed = matter(raw);
|
|
7118
7429
|
const data = parsed.data;
|
|
7119
|
-
const batch = String(data.test_batch ??
|
|
7430
|
+
const batch = String(data.test_batch ?? basename4(filePath, extname(filePath))).trim();
|
|
7120
7431
|
const lines = raw.split(/\r?\n/);
|
|
7121
7432
|
const tcStarts = [];
|
|
7122
7433
|
lines.forEach((line, index) => {
|
|
@@ -7161,7 +7472,7 @@ async function validateExecutableTraceability(root, config) {
|
|
|
7161
7472
|
const tcAnnotationRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+?)\s+\[(\w+)\]/;
|
|
7162
7473
|
const tcAnnotationNoLevelRegex = /\/\/!?\s*@(?:e2e_test|tc)\s+(\S+)/;
|
|
7163
7474
|
for (const specFile of specFiles) {
|
|
7164
|
-
const fullSpecPath =
|
|
7475
|
+
const fullSpecPath = join8(root, specFile);
|
|
7165
7476
|
let content;
|
|
7166
7477
|
try {
|
|
7167
7478
|
content = await readFile3(fullSpecPath, "utf-8");
|
|
@@ -7243,7 +7554,7 @@ async function validateExecutableTraceability(root, config) {
|
|
|
7243
7554
|
if (entry.testId) {
|
|
7244
7555
|
let content;
|
|
7245
7556
|
try {
|
|
7246
|
-
content = await readFile3(
|
|
7557
|
+
content = await readFile3(join8(root, normalizedRefFile), "utf-8");
|
|
7247
7558
|
} catch {
|
|
7248
7559
|
continue;
|
|
7249
7560
|
}
|
|
@@ -7420,11 +7731,11 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
|
7420
7731
|
let withExecutableRef = 0;
|
|
7421
7732
|
const statusBreakdown = {};
|
|
7422
7733
|
const chainTypeBreakdown = {};
|
|
7423
|
-
const e2eDir =
|
|
7734
|
+
const e2eDir = join8(root, "artifacts", "tests", "e2e");
|
|
7424
7735
|
const tcFieldsMap = /* @__PURE__ */ new Map();
|
|
7425
7736
|
let e2eFiles;
|
|
7426
7737
|
try {
|
|
7427
|
-
e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) =>
|
|
7738
|
+
e2eFiles = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).map((name) => join8(e2eDir, name));
|
|
7428
7739
|
} catch {
|
|
7429
7740
|
e2eFiles = [];
|
|
7430
7741
|
}
|
|
@@ -7439,7 +7750,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
|
7439
7750
|
}
|
|
7440
7751
|
});
|
|
7441
7752
|
const parsed = matter(raw);
|
|
7442
|
-
const batch = String(parsed.data.test_batch ??
|
|
7753
|
+
const batch = String(parsed.data.test_batch ?? basename4(filePath, extname(filePath))).trim();
|
|
7443
7754
|
for (let i = 0; i < tcStarts.length; i++) {
|
|
7444
7755
|
const start = tcStarts[i];
|
|
7445
7756
|
const end = tcStarts[i + 1]?.index ?? lines.length;
|
|
@@ -7502,7 +7813,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
|
7502
7813
|
let hasActiveE2eRef = false;
|
|
7503
7814
|
for (const entry of parseExecutableRefLines(execRef)) {
|
|
7504
7815
|
const normalized = resolveExecutableRefFile(entry.file, allProjectFiles);
|
|
7505
|
-
if (!normalized || !
|
|
7816
|
+
if (!normalized || !existsSync3(join8(root, normalized))) continue;
|
|
7506
7817
|
const accepting = await getAcceptingRunners(root, normalized, runners);
|
|
7507
7818
|
if (accepting.some((runner) => runner.kind === "e2e")) {
|
|
7508
7819
|
hasActiveE2eRef = true;
|
|
@@ -7561,7 +7872,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
|
7561
7872
|
const acCoverageRateByFeature = {};
|
|
7562
7873
|
const featureAcMap = /* @__PURE__ */ new Map();
|
|
7563
7874
|
for (const node of featureNodes) {
|
|
7564
|
-
const acs = parseAcceptanceCriteria(await readFile3(
|
|
7875
|
+
const acs = parseAcceptanceCriteria(await readFile3(join8(root, node.path), "utf-8"));
|
|
7565
7876
|
featureAcMap.set(node.code, new Set(acs));
|
|
7566
7877
|
}
|
|
7567
7878
|
const coveredAcByFeature = /* @__PURE__ */ new Map();
|
|
@@ -7603,7 +7914,7 @@ async function computeE2eCoverageStats(graph, root, thresholds = {}) {
|
|
|
7603
7914
|
};
|
|
7604
7915
|
}
|
|
7605
7916
|
async function generateE2eRegistry(root, opts) {
|
|
7606
|
-
const e2eDir =
|
|
7917
|
+
const e2eDir = join8(root, "artifacts", "tests", "e2e");
|
|
7607
7918
|
let files;
|
|
7608
7919
|
try {
|
|
7609
7920
|
files = (await readdir3(e2eDir)).filter((name) => /^test-.*\.md$/.test(name)).sort();
|
|
@@ -7619,11 +7930,11 @@ async function generateE2eRegistry(root, opts) {
|
|
|
7619
7930
|
const batches = [];
|
|
7620
7931
|
let totalTestCases = 0;
|
|
7621
7932
|
for (const file of files) {
|
|
7622
|
-
const filePath =
|
|
7933
|
+
const filePath = join8(e2eDir, file);
|
|
7623
7934
|
const raw = await readFile3(filePath, "utf-8");
|
|
7624
7935
|
const parsed = matter(raw);
|
|
7625
7936
|
const data = parsed.data;
|
|
7626
|
-
const batch = String(data.test_batch ??
|
|
7937
|
+
const batch = String(data.test_batch ?? basename4(file, extname(file))).trim();
|
|
7627
7938
|
const relPath = `artifacts/tests/e2e/${file}`;
|
|
7628
7939
|
const scope = String(data.scope ?? "").trim();
|
|
7629
7940
|
const acCoverage = normalizeAcCoverageForRegistry(data.ac_coverage);
|
|
@@ -7747,7 +8058,7 @@ async function validatePartialRustEvidence(tcFields, tcKey, root, allFiles) {
|
|
|
7747
8058
|
if (!normalizedPath) {
|
|
7748
8059
|
return { hasValidPartialRust: false, detail: `partial_rust file not found: ${ref.file}` };
|
|
7749
8060
|
}
|
|
7750
|
-
const fullPath =
|
|
8061
|
+
const fullPath = join8(root, normalizedPath);
|
|
7751
8062
|
let content;
|
|
7752
8063
|
try {
|
|
7753
8064
|
content = await readFile3(fullPath, "utf-8");
|
|
@@ -7859,7 +8170,7 @@ function escapeRegExp(value) {
|
|
|
7859
8170
|
}
|
|
7860
8171
|
async function hasMarkdownTc(tcKey, e2eDir) {
|
|
7861
8172
|
const [batch, tcId] = tcKey.split(":");
|
|
7862
|
-
const filePath =
|
|
8173
|
+
const filePath = join8(e2eDir, `${batch}.md`);
|
|
7863
8174
|
try {
|
|
7864
8175
|
const raw = await readFile3(filePath, "utf-8");
|
|
7865
8176
|
const tcRegex = new RegExp(`^#{2,3}\\s+${escapeRegExp(tcId)}\\s*[:\uFF1A]?`, "m");
|
|
@@ -8092,7 +8403,7 @@ function normalizeDesignCode(value) {
|
|
|
8092
8403
|
if (!raw) {
|
|
8093
8404
|
return "";
|
|
8094
8405
|
}
|
|
8095
|
-
return
|
|
8406
|
+
return basename4(raw, extname(raw));
|
|
8096
8407
|
}
|
|
8097
8408
|
function toUid(type, code) {
|
|
8098
8409
|
return `${type}:${code}`;
|
|
@@ -8357,14 +8668,14 @@ function resolveArtifactContext(graph, opts) {
|
|
|
8357
8668
|
}
|
|
8358
8669
|
if (root) {
|
|
8359
8670
|
for (const ap of ALWAYS_PRESENT_ITEMS) {
|
|
8360
|
-
const fullPath =
|
|
8361
|
-
let
|
|
8671
|
+
const fullPath = join8(root, ap.path);
|
|
8672
|
+
let stat2;
|
|
8362
8673
|
try {
|
|
8363
|
-
|
|
8674
|
+
stat2 = statSync(fullPath);
|
|
8364
8675
|
} catch {
|
|
8365
|
-
|
|
8676
|
+
stat2 = null;
|
|
8366
8677
|
}
|
|
8367
|
-
if (!
|
|
8678
|
+
if (!stat2) {
|
|
8368
8679
|
const msg = `Required baseline artifact not found: ${ap.path}`;
|
|
8369
8680
|
if (!missing.includes(msg)) {
|
|
8370
8681
|
missing.push(msg);
|
|
@@ -8376,7 +8687,7 @@ function resolveArtifactContext(graph, opts) {
|
|
|
8376
8687
|
suggestedAction: `\u521B\u5EFA\u6587\u4EF6 ${ap.path} \u6216\u914D\u7F6E\u8DF3\u8FC7 universal baseline`
|
|
8377
8688
|
});
|
|
8378
8689
|
}
|
|
8379
|
-
} else if (!
|
|
8690
|
+
} else if (!stat2.isFile()) {
|
|
8380
8691
|
const msg = `Required baseline artifact is not a regular file: ${ap.path}`;
|
|
8381
8692
|
if (!missing.includes(msg)) {
|
|
8382
8693
|
missing.push(msg);
|
|
@@ -8700,7 +9011,7 @@ var init_index = __esm({
|
|
|
8700
9011
|
|
|
8701
9012
|
// src/packet-prompt-audit.ts
|
|
8702
9013
|
import { mkdir as mkdir5, writeFile as writeFile4 } from "fs/promises";
|
|
8703
|
-
import { join as
|
|
9014
|
+
import { join as join9 } from "path";
|
|
8704
9015
|
function promptFilename(target) {
|
|
8705
9016
|
return `prompt-${target.type}-${target.id}.md`;
|
|
8706
9017
|
}
|
|
@@ -8752,7 +9063,7 @@ async function auditSinglePromptTarget(target, graph, options) {
|
|
|
8752
9063
|
}
|
|
8753
9064
|
if (options.outDir) {
|
|
8754
9065
|
const filename = promptFilename(target);
|
|
8755
|
-
const outPath =
|
|
9066
|
+
const outPath = join9(options.outDir, filename);
|
|
8756
9067
|
await writeFile4(outPath, prompt, "utf-8");
|
|
8757
9068
|
entry.outputPath = outPath;
|
|
8758
9069
|
}
|
|
@@ -8858,9 +9169,9 @@ async function auditPromptBatch(root, targets, options, graph) {
|
|
|
8858
9169
|
};
|
|
8859
9170
|
if (options.outDir) {
|
|
8860
9171
|
await mkdir5(options.outDir, { recursive: true });
|
|
8861
|
-
const jsonPath =
|
|
9172
|
+
const jsonPath = join9(options.outDir, "prompt-audit-summary.json");
|
|
8862
9173
|
await writeFile4(jsonPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
|
|
8863
|
-
const mdPath =
|
|
9174
|
+
const mdPath = join9(options.outDir, "prompt-audit-summary.md");
|
|
8864
9175
|
await writeFile4(mdPath, renderPromptAuditSummaryMarkdown(summary), "utf-8");
|
|
8865
9176
|
}
|
|
8866
9177
|
return summary;
|
|
@@ -8894,22 +9205,649 @@ var init_packet_prompt_audit = __esm({
|
|
|
8894
9205
|
}
|
|
8895
9206
|
});
|
|
8896
9207
|
|
|
9208
|
+
// src/refactor-id.ts
|
|
9209
|
+
var refactor_id_exports = {};
|
|
9210
|
+
__export(refactor_id_exports, {
|
|
9211
|
+
refactorId: () => refactorId,
|
|
9212
|
+
renderRefactorIdMarkdown: () => renderRefactorIdMarkdown
|
|
9213
|
+
});
|
|
9214
|
+
import { chmod, readFile as readFile4, rename as rename2, rm, readdir as readdir4, stat, writeFile as writeFile5 } from "fs/promises";
|
|
9215
|
+
import { randomBytes } from "crypto";
|
|
9216
|
+
import { basename as basename5, dirname as dirname6, join as join10, relative as relative4 } from "path";
|
|
9217
|
+
import yaml2 from "js-yaml";
|
|
9218
|
+
function escapeRegExp2(value) {
|
|
9219
|
+
return value.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
9220
|
+
}
|
|
9221
|
+
function idTokenPattern(id) {
|
|
9222
|
+
return new RegExp(`(?<![\\w-])${escapeRegExp2(id)}(?![\\w-])`, "g");
|
|
9223
|
+
}
|
|
9224
|
+
function replaceIdToken(text, oldId, newId) {
|
|
9225
|
+
return text.replace(idTokenPattern(oldId), newId);
|
|
9226
|
+
}
|
|
9227
|
+
function containsIdToken(text, id) {
|
|
9228
|
+
return idTokenPattern(id).test(text);
|
|
9229
|
+
}
|
|
9230
|
+
function lineEndingOf(line) {
|
|
9231
|
+
if (line.endsWith("\r\n")) return "\r\n";
|
|
9232
|
+
if (line.endsWith("\n")) return "\n";
|
|
9233
|
+
if (line.endsWith("\r")) return "\r";
|
|
9234
|
+
return "";
|
|
9235
|
+
}
|
|
9236
|
+
function splitLinesPreserveEndings(text) {
|
|
9237
|
+
const lines = [];
|
|
9238
|
+
const length = text.length;
|
|
9239
|
+
let index = 0;
|
|
9240
|
+
while (index < length) {
|
|
9241
|
+
let cursor = index;
|
|
9242
|
+
while (cursor < length && text[cursor] !== "\r" && text[cursor] !== "\n") cursor += 1;
|
|
9243
|
+
let end = cursor;
|
|
9244
|
+
if (cursor < length) {
|
|
9245
|
+
end = text[cursor] === "\r" && text[cursor + 1] === "\n" ? cursor + 2 : cursor + 1;
|
|
9246
|
+
}
|
|
9247
|
+
lines.push(text.slice(index, end));
|
|
9248
|
+
index = end;
|
|
9249
|
+
if (cursor >= length) break;
|
|
9250
|
+
}
|
|
9251
|
+
return lines;
|
|
9252
|
+
}
|
|
9253
|
+
function sortEdges(edges) {
|
|
9254
|
+
return edges.map((edge2) => ({ from: edge2.from, to: edge2.to, kind: edge2.kind, source: edge2.source, sourcePath: edge2.sourcePath, sourceLine: edge2.sourceLine })).sort((a, b) => a.sourcePath.localeCompare(b.sourcePath) || a.sourceLine - b.sourceLine || a.from.localeCompare(b.from) || a.kind.localeCompare(b.kind));
|
|
9255
|
+
}
|
|
9256
|
+
function frontmatterRelationFields(type, config) {
|
|
9257
|
+
const fields = /* @__PURE__ */ new Set();
|
|
9258
|
+
const declared = config.relationFields?.[type];
|
|
9259
|
+
if (declared) {
|
|
9260
|
+
for (const field of declared) {
|
|
9261
|
+
if (!field.startsWith("@")) fields.add(field);
|
|
9262
|
+
}
|
|
9263
|
+
}
|
|
9264
|
+
if (type === "feature") {
|
|
9265
|
+
for (const field of ["scenarios", "decisions", "depends_on", "design_docs"]) fields.add(field);
|
|
9266
|
+
}
|
|
9267
|
+
if (type === "decision") {
|
|
9268
|
+
for (const field of ["related_features", "related_scenarios"]) fields.add(field);
|
|
9269
|
+
}
|
|
9270
|
+
if (type === "design") {
|
|
9271
|
+
for (const field of ["related_features", "related_scenarios", "related_decisions"]) fields.add(field);
|
|
9272
|
+
}
|
|
9273
|
+
if (!declared) {
|
|
9274
|
+
fields.add("related_*");
|
|
9275
|
+
}
|
|
9276
|
+
return fields;
|
|
9277
|
+
}
|
|
9278
|
+
function fieldWhitelisted(fieldName, whitelist) {
|
|
9279
|
+
if (whitelist.has(fieldName)) return true;
|
|
9280
|
+
if (whitelist.has("related_*") && fieldName.startsWith("related_")) return true;
|
|
9281
|
+
return false;
|
|
9282
|
+
}
|
|
9283
|
+
function findFrontmatterBlock(lines) {
|
|
9284
|
+
if (lines[0]?.trim() !== "---") return null;
|
|
9285
|
+
for (let index = 1; index < lines.length; index += 1) {
|
|
9286
|
+
if (lines[index].trim() === "---") {
|
|
9287
|
+
return { startLine: 0, endLine: index, text: lines.slice(1, index).join("\n") };
|
|
9288
|
+
}
|
|
9289
|
+
}
|
|
9290
|
+
return null;
|
|
9291
|
+
}
|
|
9292
|
+
function artifactTypeOfPath(path, config) {
|
|
9293
|
+
for (const [type, definition] of Object.entries(config.types)) {
|
|
9294
|
+
for (const pattern of definition.paths) {
|
|
9295
|
+
if (globLikeMatch(pattern, path)) return type;
|
|
9296
|
+
}
|
|
9297
|
+
}
|
|
9298
|
+
return null;
|
|
9299
|
+
}
|
|
9300
|
+
function globLikeMatch(pattern, path) {
|
|
9301
|
+
const regex = pattern.split("/").map((segment) => {
|
|
9302
|
+
if (segment === "**") return "<<DOUBLESTAR>>";
|
|
9303
|
+
return segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]");
|
|
9304
|
+
}).join("/");
|
|
9305
|
+
const flattened = regex.replace(/<<DOUBLESTAR>>\//g, "(?:[^/]+/)*").replace(/\/<<DOUBLESTAR>>/g, "(?:/[^/]+)*").replace(/<<DOUBLESTAR>>/g, ".*");
|
|
9306
|
+
return new RegExp(`^${flattened}$`).test(path);
|
|
9307
|
+
}
|
|
9308
|
+
function planFrontmatterEdits(path, raw, fileType, oldId, newId, config, isOwnFile) {
|
|
9309
|
+
const lines = raw.split(/\r?\n/);
|
|
9310
|
+
const block = findFrontmatterBlock(lines);
|
|
9311
|
+
if (!block) return [];
|
|
9312
|
+
let data;
|
|
9313
|
+
try {
|
|
9314
|
+
const loaded = yaml2.load(block.text);
|
|
9315
|
+
if (typeof loaded !== "object" || loaded === null || Array.isArray(loaded)) return [];
|
|
9316
|
+
data = loaded;
|
|
9317
|
+
} catch {
|
|
9318
|
+
return [];
|
|
9319
|
+
}
|
|
9320
|
+
const whitelist = frontmatterRelationFields(fileType, config);
|
|
9321
|
+
const edits = [];
|
|
9322
|
+
let currentField = null;
|
|
9323
|
+
let inEligibleField = false;
|
|
9324
|
+
const eligibleByValue = /* @__PURE__ */ new Set();
|
|
9325
|
+
for (const [fieldName, fieldValue] of Object.entries(data)) {
|
|
9326
|
+
if (fieldName === "title" || fieldName === "status") continue;
|
|
9327
|
+
if (fieldName === "id") continue;
|
|
9328
|
+
if (!fieldWhitelisted(fieldName, whitelist)) continue;
|
|
9329
|
+
const serialized = JSON.stringify(fieldValue ?? null);
|
|
9330
|
+
if (containsIdToken(serialized.replace(/\\u/g, "u"), oldId) || serialized.includes(`"${oldId}"`)) {
|
|
9331
|
+
eligibleByValue.add(fieldName);
|
|
9332
|
+
}
|
|
9333
|
+
}
|
|
9334
|
+
for (let index = block.startLine + 1; index < block.endLine; index += 1) {
|
|
9335
|
+
const line = lines[index];
|
|
9336
|
+
const topLevel = /^([A-Za-z_][\w-]*):(.*)$/.exec(line);
|
|
9337
|
+
if (topLevel) {
|
|
9338
|
+
currentField = topLevel[1];
|
|
9339
|
+
const valueText = topLevel[2];
|
|
9340
|
+
if (currentField === "id" && isOwnFile && valueText.trim().replace(/^["']|["']$/g, "") === oldId) {
|
|
9341
|
+
const newLine = line.replace(valueText, ` ${newId}`);
|
|
9342
|
+
edits.push({ path, line: index + 1, oldLine: line, newLine, kind: "frontmatter-id" });
|
|
9343
|
+
inEligibleField = false;
|
|
9344
|
+
continue;
|
|
9345
|
+
}
|
|
9346
|
+
inEligibleField = currentField !== null && eligibleByValue.has(currentField);
|
|
9347
|
+
if (inEligibleField && containsIdToken(valueText, oldId)) {
|
|
9348
|
+
edits.push({ path, line: index + 1, oldLine: line, newLine: replaceIdToken(line, oldId, newId), kind: "frontmatter-relation" });
|
|
9349
|
+
}
|
|
9350
|
+
continue;
|
|
9351
|
+
}
|
|
9352
|
+
if (!inEligibleField || currentField === null) continue;
|
|
9353
|
+
if (/^\S/.test(line)) {
|
|
9354
|
+
inEligibleField = false;
|
|
9355
|
+
currentField = null;
|
|
9356
|
+
continue;
|
|
9357
|
+
}
|
|
9358
|
+
if (!containsIdToken(line, oldId)) continue;
|
|
9359
|
+
if (currentField === "ac_coverage") {
|
|
9360
|
+
const keyMatch = /^(\s+)([^:\s]+)(\s*[::].*)$/.exec(line);
|
|
9361
|
+
if (keyMatch && keyMatch[2] === oldId) {
|
|
9362
|
+
edits.push({ path, line: index + 1, oldLine: line, newLine: `${keyMatch[1]}${newId}${keyMatch[3]}`, kind: "frontmatter-relation" });
|
|
9363
|
+
}
|
|
9364
|
+
continue;
|
|
9365
|
+
}
|
|
9366
|
+
edits.push({ path, line: index + 1, oldLine: line, newLine: replaceIdToken(line, oldId, newId), kind: "frontmatter-relation" });
|
|
9367
|
+
}
|
|
9368
|
+
return edits;
|
|
9369
|
+
}
|
|
9370
|
+
function planE2eBlockEdits(path, raw, oldId, newId) {
|
|
9371
|
+
const lines = raw.split(/\r?\n/);
|
|
9372
|
+
const edits = [];
|
|
9373
|
+
let tcHeadingIndex = -1;
|
|
9374
|
+
lines.forEach((line, index) => {
|
|
9375
|
+
if (/^#{2,3}\s+TC-\d+[a-z]?\s*[::]?\s*/.test(line)) {
|
|
9376
|
+
tcHeadingIndex = index;
|
|
9377
|
+
return;
|
|
9378
|
+
}
|
|
9379
|
+
if (tcHeadingIndex < 0) return;
|
|
9380
|
+
const field = /^\*\*(覆盖场景|覆盖功能)\*\*\s*[::]\s*(.*)$/.exec(line);
|
|
9381
|
+
if (field && containsIdToken(line, oldId)) {
|
|
9382
|
+
edits.push({ path, line: index + 1, oldLine: line, newLine: replaceIdToken(line, oldId, newId), kind: "markdown-relation" });
|
|
9383
|
+
}
|
|
9384
|
+
});
|
|
9385
|
+
return edits;
|
|
9386
|
+
}
|
|
9387
|
+
async function walkTextFiles(root) {
|
|
9388
|
+
const found = [];
|
|
9389
|
+
async function visit(dir) {
|
|
9390
|
+
const entries = await readdir4(dir, { withFileTypes: true });
|
|
9391
|
+
for (const entry of entries) {
|
|
9392
|
+
if (entry.name.startsWith(".") && PROSE_SCAN_SKIP_DIRS.has(entry.name)) continue;
|
|
9393
|
+
if (PROSE_SCAN_SKIP_DIRS.has(entry.name)) continue;
|
|
9394
|
+
const full = join10(dir, entry.name);
|
|
9395
|
+
if (entry.isDirectory()) {
|
|
9396
|
+
await visit(full);
|
|
9397
|
+
} else if (entry.isFile()) {
|
|
9398
|
+
const ext = entry.name.includes(".") ? `.${entry.name.split(".").slice(-1)[0]}` : "";
|
|
9399
|
+
if (TEXT_EXTENSIONS.has(ext.toLowerCase())) {
|
|
9400
|
+
found.push(relative4(root, full).replace(/\\/g, "/"));
|
|
9401
|
+
}
|
|
9402
|
+
}
|
|
9403
|
+
}
|
|
9404
|
+
}
|
|
9405
|
+
await visit(root);
|
|
9406
|
+
return found.sort();
|
|
9407
|
+
}
|
|
9408
|
+
async function computeLockImpact(root, type, oldId) {
|
|
9409
|
+
const lockPath = join10(root, VERSION_LOCK_PATH);
|
|
9410
|
+
const impact = {
|
|
9411
|
+
lockPath: VERSION_LOCK_PATH,
|
|
9412
|
+
affectedImplementationLocks: [],
|
|
9413
|
+
affectedRelationLocks: [],
|
|
9414
|
+
suggestion: "artifact-graph version-lock refresh --root . --changed-only --worktree"
|
|
9415
|
+
};
|
|
9416
|
+
let raw;
|
|
9417
|
+
try {
|
|
9418
|
+
raw = await readFile4(lockPath, "utf-8");
|
|
9419
|
+
} catch {
|
|
9420
|
+
return impact;
|
|
9421
|
+
}
|
|
9422
|
+
let lockFile;
|
|
9423
|
+
try {
|
|
9424
|
+
lockFile = JSON.parse(raw);
|
|
9425
|
+
} catch {
|
|
9426
|
+
return impact;
|
|
9427
|
+
}
|
|
9428
|
+
for (const lock of lockFile.locks ?? []) {
|
|
9429
|
+
if (lock.artifact && lock.artifact.type === type && lock.artifact.id === oldId) {
|
|
9430
|
+
impact.affectedImplementationLocks.push(lock.edgeId);
|
|
9431
|
+
}
|
|
9432
|
+
}
|
|
9433
|
+
for (const relation of lockFile.artifactRelations ?? []) {
|
|
9434
|
+
const endpoints = [relation.source, relation.target];
|
|
9435
|
+
if (endpoints.some((endpoint) => endpoint && endpoint.type === type && endpoint.id === oldId)) {
|
|
9436
|
+
impact.affectedRelationLocks.push(relation.edgeId);
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
impact.affectedImplementationLocks.sort();
|
|
9440
|
+
impact.affectedRelationLocks.sort();
|
|
9441
|
+
return impact;
|
|
9442
|
+
}
|
|
9443
|
+
async function refactorId(options) {
|
|
9444
|
+
const root = options.root;
|
|
9445
|
+
const fail = (code, message, details) => ({
|
|
9446
|
+
ok: false,
|
|
9447
|
+
error: { code, message, ...details !== void 0 ? { details } : {} }
|
|
9448
|
+
});
|
|
9449
|
+
const colonIndex = options.from.indexOf(":");
|
|
9450
|
+
const config = await loadConfig(root);
|
|
9451
|
+
if (colonIndex <= 0) {
|
|
9452
|
+
const graph2 = await scanArtifacts(root, config);
|
|
9453
|
+
const candidates = graph2.nodes.filter((node) => node.code === options.from).map((node) => `${node.type}:${node.code}`);
|
|
9454
|
+
if (candidates.length === 0) {
|
|
9455
|
+
return fail("TARGET_MISSING", `\u65E7\u6807\u8BC6 "${options.from}" \u4E0D\u5B58\u5728\uFF1B--from \u5FC5\u987B\u4F7F\u7528\u7CBE\u786E type:id \u5F62\u5F0F`);
|
|
9456
|
+
}
|
|
9457
|
+
return fail("BARE_ID_AMBIGUOUS", `--from \u5FC5\u987B\u4F7F\u7528\u7CBE\u786E type:id \u5F62\u5F0F\uFF1B\u88F8\u7F16\u53F7 "${options.from}" \u5019\u9009\uFF1A${candidates.join(", ")}`, { candidates });
|
|
9458
|
+
}
|
|
9459
|
+
const typeToken = options.from.slice(0, colonIndex);
|
|
9460
|
+
const oldId = options.from.slice(colonIndex + 1);
|
|
9461
|
+
const resolvedType = resolveArtifactTypeName(config, typeToken);
|
|
9462
|
+
if (!resolvedType) {
|
|
9463
|
+
return fail("UNKNOWN_TYPE", `\u672A\u77E5\u5236\u54C1\u7C7B\u578B "${typeToken}"`);
|
|
9464
|
+
}
|
|
9465
|
+
if (!oldId) {
|
|
9466
|
+
return fail("TARGET_MISSING", `--from \u7F3A\u5C11 ID \u90E8\u5206\uFF1A${options.from}`);
|
|
9467
|
+
}
|
|
9468
|
+
if (resolvedType !== "feature" && resolvedType !== "decision" && resolvedType !== "scenario") {
|
|
9469
|
+
return fail(
|
|
9470
|
+
"UNSUPPORTED_RENAME_TARGET",
|
|
9471
|
+
`\u9996\u7248\u53EA\u652F\u6301 frontmatter/\u8282\u6807\u9898\u627F\u8F7D ID \u7684\u7C7B\u578B\u6539\u540D\uFF08feature/decision/scenario\uFF09\uFF1B${resolvedType} \u7684 ID \u7531\u8DEF\u5F84\u6D3E\u751F\uFF0C\u6539\u540D\u7B49\u4EF7\u4E8E\u6587\u4EF6\u6539\u540D\uFF0C\u4E0D\u5728\u9996\u7248\u8303\u56F4`
|
|
9472
|
+
);
|
|
9473
|
+
}
|
|
9474
|
+
const newId = options.to.trim();
|
|
9475
|
+
if (!newId || newId.includes(":")) {
|
|
9476
|
+
return fail("INVALID_NEW_ID", "--to \u5FC5\u987B\u662F\u4E0D\u542B\u7C7B\u578B\u524D\u7F00\u7684\u65B0\u7F16\u53F7");
|
|
9477
|
+
}
|
|
9478
|
+
const idPattern = config.idPatterns?.[resolvedType];
|
|
9479
|
+
if (idPattern && !new RegExp(idPattern).test(newId)) {
|
|
9480
|
+
return fail("INVALID_NEW_ID", `\u65B0 ID "${newId}" \u4E0D\u7B26\u5408 ${resolvedType} \u7684 idPattern\uFF1A${idPattern}`);
|
|
9481
|
+
}
|
|
9482
|
+
if (newId === oldId) {
|
|
9483
|
+
return fail("INVALID_NEW_ID", "\u65B0 ID \u4E0E\u65E7 ID \u76F8\u540C");
|
|
9484
|
+
}
|
|
9485
|
+
const graph = await scanArtifacts(root, config);
|
|
9486
|
+
const oldUid = `${resolvedType}:${oldId}`;
|
|
9487
|
+
const newUid = `${resolvedType}:${newId}`;
|
|
9488
|
+
const oldNode = graph.nodes.find((node) => node.uid === oldUid);
|
|
9489
|
+
if (!oldNode) {
|
|
9490
|
+
return fail("TARGET_MISSING", `\u65E7\u6807\u8BC6 ${oldUid} \u5728\u56FE\u4E2D\u4E0D\u5B58\u5728`);
|
|
9491
|
+
}
|
|
9492
|
+
if (graph.nodes.some((node) => node.uid === newUid)) {
|
|
9493
|
+
return fail("ID_CONFLICT", `\u65B0 ID ${newUid} \u5728\u56FE\u4E2D\u5DF2\u5B58\u5728`);
|
|
9494
|
+
}
|
|
9495
|
+
const incoming = graph.edges.filter((edge2) => edge2.to === oldUid);
|
|
9496
|
+
const outgoing = graph.edges.filter((edge2) => edge2.from === oldUid);
|
|
9497
|
+
const edits = [];
|
|
9498
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
9499
|
+
const readArtifactFile = async (path) => {
|
|
9500
|
+
if (!fileContents.has(path)) {
|
|
9501
|
+
fileContents.set(path, await readFile4(join10(root, path), "utf-8"));
|
|
9502
|
+
}
|
|
9503
|
+
return fileContents.get(path);
|
|
9504
|
+
};
|
|
9505
|
+
if (resolvedType === "scenario") {
|
|
9506
|
+
const raw = await readArtifactFile(oldNode.path);
|
|
9507
|
+
const lines = raw.split(/\r?\n/);
|
|
9508
|
+
const headingIndex = lines.findIndex((line) => {
|
|
9509
|
+
const match = /^#{2,3}\s+(S-\d+[a-z]?)\s*[::]/.exec(line);
|
|
9510
|
+
return match !== null && match[1] === oldId;
|
|
9511
|
+
});
|
|
9512
|
+
if (headingIndex < 0) {
|
|
9513
|
+
return fail("PLAN_DRIFT", `\u573A\u666F ${oldUid} \u7684\u8282\u6807\u9898\u5728 ${oldNode.path} \u4E2D\u672A\u627E\u5230`);
|
|
9514
|
+
}
|
|
9515
|
+
edits.push({
|
|
9516
|
+
path: oldNode.path,
|
|
9517
|
+
line: headingIndex + 1,
|
|
9518
|
+
oldLine: lines[headingIndex],
|
|
9519
|
+
newLine: replaceIdToken(lines[headingIndex], oldId, newId),
|
|
9520
|
+
kind: "frontmatter-id"
|
|
9521
|
+
});
|
|
9522
|
+
} else {
|
|
9523
|
+
edits.push(...planFrontmatterEdits(oldNode.path, await readArtifactFile(oldNode.path), resolvedType, oldId, newId, config, true));
|
|
9524
|
+
}
|
|
9525
|
+
const frontmatterTargets = /* @__PURE__ */ new Set();
|
|
9526
|
+
const lineEditsByFile = /* @__PURE__ */ new Map();
|
|
9527
|
+
for (const edge2 of incoming) {
|
|
9528
|
+
if (edge2.source === "frontmatter") {
|
|
9529
|
+
frontmatterTargets.add(edge2.sourcePath);
|
|
9530
|
+
continue;
|
|
9531
|
+
}
|
|
9532
|
+
const lines = lineEditsByFile.get(edge2.sourcePath) ?? /* @__PURE__ */ new Set();
|
|
9533
|
+
lines.add(edge2.sourceLine);
|
|
9534
|
+
lineEditsByFile.set(edge2.sourcePath, lines);
|
|
9535
|
+
}
|
|
9536
|
+
for (const path of [...frontmatterTargets].sort()) {
|
|
9537
|
+
if (path === oldNode.path) continue;
|
|
9538
|
+
const fileType = artifactTypeOfPath(path, config) ?? resolvedType;
|
|
9539
|
+
edits.push(...planFrontmatterEdits(path, await readArtifactFile(path), fileType, oldId, newId, config, false));
|
|
9540
|
+
}
|
|
9541
|
+
for (const [path, lineNumbers] of [...lineEditsByFile.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
9542
|
+
const raw = await readArtifactFile(path);
|
|
9543
|
+
const lines = raw.split(/\r?\n/);
|
|
9544
|
+
const isE2eFile = artifactTypeOfPath(path, config) === "e2e_test";
|
|
9545
|
+
if (isE2eFile) {
|
|
9546
|
+
edits.push(...planE2eBlockEdits(path, raw, oldId, newId));
|
|
9547
|
+
for (const lineNumber of [...lineNumbers].sort((a, b) => a - b)) {
|
|
9548
|
+
const line = lines[lineNumber - 1];
|
|
9549
|
+
if (line === void 0) continue;
|
|
9550
|
+
if (/^#{2,3}\s+TC-\d+/.test(line)) continue;
|
|
9551
|
+
if (containsIdToken(line, oldId)) {
|
|
9552
|
+
edits.push({ path, line: lineNumber, oldLine: line, newLine: replaceIdToken(line, oldId, newId), kind: "markdown-relation" });
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
continue;
|
|
9556
|
+
}
|
|
9557
|
+
for (const lineNumber of [...lineNumbers].sort((a, b) => a - b)) {
|
|
9558
|
+
const line = lines[lineNumber - 1];
|
|
9559
|
+
if (line === void 0 || !containsIdToken(line, oldId)) continue;
|
|
9560
|
+
const isComment = /^\s*(\/\/|<!--)/.test(line);
|
|
9561
|
+
edits.push({
|
|
9562
|
+
path,
|
|
9563
|
+
line: lineNumber,
|
|
9564
|
+
oldLine: line,
|
|
9565
|
+
newLine: replaceIdToken(line, oldId, newId),
|
|
9566
|
+
kind: isComment ? "traceability-comment" : "markdown-relation"
|
|
9567
|
+
});
|
|
9568
|
+
}
|
|
9569
|
+
}
|
|
9570
|
+
edits.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
|
|
9571
|
+
const structuredPositions = new Set(edits.map((edit) => `${edit.path}:${edit.line}`));
|
|
9572
|
+
const proseHits = [];
|
|
9573
|
+
const approveSet = new Set((options.approveProse ?? []).map((entry) => entry.trim()));
|
|
9574
|
+
for (const relativePath of await walkTextFiles(root)) {
|
|
9575
|
+
if (relativePath === VERSION_LOCK_PATH) continue;
|
|
9576
|
+
if (relativePath === "artifact-graph.config.yaml") continue;
|
|
9577
|
+
const raw = await readFile4(join10(root, relativePath), "utf-8");
|
|
9578
|
+
if (!containsIdToken(raw, oldId)) continue;
|
|
9579
|
+
const lines = raw.split(/\r?\n/);
|
|
9580
|
+
lines.forEach((line, index) => {
|
|
9581
|
+
if (!containsIdToken(line, oldId)) return;
|
|
9582
|
+
if (structuredPositions.has(`${relativePath}:${index + 1}`)) return;
|
|
9583
|
+
proseHits.push({ path: relativePath, line: index + 1, context: line.trim().slice(0, 160), approved: false });
|
|
9584
|
+
});
|
|
9585
|
+
}
|
|
9586
|
+
proseHits.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
|
|
9587
|
+
const proseKeys = new Set(proseHits.map((hit) => `${hit.path}:${hit.line}`));
|
|
9588
|
+
const unknownApprovals = [...approveSet].filter((entry) => entry && !proseKeys.has(entry)).sort();
|
|
9589
|
+
if (unknownApprovals.length > 0) {
|
|
9590
|
+
return fail("APPROVE_PROSE_MISMATCH", "--approve-prose \u5305\u542B\u4E0E\u5B9E\u9645 prose \u547D\u4E2D\u4E0D\u5339\u914D\u7684\u6761\u76EE", { unknownApprovals });
|
|
9591
|
+
}
|
|
9592
|
+
for (const hit of proseHits) {
|
|
9593
|
+
hit.approved = approveSet.has(`${hit.path}:${hit.line}`);
|
|
9594
|
+
if (hit.approved) {
|
|
9595
|
+
const raw = await readArtifactFile(hit.path);
|
|
9596
|
+
const oldLine = raw.split(/\r?\n/)[hit.line - 1] ?? "";
|
|
9597
|
+
edits.push({
|
|
9598
|
+
path: hit.path,
|
|
9599
|
+
line: hit.line,
|
|
9600
|
+
oldLine,
|
|
9601
|
+
newLine: replaceIdToken(oldLine, oldId, newId),
|
|
9602
|
+
kind: "prose-approved"
|
|
9603
|
+
});
|
|
9604
|
+
}
|
|
9605
|
+
}
|
|
9606
|
+
edits.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line);
|
|
9607
|
+
const lockImpact = await computeLockImpact(root, resolvedType, oldId);
|
|
9608
|
+
const targetFiles = [...new Set(edits.map((edit) => edit.path))].sort();
|
|
9609
|
+
const plan = {
|
|
9610
|
+
from: oldUid,
|
|
9611
|
+
to: newUid,
|
|
9612
|
+
type: resolvedType,
|
|
9613
|
+
oldSummary: { type: resolvedType, id: oldId, title: oldNode.title, status: oldNode.status, path: oldNode.path },
|
|
9614
|
+
targetFiles,
|
|
9615
|
+
edits,
|
|
9616
|
+
incomingEdges: sortEdges(incoming),
|
|
9617
|
+
outgoingEdges: sortEdges(outgoing),
|
|
9618
|
+
proseHits,
|
|
9619
|
+
lockImpact
|
|
9620
|
+
};
|
|
9621
|
+
if (!options.apply) {
|
|
9622
|
+
return { ok: true, mode: "dry-run", plan };
|
|
9623
|
+
}
|
|
9624
|
+
const newContents = /* @__PURE__ */ new Map();
|
|
9625
|
+
for (const path of targetFiles) {
|
|
9626
|
+
const original = fileContents.has(path) ? fileContents.get(path) : await readArtifactFile(path);
|
|
9627
|
+
const linesWithEndings = splitLinesPreserveEndings(original);
|
|
9628
|
+
const fileEdits = edits.filter((edit) => edit.path === path);
|
|
9629
|
+
for (const edit of fileEdits) {
|
|
9630
|
+
const currentIndex = edit.line - 1;
|
|
9631
|
+
const element = linesWithEndings[currentIndex];
|
|
9632
|
+
if (element === void 0) {
|
|
9633
|
+
return fail("PLAN_DRIFT", `\u8BA1\u5212\u4E0E\u6587\u4EF6\u5185\u5BB9\u4E0D\u4E00\u81F4\uFF08apply \u4E2D\u6B62\uFF0C\u96F6\u5199\u5165\uFF09\uFF1A${path}:${edit.line}`, { path, line: edit.line });
|
|
9634
|
+
}
|
|
9635
|
+
const ending = lineEndingOf(element);
|
|
9636
|
+
const content = element.slice(0, element.length - ending.length);
|
|
9637
|
+
if (content !== edit.oldLine) {
|
|
9638
|
+
return fail("PLAN_DRIFT", `\u8BA1\u5212\u4E0E\u6587\u4EF6\u5185\u5BB9\u4E0D\u4E00\u81F4\uFF08apply \u4E2D\u6B62\uFF0C\u96F6\u5199\u5165\uFF09\uFF1A${path}:${edit.line}`, { path, line: edit.line });
|
|
9639
|
+
}
|
|
9640
|
+
linesWithEndings[currentIndex] = `${edit.newLine}${ending}`;
|
|
9641
|
+
}
|
|
9642
|
+
const updated = linesWithEndings.join("");
|
|
9643
|
+
for (const edit of fileEdits) {
|
|
9644
|
+
const applied = updated.split(/\r?\n/)[edit.line - 1];
|
|
9645
|
+
if (containsIdToken(applied ?? "", oldId)) {
|
|
9646
|
+
return fail("WHITELIST_RESIDUE", `\u767D\u540D\u5355\u4F4D\u7F6E\u66FF\u6362\u6821\u9A8C\u5931\u8D25\uFF08apply \u4E2D\u6B62\uFF0C\u96F6\u5199\u5165\uFF09\uFF1A${path}:${edit.line}`);
|
|
9647
|
+
}
|
|
9648
|
+
}
|
|
9649
|
+
newContents.set(path, updated);
|
|
9650
|
+
}
|
|
9651
|
+
if (options.__beforeWriteVerify) await options.__beforeWriteVerify();
|
|
9652
|
+
for (const path of targetFiles) {
|
|
9653
|
+
const disk = await readFile4(join10(root, path), "utf-8");
|
|
9654
|
+
if (disk !== fileContents.get(path)) {
|
|
9655
|
+
return fail("PLAN_DRIFT", `\u8BA1\u5212\u4E0E\u5199\u5165\u4E4B\u95F4\u76EE\u6807\u6587\u4EF6\u53D1\u751F\u53D8\u5316\uFF08apply \u4E2D\u6B62\uFF0C\u96F6\u5199\u5165\uFF09\uFF1A${path}`, { path });
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
const written = [];
|
|
9659
|
+
const createdTempPaths = [];
|
|
9660
|
+
let tempSequence = 0;
|
|
9661
|
+
const tempPathFor = options.__tempPathFor ?? ((targetPath) => {
|
|
9662
|
+
tempSequence += 1;
|
|
9663
|
+
const suffix = randomBytes(4).toString("hex");
|
|
9664
|
+
return join10(dirname6(targetPath), `.${basename5(targetPath)}.${process.pid}.${tempSequence}.${suffix}.refactor-id.tmp`);
|
|
9665
|
+
});
|
|
9666
|
+
let stage = "write";
|
|
9667
|
+
try {
|
|
9668
|
+
for (const path of targetFiles) {
|
|
9669
|
+
stage = "write";
|
|
9670
|
+
if (options.__injectWriteFailure?.(path)) {
|
|
9671
|
+
throw Object.assign(new Error(`injected write failure: ${path}`), { code: "EINJECTED" });
|
|
9672
|
+
}
|
|
9673
|
+
const original = fileContents.has(path) ? fileContents.get(path) : await readFile4(join10(root, path), "utf-8");
|
|
9674
|
+
const targetPath = join10(root, path);
|
|
9675
|
+
const originalMode = (await stat(targetPath)).mode & 4095;
|
|
9676
|
+
const tempPath = tempPathFor(targetPath);
|
|
9677
|
+
await writeFile5(tempPath, newContents.get(path), { encoding: "utf-8", flag: "wx" });
|
|
9678
|
+
createdTempPaths.push(tempPath);
|
|
9679
|
+
await chmod(tempPath, originalMode);
|
|
9680
|
+
stage = "rename";
|
|
9681
|
+
if (options.__injectRenameFailure?.(tempPath, targetPath)) {
|
|
9682
|
+
throw Object.assign(new Error(`injected rename failure: ${tempPath} -> ${targetPath}`), { code: "ERENAMEINJECTED" });
|
|
9683
|
+
}
|
|
9684
|
+
await rename2(tempPath, targetPath);
|
|
9685
|
+
written.push({ path, original, originalMode });
|
|
9686
|
+
}
|
|
9687
|
+
} catch (error) {
|
|
9688
|
+
for (const tempPath of createdTempPaths) {
|
|
9689
|
+
await rm(tempPath, { force: true });
|
|
9690
|
+
}
|
|
9691
|
+
const rollbackErrors = [];
|
|
9692
|
+
for (const entry of [...written].reverse()) {
|
|
9693
|
+
try {
|
|
9694
|
+
if (options.__injectRollbackFailure?.(entry.path)) {
|
|
9695
|
+
throw Object.assign(new Error(`injected rollback failure: ${entry.path}`), { code: "EROLLBACKINJECTED" });
|
|
9696
|
+
}
|
|
9697
|
+
await writeFile5(join10(root, entry.path), entry.original, "utf-8");
|
|
9698
|
+
await chmod(join10(root, entry.path), entry.originalMode);
|
|
9699
|
+
} catch (rollbackError) {
|
|
9700
|
+
rollbackErrors.push({ path: entry.path, message: rollbackError.message });
|
|
9701
|
+
}
|
|
9702
|
+
}
|
|
9703
|
+
const failure = error;
|
|
9704
|
+
const writeCode = failure.code === "EEXIST" ? "TEMP_FILE_COLLISION" : stage === "rename" || failure.code === "ERENAMEINJECTED" ? "RENAME_FAILED" : "WRITE_FAILED";
|
|
9705
|
+
if (rollbackErrors.length > 0) {
|
|
9706
|
+
return {
|
|
9707
|
+
ok: false,
|
|
9708
|
+
error: {
|
|
9709
|
+
code: "ROLLBACK_FAILED",
|
|
9710
|
+
message: `apply \u5199\u5165\u5931\u8D25\uFF08${writeCode}\uFF09\u4E14\u56DE\u6EDA\u672A\u5B8C\u5168\u6210\u529F\uFF0C\u9700\u4EBA\u5DE5\u590D\u6838\uFF1A${failure.message}`,
|
|
9711
|
+
details: { writeError: { code: writeCode, message: failure.message }, rollbackErrors }
|
|
9712
|
+
}
|
|
9713
|
+
};
|
|
9714
|
+
}
|
|
9715
|
+
return {
|
|
9716
|
+
ok: false,
|
|
9717
|
+
error: {
|
|
9718
|
+
code: writeCode,
|
|
9719
|
+
message: `apply \u5199\u5165\u5931\u8D25\u5E76\u5DF2\u56DE\u6EDA\uFF1A${failure.message}`,
|
|
9720
|
+
details: { stage }
|
|
9721
|
+
}
|
|
9722
|
+
};
|
|
9723
|
+
}
|
|
9724
|
+
const rescanGraph = await scanArtifacts(root, config);
|
|
9725
|
+
const residue = [
|
|
9726
|
+
...rescanGraph.nodes.filter((node) => node.uid === oldUid)
|
|
9727
|
+
];
|
|
9728
|
+
const residueEdges = rescanGraph.edges.filter((edge2) => edge2.to === oldUid || edge2.from === oldUid);
|
|
9729
|
+
const newNodePresent = rescanGraph.nodes.some((node) => node.uid === newUid);
|
|
9730
|
+
const mapEdgeForCompare = (edge2, renameFrom, renameTo) => `${edge2.from === renameFrom ? renameTo : edge2.from}->${edge2.to === renameFrom ? renameTo : edge2.to}:${edge2.kind}:${edge2.source}:${edge2.sourcePath}`;
|
|
9731
|
+
const oldIncomingKeys = sortEdges(incoming).map((edge2) => mapEdgeForCompare(edge2, oldUid, newUid)).sort();
|
|
9732
|
+
const newIncoming = sortEdges(rescanGraph.edges.filter((edge2) => edge2.to === newUid));
|
|
9733
|
+
const newIncomingKeys = newIncoming.map((edge2) => mapEdgeForCompare(edge2, oldUid, newUid)).sort();
|
|
9734
|
+
const incomingDiffs = symmetricDifference(oldIncomingKeys, newIncomingKeys);
|
|
9735
|
+
const oldOutgoingKeys = sortEdges(outgoing).map((edge2) => mapEdgeForCompare(edge2, oldUid, newUid)).sort();
|
|
9736
|
+
const newOutgoing = sortEdges(rescanGraph.edges.filter((edge2) => edge2.from === newUid));
|
|
9737
|
+
const newOutgoingKeys = newOutgoing.map((edge2) => mapEdgeForCompare(edge2, oldUid, newUid)).sort();
|
|
9738
|
+
const outgoingDiffs = symmetricDifference(oldOutgoingKeys, newOutgoingKeys);
|
|
9739
|
+
const structuredResidue = residue.length + residueEdges.length;
|
|
9740
|
+
const rescanResult = {
|
|
9741
|
+
structuredResidue,
|
|
9742
|
+
incomingEquivalence: incomingDiffs.length === 0 ? "equal" : incomingDiffs,
|
|
9743
|
+
outgoingEquivalence: outgoingDiffs.length === 0 ? "equal" : outgoingDiffs,
|
|
9744
|
+
newNodePresent
|
|
9745
|
+
};
|
|
9746
|
+
if (structuredResidue > 0 || incomingDiffs.length > 0 || outgoingDiffs.length > 0 || !newNodePresent) {
|
|
9747
|
+
return {
|
|
9748
|
+
ok: false,
|
|
9749
|
+
error: {
|
|
9750
|
+
code: "RESCAN_FAILED",
|
|
9751
|
+
message: "apply \u5DF2\u5B8C\u6210\u4F46\u6539\u540E\u8BC1\u660E\u672A\u901A\u8FC7\uFF1A\u8BF7\u4EBA\u5DE5\u590D\u6838\u7ED3\u6784\u5316\u6B8B\u7559\u4E0E\u5173\u7CFB\u7B49\u4EF7\u6027",
|
|
9752
|
+
details: rescanResult
|
|
9753
|
+
}
|
|
9754
|
+
};
|
|
9755
|
+
}
|
|
9756
|
+
return { ok: true, mode: "apply", plan, rescan: rescanResult };
|
|
9757
|
+
}
|
|
9758
|
+
function symmetricDifference(left, right) {
|
|
9759
|
+
const leftSet = new Set(left);
|
|
9760
|
+
const rightSet = new Set(right);
|
|
9761
|
+
const diffs = [];
|
|
9762
|
+
for (const item of leftSet) if (!rightSet.has(item)) diffs.push(`- ${item}`);
|
|
9763
|
+
for (const item of rightSet) if (!leftSet.has(item)) diffs.push(`+ ${item}`);
|
|
9764
|
+
return diffs.sort();
|
|
9765
|
+
}
|
|
9766
|
+
function renderRefactorIdMarkdown(result) {
|
|
9767
|
+
if (!result.ok) {
|
|
9768
|
+
return [`# refactor-id \u5931\u8D25`, "", `- \u9519\u8BEF\u7801\uFF1A${result.error.code}`, `- \u8BF4\u660E\uFF1A${result.error.message}`, ""].join("\n");
|
|
9769
|
+
}
|
|
9770
|
+
const { plan } = result;
|
|
9771
|
+
const lines = [];
|
|
9772
|
+
lines.push(`# refactor-id ${result.mode === "apply" ? "apply \u62A5\u544A" : "dry-run \u8BA1\u5212"}`);
|
|
9773
|
+
lines.push("");
|
|
9774
|
+
lines.push(`- \u65E7\u6807\u8BC6\uFF1A\`${plan.from}\``);
|
|
9775
|
+
lines.push(`- \u65B0\u6807\u8BC6\uFF1A\`${plan.to}\``);
|
|
9776
|
+
lines.push(`- \u65E7\u6458\u8981\uFF1A${plan.oldSummary.title}\uFF08status: ${plan.oldSummary.status ?? "\u672A\u58F0\u660E"}\uFF0C\u8DEF\u5F84\uFF1A${plan.oldSummary.path}\uFF09`);
|
|
9777
|
+
lines.push("");
|
|
9778
|
+
lines.push(`## \u76EE\u6807\u6587\u4EF6\uFF08${plan.targetFiles.length}\uFF09`);
|
|
9779
|
+
for (const file of plan.targetFiles) lines.push(`- ${file}`);
|
|
9780
|
+
lines.push("");
|
|
9781
|
+
lines.push(`## \u62DF\u8BAE diff\uFF08${plan.edits.length} \u5904\uFF09`);
|
|
9782
|
+
for (const edit of plan.edits) {
|
|
9783
|
+
lines.push(`- ${edit.path}:${edit.line} [${edit.kind}]`);
|
|
9784
|
+
lines.push(` - \u65E7\uFF1A${edit.oldLine.trim()}`);
|
|
9785
|
+
lines.push(` - \u65B0\uFF1A${edit.newLine.trim()}`);
|
|
9786
|
+
}
|
|
9787
|
+
lines.push("");
|
|
9788
|
+
lines.push(`## \u5165\u8FB9\uFF08${plan.incomingEdges.length}\uFF09`);
|
|
9789
|
+
for (const edge2 of plan.incomingEdges) lines.push(`- ${edge2.from} --${edge2.kind}--> \uFF08${edge2.sourcePath}:${edge2.sourceLine}\uFF0C\u6765\u6E90 ${edge2.source}\uFF09`);
|
|
9790
|
+
lines.push("");
|
|
9791
|
+
lines.push(`## \u51FA\u8FB9\uFF08${plan.outgoingEdges.length}\uFF09`);
|
|
9792
|
+
for (const edge2 of plan.outgoingEdges) lines.push(`- \uFF08${edge2.sourcePath}\uFF09--${edge2.kind}--> ${edge2.to}`);
|
|
9793
|
+
lines.push("");
|
|
9794
|
+
lines.push(`## prose \u7591\u4F3C\u547D\u4E2D\uFF08${plan.proseHits.length}\uFF0C\u9ED8\u8BA4\u53EA\u62A5\u544A\u4E0D\u66FF\u6362\uFF09`);
|
|
9795
|
+
for (const hit of plan.proseHits) {
|
|
9796
|
+
lines.push(`- ${hit.path}:${hit.line}${hit.approved ? "\uFF08\u5DF2\u6279\u51C6\u66FF\u6362\uFF09" : ""}\uFF1A${hit.context}`);
|
|
9797
|
+
}
|
|
9798
|
+
lines.push("");
|
|
9799
|
+
lines.push("## \u7248\u672C\u9501\u5F71\u54CD");
|
|
9800
|
+
lines.push(`- \u53D7\u5F71\u54CD\u5B9E\u73B0\u9501\uFF1A${plan.lockImpact.affectedImplementationLocks.length} \u6761`);
|
|
9801
|
+
lines.push(`- \u53D7\u5F71\u54CD\u5173\u7CFB\u9501\uFF1A${plan.lockImpact.affectedRelationLocks.length} \u6761`);
|
|
9802
|
+
lines.push(`- \u672C\u547D\u4EE4\u4E0D\u81EA\u52A8\u5237\u65B0\u9501\uFF1Bapply \u6210\u529F\u540E\u8BF7\u4EBA\u5DE5\u5BA1\u67E5\u5E76\u8FD0\u884C\uFF1A\`${plan.lockImpact.suggestion}\``);
|
|
9803
|
+
if (result.mode === "apply" && result.rescan) {
|
|
9804
|
+
lines.push("");
|
|
9805
|
+
lines.push("## \u6539\u540E\u8BC1\u660E");
|
|
9806
|
+
lines.push(`- \u65E7 ID \u7ED3\u6784\u5316\u6B8B\u7559\uFF1A${result.rescan.structuredResidue}`);
|
|
9807
|
+
lines.push(`- \u5165\u8FB9\u96C6\u5408\u7B49\u4EF7\uFF1A${result.rescan.incomingEquivalence === "equal" ? "\u662F" : "\u5426"}`);
|
|
9808
|
+
lines.push(`- \u51FA\u8FB9\u96C6\u5408\u7B49\u4EF7\uFF1A${result.rescan.outgoingEquivalence === "equal" ? "\u662F" : "\u5426"}`);
|
|
9809
|
+
lines.push(`- \u65B0\u8282\u70B9\u5B58\u5728\uFF1A${result.rescan.newNodePresent ? "\u662F" : "\u5426"}`);
|
|
9810
|
+
}
|
|
9811
|
+
lines.push("");
|
|
9812
|
+
return lines.join("\n");
|
|
9813
|
+
}
|
|
9814
|
+
var TEXT_EXTENSIONS, PROSE_SCAN_SKIP_DIRS;
|
|
9815
|
+
var init_refactor_id = __esm({
|
|
9816
|
+
"src/refactor-id.ts"() {
|
|
9817
|
+
"use strict";
|
|
9818
|
+
init_index();
|
|
9819
|
+
TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".markdown", ".ts", ".tsx", ".mts", ".mjs", ".js", ".yaml", ".yml", ".json"]);
|
|
9820
|
+
PROSE_SCAN_SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "coverage", ".tmp", ".codex", ".worktrees", ".qoder", ".agents"]);
|
|
9821
|
+
}
|
|
9822
|
+
});
|
|
9823
|
+
|
|
8897
9824
|
// src/cli.ts
|
|
8898
9825
|
var cli_exports = {};
|
|
8899
9826
|
__export(cli_exports, {
|
|
8900
9827
|
runCli: () => runCli
|
|
8901
9828
|
});
|
|
8902
|
-
import
|
|
8903
|
-
import { realpathSync } from "fs";
|
|
8904
|
-
import { access as access2, mkdir as mkdir6, readFile as
|
|
8905
|
-
import { dirname as
|
|
8906
|
-
import { fileURLToPath } from "url";
|
|
9829
|
+
import yaml3 from "js-yaml";
|
|
9830
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
9831
|
+
import { access as access2, mkdir as mkdir6, readFile as readFile5, writeFile as writeFile6 } from "fs/promises";
|
|
9832
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, join as join11 } from "path";
|
|
9833
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8907
9834
|
async function runCli(argv, io = {}) {
|
|
8908
9835
|
const parsed = parseArgs(argv);
|
|
8909
9836
|
const cwd = io.cwd ?? process.cwd();
|
|
8910
9837
|
const out = io.stdout ?? ((chunk) => process.stdout.write(chunk));
|
|
8911
9838
|
const err = io.stderr ?? ((chunk) => process.stderr.write(chunk));
|
|
8912
9839
|
const root = String(parsed.flags.root ?? cwd);
|
|
9840
|
+
const sqliteBindingGate = () => {
|
|
9841
|
+
const runningInstallRoot = runningArtifactGraphInstallRoot();
|
|
9842
|
+
const probe = probeNativeBinding(runningInstallRoot ? { installRoot: runningInstallRoot } : {});
|
|
9843
|
+
if (probe.ok) {
|
|
9844
|
+
return void 0;
|
|
9845
|
+
}
|
|
9846
|
+
err(renderStructuredNativeBindingError(probe, {
|
|
9847
|
+
probedFrom: runningInstallRoot ?? "(current artifact-graph package)"
|
|
9848
|
+
}));
|
|
9849
|
+
return 1;
|
|
9850
|
+
};
|
|
8913
9851
|
try {
|
|
8914
9852
|
const hasHelpFlag = parsed.flags.help === true || parsed.positional.some((p) => p === "--help" || p === "-h");
|
|
8915
9853
|
if (parsed.command === "--help" || parsed.command === "-h" || parsed.command === "help" || hasHelpFlag) {
|
|
@@ -8919,11 +9857,13 @@ async function runCli(argv, io = {}) {
|
|
|
8919
9857
|
switch (parsed.command) {
|
|
8920
9858
|
case "init": {
|
|
8921
9859
|
await initConfig(root);
|
|
8922
|
-
out(`Created ${
|
|
9860
|
+
out(`Created ${join11(root, "artifact-graph.config.yaml")}
|
|
8923
9861
|
`);
|
|
8924
9862
|
return 0;
|
|
8925
9863
|
}
|
|
8926
9864
|
case "scan": {
|
|
9865
|
+
const gate = sqliteBindingGate();
|
|
9866
|
+
if (gate !== void 0) return gate;
|
|
8927
9867
|
const graph = await scanArtifacts(root);
|
|
8928
9868
|
await writeGraphCache(root, graph);
|
|
8929
9869
|
out(`Scanned ${graph.nodes.length} artifacts and ${graph.edges.length} relations
|
|
@@ -9045,6 +9985,8 @@ async function runCli(argv, io = {}) {
|
|
|
9045
9985
|
return issues.some((issue2) => issue2.severity === "error") && !parsed.flags["warning-only"] ? 1 : 0;
|
|
9046
9986
|
}
|
|
9047
9987
|
case "query": {
|
|
9988
|
+
const gate = sqliteBindingGate();
|
|
9989
|
+
if (gate !== void 0) return gate;
|
|
9048
9990
|
const graph = await scanArtifacts(root);
|
|
9049
9991
|
const result = queryGraph(graph, {
|
|
9050
9992
|
from: typeof parsed.flags.from === "string" ? parsed.flags.from : void 0,
|
|
@@ -9170,7 +10112,7 @@ async function runCli(argv, io = {}) {
|
|
|
9170
10112
|
err("\nFix the traceability gaps above before generating an implementation packet.\n");
|
|
9171
10113
|
if (typeof parsed.flags.out === "string") {
|
|
9172
10114
|
const errorReport = JSON.stringify({ error: "missing", missing: manifest.missing }, null, 2);
|
|
9173
|
-
await
|
|
10115
|
+
await writeFile6(parsed.flags.out, errorReport + "\n");
|
|
9174
10116
|
}
|
|
9175
10117
|
return 1;
|
|
9176
10118
|
}
|
|
@@ -9204,7 +10146,7 @@ async function runCli(argv, io = {}) {
|
|
|
9204
10146
|
return 1;
|
|
9205
10147
|
}
|
|
9206
10148
|
if (typeof parsed.flags.out === "string") {
|
|
9207
|
-
await
|
|
10149
|
+
await writeFile6(parsed.flags.out, output);
|
|
9208
10150
|
out(`Packet written to ${parsed.flags.out}
|
|
9209
10151
|
`);
|
|
9210
10152
|
} else {
|
|
@@ -9243,7 +10185,7 @@ async function runCli(argv, io = {}) {
|
|
|
9243
10185
|
if (packetJsonPath) {
|
|
9244
10186
|
let rawJson;
|
|
9245
10187
|
try {
|
|
9246
|
-
rawJson = await
|
|
10188
|
+
rawJson = await readFile5(packetJsonPath, "utf-8");
|
|
9247
10189
|
} catch (readErr) {
|
|
9248
10190
|
err(`\u9519\u8BEF\uFF1A\u65E0\u6CD5\u8BFB\u53D6 packet \u6587\u4EF6: "${packetJsonPath}" \u2014 ${readErr.message}
|
|
9249
10191
|
`);
|
|
@@ -9333,7 +10275,7 @@ async function runCli(argv, io = {}) {
|
|
|
9333
10275
|
`);
|
|
9334
10276
|
}
|
|
9335
10277
|
if (typeof parsed.flags.out === "string") {
|
|
9336
|
-
await
|
|
10278
|
+
await writeFile6(parsed.flags.out, prompt);
|
|
9337
10279
|
out(`Prompt written to ${parsed.flags.out}
|
|
9338
10280
|
`);
|
|
9339
10281
|
} else {
|
|
@@ -9444,7 +10386,7 @@ async function runCli(argv, io = {}) {
|
|
|
9444
10386
|
universalBaseline: discoverConfig.context?.universal_baseline
|
|
9445
10387
|
});
|
|
9446
10388
|
} else {
|
|
9447
|
-
const targetsContent = await
|
|
10389
|
+
const targetsContent = await readFile5(targetsFile, "utf-8");
|
|
9448
10390
|
const auditConfig2 = await loadConfig(root);
|
|
9449
10391
|
const parseResult = parseTargetsFile(targetsContent, auditConfig2);
|
|
9450
10392
|
if (parseResult.errors.length > 0) {
|
|
@@ -9569,7 +10511,7 @@ async function runCli(argv, io = {}) {
|
|
|
9569
10511
|
} else {
|
|
9570
10512
|
let ppaTargetsContent;
|
|
9571
10513
|
try {
|
|
9572
|
-
ppaTargetsContent = await
|
|
10514
|
+
ppaTargetsContent = await readFile5(ppaTargetsFile, "utf-8");
|
|
9573
10515
|
} catch (readErr) {
|
|
9574
10516
|
err(`\u9519\u8BEF\uFF1A\u65E0\u6CD5\u8BFB\u53D6 targets \u6587\u4EF6: "${ppaTargetsFile}" \u2014 ${readErr.message}
|
|
9575
10517
|
`);
|
|
@@ -9648,7 +10590,7 @@ async function runCli(argv, io = {}) {
|
|
|
9648
10590
|
const output = `${JSON.stringify(index, null, 2)}
|
|
9649
10591
|
`;
|
|
9650
10592
|
if (typeof parsed.flags.out === "string") {
|
|
9651
|
-
await
|
|
10593
|
+
await writeFile6(parsed.flags.out, output);
|
|
9652
10594
|
out(`Version index written to ${parsed.flags.out}
|
|
9653
10595
|
`);
|
|
9654
10596
|
} else {
|
|
@@ -9816,8 +10758,8 @@ async function runCli(argv, io = {}) {
|
|
|
9816
10758
|
const hooks = hookFlag === "all" ? ["pre-commit", "pre-push"] : [hookFlag];
|
|
9817
10759
|
const prepared = [];
|
|
9818
10760
|
for (const hookName of hooks) {
|
|
9819
|
-
const templatePath =
|
|
9820
|
-
const block = await
|
|
10761
|
+
const templatePath = fileURLToPath2(new URL(`../templates/git-hooks/${hookName}.sh`, import.meta.url));
|
|
10762
|
+
const block = await readFile5(templatePath, "utf-8");
|
|
9821
10763
|
prepared.push(await prepareManagedHookBlock({
|
|
9822
10764
|
hookPath: await resolveGitHookPath(root, hookName),
|
|
9823
10765
|
block,
|
|
@@ -9850,6 +10792,9 @@ async function runCli(argv, io = {}) {
|
|
|
9850
10792
|
out(`Forbidden edges: ${config.forbiddenEdges.length}
|
|
9851
10793
|
`);
|
|
9852
10794
|
}
|
|
10795
|
+
if (!report.nativeBinding.ok) {
|
|
10796
|
+
return 1;
|
|
10797
|
+
}
|
|
9853
10798
|
return 0;
|
|
9854
10799
|
}
|
|
9855
10800
|
case "validate-review-result": {
|
|
@@ -9858,10 +10803,10 @@ async function runCli(argv, io = {}) {
|
|
|
9858
10803
|
err("Usage: artifact-graph validate-review-result --file <path> [--format json]\n");
|
|
9859
10804
|
return 1;
|
|
9860
10805
|
}
|
|
9861
|
-
const resolvedPath = isAbsolute4(filePath) ? filePath :
|
|
10806
|
+
const resolvedPath = isAbsolute4(filePath) ? filePath : join11(root, filePath);
|
|
9862
10807
|
let content;
|
|
9863
10808
|
try {
|
|
9864
|
-
content = await
|
|
10809
|
+
content = await readFile5(resolvedPath, "utf-8");
|
|
9865
10810
|
} catch (readErr) {
|
|
9866
10811
|
err(`Error: Cannot read file: "${resolvedPath}" \u2014 ${readErr.message}
|
|
9867
10812
|
`);
|
|
@@ -9896,11 +10841,11 @@ async function runCli(argv, io = {}) {
|
|
|
9896
10841
|
const deterministic = checkMode || parsed.flags.deterministic === true;
|
|
9897
10842
|
const registry = await generateE2eRegistry(root, { deterministic });
|
|
9898
10843
|
const output = JSON.stringify(registry, null, 2) + "\n";
|
|
9899
|
-
const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out :
|
|
10844
|
+
const outPath = typeof parsed.flags.out === "string" ? parsed.flags.out : join11(root, "artifacts/tests/e2e/e2e-test-registry.json");
|
|
9900
10845
|
if (checkMode) {
|
|
9901
10846
|
let existing = "";
|
|
9902
10847
|
try {
|
|
9903
|
-
existing = await
|
|
10848
|
+
existing = await readFile5(outPath, "utf-8");
|
|
9904
10849
|
} catch {
|
|
9905
10850
|
err(`Check failed: ${outPath} does not exist or is not readable
|
|
9906
10851
|
`);
|
|
@@ -9916,7 +10861,7 @@ async function runCli(argv, io = {}) {
|
|
|
9916
10861
|
return 0;
|
|
9917
10862
|
}
|
|
9918
10863
|
if (typeof parsed.flags.out === "string") {
|
|
9919
|
-
await
|
|
10864
|
+
await writeFile6(parsed.flags.out, output);
|
|
9920
10865
|
out(`Registry written to ${parsed.flags.out} (${registry.total_batches} batches, ${registry.total_test_cases} TCs)
|
|
9921
10866
|
`);
|
|
9922
10867
|
} else {
|
|
@@ -9932,8 +10877,8 @@ async function runCli(argv, io = {}) {
|
|
|
9932
10877
|
`);
|
|
9933
10878
|
return 1;
|
|
9934
10879
|
}
|
|
9935
|
-
const packageDir =
|
|
9936
|
-
const contractsDir = typeof parsed.flags["contracts-dir"] === "string" ? parsed.flags["contracts-dir"] :
|
|
10880
|
+
const packageDir = dirname7(fileURLToPath2(import.meta.url));
|
|
10881
|
+
const contractsDir = typeof parsed.flags["contracts-dir"] === "string" ? parsed.flags["contracts-dir"] : join11(packageDir, "..", "contracts");
|
|
9937
10882
|
const revisionDigest = typeof parsed.flags["revision-digest"] === "string" ? parsed.flags["revision-digest"] : void 0;
|
|
9938
10883
|
async function resolveContract(contractId) {
|
|
9939
10884
|
const catalog = await loadContractCatalog(contractsDir);
|
|
@@ -10147,7 +11092,7 @@ async function runCli(argv, io = {}) {
|
|
|
10147
11092
|
}
|
|
10148
11093
|
let markdownContent;
|
|
10149
11094
|
try {
|
|
10150
|
-
markdownContent = await
|
|
11095
|
+
markdownContent = await readFile5(markdownPath, "utf-8");
|
|
10151
11096
|
} catch {
|
|
10152
11097
|
out(`${JSON.stringify({ ok: false, error: { code: "SCHEMA_VALIDATION_FAILED", path: "/markdown", message: `Could not read markdown file "${markdownPath}"` } }, null, 2)}
|
|
10153
11098
|
`);
|
|
@@ -10168,6 +11113,52 @@ async function runCli(argv, io = {}) {
|
|
|
10168
11113
|
`);
|
|
10169
11114
|
return 1;
|
|
10170
11115
|
}
|
|
11116
|
+
case "refactor-id": {
|
|
11117
|
+
const { refactorId: refactorId2, renderRefactorIdMarkdown: renderRefactorIdMarkdown2 } = await Promise.resolve().then(() => (init_refactor_id(), refactor_id_exports));
|
|
11118
|
+
if (typeof parsed.flags.format !== "string" && parsed.flags.format !== void 0) {
|
|
11119
|
+
out(`${JSON.stringify({ ok: false, error: { code: "INVALID_FORMAT", path: "/format", message: 'Invalid --format: "" (missing value). Allowed values: json, markdown' } }, null, 2)}
|
|
11120
|
+
`);
|
|
11121
|
+
err('Invalid --format: "" (missing value). Allowed values: json, markdown\n');
|
|
11122
|
+
return 1;
|
|
11123
|
+
}
|
|
11124
|
+
const refactorIdFormat = typeof parsed.flags.format === "string" ? parsed.flags.format : "markdown";
|
|
11125
|
+
if (refactorIdFormat !== "json" && refactorIdFormat !== "markdown") {
|
|
11126
|
+
out(`${JSON.stringify({ ok: false, error: { code: "INVALID_FORMAT", path: "/format", message: `Invalid --format: "${refactorIdFormat}". Allowed values: json, markdown` } }, null, 2)}
|
|
11127
|
+
`);
|
|
11128
|
+
err(`Invalid --format: "${refactorIdFormat}". Allowed values: json, markdown
|
|
11129
|
+
`);
|
|
11130
|
+
return 1;
|
|
11131
|
+
}
|
|
11132
|
+
for (const forbidden of ["force", "all", "recursive"]) {
|
|
11133
|
+
if (parsed.flags[forbidden] !== void 0) {
|
|
11134
|
+
err(`refactor-id \u4E0D\u652F\u6301 --${forbidden}\uFF1A\u9996\u7248\u53EA\u6709\u4E00\u5BF9\u4E00\u6539\u540D\uFF0CD-ACA-29 \u5DF2\u51BB\u7ED3\u547D\u4EE4\u9762
|
|
11135
|
+
`);
|
|
11136
|
+
return 1;
|
|
11137
|
+
}
|
|
11138
|
+
}
|
|
11139
|
+
const from = typeof parsed.flags.from === "string" ? parsed.flags.from : "";
|
|
11140
|
+
const to = typeof parsed.flags.to === "string" ? parsed.flags.to : "";
|
|
11141
|
+
if (!from || !to) {
|
|
11142
|
+
err("Usage: artifact-graph refactor-id --from <type:id> --to <new-id> [--apply] [--approve-prose <file:line,...>] [--format json|markdown]\n");
|
|
11143
|
+
return 1;
|
|
11144
|
+
}
|
|
11145
|
+
const approveProse = typeof parsed.flags["approve-prose"] === "string" ? String(parsed.flags["approve-prose"]).split(",").map((entry) => entry.trim()).filter(Boolean) : [];
|
|
11146
|
+
const result = await refactorId2({
|
|
11147
|
+
root,
|
|
11148
|
+
from,
|
|
11149
|
+
to,
|
|
11150
|
+
apply: parsed.flags.apply === true,
|
|
11151
|
+
approveProse,
|
|
11152
|
+
format: refactorIdFormat
|
|
11153
|
+
});
|
|
11154
|
+
if (refactorIdFormat === "json") {
|
|
11155
|
+
out(`${JSON.stringify(result, null, 2)}
|
|
11156
|
+
`);
|
|
11157
|
+
} else {
|
|
11158
|
+
out(renderRefactorIdMarkdown2(result));
|
|
11159
|
+
}
|
|
11160
|
+
return result.ok ? 0 : 1;
|
|
11161
|
+
}
|
|
10171
11162
|
default:
|
|
10172
11163
|
err(helpText());
|
|
10173
11164
|
return 1;
|
|
@@ -10200,7 +11191,7 @@ function isGraphRelevantPath(path, schema) {
|
|
|
10200
11191
|
return path === "artifact-graph.config.yaml" || path === VERSION_LOCK_PATH || matchesConfiguredArtifactPath(path, schema);
|
|
10201
11192
|
}
|
|
10202
11193
|
async function initConfig(root) {
|
|
10203
|
-
const configPath =
|
|
11194
|
+
const configPath = join11(root, "artifact-graph.config.yaml");
|
|
10204
11195
|
try {
|
|
10205
11196
|
await access2(configPath);
|
|
10206
11197
|
throw new Error(`Config already exists: ${configPath}`);
|
|
@@ -10210,7 +11201,7 @@ async function initConfig(root) {
|
|
|
10210
11201
|
}
|
|
10211
11202
|
}
|
|
10212
11203
|
await mkdir6(root, { recursive: true });
|
|
10213
|
-
await
|
|
11204
|
+
await writeFile6(configPath, yaml3.dump(DEFAULT_SCHEMA, { lineWidth: 120 }));
|
|
10214
11205
|
}
|
|
10215
11206
|
function parseArgs(argv) {
|
|
10216
11207
|
const [command, ...rest] = argv;
|
|
@@ -10268,6 +11259,10 @@ Commands:
|
|
|
10268
11259
|
next-id <type> --range <name>
|
|
10269
11260
|
render [--format mermaid]
|
|
10270
11261
|
doctor [--format json|markdown]
|
|
11262
|
+
refactor-id --from <type:id> --to <new-id> [--apply] [--approve-prose <file:line,...>] [--format json|markdown]
|
|
11263
|
+
\u5B89\u5168\u5236\u54C1\u6807\u8BC6\u4E00\u5BF9\u4E00\u91CD\u6784\uFF1A\u7F3A\u7701 dry-run \u53EA\u8F93\u51FA\u8BA1\u5212\uFF1B--apply \u4EE5\u5355\u4E00\u4E8B\u52A1\u6539\u5199\u7ED3\u6784\u5316
|
|
11264
|
+
\u767D\u540D\u5355\u4F4D\u7F6E\u5E76\u81EA\u52A8 rescan \u8BC1\u660E\u3002\u9501\u4E0D\u81EA\u52A8\u5237\u65B0\uFF0Capply \u540E\u6309\u63D0\u793A\u8FD0\u884C
|
|
11265
|
+
version-lock refresh --changed-only --worktree\u3002
|
|
10271
11266
|
validate-review-result --file <path> [--format json]
|
|
10272
11267
|
generate-e2e-registry [--deterministic] [--out <path>] [--check]
|
|
10273
11268
|
contract list [--contracts-dir <path>] [--format json]
|
|
@@ -10283,7 +11278,7 @@ function isCliEntrypoint(argvPath) {
|
|
|
10283
11278
|
return false;
|
|
10284
11279
|
}
|
|
10285
11280
|
try {
|
|
10286
|
-
return
|
|
11281
|
+
return realpathSync2(argvPath) === realpathSync2(fileURLToPath2(import.meta.url));
|
|
10287
11282
|
} catch {
|
|
10288
11283
|
return false;
|
|
10289
11284
|
}
|
|
@@ -10299,6 +11294,7 @@ var init_cli = __esm({
|
|
|
10299
11294
|
init_packet_prompt_validator();
|
|
10300
11295
|
init_packet_prompt_audit();
|
|
10301
11296
|
init_cli_resolver();
|
|
11297
|
+
init_native_binding_diagnostics();
|
|
10302
11298
|
init_review_result_validator();
|
|
10303
11299
|
init_git_changes();
|
|
10304
11300
|
init_git_hook_path();
|