recall-os 0.2.0 → 0.2.1
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 +8 -8
- package/dist/cli.js +388 -133
- package/dist/cli.js.map +1 -1
- package/dist/index.js +388 -133
- package/dist/index.js.map +1 -1
- 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-generic/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-generic/docs/20-security/THREAT_MODEL.md +35 -3
- 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-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-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-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-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-nextjs/docs/20-security/SECURITY_MODEL.md +25 -4
- package/examples/generated-nextjs/docs/20-security/THREAT_MODEL.md +35 -3
- 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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1228,111 +1228,31 @@ function createDefaultConfig(overrides = {}) {
|
|
|
1228
1228
|
});
|
|
1229
1229
|
}
|
|
1230
1230
|
|
|
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
1231
|
// src/core/adopt/inspect-repo.ts
|
|
1333
1232
|
import { existsSync as existsSync3 } from "fs";
|
|
1334
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
1233
|
+
import { readFile as readFile3, readdir as readdir4 } from "fs/promises";
|
|
1335
1234
|
import path6 from "path";
|
|
1235
|
+
var FRAMEWORK_SOURCES = {
|
|
1236
|
+
"Next.js": "package.json",
|
|
1237
|
+
React: "package.json",
|
|
1238
|
+
NestJS: "package.json",
|
|
1239
|
+
Express: "package.json",
|
|
1240
|
+
FastAPI: "pyproject.toml / requirements.txt",
|
|
1241
|
+
Flask: "pyproject.toml / requirements.txt",
|
|
1242
|
+
Django: "pyproject.toml / requirements.txt",
|
|
1243
|
+
Gin: "go.mod",
|
|
1244
|
+
Echo: "go.mod",
|
|
1245
|
+
Fiber: "go.mod",
|
|
1246
|
+
Chi: "go.mod",
|
|
1247
|
+
"Spring Boot": "pom.xml / build.gradle",
|
|
1248
|
+
"Actix Web": "Cargo.toml",
|
|
1249
|
+
Axum: "Cargo.toml",
|
|
1250
|
+
Rocket: "Cargo.toml",
|
|
1251
|
+
Laravel: "composer.json",
|
|
1252
|
+
Symfony: "composer.json",
|
|
1253
|
+
"Ruby on Rails": "Gemfile",
|
|
1254
|
+
Flutter: "pubspec.yaml"
|
|
1255
|
+
};
|
|
1336
1256
|
async function inspectRepo(rootDir) {
|
|
1337
1257
|
const has = (relativePath) => existsSync3(path6.join(rootDir, relativePath));
|
|
1338
1258
|
const languages = /* @__PURE__ */ new Set();
|
|
@@ -1366,6 +1286,22 @@ async function inspectRepo(rootDir) {
|
|
|
1366
1286
|
languages.add("Dart");
|
|
1367
1287
|
frameworks.add("Flutter");
|
|
1368
1288
|
}
|
|
1289
|
+
if (has("composer.json")) {
|
|
1290
|
+
languages.add("PHP");
|
|
1291
|
+
const composer = (await readText(rootDir, "composer.json")).toLowerCase();
|
|
1292
|
+
if (composer.includes("laravel/framework")) {
|
|
1293
|
+
frameworks.add("Laravel");
|
|
1294
|
+
} else if (composer.includes("symfony/")) {
|
|
1295
|
+
frameworks.add("Symfony");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (has("Gemfile")) {
|
|
1299
|
+
languages.add("Ruby");
|
|
1300
|
+
const gemfile = (await readText(rootDir, "Gemfile")).toLowerCase();
|
|
1301
|
+
if (gemfile.includes("rails")) {
|
|
1302
|
+
frameworks.add("Ruby on Rails");
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1369
1305
|
const deps = collectDependencies(pkg);
|
|
1370
1306
|
if ("next" in deps) {
|
|
1371
1307
|
frameworks.add("Next.js");
|
|
@@ -1411,25 +1347,147 @@ async function inspectRepo(rootDir) {
|
|
|
1411
1347
|
frameworks.add("Rocket");
|
|
1412
1348
|
}
|
|
1413
1349
|
}
|
|
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
|
-
}
|
|
1350
|
+
const [packageManager, packageManagerSource] = detectPackageManager(has);
|
|
1422
1351
|
const scripts = pkg !== null && isRecord(pkg.scripts) ? pkg.scripts : {};
|
|
1423
|
-
const
|
|
1352
|
+
const testsEvidence = await detectTestsEvidence(rootDir, has, "test" in scripts, python);
|
|
1424
1353
|
return {
|
|
1425
1354
|
languages: [...languages],
|
|
1426
1355
|
packageManager,
|
|
1356
|
+
packageManagerSource,
|
|
1427
1357
|
frameworks: [...frameworks],
|
|
1428
|
-
hasTests,
|
|
1358
|
+
hasTests: testsEvidence !== null,
|
|
1359
|
+
testsEvidence,
|
|
1429
1360
|
hasReadme: has("README.md") || has("README"),
|
|
1430
1361
|
hasDocs: has("docs")
|
|
1431
1362
|
};
|
|
1432
1363
|
}
|
|
1364
|
+
function summarizeSignals(signals) {
|
|
1365
|
+
const lines = [];
|
|
1366
|
+
lines.push(`- Languages: ${formatList(signals.languages)}`);
|
|
1367
|
+
lines.push(
|
|
1368
|
+
signals.packageManager === null ? "- Package manager: none detected" : `- Package manager: ${signals.packageManager}${signals.packageManagerSource === null ? "" : ` (from \`${signals.packageManagerSource}\`)`}`
|
|
1369
|
+
);
|
|
1370
|
+
if (signals.frameworks.length === 0) {
|
|
1371
|
+
lines.push("- Frameworks: none detected");
|
|
1372
|
+
} else {
|
|
1373
|
+
const withSource = signals.frameworks.map((framework) => {
|
|
1374
|
+
const source = FRAMEWORK_SOURCES[framework];
|
|
1375
|
+
return source === void 0 ? framework : `${framework} (from \`${source}\`)`;
|
|
1376
|
+
});
|
|
1377
|
+
lines.push(`- Frameworks: ${withSource.join(", ")}`);
|
|
1378
|
+
}
|
|
1379
|
+
lines.push(
|
|
1380
|
+
signals.testsEvidence === null ? "- Tests: none detected \u2014 if tests exist, point Recall at them by correcting this report" : `- Tests: detected via ${signals.testsEvidence}`
|
|
1381
|
+
);
|
|
1382
|
+
lines.push(`- README present: ${signals.hasReadme ? "yes" : "no"}`);
|
|
1383
|
+
lines.push(`- Docs folder present: ${signals.hasDocs ? "yes" : "no"}`);
|
|
1384
|
+
return lines;
|
|
1385
|
+
}
|
|
1386
|
+
function formatList(values) {
|
|
1387
|
+
return values.length === 0 ? "none detected" : values.join(", ");
|
|
1388
|
+
}
|
|
1389
|
+
function detectPackageManager(has) {
|
|
1390
|
+
const candidates = [
|
|
1391
|
+
[has("go.mod"), "Go modules", "go.mod"],
|
|
1392
|
+
[has("Cargo.toml"), "Cargo", "Cargo.toml"],
|
|
1393
|
+
[has("pom.xml"), "Maven", "pom.xml"],
|
|
1394
|
+
[has("build.gradle"), "Gradle", "build.gradle"],
|
|
1395
|
+
[has("build.gradle.kts"), "Gradle", "build.gradle.kts"],
|
|
1396
|
+
[has("composer.json"), "Composer", "composer.json"],
|
|
1397
|
+
[has("Gemfile"), "Bundler", "Gemfile"],
|
|
1398
|
+
[has("Package.swift"), "Swift Package Manager", "Package.swift"],
|
|
1399
|
+
[has("pubspec.yaml"), "pub", "pubspec.yaml"],
|
|
1400
|
+
[has("uv.lock"), "uv", "uv.lock"],
|
|
1401
|
+
[has("poetry.lock"), "Poetry", "poetry.lock"],
|
|
1402
|
+
[has("requirements.txt"), "pip", "requirements.txt"],
|
|
1403
|
+
[has("pyproject.toml"), "pip", "pyproject.toml"],
|
|
1404
|
+
[has("pnpm-lock.yaml"), "pnpm", "pnpm-lock.yaml"],
|
|
1405
|
+
[has("yarn.lock"), "yarn", "yarn.lock"],
|
|
1406
|
+
[has("package-lock.json"), "npm", "package-lock.json"],
|
|
1407
|
+
[has("package.json"), "npm", "package.json"]
|
|
1408
|
+
];
|
|
1409
|
+
for (const [present, name, source] of candidates) {
|
|
1410
|
+
if (present) {
|
|
1411
|
+
return [name, source];
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
return [null, null];
|
|
1415
|
+
}
|
|
1416
|
+
async function detectTestsEvidence(rootDir, has, hasTestScript, pythonText) {
|
|
1417
|
+
if (has("tests")) {
|
|
1418
|
+
return "`tests/` directory";
|
|
1419
|
+
}
|
|
1420
|
+
if (has("test")) {
|
|
1421
|
+
return "`test/` directory";
|
|
1422
|
+
}
|
|
1423
|
+
if (has("__tests__")) {
|
|
1424
|
+
return "`__tests__/` directory";
|
|
1425
|
+
}
|
|
1426
|
+
if (has("pytest.ini") || pythonText.includes("pytest")) {
|
|
1427
|
+
return "pytest configuration";
|
|
1428
|
+
}
|
|
1429
|
+
if (has("phpunit.xml") || has("phpunit.xml.dist")) {
|
|
1430
|
+
return "PHPUnit configuration";
|
|
1431
|
+
}
|
|
1432
|
+
if (hasTestScript) {
|
|
1433
|
+
return '`"test"` script in package.json';
|
|
1434
|
+
}
|
|
1435
|
+
const testFile = await findTestFile(rootDir);
|
|
1436
|
+
if (testFile !== null) {
|
|
1437
|
+
return `\`${testFile}\``;
|
|
1438
|
+
}
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
var TEST_FILE_PATTERNS = [
|
|
1442
|
+
/_test\.go$/u,
|
|
1443
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/u,
|
|
1444
|
+
/^test_.+\.py$/u,
|
|
1445
|
+
/_test\.py$/u,
|
|
1446
|
+
/.+Tests?\.(java|kt)$/u,
|
|
1447
|
+
/.+Test\.php$/u,
|
|
1448
|
+
/_spec\.rb$/u,
|
|
1449
|
+
/_test\.rb$/u
|
|
1450
|
+
];
|
|
1451
|
+
var TEST_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1452
|
+
"node_modules",
|
|
1453
|
+
"vendor",
|
|
1454
|
+
"dist",
|
|
1455
|
+
"build",
|
|
1456
|
+
"target",
|
|
1457
|
+
"coverage",
|
|
1458
|
+
"Pods",
|
|
1459
|
+
"__pycache__"
|
|
1460
|
+
]);
|
|
1461
|
+
async function findTestFile(rootDir) {
|
|
1462
|
+
let budget = 4e3;
|
|
1463
|
+
const stack = [rootDir];
|
|
1464
|
+
while (stack.length > 0 && budget > 0) {
|
|
1465
|
+
const dir = stack.pop();
|
|
1466
|
+
if (dir === void 0) {
|
|
1467
|
+
break;
|
|
1468
|
+
}
|
|
1469
|
+
let entries;
|
|
1470
|
+
try {
|
|
1471
|
+
entries = await readdir4(dir, { withFileTypes: true });
|
|
1472
|
+
} catch {
|
|
1473
|
+
continue;
|
|
1474
|
+
}
|
|
1475
|
+
for (const entry of entries) {
|
|
1476
|
+
budget -= 1;
|
|
1477
|
+
if (budget <= 0) {
|
|
1478
|
+
break;
|
|
1479
|
+
}
|
|
1480
|
+
if (entry.isDirectory()) {
|
|
1481
|
+
if (!TEST_WALK_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
|
|
1482
|
+
stack.push(path6.join(dir, entry.name));
|
|
1483
|
+
}
|
|
1484
|
+
} else if (TEST_FILE_PATTERNS.some((pattern) => pattern.test(entry.name))) {
|
|
1485
|
+
return path6.relative(rootDir, path6.join(dir, entry.name));
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1433
1491
|
function collectDependencies(pkg) {
|
|
1434
1492
|
if (pkg === null) {
|
|
1435
1493
|
return {};
|
|
@@ -1457,6 +1515,100 @@ function isRecord(value) {
|
|
|
1457
1515
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1458
1516
|
}
|
|
1459
1517
|
|
|
1518
|
+
// src/core/adopt/generate-adoption.ts
|
|
1519
|
+
var ADOPTION_REPORT_PATH = "docs/adopt/ADOPTION_REPORT.md";
|
|
1520
|
+
function generateAdoptionFiles(options) {
|
|
1521
|
+
const files = [
|
|
1522
|
+
{
|
|
1523
|
+
path: ADOPTION_REPORT_PATH,
|
|
1524
|
+
content: renderReport(options.adrDir, options.signals)
|
|
1525
|
+
}
|
|
1526
|
+
];
|
|
1527
|
+
for (const framework of options.signals.frameworks) {
|
|
1528
|
+
files.push({
|
|
1529
|
+
path: `${options.adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md`,
|
|
1530
|
+
content: renderProposedAdr(framework)
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
return files;
|
|
1534
|
+
}
|
|
1535
|
+
function renderReport(adrDir, signals) {
|
|
1536
|
+
return `# Adoption Report
|
|
1537
|
+
|
|
1538
|
+
## Status
|
|
1539
|
+
|
|
1540
|
+
Proposed. Everything below is inferred from this repository and requires human review. Nothing here
|
|
1541
|
+
is accepted repository memory until you accept it.
|
|
1542
|
+
|
|
1543
|
+
## Detected Signals
|
|
1544
|
+
|
|
1545
|
+
Each signal notes the file it was inferred from. If one is wrong, correct the source or edit this
|
|
1546
|
+
report \u2014 nothing here is accepted.
|
|
1547
|
+
|
|
1548
|
+
${summarizeSignals(signals).join("\n")}
|
|
1549
|
+
|
|
1550
|
+
## Proposed Decisions
|
|
1551
|
+
|
|
1552
|
+
${renderProposedDecisions(adrDir, signals)}
|
|
1553
|
+
|
|
1554
|
+
## Review Checklist
|
|
1555
|
+
|
|
1556
|
+
- [ ] Confirm the detected languages and package manager (and the source each was read from).
|
|
1557
|
+
- [ ] Confirm where tests were detected, or point Recall at the right location if it is wrong.
|
|
1558
|
+
- [ ] Accept or reject each proposed framework ADR under \`${adrDir}/proposed/\`.
|
|
1559
|
+
- [ ] Run \`recall init\` to establish neutral repository memory if it does not exist yet.
|
|
1560
|
+
- [ ] Record any decision you accept with \`recall adr create\` or by accepting the proposed ADR.
|
|
1561
|
+
|
|
1562
|
+
## Notes
|
|
1563
|
+
|
|
1564
|
+
This report was produced by \`recall adopt\` through read-only inspection of manifest and marker
|
|
1565
|
+
files. No repository code was executed and no decision was accepted automatically.
|
|
1566
|
+
`;
|
|
1567
|
+
}
|
|
1568
|
+
function renderProposedDecisions(adrDir, signals) {
|
|
1569
|
+
if (signals.frameworks.length === 0) {
|
|
1570
|
+
return "- No framework decisions were inferred. Add decisions with `recall adr create` as needed.";
|
|
1571
|
+
}
|
|
1572
|
+
return signals.frameworks.map(
|
|
1573
|
+
(framework) => `- Proposed: record **${framework}** as an architecture decision (see \`${adrDir}/proposed/ADR-PROPOSED-adopt-${frameworkSlug(framework)}.md\`). Requires review.`
|
|
1574
|
+
).join("\n");
|
|
1575
|
+
}
|
|
1576
|
+
function renderProposedAdr(framework) {
|
|
1577
|
+
return `# Proposed ADR: Use ${framework}
|
|
1578
|
+
|
|
1579
|
+
## Status
|
|
1580
|
+
|
|
1581
|
+
Proposed
|
|
1582
|
+
|
|
1583
|
+
## Context
|
|
1584
|
+
|
|
1585
|
+
\`recall adopt\` detected ${framework} in this repository through read-only inspection.
|
|
1586
|
+
|
|
1587
|
+
## Decision
|
|
1588
|
+
|
|
1589
|
+
Consider recording ${framework} as an accepted architecture decision. This is proposed by adoption
|
|
1590
|
+
and is not accepted until a human reviews and accepts it.
|
|
1591
|
+
|
|
1592
|
+
## Alternatives Considered
|
|
1593
|
+
|
|
1594
|
+
- Record a different framework.
|
|
1595
|
+
- Leave the decision unrecorded for now.
|
|
1596
|
+
|
|
1597
|
+
## Consequences
|
|
1598
|
+
|
|
1599
|
+
- Captures a framework already in use as reviewable repository memory.
|
|
1600
|
+
- Requires explicit human acceptance before it becomes repository truth.
|
|
1601
|
+
|
|
1602
|
+
## Related Documents
|
|
1603
|
+
|
|
1604
|
+
- \`docs/10-architecture/ARCHITECTURE.md\` \u2014 record the accepted architecture here once promoted.
|
|
1605
|
+
- The adoption report generated alongside this proposal.
|
|
1606
|
+
`;
|
|
1607
|
+
}
|
|
1608
|
+
function frameworkSlug(framework) {
|
|
1609
|
+
return framework.toLowerCase().replace(/\./gu, "").replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1460
1612
|
// src/commands/adopt.ts
|
|
1461
1613
|
var AdoptError = class extends Error {
|
|
1462
1614
|
code;
|
|
@@ -1530,7 +1682,7 @@ function formatList2(values) {
|
|
|
1530
1682
|
|
|
1531
1683
|
// src/core/doctor/checks/code-reference-check.ts
|
|
1532
1684
|
import { existsSync as existsSync4 } from "fs";
|
|
1533
|
-
import { readFile as readFile4, readdir as
|
|
1685
|
+
import { readFile as readFile4, readdir as readdir5 } from "fs/promises";
|
|
1534
1686
|
import path7 from "path";
|
|
1535
1687
|
var featureFolderPattern = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1536
1688
|
var FEATURE_DOCS = ["PRD.md", "ARCHITECTURE_IMPACT.md"];
|
|
@@ -1590,7 +1742,7 @@ async function checkDoc(rootDir, relativePath) {
|
|
|
1590
1742
|
}
|
|
1591
1743
|
async function readDirIfExists(rootDir, relativePath) {
|
|
1592
1744
|
try {
|
|
1593
|
-
return await
|
|
1745
|
+
return await readdir5(path7.join(rootDir, relativePath), { withFileTypes: true });
|
|
1594
1746
|
} catch (error) {
|
|
1595
1747
|
const nodeError = error;
|
|
1596
1748
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1680,9 +1832,12 @@ async function checkConfig(rootDir) {
|
|
|
1680
1832
|
}
|
|
1681
1833
|
|
|
1682
1834
|
// src/core/doctor/checks/content-check.ts
|
|
1683
|
-
import { readFile as readFile6, readdir as
|
|
1835
|
+
import { readFile as readFile6, readdir as readdir6 } from "fs/promises";
|
|
1684
1836
|
import path8 from "path";
|
|
1685
1837
|
var featureFolderPattern2 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
1838
|
+
var acceptedAdrPattern = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
1839
|
+
var SECURITY_MODEL_PATH = "docs/20-security/SECURITY_MODEL.md";
|
|
1840
|
+
var THREAT_MODEL_PATH = "docs/20-security/THREAT_MODEL.md";
|
|
1686
1841
|
async function checkContent(context) {
|
|
1687
1842
|
if (context.config === void 0) {
|
|
1688
1843
|
return [];
|
|
@@ -1717,6 +1872,14 @@ async function checkContent(context) {
|
|
|
1717
1872
|
}
|
|
1718
1873
|
const moduleEntries = await readDirIfExists2(context.rootDir, context.config.modulesDir);
|
|
1719
1874
|
const moduleFolders = moduleEntries.filter((entry) => entry.isDirectory());
|
|
1875
|
+
const adrEntries = await readDirIfExists2(context.rootDir, context.config.adrDir);
|
|
1876
|
+
const acceptedAdrs = adrEntries.filter(
|
|
1877
|
+
(entry) => entry.isFile() && acceptedAdrPattern.test(entry.name)
|
|
1878
|
+
);
|
|
1879
|
+
const hasWork = featureFolders.length > 0 || moduleFolders.length > 0 || acceptedAdrs.length > 0;
|
|
1880
|
+
if (hasWork) {
|
|
1881
|
+
findings.push(...await checkSecurityDoc(context.rootDir));
|
|
1882
|
+
}
|
|
1720
1883
|
for (const folder of moduleFolders) {
|
|
1721
1884
|
const modulePath = path8.posix.join(context.config.modulesDir, folder.name, "MODULE.md");
|
|
1722
1885
|
const moduleDoc = await readFileIfExists2(context.rootDir, modulePath);
|
|
@@ -1742,6 +1905,28 @@ async function checkContent(context) {
|
|
|
1742
1905
|
}
|
|
1743
1906
|
return findings;
|
|
1744
1907
|
}
|
|
1908
|
+
async function checkSecurityDoc(rootDir) {
|
|
1909
|
+
const findings = [];
|
|
1910
|
+
const security = await readFileIfExists2(rootDir, SECURITY_MODEL_PATH);
|
|
1911
|
+
if (security !== void 0 && sectionIsUnfilled(security, "Authentication And Authorization")) {
|
|
1912
|
+
findings.push({
|
|
1913
|
+
severity: "warning",
|
|
1914
|
+
check: "content-security",
|
|
1915
|
+
message: "Security model authentication and authorization section is still an unfilled template.",
|
|
1916
|
+
path: SECURITY_MODEL_PATH
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
const threat = await readFileIfExists2(rootDir, THREAT_MODEL_PATH);
|
|
1920
|
+
if (threat !== void 0 && sectionIsUnfilled(threat, "Assets")) {
|
|
1921
|
+
findings.push({
|
|
1922
|
+
severity: "warning",
|
|
1923
|
+
check: "content-threat-model",
|
|
1924
|
+
message: "Threat model assets section is still an unfilled template.",
|
|
1925
|
+
path: THREAT_MODEL_PATH
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
return findings;
|
|
1929
|
+
}
|
|
1745
1930
|
function sectionIsUnfilled(content, heading) {
|
|
1746
1931
|
const section = getSection(content, heading);
|
|
1747
1932
|
return section !== void 0 && isUnfilled(section);
|
|
@@ -1754,7 +1939,7 @@ function isUnfilled(value) {
|
|
|
1754
1939
|
if (normalized === "tbd" || normalized === "todo" || normalized === "pending" || normalized === "none" || normalized === "n/a") {
|
|
1755
1940
|
return true;
|
|
1756
1941
|
}
|
|
1757
|
-
return normalized.includes("describe why this feature exists") || normalized.includes("describe what this module owns");
|
|
1942
|
+
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
1943
|
}
|
|
1759
1944
|
function getSection(content, heading) {
|
|
1760
1945
|
const lines = content.split(/\r?\n/u);
|
|
@@ -1774,7 +1959,7 @@ function getSection(content, heading) {
|
|
|
1774
1959
|
}
|
|
1775
1960
|
async function readDirIfExists2(rootDir, relativePath) {
|
|
1776
1961
|
try {
|
|
1777
|
-
return await
|
|
1962
|
+
return await readdir6(path8.join(rootDir, relativePath), { withFileTypes: true });
|
|
1778
1963
|
} catch (error) {
|
|
1779
1964
|
const nodeError = error;
|
|
1780
1965
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1796,7 +1981,7 @@ async function readFileIfExists2(rootDir, relativePath) {
|
|
|
1796
1981
|
}
|
|
1797
1982
|
|
|
1798
1983
|
// src/core/doctor/checks/drift-check.ts
|
|
1799
|
-
import { readFile as readFile7, readdir as
|
|
1984
|
+
import { readFile as readFile7, readdir as readdir7 } from "fs/promises";
|
|
1800
1985
|
import path9 from "path";
|
|
1801
1986
|
var adrFilePattern = /^ADR-(\d{4,})-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/iu;
|
|
1802
1987
|
var adrReferencePattern = /ADR-\d{4,}/giu;
|
|
@@ -1900,7 +2085,7 @@ function getSection2(content, heading) {
|
|
|
1900
2085
|
}
|
|
1901
2086
|
async function readDirIfExists3(rootDir, relativePath) {
|
|
1902
2087
|
try {
|
|
1903
|
-
return await
|
|
2088
|
+
return await readdir7(path9.join(rootDir, relativePath), { withFileTypes: true });
|
|
1904
2089
|
} catch (error) {
|
|
1905
2090
|
const nodeError = error;
|
|
1906
2091
|
if (nodeError.code === "ENOENT") {
|
|
@@ -1911,7 +2096,7 @@ async function readDirIfExists3(rootDir, relativePath) {
|
|
|
1911
2096
|
}
|
|
1912
2097
|
|
|
1913
2098
|
// src/core/doctor/checks/memory-integrity-check.ts
|
|
1914
|
-
import { lstat as lstat2, readFile as readFile8, readdir as
|
|
2099
|
+
import { lstat as lstat2, readFile as readFile8, readdir as readdir8 } from "fs/promises";
|
|
1915
2100
|
import path10 from "path";
|
|
1916
2101
|
|
|
1917
2102
|
// src/core/adr/adr-sections.ts
|
|
@@ -2045,7 +2230,7 @@ async function checkAdrFiles(rootDir, adrDir) {
|
|
|
2045
2230
|
}
|
|
2046
2231
|
async function readDirIfExists4(rootDir, relativePath) {
|
|
2047
2232
|
try {
|
|
2048
|
-
return await
|
|
2233
|
+
return await readdir8(path10.join(rootDir, relativePath), { withFileTypes: true });
|
|
2049
2234
|
} catch (error) {
|
|
2050
2235
|
const nodeError = error;
|
|
2051
2236
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2156,7 +2341,7 @@ function missingFile(pathValue, check) {
|
|
|
2156
2341
|
}
|
|
2157
2342
|
|
|
2158
2343
|
// src/core/doctor/checks/standards-check.ts
|
|
2159
|
-
import { lstat as lstat4, readFile as readFile9, readdir as
|
|
2344
|
+
import { lstat as lstat4, readFile as readFile9, readdir as readdir9 } from "fs/promises";
|
|
2160
2345
|
import path12 from "path";
|
|
2161
2346
|
var featureFolderPattern4 = /^F-\d{3,}-[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
2162
2347
|
var adrFilePattern3 = /^ADR-\d{4,}-[a-z0-9]+(?:-[a-z0-9]+)*\.md$/u;
|
|
@@ -2315,7 +2500,7 @@ function isPlaceholder(value) {
|
|
|
2315
2500
|
}
|
|
2316
2501
|
async function readDirIfExists5(rootDir, relativePath) {
|
|
2317
2502
|
try {
|
|
2318
|
-
return await
|
|
2503
|
+
return await readdir9(path12.join(rootDir, relativePath), { withFileTypes: true });
|
|
2319
2504
|
} catch (error) {
|
|
2320
2505
|
const nodeError = error;
|
|
2321
2506
|
if (nodeError.code === "ENOENT") {
|
|
@@ -2589,26 +2774,78 @@ Default behavior:
|
|
|
2589
2774
|
path: "docs/20-security/SECURITY_MODEL.md",
|
|
2590
2775
|
content: `# Security Model
|
|
2591
2776
|
|
|
2592
|
-
##
|
|
2777
|
+
## Status
|
|
2593
2778
|
|
|
2594
|
-
Draft.
|
|
2779
|
+
Draft \u2014 fill the prompted sections below with this repository's real model as it grows. \`recall doctor\`
|
|
2780
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2595
2781
|
|
|
2596
2782
|
## Baseline Rules
|
|
2597
2783
|
|
|
2598
|
-
-
|
|
2599
|
-
-
|
|
2784
|
+
- Never commit secrets or credentials, and never read or copy \`.env\` files into docs.
|
|
2785
|
+
- Validate and authorize untrusted input at every trust boundary.
|
|
2600
2786
|
- Do not add network, telemetry, cloud, MCP runtime, or AI API behavior without explicit review.
|
|
2787
|
+
|
|
2788
|
+
## Authentication And Authorization
|
|
2789
|
+
|
|
2790
|
+
Describe how this repository authenticates users or clients and how it authorizes actions, including
|
|
2791
|
+
where those checks live.
|
|
2792
|
+
|
|
2793
|
+
## Secrets And Configuration
|
|
2794
|
+
|
|
2795
|
+
Describe where secrets live, how they are injected, and how configuration is kept out of version
|
|
2796
|
+
control.
|
|
2797
|
+
|
|
2798
|
+
## Sensitive Data
|
|
2799
|
+
|
|
2800
|
+
Describe the sensitive or personal data this repository handles, and how it is protected at rest and
|
|
2801
|
+
in transit.
|
|
2802
|
+
|
|
2803
|
+
## Dependencies And Supply Chain
|
|
2804
|
+
|
|
2805
|
+
Describe how third-party dependencies are vetted, pinned, and updated.
|
|
2601
2806
|
`
|
|
2602
2807
|
},
|
|
2603
2808
|
{
|
|
2604
2809
|
path: "docs/20-security/THREAT_MODEL.md",
|
|
2605
2810
|
content: `# Threat Model
|
|
2606
2811
|
|
|
2607
|
-
##
|
|
2812
|
+
## Status
|
|
2608
2813
|
|
|
2609
|
-
Draft.
|
|
2814
|
+
Draft \u2014 replace the prompts below with this repository's real analysis as it grows. \`recall doctor\`
|
|
2815
|
+
flags these as warnings once the repository has real work (a feature, module, or accepted decision).
|
|
2816
|
+
|
|
2817
|
+
## Assets
|
|
2818
|
+
|
|
2819
|
+
Describe what this repository must protect: user data, credentials, money, availability, or
|
|
2820
|
+
reputation.
|
|
2821
|
+
|
|
2822
|
+
## Entry Points
|
|
2823
|
+
|
|
2824
|
+
Describe where untrusted input enters: HTTP endpoints, webhooks, file uploads, queues, CLI input, or
|
|
2825
|
+
third-party callbacks.
|
|
2610
2826
|
|
|
2611
|
-
|
|
2827
|
+
## Trust Boundaries
|
|
2828
|
+
|
|
2829
|
+
Describe where trust changes: client to server, service to database, your code to third-party APIs.
|
|
2830
|
+
|
|
2831
|
+
## Threats
|
|
2832
|
+
|
|
2833
|
+
Describe the concrete threats that apply to this repository, by category:
|
|
2834
|
+
|
|
2835
|
+
- Spoofing \u2014 how identities are faked or sessions stolen.
|
|
2836
|
+
- Tampering \u2014 how requests, data, or builds are altered (injection, mass assignment).
|
|
2837
|
+
- Repudiation \u2014 actions that must remain auditable.
|
|
2838
|
+
- Information disclosure \u2014 how sensitive data or secrets could leak.
|
|
2839
|
+
- Denial of service \u2014 how the system can be overwhelmed or abused.
|
|
2840
|
+
- Elevation of privilege \u2014 how a user could gain access they should not have.
|
|
2841
|
+
|
|
2842
|
+
## Mitigations
|
|
2843
|
+
|
|
2844
|
+
Describe the control in place or planned for each threat above.
|
|
2845
|
+
|
|
2846
|
+
## Open Risks
|
|
2847
|
+
|
|
2848
|
+
Describe accepted or unresolved risks and who owns them.
|
|
2612
2849
|
`
|
|
2613
2850
|
},
|
|
2614
2851
|
{
|
|
@@ -2938,12 +3175,12 @@ async function detectPreCommitGates(rootDir) {
|
|
|
2938
3175
|
if (typeof scripts !== "object" || scripts === null) {
|
|
2939
3176
|
return [];
|
|
2940
3177
|
}
|
|
2941
|
-
const packageManager =
|
|
3178
|
+
const packageManager = detectPackageManager2(rootDir);
|
|
2942
3179
|
return KNOWN_SCRIPTS.filter((script) => typeof scripts[script] === "string").map(
|
|
2943
3180
|
(script) => `${packageManager} run ${script}`
|
|
2944
3181
|
);
|
|
2945
3182
|
}
|
|
2946
|
-
function
|
|
3183
|
+
function detectPackageManager2(rootDir) {
|
|
2947
3184
|
if (existsSync5(path14.join(rootDir, "pnpm-lock.yaml"))) {
|
|
2948
3185
|
return "pnpm";
|
|
2949
3186
|
}
|
|
@@ -5154,6 +5391,7 @@ async function initProject(options) {
|
|
|
5154
5391
|
);
|
|
5155
5392
|
}
|
|
5156
5393
|
const preset = resolvePreset(options.preset);
|
|
5394
|
+
const detected = await inspectRepo(options.rootDir);
|
|
5157
5395
|
const preCommitGates = await detectPreCommitGates(options.rootDir);
|
|
5158
5396
|
const config = createDefaultConfig({ preset: preset?.id ?? null, preCommitGates });
|
|
5159
5397
|
const files = createInitWriteFiles(options.rootDir, config, preset);
|
|
@@ -5174,7 +5412,8 @@ async function initProject(options) {
|
|
|
5174
5412
|
preset: preset?.id ?? null,
|
|
5175
5413
|
dryRun: options.dryRun ?? false,
|
|
5176
5414
|
plan,
|
|
5177
|
-
writeResult
|
|
5415
|
+
writeResult,
|
|
5416
|
+
detected
|
|
5178
5417
|
};
|
|
5179
5418
|
}
|
|
5180
5419
|
function formatInitResult(result) {
|
|
@@ -5191,6 +5430,7 @@ function formatInitResult(result) {
|
|
|
5191
5430
|
dryRun: result.dryRun,
|
|
5192
5431
|
writeResult: result.writeResult
|
|
5193
5432
|
});
|
|
5433
|
+
appendDetectedStack(lines, result.detected);
|
|
5194
5434
|
const hookWritten = result.writeResult.created.includes(PRE_COMMIT_HOOK_PATH) || result.writeResult.overwritten.includes(PRE_COMMIT_HOOK_PATH);
|
|
5195
5435
|
if (hookWritten) {
|
|
5196
5436
|
lines.push("");
|
|
@@ -5213,6 +5453,21 @@ function formatInitResult(result) {
|
|
|
5213
5453
|
return `${lines.join("\n")}
|
|
5214
5454
|
`;
|
|
5215
5455
|
}
|
|
5456
|
+
function appendDetectedStack(lines, detected) {
|
|
5457
|
+
const hasSignal = detected.languages.length > 0 || detected.frameworks.length > 0 || detected.packageManager !== null || detected.testsEvidence !== null;
|
|
5458
|
+
if (!hasSignal) {
|
|
5459
|
+
return;
|
|
5460
|
+
}
|
|
5461
|
+
const stack = summarizeSignals(detected).filter(
|
|
5462
|
+
(line) => !line.startsWith("- README") && !line.startsWith("- Docs")
|
|
5463
|
+
);
|
|
5464
|
+
lines.push("");
|
|
5465
|
+
lines.push("Detected in this repository (proposed \u2014 review, nothing was accepted):");
|
|
5466
|
+
lines.push(...stack);
|
|
5467
|
+
lines.push(
|
|
5468
|
+
"If any signal is wrong, correct the source file noted. Run `recall adopt` to record this as proposed memory."
|
|
5469
|
+
);
|
|
5470
|
+
}
|
|
5216
5471
|
function resolvePreset(presetId) {
|
|
5217
5472
|
if (presetId === void 0) {
|
|
5218
5473
|
return null;
|