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/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) {
|
|
@@ -1228,113 +1416,33 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1228
1416
|
});
|
|
1229
1417
|
}
|
|
1230
1418
|
|
|
1231
|
-
// src/core/adopt/generate-adoption.ts
|
|
1232
|
-
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1233
|
-
function generateAdoptionFiles(options) {
|
|
1234
|
-
const files = [
|
|
1235
|
-
{
|
|
1236
|
-
path: ADOPTION_REPORT_PATH,
|
|
1237
|
-
content: renderReport(options.adrDir, options.signals)
|
|
1238
|
-
}
|
|
1239
|
-
];
|
|
1240
|
-
for (const framework of options.signals.frameworks) {
|
|
1241
|
-
files.push({
|
|
1242
|
-
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1243
|
-
content: renderProposedAdr(framework)
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
return files;
|
|
1247
|
-
}
|
|
1248
|
-
function renderReport(adrDir, signals) {
|
|
1249
|
-
return `# Adoption Report
|
|
1250
|
-
|
|
1251
|
-
## Status
|
|
1252
|
-
|
|
1253
|
-
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1254
|
-
is accepted repository memory until you accept it.
|
|
1255
|
-
|
|
1256
|
-
## Detected Signals
|
|
1257
|
-
|
|
1258
|
-
- Languages: ${formatList(signals.languages)}
|
|
1259
|
-
- Package manager: ${signals.packageManager ?? "none detected"}
|
|
1260
|
-
- Frameworks: ${formatList(signals.frameworks)}
|
|
1261
|
-
- Tests present: ${formatBool(signals.hasTests)}
|
|
1262
|
-
- README present: ${formatBool(signals.hasReadme)}
|
|
1263
|
-
- Docs folder present: ${formatBool(signals.hasDocs)}
|
|
1264
|
-
|
|
1265
|
-
## Proposed Decisions
|
|
1266
|
-
|
|
1267
|
-
${renderProposedDecisions(adrDir, signals)}
|
|
1268
|
-
|
|
1269
|
-
## Review Checklist
|
|
1270
|
-
|
|
1271
|
-
- [ ] Confirm the detected languages and package manager.
|
|
1272
|
-
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1273
|
-
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1274
|
-
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1275
|
-
|
|
1276
|
-
## Notes
|
|
1277
|
-
|
|
1278
|
-
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1279
|
-
files. No repository code was executed and no decision was accepted automatically.
|
|
1280
|
-
`;
|
|
1281
|
-
}
|
|
1282
|
-
function renderProposedDecisions(adrDir, signals) {
|
|
1283
|
-
if (signals.frameworks.length === 0) {
|
|
1284
|
-
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1285
|
-
}
|
|
1286
|
-
return signals.frameworks.map(
|
|
1287
|
-
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1288
|
-
).join("\n");
|
|
1289
|
-
}
|
|
1290
|
-
function renderProposedAdr(framework) {
|
|
1291
|
-
return `# Proposed ADR: Use ${framework}
|
|
1292
|
-
|
|
1293
|
-
## Status
|
|
1294
|
-
|
|
1295
|
-
Proposed
|
|
1296
|
-
|
|
1297
|
-
## Context
|
|
1298
|
-
|
|
1299
|
-
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1300
|
-
|
|
1301
|
-
## Decision
|
|
1302
|
-
|
|
1303
|
-
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1304
|
-
and is not accepted until a human reviews and accepts it.
|
|
1305
|
-
|
|
1306
|
-
## Alternatives Considered
|
|
1307
|
-
|
|
1308
|
-
- Record a different framework.
|
|
1309
|
-
- Leave the decision unrecorded for now.
|
|
1310
|
-
|
|
1311
|
-
## Consequences
|
|
1312
|
-
|
|
1313
|
-
- Captures a framework already in use as reviewable repository memory.
|
|
1314
|
-
- Requires explicit human acceptance before it becomes repository truth.
|
|
1315
|
-
|
|
1316
|
-
## Related Documents
|
|
1317
|
-
|
|
1318
|
-
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1319
|
-
- The adoption report generated alongside this proposal.
|
|
1320
|
-
`;
|
|
1321
|
-
}
|
|
1322
|
-
function frameworkSlug(framework) {
|
|
1323
|
-
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1324
|
-
}
|
|
1325
|
-
function formatList(values) {
|
|
1326
|
-
return values.length > 0 ? values.join(", ") : "none detected";
|
|
1327
|
-
}
|
|
1328
|
-
function formatBool(value) {
|
|
1329
|
-
return value ? "yes" : "no";
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
1419
|
// src/core/adopt/inspect-repo.ts
|
|
1333
1420
|
import { existsSync as existsSync3 } from "fs";
|
|
1334
|
-
import { readFile as
|
|
1335
|
-
import
|
|
1421
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1422
|
+
import path7 from "path";
|
|
1423
|
+
var FRAMEWORK_SOURCES = {
|
|
1424
|
+
"Next.js": "package.json",
|
|
1425
|
+
React: "package.json",
|
|
1426
|
+
NestJS: "package.json",
|
|
1427
|
+
Express: "package.json",
|
|
1428
|
+
FastAPI: "pyproject.toml / requirements.txt",
|
|
1429
|
+
Flask: "pyproject.toml / requirements.txt",
|
|
1430
|
+
Django: "pyproject.toml / requirements.txt",
|
|
1431
|
+
Gin: "go.mod",
|
|
1432
|
+
Echo: "go.mod",
|
|
1433
|
+
Fiber: "go.mod",
|
|
1434
|
+
Chi: "go.mod",
|
|
1435
|
+
"Spring Boot": "pom.xml / build.gradle",
|
|
1436
|
+
"Actix Web": "Cargo.toml",
|
|
1437
|
+
Axum: "Cargo.toml",
|
|
1438
|
+
Rocket: "Cargo.toml",
|
|
1439
|
+
Laravel: "composer.json",
|
|
1440
|
+
Symfony: "composer.json",
|
|
1441
|
+
"Ruby on Rails": "Gemfile",
|
|
1442
|
+
Flutter: "pubspec.yaml"
|
|
1443
|
+
};
|
|
1336
1444
|
async function inspectRepo(rootDir) {
|
|
1337
|
-
const has = (relativePath) => existsSync3(
|
|
1445
|
+
const has = (relativePath) => existsSync3(path7.join(rootDir, relativePath));
|
|
1338
1446
|
const languages = /* @__PURE__ */ new Set();
|
|
1339
1447
|
const frameworks = /* @__PURE__ */ new Set();
|
|
1340
1448
|
const pkg = has("package.json") ? await readJson(rootDir, "package.json") : null;
|
|
@@ -1366,6 +1474,22 @@ async function inspectRepo(rootDir) {
|
|
|
1366
1474
|
languages.add("Dart");
|
|
1367
1475
|
frameworks.add("Flutter");
|
|
1368
1476
|
}
|
|
1477
|
+
if (has("composer.json")) {
|
|
1478
|
+
languages.add("PHP");
|
|
1479
|
+
const composer = (await readText(rootDir, "composer.json")).toLowerCase();
|
|
1480
|
+
if (composer.includes("laravel/framework")) {
|
|
1481
|
+
frameworks.add("Laravel");
|
|
1482
|
+
} else if (composer.includes("symfony/")) {
|
|
1483
|
+
frameworks.add("Symfony");
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (has("Gemfile")) {
|
|
1487
|
+
languages.add("Ruby");
|
|
1488
|
+
const gemfile = (await readText(rootDir, "Gemfile")).toLowerCase();
|
|
1489
|
+
if (gemfile.includes("rails")) {
|
|
1490
|
+
frameworks.add("Ruby on Rails");
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1369
1493
|
const deps = collectDependencies(pkg);
|
|
1370
1494
|
if ("next" in deps) {
|
|
1371
1495
|
frameworks.add("Next.js");
|
|
@@ -1411,25 +1535,147 @@ async function inspectRepo(rootDir) {
|
|
|
1411
1535
|
frameworks.add("Rocket");
|
|
1412
1536
|
}
|
|
1413
1537
|
}
|
|
1414
|
-
|
|
1415
|
-
if (has("pnpm-lock.yaml")) {
|
|
1416
|
-
packageManager = "pnpm";
|
|
1417
|
-
} else if (has("yarn.lock")) {
|
|
1418
|
-
packageManager = "yarn";
|
|
1419
|
-
} else if (has("package-lock.json")) {
|
|
1420
|
-
packageManager = "npm";
|
|
1421
|
-
}
|
|
1538
|
+
const [packageManager, packageManagerSource] = detectPackageManager(has);
|
|
1422
1539
|
const scripts = pkg !== null && isRecord(pkg.scripts) ? pkg.scripts : {};
|
|
1423
|
-
const
|
|
1540
|
+
const testsEvidence = await detectTestsEvidence(rootDir, has, "test" in scripts, python);
|
|
1424
1541
|
return {
|
|
1425
1542
|
languages: [...languages],
|
|
1426
1543
|
packageManager,
|
|
1544
|
+
packageManagerSource,
|
|
1427
1545
|
frameworks: [...frameworks],
|
|
1428
|
-
hasTests,
|
|
1546
|
+
hasTests: testsEvidence !== null,
|
|
1547
|
+
testsEvidence,
|
|
1429
1548
|
hasReadme: has("README.md") || has("README"),
|
|
1430
1549
|
hasDocs: has("docs")
|
|
1431
1550
|
};
|
|
1432
1551
|
}
|
|
1552
|
+
function summarizeSignals(signals) {
|
|
1553
|
+
const lines = [];
|
|
1554
|
+
lines.push(`- Languages: ${formatList(signals.languages)}`);
|
|
1555
|
+
lines.push(
|
|
1556
|
+
signals.packageManager === null ? "- Package manager: none detected" : `- Package manager: ${signals.packageManager}${signals.packageManagerSource === null ? "" : ` (from \`${signals.packageManagerSource}\`)`}`
|
|
1557
|
+
);
|
|
1558
|
+
if (signals.frameworks.length === 0) {
|
|
1559
|
+
lines.push("- Frameworks: none detected");
|
|
1560
|
+
} else {
|
|
1561
|
+
const withSource = signals.frameworks.map((framework) => {
|
|
1562
|
+
const source = FRAMEWORK_SOURCES[framework];
|
|
1563
|
+
return source === void 0 ? framework : `${framework} (from \`${source}\`)`;
|
|
1564
|
+
});
|
|
1565
|
+
lines.push(`- Frameworks: ${withSource.join(", ")}`);
|
|
1566
|
+
}
|
|
1567
|
+
lines.push(
|
|
1568
|
+
signals.testsEvidence === null ? "- Tests: none detected \u2014 if tests exist, point Recall at them by correcting this report" : `- Tests: detected via ${signals.testsEvidence}`
|
|
1569
|
+
);
|
|
1570
|
+
lines.push(`- README present: ${signals.hasReadme ? "yes" : "no"}`);
|
|
1571
|
+
lines.push(`- Docs folder present: ${signals.hasDocs ? "yes" : "no"}`);
|
|
1572
|
+
return lines;
|
|
1573
|
+
}
|
|
1574
|
+
function formatList(values) {
|
|
1575
|
+
return values.length === 0 ? "none detected" : values.join(", ");
|
|
1576
|
+
}
|
|
1577
|
+
function detectPackageManager(has) {
|
|
1578
|
+
const candidates = [
|
|
1579
|
+
[has("go.mod"), "Go modules", "go.mod"],
|
|
1580
|
+
[has("Cargo.toml"), "Cargo", "Cargo.toml"],
|
|
1581
|
+
[has("pom.xml"), "Maven", "pom.xml"],
|
|
1582
|
+
[has("build.gradle"), "Gradle", "build.gradle"],
|
|
1583
|
+
[has("build.gradle.kts"), "Gradle", "build.gradle.kts"],
|
|
1584
|
+
[has("composer.json"), "Composer", "composer.json"],
|
|
1585
|
+
[has("Gemfile"), "Bundler", "Gemfile"],
|
|
1586
|
+
[has("Package.swift"), "Swift Package Manager", "Package.swift"],
|
|
1587
|
+
[has("pubspec.yaml"), "pub", "pubspec.yaml"],
|
|
1588
|
+
[has("uv.lock"), "uv", "uv.lock"],
|
|
1589
|
+
[has("poetry.lock"), "Poetry", "poetry.lock"],
|
|
1590
|
+
[has("requirements.txt"), "pip", "requirements.txt"],
|
|
1591
|
+
[has("pyproject.toml"), "pip", "pyproject.toml"],
|
|
1592
|
+
[has("pnpm-lock.yaml"), "pnpm", "pnpm-lock.yaml"],
|
|
1593
|
+
[has("yarn.lock"), "yarn", "yarn.lock"],
|
|
1594
|
+
[has("package-lock.json"), "npm", "package-lock.json"],
|
|
1595
|
+
[has("package.json"), "npm", "package.json"]
|
|
1596
|
+
];
|
|
1597
|
+
for (const [present, name, source] of candidates) {
|
|
1598
|
+
if (present) {
|
|
1599
|
+
return [name, source];
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return [null, null];
|
|
1603
|
+
}
|
|
1604
|
+
async function detectTestsEvidence(rootDir, has, hasTestScript, pythonText) {
|
|
1605
|
+
if (has("tests")) {
|
|
1606
|
+
return "`tests/` directory";
|
|
1607
|
+
}
|
|
1608
|
+
if (has("test")) {
|
|
1609
|
+
return "`test/` directory";
|
|
1610
|
+
}
|
|
1611
|
+
if (has("__tests__")) {
|
|
1612
|
+
return "`__tests__/` directory";
|
|
1613
|
+
}
|
|
1614
|
+
if (has("pytest.ini") || pythonText.includes("pytest")) {
|
|
1615
|
+
return "pytest configuration";
|
|
1616
|
+
}
|
|
1617
|
+
if (has("phpunit.xml") || has("phpunit.xml.dist")) {
|
|
1618
|
+
return "PHPUnit configuration";
|
|
1619
|
+
}
|
|
1620
|
+
if (hasTestScript) {
|
|
1621
|
+
return '`"test"` script in package.json';
|
|
1622
|
+
}
|
|
1623
|
+
const testFile = await findTestFile(rootDir);
|
|
1624
|
+
if (testFile !== null) {
|
|
1625
|
+
return `\`${testFile}\``;
|
|
1626
|
+
}
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
var TEST_FILE_PATTERNS = [
|
|
1630
|
+
/_test\.go$/u,
|
|
1631
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/u,
|
|
1632
|
+
/^test_.+\.py$/u,
|
|
1633
|
+
/_test\.py$/u,
|
|
1634
|
+
/.+Tests?\.(java|kt)$/u,
|
|
1635
|
+
/.+Test\.php$/u,
|
|
1636
|
+
/_spec\.rb$/u,
|
|
1637
|
+
/_test\.rb$/u
|
|
1638
|
+
];
|
|
1639
|
+
var TEST_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1640
|
+
"node_modules",
|
|
1641
|
+
"vendor",
|
|
1642
|
+
"dist",
|
|
1643
|
+
"build",
|
|
1644
|
+
"target",
|
|
1645
|
+
"coverage",
|
|
1646
|
+
"Pods",
|
|
1647
|
+
"__pycache__"
|
|
1648
|
+
]);
|
|
1649
|
+
async function findTestFile(rootDir) {
|
|
1650
|
+
let budget = 4e3;
|
|
1651
|
+
const stack = [rootDir];
|
|
1652
|
+
while (stack.length > 0 && budget > 0) {
|
|
1653
|
+
const dir = stack.pop();
|
|
1654
|
+
if (dir === void 0) {
|
|
1655
|
+
break;
|
|
1656
|
+
}
|
|
1657
|
+
let entries;
|
|
1658
|
+
try {
|
|
1659
|
+
entries = await readdir5(dir, { withFileTypes: true });
|
|
1660
|
+
} catch {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
for (const entry of entries) {
|
|
1664
|
+
budget -= 1;
|
|
1665
|
+
if (budget <= 0) {
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
if (entry.isDirectory()) {
|
|
1669
|
+
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1670
|
+
stack.push(path7.join(dir, entry.name));
|
|
1671
|
+
}
|
|
1672
|
+
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1673
|
+
return path7.relative(rootDir, path7.join(dir, entry.name));
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return null;
|
|
1678
|
+
}
|
|
1433
1679
|
function collectDependencies(pkg) {
|
|
1434
1680
|
if (pkg === null) {
|
|
1435
1681
|
return {};
|
|
@@ -1448,7 +1694,7 @@ async function readJson(rootDir, relativePath) {
|
|
|
1448
1694
|
}
|
|
1449
1695
|
async function readText(rootDir, relativePath) {
|
|
1450
1696
|
try {
|
|
1451
|
-
return await
|
|
1697
|
+
return await readFile4(path7.join(rootDir, relativePath), "utf8");
|
|
1452
1698
|
} catch {
|
|
1453
1699
|
return "";
|
|
1454
1700
|
}
|
|
@@ -1457,6 +1703,100 @@ function isRecord(value) {
|
|
|
1457
1703
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1458
1704
|
}
|
|
1459
1705
|
|
|
1706
|
+
// src/core/adopt/generate-adoption.ts
|
|
1707
|
+
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1708
|
+
function generateAdoptionFiles(options) {
|
|
1709
|
+
const files = [
|
|
1710
|
+
{
|
|
1711
|
+
path: ADOPTION_REPORT_PATH,
|
|
1712
|
+
content: renderReport(options.adrDir, options.signals)
|
|
1713
|
+
}
|
|
1714
|
+
];
|
|
1715
|
+
for (const framework of options.signals.frameworks) {
|
|
1716
|
+
files.push({
|
|
1717
|
+
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1718
|
+
content: renderProposedAdr(framework)
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
return files;
|
|
1722
|
+
}
|
|
1723
|
+
function renderReport(adrDir, signals) {
|
|
1724
|
+
return `# Adoption Report
|
|
1725
|
+
|
|
1726
|
+
## Status
|
|
1727
|
+
|
|
1728
|
+
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1729
|
+
is accepted repository memory until you accept it.
|
|
1730
|
+
|
|
1731
|
+
## Detected Signals
|
|
1732
|
+
|
|
1733
|
+
Each signal notes the file it was inferred from. If one is wrong, correct the source or edit this
|
|
1734
|
+
report \u2014 nothing here is accepted.
|
|
1735
|
+
|
|
1736
|
+
${summarizeSignals(signals).join("\n")}
|
|
1737
|
+
|
|
1738
|
+
## Proposed Decisions
|
|
1739
|
+
|
|
1740
|
+
${renderProposedDecisions(adrDir, signals)}
|
|
1741
|
+
|
|
1742
|
+
## Review Checklist
|
|
1743
|
+
|
|
1744
|
+
- [ ] Confirm the detected languages and package manager (and the source each was read from).
|
|
1745
|
+
- [ ] Confirm where tests were detected, or point Recall at the right location if it is wrong.
|
|
1746
|
+
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1747
|
+
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1748
|
+
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1749
|
+
|
|
1750
|
+
## Notes
|
|
1751
|
+
|
|
1752
|
+
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1753
|
+
files. No repository code was executed and no decision was accepted automatically.
|
|
1754
|
+
`;
|
|
1755
|
+
}
|
|
1756
|
+
function renderProposedDecisions(adrDir, signals) {
|
|
1757
|
+
if (signals.frameworks.length === 0) {
|
|
1758
|
+
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1759
|
+
}
|
|
1760
|
+
return signals.frameworks.map(
|
|
1761
|
+
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1762
|
+
).join("\n");
|
|
1763
|
+
}
|
|
1764
|
+
function renderProposedAdr(framework) {
|
|
1765
|
+
return `# Proposed ADR: Use ${framework}
|
|
1766
|
+
|
|
1767
|
+
## Status
|
|
1768
|
+
|
|
1769
|
+
Proposed
|
|
1770
|
+
|
|
1771
|
+
## Context
|
|
1772
|
+
|
|
1773
|
+
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1774
|
+
|
|
1775
|
+
## Decision
|
|
1776
|
+
|
|
1777
|
+
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1778
|
+
and is not accepted until a human reviews and accepts it.
|
|
1779
|
+
|
|
1780
|
+
## Alternatives Considered
|
|
1781
|
+
|
|
1782
|
+
- Record a different framework.
|
|
1783
|
+
- Leave the decision unrecorded for now.
|
|
1784
|
+
|
|
1785
|
+
## Consequences
|
|
1786
|
+
|
|
1787
|
+
- Captures a framework already in use as reviewable repository memory.
|
|
1788
|
+
- Requires explicit human acceptance before it becomes repository truth.
|
|
1789
|
+
|
|
1790
|
+
## Related Documents
|
|
1791
|
+
|
|
1792
|
+
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1793
|
+
- The adoption report generated alongside this proposal.
|
|
1794
|
+
`;
|
|
1795
|
+
}
|
|
1796
|
+
function frameworkSlug(framework) {
|
|
1797
|
+
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1460
1800
|
// src/commands/adopt.ts
|
|
1461
1801
|
var AdoptError = class extends Error {
|
|
1462
1802
|
code;
|
|
@@ -1530,8 +1870,8 @@ function formatList2(values) {
|
|
|
1530
1870
|
|
|
1531
1871
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1532
1872
|
import { existsSync as existsSync4 } from "fs";
|
|
1533
|
-
import { readFile as
|
|
1534
|
-
import
|
|
1873
|
+
import { readFile as readFile5, readdir as readdir6 } from "fs/promises";
|
|
1874
|
+
import path8 from "path";
|
|
1535
1875
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1536
1876
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
1537
1877
|
var MODULE_DOCS = ["MODULE.md", "DECISIONS.md"];
|
|
@@ -1548,7 +1888,7 @@ async function checkCodeReferences(context) {
|
|
|
1548
1888
|
continue;
|
|
1549
1889
|
}
|
|
1550
1890
|
for (const doc of FEATURE_DOCS) {
|
|
1551
|
-
const relativePath =
|
|
1891
|
+
const relativePath = path8.posix.join(context.config.featuresDir, folder.name, doc);
|
|
1552
1892
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1553
1893
|
}
|
|
1554
1894
|
}
|
|
@@ -1558,7 +1898,7 @@ async function checkCodeReferences(context) {
|
|
|
1558
1898
|
continue;
|
|
1559
1899
|
}
|
|
1560
1900
|
for (const doc of MODULE_DOCS) {
|
|
1561
|
-
const relativePath =
|
|
1901
|
+
const relativePath = path8.posix.join(context.config.modulesDir, folder.name, doc);
|
|
1562
1902
|
findings.push(...await checkDoc(context.rootDir, relativePath));
|
|
1563
1903
|
}
|
|
1564
1904
|
}
|
|
@@ -1577,7 +1917,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1577
1917
|
continue;
|
|
1578
1918
|
}
|
|
1579
1919
|
seen.add(reference);
|
|
1580
|
-
if (!existsSync4(
|
|
1920
|
+
if (!existsSync4(path8.join(rootDir, reference))) {
|
|
1581
1921
|
findings.push({
|
|
1582
1922
|
severity: "warning",
|
|
1583
1923
|
check: "drift-code-reference",
|
|
@@ -1590,7 +1930,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1590
1930
|
}
|
|
1591
1931
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1592
1932
|
try {
|
|
1593
|
-
return await
|
|
1933
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1594
1934
|
} catch (error) {
|
|
1595
1935
|
const nodeError = error;
|
|
1596
1936
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1601,7 +1941,7 @@ async function readDirIfExists(rootDir, relativePath) {
|
|
|
1601
1941
|
}
|
|
1602
1942
|
async function readFileIfExists(rootDir, relativePath) {
|
|
1603
1943
|
try {
|
|
1604
|
-
return await
|
|
1944
|
+
return await readFile5(path8.join(rootDir, relativePath), "utf8");
|
|
1605
1945
|
} catch (error) {
|
|
1606
1946
|
const nodeError = error;
|
|
1607
1947
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1612,12 +1952,12 @@ async function readFileIfExists(rootDir, relativePath) {
|
|
|
1612
1952
|
}
|
|
1613
1953
|
|
|
1614
1954
|
// src/core/doctor/checks/config-check.ts
|
|
1615
|
-
import { readFile as
|
|
1955
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
1616
1956
|
async function checkConfig(rootDir) {
|
|
1617
1957
|
const configPath = resolveSafePath(rootDir, CONFIG_PATH);
|
|
1618
1958
|
let rawConfig;
|
|
1619
1959
|
try {
|
|
1620
|
-
rawConfig = await
|
|
1960
|
+
rawConfig = await readFile6(configPath.absolutePath, "utf8");
|
|
1621
1961
|
} catch (error) {
|
|
1622
1962
|
const nodeError = error;
|
|
1623
1963
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1680,9 +2020,12 @@ async function checkConfig(rootDir) {
|
|
|
1680
2020
|
}
|
|
1681
2021
|
|
|
1682
2022
|
// src/core/doctor/checks/content-check.ts
|
|
1683
|
-
import { readFile as
|
|
1684
|
-
import
|
|
2023
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
2024
|
+
import path9 from "path";
|
|
1685
2025
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2026
|
+
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2027
|
+
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
2028
|
+
var THREAT_MODEL_PATH = "docs/20-security/THREAT_MODEL.md";
|
|
1686
2029
|
async function checkContent(context) {
|
|
1687
2030
|
if (context.config === void 0) {
|
|
1688
2031
|
return [];
|
|
@@ -1693,7 +2036,7 @@ async function checkContent(context) {
|
|
|
1693
2036
|
(entry) => entry.isDirectory() && featureFolderPattern2.test(entry.name)
|
|
1694
2037
|
);
|
|
1695
2038
|
for (const folder of featureFolders) {
|
|
1696
|
-
const prdPath =
|
|
2039
|
+
const prdPath = path9.posix.join(context.config.featuresDir, folder.name, "PRD.md");
|
|
1697
2040
|
const prd = await readFileIfExists2(context.rootDir, prdPath);
|
|
1698
2041
|
if (prd === void 0) {
|
|
1699
2042
|
continue;
|
|
@@ -1717,8 +2060,16 @@ async function checkContent(context) {
|
|
|
1717
2060
|
}
|
|
1718
2061
|
const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
|
|
1719
2062
|
const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
|
|
2063
|
+
const adrEntries = await readDirIfExists2(context.rootDir, context.config.adrDir);
|
|
2064
|
+
const acceptedAdrs = adrEntries.filter(
|
|
2065
|
+
(entry) => entry.isFile() && acceptedAdrPattern.test(entry.name)
|
|
2066
|
+
);
|
|
2067
|
+
const hasWork = featureFolders.length > 0 || moduleFolders.length > 0 || acceptedAdrs.length > 0;
|
|
2068
|
+
if (hasWork) {
|
|
2069
|
+
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
2070
|
+
}
|
|
1720
2071
|
for (const folder of moduleFolders) {
|
|
1721
|
-
const modulePath =
|
|
2072
|
+
const modulePath = path9.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1722
2073
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
1723
2074
|
if (moduleDoc === void 0) {
|
|
1724
2075
|
continue;
|
|
@@ -1742,6 +2093,28 @@ async function checkContent(context) {
|
|
|
1742
2093
|
}
|
|
1743
2094
|
return findings;
|
|
1744
2095
|
}
|
|
2096
|
+
async function checkSecurityDoc(rootDir) {
|
|
2097
|
+
const findings = [];
|
|
2098
|
+
const security = await readFileIfExists2(rootDir, SECURITY_MODEL_PATH);
|
|
2099
|
+
if (security !== void 0 && sectionIsUnfilled(security, "Authentication And Authorization")) {
|
|
2100
|
+
findings.push({
|
|
2101
|
+
severity: "warning",
|
|
2102
|
+
check: "content-security",
|
|
2103
|
+
message: "Security model authentication and authorization section is still an unfilled template.",
|
|
2104
|
+
path: SECURITY_MODEL_PATH
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
const threat = await readFileIfExists2(rootDir, THREAT_MODEL_PATH);
|
|
2108
|
+
if (threat !== void 0 && sectionIsUnfilled(threat, "Assets")) {
|
|
2109
|
+
findings.push({
|
|
2110
|
+
severity: "warning",
|
|
2111
|
+
check: "content-threat-model",
|
|
2112
|
+
message: "Threat model assets section is still an unfilled template.",
|
|
2113
|
+
path: THREAT_MODEL_PATH
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
return findings;
|
|
2117
|
+
}
|
|
1745
2118
|
function sectionIsUnfilled(content, heading) {
|
|
1746
2119
|
const section = getSection(content, heading);
|
|
1747
2120
|
return section !== void 0 && isUnfilled(section);
|
|
@@ -1754,7 +2127,7 @@ function isUnfilled(value) {
|
|
|
1754
2127
|
if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
|
|
1755
2128
|
return true;
|
|
1756
2129
|
}
|
|
1757
|
-
return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns");
|
|
2130
|
+
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");
|
|
1758
2131
|
}
|
|
1759
2132
|
function getSection(content, heading) {
|
|
1760
2133
|
const lines = content.split(/\r?\n/u);
|
|
@@ -1774,7 +2147,7 @@ function getSection(content, heading) {
|
|
|
1774
2147
|
}
|
|
1775
2148
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1776
2149
|
try {
|
|
1777
|
-
return await
|
|
2150
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1778
2151
|
} catch (error) {
|
|
1779
2152
|
const nodeError = error;
|
|
1780
2153
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1785,7 +2158,7 @@ async function readDirIfExists2(rootDir, relativePath) {
|
|
|
1785
2158
|
}
|
|
1786
2159
|
async function readFileIfExists2(rootDir, relativePath) {
|
|
1787
2160
|
try {
|
|
1788
|
-
return await
|
|
2161
|
+
return await readFile7(path9.join(rootDir, relativePath), "utf8");
|
|
1789
2162
|
} catch (error) {
|
|
1790
2163
|
const nodeError = error;
|
|
1791
2164
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1796,8 +2169,8 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1796
2169
|
}
|
|
1797
2170
|
|
|
1798
2171
|
// src/core/doctor/checks/drift-check.ts
|
|
1799
|
-
import { readFile as
|
|
1800
|
-
import
|
|
2172
|
+
import { readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
2173
|
+
import path10 from "path";
|
|
1801
2174
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1802
2175
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
1803
2176
|
async function checkDrift(context) {
|
|
@@ -1814,12 +2187,12 @@ async function loadKnownAdrs(rootDir, adrDir) {
|
|
|
1814
2187
|
const known = /* @__PURE__ */ new Map();
|
|
1815
2188
|
const files = await readMarkdownFiles(rootDir, adrDir);
|
|
1816
2189
|
for (const file of files) {
|
|
1817
|
-
const match = adrFilePattern.exec(
|
|
2190
|
+
const match = adrFilePattern.exec(path10.basename(file));
|
|
1818
2191
|
if (match === null) {
|
|
1819
2192
|
continue;
|
|
1820
2193
|
}
|
|
1821
2194
|
const id = `ADR-${match[1]}`;
|
|
1822
|
-
const content = await
|
|
2195
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
1823
2196
|
const accepted = sectionContains(content, "Status", /\baccepted\b/iu);
|
|
1824
2197
|
const existing = known.get(id);
|
|
1825
2198
|
if (existing === void 0 || !existing.accepted && accepted) {
|
|
@@ -1832,7 +2205,7 @@ async function checkReferences(rootDir, referenceDir, knownAdrs) {
|
|
|
1832
2205
|
const findings = [];
|
|
1833
2206
|
const files = await readMarkdownFiles(rootDir, referenceDir);
|
|
1834
2207
|
for (const file of files) {
|
|
1835
|
-
const content = await
|
|
2208
|
+
const content = await readFile8(path10.join(rootDir, file), "utf8");
|
|
1836
2209
|
const referenced = /* @__PURE__ */ new Set();
|
|
1837
2210
|
for (const match of stripCode(content).matchAll(adrReferencePattern)) {
|
|
1838
2211
|
referenced.add(match[0].toUpperCase());
|
|
@@ -1867,7 +2240,7 @@ async function readMarkdownFiles(rootDir, relativeDir) {
|
|
|
1867
2240
|
const entries = await readDirIfExists3(rootDir, relativeDir);
|
|
1868
2241
|
const files = [];
|
|
1869
2242
|
for (const entry of entries) {
|
|
1870
|
-
const childRelative =
|
|
2243
|
+
const childRelative = path10.posix.join(relativeDir, entry.name);
|
|
1871
2244
|
if (entry.isDirectory()) {
|
|
1872
2245
|
files.push(...await readMarkdownFiles(rootDir, childRelative));
|
|
1873
2246
|
continue;
|
|
@@ -1900,7 +2273,7 @@ function getSection2(content, heading) {
|
|
|
1900
2273
|
}
|
|
1901
2274
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
1902
2275
|
try {
|
|
1903
|
-
return await
|
|
2276
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
1904
2277
|
} catch (error) {
|
|
1905
2278
|
const nodeError = error;
|
|
1906
2279
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1911,8 +2284,8 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
1911
2284
|
}
|
|
1912
2285
|
|
|
1913
2286
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
1914
|
-
import { lstat as lstat2, readFile as
|
|
1915
|
-
import
|
|
2287
|
+
import { lstat as lstat2, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2288
|
+
import path11 from "path";
|
|
1916
2289
|
|
|
1917
2290
|
// src/core/adr/adr-sections.ts
|
|
1918
2291
|
var REQUIRED_ADR_SECTIONS = [
|
|
@@ -1976,7 +2349,7 @@ async function checkFeatureFolders(rootDir, featuresDir) {
|
|
|
1976
2349
|
);
|
|
1977
2350
|
for (const featureFolder of featureFolders) {
|
|
1978
2351
|
for (const requiredDoc of requiredFeatureDocs) {
|
|
1979
|
-
const filePath =
|
|
2352
|
+
const filePath = path11.posix.join(featuresDir, featureFolder.name, requiredDoc);
|
|
1980
2353
|
if (!await isFile(rootDir, filePath)) {
|
|
1981
2354
|
findings.push({
|
|
1982
2355
|
severity: "error",
|
|
@@ -2000,7 +2373,7 @@ async function checkModuleFolders(rootDir, modulesDir) {
|
|
|
2000
2373
|
const moduleFolders = entries.filter((entry) => entry.isDirectory());
|
|
2001
2374
|
for (const moduleFolder of moduleFolders) {
|
|
2002
2375
|
for (const requiredDoc of requiredModuleDocs) {
|
|
2003
|
-
const filePath =
|
|
2376
|
+
const filePath = path11.posix.join(modulesDir, moduleFolder.name, requiredDoc);
|
|
2004
2377
|
if (!await isFile(rootDir, filePath)) {
|
|
2005
2378
|
findings.push({
|
|
2006
2379
|
severity: "error",
|
|
@@ -2023,8 +2396,8 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2023
2396
|
const entries = await readDirIfExists4(rootDir, adrDir);
|
|
2024
2397
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern2.test(entry.name));
|
|
2025
2398
|
for (const adrFile of adrFiles) {
|
|
2026
|
-
const filePath =
|
|
2027
|
-
const content = await
|
|
2399
|
+
const filePath = path11.posix.join(adrDir, adrFile.name);
|
|
2400
|
+
const content = await readFile9(path11.join(rootDir, filePath), "utf8");
|
|
2028
2401
|
for (const requiredSection of requiredAdrSections) {
|
|
2029
2402
|
if (!content.includes(requiredSection)) {
|
|
2030
2403
|
findings.push({
|
|
@@ -2045,7 +2418,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2045
2418
|
}
|
|
2046
2419
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2047
2420
|
try {
|
|
2048
|
-
return await
|
|
2421
|
+
return await readdir9(path11.join(rootDir, relativePath), { withFileTypes: true });
|
|
2049
2422
|
} catch (error) {
|
|
2050
2423
|
const nodeError = error;
|
|
2051
2424
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2056,7 +2429,7 @@ async function readDirIfExists4(rootDir, relativePath) {
|
|
|
2056
2429
|
}
|
|
2057
2430
|
async function isFile(rootDir, relativePath) {
|
|
2058
2431
|
try {
|
|
2059
|
-
return (await lstat2(
|
|
2432
|
+
return (await lstat2(path11.join(rootDir, relativePath))).isFile();
|
|
2060
2433
|
} catch (error) {
|
|
2061
2434
|
const nodeError = error;
|
|
2062
2435
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2068,7 +2441,7 @@ async function isFile(rootDir, relativePath) {
|
|
|
2068
2441
|
|
|
2069
2442
|
// src/core/doctor/checks/required-files-check.ts
|
|
2070
2443
|
import { lstat as lstat3 } from "fs/promises";
|
|
2071
|
-
import
|
|
2444
|
+
import path12 from "path";
|
|
2072
2445
|
var rootFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
2073
2446
|
var requiredDocs = [
|
|
2074
2447
|
"00-product/PRD.md",
|
|
@@ -2095,12 +2468,12 @@ async function checkRequiredFiles(context) {
|
|
|
2095
2468
|
}
|
|
2096
2469
|
}
|
|
2097
2470
|
for (const relativeDocPath of requiredDocs) {
|
|
2098
|
-
const filePath =
|
|
2471
|
+
const filePath = path12.posix.join(docsDir, relativeDocPath);
|
|
2099
2472
|
if (!await isFile2(context.rootDir, filePath)) {
|
|
2100
2473
|
findings.push(missingFile(filePath, "required-docs"));
|
|
2101
2474
|
}
|
|
2102
2475
|
}
|
|
2103
|
-
const adrIndexPath =
|
|
2476
|
+
const adrIndexPath = path12.posix.join(context.config?.adrDir ?? "docs/adrs", "README.md");
|
|
2104
2477
|
if (!await isFile2(context.rootDir, adrIndexPath)) {
|
|
2105
2478
|
findings.push(missingFile(adrIndexPath, "required-docs"));
|
|
2106
2479
|
}
|
|
@@ -2126,7 +2499,7 @@ async function checkRequiredFiles(context) {
|
|
|
2126
2499
|
}
|
|
2127
2500
|
async function isFile2(rootDir, relativePath) {
|
|
2128
2501
|
try {
|
|
2129
|
-
return (await lstat3(
|
|
2502
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isFile();
|
|
2130
2503
|
} catch (error) {
|
|
2131
2504
|
const nodeError = error;
|
|
2132
2505
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2137,7 +2510,7 @@ async function isFile2(rootDir, relativePath) {
|
|
|
2137
2510
|
}
|
|
2138
2511
|
async function isDirectory(rootDir, relativePath) {
|
|
2139
2512
|
try {
|
|
2140
|
-
return (await lstat3(
|
|
2513
|
+
return (await lstat3(path12.join(rootDir, relativePath))).isDirectory();
|
|
2141
2514
|
} catch (error) {
|
|
2142
2515
|
const nodeError = error;
|
|
2143
2516
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2156,8 +2529,8 @@ function missingFile(pathValue, check) {
|
|
|
2156
2529
|
}
|
|
2157
2530
|
|
|
2158
2531
|
// src/core/doctor/checks/standards-check.ts
|
|
2159
|
-
import { lstat as lstat4, readFile as
|
|
2160
|
-
import
|
|
2532
|
+
import { lstat as lstat4, readFile as readFile10, readdir as readdir10 } from "fs/promises";
|
|
2533
|
+
import path13 from "path";
|
|
2161
2534
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2162
2535
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
2163
2536
|
var securitySensitivePattern = /\b(auth|authentication|authorization|secrets?|storage|networking?|telemetry|file writes?|write policy|dependencies?|mcp|ai api|cloud|runtime)\b/iu;
|
|
@@ -2177,10 +2550,10 @@ async function checkFeatureStandards(rootDir, featuresDir) {
|
|
|
2177
2550
|
(entry) => entry.isDirectory() && featureFolderPattern4.test(entry.name)
|
|
2178
2551
|
);
|
|
2179
2552
|
for (const featureFolder of featureFolders) {
|
|
2180
|
-
const featureDir =
|
|
2181
|
-
const completionReportPath =
|
|
2182
|
-
const reviewPath =
|
|
2183
|
-
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");
|
|
2184
2557
|
const completionReport = await readFileIfExists3(rootDir, completionReportPath);
|
|
2185
2558
|
const review = await readFileIfExists3(rootDir, reviewPath);
|
|
2186
2559
|
const architectureImpact = await readFileIfExists3(rootDir, architectureImpactPath);
|
|
@@ -2230,8 +2603,8 @@ async function checkAdrStandards(rootDir, adrDir) {
|
|
|
2230
2603
|
const entries = await readDirIfExists5(rootDir, adrDir);
|
|
2231
2604
|
const adrFiles = entries.filter((entry) => entry.isFile() && adrFilePattern3.test(entry.name));
|
|
2232
2605
|
for (const adrFile of adrFiles) {
|
|
2233
|
-
const adrPath =
|
|
2234
|
-
const content = await
|
|
2606
|
+
const adrPath = path13.posix.join(adrDir, adrFile.name);
|
|
2607
|
+
const content = await readFile10(path13.join(rootDir, adrPath), "utf8");
|
|
2235
2608
|
const isAccepted = sectionContains2(content, "Status", /\baccepted\b/iu);
|
|
2236
2609
|
if (!hasMeaningfulSection(content, "Consequences")) {
|
|
2237
2610
|
findings.push({
|
|
@@ -2315,7 +2688,7 @@ function isPlaceholder(value) {
|
|
|
2315
2688
|
}
|
|
2316
2689
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2317
2690
|
try {
|
|
2318
|
-
return await
|
|
2691
|
+
return await readdir10(path13.join(rootDir, relativePath), { withFileTypes: true });
|
|
2319
2692
|
} catch (error) {
|
|
2320
2693
|
const nodeError = error;
|
|
2321
2694
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2329,7 +2702,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2329
2702
|
if (!await isFile3(rootDir, relativePath)) {
|
|
2330
2703
|
return void 0;
|
|
2331
2704
|
}
|
|
2332
|
-
return await
|
|
2705
|
+
return await readFile10(path13.join(rootDir, relativePath), "utf8");
|
|
2333
2706
|
} catch (error) {
|
|
2334
2707
|
const nodeError = error;
|
|
2335
2708
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2340,7 +2713,7 @@ async function readFileIfExists3(rootDir, relativePath) {
|
|
|
2340
2713
|
}
|
|
2341
2714
|
async function isFile3(rootDir, relativePath) {
|
|
2342
2715
|
try {
|
|
2343
|
-
return (await lstat4(
|
|
2716
|
+
return (await lstat4(path13.join(rootDir, relativePath))).isFile();
|
|
2344
2717
|
} catch (error) {
|
|
2345
2718
|
const nodeError = error;
|
|
2346
2719
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2350,6 +2723,110 @@ async function isFile3(rootDir, relativePath) {
|
|
|
2350
2723
|
}
|
|
2351
2724
|
}
|
|
2352
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
|
+
|
|
2353
2830
|
// src/core/doctor/doctor-check.ts
|
|
2354
2831
|
async function runDoctor(rootDir) {
|
|
2355
2832
|
const findings = [];
|
|
@@ -2366,6 +2843,7 @@ async function runDoctor(rootDir) {
|
|
|
2366
2843
|
findings.push(...await checkDrift(context));
|
|
2367
2844
|
findings.push(...await checkContent(context));
|
|
2368
2845
|
findings.push(...await checkCodeReferences(context));
|
|
2846
|
+
findings.push(...await checkSuperseded(context));
|
|
2369
2847
|
}
|
|
2370
2848
|
return createDoctorReport(findings);
|
|
2371
2849
|
}
|
|
@@ -2443,10 +2921,10 @@ function formatDoctorResult(result) {
|
|
|
2443
2921
|
|
|
2444
2922
|
// src/commands/init.ts
|
|
2445
2923
|
import { existsSync as existsSync6 } from "fs";
|
|
2446
|
-
import
|
|
2924
|
+
import path17 from "path";
|
|
2447
2925
|
|
|
2448
2926
|
// src/core/generator/generate-init.ts
|
|
2449
|
-
import
|
|
2927
|
+
import path15 from "path";
|
|
2450
2928
|
var neutralTemplates = [
|
|
2451
2929
|
{
|
|
2452
2930
|
path: "AGENTS.md",
|
|
@@ -2465,6 +2943,18 @@ Required reading:
|
|
|
2465
2943
|
- \`docs/60-engineering/ENGINEERING_STANDARDS.md\`
|
|
2466
2944
|
|
|
2467
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.
|
|
2468
2958
|
`
|
|
2469
2959
|
},
|
|
2470
2960
|
{
|
|
@@ -2501,6 +2991,8 @@ Before non-trivial work:
|
|
|
2501
2991
|
- Read \`AGENTS.md\` and the docs it routes to.
|
|
2502
2992
|
- Accepted decisions live in \`docs/adrs/\`; module memory lives in \`docs/30-modules/\`.
|
|
2503
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.
|
|
2504
2996
|
|
|
2505
2997
|
Source-of-truth order: accepted ADRs and repository decisions, then architecture docs, engineering
|
|
2506
2998
|
standards, the current PRD, security and testing docs, module docs, feature plans, then chat history.
|
|
@@ -2589,26 +3081,78 @@ Default behavior:
|
|
|
2589
3081
|
path: "docs/20-security/SECURITY_MODEL.md",
|
|
2590
3082
|
content: `# Security Model
|
|
2591
3083
|
|
|
2592
|
-
##
|
|
3084
|
+
## Status
|
|
2593
3085
|
|
|
2594
|
-
Draft.
|
|
3086
|
+
Draft \u2014 fill the prompted sections below with this repository's real model as it grows. \`recall doctor\`
|
|
3087
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2595
3088
|
|
|
2596
3089
|
## Baseline Rules
|
|
2597
3090
|
|
|
2598
|
-
-
|
|
2599
|
-
-
|
|
3091
|
+
- Never commit secrets or credentials, and never read or copy \`.env\` files into docs.
|
|
3092
|
+
- Validate and authorize untrusted input at every trust boundary.
|
|
2600
3093
|
- Do not add network, telemetry, cloud, MCP runtime, or AI API behavior without explicit review.
|
|
3094
|
+
|
|
3095
|
+
## Authentication And Authorization
|
|
3096
|
+
|
|
3097
|
+
Describe how this repository authenticates users or clients and how it authorizes actions, including
|
|
3098
|
+
where those checks live.
|
|
3099
|
+
|
|
3100
|
+
## Secrets And Configuration
|
|
3101
|
+
|
|
3102
|
+
Describe where secrets live, how they are injected, and how configuration is kept out of version
|
|
3103
|
+
control.
|
|
3104
|
+
|
|
3105
|
+
## Sensitive Data
|
|
3106
|
+
|
|
3107
|
+
Describe the sensitive or personal data this repository handles, and how it is protected at rest and
|
|
3108
|
+
in transit.
|
|
3109
|
+
|
|
3110
|
+
## Dependencies And Supply Chain
|
|
3111
|
+
|
|
3112
|
+
Describe how third-party dependencies are vetted, pinned, and updated.
|
|
2601
3113
|
`
|
|
2602
3114
|
},
|
|
2603
3115
|
{
|
|
2604
3116
|
path: "docs/20-security/THREAT_MODEL.md",
|
|
2605
3117
|
content: `# Threat Model
|
|
2606
3118
|
|
|
2607
|
-
##
|
|
3119
|
+
## Status
|
|
2608
3120
|
|
|
2609
|
-
Draft.
|
|
3121
|
+
Draft \u2014 replace the prompts below with this repository's real analysis as it grows. \`recall doctor\`
|
|
3122
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
3123
|
+
|
|
3124
|
+
## Assets
|
|
2610
3125
|
|
|
2611
|
-
|
|
3126
|
+
Describe what this repository must protect: user data, credentials, money, availability, or
|
|
3127
|
+
reputation.
|
|
3128
|
+
|
|
3129
|
+
## Entry Points
|
|
3130
|
+
|
|
3131
|
+
Describe where untrusted input enters: HTTP endpoints, webhooks, file uploads, queues, CLI input, or
|
|
3132
|
+
third-party callbacks.
|
|
3133
|
+
|
|
3134
|
+
## Trust Boundaries
|
|
3135
|
+
|
|
3136
|
+
Describe where trust changes: client to server, service to database, your code to third-party APIs.
|
|
3137
|
+
|
|
3138
|
+
## Threats
|
|
3139
|
+
|
|
3140
|
+
Describe the concrete threats that apply to this repository, by category:
|
|
3141
|
+
|
|
3142
|
+
- Spoofing \u2014 how identities are faked or sessions stolen.
|
|
3143
|
+
- Tampering \u2014 how requests, data, or builds are altered (injection, mass assignment).
|
|
3144
|
+
- Repudiation \u2014 actions that must remain auditable.
|
|
3145
|
+
- Information disclosure \u2014 how sensitive data or secrets could leak.
|
|
3146
|
+
- Denial of service \u2014 how the system can be overwhelmed or abused.
|
|
3147
|
+
- Elevation of privilege \u2014 how a user could gain access they should not have.
|
|
3148
|
+
|
|
3149
|
+
## Mitigations
|
|
3150
|
+
|
|
3151
|
+
Describe the control in place or planned for each threat above.
|
|
3152
|
+
|
|
3153
|
+
## Open Risks
|
|
3154
|
+
|
|
3155
|
+
Describe accepted or unresolved risks and who owns them.
|
|
2612
3156
|
`
|
|
2613
3157
|
},
|
|
2614
3158
|
{
|
|
@@ -2657,6 +3201,15 @@ Baseline rules:
|
|
|
2657
3201
|
AI agents must follow repository memory over model preference.
|
|
2658
3202
|
|
|
2659
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.
|
|
2660
3213
|
`
|
|
2661
3214
|
},
|
|
2662
3215
|
{
|
|
@@ -2790,6 +3343,17 @@ Options:
|
|
|
2790
3343
|
- \`--dry-run\`: show planned writes without writing files.
|
|
2791
3344
|
- \`--force\`: overwrite existing files explicitly.
|
|
2792
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
|
+
|
|
2793
3357
|
### \`recall module create <name>\`
|
|
2794
3358
|
|
|
2795
3359
|
Create module memory docs under the configured modules directory.
|
|
@@ -2805,7 +3369,8 @@ Check whether repository memory is structurally healthy enough for AI-assisted w
|
|
|
2805
3369
|
engineering evidence is present, and whether memory references decisions that exist and are accepted.
|
|
2806
3370
|
|
|
2807
3371
|
Doctor also runs deterministic drift checks: feature or module memory that references a missing ADR
|
|
2808
|
-
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.
|
|
2809
3374
|
|
|
2810
3375
|
Exit codes:
|
|
2811
3376
|
|
|
@@ -2892,7 +3457,7 @@ jobs:
|
|
|
2892
3457
|
}
|
|
2893
3458
|
];
|
|
2894
3459
|
function generateInitFiles(options) {
|
|
2895
|
-
const repositoryName =
|
|
3460
|
+
const repositoryName = path15.basename(path15.resolve(options.rootDir)) || "repository";
|
|
2896
3461
|
const context = createTemplateContext({ repositoryName });
|
|
2897
3462
|
const files = neutralTemplates.map((template) => ({
|
|
2898
3463
|
path: template.path,
|
|
@@ -2919,17 +3484,17 @@ function generatePresetFiles(preset) {
|
|
|
2919
3484
|
|
|
2920
3485
|
// src/core/hooks/detect-gates.ts
|
|
2921
3486
|
import { existsSync as existsSync5 } from "fs";
|
|
2922
|
-
import { readFile as
|
|
2923
|
-
import
|
|
3487
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3488
|
+
import path16 from "path";
|
|
2924
3489
|
var KNOWN_SCRIPTS = ["test", "typecheck", "lint"];
|
|
2925
3490
|
async function detectPreCommitGates(rootDir) {
|
|
2926
|
-
const packageJsonPath =
|
|
3491
|
+
const packageJsonPath = path16.join(rootDir, "package.json");
|
|
2927
3492
|
if (!existsSync5(packageJsonPath)) {
|
|
2928
3493
|
return [];
|
|
2929
3494
|
}
|
|
2930
3495
|
let scripts;
|
|
2931
3496
|
try {
|
|
2932
|
-
const raw = await
|
|
3497
|
+
const raw = await readFile12(packageJsonPath, "utf8");
|
|
2933
3498
|
const parsed = JSON.parse(raw);
|
|
2934
3499
|
scripts = parsed.scripts ?? {};
|
|
2935
3500
|
} catch {
|
|
@@ -2938,16 +3503,16 @@ async function detectPreCommitGates(rootDir) {
|
|
|
2938
3503
|
if (typeof scripts !== "object" || scripts === null) {
|
|
2939
3504
|
return [];
|
|
2940
3505
|
}
|
|
2941
|
-
const packageManager =
|
|
3506
|
+
const packageManager = detectPackageManager2(rootDir);
|
|
2942
3507
|
return KNOWN_SCRIPTS.filter((script) => typeof scripts[script] === "string").map(
|
|
2943
3508
|
(script) => `${packageManager} run ${script}`
|
|
2944
3509
|
);
|
|
2945
3510
|
}
|
|
2946
|
-
function
|
|
2947
|
-
if (existsSync5(
|
|
3511
|
+
function detectPackageManager2(rootDir) {
|
|
3512
|
+
if (existsSync5(path16.join(rootDir, "pnpm-lock.yaml"))) {
|
|
2948
3513
|
return "pnpm";
|
|
2949
3514
|
}
|
|
2950
|
-
if (existsSync5(
|
|
3515
|
+
if (existsSync5(path16.join(rootDir, "yarn.lock"))) {
|
|
2951
3516
|
return "yarn";
|
|
2952
3517
|
}
|
|
2953
3518
|
return "npm";
|
|
@@ -4452,8 +5017,8 @@ function parsePreset(value) {
|
|
|
4452
5017
|
if (!result.success) {
|
|
4453
5018
|
throw new PresetValidationError(
|
|
4454
5019
|
result.error.issues.map((issue) => {
|
|
4455
|
-
const
|
|
4456
|
-
return `${
|
|
5020
|
+
const path19 = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
|
|
5021
|
+
return `${path19}${issue.message}`;
|
|
4457
5022
|
})
|
|
4458
5023
|
);
|
|
4459
5024
|
}
|
|
@@ -5142,7 +5707,7 @@ var InitError = class extends Error {
|
|
|
5142
5707
|
}
|
|
5143
5708
|
};
|
|
5144
5709
|
async function initProject(options) {
|
|
5145
|
-
if (options.force === true && options.reinit !== true && existsSync6(
|
|
5710
|
+
if (options.force === true && options.reinit !== true && existsSync6(path17.join(options.rootDir, CONFIG_PATH))) {
|
|
5146
5711
|
throw new InitError(
|
|
5147
5712
|
"EXISTING_INSTALLATION",
|
|
5148
5713
|
"Refusing to re-initialize an existing Recall OS installation.",
|
|
@@ -5154,6 +5719,7 @@ async function initProject(options) {
|
|
|
5154
5719
|
);
|
|
5155
5720
|
}
|
|
5156
5721
|
const preset = resolvePreset(options.preset);
|
|
5722
|
+
const detected = await inspectRepo(options.rootDir);
|
|
5157
5723
|
const preCommitGates = await detectPreCommitGates(options.rootDir);
|
|
5158
5724
|
const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
|
|
5159
5725
|
const files = createInitWriteFiles(options.rootDir, config, preset);
|
|
@@ -5174,7 +5740,8 @@ async function initProject(options) {
|
|
|
5174
5740
|
preset: preset?.id ?? null,
|
|
5175
5741
|
dryRun: options.dryRun ?? false,
|
|
5176
5742
|
plan,
|
|
5177
|
-
writeResult
|
|
5743
|
+
writeResult,
|
|
5744
|
+
detected
|
|
5178
5745
|
};
|
|
5179
5746
|
}
|
|
5180
5747
|
function formatInitResult(result) {
|
|
@@ -5191,6 +5758,7 @@ function formatInitResult(result) {
|
|
|
5191
5758
|
dryRun: result.dryRun,
|
|
5192
5759
|
writeResult: result.writeResult
|
|
5193
5760
|
});
|
|
5761
|
+
appendDetectedStack(lines, result.detected);
|
|
5194
5762
|
const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
|
|
5195
5763
|
if (hookWritten) {
|
|
5196
5764
|
lines.push("");
|
|
@@ -5213,6 +5781,21 @@ function formatInitResult(result) {
|
|
|
5213
5781
|
return `${lines.join("\n")}
|
|
5214
5782
|
`;
|
|
5215
5783
|
}
|
|
5784
|
+
function appendDetectedStack(lines, detected) {
|
|
5785
|
+
const hasSignal = detected.languages.length > 0 || detected.frameworks.length > 0 || detected.packageManager !== null || detected.testsEvidence !== null;
|
|
5786
|
+
if (!hasSignal) {
|
|
5787
|
+
return;
|
|
5788
|
+
}
|
|
5789
|
+
const stack = summarizeSignals(detected).filter(
|
|
5790
|
+
(line) => !line.startsWith("- README") && !line.startsWith("- Docs")
|
|
5791
|
+
);
|
|
5792
|
+
lines.push("");
|
|
5793
|
+
lines.push("Detected in this repository (proposed \u2014 review, nothing was accepted):");
|
|
5794
|
+
lines.push(...stack);
|
|
5795
|
+
lines.push(
|
|
5796
|
+
"If any signal is wrong, correct the source file noted. Run `recall adopt` to record this as proposed memory."
|
|
5797
|
+
);
|
|
5798
|
+
}
|
|
5216
5799
|
function resolvePreset(presetId) {
|
|
5217
5800
|
if (presetId === void 0) {
|
|
5218
5801
|
return null;
|
|
@@ -5488,7 +6071,7 @@ async function loadConfigOrDefault2(rootDir) {
|
|
|
5488
6071
|
}
|
|
5489
6072
|
|
|
5490
6073
|
// src/core/generator/generate-module.ts
|
|
5491
|
-
import
|
|
6074
|
+
import path18 from "path";
|
|
5492
6075
|
var moduleTemplates = [
|
|
5493
6076
|
{
|
|
5494
6077
|
fileName: "MODULE.md",
|
|
@@ -5561,14 +6144,14 @@ Record durable module decisions here.
|
|
|
5561
6144
|
];
|
|
5562
6145
|
function generateModuleFiles(options) {
|
|
5563
6146
|
const slug = slugify(options.moduleName);
|
|
5564
|
-
const moduleDir =
|
|
6147
|
+
const moduleDir = path18.posix.join(options.modulesDir, slug);
|
|
5565
6148
|
const title = titleizeModuleName(options.moduleName);
|
|
5566
6149
|
const context = createTemplateContext({
|
|
5567
6150
|
slug,
|
|
5568
6151
|
title
|
|
5569
6152
|
});
|
|
5570
6153
|
return moduleTemplates.map((template) => ({
|
|
5571
|
-
path:
|
|
6154
|
+
path: path18.posix.join(moduleDir, template.fileName),
|
|
5572
6155
|
content: renderTemplate(template.content, context)
|
|
5573
6156
|
}));
|
|
5574
6157
|
}
|
|
@@ -5589,7 +6172,7 @@ var ModuleCreateError = class extends Error {
|
|
|
5589
6172
|
};
|
|
5590
6173
|
async function createModule(options) {
|
|
5591
6174
|
const slug = createModuleSlug(options.name);
|
|
5592
|
-
const config = await
|
|
6175
|
+
const config = await loadRequiredConfig5(options.rootDir);
|
|
5593
6176
|
const files = generateModuleFiles({
|
|
5594
6177
|
modulesDir: config.modulesDir,
|
|
5595
6178
|
moduleName: options.name
|
|
@@ -5643,7 +6226,7 @@ function createModuleSlug(name) {
|
|
|
5643
6226
|
throw error;
|
|
5644
6227
|
}
|
|
5645
6228
|
}
|
|
5646
|
-
async function
|
|
6229
|
+
async function loadRequiredConfig5(rootDir) {
|
|
5647
6230
|
try {
|
|
5648
6231
|
return await loadConfig(rootDir);
|
|
5649
6232
|
} catch (error) {
|
|
@@ -5817,6 +6400,20 @@ function createCliProgram(io = {}, state = { exitCode: 0 }) {
|
|
|
5817
6400
|
});
|
|
5818
6401
|
stdout.write(formatAdrAcceptResult(result));
|
|
5819
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
|
+
);
|
|
5820
6417
|
const moduleCommand = program.command("module").description("Manage Recall OS module memory.");
|
|
5821
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) => {
|
|
5822
6419
|
const result = await createModule({
|
|
@@ -5910,6 +6507,15 @@ async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
5910
6507
|
`);
|
|
5911
6508
|
for (const detail of error.details) {
|
|
5912
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}
|
|
5913
6519
|
`);
|
|
5914
6520
|
}
|
|
5915
6521
|
return 1;
|