mdkg 0.3.2 → 0.3.4
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 +74 -0
- package/CLI_COMMAND_MATRIX.md +28 -11
- package/README.md +10 -6
- package/dist/cli.js +87 -10
- package/dist/command-contract.json +406 -11
- package/dist/commands/fix.js +468 -16
- package/dist/commands/goal.js +148 -12
- package/dist/commands/new.js +4 -3
- package/dist/graph/node.js +4 -3
- package/dist/graph/validate_graph.js +21 -0
- package/dist/init/AGENT_START.md +2 -2
- package/dist/init/CLI_COMMAND_MATRIX.md +15 -4
- package/dist/init/README.md +8 -3
- package/dist/init/init-manifest.json +5 -5
- package/dist/init/skills/default/pursue-mdkg-goal/SKILL.md +2 -1
- package/package.json +4 -2
package/dist/commands/fix.js
CHANGED
|
@@ -4,7 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.collectFixPlan = collectFixPlan;
|
|
7
|
+
exports.collectFixApply = collectFixApply;
|
|
7
8
|
exports.runFixPlanCommand = runFixPlanCommand;
|
|
9
|
+
exports.runFixApplyCommand = runFixApplyCommand;
|
|
10
|
+
exports.runFixIdsCommand = runFixIdsCommand;
|
|
8
11
|
const crypto_1 = __importDefault(require("crypto"));
|
|
9
12
|
const child_process_1 = require("child_process");
|
|
10
13
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -23,6 +26,9 @@ const template_schema_1 = require("../graph/template_schema");
|
|
|
23
26
|
const workspace_files_1 = require("../graph/workspace_files");
|
|
24
27
|
const errors_1 = require("../util/errors");
|
|
25
28
|
const refs_1 = require("../util/refs");
|
|
29
|
+
const atomic_1 = require("../util/atomic");
|
|
30
|
+
const lock_1 = require("../util/lock");
|
|
31
|
+
const index_1 = require("./index");
|
|
26
32
|
const FAMILY_VALUES = new Set(["index", "refs", "ids", "all"]);
|
|
27
33
|
const CONCRETE_FAMILIES = ["index", "refs", "ids"];
|
|
28
34
|
function stableValue(value) {
|
|
@@ -668,14 +674,143 @@ function planRefRepairs(root, target) {
|
|
|
668
674
|
return { proposed: changes, blocked: [] };
|
|
669
675
|
}
|
|
670
676
|
function candidateDuplicateId(baseId, used) {
|
|
671
|
-
|
|
672
|
-
|
|
677
|
+
const match = /^([a-z]+)-([0-9]+)$/.exec(baseId);
|
|
678
|
+
if (!match) {
|
|
679
|
+
throw new errors_1.UsageError(`duplicate id ${baseId} cannot be repaired automatically because it is not a canonical numeric id`);
|
|
680
|
+
}
|
|
681
|
+
const prefix = match[1];
|
|
682
|
+
const start = Number.parseInt(match[2], 10) + 1;
|
|
683
|
+
for (let index = start;; index += 1) {
|
|
684
|
+
const candidate = `${prefix}-${index}`;
|
|
673
685
|
if (!used.has(candidate)) {
|
|
674
686
|
used.add(candidate);
|
|
675
687
|
return candidate;
|
|
676
688
|
}
|
|
677
689
|
}
|
|
678
690
|
}
|
|
691
|
+
function gitShow(root, refPath) {
|
|
692
|
+
const result = (0, child_process_1.spawnSync)("git", ["show", refPath], { cwd: root, encoding: "utf8" });
|
|
693
|
+
if (result.status !== 0) {
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
return result.stdout;
|
|
697
|
+
}
|
|
698
|
+
function runGitStrict(root, args) {
|
|
699
|
+
const result = (0, child_process_1.spawnSync)("git", args, { cwd: root, encoding: "utf8" });
|
|
700
|
+
if (result.status !== 0) {
|
|
701
|
+
throw new errors_1.UsageError(`git ${args.join(" ")} failed: ${(result.stderr || result.stdout).trim() || "unknown error"}`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function baseRefIdPaths(root, baseRef, files, config) {
|
|
705
|
+
const pathsById = new Map();
|
|
706
|
+
if (!baseRef) {
|
|
707
|
+
return pathsById;
|
|
708
|
+
}
|
|
709
|
+
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES);
|
|
710
|
+
for (const absPath of files) {
|
|
711
|
+
const relativePath = rel(root, absPath);
|
|
712
|
+
const content = gitShow(root, `${baseRef}:${relativePath}`);
|
|
713
|
+
if (content === undefined) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const node = (0, node_1.parseNode)(content, absPath, {
|
|
718
|
+
workStatusEnum: config.work.status_enum,
|
|
719
|
+
priorityMin: config.work.priority_min,
|
|
720
|
+
priorityMax: config.work.priority_max,
|
|
721
|
+
templateSchemas,
|
|
722
|
+
});
|
|
723
|
+
if (!pathsById.has(node.id)) {
|
|
724
|
+
pathsById.set(node.id, new Set());
|
|
725
|
+
}
|
|
726
|
+
pathsById.get(node.id)?.add(relativePath);
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return pathsById;
|
|
733
|
+
}
|
|
734
|
+
function rewriteIdInNodeContent(content, fromId, toId) {
|
|
735
|
+
const lines = content.split(/\n/);
|
|
736
|
+
let inFrontmatter = false;
|
|
737
|
+
let frontmatterClosed = false;
|
|
738
|
+
let idRewritten = false;
|
|
739
|
+
const rewritten = lines.map((line, index) => {
|
|
740
|
+
if (index === 0 && line.trim() === "---") {
|
|
741
|
+
inFrontmatter = true;
|
|
742
|
+
return line;
|
|
743
|
+
}
|
|
744
|
+
if (inFrontmatter && !frontmatterClosed && line.trim() === "---") {
|
|
745
|
+
frontmatterClosed = true;
|
|
746
|
+
inFrontmatter = false;
|
|
747
|
+
return line;
|
|
748
|
+
}
|
|
749
|
+
if (inFrontmatter && !idRewritten && line === `id: ${fromId}`) {
|
|
750
|
+
idRewritten = true;
|
|
751
|
+
return `id: ${toId}`;
|
|
752
|
+
}
|
|
753
|
+
return line.split(fromId).join(toId);
|
|
754
|
+
});
|
|
755
|
+
if (!idRewritten) {
|
|
756
|
+
throw new errors_1.UsageError(`unable to rewrite id ${fromId}; frontmatter id line not found`);
|
|
757
|
+
}
|
|
758
|
+
return rewritten.join("\n");
|
|
759
|
+
}
|
|
760
|
+
function isInsideRoot(root, filePath) {
|
|
761
|
+
const realRoot = `${relativeRoot(root)}${path_1.default.sep}`;
|
|
762
|
+
return path_1.default.resolve(filePath).startsWith(realRoot);
|
|
763
|
+
}
|
|
764
|
+
function replaceIdInRelativePath(relativePath, fromId, toId, usedPaths) {
|
|
765
|
+
const parsed = path_1.default.posix.parse(relativePath);
|
|
766
|
+
const baseName = parsed.base.includes(fromId)
|
|
767
|
+
? parsed.base.replace(fromId, toId)
|
|
768
|
+
: `${toId}-${parsed.base}`;
|
|
769
|
+
let candidate = path_1.default.posix.join(parsed.dir, baseName);
|
|
770
|
+
if (!usedPaths.has(candidate)) {
|
|
771
|
+
usedPaths.add(candidate);
|
|
772
|
+
return candidate;
|
|
773
|
+
}
|
|
774
|
+
for (let index = 2;; index += 1) {
|
|
775
|
+
const suffixed = path_1.default.posix.join(parsed.dir, `${parsed.name}-${toId}-${index}${parsed.ext}`);
|
|
776
|
+
if (!usedPaths.has(suffixed)) {
|
|
777
|
+
usedPaths.add(suffixed);
|
|
778
|
+
return suffixed;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function gitConflictStages(root) {
|
|
783
|
+
const output = runGit(root, ["ls-files", "-u", "--", ".mdkg"]) ?? "";
|
|
784
|
+
const groups = new Map();
|
|
785
|
+
for (const line of output.split(/\r?\n/).filter(Boolean)) {
|
|
786
|
+
const match = /^([0-7]+) ([0-9a-f]+) ([123])\t(.+)$/.exec(line);
|
|
787
|
+
if (!match || !match[4].endsWith(".md")) {
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
const entry = {
|
|
791
|
+
mode: match[1],
|
|
792
|
+
object: match[2],
|
|
793
|
+
stage: Number.parseInt(match[3], 10),
|
|
794
|
+
path: match[4],
|
|
795
|
+
};
|
|
796
|
+
groups.set(entry.path, [...(groups.get(entry.path) ?? []), entry]);
|
|
797
|
+
}
|
|
798
|
+
return groups;
|
|
799
|
+
}
|
|
800
|
+
function workspaceAliasForPath(root, config, relativePath) {
|
|
801
|
+
const absPath = path_1.default.resolve(root, relativePath);
|
|
802
|
+
for (const alias of Object.keys(config.workspaces).sort()) {
|
|
803
|
+
const entry = config.workspaces[alias];
|
|
804
|
+
if (!entry.enabled) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
const wsRoot = path_1.default.resolve(root, entry.path, entry.mdkg_dir);
|
|
808
|
+
if (absPath === wsRoot || absPath.startsWith(`${wsRoot}${path_1.default.sep}`)) {
|
|
809
|
+
return alias;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
679
814
|
function filesContaining(root, files, needle) {
|
|
680
815
|
return files
|
|
681
816
|
.filter((filePath) => {
|
|
@@ -728,18 +863,118 @@ function referenceRewriteItems(root, files, from, to) {
|
|
|
728
863
|
.filter((item) => Boolean(item))
|
|
729
864
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
730
865
|
}
|
|
731
|
-
function
|
|
866
|
+
function planGitStageDuplicateIdRepairs(root, target, config, usedIdsByAlias, usedPaths, startIndex) {
|
|
867
|
+
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES);
|
|
868
|
+
const proposed = [];
|
|
869
|
+
let matchedTarget = false;
|
|
870
|
+
for (const [relativePath, stages] of [...gitConflictStages(root).entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
871
|
+
const ours = stages.find((entry) => entry.stage === 2);
|
|
872
|
+
const theirs = stages.find((entry) => entry.stage === 3);
|
|
873
|
+
if (!ours || !theirs) {
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
const alias = workspaceAliasForPath(root, config, relativePath);
|
|
877
|
+
if (!alias) {
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
const oursContent = gitShow(root, `:2:${relativePath}`);
|
|
881
|
+
const theirsContent = gitShow(root, `:3:${relativePath}`);
|
|
882
|
+
if (oursContent === undefined || theirsContent === undefined) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
const absPath = path_1.default.resolve(root, relativePath);
|
|
887
|
+
const oursNode = (0, node_1.parseNode)(oursContent, absPath, {
|
|
888
|
+
workStatusEnum: config.work.status_enum,
|
|
889
|
+
priorityMin: config.work.priority_min,
|
|
890
|
+
priorityMax: config.work.priority_max,
|
|
891
|
+
templateSchemas,
|
|
892
|
+
});
|
|
893
|
+
const theirsNode = (0, node_1.parseNode)(theirsContent, absPath, {
|
|
894
|
+
workStatusEnum: config.work.status_enum,
|
|
895
|
+
priorityMin: config.work.priority_min,
|
|
896
|
+
priorityMax: config.work.priority_max,
|
|
897
|
+
templateSchemas,
|
|
898
|
+
});
|
|
899
|
+
if (oursNode.id !== theirsNode.id) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
const duplicateId = oursNode.id;
|
|
903
|
+
const qid = `${alias}:${duplicateId}`;
|
|
904
|
+
const targetMatches = !target || target.toLowerCase() === duplicateId || target.toLowerCase() === qid || target === relativePath;
|
|
905
|
+
if (!targetMatches) {
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
matchedTarget = true;
|
|
909
|
+
const usedIds = usedIdsByAlias.get(alias) ?? new Set();
|
|
910
|
+
usedIds.add(oursNode.id);
|
|
911
|
+
usedIds.add(theirsNode.id);
|
|
912
|
+
usedIdsByAlias.set(alias, usedIds);
|
|
913
|
+
const candidate = candidateDuplicateId(duplicateId, usedIds);
|
|
914
|
+
const candidatePath = replaceIdInRelativePath(relativePath, duplicateId, candidate, usedPaths);
|
|
915
|
+
proposed.push({
|
|
916
|
+
id: `ids.${String(startIndex + proposed.length + 1).padStart(3, "0")}`,
|
|
917
|
+
family: "ids",
|
|
918
|
+
risk: "high",
|
|
919
|
+
status: "planned",
|
|
920
|
+
reason: "git_stage_duplicate_id",
|
|
921
|
+
paths: [relativePath, candidatePath],
|
|
922
|
+
refs: [qid, `${alias}:${candidate}`].sort(),
|
|
923
|
+
evidence: {
|
|
924
|
+
conflict_kind: "git_index_unresolved_duplicate_id",
|
|
925
|
+
workspace: alias,
|
|
926
|
+
duplicate_id: duplicateId,
|
|
927
|
+
conflict_path: relativePath,
|
|
928
|
+
canonical_stage: 2,
|
|
929
|
+
duplicate_stage: 3,
|
|
930
|
+
current_blob: ours.object,
|
|
931
|
+
incoming_blob: theirs.object,
|
|
932
|
+
deterministic_rule: "keep stage 2 at the conflicted path, rewrite stage 3 to the next unused canonical numeric id and path, then git add both files",
|
|
933
|
+
},
|
|
934
|
+
before: {
|
|
935
|
+
duplicate_id: duplicateId,
|
|
936
|
+
workspace: alias,
|
|
937
|
+
conflict_path: relativePath,
|
|
938
|
+
canonical_stage: 2,
|
|
939
|
+
duplicate_stage: 3,
|
|
940
|
+
current_blob: ours.object,
|
|
941
|
+
incoming_blob: theirs.object,
|
|
942
|
+
},
|
|
943
|
+
after: {
|
|
944
|
+
candidate_id: candidate,
|
|
945
|
+
candidate_qid: `${alias}:${candidate}`,
|
|
946
|
+
candidate_path: candidatePath,
|
|
947
|
+
canonical_path: relativePath,
|
|
948
|
+
collision_free: true,
|
|
949
|
+
deterministic_rule: "keep stage 2 at the conflicted path, rewrite stage 3 to the next unused canonical numeric id and path, then git add both files",
|
|
950
|
+
},
|
|
951
|
+
command_hint: `mdkg fix ids --target ${duplicateId} --apply --json`,
|
|
952
|
+
apply_supported: true,
|
|
953
|
+
apply_kind: "git_stage_duplicate_id_rewrite",
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return { proposed, matchedTarget };
|
|
961
|
+
}
|
|
962
|
+
function planDuplicateIdRepairs(root, target, baseRef) {
|
|
732
963
|
const config = (0, config_1.loadConfig)(root);
|
|
733
964
|
const templateSchemas = (0, template_schema_1.loadTemplateSchemas)(root, config, node_1.ALLOWED_TYPES);
|
|
734
965
|
const docsByAlias = (0, workspace_files_1.listWorkspaceDocFilesByAlias)(root, config);
|
|
735
966
|
const proposed = [];
|
|
736
967
|
const blocked = [];
|
|
968
|
+
const usedIdsByAlias = new Map();
|
|
969
|
+
const usedPaths = new Set();
|
|
737
970
|
let matchedTarget = !target;
|
|
738
971
|
for (const alias of Object.keys(docsByAlias).sort()) {
|
|
739
972
|
const records = [];
|
|
740
973
|
const usedIds = new Set();
|
|
741
974
|
const files = docsByAlias[alias].sort();
|
|
975
|
+
const basePathsById = baseRefIdPaths(root, baseRef, files, config);
|
|
742
976
|
for (const filePath of files) {
|
|
977
|
+
usedPaths.add(rel(root, filePath));
|
|
743
978
|
if (path_1.default.basename(filePath) === "core.md" && path_1.default.basename(path_1.default.dirname(filePath)) === "core") {
|
|
744
979
|
continue;
|
|
745
980
|
}
|
|
@@ -762,6 +997,7 @@ function planDuplicateIdRepairs(root, target) {
|
|
|
762
997
|
continue;
|
|
763
998
|
}
|
|
764
999
|
}
|
|
1000
|
+
usedIdsByAlias.set(alias, usedIds);
|
|
765
1001
|
const groups = new Map();
|
|
766
1002
|
for (const record of records) {
|
|
767
1003
|
groups.set(record.id, [...(groups.get(record.id) ?? []), record]);
|
|
@@ -778,18 +1014,29 @@ function planDuplicateIdRepairs(root, target) {
|
|
|
778
1014
|
continue;
|
|
779
1015
|
}
|
|
780
1016
|
matchedTarget = true;
|
|
781
|
-
const
|
|
1017
|
+
const basePaths = basePathsById.get(id);
|
|
1018
|
+
const baseCanonical = basePaths ? group.find((record) => basePaths.has(record.path)) : undefined;
|
|
1019
|
+
const canonical = baseCanonical ?? group[0];
|
|
782
1020
|
const referencePaths = filesContaining(root, files, id);
|
|
783
|
-
const duplicateRecords = group.
|
|
1021
|
+
const duplicateRecords = group.filter((record) => record.path !== canonical.path);
|
|
784
1022
|
const groupPaths = group.map((record) => record.path).sort();
|
|
785
|
-
const deterministicRule =
|
|
1023
|
+
const deterministicRule = baseCanonical
|
|
1024
|
+
? "keep the path that already existed at --base-ref unchanged; propose the next unused canonical numeric id for each other path"
|
|
1025
|
+
: "keep the lexicographically first path unchanged; propose the next unused canonical numeric id for each later path";
|
|
786
1026
|
for (const duplicate of duplicateRecords) {
|
|
787
1027
|
const candidate = candidateDuplicateId(id, usedIds);
|
|
1028
|
+
const selfReferenceRewrites = referenceRewriteItems(root, [duplicate.absPath], id, candidate);
|
|
1029
|
+
const externalReferenceRewrites = referenceRewriteItems(root, files.filter((filePath) => filePath !== duplicate.absPath), id, candidate);
|
|
1030
|
+
const safeReferenceRewrites = baseRef
|
|
1031
|
+
? externalReferenceRewrites.filter((item) => gitShow(root, `${baseRef}:${item.path}`) === undefined)
|
|
1032
|
+
: [];
|
|
1033
|
+
const safeReferencePaths = new Set(safeReferenceRewrites.map((item) => item.path));
|
|
1034
|
+
const ambiguousReferenceRewrites = externalReferenceRewrites.filter((item) => !safeReferencePaths.has(item.path));
|
|
788
1035
|
proposed.push({
|
|
789
1036
|
id: `ids.${String(proposed.length + 1).padStart(3, "0")}`,
|
|
790
1037
|
family: "ids",
|
|
791
1038
|
risk: "high",
|
|
792
|
-
status: "
|
|
1039
|
+
status: "planned",
|
|
793
1040
|
reason: "duplicate_id",
|
|
794
1041
|
paths: [duplicate.path],
|
|
795
1042
|
refs: Array.from(new Set([canonical.qid, duplicate.qid])).sort(),
|
|
@@ -797,6 +1044,8 @@ function planDuplicateIdRepairs(root, target) {
|
|
|
797
1044
|
conflict_kind: "duplicate_local_id",
|
|
798
1045
|
branch_merge_suspected: true,
|
|
799
1046
|
workspace: alias,
|
|
1047
|
+
base_ref: baseRef ?? null,
|
|
1048
|
+
base_ref_canonical: Boolean(baseCanonical),
|
|
800
1049
|
duplicate_id: id,
|
|
801
1050
|
group_size: group.length,
|
|
802
1051
|
group_paths: groupPaths,
|
|
@@ -813,6 +1062,7 @@ function planDuplicateIdRepairs(root, target) {
|
|
|
813
1062
|
before: {
|
|
814
1063
|
duplicate_id: id,
|
|
815
1064
|
workspace: alias,
|
|
1065
|
+
base_ref: baseRef ?? null,
|
|
816
1066
|
canonical_path: canonical.path,
|
|
817
1067
|
duplicate_path: duplicate.path,
|
|
818
1068
|
duplicate_group: {
|
|
@@ -827,14 +1077,23 @@ function planDuplicateIdRepairs(root, target) {
|
|
|
827
1077
|
collision_free: true,
|
|
828
1078
|
deterministic_rule: deterministicRule,
|
|
829
1079
|
reference_paths: referencePaths,
|
|
830
|
-
|
|
1080
|
+
self_reference_rewrites: selfReferenceRewrites,
|
|
1081
|
+
safe_reference_rewrites: safeReferenceRewrites,
|
|
1082
|
+
ambiguous_reference_rewrites: ambiguousReferenceRewrites,
|
|
1083
|
+
reference_rewrite_plan: [...selfReferenceRewrites, ...safeReferenceRewrites, ...ambiguousReferenceRewrites].sort((a, b) => a.path.localeCompare(b.path)),
|
|
831
1084
|
},
|
|
832
|
-
command_hint: `
|
|
833
|
-
apply_supported:
|
|
1085
|
+
command_hint: `mdkg fix apply --family ids --target ${id}${baseRef ? ` --base-ref ${baseRef}` : ""} --json`,
|
|
1086
|
+
apply_supported: true,
|
|
1087
|
+
apply_kind: "duplicate_id_rewrite",
|
|
834
1088
|
});
|
|
835
1089
|
}
|
|
836
1090
|
}
|
|
837
1091
|
}
|
|
1092
|
+
const stageRepairs = planGitStageDuplicateIdRepairs(root, target, config, usedIdsByAlias, usedPaths, proposed.length);
|
|
1093
|
+
proposed.push(...stageRepairs.proposed);
|
|
1094
|
+
if (stageRepairs.matchedTarget) {
|
|
1095
|
+
matchedTarget = true;
|
|
1096
|
+
}
|
|
838
1097
|
if (!matchedTarget && target) {
|
|
839
1098
|
blocked.push({
|
|
840
1099
|
id: "ids.target.001",
|
|
@@ -881,15 +1140,20 @@ function collectFixPlan(options) {
|
|
|
881
1140
|
const root = relativeRoot(options.root);
|
|
882
1141
|
const indexRepairs = selected.includes("index") ? planIndexRepairs(root) : { proposed: [], blocked: [] };
|
|
883
1142
|
const refRepairs = selected.includes("refs") ? planRefRepairs(root, options.target) : { proposed: [], blocked: [] };
|
|
884
|
-
const idRepairs = selected.includes("ids")
|
|
1143
|
+
const idRepairs = selected.includes("ids")
|
|
1144
|
+
? planDuplicateIdRepairs(root, options.target, options.baseRef)
|
|
1145
|
+
: { proposed: [], blocked: [] };
|
|
885
1146
|
const proposedChanges = sortChanges([...indexRepairs.proposed, ...refRepairs.proposed, ...idRepairs.proposed]);
|
|
886
1147
|
const blockedChanges = sortChanges([...indexRepairs.blocked, ...refRepairs.blocked, ...idRepairs.blocked]);
|
|
1148
|
+
const supportedApplyCount = proposedChanges.filter((change) => change.apply_supported).length;
|
|
1149
|
+
const unsupportedApplyCount = proposedChanges.filter((change) => !change.apply_supported).length;
|
|
887
1150
|
const body = {
|
|
888
1151
|
action: "fix.plan",
|
|
889
1152
|
schema_version: 1,
|
|
890
1153
|
root,
|
|
891
1154
|
family,
|
|
892
1155
|
target: options.target ?? null,
|
|
1156
|
+
base_ref: options.baseRef ?? null,
|
|
893
1157
|
dirty: collectDirtyState(root),
|
|
894
1158
|
families: emptyFamilySummaries(selected).map((entry) => ({
|
|
895
1159
|
...entry,
|
|
@@ -903,9 +1167,13 @@ function collectFixPlan(options) {
|
|
|
903
1167
|
selected_families: selected,
|
|
904
1168
|
proposed_count: proposedChanges.length,
|
|
905
1169
|
blocked_count: blockedChanges.length,
|
|
906
|
-
apply_supported:
|
|
907
|
-
apply_deferred:
|
|
908
|
-
|
|
1170
|
+
apply_supported: supportedApplyCount > 0,
|
|
1171
|
+
apply_deferred: unsupportedApplyCount > 0 || blockedChanges.length > 0,
|
|
1172
|
+
supported_apply_count: supportedApplyCount,
|
|
1173
|
+
unsupported_apply_count: unsupportedApplyCount,
|
|
1174
|
+
message: supportedApplyCount > 0
|
|
1175
|
+
? "ids-family duplicate-id repairs can be applied with mdkg fix apply --family ids or mdkg fix ids --apply"
|
|
1176
|
+
: "this command is review-only for the selected findings and writes no files",
|
|
909
1177
|
},
|
|
910
1178
|
};
|
|
911
1179
|
const planHash = sha256(body);
|
|
@@ -917,6 +1185,171 @@ function collectFixPlan(options) {
|
|
|
917
1185
|
plan_id: `fix-plan-${planHash.slice("sha256:".length, "sha256:".length + 16)}`,
|
|
918
1186
|
};
|
|
919
1187
|
}
|
|
1188
|
+
function normalizeApplyFamily(value) {
|
|
1189
|
+
const family = normalizeFamily(value ?? "ids");
|
|
1190
|
+
if (family !== "ids") {
|
|
1191
|
+
throw new errors_1.UsageError("fix apply currently supports only --family ids");
|
|
1192
|
+
}
|
|
1193
|
+
return "ids";
|
|
1194
|
+
}
|
|
1195
|
+
function applyDuplicateIdChange(root, change) {
|
|
1196
|
+
if (change.family !== "ids" || change.reason !== "duplicate_id" || change.apply_kind !== "duplicate_id_rewrite") {
|
|
1197
|
+
throw new errors_1.UsageError(`unsupported fix apply change ${change.id}`);
|
|
1198
|
+
}
|
|
1199
|
+
const relativePath = change.paths[0];
|
|
1200
|
+
if (!relativePath) {
|
|
1201
|
+
throw new errors_1.UsageError(`fix apply change ${change.id} is missing a path`);
|
|
1202
|
+
}
|
|
1203
|
+
const absPath = path_1.default.resolve(root, relativePath);
|
|
1204
|
+
if (!isInsideRoot(root, absPath)) {
|
|
1205
|
+
throw new errors_1.UsageError(`fix apply refused path outside repo: ${relativePath}`);
|
|
1206
|
+
}
|
|
1207
|
+
const before = change.before;
|
|
1208
|
+
const after = change.after;
|
|
1209
|
+
const fromId = typeof before.duplicate_id === "string" ? before.duplicate_id : undefined;
|
|
1210
|
+
const toId = typeof after.candidate_id === "string" ? after.candidate_id : undefined;
|
|
1211
|
+
if (!fromId || !toId) {
|
|
1212
|
+
throw new errors_1.UsageError(`fix apply change ${change.id} is missing duplicate id rewrite details`);
|
|
1213
|
+
}
|
|
1214
|
+
const current = fs_1.default.readFileSync(absPath, "utf8");
|
|
1215
|
+
const rewritten = rewriteIdInNodeContent(current, fromId, toId);
|
|
1216
|
+
if (rewritten === current) {
|
|
1217
|
+
throw new errors_1.UsageError(`fix apply change ${change.id} produced no file changes`);
|
|
1218
|
+
}
|
|
1219
|
+
(0, atomic_1.atomicWriteFile)(absPath, rewritten);
|
|
1220
|
+
const afterDetails = change.after;
|
|
1221
|
+
const safeReferenceRewrites = Array.isArray(afterDetails.safe_reference_rewrites)
|
|
1222
|
+
? afterDetails.safe_reference_rewrites
|
|
1223
|
+
: [];
|
|
1224
|
+
const touchedPaths = new Set([relativePath]);
|
|
1225
|
+
for (const rewrite of safeReferenceRewrites) {
|
|
1226
|
+
if (typeof rewrite.path !== "string" || typeof rewrite.from !== "string" || typeof rewrite.to !== "string") {
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
const rewriteAbs = path_1.default.resolve(root, rewrite.path);
|
|
1230
|
+
if (!isInsideRoot(root, rewriteAbs) || !fs_1.default.existsSync(rewriteAbs)) {
|
|
1231
|
+
continue;
|
|
1232
|
+
}
|
|
1233
|
+
const rewriteCurrent = fs_1.default.readFileSync(rewriteAbs, "utf8");
|
|
1234
|
+
const rewriteNext = rewriteCurrent.split(rewrite.from).join(rewrite.to);
|
|
1235
|
+
if (rewriteNext !== rewriteCurrent) {
|
|
1236
|
+
(0, atomic_1.atomicWriteFile)(rewriteAbs, rewriteNext);
|
|
1237
|
+
touchedPaths.add(rewrite.path);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return {
|
|
1241
|
+
id: change.id,
|
|
1242
|
+
family: "ids",
|
|
1243
|
+
reason: change.reason,
|
|
1244
|
+
apply_kind: "duplicate_id_rewrite",
|
|
1245
|
+
path: relativePath,
|
|
1246
|
+
touched_paths: Array.from(touchedPaths).sort(),
|
|
1247
|
+
refs: change.refs,
|
|
1248
|
+
before: change.before,
|
|
1249
|
+
after: change.after,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
function applyGitStageDuplicateIdChange(root, change) {
|
|
1253
|
+
if (change.family !== "ids" || change.reason !== "git_stage_duplicate_id" || change.apply_kind !== "git_stage_duplicate_id_rewrite") {
|
|
1254
|
+
throw new errors_1.UsageError(`unsupported git-stage fix apply change ${change.id}`);
|
|
1255
|
+
}
|
|
1256
|
+
const before = change.before;
|
|
1257
|
+
const after = change.after;
|
|
1258
|
+
const fromId = typeof before.duplicate_id === "string" ? before.duplicate_id : undefined;
|
|
1259
|
+
const conflictPath = typeof before.conflict_path === "string" ? before.conflict_path : undefined;
|
|
1260
|
+
const candidateId = typeof after.candidate_id === "string" ? after.candidate_id : undefined;
|
|
1261
|
+
const candidatePath = typeof after.candidate_path === "string" ? after.candidate_path : undefined;
|
|
1262
|
+
if (!fromId || !conflictPath || !candidateId || !candidatePath) {
|
|
1263
|
+
throw new errors_1.UsageError(`fix apply change ${change.id} is missing git-stage rewrite details`);
|
|
1264
|
+
}
|
|
1265
|
+
const canonicalContent = gitShow(root, `:2:${conflictPath}`);
|
|
1266
|
+
const duplicateContent = gitShow(root, `:3:${conflictPath}`);
|
|
1267
|
+
if (canonicalContent === undefined || duplicateContent === undefined) {
|
|
1268
|
+
throw new errors_1.UsageError(`fix apply change ${change.id} could not read Git conflict stages for ${conflictPath}`);
|
|
1269
|
+
}
|
|
1270
|
+
const canonicalAbs = path_1.default.resolve(root, conflictPath);
|
|
1271
|
+
const candidateAbs = path_1.default.resolve(root, candidatePath);
|
|
1272
|
+
if (!isInsideRoot(root, canonicalAbs) || !isInsideRoot(root, candidateAbs)) {
|
|
1273
|
+
throw new errors_1.UsageError(`fix apply refused path outside repo while resolving ${conflictPath}`);
|
|
1274
|
+
}
|
|
1275
|
+
const rewrittenDuplicate = rewriteIdInNodeContent(duplicateContent, fromId, candidateId);
|
|
1276
|
+
(0, atomic_1.atomicWriteFile)(canonicalAbs, canonicalContent);
|
|
1277
|
+
(0, atomic_1.atomicWriteFile)(candidateAbs, rewrittenDuplicate);
|
|
1278
|
+
runGitStrict(root, ["add", "--", conflictPath, candidatePath]);
|
|
1279
|
+
return {
|
|
1280
|
+
id: change.id,
|
|
1281
|
+
family: "ids",
|
|
1282
|
+
reason: change.reason,
|
|
1283
|
+
apply_kind: "git_stage_duplicate_id_rewrite",
|
|
1284
|
+
path: conflictPath,
|
|
1285
|
+
touched_paths: [conflictPath, candidatePath],
|
|
1286
|
+
refs: change.refs,
|
|
1287
|
+
before: change.before,
|
|
1288
|
+
after: change.after,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
function collectFixApply(options) {
|
|
1292
|
+
const family = normalizeApplyFamily(options.family);
|
|
1293
|
+
const root = relativeRoot(options.root);
|
|
1294
|
+
const config = (0, config_1.loadConfig)(root);
|
|
1295
|
+
return (0, lock_1.withMutationLock)(root, config.index.lock_timeout_ms, () => {
|
|
1296
|
+
const plan = collectFixPlan({ ...options, root, family });
|
|
1297
|
+
const applicable = plan.proposed_changes.filter((change) => change.apply_supported);
|
|
1298
|
+
const unsupported = plan.proposed_changes.filter((change) => !change.apply_supported);
|
|
1299
|
+
if (plan.blocked_changes.length > 0) {
|
|
1300
|
+
throw new errors_1.UsageError("fix apply refused because the plan contains blocked changes");
|
|
1301
|
+
}
|
|
1302
|
+
if (applicable.length === 0) {
|
|
1303
|
+
throw new errors_1.UsageError("fix apply found no supported ids-family changes to apply");
|
|
1304
|
+
}
|
|
1305
|
+
const appliedChanges = applicable.map((change) => change.apply_kind === "git_stage_duplicate_id_rewrite"
|
|
1306
|
+
? applyGitStageDuplicateIdChange(root, change)
|
|
1307
|
+
: applyDuplicateIdChange(root, change));
|
|
1308
|
+
const indexReceipt = (0, index_1.rebuildDerivedIndexCaches)({ root, tolerant: true });
|
|
1309
|
+
const body = {
|
|
1310
|
+
action: "fix.apply",
|
|
1311
|
+
ok: true,
|
|
1312
|
+
schema_version: 1,
|
|
1313
|
+
root,
|
|
1314
|
+
family,
|
|
1315
|
+
target: options.target ?? null,
|
|
1316
|
+
base_ref: options.baseRef ?? null,
|
|
1317
|
+
plan_id: plan.plan_id,
|
|
1318
|
+
plan_hash: plan.plan_hash,
|
|
1319
|
+
applied_changes: appliedChanges,
|
|
1320
|
+
blocked_changes: plan.blocked_changes,
|
|
1321
|
+
unsupported_changes: unsupported,
|
|
1322
|
+
touched_paths: Array.from(new Set(appliedChanges.flatMap((change) => change.touched_paths))).sort(),
|
|
1323
|
+
ambiguous_reference_rewrites: applicable.flatMap((change) => {
|
|
1324
|
+
const after = change.after;
|
|
1325
|
+
return Array.isArray(after.ambiguous_reference_rewrites) ? after.ambiguous_reference_rewrites : [];
|
|
1326
|
+
}),
|
|
1327
|
+
index: {
|
|
1328
|
+
rebuilt: true,
|
|
1329
|
+
paths: {
|
|
1330
|
+
nodes: rel(root, indexReceipt.paths.nodes),
|
|
1331
|
+
skills: rel(root, indexReceipt.paths.skills),
|
|
1332
|
+
capabilities: rel(root, indexReceipt.paths.capabilities),
|
|
1333
|
+
subgraphs: rel(root, indexReceipt.paths.subgraphs),
|
|
1334
|
+
sqlite: indexReceipt.paths.sqlite ? rel(root, indexReceipt.paths.sqlite) : null,
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
summary: {
|
|
1338
|
+
applied_count: appliedChanges.length,
|
|
1339
|
+
unsupported_count: unsupported.length,
|
|
1340
|
+
blocked_count: plan.blocked_changes.length,
|
|
1341
|
+
message: unsupported.length > 0
|
|
1342
|
+
? "applied supported ids-family changes; unsupported findings remain review-only"
|
|
1343
|
+
: "applied supported ids-family duplicate-id repairs",
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
return {
|
|
1347
|
+
...body,
|
|
1348
|
+
generated_at: new Date().toISOString(),
|
|
1349
|
+
receipt_hash: sha256(body),
|
|
1350
|
+
};
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
920
1353
|
function runFixPlanCommand(options) {
|
|
921
1354
|
const payload = collectFixPlan(options);
|
|
922
1355
|
if (options.json) {
|
|
@@ -929,6 +1362,25 @@ function runFixPlanCommand(options) {
|
|
|
929
1362
|
console.log(`family: ${payload.family}`);
|
|
930
1363
|
console.log(`proposed_changes: ${payload.proposed_changes.length}`);
|
|
931
1364
|
console.log(`blocked_changes: ${payload.blocked_changes.length}`);
|
|
932
|
-
console.log(
|
|
933
|
-
console.log("note:
|
|
1365
|
+
console.log(`apply_supported: ${payload.summary.apply_supported}`);
|
|
1366
|
+
console.log("note: use --json for the machine-readable receipt");
|
|
1367
|
+
}
|
|
1368
|
+
function runFixApplyCommand(options) {
|
|
1369
|
+
const payload = collectFixApply(options);
|
|
1370
|
+
if (options.json) {
|
|
1371
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
console.log("fix apply");
|
|
1375
|
+
console.log(`receipt_hash: ${payload.receipt_hash}`);
|
|
1376
|
+
console.log(`family: ${payload.family}`);
|
|
1377
|
+
console.log(`applied_changes: ${payload.applied_changes.length}`);
|
|
1378
|
+
console.log(`touched_paths: ${payload.touched_paths.join(", ")}`);
|
|
1379
|
+
}
|
|
1380
|
+
function runFixIdsCommand(options) {
|
|
1381
|
+
if (options.apply) {
|
|
1382
|
+
runFixApplyCommand({ ...options, family: "ids" });
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
runFixPlanCommand({ ...options, family: "ids" });
|
|
934
1386
|
}
|