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/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
- let packageManager = null;
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 hasTests = "test" in scripts || has("test") || has("tests") || has("__tests__") || has("pytest.ini") || python.includes("pytest");
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 readdir4 } from "fs/promises";
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 readdir4(path7.join(rootDir, relativePath), { withFileTypes: true });
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 readdir5 } from "fs/promises";
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 readdir5(path8.join(rootDir, relativePath), { withFileTypes: true });
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 readdir6 } from "fs/promises";
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 readdir6(path9.join(rootDir, relativePath), { withFileTypes: true });
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 readdir7 } from "fs/promises";
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 readdir7(path10.join(rootDir, relativePath), { withFileTypes: true });
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 readdir8 } from "fs/promises";
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 readdir8(path12.join(rootDir, relativePath), { withFileTypes: true });
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
- ## Current Status
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
- - Do not commit secrets.
2599
- - Do not read or copy \`.env\` files into docs.
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
- ## Current Status
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
- Track repository-specific risks here as the project evolves.
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 = detectPackageManager(rootDir);
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 detectPackageManager(rootDir) {
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;