recall-os 0.2.0 → 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 +44 -40
- package/dist/cli.js +805 -199
- package/dist/cli.js.map +1 -1
- package/dist/index.js +805 -199
- package/dist/index.js.map +1 -1
- package/examples/generated-flutter/AGENTS.md +13 -0
- package/examples/generated-flutter/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-flutter/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-generic/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-ios-swift/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-kotlin-android/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-laravel-api/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-laravel-react/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-laravel-vue/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-nextjs/docs/20-security/THREAT_MODEL.md +35 -3
- 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/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-python-fastapi/docs/20-security/THREAT_MODEL.md +35 -3
- 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/cli.js
CHANGED
|
@@ -134,8 +134,8 @@ function parseConfig(value) {
|
|
|
134
134
|
if (!result.success) {
|
|
135
135
|
throw new ConfigValidationError(
|
|
136
136
|
result.error.issues.map((issue) => {
|
|
137
|
-
const
|
|
138
|
-
return `${
|
|
137
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
138
|
+
return `${path19}${issue.message}`;
|
|
139
139
|
})
|
|
140
140
|
);
|
|
141
141
|
}
|
|
@@ -800,6 +800,54 @@ function generateAdrFile(options) {
|
|
|
800
800
|
}
|
|
801
801
|
];
|
|
802
802
|
}
|
|
803
|
+
var supersedingAdrTemplate = `# {{adrId}}: {{title}}
|
|
804
|
+
|
|
805
|
+
## Status
|
|
806
|
+
|
|
807
|
+
Accepted
|
|
808
|
+
|
|
809
|
+
## Supersedes
|
|
810
|
+
|
|
811
|
+
- {{supersedesRef}}
|
|
812
|
+
|
|
813
|
+
## Context
|
|
814
|
+
|
|
815
|
+
What changed, and why the previous decision no longer holds?
|
|
816
|
+
|
|
817
|
+
## Decision
|
|
818
|
+
|
|
819
|
+
What is the new decision?
|
|
820
|
+
|
|
821
|
+
## Alternatives Considered
|
|
822
|
+
|
|
823
|
+
What other options were considered?
|
|
824
|
+
|
|
825
|
+
## Consequences
|
|
826
|
+
|
|
827
|
+
What improves, what worsens, and what risks remain?
|
|
828
|
+
|
|
829
|
+
## Related Documents
|
|
830
|
+
|
|
831
|
+
- PRD:
|
|
832
|
+
- Architecture:
|
|
833
|
+
- Security:
|
|
834
|
+
- Feature:
|
|
835
|
+
`;
|
|
836
|
+
function generateSupersedingAdr(options) {
|
|
837
|
+
const slug = slugify(options.title);
|
|
838
|
+
const title = titleizeAdrTitle(options.title);
|
|
839
|
+
const context = createTemplateContext({
|
|
840
|
+
adrId: options.adrId,
|
|
841
|
+
slug,
|
|
842
|
+
title,
|
|
843
|
+
supersedesRef: options.supersedesRef
|
|
844
|
+
});
|
|
845
|
+
return {
|
|
846
|
+
path: path4.posix.join(options.adrDir, `${options.adrId}-${slug}.md`),
|
|
847
|
+
content: renderTemplate(supersedingAdrTemplate, context),
|
|
848
|
+
slug
|
|
849
|
+
};
|
|
850
|
+
}
|
|
803
851
|
function titleizeAdrTitle(title) {
|
|
804
852
|
return title.trim().replace(/[-_]+/gu, " ").replace(/\s+/gu, " ").replace(/\b\w/gu, (character) => character.toUpperCase());
|
|
805
853
|
}
|
|
@@ -890,8 +938,148 @@ async function loadRequiredConfig2(rootDir) {
|
|
|
890
938
|
}
|
|
891
939
|
}
|
|
892
940
|
|
|
893
|
-
// src/
|
|
941
|
+
// src/commands/adr/supersede.ts
|
|
942
|
+
import { readFile as readFile3, readdir as readdir3 } from "fs/promises";
|
|
894
943
|
import path5 from "path";
|
|
944
|
+
var AdrSupersedeError = class extends Error {
|
|
945
|
+
code;
|
|
946
|
+
details;
|
|
947
|
+
constructor(code, message, details = []) {
|
|
948
|
+
super(message);
|
|
949
|
+
this.name = "AdrSupersedeError";
|
|
950
|
+
this.code = code;
|
|
951
|
+
this.details = details;
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
async function supersedeAdr(options) {
|
|
955
|
+
const oldSlug = createSlug2(options.oldName, "oldName");
|
|
956
|
+
createSlug2(options.newTitle, "newTitle");
|
|
957
|
+
const config = await loadRequiredConfig3(options.rootDir);
|
|
958
|
+
const adrDirAbsolute = resolveSafePath(options.rootDir, config.adrDir).absolutePath;
|
|
959
|
+
const old = await findAcceptedAdr(adrDirAbsolute, oldSlug);
|
|
960
|
+
const next = await getNextAdrNumber(adrDirAbsolute);
|
|
961
|
+
const oldRef = old.fileName.replace(/\.md$/u, "");
|
|
962
|
+
const superseding = generateSupersedingAdr({
|
|
963
|
+
adrDir: config.adrDir,
|
|
964
|
+
adrId: next.id,
|
|
965
|
+
title: options.newTitle,
|
|
966
|
+
supersedesRef: oldRef
|
|
967
|
+
});
|
|
968
|
+
const newRef = `${next.id}-${superseding.slug}`;
|
|
969
|
+
const markedOld = old.content.replace(
|
|
970
|
+
/(##\s+Status\r?\n\r?\n)Accepted[^\n]*/u,
|
|
971
|
+
`$1Accepted \u2014 superseded by ${newRef}`
|
|
972
|
+
);
|
|
973
|
+
const writeNew = await write2(options, superseding.path, superseding.content, options.force);
|
|
974
|
+
const oldRelative = `${config.adrDir}/${old.fileName}`;
|
|
975
|
+
const writeOld = await write2(options, oldRelative, markedOld, true);
|
|
976
|
+
return {
|
|
977
|
+
oldRef,
|
|
978
|
+
oldPath: oldRelative,
|
|
979
|
+
newRef,
|
|
980
|
+
newPath: superseding.path,
|
|
981
|
+
dryRun: options.dryRun ?? false,
|
|
982
|
+
writeResult: {
|
|
983
|
+
created: [...writeNew.created, ...writeOld.created],
|
|
984
|
+
overwritten: [...writeNew.overwritten, ...writeOld.overwritten],
|
|
985
|
+
skipped: [...writeNew.skipped, ...writeOld.skipped],
|
|
986
|
+
dryRun: options.dryRun ?? false
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
async function findAcceptedAdr(adrDirAbsolute, slug) {
|
|
991
|
+
const pattern = new RegExp(`^ADR-\\d{4,}-${escapeRegExp2(slug)}\\.md$`, "u");
|
|
992
|
+
let entries;
|
|
993
|
+
try {
|
|
994
|
+
entries = await readdir3(adrDirAbsolute, { withFileTypes: true });
|
|
995
|
+
} catch (error) {
|
|
996
|
+
const nodeError = error;
|
|
997
|
+
if (nodeError.code === "ENOENT") {
|
|
998
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`);
|
|
999
|
+
}
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
const match = entries.find((entry) => entry.isFile() && pattern.test(entry.name));
|
|
1003
|
+
if (match === void 0) {
|
|
1004
|
+
throw new AdrSupersedeError("NOT_FOUND", `No accepted ADR found for "${slug}".`, [
|
|
1005
|
+
`Looked for an ADR-####-${slug}.md in the ADR directory.`
|
|
1006
|
+
]);
|
|
1007
|
+
}
|
|
1008
|
+
const content = await readFile3(path5.join(adrDirAbsolute, match.name), "utf8");
|
|
1009
|
+
if (!/(##\s+Status\r?\n\r?\n)Accepted\b/u.test(content)) {
|
|
1010
|
+
throw new AdrSupersedeError(
|
|
1011
|
+
"NOT_ACCEPTED",
|
|
1012
|
+
`ADR ${match.name} is not Accepted, so there is nothing to supersede.`,
|
|
1013
|
+
["Only an accepted decision can be superseded. Accept it first with `recall adr accept`."]
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return { fileName: match.name, content };
|
|
1017
|
+
}
|
|
1018
|
+
async function write2(options, relativePath, content, force) {
|
|
1019
|
+
const plan = createWritePlan({
|
|
1020
|
+
rootDir: options.rootDir,
|
|
1021
|
+
files: [{ path: relativePath, content }],
|
|
1022
|
+
force
|
|
1023
|
+
});
|
|
1024
|
+
if (plan.hasErrors) {
|
|
1025
|
+
throw new AdrSupersedeError(
|
|
1026
|
+
"WRITE_PLAN_ERROR",
|
|
1027
|
+
"ADR supersede write plan contains errors.",
|
|
1028
|
+
plan.entries.filter(
|
|
1029
|
+
(entry) => entry.action === "error"
|
|
1030
|
+
).map((entry) => `${entry.path}: ${entry.reason}`)
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
return executeWritePlan(plan, { dryRun: options.dryRun });
|
|
1034
|
+
}
|
|
1035
|
+
function formatAdrSupersedeResult(result) {
|
|
1036
|
+
const lines = [
|
|
1037
|
+
result.dryRun ? "Recall OS ADR supersede dry run complete." : "Recall OS ADR supersede complete.",
|
|
1038
|
+
`Superseded: ${result.oldPath} (now marked superseded by ${result.newRef})`,
|
|
1039
|
+
`New decision: ${result.newPath}`
|
|
1040
|
+
];
|
|
1041
|
+
appendWriteSummary(lines, { dryRun: result.dryRun, writeResult: result.writeResult });
|
|
1042
|
+
if (!result.dryRun) {
|
|
1043
|
+
appendNextSteps(lines, [
|
|
1044
|
+
`Fill ${result.newPath}: Context (what changed), Decision, Alternatives, Consequences.`,
|
|
1045
|
+
`${result.oldRef} stays in history as superseded; update any memory that still relies on it.`,
|
|
1046
|
+
"Run `recall doctor` \u2014 it flags memory that still references the superseded decision."
|
|
1047
|
+
]);
|
|
1048
|
+
}
|
|
1049
|
+
return `${lines.join("\n")}
|
|
1050
|
+
`;
|
|
1051
|
+
}
|
|
1052
|
+
function createSlug2(name, field) {
|
|
1053
|
+
const withoutPrefix = name.replace(/^ADR-(?:PROPOSED-|\d{4,}-)/iu, "");
|
|
1054
|
+
try {
|
|
1055
|
+
return slugify(withoutPrefix);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error instanceof SlugifyError) {
|
|
1058
|
+
throw new AdrSupersedeError("INVALID_ADR_NAME", `Invalid ${field}: ${error.message}`);
|
|
1059
|
+
}
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
function escapeRegExp2(value) {
|
|
1064
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
1065
|
+
}
|
|
1066
|
+
async function loadRequiredConfig3(rootDir) {
|
|
1067
|
+
try {
|
|
1068
|
+
return await loadConfig(rootDir);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (error instanceof ConfigLoadError || error instanceof ConfigValidationError) {
|
|
1071
|
+
throw new AdrSupersedeError(
|
|
1072
|
+
"CONFIG_REQUIRED",
|
|
1073
|
+
"Recall OS config not found or invalid. Run `recall init` first.",
|
|
1074
|
+
[error.message]
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
throw error;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// src/core/generator/generate-feature.ts
|
|
1082
|
+
import path6 from "path";
|
|
895
1083
|
var featureTemplates = [
|
|
896
1084
|
{
|
|
897
1085
|
fileName: "PRD.md",
|
|
@@ -1043,7 +1231,7 @@ Pending.
|
|
|
1043
1231
|
];
|
|
1044
1232
|
function generateFeatureFiles(options) {
|
|
1045
1233
|
const slug = slugify(options.featureName);
|
|
1046
|
-
const featureDir =
|
|
1234
|
+
const featureDir = path6.posix.join(options.featuresDir, `${options.featureId}-${slug}`);
|
|
1047
1235
|
const title = titleizeFeatureName(options.featureName);
|
|
1048
1236
|
const context = createTemplateContext({
|
|
1049
1237
|
featureId: options.featureId,
|
|
@@ -1051,7 +1239,7 @@ function generateFeatureFiles(options) {
|
|
|
1051
1239
|
title
|
|
1052
1240
|
});
|
|
1053
1241
|
return featureTemplates.map((template) => ({
|
|
1054
|
-
path:
|
|
1242
|
+
path: path6.posix.join(featureDir, template.fileName),
|
|
1055
1243
|
content: renderTemplate(template.content, context)
|
|
1056
1244
|
}));
|
|
1057
1245
|
}
|
|
@@ -1060,7 +1248,7 @@ function titleizeFeatureName(featureName) {
|
|
|
1060
1248
|
}
|
|
1061
1249
|
|
|
1062
1250
|
// src/core/naming/feature-number.ts
|
|
1063
|
-
import { readdir as
|
|
1251
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
1064
1252
|
var FEATURE_FOLDER_PATTERN = /^F-(\d{3,})-([a-z0-9]+(?:-[a-z0-9]+)*)$/u;
|
|
1065
1253
|
async function getFeatureFolderForSlug(featuresDirAbsolutePath, slug) {
|
|
1066
1254
|
const existingFolders = await readExistingFeatureFolders(featuresDirAbsolutePath);
|
|
@@ -1093,7 +1281,7 @@ function formatFeatureNumber(featureNumber) {
|
|
|
1093
1281
|
async function readExistingFeatureFolders(featuresDirAbsolutePath) {
|
|
1094
1282
|
let entries;
|
|
1095
1283
|
try {
|
|
1096
|
-
entries = await
|
|
1284
|
+
entries = await readdir4(featuresDirAbsolutePath, { withFileTypes: true });
|
|
1097
1285
|
} catch (error) {
|
|
1098
1286
|
const nodeError = error;
|
|
1099
1287
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1132,7 +1320,7 @@ var FeatureCreateError = class extends Error {
|
|
|
1132
1320
|
};
|
|
1133
1321
|
async function createFeature(options) {
|
|
1134
1322
|
const slug = createFeatureSlug(options.name);
|
|
1135
|
-
const config = await
|
|
1323
|
+
const config = await loadRequiredConfig4(options.rootDir);
|
|
1136
1324
|
const featuresDirPath = resolveSafePath(options.rootDir, config.featuresDir);
|
|
1137
1325
|
const featureFolder = await getFeatureFolderForSlug(featuresDirPath.absolutePath, slug);
|
|
1138
1326
|
const files = generateFeatureFiles({
|
|
@@ -1191,7 +1379,7 @@ function createFeatureSlug(name) {
|
|
|
1191
1379
|
throw error;
|
|
1192
1380
|
}
|
|
1193
1381
|
}
|
|
1194
|
-
async function
|
|
1382
|
+
async function loadRequiredConfig4(rootDir) {
|
|
1195
1383
|
try {
|
|
1196
1384
|
return await loadConfig(rootDir);
|
|
1197
1385
|
} catch (error) {
|
|
@@ -1230,113 +1418,33 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1230
1418
|
});
|
|
1231
1419
|
}
|
|
1232
1420
|
|
|
1233
|
-
// src/core/adopt/generate-adoption.ts
|
|
1234
|
-
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1235
|
-
function generateAdoptionFiles(options) {
|
|
1236
|
-
const files = [
|
|
1237
|
-
{
|
|
1238
|
-
path: ADOPTION_REPORT_PATH,
|
|
1239
|
-
content: renderReport(options.adrDir, options.signals)
|
|
1240
|
-
}
|
|
1241
|
-
];
|
|
1242
|
-
for (const framework of options.signals.frameworks) {
|
|
1243
|
-
files.push({
|
|
1244
|
-
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1245
|
-
content: renderProposedAdr(framework)
|
|
1246
|
-
});
|
|
1247
|
-
}
|
|
1248
|
-
return files;
|
|
1249
|
-
}
|
|
1250
|
-
function renderReport(adrDir, signals) {
|
|
1251
|
-
return `# Adoption Report
|
|
1252
|
-
|
|
1253
|
-
## Status
|
|
1254
|
-
|
|
1255
|
-
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1256
|
-
is accepted repository memory until you accept it.
|
|
1257
|
-
|
|
1258
|
-
## Detected Signals
|
|
1259
|
-
|
|
1260
|
-
- Languages: ${formatList(signals.languages)}
|
|
1261
|
-
- Package manager: ${signals.packageManager ?? "none detected"}
|
|
1262
|
-
- Frameworks: ${formatList(signals.frameworks)}
|
|
1263
|
-
- Tests present: ${formatBool(signals.hasTests)}
|
|
1264
|
-
- README present: ${formatBool(signals.hasReadme)}
|
|
1265
|
-
- Docs folder present: ${formatBool(signals.hasDocs)}
|
|
1266
|
-
|
|
1267
|
-
## Proposed Decisions
|
|
1268
|
-
|
|
1269
|
-
${renderProposedDecisions(adrDir, signals)}
|
|
1270
|
-
|
|
1271
|
-
## Review Checklist
|
|
1272
|
-
|
|
1273
|
-
- [ ] Confirm the detected languages and package manager.
|
|
1274
|
-
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1275
|
-
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1276
|
-
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1277
|
-
|
|
1278
|
-
## Notes
|
|
1279
|
-
|
|
1280
|
-
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1281
|
-
files. No repository code was executed and no decision was accepted automatically.
|
|
1282
|
-
`;
|
|
1283
|
-
}
|
|
1284
|
-
function renderProposedDecisions(adrDir, signals) {
|
|
1285
|
-
if (signals.frameworks.length === 0) {
|
|
1286
|
-
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1287
|
-
}
|
|
1288
|
-
return signals.frameworks.map(
|
|
1289
|
-
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1290
|
-
).join("\n");
|
|
1291
|
-
}
|
|
1292
|
-
function renderProposedAdr(framework) {
|
|
1293
|
-
return `# Proposed ADR: Use ${framework}
|
|
1294
|
-
|
|
1295
|
-
## Status
|
|
1296
|
-
|
|
1297
|
-
Proposed
|
|
1298
|
-
|
|
1299
|
-
## Context
|
|
1300
|
-
|
|
1301
|
-
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1302
|
-
|
|
1303
|
-
## Decision
|
|
1304
|
-
|
|
1305
|
-
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1306
|
-
and is not accepted until a human reviews and accepts it.
|
|
1307
|
-
|
|
1308
|
-
## Alternatives Considered
|
|
1309
|
-
|
|
1310
|
-
- Record a different framework.
|
|
1311
|
-
- Leave the decision unrecorded for now.
|
|
1312
|
-
|
|
1313
|
-
## Consequences
|
|
1314
|
-
|
|
1315
|
-
- Captures a framework already in use as reviewable repository memory.
|
|
1316
|
-
- Requires explicit human acceptance before it becomes repository truth.
|
|
1317
|
-
|
|
1318
|
-
## Related Documents
|
|
1319
|
-
|
|
1320
|
-
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1321
|
-
- The adoption report generated alongside this proposal.
|
|
1322
|
-
`;
|
|
1323
|
-
}
|
|
1324
|
-
function frameworkSlug(framework) {
|
|
1325
|
-
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1326
|
-
}
|
|
1327
|
-
function formatList(values) {
|
|
1328
|
-
return values.length > 0 ? values.join(", ") : "none detected";
|
|
1329
|
-
}
|
|
1330
|
-
function formatBool(value) {
|
|
1331
|
-
return value ? "yes" : "no";
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
1421
|
// src/core/adopt/inspect-repo.ts
|
|
1335
1422
|
import { existsSync as existsSync3 } from "fs";
|
|
1336
|
-
import { readFile as
|
|
1337
|
-
import
|
|
1423
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1424
|
+
import path7 from "path";
|
|
1425
|
+
var FRAMEWORK_SOURCES = {
|
|
1426
|
+
"Next.js": "package.json",
|
|
1427
|
+
React: "package.json",
|
|
1428
|
+
NestJS: "package.json",
|
|
1429
|
+
Express: "package.json",
|
|
1430
|
+
FastAPI: "pyproject.toml / requirements.txt",
|
|
1431
|
+
Flask: "pyproject.toml / requirements.txt",
|
|
1432
|
+
Django: "pyproject.toml / requirements.txt",
|
|
1433
|
+
Gin: "go.mod",
|
|
1434
|
+
Echo: "go.mod",
|
|
1435
|
+
Fiber: "go.mod",
|
|
1436
|
+
Chi: "go.mod",
|
|
1437
|
+
"Spring Boot": "pom.xml / build.gradle",
|
|
1438
|
+
"Actix Web": "Cargo.toml",
|
|
1439
|
+
Axum: "Cargo.toml",
|
|
1440
|
+
Rocket: "Cargo.toml",
|
|
1441
|
+
Laravel: "composer.json",
|
|
1442
|
+
Symfony: "composer.json",
|
|
1443
|
+
"Ruby on Rails": "Gemfile",
|
|
1444
|
+
Flutter: "pubspec.yaml"
|
|
1445
|
+
};
|
|
1338
1446
|
async function inspectRepo(rootDir) {
|
|
1339
|
-
const has = (relativePath) => existsSync3(
|
|
1447
|
+
const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
|
|
1340
1448
|
const languages = /* @__PURE__ */ new Set();
|
|
1341
1449
|
const frameworks = /* @__PURE__ */ new Set();
|
|
1342
1450
|
const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
|
|
@@ -1368,6 +1476,22 @@ async function inspectRepo(rootDir) {
|
|
|
1368
1476
|
languages.add("Dart");
|
|
1369
1477
|
frameworks.add("Flutter");
|
|
1370
1478
|
}
|
|
1479
|
+
if (has("composer.json")) {
|
|
1480
|
+
languages.add("PHP");
|
|
1481
|
+
const composer = (await readText(rootDir, "composer.json")).toLowerCase();
|
|
1482
|
+
if (composer.includes("laravel/framework")) {
|
|
1483
|
+
frameworks.add("Laravel");
|
|
1484
|
+
} else if (composer.includes("symfony/")) {
|
|
1485
|
+
frameworks.add("Symfony");
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (has("Gemfile")) {
|
|
1489
|
+
languages.add("Ruby");
|
|
1490
|
+
const gemfile = (await readText(rootDir, "Gemfile")).toLowerCase();
|
|
1491
|
+
if (gemfile.includes("rails")) {
|
|
1492
|
+
frameworks.add("Ruby on Rails");
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1371
1495
|
const deps = collectDependencies(pkg);
|
|
1372
1496
|
if ("next" in deps) {
|
|
1373
1497
|
frameworks.add("Next.js");
|
|
@@ -1413,25 +1537,147 @@ async function inspectRepo(rootDir) {
|
|
|
1413
1537
|
frameworks.add("Rocket");
|
|
1414
1538
|
}
|
|
1415
1539
|
}
|
|
1416
|
-
|
|
1417
|
-
if (has("pnpm-lock.yaml")) {
|
|
1418
|
-
packageManager = "pnpm";
|
|
1419
|
-
} else if (has("yarn.lock")) {
|
|
1420
|
-
packageManager = "yarn";
|
|
1421
|
-
} else if (has("package-lock.json")) {
|
|
1422
|
-
packageManager = "npm";
|
|
1423
|
-
}
|
|
1540
|
+
const [packageManager, packageManagerSource] = detectPackageManager(has);
|
|
1424
1541
|
const scripts = pkg !== null && isRecord(pkg.scripts) ? pkg.scripts : {};
|
|
1425
|
-
const
|
|
1542
|
+
const testsEvidence = await detectTestsEvidence(rootDir, has, "test" in scripts, python);
|
|
1426
1543
|
return {
|
|
1427
1544
|
languages: [...languages],
|
|
1428
1545
|
packageManager,
|
|
1546
|
+
packageManagerSource,
|
|
1429
1547
|
frameworks: [...frameworks],
|
|
1430
|
-
hasTests,
|
|
1548
|
+
hasTests: testsEvidence !== null,
|
|
1549
|
+
testsEvidence,
|
|
1431
1550
|
hasReadme: has("README.md") || has("README"),
|
|
1432
1551
|
hasDocs: has("docs")
|
|
1433
1552
|
};
|
|
1434
1553
|
}
|
|
1554
|
+
function summarizeSignals(signals) {
|
|
1555
|
+
const lines = [];
|
|
1556
|
+
lines.push(`- Languages: ${formatList(signals.languages)}`);
|
|
1557
|
+
lines.push(
|
|
1558
|
+
signals.packageManager === null ? "- Package manager: none detected" : `- Package manager: ${signals.packageManager}${signals.packageManagerSource === null ? "" : ` (from \`${signals.packageManagerSource}\`)`}`
|
|
1559
|
+
);
|
|
1560
|
+
if (signals.frameworks.length === 0) {
|
|
1561
|
+
lines.push("- Frameworks: none detected");
|
|
1562
|
+
} else {
|
|
1563
|
+
const withSource = signals.frameworks.map((framework) => {
|
|
1564
|
+
const source = FRAMEWORK_SOURCES[framework];
|
|
1565
|
+
return source === void 0 ? framework : `${framework} (from \`${source}\`)`;
|
|
1566
|
+
});
|
|
1567
|
+
lines.push(`- Frameworks: ${withSource.join(", ")}`);
|
|
1568
|
+
}
|
|
1569
|
+
lines.push(
|
|
1570
|
+
signals.testsEvidence === null ? "- Tests: none detected \u2014 if tests exist, point Recall at them by correcting this report" : `- Tests: detected via ${signals.testsEvidence}`
|
|
1571
|
+
);
|
|
1572
|
+
lines.push(`- README present: ${signals.hasReadme ? "yes" : "no"}`);
|
|
1573
|
+
lines.push(`- Docs folder present: ${signals.hasDocs ? "yes" : "no"}`);
|
|
1574
|
+
return lines;
|
|
1575
|
+
}
|
|
1576
|
+
function formatList(values) {
|
|
1577
|
+
return values.length === 0 ? "none detected" : values.join(", ");
|
|
1578
|
+
}
|
|
1579
|
+
function detectPackageManager(has) {
|
|
1580
|
+
const candidates = [
|
|
1581
|
+
[has("go.mod"), "Go modules", "go.mod"],
|
|
1582
|
+
[has("Cargo.toml"), "Cargo", "Cargo.toml"],
|
|
1583
|
+
[has("pom.xml"), "Maven", "pom.xml"],
|
|
1584
|
+
[has("build.gradle"), "Gradle", "build.gradle"],
|
|
1585
|
+
[has("build.gradle.kts"), "Gradle", "build.gradle.kts"],
|
|
1586
|
+
[has("composer.json"), "Composer", "composer.json"],
|
|
1587
|
+
[has("Gemfile"), "Bundler", "Gemfile"],
|
|
1588
|
+
[has("Package.swift"), "Swift Package Manager", "Package.swift"],
|
|
1589
|
+
[has("pubspec.yaml"), "pub", "pubspec.yaml"],
|
|
1590
|
+
[has("uv.lock"), "uv", "uv.lock"],
|
|
1591
|
+
[has("poetry.lock"), "Poetry", "poetry.lock"],
|
|
1592
|
+
[has("requirements.txt"), "pip", "requirements.txt"],
|
|
1593
|
+
[has("pyproject.toml"), "pip", "pyproject.toml"],
|
|
1594
|
+
[has("pnpm-lock.yaml"), "pnpm", "pnpm-lock.yaml"],
|
|
1595
|
+
[has("yarn.lock"), "yarn", "yarn.lock"],
|
|
1596
|
+
[has("package-lock.json"), "npm", "package-lock.json"],
|
|
1597
|
+
[has("package.json"), "npm", "package.json"]
|
|
1598
|
+
];
|
|
1599
|
+
for (const [present, name, source] of candidates) {
|
|
1600
|
+
if (present) {
|
|
1601
|
+
return [name, source];
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
return [null, null];
|
|
1605
|
+
}
|
|
1606
|
+
async function detectTestsEvidence(rootDir, has, hasTestScript, pythonText) {
|
|
1607
|
+
if (has("tests")) {
|
|
1608
|
+
return "`tests/` directory";
|
|
1609
|
+
}
|
|
1610
|
+
if (has("test")) {
|
|
1611
|
+
return "`test/` directory";
|
|
1612
|
+
}
|
|
1613
|
+
if (has("__tests__")) {
|
|
1614
|
+
return "`__tests__/` directory";
|
|
1615
|
+
}
|
|
1616
|
+
if (has("pytest.ini") || pythonText.includes("pytest")) {
|
|
1617
|
+
return "pytest configuration";
|
|
1618
|
+
}
|
|
1619
|
+
if (has("phpunit.xml") || has("phpunit.xml.dist")) {
|
|
1620
|
+
return "PHPUnit configuration";
|
|
1621
|
+
}
|
|
1622
|
+
if (hasTestScript) {
|
|
1623
|
+
return '`"test"` script in package.json';
|
|
1624
|
+
}
|
|
1625
|
+
const testFile = await findTestFile(rootDir);
|
|
1626
|
+
if (testFile !== null) {
|
|
1627
|
+
return `\`${testFile}\``;
|
|
1628
|
+
}
|
|
1629
|
+
return null;
|
|
1630
|
+
}
|
|
1631
|
+
var TEST_FILE_PATTERNS = [
|
|
1632
|
+
/_test\.go$/u,
|
|
1633
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/u,
|
|
1634
|
+
/^test_.+\.py$/u,
|
|
1635
|
+
/_test\.py$/u,
|
|
1636
|
+
/.+Tests?\.(java|kt)$/u,
|
|
1637
|
+
/.+Test\.php$/u,
|
|
1638
|
+
/_spec\.rb$/u,
|
|
1639
|
+
/_test\.rb$/u
|
|
1640
|
+
];
|
|
1641
|
+
var TEST_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1642
|
+
"node_modules",
|
|
1643
|
+
"vendor",
|
|
1644
|
+
"dist",
|
|
1645
|
+
"build",
|
|
1646
|
+
"target",
|
|
1647
|
+
"coverage",
|
|
1648
|
+
"Pods",
|
|
1649
|
+
"__pycache__"
|
|
1650
|
+
]);
|
|
1651
|
+
async function findTestFile(rootDir) {
|
|
1652
|
+
let budget = 4e3;
|
|
1653
|
+
const stack = [rootDir];
|
|
1654
|
+
while (stack.length > 0 && budget > 0) {
|
|
1655
|
+
const dir = stack.pop();
|
|
1656
|
+
if (dir === void 0) {
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
let entries;
|
|
1660
|
+
try {
|
|
1661
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1662
|
+
} catch {
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
for (const entry of entries) {
|
|
1666
|
+
budget -= 1;
|
|
1667
|
+
if (budget <= 0) {
|
|
1668
|
+
break;
|
|
1669
|
+
}
|
|
1670
|
+
if (entry.isDirectory()) {
|
|
1671
|
+
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1672
|
+
stack.push(path7.join(dir, entry.name));
|
|
1673
|
+
}
|
|
1674
|
+
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1675
|
+
return path7.relative(rootDir, path7.join(dir, entry.name));
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
return null;
|
|
1680
|
+
}
|
|
1435
1681
|
function collectDependencies(pkg) {
|
|
1436
1682
|
if (pkg === null) {
|
|
1437
1683
|
return {};
|
|
@@ -1450,7 +1696,7 @@ async function readJson(rootDir, relativePath) {
|
|
|
1450
1696
|
}
|
|
1451
1697
|
async function readText(rootDir, relativePath) {
|
|
1452
1698
|
try {
|
|
1453
|
-
return await
|
|
1699
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1454
1700
|
} catch {
|
|
1455
1701
|
return "";
|
|
1456
1702
|
}
|
|
@@ -1459,6 +1705,100 @@ function isRecord(value) {
|
|
|
1459
1705
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1460
1706
|
}
|
|
1461
1707
|
|
|
1708
|
+
// src/core/adopt/generate-adoption.ts
|
|
1709
|
+
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1710
|
+
function generateAdoptionFiles(options) {
|
|
1711
|
+
const files = [
|
|
1712
|
+
{
|
|
1713
|
+
path: ADOPTION_REPORT_PATH,
|
|
1714
|
+
content: renderReport(options.adrDir, options.signals)
|
|
1715
|
+
}
|
|
1716
|
+
];
|
|
1717
|
+
for (const framework of options.signals.frameworks) {
|
|
1718
|
+
files.push({
|
|
1719
|
+
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1720
|
+
content: renderProposedAdr(framework)
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
return files;
|
|
1724
|
+
}
|
|
1725
|
+
function renderReport(adrDir, signals) {
|
|
1726
|
+
return `# Adoption Report
|
|
1727
|
+
|
|
1728
|
+
## Status
|
|
1729
|
+
|
|
1730
|
+
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1731
|
+
is accepted repository memory until you accept it.
|
|
1732
|
+
|
|
1733
|
+
## Detected Signals
|
|
1734
|
+
|
|
1735
|
+
Each signal notes the file it was inferred from. If one is wrong, correct the source or edit this
|
|
1736
|
+
report \u2014 nothing here is accepted.
|
|
1737
|
+
|
|
1738
|
+
${summarizeSignals(signals).join("\n")}
|
|
1739
|
+
|
|
1740
|
+
## Proposed Decisions
|
|
1741
|
+
|
|
1742
|
+
${renderProposedDecisions(adrDir, signals)}
|
|
1743
|
+
|
|
1744
|
+
## Review Checklist
|
|
1745
|
+
|
|
1746
|
+
- [ ] Confirm the detected languages and package manager (and the source each was read from).
|
|
1747
|
+
- [ ] Confirm where tests were detected, or point Recall at the right location if it is wrong.
|
|
1748
|
+
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1749
|
+
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1750
|
+
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1751
|
+
|
|
1752
|
+
## Notes
|
|
1753
|
+
|
|
1754
|
+
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1755
|
+
files. No repository code was executed and no decision was accepted automatically.
|
|
1756
|
+
`;
|
|
1757
|
+
}
|
|
1758
|
+
function renderProposedDecisions(adrDir, signals) {
|
|
1759
|
+
if (signals.frameworks.length === 0) {
|
|
1760
|
+
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1761
|
+
}
|
|
1762
|
+
return signals.frameworks.map(
|
|
1763
|
+
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1764
|
+
).join("\n");
|
|
1765
|
+
}
|
|
1766
|
+
function renderProposedAdr(framework) {
|
|
1767
|
+
return `# Proposed ADR: Use ${framework}
|
|
1768
|
+
|
|
1769
|
+
## Status
|
|
1770
|
+
|
|
1771
|
+
Proposed
|
|
1772
|
+
|
|
1773
|
+
## Context
|
|
1774
|
+
|
|
1775
|
+
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1776
|
+
|
|
1777
|
+
## Decision
|
|
1778
|
+
|
|
1779
|
+
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1780
|
+
and is not accepted until a human reviews and accepts it.
|
|
1781
|
+
|
|
1782
|
+
## Alternatives Considered
|
|
1783
|
+
|
|
1784
|
+
- Record a different framework.
|
|
1785
|
+
- Leave the decision unrecorded for now.
|
|
1786
|
+
|
|
1787
|
+
## Consequences
|
|
1788
|
+
|
|
1789
|
+
- Captures a framework already in use as reviewable repository memory.
|
|
1790
|
+
- Requires explicit human acceptance before it becomes repository truth.
|
|
1791
|
+
|
|
1792
|
+
## Related Documents
|
|
1793
|
+
|
|
1794
|
+
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1795
|
+
- The adoption report generated alongside this proposal.
|
|
1796
|
+
`;
|
|
1797
|
+
}
|
|
1798
|
+
function frameworkSlug(framework) {
|
|
1799
|
+
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1462
1802
|
// src/commands/adopt.ts
|
|
1463
1803
|
var AdoptError = class extends Error {
|
|
1464
1804
|
code;
|
|
@@ -1532,8 +1872,8 @@ function formatList2(values) {
|
|
|
1532
1872
|
|
|
1533
1873
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1534
1874
|
import { existsSync as existsSync4 } from "fs";
|
|
1535
|
-
import { readFile as
|
|
1536
|
-
import
|
|
1875
|
+
import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
|
|
1876
|
+
import path8 from "path";
|
|
1537
1877
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1538
1878
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1539
1879
|
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
@@ -1550,7 +1890,7 @@ async function checkCodeReferences(context) {
|
|
|
1550
1890
|
continue;
|
|
1551
1891
|
}
|
|
1552
1892
|
for (const doc of FEATURE_DOCS) {
|
|
1553
|
-
const relativePath =
|
|
1893
|
+
const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1554
1894
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1555
1895
|
}
|
|
1556
1896
|
}
|
|
@@ -1560,7 +1900,7 @@ async function checkCodeReferences(context) {
|
|
|
1560
1900
|
continue;
|
|
1561
1901
|
}
|
|
1562
1902
|
for (const doc of MODULE_DOCS) {
|
|
1563
|
-
const relativePath =
|
|
1903
|
+
const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1564
1904
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1565
1905
|
}
|
|
1566
1906
|
}
|
|
@@ -1579,7 +1919,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1579
1919
|
continue;
|
|
1580
1920
|
}
|
|
1581
1921
|
seen.add(reference);
|
|
1582
|
-
if (!existsSync4(
|
|
1922
|
+
if (!existsSync4(path8.join(rootDir, reference))) {
|
|
1583
1923
|
findings.push({
|
|
1584
1924
|
severity: "warning",
|
|
1585
1925
|
check: "drift-code-reference",
|
|
@@ -1592,7 +1932,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1592
1932
|
}
|
|
1593
1933
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1594
1934
|
try {
|
|
1595
|
-
return await
|
|
1935
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1596
1936
|
} catch (error) {
|
|
1597
1937
|
const nodeError = error;
|
|
1598
1938
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1603,7 +1943,7 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1603
1943
|
}
|
|
1604
1944
|
async function readFileIfExists(rootDir, relativePath) {
|
|
1605
1945
|
try {
|
|
1606
|
-
return await
|
|
1946
|
+
return await readFile5(path8.join(rootDir, relativePath), "utf8");
|
|
1607
1947
|
} catch (error) {
|
|
1608
1948
|
const nodeError = error;
|
|
1609
1949
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1614,12 +1954,12 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1614
1954
|
}
|
|
1615
1955
|
|
|
1616
1956
|
// src/core/doctor/checks/config-check.ts
|
|
1617
|
-
import { readFile as
|
|
1957
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1618
1958
|
async function checkConfig(rootDir) {
|
|
1619
1959
|
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1620
1960
|
let rawConfig;
|
|
1621
1961
|
try {
|
|
1622
|
-
rawConfig = await
|
|
1962
|
+
rawConfig = await readFile6(configPath.absolutePath, "utf8");
|
|
1623
1963
|
} catch (error) {
|
|
1624
1964
|
const nodeError = error;
|
|
1625
1965
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1682,9 +2022,12 @@ async function checkConfig(rootDir) {
|
|
|
1682
2022
|
}
|
|
1683
2023
|
|
|
1684
2024
|
// src/core/doctor/checks/content-check.ts
|
|
1685
|
-
import { readFile as
|
|
1686
|
-
import
|
|
2025
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
2026
|
+
import path9 from "path";
|
|
1687
2027
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2028
|
+
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2029
|
+
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
2030
|
+
var THREAT_MODEL_PATH = "docs/20-security/THREAT_MODEL.md";
|
|
1688
2031
|
async function checkContent(context) {
|
|
1689
2032
|
if (context.config === void 0) {
|
|
1690
2033
|
return [];
|
|
@@ -1695,7 +2038,7 @@ async function checkContent(context) {
|
|
|
1695
2038
|
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1696
2039
|
);
|
|
1697
2040
|
for (const folder of featureFolders) {
|
|
1698
|
-
const prdPath =
|
|
2041
|
+
const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1699
2042
|
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1700
2043
|
if (prd === void 0) {
|
|
1701
2044
|
continue;
|
|
@@ -1719,8 +2062,16 @@ async function checkContent(context) {
|
|
|
1719
2062
|
}
|
|
1720
2063
|
const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
|
|
1721
2064
|
const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
|
|
2065
|
+
const adrEntries = await readDirIfExists2(context.rootDir, context.config.adrDir);
|
|
2066
|
+
const acceptedAdrs = adrEntries.filter(
|
|
2067
|
+
(entry) => entry.isFile() && acceptedAdrPattern.test(entry.name)
|
|
2068
|
+
);
|
|
2069
|
+
const hasWork = featureFolders.length > 0 || moduleFolders.length > 0 || acceptedAdrs.length > 0;
|
|
2070
|
+
if (hasWork) {
|
|
2071
|
+
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
2072
|
+
}
|
|
1722
2073
|
for (const folder of moduleFolders) {
|
|
1723
|
-
const modulePath =
|
|
2074
|
+
const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1724
2075
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1725
2076
|
if (moduleDoc === void 0) {
|
|
1726
2077
|
continue;
|
|
@@ -1744,6 +2095,28 @@ async function checkContent(context) {
|
|
|
1744
2095
|
}
|
|
1745
2096
|
return findings;
|
|
1746
2097
|
}
|
|
2098
|
+
async function checkSecurityDoc(rootDir) {
|
|
2099
|
+
const findings = [];
|
|
2100
|
+
const security = await readFileIfExists2(rootDir, SECURITY_MODEL_PATH);
|
|
2101
|
+
if (security !== void 0 && sectionIsUnfilled(security, "Authentication And Authorization")) {
|
|
2102
|
+
findings.push({
|
|
2103
|
+
severity: "warning",
|
|
2104
|
+
check: "content-security",
|
|
2105
|
+
message: "Security model authentication and authorization section is still an unfilled template.",
|
|
2106
|
+
path: SECURITY_MODEL_PATH
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
const threat = await readFileIfExists2(rootDir, THREAT_MODEL_PATH);
|
|
2110
|
+
if (threat !== void 0 && sectionIsUnfilled(threat, "Assets")) {
|
|
2111
|
+
findings.push({
|
|
2112
|
+
severity: "warning",
|
|
2113
|
+
check: "content-threat-model",
|
|
2114
|
+
message: "Threat model assets section is still an unfilled template.",
|
|
2115
|
+
path: THREAT_MODEL_PATH
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
return findings;
|
|
2119
|
+
}
|
|
1747
2120
|
function sectionIsUnfilled(content, heading) {
|
|
1748
2121
|
const section = getSection(content, heading);
|
|
1749
2122
|
return section !== void 0 && isUnfilled(section);
|
|
@@ -1756,7 +2129,7 @@ function isUnfilled(value) {
|
|
|
1756
2129
|
if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
|
|
1757
2130
|
return true;
|
|
1758
2131
|
}
|
|
1759
|
-
return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns");
|
|
2132
|
+
return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns") || normalized.includes("describe how this repository authenticates") || normalized.includes("describe what this repository must protect");
|
|
1760
2133
|
}
|
|
1761
2134
|
function getSection(content, heading) {
|
|
1762
2135
|
const lines = content.split(/\r?\n/u);
|
|
@@ -1776,7 +2149,7 @@ function getSection(content, heading) {
|
|
|
1776
2149
|
}
|
|
1777
2150
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1778
2151
|
try {
|
|
1779
|
-
return await
|
|
2152
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1780
2153
|
} catch (error) {
|
|
1781
2154
|
const nodeError = error;
|
|
1782
2155
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1787,7 +2160,7 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1787
2160
|
}
|
|
1788
2161
|
async function readFileIfExists2(rootDir, relativePath) {
|
|
1789
2162
|
try {
|
|
1790
|
-
return await
|
|
2163
|
+
return await readFile7(path9.join(rootDir, relativePath), "utf8");
|
|
1791
2164
|
} catch (error) {
|
|
1792
2165
|
const nodeError = error;
|
|
1793
2166
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1798,8 +2171,8 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1798
2171
|
}
|
|
1799
2172
|
|
|
1800
2173
|
// src/core/doctor/checks/drift-check.ts
|
|
1801
|
-
import { readFile as
|
|
1802
|
-
import
|
|
2174
|
+
import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2175
|
+
import path10 from "path";
|
|
1803
2176
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1804
2177
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1805
2178
|
async function checkDrift(context) {
|
|
@@ -1816,12 +2189,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
1816
2189
|
const known = /* @__PURE__ */ new Map();
|
|
1817
2190
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
1818
2191
|
for (const file of files) {
|
|
1819
|
-
const match = adrFilePattern.exec(
|
|
2192
|
+
const match = adrFilePattern.exec(path10.basename(file));
|
|
1820
2193
|
if (match === null) {
|
|
1821
2194
|
continue;
|
|
1822
2195
|
}
|
|
1823
2196
|
const id = `ADR-${match[1]}`;
|
|
1824
|
-
const content = await
|
|
2197
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
1825
2198
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
1826
2199
|
const existing = known.get(id);
|
|
1827
2200
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -1834,7 +2207,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
1834
2207
|
const findings = [];
|
|
1835
2208
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
1836
2209
|
for (const file of files) {
|
|
1837
|
-
const content = await
|
|
2210
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
1838
2211
|
const referenced = /* @__PURE__ */ new Set();
|
|
1839
2212
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
1840
2213
|
referenced.add(match[0].toUpperCase());
|
|
@@ -1869,7 +2242,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
|
|
|
1869
2242
|
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
1870
2243
|
const files = [];
|
|
1871
2244
|
for (const entry of entries) {
|
|
1872
|
-
const childRelative =
|
|
2245
|
+
const childRelative = path10.posix.join(relativeDir, entry.name);
|
|
1873
2246
|
if (entry.isDirectory()) {
|
|
1874
2247
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
1875
2248
|
continue;
|
|
@@ -1902,7 +2275,7 @@ function getSection2(content, heading) {
|
|
|
1902
2275
|
}
|
|
1903
2276
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
1904
2277
|
try {
|
|
1905
|
-
return await
|
|
2278
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
1906
2279
|
} catch (error) {
|
|
1907
2280
|
const nodeError = error;
|
|
1908
2281
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1913,8 +2286,8 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
1913
2286
|
}
|
|
1914
2287
|
|
|
1915
2288
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
1916
|
-
import { lstat as lstat2, readFile as
|
|
1917
|
-
import
|
|
2289
|
+
import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2290
|
+
import path11 from "path";
|
|
1918
2291
|
|
|
1919
2292
|
// src/core/adr/adr-sections.ts
|
|
1920
2293
|
var REQUIRED_ADR_SECTIONS = [
|
|
@@ -1978,7 +2351,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
1978
2351
|
);
|
|
1979
2352
|
for (const featureFolder of featureFolders) {
|
|
1980
2353
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
1981
|
-
const filePath =
|
|
2354
|
+
const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
1982
2355
|
if (!await isFile(rootDir, filePath)) {
|
|
1983
2356
|
findings.push({
|
|
1984
2357
|
severity: "error",
|
|
@@ -2002,7 +2375,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
2002
2375
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
2003
2376
|
for (const moduleFolder of moduleFolders) {
|
|
2004
2377
|
for (const requiredDoc of requiredModuleDocs) {
|
|
2005
|
-
const filePath =
|
|
2378
|
+
const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
2006
2379
|
if (!await isFile(rootDir, filePath)) {
|
|
2007
2380
|
findings.push({
|
|
2008
2381
|
severity: "error",
|
|
@@ -2025,8 +2398,8 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2025
2398
|
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
2026
2399
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
2027
2400
|
for (const adrFile of adrFiles) {
|
|
2028
|
-
const filePath =
|
|
2029
|
-
const content = await
|
|
2401
|
+
const filePath = path11.posix.join(adrDir, adrFile.name);
|
|
2402
|
+
const content = await readFile9(path11.join(rootDir, filePath), "utf8");
|
|
2030
2403
|
for (const requiredSection of requiredAdrSections) {
|
|
2031
2404
|
if (!content.includes(requiredSection)) {
|
|
2032
2405
|
findings.push({
|
|
@@ -2047,7 +2420,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2047
2420
|
}
|
|
2048
2421
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2049
2422
|
try {
|
|
2050
|
-
return await
|
|
2423
|
+
return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
|
|
2051
2424
|
} catch (error) {
|
|
2052
2425
|
const nodeError = error;
|
|
2053
2426
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2058,7 +2431,7 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2058
2431
|
}
|
|
2059
2432
|
async function isFile(rootDir, relativePath) {
|
|
2060
2433
|
try {
|
|
2061
|
-
return (await lstat2(
|
|
2434
|
+
return (await lstat2(path11.join(rootDir, relativePath))).isFile();
|
|
2062
2435
|
} catch (error) {
|
|
2063
2436
|
const nodeError = error;
|
|
2064
2437
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2070,7 +2443,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
2070
2443
|
|
|
2071
2444
|
// src/core/doctor/checks/required-files-check.ts
|
|
2072
2445
|
import { lstat as lstat3 } from "fs/promises";
|
|
2073
|
-
import
|
|
2446
|
+
import path12 from "path";
|
|
2074
2447
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
2075
2448
|
var requiredDocs = [
|
|
2076
2449
|
"00-product/PRD.md",
|
|
@@ -2097,12 +2470,12 @@ async function checkRequiredFiles(context) {
|
|
|
2097
2470
|
}
|
|
2098
2471
|
}
|
|
2099
2472
|
for (const relativeDocPath of requiredDocs) {
|
|
2100
|
-
const filePath =
|
|
2473
|
+
const filePath = path12.posix.join(docsDir, relativeDocPath);
|
|
2101
2474
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
2102
2475
|
findings.push(missingFile(filePath, "required-docs"));
|
|
2103
2476
|
}
|
|
2104
2477
|
}
|
|
2105
|
-
const adrIndexPath =
|
|
2478
|
+
const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
2106
2479
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
2107
2480
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
2108
2481
|
}
|
|
@@ -2128,7 +2501,7 @@ async function checkRequiredFiles(context) {
|
|
|
2128
2501
|
}
|
|
2129
2502
|
async function isFile2(rootDir, relativePath) {
|
|
2130
2503
|
try {
|
|
2131
|
-
return (await lstat3(
|
|
2504
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isFile();
|
|
2132
2505
|
} catch (error) {
|
|
2133
2506
|
const nodeError = error;
|
|
2134
2507
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2139,7 +2512,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
2139
2512
|
}
|
|
2140
2513
|
async function isDirectory(rootDir, relativePath) {
|
|
2141
2514
|
try {
|
|
2142
|
-
return (await lstat3(
|
|
2515
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
|
|
2143
2516
|
} catch (error) {
|
|
2144
2517
|
const nodeError = error;
|
|
2145
2518
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2158,8 +2531,8 @@ function missingFile(pathValue, check) {
|
|
|
2158
2531
|
}
|
|
2159
2532
|
|
|
2160
2533
|
// src/core/doctor/checks/standards-check.ts
|
|
2161
|
-
import { lstat as lstat4, readFile as
|
|
2162
|
-
import
|
|
2534
|
+
import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
|
|
2535
|
+
import path13 from "path";
|
|
2163
2536
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2164
2537
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2165
2538
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
@@ -2179,10 +2552,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2179
2552
|
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2180
2553
|
);
|
|
2181
2554
|
for (const featureFolder of featureFolders) {
|
|
2182
|
-
const featureDir =
|
|
2183
|
-
const completionReportPath =
|
|
2184
|
-
const reviewPath =
|
|
2185
|
-
const architectureImpactPath =
|
|
2555
|
+
const featureDir = path13.posix.join(featuresDir, featureFolder.name);
|
|
2556
|
+
const completionReportPath = path13.posix.join(featureDir, "COMPLETION_REPORT.md");
|
|
2557
|
+
const reviewPath = path13.posix.join(featureDir, "REVIEW.md");
|
|
2558
|
+
const architectureImpactPath = path13.posix.join(featureDir, "ARCHITECTURE_IMPACT.md");
|
|
2186
2559
|
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2187
2560
|
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2188
2561
|
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
@@ -2232,8 +2605,8 @@ async function checkAdrStandards(rootDir, adrDir) {
|
|
|
2232
2605
|
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2233
2606
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2234
2607
|
for (const adrFile of adrFiles) {
|
|
2235
|
-
const adrPath =
|
|
2236
|
-
const content = await
|
|
2608
|
+
const adrPath = path13.posix.join(adrDir, adrFile.name);
|
|
2609
|
+
const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
|
|
2237
2610
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2238
2611
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2239
2612
|
findings.push({
|
|
@@ -2317,7 +2690,7 @@ function isPlaceholder(value) {
|
|
|
2317
2690
|
}
|
|
2318
2691
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2319
2692
|
try {
|
|
2320
|
-
return await
|
|
2693
|
+
return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
|
|
2321
2694
|
} catch (error) {
|
|
2322
2695
|
const nodeError = error;
|
|
2323
2696
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2331,7 +2704,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2331
2704
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2332
2705
|
return void 0;
|
|
2333
2706
|
}
|
|
2334
|
-
return await
|
|
2707
|
+
return await readFile10(path13.join(rootDir, relativePath), "utf8");
|
|
2335
2708
|
} catch (error) {
|
|
2336
2709
|
const nodeError = error;
|
|
2337
2710
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2342,7 +2715,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2342
2715
|
}
|
|
2343
2716
|
async function isFile3(rootDir, relativePath) {
|
|
2344
2717
|
try {
|
|
2345
|
-
return (await lstat4(
|
|
2718
|
+
return (await lstat4(path13.join(rootDir, relativePath))).isFile();
|
|
2346
2719
|
} catch (error) {
|
|
2347
2720
|
const nodeError = error;
|
|
2348
2721
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2352,6 +2725,110 @@ async function isFile3(rootDir, relativePath) {
|
|
|
2352
2725
|
}
|
|
2353
2726
|
}
|
|
2354
2727
|
|
|
2728
|
+
// src/core/doctor/checks/superseded-check.ts
|
|
2729
|
+
import { readFile as readFile11, readdir as readdir11 } from "fs/promises";
|
|
2730
|
+
import path14 from "path";
|
|
2731
|
+
var adrFilePattern4 = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
2732
|
+
var adrReferencePattern2 = /ADR-\d{4,}/giu;
|
|
2733
|
+
async function checkSuperseded(context) {
|
|
2734
|
+
if (context.config === void 0) {
|
|
2735
|
+
return [];
|
|
2736
|
+
}
|
|
2737
|
+
const supersededIds = await loadSupersededAdrIds(context.rootDir, context.config.adrDir);
|
|
2738
|
+
if (supersededIds.size === 0) {
|
|
2739
|
+
return [];
|
|
2740
|
+
}
|
|
2741
|
+
const findings = [];
|
|
2742
|
+
findings.push(
|
|
2743
|
+
...await checkReferences2(context.rootDir, context.config.featuresDir, supersededIds)
|
|
2744
|
+
);
|
|
2745
|
+
findings.push(
|
|
2746
|
+
...await checkReferences2(context.rootDir, context.config.modulesDir, supersededIds)
|
|
2747
|
+
);
|
|
2748
|
+
return findings;
|
|
2749
|
+
}
|
|
2750
|
+
async function loadSupersededAdrIds(rootDir, adrDir) {
|
|
2751
|
+
const superseded = /* @__PURE__ */ new Set();
|
|
2752
|
+
const files = await readMarkdownFiles2(rootDir, adrDir);
|
|
2753
|
+
for (const file of files) {
|
|
2754
|
+
const match = adrFilePattern4.exec(path14.basename(file));
|
|
2755
|
+
if (match === null) {
|
|
2756
|
+
continue;
|
|
2757
|
+
}
|
|
2758
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2759
|
+
if (statusContains(content, /superseded\s+by/iu)) {
|
|
2760
|
+
superseded.add(`ADR-${match[1]}`.toUpperCase());
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
return superseded;
|
|
2764
|
+
}
|
|
2765
|
+
async function checkReferences2(rootDir, referenceDir, supersededIds) {
|
|
2766
|
+
const findings = [];
|
|
2767
|
+
const files = await readMarkdownFiles2(rootDir, referenceDir);
|
|
2768
|
+
for (const file of files) {
|
|
2769
|
+
const content = await readFile11(path14.join(rootDir, file), "utf8");
|
|
2770
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
2771
|
+
for (const match of stripCode2(content).matchAll(adrReferencePattern2)) {
|
|
2772
|
+
referenced.add(match[0].toUpperCase());
|
|
2773
|
+
}
|
|
2774
|
+
for (const id of referenced) {
|
|
2775
|
+
if (supersededIds.has(id)) {
|
|
2776
|
+
findings.push({
|
|
2777
|
+
severity: "warning",
|
|
2778
|
+
check: "superseded-reference",
|
|
2779
|
+
message: `Repository memory references ${id}, which has been superseded \u2014 update it to the current decision.`,
|
|
2780
|
+
path: file
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
return findings;
|
|
2786
|
+
}
|
|
2787
|
+
function statusContains(content, pattern) {
|
|
2788
|
+
const lines = content.split(/\r?\n/u);
|
|
2789
|
+
const startIndex = lines.findIndex((line) => line.trim().toLowerCase() === "## status");
|
|
2790
|
+
if (startIndex === -1) {
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
const body = [];
|
|
2794
|
+
for (let index = startIndex + 1; index < lines.length; index += 1) {
|
|
2795
|
+
if (/^##\s+/u.test(lines[index])) {
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
body.push(lines[index]);
|
|
2799
|
+
}
|
|
2800
|
+
return pattern.test(body.join("\n"));
|
|
2801
|
+
}
|
|
2802
|
+
function stripCode2(content) {
|
|
2803
|
+
return content.replace(/```[\s\S]*?```/gu, " ").replace(/~~~[\s\S]*?~~~/gu, " ").replace(/`[^`]*`/gu, " ");
|
|
2804
|
+
}
|
|
2805
|
+
async function readMarkdownFiles2(rootDir, relativeDir) {
|
|
2806
|
+
const entries = await readDirIfExists6(rootDir, relativeDir);
|
|
2807
|
+
const files = [];
|
|
2808
|
+
for (const entry of entries) {
|
|
2809
|
+
const childRelative = path14.posix.join(relativeDir, entry.name);
|
|
2810
|
+
if (entry.isDirectory()) {
|
|
2811
|
+
files.push(...await readMarkdownFiles2(rootDir, childRelative));
|
|
2812
|
+
continue;
|
|
2813
|
+
}
|
|
2814
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
2815
|
+
files.push(childRelative);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
return files;
|
|
2819
|
+
}
|
|
2820
|
+
async function readDirIfExists6(rootDir, relativePath) {
|
|
2821
|
+
try {
|
|
2822
|
+
return await readdir11(path14.join(rootDir, relativePath), { withFileTypes: true });
|
|
2823
|
+
} catch (error) {
|
|
2824
|
+
const nodeError = error;
|
|
2825
|
+
if (nodeError.code === "ENOENT") {
|
|
2826
|
+
return [];
|
|
2827
|
+
}
|
|
2828
|
+
throw error;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2355
2832
|
// src/core/doctor/doctor-check.ts
|
|
2356
2833
|
async function runDoctor(rootDir) {
|
|
2357
2834
|
const findings = [];
|
|
@@ -2368,6 +2845,7 @@ async function runDoctor(rootDir) {
|
|
|
2368
2845
|
findings.push(...await checkDrift(context));
|
|
2369
2846
|
findings.push(...await checkContent(context));
|
|
2370
2847
|
findings.push(...await checkCodeReferences(context));
|
|
2848
|
+
findings.push(...await checkSuperseded(context));
|
|
2371
2849
|
}
|
|
2372
2850
|
return createDoctorReport(findings);
|
|
2373
2851
|
}
|
|
@@ -2445,10 +2923,10 @@ function formatDoctorResult(result) {
|
|
|
2445
2923
|
|
|
2446
2924
|
// src/commands/init.ts
|
|
2447
2925
|
import { existsSync as existsSync6 } from "fs";
|
|
2448
|
-
import
|
|
2926
|
+
import path17 from "path";
|
|
2449
2927
|
|
|
2450
2928
|
// src/core/generator/generate-init.ts
|
|
2451
|
-
import
|
|
2929
|
+
import path15 from "path";
|
|
2452
2930
|
var neutralTemplates = [
|
|
2453
2931
|
{
|
|
2454
2932
|
path: "AGENTS.md",
|
|
@@ -2467,6 +2945,18 @@ Required reading:
|
|
|
2467
2945
|
- \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
|
|
2468
2946
|
|
|
2469
2947
|
Repository rules override model preferences. If instructions conflict, stop and report the conflict.
|
|
2948
|
+
|
|
2949
|
+
## Changing an accepted decision
|
|
2950
|
+
|
|
2951
|
+
Before changing anything an accepted ADR governs (framework, database, auth, API shape, and similar):
|
|
2952
|
+
|
|
2953
|
+
1. Check \`docs/adrs/\` for an accepted ADR that covers it.
|
|
2954
|
+
2. If your change contradicts one, stop and confirm with a human first \u2014 do not silently change the
|
|
2955
|
+
code and leave the ADR saying the opposite.
|
|
2956
|
+
3. Record the change as a new decision with \`recall adr supersede <old> <new-title>\`. That supersedes
|
|
2957
|
+
the old ADR instead of overwriting history, so the reasoning stays auditable.
|
|
2958
|
+
|
|
2959
|
+
Repository memory is only trustworthy if decisions change through this trail, not silently.
|
|
2470
2960
|
`
|
|
2471
2961
|
},
|
|
2472
2962
|
{
|
|
@@ -2503,6 +2993,8 @@ Before non-trivial work:
|
|
|
2503
2993
|
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2504
2994
|
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2505
2995
|
- If an instruction conflicts with accepted repository memory, stop and report the conflict.
|
|
2996
|
+
- Before changing what an accepted ADR governs, confirm with a human and record it with
|
|
2997
|
+
\`recall adr supersede <old> <new-title>\` \u2014 never silently contradict an accepted decision.
|
|
2506
2998
|
|
|
2507
2999
|
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2508
3000
|
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
@@ -2591,26 +3083,78 @@ Default behavior:
|
|
|
2591
3083
|
path: "docs/20-security/SECURITY_MODEL.md",
|
|
2592
3084
|
content: `# Security Model
|
|
2593
3085
|
|
|
2594
|
-
##
|
|
3086
|
+
## Status
|
|
2595
3087
|
|
|
2596
|
-
Draft.
|
|
3088
|
+
Draft \u2014 fill the prompted sections below with this repository's real model as it grows. \`recall doctor\`
|
|
3089
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2597
3090
|
|
|
2598
3091
|
## Baseline Rules
|
|
2599
3092
|
|
|
2600
|
-
-
|
|
2601
|
-
-
|
|
3093
|
+
- Never commit secrets or credentials, and never read or copy \`.env\` files into docs.
|
|
3094
|
+
- Validate and authorize untrusted input at every trust boundary.
|
|
2602
3095
|
- Do not add network, telemetry, cloud, MCP runtime, or AI API behavior without explicit review.
|
|
3096
|
+
|
|
3097
|
+
## Authentication And Authorization
|
|
3098
|
+
|
|
3099
|
+
Describe how this repository authenticates users or clients and how it authorizes actions, including
|
|
3100
|
+
where those checks live.
|
|
3101
|
+
|
|
3102
|
+
## Secrets And Configuration
|
|
3103
|
+
|
|
3104
|
+
Describe where secrets live, how they are injected, and how configuration is kept out of version
|
|
3105
|
+
control.
|
|
3106
|
+
|
|
3107
|
+
## Sensitive Data
|
|
3108
|
+
|
|
3109
|
+
Describe the sensitive or personal data this repository handles, and how it is protected at rest and
|
|
3110
|
+
in transit.
|
|
3111
|
+
|
|
3112
|
+
## Dependencies And Supply Chain
|
|
3113
|
+
|
|
3114
|
+
Describe how third-party dependencies are vetted, pinned, and updated.
|
|
2603
3115
|
`
|
|
2604
3116
|
},
|
|
2605
3117
|
{
|
|
2606
3118
|
path: "docs/20-security/THREAT_MODEL.md",
|
|
2607
3119
|
content: `# Threat Model
|
|
2608
3120
|
|
|
2609
|
-
##
|
|
3121
|
+
## Status
|
|
2610
3122
|
|
|
2611
|
-
Draft.
|
|
3123
|
+
Draft \u2014 replace the prompts below with this repository's real analysis as it grows. \`recall doctor\`
|
|
3124
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
3125
|
+
|
|
3126
|
+
## Assets
|
|
2612
3127
|
|
|
2613
|
-
|
|
3128
|
+
Describe what this repository must protect: user data, credentials, money, availability, or
|
|
3129
|
+
reputation.
|
|
3130
|
+
|
|
3131
|
+
## Entry Points
|
|
3132
|
+
|
|
3133
|
+
Describe where untrusted input enters: HTTP endpoints, webhooks, file uploads, queues, CLI input, or
|
|
3134
|
+
third-party callbacks.
|
|
3135
|
+
|
|
3136
|
+
## Trust Boundaries
|
|
3137
|
+
|
|
3138
|
+
Describe where trust changes: client to server, service to database, your code to third-party APIs.
|
|
3139
|
+
|
|
3140
|
+
## Threats
|
|
3141
|
+
|
|
3142
|
+
Describe the concrete threats that apply to this repository, by category:
|
|
3143
|
+
|
|
3144
|
+
- Spoofing \u2014 how identities are faked or sessions stolen.
|
|
3145
|
+
- Tampering \u2014 how requests, data, or builds are altered (injection, mass assignment).
|
|
3146
|
+
- Repudiation \u2014 actions that must remain auditable.
|
|
3147
|
+
- Information disclosure \u2014 how sensitive data or secrets could leak.
|
|
3148
|
+
- Denial of service \u2014 how the system can be overwhelmed or abused.
|
|
3149
|
+
- Elevation of privilege \u2014 how a user could gain access they should not have.
|
|
3150
|
+
|
|
3151
|
+
## Mitigations
|
|
3152
|
+
|
|
3153
|
+
Describe the control in place or planned for each threat above.
|
|
3154
|
+
|
|
3155
|
+
## Open Risks
|
|
3156
|
+
|
|
3157
|
+
Describe accepted or unresolved risks and who owns them.
|
|
2614
3158
|
`
|
|
2615
3159
|
},
|
|
2616
3160
|
{
|
|
@@ -2659,6 +3203,15 @@ Baseline rules:
|
|
|
2659
3203
|
AI agents must follow repository memory over model preference.
|
|
2660
3204
|
|
|
2661
3205
|
If a request conflicts with accepted repository memory or engineering standards, stop and report the conflict.
|
|
3206
|
+
|
|
3207
|
+
## Changing an accepted decision
|
|
3208
|
+
|
|
3209
|
+
When work would change something an accepted ADR governs:
|
|
3210
|
+
|
|
3211
|
+
1. Find the accepted ADR in \`docs/adrs/\` that covers it.
|
|
3212
|
+
2. If the change contradicts it, stop and confirm with a human before changing the code.
|
|
3213
|
+
3. Record the new decision with \`recall adr supersede <old> <new-title>\` so the old ADR is marked
|
|
3214
|
+
superseded and the reasoning is preserved, instead of silently editing or contradicting it.
|
|
2662
3215
|
`
|
|
2663
3216
|
},
|
|
2664
3217
|
{
|
|
@@ -2792,6 +3345,17 @@ Options:
|
|
|
2792
3345
|
- \`--dry-run\`: show planned writes without writing files.
|
|
2793
3346
|
- \`--force\`: overwrite existing files explicitly.
|
|
2794
3347
|
|
|
3348
|
+
### \`recall adr supersede <old> <new-title>\`
|
|
3349
|
+
|
|
3350
|
+
Record a changed decision. Marks an accepted ADR as \`Accepted \u2014 superseded by ADR-####\` and creates a
|
|
3351
|
+
new accepted ADR that declares what it supersedes, so the reasoning trail stays auditable instead of
|
|
3352
|
+
being overwritten. Doctor then warns about any memory still referencing the superseded decision.
|
|
3353
|
+
|
|
3354
|
+
Options:
|
|
3355
|
+
|
|
3356
|
+
- \`--dry-run\`: show planned writes without writing files.
|
|
3357
|
+
- \`--force\`: overwrite existing files explicitly.
|
|
3358
|
+
|
|
2795
3359
|
### \`recall module create <name>\`
|
|
2796
3360
|
|
|
2797
3361
|
Create module memory docs under the configured modules directory.
|
|
@@ -2807,7 +3371,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
|
|
|
2807
3371
|
engineering evidence is present, and whether memory references decisions that exist and are accepted.
|
|
2808
3372
|
|
|
2809
3373
|
Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
|
|
2810
|
-
is an error,
|
|
3374
|
+
is an error, memory that references a not-yet-accepted ADR is a warning, and memory that still
|
|
3375
|
+
references a superseded decision is a warning.
|
|
2811
3376
|
|
|
2812
3377
|
Exit codes:
|
|
2813
3378
|
|
|
@@ -2894,7 +3459,7 @@ jobs:
|
|
|
2894
3459
|
}
|
|
2895
3460
|
];
|
|
2896
3461
|
function generateInitFiles(options) {
|
|
2897
|
-
const repositoryName =
|
|
3462
|
+
const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
|
|
2898
3463
|
const context = createTemplateContext({ repositoryName });
|
|
2899
3464
|
const files = neutralTemplates.map((template) => ({
|
|
2900
3465
|
path: template.path,
|
|
@@ -2921,17 +3486,17 @@ function generatePresetFiles(preset) {
|
|
|
2921
3486
|
|
|
2922
3487
|
// src/core/hooks/detect-gates.ts
|
|
2923
3488
|
import { existsSync as existsSync5 } from "fs";
|
|
2924
|
-
import { readFile as
|
|
2925
|
-
import
|
|
3489
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3490
|
+
import path16 from "path";
|
|
2926
3491
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
2927
3492
|
async function detectPreCommitGates(rootDir) {
|
|
2928
|
-
const packageJsonPath =
|
|
3493
|
+
const packageJsonPath = path16.join(rootDir, "package.json");
|
|
2929
3494
|
if (!existsSync5(packageJsonPath)) {
|
|
2930
3495
|
return [];
|
|
2931
3496
|
}
|
|
2932
3497
|
let scripts;
|
|
2933
3498
|
try {
|
|
2934
|
-
const raw = await
|
|
3499
|
+
const raw = await readFile12(packageJsonPath, "utf8");
|
|
2935
3500
|
const parsed = JSON.parse(raw);
|
|
2936
3501
|
scripts = parsed.scripts ?? {};
|
|
2937
3502
|
} catch {
|
|
@@ -2940,16 +3505,16 @@ async function detectPreCommitGates(rootDir) {
|
|
|
2940
3505
|
if (typeof scripts !== "object" || scripts === null) {
|
|
2941
3506
|
return [];
|
|
2942
3507
|
}
|
|
2943
|
-
const packageManager =
|
|
3508
|
+
const packageManager = detectPackageManager2(rootDir);
|
|
2944
3509
|
return KNOWN_SCRIPTS.filter((script) => typeof scripts[script] === "string").map(
|
|
2945
3510
|
(script) => `${packageManager} run ${script}`
|
|
2946
3511
|
);
|
|
2947
3512
|
}
|
|
2948
|
-
function
|
|
2949
|
-
if (existsSync5(
|
|
3513
|
+
function detectPackageManager2(rootDir) {
|
|
3514
|
+
if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
|
|
2950
3515
|
return "pnpm";
|
|
2951
3516
|
}
|
|
2952
|
-
if (existsSync5(
|
|
3517
|
+
if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
|
|
2953
3518
|
return "yarn";
|
|
2954
3519
|
}
|
|
2955
3520
|
return "npm";
|
|
@@ -4454,8 +5019,8 @@ function parsePreset(value) {
|
|
|
4454
5019
|
if (!result.success) {
|
|
4455
5020
|
throw new PresetValidationError(
|
|
4456
5021
|
result.error.issues.map((issue) => {
|
|
4457
|
-
const
|
|
4458
|
-
return `${
|
|
5022
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
5023
|
+
return `${path19}${issue.message}`;
|
|
4459
5024
|
})
|
|
4460
5025
|
);
|
|
4461
5026
|
}
|
|
@@ -5144,7 +5709,7 @@ var InitError = class extends Error {
|
|
|
5144
5709
|
}
|
|
5145
5710
|
};
|
|
5146
5711
|
async function initProject(options) {
|
|
5147
|
-
if (options.force === true && options.reinit !== true && existsSync6(
|
|
5712
|
+
if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
|
|
5148
5713
|
throw new InitError(
|
|
5149
5714
|
"EXISTING_INSTALLATION",
|
|
5150
5715
|
"Refusing to re-initialize an existing Recall OS installation.",
|
|
@@ -5156,6 +5721,7 @@ async function initProject(options) {
|
|
|
5156
5721
|
);
|
|
5157
5722
|
}
|
|
5158
5723
|
const preset = resolvePreset(options.preset);
|
|
5724
|
+
const detected = await inspectRepo(options.rootDir);
|
|
5159
5725
|
const preCommitGates = await detectPreCommitGates(options.rootDir);
|
|
5160
5726
|
const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
|
|
5161
5727
|
const files = createInitWriteFiles(options.rootDir, config, preset);
|
|
@@ -5176,7 +5742,8 @@ async function initProject(options) {
|
|
|
5176
5742
|
preset: preset?.id ?? null,
|
|
5177
5743
|
dryRun: options.dryRun ?? false,
|
|
5178
5744
|
plan,
|
|
5179
|
-
writeResult
|
|
5745
|
+
writeResult,
|
|
5746
|
+
detected
|
|
5180
5747
|
};
|
|
5181
5748
|
}
|
|
5182
5749
|
function formatInitResult(result) {
|
|
@@ -5193,6 +5760,7 @@ function formatInitResult(result) {
|
|
|
5193
5760
|
dryRun: result.dryRun,
|
|
5194
5761
|
writeResult: result.writeResult
|
|
5195
5762
|
});
|
|
5763
|
+
appendDetectedStack(lines, result.detected);
|
|
5196
5764
|
const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
|
|
5197
5765
|
if (hookWritten) {
|
|
5198
5766
|
lines.push("");
|
|
@@ -5215,6 +5783,21 @@ function formatInitResult(result) {
|
|
|
5215
5783
|
return `${lines.join("\n")}
|
|
5216
5784
|
`;
|
|
5217
5785
|
}
|
|
5786
|
+
function appendDetectedStack(lines, detected) {
|
|
5787
|
+
const hasSignal = detected.languages.length > 0 || detected.frameworks.length > 0 || detected.packageManager !== null || detected.testsEvidence !== null;
|
|
5788
|
+
if (!hasSignal) {
|
|
5789
|
+
return;
|
|
5790
|
+
}
|
|
5791
|
+
const stack = summarizeSignals(detected).filter(
|
|
5792
|
+
(line) => !line.startsWith("- README") && !line.startsWith("- Docs")
|
|
5793
|
+
);
|
|
5794
|
+
lines.push("");
|
|
5795
|
+
lines.push("Detected in this repository (proposed \u2014 review, nothing was accepted):");
|
|
5796
|
+
lines.push(...stack);
|
|
5797
|
+
lines.push(
|
|
5798
|
+
"If any signal is wrong, correct the source file noted. Run `recall adopt` to record this as proposed memory."
|
|
5799
|
+
);
|
|
5800
|
+
}
|
|
5218
5801
|
function resolvePreset(presetId) {
|
|
5219
5802
|
if (presetId === void 0) {
|
|
5220
5803
|
return null;
|
|
@@ -5490,7 +6073,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
5490
6073
|
}
|
|
5491
6074
|
|
|
5492
6075
|
// src/core/generator/generate-module.ts
|
|
5493
|
-
import
|
|
6076
|
+
import path18 from "path";
|
|
5494
6077
|
var moduleTemplates = [
|
|
5495
6078
|
{
|
|
5496
6079
|
fileName: "MODULE.md",
|
|
@@ -5563,14 +6146,14 @@ Record durable module decisions here.
|
|
|
5563
6146
|
];
|
|
5564
6147
|
function generateModuleFiles(options) {
|
|
5565
6148
|
const slug = slugify(options.moduleName);
|
|
5566
|
-
const moduleDir =
|
|
6149
|
+
const moduleDir = path18.posix.join(options.modulesDir, slug);
|
|
5567
6150
|
const title = titleizeModuleName(options.moduleName);
|
|
5568
6151
|
const context = createTemplateContext({
|
|
5569
6152
|
slug,
|
|
5570
6153
|
title
|
|
5571
6154
|
});
|
|
5572
6155
|
return moduleTemplates.map((template) => ({
|
|
5573
|
-
path:
|
|
6156
|
+
path: path18.posix.join(moduleDir, template.fileName),
|
|
5574
6157
|
content: renderTemplate(template.content, context)
|
|
5575
6158
|
}));
|
|
5576
6159
|
}
|
|
@@ -5591,7 +6174,7 @@ var ModuleCreateError = class extends Error {
|
|
|
5591
6174
|
};
|
|
5592
6175
|
async function createModule(options) {
|
|
5593
6176
|
const slug = createModuleSlug(options.name);
|
|
5594
|
-
const config = await
|
|
6177
|
+
const config = await loadRequiredConfig5(options.rootDir);
|
|
5595
6178
|
const files = generateModuleFiles({
|
|
5596
6179
|
modulesDir: config.modulesDir,
|
|
5597
6180
|
moduleName: options.name
|
|
@@ -5645,7 +6228,7 @@ function createModuleSlug(name) {
|
|
|
5645
6228
|
throw error;
|
|
5646
6229
|
}
|
|
5647
6230
|
}
|
|
5648
|
-
async function
|
|
6231
|
+
async function loadRequiredConfig5(rootDir) {
|
|
5649
6232
|
try {
|
|
5650
6233
|
return await loadConfig(rootDir);
|
|
5651
6234
|
} catch (error) {
|
|
@@ -5819,6 +6402,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
|
|
|
5819
6402
|
});
|
|
5820
6403
|
stdout.write(formatAdrAcceptResult(result));
|
|
5821
6404
|
});
|
|
6405
|
+
adrCommand.command("supersede").description(
|
|
6406
|
+
"Record a changed decision: mark an accepted ADR superseded by a new accepted ADR."
|
|
6407
|
+
).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(
|
|
6408
|
+
async (oldName, newTitle, options) => {
|
|
6409
|
+
const result = await supersedeAdr({
|
|
6410
|
+
rootDir: cwd,
|
|
6411
|
+
oldName,
|
|
6412
|
+
newTitle,
|
|
6413
|
+
dryRun: options.dryRun,
|
|
6414
|
+
force: options.force
|
|
6415
|
+
});
|
|
6416
|
+
stdout.write(formatAdrSupersedeResult(result));
|
|
6417
|
+
}
|
|
6418
|
+
);
|
|
5822
6419
|
const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
|
|
5823
6420
|
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) => {
|
|
5824
6421
|
const result = await createModule({
|
|
@@ -5912,6 +6509,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
5912
6509
|
`);
|
|
5913
6510
|
for (const detail of error.details) {
|
|
5914
6511
|
stderr.write(`- ${detail}
|
|
6512
|
+
`);
|
|
6513
|
+
}
|
|
6514
|
+
return 1;
|
|
6515
|
+
}
|
|
6516
|
+
if (error instanceof AdrSupersedeError) {
|
|
6517
|
+
stderr.write(`${error.message}
|
|
6518
|
+
`);
|
|
6519
|
+
for (const detail of error.details) {
|
|
6520
|
+
stderr.write(`- ${detail}
|
|
5915
6521
|
`);
|
|
5916
6522
|
}
|
|
5917
6523
|
return 1;
|