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.
Files changed (24) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +388 -133
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +388 -133
  5. package/dist/index.js.map +1 -1
  6. package/examples/generated-flutter/docs/20-security/SECURITY_MODEL.md +25 -4
  7. package/examples/generated-flutter/docs/20-security/THREAT_MODEL.md +35 -3
  8. package/examples/generated-generic/docs/20-security/SECURITY_MODEL.md +25 -4
  9. package/examples/generated-generic/docs/20-security/THREAT_MODEL.md +35 -3
  10. package/examples/generated-ios-swift/docs/20-security/SECURITY_MODEL.md +25 -4
  11. package/examples/generated-ios-swift/docs/20-security/THREAT_MODEL.md +35 -3
  12. package/examples/generated-kotlin-android/docs/20-security/SECURITY_MODEL.md +25 -4
  13. package/examples/generated-kotlin-android/docs/20-security/THREAT_MODEL.md +35 -3
  14. package/examples/generated-laravel-api/docs/20-security/SECURITY_MODEL.md +25 -4
  15. package/examples/generated-laravel-api/docs/20-security/THREAT_MODEL.md +35 -3
  16. package/examples/generated-laravel-react/docs/20-security/SECURITY_MODEL.md +25 -4
  17. package/examples/generated-laravel-react/docs/20-security/THREAT_MODEL.md +35 -3
  18. package/examples/generated-laravel-vue/docs/20-security/SECURITY_MODEL.md +25 -4
  19. package/examples/generated-laravel-vue/docs/20-security/THREAT_MODEL.md +35 -3
  20. package/examples/generated-nextjs/docs/20-security/SECURITY_MODEL.md +25 -4
  21. package/examples/generated-nextjs/docs/20-security/THREAT_MODEL.md +35 -3
  22. package/examples/generated-python-fastapi/docs/20-security/SECURITY_MODEL.md +25 -4
  23. package/examples/generated-python-fastapi/docs/20-security/THREAT_MODEL.md +35 -3
  24. 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
- let packageManager = null;
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 hasTests = "test" in scripts || has("test") || has("tests") || has("__tests__") || has("pytest.ini") || python.includes("pytest");
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 readdir4 } from "fs/promises";
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 readdir4(path7.join(rootDir, relativePath), { withFileTypes: true });
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 readdir5 } from "fs/promises";
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 readdir5(path8.join(rootDir, relativePath), { withFileTypes: true });
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 readdir6 } from "fs/promises";
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 readdir6(path9.join(rootDir, relativePath), { withFileTypes: true });
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 readdir7 } from "fs/promises";
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 readdir7(path10.join(rootDir, relativePath), { withFileTypes: true });
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 readdir8 } from "fs/promises";
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 readdir8(path12.join(rootDir, relativePath), { withFileTypes: true });
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
- ## Current Status
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
- - Do not commit secrets.
2601
- - Do not read or copy \`.env\` files into docs.
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
- ## Current Status
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
- Track repository-specific risks here as the project evolves.
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 = detectPackageManager(rootDir);
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 detectPackageManager(rootDir) {
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;