recall-os 0.2.1 → 0.3.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/README.md +36 -32
- package/dist/cli.js +431 -80
- package/dist/cli.js.map +1 -1
- package/dist/index.js +431 -80
- package/dist/index.js.map +1 -1
- package/examples/generated-flutter/AGENTS.md +13 -0
- package/examples/generated-flutter/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-flutter/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-generic/AGENTS.md +13 -0
- package/examples/generated-generic/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-generic/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-ios-swift/AGENTS.md +13 -0
- package/examples/generated-ios-swift/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-ios-swift/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-kotlin-android/AGENTS.md +13 -0
- package/examples/generated-kotlin-android/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-kotlin-android/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-api/AGENTS.md +13 -0
- package/examples/generated-laravel-api/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-api/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-react/AGENTS.md +13 -0
- package/examples/generated-laravel-react/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-react/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-laravel-vue/AGENTS.md +13 -0
- package/examples/generated-laravel-vue/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-laravel-vue/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-nextjs/AGENTS.md +13 -0
- package/examples/generated-nextjs/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-nextjs/docs/ai/RECALL_COMMANDS.md +13 -1
- package/examples/generated-python-fastapi/AGENTS.md +13 -0
- package/examples/generated-python-fastapi/docs/60-engineering/AI_AGENT_RULES.md +9 -0
- package/examples/generated-python-fastapi/docs/ai/RECALL_COMMANDS.md +13 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -132,8 +132,8 @@ function parseConfig(value) {
|
|
|
132
132
|
if (!result.success) {
|
|
133
133
|
throw new ConfigValidationError(
|
|
134
134
|
result.error.issues.map((issue) => {
|
|
135
|
-
const
|
|
136
|
-
return `${
|
|
135
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
136
|
+
return `${path19}${issue.message}`;
|
|
137
137
|
})
|
|
138
138
|
);
|
|
139
139
|
}
|
|
@@ -798,6 +798,54 @@ function generateAdrFile(options) {
|
|
|
798
798
|
}
|
|
799
799
|
];
|
|
800
800
|
}
|
|
801
|
+
var supersedingAdrTemplate = `# {{adrId}}: {{title}}
|
|
802
|
+
|
|
803
|
+
## Status
|
|
804
|
+
|
|
805
|
+
Accepted
|
|
806
|
+
|
|
807
|
+
## Supersedes
|
|
808
|
+
|
|
809
|
+
- {{supersedesRef}}
|
|
810
|
+
|
|
811
|
+
## Context
|
|
812
|
+
|
|
813
|
+
What changed, and why the previous decision no longer holds?
|
|
814
|
+
|
|
815
|
+
## Decision
|
|
816
|
+
|
|
817
|
+
What is the new decision?
|
|
818
|
+
|
|
819
|
+
## Alternatives Considered
|
|
820
|
+
|
|
821
|
+
What other options were considered?
|
|
822
|
+
|
|
823
|
+
## Consequences
|
|
824
|
+
|
|
825
|
+
What improves, what worsens, and what risks remain?
|
|
826
|
+
|
|
827
|
+
## Related Documents
|
|
828
|
+
|
|
829
|
+
- PRD:
|
|
830
|
+
- Architecture:
|
|
831
|
+
- Security:
|
|
832
|
+
- Feature:
|
|
833
|
+
`;
|
|
834
|
+
function generateSupersedingAdr(options) {
|
|
835
|
+
const slug = slugify(options.title);
|
|
836
|
+
const title = titleizeAdrTitle(options.title);
|
|
837
|
+
const context = createTemplateContext({
|
|
838
|
+
adrId: options.adrId,
|
|
839
|
+
slug,
|
|
840
|
+
title,
|
|
841
|
+
supersedesRef: options.supersedesRef
|
|
842
|
+
});
|
|
843
|
+
return {
|
|
844
|
+
path: path4.posix.join(options.adrDir, `${options.adrId}-${slug}.md`),
|
|
845
|
+
content: renderTemplate(supersedingAdrTemplate, context),
|
|
846
|
+
slug
|
|
847
|
+
};
|
|
848
|
+
}
|
|
801
849
|
function titleizeAdrTitle(title) {
|
|
802
850
|
return title.trim().replace(/[-_]+/gu, " ").replace(/\s+/gu, " ").replace(/\b\w/gu, (character) => character.toUpperCase());
|
|
803
851
|
}
|
|
@@ -888,8 +936,148 @@ async function loadRequiredConfig2(rootDir) {
|
|
|
888
936
|
}
|
|
889
937
|
}
|
|
890
938
|
|
|
891
|
-
// src/
|
|
939
|
+
// src/commands/adr/supersede.ts
|
|
940
|
+
import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
892
941
|
import path5 from "path";
|
|
942
|
+
var AdrSupersedeError = class extends Error {
|
|
943
|
+
code;
|
|
944
|
+
details;
|
|
945
|
+
constructor(code, message, details = []) {
|
|
946
|
+
super(message);
|
|
947
|
+
this.name = "AdrSupersedeError";
|
|
948
|
+
this.code = code;
|
|
949
|
+
this.details = details;
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
async function supersedeAdr(options) {
|
|
953
|
+
const oldSlug = createSlug2(options.oldName, "oldName");
|
|
954
|
+
createSlug2(options.newTitle, "newTitle");
|
|
955
|
+
const config = await loadRequiredConfig3(options.rootDir);
|
|
956
|
+
const adrDirAbsolute = resolveSafePath(options.rootDir, config.adrDir).absolutePath;
|
|
957
|
+
const old = await findAcceptedAdr(adrDirAbsolute, oldSlug);
|
|
958
|
+
const next = await getNextAdrNumber(adrDirAbsolute);
|
|
959
|
+
const oldRef = old.fileName.replace(/\.md$/u, "");
|
|
960
|
+
const superseding = generateSupersedingAdr({
|
|
961
|
+
adrDir: config.adrDir,
|
|
962
|
+
adrId: next.id,
|
|
963
|
+
title: options.newTitle,
|
|
964
|
+
supersedesRef: oldRef
|
|
965
|
+
});
|
|
966
|
+
const newRef = `${next.id}-${superseding.slug}`;
|
|
967
|
+
const markedOld = old.content.replace(
|
|
968
|
+
/(##\s+Status\r?\n\r?\n)Accepted[^\n]*/u,
|
|
969
|
+
`$1Accepted \u2014 superseded by ${newRef}`
|
|
970
|
+
);
|
|
971
|
+
const writeNew = await write2(options, superseding.path, superseding.content, options.force);
|
|
972
|
+
const oldRelative = `${config.adrDir}/${old.fileName}`;
|
|
973
|
+
const writeOld = await write2(options, oldRelative, markedOld, true);
|
|
974
|
+
return {
|
|
975
|
+
oldRef,
|
|
976
|
+
oldPath: oldRelative,
|
|
977
|
+
newRef,
|
|
978
|
+
newPath: superseding.path,
|
|
979
|
+
dryRun: options.dryRun ?? false,
|
|
980
|
+
writeResult: {
|
|
981
|
+
created: [...writeNew.created, ...writeOld.created],
|
|
982
|
+
overwritten: [...writeNew.overwritten, ...writeOld.overwritten],
|
|
983
|
+
skipped: [...writeNew.skipped, ...writeOld.skipped],
|
|
984
|
+
dryRun: options.dryRun ?? false
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
async function findAcceptedAdr(adrDirAbsolute, slug) {
|
|
989
|
+
const pattern = new RegExp(`^ADR-\\d{4,}-${escapeRegExp2(slug)}\\.md$`, "u");
|
|
990
|
+
let entries;
|
|
991
|
+
try {
|
|
992
|
+
entries = await readdir3(adrDirAbsolute, { withFileTypes: true });
|
|
993
|
+
} catch (error) {
|
|
994
|
+
const nodeError = error;
|
|
995
|
+
if (nodeError.code === "ENOENT") {
|
|
996
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`);
|
|
997
|
+
}
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
const match = entries.find((entry) => entry.isFile() && pattern.test(entry.name));
|
|
1001
|
+
if (match === void 0) {
|
|
1002
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`, [
|
|
1003
|
+
`Looked for an ADR-####-${slug}.md in the ADR directory.`
|
|
1004
|
+
]);
|
|
1005
|
+
}
|
|
1006
|
+
const content = await readFile3(path5.join(adrDirAbsolute, match.name), "utf8");
|
|
1007
|
+
if (!/(##\s+Status\r?\n\r?\n)Accepted\b/u.test(content)) {
|
|
1008
|
+
throw new AdrSupersedeError(
|
|
1009
|
+
"NOT_ACCEPTED",
|
|
1010
|
+
`ADR ${match.name} is not Accepted, so there is nothing to supersede.`,
|
|
1011
|
+
["Only an accepted decision can be superseded. Accept it first with `recall adr accept`."]
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
return { fileName: match.name, content };
|
|
1015
|
+
}
|
|
1016
|
+
async function write2(options, relativePath, content, force) {
|
|
1017
|
+
const plan = createWritePlan({
|
|
1018
|
+
rootDir: options.rootDir,
|
|
1019
|
+
files: [{ path: relativePath, content }],
|
|
1020
|
+
force
|
|
1021
|
+
});
|
|
1022
|
+
if (plan.hasErrors) {
|
|
1023
|
+
throw new AdrSupersedeError(
|
|
1024
|
+
"WRITE_PLAN_ERROR",
|
|
1025
|
+
"ADR supersede write plan contains errors.",
|
|
1026
|
+
plan.entries.filter(
|
|
1027
|
+
(entry) => entry.action === "error"
|
|
1028
|
+
).map((entry) => `${entry.path}: ${entry.reason}`)
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
return executeWritePlan(plan, { dryRun: options.dryRun });
|
|
1032
|
+
}
|
|
1033
|
+
function formatAdrSupersedeResult(result) {
|
|
1034
|
+
const lines = [
|
|
1035
|
+
result.dryRun ? "Recall OS ADR supersede dry run complete." : "Recall OS ADR supersede complete.",
|
|
1036
|
+
`Superseded: ${result.oldPath} (now marked superseded by ${result.newRef})`,
|
|
1037
|
+
`New decision: ${result.newPath}`
|
|
1038
|
+
];
|
|
1039
|
+
appendWriteSummary(lines, { dryRun: result.dryRun, writeResult: result.writeResult });
|
|
1040
|
+
if (!result.dryRun) {
|
|
1041
|
+
appendNextSteps(lines, [
|
|
1042
|
+
`Fill ${result.newPath}: Context (what changed), Decision, Alternatives, Consequences.`,
|
|
1043
|
+
`${result.oldRef} stays in history as superseded; update any memory that still relies on it.`,
|
|
1044
|
+
"Run `recall doctor` \u2014 it flags memory that still references the superseded decision."
|
|
1045
|
+
]);
|
|
1046
|
+
}
|
|
1047
|
+
return `${lines.join("\n")}
|
|
1048
|
+
`;
|
|
1049
|
+
}
|
|
1050
|
+
function createSlug2(name, field) {
|
|
1051
|
+
const withoutPrefix = name.replace(/^ADR-(?:PROPOSED-|\d{4,}-)/iu, "");
|
|
1052
|
+
try {
|
|
1053
|
+
return slugify(withoutPrefix);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
if (error instanceof SlugifyError) {
|
|
1056
|
+
throw new AdrSupersedeError("INVALID_ADR_NAME", `Invalid ${field}: ${error.message}`);
|
|
1057
|
+
}
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function escapeRegExp2(value) {
|
|
1062
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1063
|
+
}
|
|
1064
|
+
async function loadRequiredConfig3(rootDir) {
|
|
1065
|
+
try {
|
|
1066
|
+
return await loadConfig(rootDir);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
if (error instanceof ConfigLoadError || error instanceof ConfigValidationError) {
|
|
1069
|
+
throw new AdrSupersedeError(
|
|
1070
|
+
"CONFIG_REQUIRED",
|
|
1071
|
+
"Recall OS config not found or invalid. Run `recall init` first.",
|
|
1072
|
+
[error.message]
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// src/core/generator/generate-feature.ts
|
|
1080
|
+
import path6 from "path";
|
|
893
1081
|
var featureTemplates = [
|
|
894
1082
|
{
|
|
895
1083
|
fileName: "PRD.md",
|
|
@@ -1041,7 +1229,7 @@ Pending.
|
|
|
1041
1229
|
];
|
|
1042
1230
|
function generateFeatureFiles(options) {
|
|
1043
1231
|
const slug = slugify(options.featureName);
|
|
1044
|
-
const featureDir =
|
|
1232
|
+
const featureDir = path6.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
|
|
1045
1233
|
const title = titleizeFeatureName(options.featureName);
|
|
1046
1234
|
const context = createTemplateContext({
|
|
1047
1235
|
featureId: options.featureId,
|
|
@@ -1049,7 +1237,7 @@ function generateFeatureFiles(options) {
|
|
|
1049
1237
|
title
|
|
1050
1238
|
});
|
|
1051
1239
|
return featureTemplates.map((template) => ({
|
|
1052
|
-
path:
|
|
1240
|
+
path: path6.posix.join(featureDir, template.fileName),
|
|
1053
1241
|
content: renderTemplate(template.content, context)
|
|
1054
1242
|
}));
|
|
1055
1243
|
}
|
|
@@ -1058,7 +1246,7 @@ function titleizeFeatureName(featureName) {
|
|
|
1058
1246
|
}
|
|
1059
1247
|
|
|
1060
1248
|
// src/core/naming/feature-number.ts
|
|
1061
|
-
import { readdir as
|
|
1249
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1062
1250
|
var FEATURE_FOLDER_PATTERN = /^F-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/u;
|
|
1063
1251
|
async function getFeatureFolderForSlug(featuresDirAbsolutePath, slug) {
|
|
1064
1252
|
const existingFolders = await readExistingFeatureFolders(featuresDirAbsolutePath);
|
|
@@ -1091,7 +1279,7 @@ function formatFeatureNumber(featureNumber) {
|
|
|
1091
1279
|
async function readExistingFeatureFolders(featuresDirAbsolutePath) {
|
|
1092
1280
|
let entries;
|
|
1093
1281
|
try {
|
|
1094
|
-
entries = await
|
|
1282
|
+
entries = await readdir4(featuresDirAbsolutePath, { withFileTypes: true });
|
|
1095
1283
|
} catch (error) {
|
|
1096
1284
|
const nodeError = error;
|
|
1097
1285
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1130,7 +1318,7 @@ var FeatureCreateError = class extends Error {
|
|
|
1130
1318
|
};
|
|
1131
1319
|
async function createFeature(options) {
|
|
1132
1320
|
const slug = createFeatureSlug(options.name);
|
|
1133
|
-
const config = await
|
|
1321
|
+
const config = await loadRequiredConfig4(options.rootDir);
|
|
1134
1322
|
const featuresDirPath = resolveSafePath(options.rootDir, config.featuresDir);
|
|
1135
1323
|
const featureFolder = await getFeatureFolderForSlug(featuresDirPath.absolutePath, slug);
|
|
1136
1324
|
const files = generateFeatureFiles({
|
|
@@ -1189,7 +1377,7 @@ function createFeatureSlug(name) {
|
|
|
1189
1377
|
throw error;
|
|
1190
1378
|
}
|
|
1191
1379
|
}
|
|
1192
|
-
async function
|
|
1380
|
+
async function loadRequiredConfig4(rootDir) {
|
|
1193
1381
|
try {
|
|
1194
1382
|
return await loadConfig(rootDir);
|
|
1195
1383
|
} catch (error) {
|
|
@@ -1230,8 +1418,8 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1230
1418
|
|
|
1231
1419
|
// src/core/adopt/inspect-repo.ts
|
|
1232
1420
|
import { existsSync as existsSync3 } from "fs";
|
|
1233
|
-
import { readFile as
|
|
1234
|
-
import
|
|
1421
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1422
|
+
import path7 from "path";
|
|
1235
1423
|
var FRAMEWORK_SOURCES = {
|
|
1236
1424
|
"Next.js": "package.json",
|
|
1237
1425
|
React: "package.json",
|
|
@@ -1254,7 +1442,7 @@ var FRAMEWORK_SOURCES = {
|
|
|
1254
1442
|
Flutter: "pubspec.yaml"
|
|
1255
1443
|
};
|
|
1256
1444
|
async function inspectRepo(rootDir) {
|
|
1257
|
-
const has = (relativePath) => existsSync3(
|
|
1445
|
+
const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
|
|
1258
1446
|
const languages = /* @__PURE__ */ new Set();
|
|
1259
1447
|
const frameworks = /* @__PURE__ */ new Set();
|
|
1260
1448
|
const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
|
|
@@ -1468,7 +1656,7 @@ async function findTestFile(rootDir) {
|
|
|
1468
1656
|
}
|
|
1469
1657
|
let entries;
|
|
1470
1658
|
try {
|
|
1471
|
-
entries = await
|
|
1659
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1472
1660
|
} catch {
|
|
1473
1661
|
continue;
|
|
1474
1662
|
}
|
|
@@ -1479,10 +1667,10 @@ async function findTestFile(rootDir) {
|
|
|
1479
1667
|
}
|
|
1480
1668
|
if (entry.isDirectory()) {
|
|
1481
1669
|
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1482
|
-
stack.push(
|
|
1670
|
+
stack.push(path7.join(dir, entry.name));
|
|
1483
1671
|
}
|
|
1484
1672
|
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1485
|
-
return
|
|
1673
|
+
return path7.relative(rootDir, path7.join(dir, entry.name));
|
|
1486
1674
|
}
|
|
1487
1675
|
}
|
|
1488
1676
|
}
|
|
@@ -1506,7 +1694,7 @@ async function readJson(rootDir, relativePath) {
|
|
|
1506
1694
|
}
|
|
1507
1695
|
async function readText(rootDir, relativePath) {
|
|
1508
1696
|
try {
|
|
1509
|
-
return await
|
|
1697
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1510
1698
|
} catch {
|
|
1511
1699
|
return "";
|
|
1512
1700
|
}
|
|
@@ -1682,8 +1870,8 @@ function formatList2(values) {
|
|
|
1682
1870
|
|
|
1683
1871
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1684
1872
|
import { existsSync as existsSync4 } from "fs";
|
|
1685
|
-
import { readFile as
|
|
1686
|
-
import
|
|
1873
|
+
import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
|
|
1874
|
+
import path8 from "path";
|
|
1687
1875
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1688
1876
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1689
1877
|
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
@@ -1700,7 +1888,7 @@ async function checkCodeReferences(context) {
|
|
|
1700
1888
|
continue;
|
|
1701
1889
|
}
|
|
1702
1890
|
for (const doc of FEATURE_DOCS) {
|
|
1703
|
-
const relativePath =
|
|
1891
|
+
const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1704
1892
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1705
1893
|
}
|
|
1706
1894
|
}
|
|
@@ -1710,7 +1898,7 @@ async function checkCodeReferences(context) {
|
|
|
1710
1898
|
continue;
|
|
1711
1899
|
}
|
|
1712
1900
|
for (const doc of MODULE_DOCS) {
|
|
1713
|
-
const relativePath =
|
|
1901
|
+
const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1714
1902
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1715
1903
|
}
|
|
1716
1904
|
}
|
|
@@ -1729,7 +1917,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1729
1917
|
continue;
|
|
1730
1918
|
}
|
|
1731
1919
|
seen.add(reference);
|
|
1732
|
-
if (!existsSync4(
|
|
1920
|
+
if (!existsSync4(path8.join(rootDir, reference))) {
|
|
1733
1921
|
findings.push({
|
|
1734
1922
|
severity: "warning",
|
|
1735
1923
|
check: "drift-code-reference",
|
|
@@ -1742,7 +1930,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1742
1930
|
}
|
|
1743
1931
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1744
1932
|
try {
|
|
1745
|
-
return await
|
|
1933
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1746
1934
|
} catch (error) {
|
|
1747
1935
|
const nodeError = error;
|
|
1748
1936
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1753,7 +1941,7 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1753
1941
|
}
|
|
1754
1942
|
async function readFileIfExists(rootDir, relativePath) {
|
|
1755
1943
|
try {
|
|
1756
|
-
return await
|
|
1944
|
+
return await readFile5(path8.join(rootDir, relativePath), "utf8");
|
|
1757
1945
|
} catch (error) {
|
|
1758
1946
|
const nodeError = error;
|
|
1759
1947
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1764,12 +1952,12 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1764
1952
|
}
|
|
1765
1953
|
|
|
1766
1954
|
// src/core/doctor/checks/config-check.ts
|
|
1767
|
-
import { readFile as
|
|
1955
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1768
1956
|
async function checkConfig(rootDir) {
|
|
1769
1957
|
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1770
1958
|
let rawConfig;
|
|
1771
1959
|
try {
|
|
1772
|
-
rawConfig = await
|
|
1960
|
+
rawConfig = await readFile6(configPath.absolutePath, "utf8");
|
|
1773
1961
|
} catch (error) {
|
|
1774
1962
|
const nodeError = error;
|
|
1775
1963
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1832,8 +2020,8 @@ async function checkConfig(rootDir) {
|
|
|
1832
2020
|
}
|
|
1833
2021
|
|
|
1834
2022
|
// src/core/doctor/checks/content-check.ts
|
|
1835
|
-
import { readFile as
|
|
1836
|
-
import
|
|
2023
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
2024
|
+
import path9 from "path";
|
|
1837
2025
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1838
2026
|
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1839
2027
|
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
@@ -1848,7 +2036,7 @@ async function checkContent(context) {
|
|
|
1848
2036
|
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1849
2037
|
);
|
|
1850
2038
|
for (const folder of featureFolders) {
|
|
1851
|
-
const prdPath =
|
|
2039
|
+
const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1852
2040
|
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1853
2041
|
if (prd === void 0) {
|
|
1854
2042
|
continue;
|
|
@@ -1881,7 +2069,7 @@ async function checkContent(context) {
|
|
|
1881
2069
|
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
1882
2070
|
}
|
|
1883
2071
|
for (const folder of moduleFolders) {
|
|
1884
|
-
const modulePath =
|
|
2072
|
+
const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1885
2073
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1886
2074
|
if (moduleDoc === void 0) {
|
|
1887
2075
|
continue;
|
|
@@ -1959,7 +2147,7 @@ function getSection(content, heading) {
|
|
|
1959
2147
|
}
|
|
1960
2148
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1961
2149
|
try {
|
|
1962
|
-
return await
|
|
2150
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1963
2151
|
} catch (error) {
|
|
1964
2152
|
const nodeError = error;
|
|
1965
2153
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1970,7 +2158,7 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1970
2158
|
}
|
|
1971
2159
|
async function readFileIfExists2(rootDir, relativePath) {
|
|
1972
2160
|
try {
|
|
1973
|
-
return await
|
|
2161
|
+
return await readFile7(path9.join(rootDir, relativePath), "utf8");
|
|
1974
2162
|
} catch (error) {
|
|
1975
2163
|
const nodeError = error;
|
|
1976
2164
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1981,8 +2169,8 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1981
2169
|
}
|
|
1982
2170
|
|
|
1983
2171
|
// src/core/doctor/checks/drift-check.ts
|
|
1984
|
-
import { readFile as
|
|
1985
|
-
import
|
|
2172
|
+
import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2173
|
+
import path10 from "path";
|
|
1986
2174
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1987
2175
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1988
2176
|
async function checkDrift(context) {
|
|
@@ -1999,12 +2187,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
1999
2187
|
const known = /* @__PURE__ */ new Map();
|
|
2000
2188
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
2001
2189
|
for (const file of files) {
|
|
2002
|
-
const match = adrFilePattern.exec(
|
|
2190
|
+
const match = adrFilePattern.exec(path10.basename(file));
|
|
2003
2191
|
if (match === null) {
|
|
2004
2192
|
continue;
|
|
2005
2193
|
}
|
|
2006
2194
|
const id = `ADR-${match[1]}`;
|
|
2007
|
-
const content = await
|
|
2195
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2008
2196
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
2009
2197
|
const existing = known.get(id);
|
|
2010
2198
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -2017,7 +2205,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
2017
2205
|
const findings = [];
|
|
2018
2206
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
2019
2207
|
for (const file of files) {
|
|
2020
|
-
const content = await
|
|
2208
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
2021
2209
|
const referenced = /* @__PURE__ */ new Set();
|
|
2022
2210
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
2023
2211
|
referenced.add(match[0].toUpperCase());
|
|
@@ -2052,7 +2240,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
|
|
|
2052
2240
|
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
2053
2241
|
const files = [];
|
|
2054
2242
|
for (const entry of entries) {
|
|
2055
|
-
const childRelative =
|
|
2243
|
+
const childRelative = path10.posix.join(relativeDir, entry.name);
|
|
2056
2244
|
if (entry.isDirectory()) {
|
|
2057
2245
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
2058
2246
|
continue;
|
|
@@ -2085,7 +2273,7 @@ function getSection2(content, heading) {
|
|
|
2085
2273
|
}
|
|
2086
2274
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
2087
2275
|
try {
|
|
2088
|
-
return await
|
|
2276
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
2089
2277
|
} catch (error) {
|
|
2090
2278
|
const nodeError = error;
|
|
2091
2279
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2096,8 +2284,8 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
2096
2284
|
}
|
|
2097
2285
|
|
|
2098
2286
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
2099
|
-
import { lstat as lstat2, readFile as
|
|
2100
|
-
import
|
|
2287
|
+
import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2288
|
+
import path11 from "path";
|
|
2101
2289
|
|
|
2102
2290
|
// src/core/adr/adr-sections.ts
|
|
2103
2291
|
var REQUIRED_ADR_SECTIONS = [
|
|
@@ -2161,7 +2349,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
2161
2349
|
);
|
|
2162
2350
|
for (const featureFolder of featureFolders) {
|
|
2163
2351
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
2164
|
-
const filePath =
|
|
2352
|
+
const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
2165
2353
|
if (!await isFile(rootDir, filePath)) {
|
|
2166
2354
|
findings.push({
|
|
2167
2355
|
severity: "error",
|
|
@@ -2185,7 +2373,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
2185
2373
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
2186
2374
|
for (const moduleFolder of moduleFolders) {
|
|
2187
2375
|
for (const requiredDoc of requiredModuleDocs) {
|
|
2188
|
-
const filePath =
|
|
2376
|
+
const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
2189
2377
|
if (!await isFile(rootDir, filePath)) {
|
|
2190
2378
|
findings.push({
|
|
2191
2379
|
severity: "error",
|
|
@@ -2208,8 +2396,8 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2208
2396
|
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
2209
2397
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
2210
2398
|
for (const adrFile of adrFiles) {
|
|
2211
|
-
const filePath =
|
|
2212
|
-
const content = await
|
|
2399
|
+
const filePath = path11.posix.join(adrDir, adrFile.name);
|
|
2400
|
+
const content = await readFile9(path11.join(rootDir, filePath), "utf8");
|
|
2213
2401
|
for (const requiredSection of requiredAdrSections) {
|
|
2214
2402
|
if (!content.includes(requiredSection)) {
|
|
2215
2403
|
findings.push({
|
|
@@ -2230,7 +2418,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2230
2418
|
}
|
|
2231
2419
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2232
2420
|
try {
|
|
2233
|
-
return await
|
|
2421
|
+
return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
|
|
2234
2422
|
} catch (error) {
|
|
2235
2423
|
const nodeError = error;
|
|
2236
2424
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2241,7 +2429,7 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2241
2429
|
}
|
|
2242
2430
|
async function isFile(rootDir, relativePath) {
|
|
2243
2431
|
try {
|
|
2244
|
-
return (await lstat2(
|
|
2432
|
+
return (await lstat2(path11.join(rootDir, relativePath))).isFile();
|
|
2245
2433
|
} catch (error) {
|
|
2246
2434
|
const nodeError = error;
|
|
2247
2435
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2253,7 +2441,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
2253
2441
|
|
|
2254
2442
|
// src/core/doctor/checks/required-files-check.ts
|
|
2255
2443
|
import { lstat as lstat3 } from "fs/promises";
|
|
2256
|
-
import
|
|
2444
|
+
import path12 from "path";
|
|
2257
2445
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
2258
2446
|
var requiredDocs = [
|
|
2259
2447
|
"00-product/PRD.md",
|
|
@@ -2280,12 +2468,12 @@ async function checkRequiredFiles(context) {
|
|
|
2280
2468
|
}
|
|
2281
2469
|
}
|
|
2282
2470
|
for (const relativeDocPath of requiredDocs) {
|
|
2283
|
-
const filePath =
|
|
2471
|
+
const filePath = path12.posix.join(docsDir, relativeDocPath);
|
|
2284
2472
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
2285
2473
|
findings.push(missingFile(filePath, "required-docs"));
|
|
2286
2474
|
}
|
|
2287
2475
|
}
|
|
2288
|
-
const adrIndexPath =
|
|
2476
|
+
const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
2289
2477
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
2290
2478
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
2291
2479
|
}
|
|
@@ -2311,7 +2499,7 @@ async function checkRequiredFiles(context) {
|
|
|
2311
2499
|
}
|
|
2312
2500
|
async function isFile2(rootDir, relativePath) {
|
|
2313
2501
|
try {
|
|
2314
|
-
return (await lstat3(
|
|
2502
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isFile();
|
|
2315
2503
|
} catch (error) {
|
|
2316
2504
|
const nodeError = error;
|
|
2317
2505
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2322,7 +2510,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
2322
2510
|
}
|
|
2323
2511
|
async function isDirectory(rootDir, relativePath) {
|
|
2324
2512
|
try {
|
|
2325
|
-
return (await lstat3(
|
|
2513
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
|
|
2326
2514
|
} catch (error) {
|
|
2327
2515
|
const nodeError = error;
|
|
2328
2516
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2341,8 +2529,8 @@ function missingFile(pathValue, check) {
|
|
|
2341
2529
|
}
|
|
2342
2530
|
|
|
2343
2531
|
// src/core/doctor/checks/standards-check.ts
|
|
2344
|
-
import { lstat as lstat4, readFile as
|
|
2345
|
-
import
|
|
2532
|
+
import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
|
|
2533
|
+
import path13 from "path";
|
|
2346
2534
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2347
2535
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2348
2536
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
@@ -2362,10 +2550,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2362
2550
|
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2363
2551
|
);
|
|
2364
2552
|
for (const featureFolder of featureFolders) {
|
|
2365
|
-
const featureDir =
|
|
2366
|
-
const completionReportPath =
|
|
2367
|
-
const reviewPath =
|
|
2368
|
-
const architectureImpactPath =
|
|
2553
|
+
const featureDir = path13.posix.join(featuresDir, featureFolder.name);
|
|
2554
|
+
const completionReportPath = path13.posix.join(featureDir, "COMPLETION_REPORT.md");
|
|
2555
|
+
const reviewPath = path13.posix.join(featureDir, "REVIEW.md");
|
|
2556
|
+
const architectureImpactPath = path13.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
|
|
2369
2557
|
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2370
2558
|
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2371
2559
|
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
@@ -2415,8 +2603,8 @@ async function checkAdrStandards(rootDir, adrDir) {
|
|
|
2415
2603
|
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2416
2604
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2417
2605
|
for (const adrFile of adrFiles) {
|
|
2418
|
-
const adrPath =
|
|
2419
|
-
const content = await
|
|
2606
|
+
const adrPath = path13.posix.join(adrDir, adrFile.name);
|
|
2607
|
+
const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
|
|
2420
2608
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2421
2609
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2422
2610
|
findings.push({
|
|
@@ -2500,7 +2688,7 @@ function isPlaceholder(value) {
|
|
|
2500
2688
|
}
|
|
2501
2689
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2502
2690
|
try {
|
|
2503
|
-
return await
|
|
2691
|
+
return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
|
|
2504
2692
|
} catch (error) {
|
|
2505
2693
|
const nodeError = error;
|
|
2506
2694
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2514,7 +2702,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2514
2702
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2515
2703
|
return void 0;
|
|
2516
2704
|
}
|
|
2517
|
-
return await
|
|
2705
|
+
return await readFile10(path13.join(rootDir, relativePath), "utf8");
|
|
2518
2706
|
} catch (error) {
|
|
2519
2707
|
const nodeError = error;
|
|
2520
2708
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2525,7 +2713,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2525
2713
|
}
|
|
2526
2714
|
async function isFile3(rootDir, relativePath) {
|
|
2527
2715
|
try {
|
|
2528
|
-
return (await lstat4(
|
|
2716
|
+
return (await lstat4(path13.join(rootDir, relativePath))).isFile();
|
|
2529
2717
|
} catch (error) {
|
|
2530
2718
|
const nodeError = error;
|
|
2531
2719
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2535,6 +2723,110 @@ async function isFile3(rootDir, relativePath) {
|
|
|
2535
2723
|
}
|
|
2536
2724
|
}
|
|
2537
2725
|
|
|
2726
|
+
// src/core/doctor/checks/superseded-check.ts
|
|
2727
|
+
import { readFile as readFile11, readdir as readdir11 } from "fs/promises";
|
|
2728
|
+
import path14 from "path";
|
|
2729
|
+
var adrFilePattern4 = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
2730
|
+
var adrReferencePattern2 = /ADR-\d{4,}/giu;
|
|
2731
|
+
async function checkSuperseded(context) {
|
|
2732
|
+
if (context.config === void 0) {
|
|
2733
|
+
return [];
|
|
2734
|
+
}
|
|
2735
|
+
const supersededIds = await loadSupersededAdrIds(context.rootDir, context.config.adrDir);
|
|
2736
|
+
if (supersededIds.size === 0) {
|
|
2737
|
+
return [];
|
|
2738
|
+
}
|
|
2739
|
+
const findings = [];
|
|
2740
|
+
findings.push(
|
|
2741
|
+
...await checkReferences2(context.rootDir, context.config.featuresDir, supersededIds)
|
|
2742
|
+
);
|
|
2743
|
+
findings.push(
|
|
2744
|
+
...await checkReferences2(context.rootDir, context.config.modulesDir, supersededIds)
|
|
2745
|
+
);
|
|
2746
|
+
return findings;
|
|
2747
|
+
}
|
|
2748
|
+
async function loadSupersededAdrIds(rootDir, adrDir) {
|
|
2749
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2750
|
+
const files = await readMarkdownFiles2(rootDir, adrDir);
|
|
2751
|
+
for (const file of files) {
|
|
2752
|
+
const match = adrFilePattern4.exec(path14.basename(file));
|
|
2753
|
+
if (match === null) {
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2757
|
+
if (statusContains(content, /superseded\s+by/iu)) {
|
|
2758
|
+
superseded.add(`ADR-${match[1]}`.toUpperCase());
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
return superseded;
|
|
2762
|
+
}
|
|
2763
|
+
async function checkReferences2(rootDir, referenceDir, supersededIds) {
|
|
2764
|
+
const findings = [];
|
|
2765
|
+
const files = await readMarkdownFiles2(rootDir, referenceDir);
|
|
2766
|
+
for (const file of files) {
|
|
2767
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2768
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2769
|
+
for (const match of stripCode2(content).matchAll(adrReferencePattern2)) {
|
|
2770
|
+
referenced.add(match[0].toUpperCase());
|
|
2771
|
+
}
|
|
2772
|
+
for (const id of referenced) {
|
|
2773
|
+
if (supersededIds.has(id)) {
|
|
2774
|
+
findings.push({
|
|
2775
|
+
severity: "warning",
|
|
2776
|
+
check: "superseded-reference",
|
|
2777
|
+
message: `Repository memory references ${id}, which has been superseded \u2014 update it to the current decision.`,
|
|
2778
|
+
path: file
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
return findings;
|
|
2784
|
+
}
|
|
2785
|
+
function statusContains(content, pattern) {
|
|
2786
|
+
const lines = content.split(/\r?\n/u);
|
|
2787
|
+
const startIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## status");
|
|
2788
|
+
if (startIndex === -1) {
|
|
2789
|
+
return false;
|
|
2790
|
+
}
|
|
2791
|
+
const body = [];
|
|
2792
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
2793
|
+
if (/^##\s+/u.test(lines[index])) {
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
body.push(lines[index]);
|
|
2797
|
+
}
|
|
2798
|
+
return pattern.test(body.join("\n"));
|
|
2799
|
+
}
|
|
2800
|
+
function stripCode2(content) {
|
|
2801
|
+
return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
|
|
2802
|
+
}
|
|
2803
|
+
async function readMarkdownFiles2(rootDir, relativeDir) {
|
|
2804
|
+
const entries = await readDirIfExists6(rootDir, relativeDir);
|
|
2805
|
+
const files = [];
|
|
2806
|
+
for (const entry of entries) {
|
|
2807
|
+
const childRelative = path14.posix.join(relativeDir, entry.name);
|
|
2808
|
+
if (entry.isDirectory()) {
|
|
2809
|
+
files.push(...await readMarkdownFiles2(rootDir, childRelative));
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
2813
|
+
files.push(childRelative);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return files;
|
|
2817
|
+
}
|
|
2818
|
+
async function readDirIfExists6(rootDir, relativePath) {
|
|
2819
|
+
try {
|
|
2820
|
+
return await readdir11(path14.join(rootDir, relativePath), { withFileTypes: true });
|
|
2821
|
+
} catch (error) {
|
|
2822
|
+
const nodeError = error;
|
|
2823
|
+
if (nodeError.code === "ENOENT") {
|
|
2824
|
+
return [];
|
|
2825
|
+
}
|
|
2826
|
+
throw error;
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2538
2830
|
// src/core/doctor/doctor-check.ts
|
|
2539
2831
|
async function runDoctor(rootDir) {
|
|
2540
2832
|
const findings = [];
|
|
@@ -2551,6 +2843,7 @@ async function runDoctor(rootDir) {
|
|
|
2551
2843
|
findings.push(...await checkDrift(context));
|
|
2552
2844
|
findings.push(...await checkContent(context));
|
|
2553
2845
|
findings.push(...await checkCodeReferences(context));
|
|
2846
|
+
findings.push(...await checkSuperseded(context));
|
|
2554
2847
|
}
|
|
2555
2848
|
return createDoctorReport(findings);
|
|
2556
2849
|
}
|
|
@@ -2628,10 +2921,10 @@ function formatDoctorResult(result) {
|
|
|
2628
2921
|
|
|
2629
2922
|
// src/commands/init.ts
|
|
2630
2923
|
import { existsSync as existsSync6 } from "fs";
|
|
2631
|
-
import
|
|
2924
|
+
import path17 from "path";
|
|
2632
2925
|
|
|
2633
2926
|
// src/core/generator/generate-init.ts
|
|
2634
|
-
import
|
|
2927
|
+
import path15 from "path";
|
|
2635
2928
|
var neutralTemplates = [
|
|
2636
2929
|
{
|
|
2637
2930
|
path: "AGENTS.md",
|
|
@@ -2650,6 +2943,18 @@ Required reading:
|
|
|
2650
2943
|
- \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
|
|
2651
2944
|
|
|
2652
2945
|
Repository rules override model preferences. If instructions conflict, stop and report the conflict.
|
|
2946
|
+
|
|
2947
|
+
## Changing an accepted decision
|
|
2948
|
+
|
|
2949
|
+
Before changing anything an accepted ADR governs (framework, database, auth, API shape, and similar):
|
|
2950
|
+
|
|
2951
|
+
1. Check \`docs/adrs/\` for an accepted ADR that covers it.
|
|
2952
|
+
2. If your change contradicts one, stop and confirm with a human first \u2014 do not silently change the
|
|
2953
|
+
code and leave the ADR saying the opposite.
|
|
2954
|
+
3. Record the change as a new decision with \`recall adr supersede <old> <new-title>\`. That supersedes
|
|
2955
|
+
the old ADR instead of overwriting history, so the reasoning stays auditable.
|
|
2956
|
+
|
|
2957
|
+
Repository memory is only trustworthy if decisions change through this trail, not silently.
|
|
2653
2958
|
`
|
|
2654
2959
|
},
|
|
2655
2960
|
{
|
|
@@ -2686,6 +2991,8 @@ Before non-trivial work:
|
|
|
2686
2991
|
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2687
2992
|
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2688
2993
|
- If an instruction conflicts with accepted repository memory, stop and report the conflict.
|
|
2994
|
+
- Before changing what an accepted ADR governs, confirm with a human and record it with
|
|
2995
|
+
\`recall adr supersede <old> <new-title>\` \u2014 never silently contradict an accepted decision.
|
|
2689
2996
|
|
|
2690
2997
|
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2691
2998
|
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
@@ -2894,6 +3201,15 @@ Baseline rules:
|
|
|
2894
3201
|
AI agents must follow repository memory over model preference.
|
|
2895
3202
|
|
|
2896
3203
|
If a request conflicts with accepted repository memory or engineering standards, stop and report the conflict.
|
|
3204
|
+
|
|
3205
|
+
## Changing an accepted decision
|
|
3206
|
+
|
|
3207
|
+
When work would change something an accepted ADR governs:
|
|
3208
|
+
|
|
3209
|
+
1. Find the accepted ADR in \`docs/adrs/\` that covers it.
|
|
3210
|
+
2. If the change contradicts it, stop and confirm with a human before changing the code.
|
|
3211
|
+
3. Record the new decision with \`recall adr supersede <old> <new-title>\` so the old ADR is marked
|
|
3212
|
+
superseded and the reasoning is preserved, instead of silently editing or contradicting it.
|
|
2897
3213
|
`
|
|
2898
3214
|
},
|
|
2899
3215
|
{
|
|
@@ -3027,6 +3343,17 @@ Options:
|
|
|
3027
3343
|
- \`--dry-run\`: show planned writes without writing files.
|
|
3028
3344
|
- \`--force\`: overwrite existing files explicitly.
|
|
3029
3345
|
|
|
3346
|
+
### \`recall adr supersede <old> <new-title>\`
|
|
3347
|
+
|
|
3348
|
+
Record a changed decision. Marks an accepted ADR as \`Accepted \u2014 superseded by ADR-####\` and creates a
|
|
3349
|
+
new accepted ADR that declares what it supersedes, so the reasoning trail stays auditable instead of
|
|
3350
|
+
being overwritten. Doctor then warns about any memory still referencing the superseded decision.
|
|
3351
|
+
|
|
3352
|
+
Options:
|
|
3353
|
+
|
|
3354
|
+
- \`--dry-run\`: show planned writes without writing files.
|
|
3355
|
+
- \`--force\`: overwrite existing files explicitly.
|
|
3356
|
+
|
|
3030
3357
|
### \`recall module create <name>\`
|
|
3031
3358
|
|
|
3032
3359
|
Create module memory docs under the configured modules directory.
|
|
@@ -3042,7 +3369,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
|
|
|
3042
3369
|
engineering evidence is present, and whether memory references decisions that exist and are accepted.
|
|
3043
3370
|
|
|
3044
3371
|
Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
|
|
3045
|
-
is an error,
|
|
3372
|
+
is an error, memory that references a not-yet-accepted ADR is a warning, and memory that still
|
|
3373
|
+
references a superseded decision is a warning.
|
|
3046
3374
|
|
|
3047
3375
|
Exit codes:
|
|
3048
3376
|
|
|
@@ -3129,7 +3457,7 @@ jobs:
|
|
|
3129
3457
|
}
|
|
3130
3458
|
];
|
|
3131
3459
|
function generateInitFiles(options) {
|
|
3132
|
-
const repositoryName =
|
|
3460
|
+
const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
|
|
3133
3461
|
const context = createTemplateContext({ repositoryName });
|
|
3134
3462
|
const files = neutralTemplates.map((template) => ({
|
|
3135
3463
|
path: template.path,
|
|
@@ -3156,17 +3484,17 @@ function generatePresetFiles(preset) {
|
|
|
3156
3484
|
|
|
3157
3485
|
// src/core/hooks/detect-gates.ts
|
|
3158
3486
|
import { existsSync as existsSync5 } from "fs";
|
|
3159
|
-
import { readFile as
|
|
3160
|
-
import
|
|
3487
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3488
|
+
import path16 from "path";
|
|
3161
3489
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
3162
3490
|
async function detectPreCommitGates(rootDir) {
|
|
3163
|
-
const packageJsonPath =
|
|
3491
|
+
const packageJsonPath = path16.join(rootDir, "package.json");
|
|
3164
3492
|
if (!existsSync5(packageJsonPath)) {
|
|
3165
3493
|
return [];
|
|
3166
3494
|
}
|
|
3167
3495
|
let scripts;
|
|
3168
3496
|
try {
|
|
3169
|
-
const raw = await
|
|
3497
|
+
const raw = await readFile12(packageJsonPath, "utf8");
|
|
3170
3498
|
const parsed = JSON.parse(raw);
|
|
3171
3499
|
scripts = parsed.scripts ?? {};
|
|
3172
3500
|
} catch {
|
|
@@ -3181,10 +3509,10 @@ async function detectPreCommitGates(rootDir) {
|
|
|
3181
3509
|
);
|
|
3182
3510
|
}
|
|
3183
3511
|
function detectPackageManager2(rootDir) {
|
|
3184
|
-
if (existsSync5(
|
|
3512
|
+
if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
|
|
3185
3513
|
return "pnpm";
|
|
3186
3514
|
}
|
|
3187
|
-
if (existsSync5(
|
|
3515
|
+
if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
|
|
3188
3516
|
return "yarn";
|
|
3189
3517
|
}
|
|
3190
3518
|
return "npm";
|
|
@@ -4689,8 +5017,8 @@ function parsePreset(value) {
|
|
|
4689
5017
|
if (!result.success) {
|
|
4690
5018
|
throw new PresetValidationError(
|
|
4691
5019
|
result.error.issues.map((issue) => {
|
|
4692
|
-
const
|
|
4693
|
-
return `${
|
|
5020
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
5021
|
+
return `${path19}${issue.message}`;
|
|
4694
5022
|
})
|
|
4695
5023
|
);
|
|
4696
5024
|
}
|
|
@@ -5379,7 +5707,7 @@ var InitError = class extends Error {
|
|
|
5379
5707
|
}
|
|
5380
5708
|
};
|
|
5381
5709
|
async function initProject(options) {
|
|
5382
|
-
if (options.force === true && options.reinit !== true && existsSync6(
|
|
5710
|
+
if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
|
|
5383
5711
|
throw new InitError(
|
|
5384
5712
|
"EXISTING_INSTALLATION",
|
|
5385
5713
|
"Refusing to re-initialize an existing Recall OS installation.",
|
|
@@ -5743,7 +6071,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
5743
6071
|
}
|
|
5744
6072
|
|
|
5745
6073
|
// src/core/generator/generate-module.ts
|
|
5746
|
-
import
|
|
6074
|
+
import path18 from "path";
|
|
5747
6075
|
var moduleTemplates = [
|
|
5748
6076
|
{
|
|
5749
6077
|
fileName: "MODULE.md",
|
|
@@ -5816,14 +6144,14 @@ Record durable module decisions here.
|
|
|
5816
6144
|
];
|
|
5817
6145
|
function generateModuleFiles(options) {
|
|
5818
6146
|
const slug = slugify(options.moduleName);
|
|
5819
|
-
const moduleDir =
|
|
6147
|
+
const moduleDir = path18.posix.join(options.modulesDir, slug);
|
|
5820
6148
|
const title = titleizeModuleName(options.moduleName);
|
|
5821
6149
|
const context = createTemplateContext({
|
|
5822
6150
|
slug,
|
|
5823
6151
|
title
|
|
5824
6152
|
});
|
|
5825
6153
|
return moduleTemplates.map((template) => ({
|
|
5826
|
-
path:
|
|
6154
|
+
path: path18.posix.join(moduleDir, template.fileName),
|
|
5827
6155
|
content: renderTemplate(template.content, context)
|
|
5828
6156
|
}));
|
|
5829
6157
|
}
|
|
@@ -5844,7 +6172,7 @@ var ModuleCreateError = class extends Error {
|
|
|
5844
6172
|
};
|
|
5845
6173
|
async function createModule(options) {
|
|
5846
6174
|
const slug = createModuleSlug(options.name);
|
|
5847
|
-
const config = await
|
|
6175
|
+
const config = await loadRequiredConfig5(options.rootDir);
|
|
5848
6176
|
const files = generateModuleFiles({
|
|
5849
6177
|
modulesDir: config.modulesDir,
|
|
5850
6178
|
moduleName: options.name
|
|
@@ -5898,7 +6226,7 @@ function createModuleSlug(name) {
|
|
|
5898
6226
|
throw error;
|
|
5899
6227
|
}
|
|
5900
6228
|
}
|
|
5901
|
-
async function
|
|
6229
|
+
async function loadRequiredConfig5(rootDir) {
|
|
5902
6230
|
try {
|
|
5903
6231
|
return await loadConfig(rootDir);
|
|
5904
6232
|
} catch (error) {
|
|
@@ -6072,6 +6400,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
|
|
|
6072
6400
|
});
|
|
6073
6401
|
stdout.write(formatAdrAcceptResult(result));
|
|
6074
6402
|
});
|
|
6403
|
+
adrCommand.command("supersede").description(
|
|
6404
|
+
"Record a changed decision: mark an accepted ADR superseded by a new accepted ADR."
|
|
6405
|
+
).argument("<old>", "Accepted ADR name or slug being superseded, e.g. database-postgres.").argument("<new-title>", "Title of the new decision that replaces it.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(
|
|
6406
|
+
async (oldName, newTitle, options) => {
|
|
6407
|
+
const result = await supersedeAdr({
|
|
6408
|
+
rootDir: cwd,
|
|
6409
|
+
oldName,
|
|
6410
|
+
newTitle,
|
|
6411
|
+
dryRun: options.dryRun,
|
|
6412
|
+
force: options.force
|
|
6413
|
+
});
|
|
6414
|
+
stdout.write(formatAdrSupersedeResult(result));
|
|
6415
|
+
}
|
|
6416
|
+
);
|
|
6075
6417
|
const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
|
|
6076
6418
|
moduleCommand.command("create").description("Create module memory docs.").argument("<name>", "Module name.").option("--dry-run", "Show planned writes without writing files.").option("--force", "Overwrite existing files explicitly.").action(async (name, options) => {
|
|
6077
6419
|
const result = await createModule({
|
|
@@ -6165,6 +6507,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
6165
6507
|
`);
|
|
6166
6508
|
for (const detail of error.details) {
|
|
6167
6509
|
stderr.write(`- ${detail}
|
|
6510
|
+
`);
|
|
6511
|
+
}
|
|
6512
|
+
return 1;
|
|
6513
|
+
}
|
|
6514
|
+
if (error instanceof AdrSupersedeError) {
|
|
6515
|
+
stderr.write(`${error.message}
|
|
6516
|
+
`);
|
|
6517
|
+
for (const detail of error.details) {
|
|
6518
|
+
stderr.write(`- ${detail}
|
|
6168
6519
|
`);
|
|
6169
6520
|
}
|
|
6170
6521
|
return 1;
|