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