project-athena 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/README.md +15 -8
- package/dist/activity-P4UUFHWV.js +77 -0
- package/dist/agents-3HTSQGL2.js +69 -0
- package/dist/ai-ASL4FFQ7.js +96 -0
- package/dist/analyze-F3YOILNL.js +49 -0
- package/dist/chunk-2DY4PUR4.js +465 -0
- package/dist/chunk-2R6PXY6Z.js +33 -0
- package/dist/chunk-2RS6TA3D.js +908 -0
- package/dist/chunk-32PRS2MG.js +169 -0
- package/dist/chunk-3IS2X2Z7.js +89 -0
- package/dist/chunk-65TSRDQ3.js +124 -0
- package/dist/chunk-673EPYYH.js +105 -0
- package/dist/chunk-AD4OZCU2.js +34 -0
- package/dist/chunk-CXUTYCZX.js +238 -0
- package/dist/chunk-DOQMAZL5.js +436 -0
- package/dist/chunk-EF6GHOIN.js +343 -0
- package/dist/chunk-ELKFRTZT.js +221 -0
- package/dist/chunk-EUPETUWA.js +14 -0
- package/dist/chunk-FQEGEVLY.js +38 -0
- package/dist/chunk-GHFRQAYT.js +112 -0
- package/dist/chunk-GR4DL2VM.js +47 -0
- package/dist/chunk-INT3IGMQ.js +111 -0
- package/dist/chunk-IQQBAWSM.js +90 -0
- package/dist/chunk-JCGDR4UV.js +182 -0
- package/dist/chunk-JY6BFIJJ.js +95 -0
- package/dist/chunk-KBO7XRBW.js +26 -0
- package/dist/chunk-LEMPCNAY.js +209 -0
- package/dist/chunk-LMOFZNTQ.js +379 -0
- package/dist/chunk-OLPSEQS5.js +264 -0
- package/dist/chunk-PANVPNYF.js +2892 -0
- package/dist/chunk-PDFQ5BFG.js +378 -0
- package/dist/chunk-PR53DT7I.js +179 -0
- package/dist/chunk-PVCIGUIT.js +64 -0
- package/dist/chunk-RXNRMIZP.js +390 -0
- package/dist/chunk-S7T6VX56.js +61 -0
- package/dist/chunk-SHKXELUX.js +295 -0
- package/dist/chunk-SJOOXO34.js +904 -0
- package/dist/chunk-TGJUUB44.js +399 -0
- package/dist/chunk-VSYBC3VY.js +28 -0
- package/dist/chunk-XB66PZ6Z.js +182 -0
- package/dist/chunk-XFN7UZCS.js +35 -0
- package/dist/chunk-XW3MLHI2.js +32 -0
- package/dist/chunk-Z6QWMJLK.js +124 -0
- package/dist/chunk-ZB7LC6FP.js +221 -0
- package/dist/clean-S7PKCOLM.js +23 -0
- package/dist/cli.js +18 -10716
- package/dist/context-AH5MFHRG.js +147 -0
- package/dist/doctor-RSJMLE2P.js +62 -0
- package/dist/event-TNT6CQZR.js +101 -0
- package/dist/git-hook-YZNZGAAP.js +252 -0
- package/dist/init-2V34DVRQ.js +37 -0
- package/dist/mcp-T2GRTEX5.js +255 -0
- package/dist/open-2QSZKKUG.js +1259 -0
- package/dist/program-V2USHGXN.js +148 -0
- package/dist/review-R3R2J5BJ.js +115 -0
- package/dist/rules-4FXJUCXB.js +83 -0
- package/dist/security-PI4LXCOB.js +175 -0
- package/dist/staged-check-HHIBKKMR.js +500 -0
- package/dist/status-6M73WX5I.js +75 -0
- package/dist/sync-3ZNJDSSU.js +216 -0
- package/dist/watch-USN5CQIV.js +121 -0
- package/dist/web/assets/{Editor-DAXvniWB.js → Editor-BI56-HX5.js} +1 -1
- package/dist/web/assets/{abnfDiagram-O67JEVCF-Az7EW1BC.js → abnfDiagram-O67JEVCF-y_9px2ob.js} +1 -1
- package/dist/web/assets/architecture-7GRP2DOG-DyxkwYiW.js +1 -0
- package/dist/web/assets/{architectureDiagram-NJMV4G6O-BxeXqSTn.js → architectureDiagram-NJMV4G6O-lB6fcVzU.js} +1 -1
- package/dist/web/assets/{blockDiagram-BEXU5L5S-aeKZl9lJ.js → blockDiagram-BEXU5L5S-DcM5pEjM.js} +1 -1
- package/dist/web/assets/{c4Diagram-YGBWAQC7-CgmntYsG.js → c4Diagram-YGBWAQC7-CJgqxTY2.js} +1 -1
- package/dist/web/assets/channel-BvDD7AMO.js +1 -0
- package/dist/web/assets/{chunk-3FUC2YCW-BpmRD2yo.js → chunk-3FUC2YCW-B8l_VuWR.js} +1 -1
- package/dist/web/assets/{chunk-5DYCD2WN-CRIFDwod.js → chunk-5DYCD2WN-CZecgNhf.js} +1 -1
- package/dist/web/assets/{chunk-742MDFTN-hRW9VeNV.js → chunk-742MDFTN-BeZ7P5jB.js} +1 -1
- package/dist/web/assets/{chunk-7INBJB4K-C6VEpT0Y.js → chunk-7INBJB4K-IKy0pr5m.js} +1 -1
- package/dist/web/assets/{chunk-7M6MHVWA-Dnajov2w.js → chunk-7M6MHVWA-Dg_QqJEX.js} +1 -1
- package/dist/web/assets/{chunk-7PRAP22T-DSAc0_CZ.js → chunk-7PRAP22T-DqQNS8oq.js} +1 -1
- package/dist/web/assets/{chunk-GTNCS2PH-CefpYUjb.js → chunk-GTNCS2PH-x4sM51nJ.js} +1 -1
- package/dist/web/assets/{chunk-GWA4HPMP-MLNflMmF.js → chunk-GWA4HPMP-CE5GcwVN.js} +1 -1
- package/dist/web/assets/{chunk-MBY4JIJT-D3GmmEiU.js → chunk-MBY4JIJT-DA_D1hjF.js} +1 -1
- package/dist/web/assets/{chunk-NETBCI7D-DzTj6xVk.js → chunk-NETBCI7D-CeJ3XBZ2.js} +1 -1
- package/dist/web/assets/{chunk-O7XYJQB3-D1d0hCko.js → chunk-O7XYJQB3-BH2gESvO.js} +1 -1
- package/dist/web/assets/{chunk-UA2S7LBM-C9hIFeWR.js → chunk-UA2S7LBM-BWr0xCux.js} +1 -1
- package/dist/web/assets/{chunk-WEXAMYUT-BFGtcjsi.js → chunk-WEXAMYUT-B4QwUZPM.js} +1 -1
- package/dist/web/assets/{chunk-Z7XXMR3K-BRZ8_wtE.js → chunk-Z7XXMR3K-BxcA-y5B.js} +1 -1
- package/dist/web/assets/{chunk-ZIGJFQKS-CTKOioal.js → chunk-ZIGJFQKS-BZYvP63G.js} +1 -1
- package/dist/web/assets/{classDiagram-v2-NBCMYWYE-DJGKtWJv.js → classDiagram-v2-NBCMYWYE-BS3P_NXb.js} +1 -1
- package/dist/web/assets/{cynefin-OW5HDTMX-hNq1kGmA.js → cynefin-OW5HDTMX-BALD4Pve.js} +1 -1
- package/dist/web/assets/{cynefinDiagram-VND7K2PF-sODQM_ZZ.js → cynefinDiagram-VND7K2PF-BM2y4W6d.js} +1 -1
- package/dist/web/assets/{dagre-6A5THRUB-DEzTThIf.js → dagre-6A5THRUB-PTfOrIdF.js} +1 -1
- package/dist/web/assets/{diagram-22UHCM2B-s_eellsa.js → diagram-22UHCM2B-CpXEHhUl.js} +1 -1
- package/dist/web/assets/{diagram-3UASUU5V-B2onY_Xw.js → diagram-3UASUU5V-DPHfXzr6.js} +1 -1
- package/dist/web/assets/{diagram-ATOU4E4O-B8V6s13s.js → diagram-ATOU4E4O-DMBNXvAC.js} +1 -1
- package/dist/web/assets/{diagram-CDSNMT55-CJxiE6mf.js → diagram-CDSNMT55-CDLe1M7G.js} +1 -1
- package/dist/web/assets/{diagram-MLGK6HIB-m5OHPLAp.js → diagram-MLGK6HIB-D6L8oE7j.js} +1 -1
- package/dist/web/assets/{diagram-MPIPVDR6-Buvvrk_A.js → diagram-MPIPVDR6-CP-S7rrQ.js} +1 -1
- package/dist/web/assets/{ebnfDiagram-ZINNZB2B-CpL7Wt3R.js → ebnfDiagram-ZINNZB2B-CkUc63TK.js} +1 -1
- package/dist/web/assets/{elk-276RUBZZ-CwYhbsXa.js → elk-276RUBZZ-C1CAJLH_.js} +1 -1
- package/dist/web/assets/{erDiagram-OPXOYQCR-DNikci2r.js → erDiagram-OPXOYQCR-DXBnaTHm.js} +1 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-cLIzvgDp.js +1 -0
- package/dist/web/assets/flowDiagram-KWPJA3E3-D7Zq63VQ.js +1 -0
- package/dist/web/assets/{ganttDiagram-FUAMR5RP-d-ynDlvJ.js → ganttDiagram-FUAMR5RP-BVm8SiGg.js} +1 -1
- package/dist/web/assets/{gitGraph-4MIJSDKK-DwIeBI91.js → gitGraph-4MIJSDKK-Dm7xBrW0.js} +1 -1
- package/dist/web/assets/{gitGraphDiagram-X574FWY7-04RZccu6.js → gitGraphDiagram-X574FWY7-D_PIKKhx.js} +1 -1
- package/dist/web/assets/{index-C3-8uYXr.js → index-CeOIBpu2.js} +3 -3
- package/dist/web/assets/{info-A6RAGUB7-xf4Nzgv-.js → info-A6RAGUB7-CLGrx8pn.js} +1 -1
- package/dist/web/assets/{infoDiagram-VRGFBTTK-BYjw0eiK.js → infoDiagram-VRGFBTTK-BlBjaKm7.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-Bx9s4nPM.js → ishikawaDiagram-OU5B5YK6-Kxe7GGS3.js} +1 -1
- package/dist/web/assets/{journeyDiagram-ZHPQQLJL-DrdHs7B-.js → journeyDiagram-ZHPQQLJL-wCMoHK4M.js} +1 -1
- package/dist/web/assets/{kanban-definition-PNTS6WVX-CVd8v2Te.js → kanban-definition-PNTS6WVX-BxdCz9Ko.js} +1 -1
- package/dist/web/assets/{line-V_WrJ-6y.js → line-Zf11FnWF.js} +1 -1
- package/dist/web/assets/{mermaid-parser.core-B-ilyEpO.js → mermaid-parser.core-BvFFklev.js} +3 -3
- package/dist/web/assets/{mermaid.core-Bnyaa0nF.js → mermaid.core-Cd_Ssti9.js} +4 -4
- package/dist/web/assets/{mindmap-definition-NLK3R4M7-B3AbvHPR.js → mindmap-definition-NLK3R4M7-BGPQl-GY.js} +1 -1
- package/dist/web/assets/{packet-AYTQ26CC-CBAIUXIc.js → packet-AYTQ26CC-6lI3cEX_.js} +1 -1
- package/dist/web/assets/{pegDiagram-GJSIUBJH-DIdeELUM.js → pegDiagram-GJSIUBJH-BPIjlSAj.js} +1 -1
- package/dist/web/assets/{pie-WAS4IAKB-a1r3tzPR.js → pie-WAS4IAKB-D6Dzj3Ja.js} +1 -1
- package/dist/web/assets/{pieDiagram-5QR66LMP-pQBEio0x.js → pieDiagram-5QR66LMP-DtA-vZmv.js} +1 -1
- package/dist/web/assets/{quadrantDiagram-O4NWA36T-CkJK5Y03.js → quadrantDiagram-O4NWA36T-EettyqQn.js} +1 -1
- package/dist/web/assets/{radar-RG4KPBEZ-BZfLYjx5.js → radar-RG4KPBEZ-xmRc3Dl8.js} +1 -1
- package/dist/web/assets/{railroad-74A4TZTK-CuIW1ZAU.js → railroad-74A4TZTK-BLB7dhOn.js} +1 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-DBFym6g8.js +1 -0
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-CkG28ig8.js +1 -0
- package/dist/web/assets/railroad-peg-WCYAUIDC-Df_Ifaad.js +1 -0
- package/dist/web/assets/{railroadDiagram-XR7U4H2S-_BpGE8aJ.js → railroadDiagram-XR7U4H2S-PBw4We6b.js} +1 -1
- package/dist/web/assets/{requirementDiagram-PLB6GJNP-CnzF90GS.js → requirementDiagram-PLB6GJNP-DjQpaUFA.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-IPEJSGJF-DYeisDj_.js → sankeyDiagram-IPEJSGJF-CBW-_AoF.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-PO4LG4MO-ll9cBvca.js → sequenceDiagram-PO4LG4MO-DEoPYqY9.js} +1 -1
- package/dist/web/assets/{stateDiagram-v2-GCMORJYK-CdKcOIKQ.js → stateDiagram-v2-GCMORJYK-UZq5rPud.js} +1 -1
- package/dist/web/assets/{swimlanes-2SLR337P-y6piTWEW.js → swimlanes-2SLR337P-CACfuXJR.js} +1 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-CDSrqdgx.js +8 -0
- package/dist/web/assets/{timeline-definition-EJHVYXUP-DuC_4xK0.js → timeline-definition-EJHVYXUP-CYJl7mP-.js} +1 -1
- package/dist/web/assets/{treeView-Q6P3EWNA-gCQFmND0.js → treeView-Q6P3EWNA-DtZ5nkPQ.js} +1 -1
- package/dist/web/assets/{treemap-WGGIJYW6-BQ2010OY.js → treemap-WGGIJYW6-m49GDM-M.js} +1 -1
- package/dist/web/assets/{usecaseDiagram-POWQR4AR-CCTqzHz6.js → usecaseDiagram-POWQR4AR-C2Vg20UK.js} +1 -1
- package/dist/web/assets/{vennDiagram-UO4OBE2U-0_O7guNq.js → vennDiagram-UO4OBE2U-C07v5OGI.js} +1 -1
- package/dist/web/assets/{wardley-WFR3VGLG-SsX7uMDs.js → wardley-WFR3VGLG-C9Z7EZtz.js} +1 -1
- package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DlqH6Hox.js → wardleyDiagram-VNRHLVJA-DdA8CcSC.js} +1 -1
- package/dist/web/assets/{xychartDiagram-PMCCYNJV-nr4UYDtd.js → xychartDiagram-PMCCYNJV-C9EZw07b.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +4 -2
- package/dist/web/assets/architecture-7GRP2DOG-blXBarB2.js +0 -1
- package/dist/web/assets/channel-f4QzewnN.js +0 -1
- package/dist/web/assets/eventmodeling-NTZA5JFV-CA_ZFPZ2.js +0 -1
- package/dist/web/assets/flowDiagram-KWPJA3E3-DObsBuWT.js +0 -1
- package/dist/web/assets/railroad-abnf-HS5TGJTU-6B4Ffbxn.js +0 -1
- package/dist/web/assets/railroad-ebnf-LZEXJU2U-BGL9lIQc.js +0 -1
- package/dist/web/assets/railroad-peg-WCYAUIDC-DcqxTem7.js +0 -1
- package/dist/web/assets/swimlanesDiagram-TC7HE7FX-DFS8a3jO.js +0 -8
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
analyzeProject,
|
|
4
|
+
loadScanFromDir,
|
|
5
|
+
persistAnalysisCache
|
|
6
|
+
} from "./chunk-PANVPNYF.js";
|
|
7
|
+
import {
|
|
8
|
+
ATHENA_VERSION
|
|
9
|
+
} from "./chunk-2R6PXY6Z.js";
|
|
10
|
+
import {
|
|
11
|
+
LOCAL_CONFIG_FILE
|
|
12
|
+
} from "./chunk-PVCIGUIT.js";
|
|
13
|
+
import {
|
|
14
|
+
redact
|
|
15
|
+
} from "./chunk-JCGDR4UV.js";
|
|
16
|
+
import {
|
|
17
|
+
ADAPTERS,
|
|
18
|
+
applyChanges
|
|
19
|
+
} from "./chunk-2RS6TA3D.js";
|
|
20
|
+
import {
|
|
21
|
+
mergeDocument,
|
|
22
|
+
renderBlock
|
|
23
|
+
} from "./chunk-673EPYYH.js";
|
|
24
|
+
import {
|
|
25
|
+
KNOWLEDGE_DOCS
|
|
26
|
+
} from "./chunk-FQEGEVLY.js";
|
|
27
|
+
import {
|
|
28
|
+
STATE_SCHEMA_VERSION,
|
|
29
|
+
athenaDir,
|
|
30
|
+
backupCorruptedState,
|
|
31
|
+
buildFileIndex,
|
|
32
|
+
readState,
|
|
33
|
+
writeState
|
|
34
|
+
} from "./chunk-2DY4PUR4.js";
|
|
35
|
+
import {
|
|
36
|
+
exists,
|
|
37
|
+
readTextIfExists,
|
|
38
|
+
sha256,
|
|
39
|
+
writeFileAtomic
|
|
40
|
+
} from "./chunk-IQQBAWSM.js";
|
|
41
|
+
import {
|
|
42
|
+
ATHENA_DIR
|
|
43
|
+
} from "./chunk-EUPETUWA.js";
|
|
44
|
+
|
|
45
|
+
// src/core/knowledge/generate.ts
|
|
46
|
+
import path from "path";
|
|
47
|
+
|
|
48
|
+
// src/core/knowledge/md.ts
|
|
49
|
+
function esc(s) {
|
|
50
|
+
return s.replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/[<>]/g, (c) => c === "<" ? "<" : ">").replace(/\r?\n/g, " ");
|
|
51
|
+
}
|
|
52
|
+
function code(s) {
|
|
53
|
+
const clean = s.replace(/\r?\n/g, " ");
|
|
54
|
+
const ticks = clean.includes("`") ? "``" : "`";
|
|
55
|
+
return `${ticks}${ticks.length === 2 ? " " : ""}${clean}${ticks.length === 2 ? " " : ""}${ticks}`;
|
|
56
|
+
}
|
|
57
|
+
function evidenceRef(e) {
|
|
58
|
+
return code(`${e.file}${e.line ? `:${e.line}` : ""}`);
|
|
59
|
+
}
|
|
60
|
+
function evidenceList(evidence, max = 3) {
|
|
61
|
+
if (!evidence.length) return "\u2014";
|
|
62
|
+
const shown = evidence.slice(0, max).map(evidenceRef).join(", ");
|
|
63
|
+
return evidence.length > max ? `${shown} +${evidence.length - max} more` : shown;
|
|
64
|
+
}
|
|
65
|
+
function status(p) {
|
|
66
|
+
return p.status === "UNKNOWN" ? "UNKNOWN" : `${p.status} \xB7 ${p.confidence}`;
|
|
67
|
+
}
|
|
68
|
+
function table(headers, rows) {
|
|
69
|
+
if (!rows.length) return "";
|
|
70
|
+
const head = `| ${headers.join(" | ")} |`;
|
|
71
|
+
const sep = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
72
|
+
return [head, sep, ...rows.map((r) => `| ${r.join(" | ")} |`)].join("\n");
|
|
73
|
+
}
|
|
74
|
+
function unknown(what, hint) {
|
|
75
|
+
return `**${what}:** UNKNOWN${hint ? ` \u2014 ${hint}` : ""}`;
|
|
76
|
+
}
|
|
77
|
+
function notDetected(what) {
|
|
78
|
+
return `_No ${what} detected._ Status: **UNKNOWN** (absence of detection is not proof of absence).`;
|
|
79
|
+
}
|
|
80
|
+
function bullets(items) {
|
|
81
|
+
return items.map((i) => `- ${i}`).join("\n");
|
|
82
|
+
}
|
|
83
|
+
function truncatedNote(shown, total, what) {
|
|
84
|
+
return total > shown ? `
|
|
85
|
+
|
|
86
|
+
_Showing ${shown} of ${total} ${what}. The full list is in \`.athena/model.json\`._` : "";
|
|
87
|
+
}
|
|
88
|
+
function mermaidLabel(s) {
|
|
89
|
+
return s.replace(/["`<>{}[\]()|#;]/g, " ").replace(/\s+/g, " ").trim().slice(0, 60) || "node";
|
|
90
|
+
}
|
|
91
|
+
function mermaidId(s) {
|
|
92
|
+
return `n_${s.replace(/[^A-Za-z0-9]/g, "_")}`.slice(0, 64);
|
|
93
|
+
}
|
|
94
|
+
var LEGEND = [
|
|
95
|
+
"**Status legend** \u2014 `FACT`: declared or developer-asserted \xB7 `DETECTED`: found by analysis, with evidence \xB7 `INFERRED`: heuristic, verify before relying on it \xB7 `UNKNOWN`: could not be determined."
|
|
96
|
+
].join("\n");
|
|
97
|
+
|
|
98
|
+
// src/core/knowledge/renderers/project.ts
|
|
99
|
+
function renderProject(m) {
|
|
100
|
+
const sections = [];
|
|
101
|
+
const purpose = m.description ? `${esc(m.description)}
|
|
102
|
+
|
|
103
|
+
_Source: package manifest description (FACT)._` : `${unknown("Project purpose", "no description found in manifests. Describe the project in Developer Notes below.")}`;
|
|
104
|
+
sections.push({ id: "overview", content: `## Overview
|
|
105
|
+
|
|
106
|
+
**Name:** ${esc(m.name)}
|
|
107
|
+
|
|
108
|
+
${purpose}` });
|
|
109
|
+
const totalBytes = m.languages.reduce((n, l) => n + l.bytes, 0) || 1;
|
|
110
|
+
const share = (bytes) => {
|
|
111
|
+
const pct = bytes / totalBytes * 100;
|
|
112
|
+
return pct < 5 ? "<5%" : `~${Math.round(pct / 5) * 5}%`;
|
|
113
|
+
};
|
|
114
|
+
const langRows = m.languages.slice(0, 12).map((l) => [esc(l.name), share(l.bytes)]);
|
|
115
|
+
sections.push({
|
|
116
|
+
id: "technologies",
|
|
117
|
+
content: [
|
|
118
|
+
"## Technologies",
|
|
119
|
+
"",
|
|
120
|
+
"### Languages",
|
|
121
|
+
"",
|
|
122
|
+
langRows.length ? table(["Language", "Share of source"], langRows) : notDetected("source languages"),
|
|
123
|
+
"",
|
|
124
|
+
"### Frameworks & Libraries",
|
|
125
|
+
"",
|
|
126
|
+
m.frameworks.length ? table(["Name", "Category", "Location", "Status", "Evidence"], m.frameworks.map((f) => [esc(f.name), f.category, code(f.root), status(f.provenance), evidenceList(f.provenance.evidence, 2)])) : notDetected("known frameworks"),
|
|
127
|
+
"",
|
|
128
|
+
"### Package Managers & Build Tools",
|
|
129
|
+
"",
|
|
130
|
+
m.packageManagers.length || m.buildSystems.length ? bullets([...m.packageManagers.map((p) => `Package manager: **${esc(p.name)}** \u2014 ${status(p.provenance)} (${evidenceList(p.provenance.evidence, 2)})`), ...m.buildSystems.map((b) => `Build tool: **${esc(b.name)}** \u2014 ${status(b.provenance)} (${evidenceList(b.provenance.evidence, 2)})`)]) : notDetected("package managers or build tools")
|
|
131
|
+
].join("\n")
|
|
132
|
+
});
|
|
133
|
+
const ws = m.workspace;
|
|
134
|
+
const structure = ["## Structure", ""];
|
|
135
|
+
structure.push(`**Top-level directories:** ${m.topLevelDirs.length ? m.topLevelDirs.map(code).join(", ") : "_none_"}`);
|
|
136
|
+
if (ws.isMonorepo) {
|
|
137
|
+
structure.push("", `**Monorepo:** yes${ws.tool ? ` (${esc(ws.tool)})` : ""} \u2014 DETECTED`, "");
|
|
138
|
+
structure.push(table(["Package", "Path", "Kind", "Ecosystem", "Internal deps"], ws.packages.map((p) => [esc(p.name), code(p.path), p.kind, p.ecosystem, p.internalDependencies.map(esc).join(", ") || "\u2014"])));
|
|
139
|
+
} else {
|
|
140
|
+
structure.push("", "**Monorepo:** no workspace configuration detected.");
|
|
141
|
+
}
|
|
142
|
+
sections.push({ id: "structure", content: structure.join("\n") });
|
|
143
|
+
sections.push({
|
|
144
|
+
id: "entry-points",
|
|
145
|
+
content: ["## Entry Points", "", m.entryPoints.length ? table(["Path", "Status", "Evidence"], m.entryPoints.map((e) => [code(e.path), status(e.provenance), esc(e.provenance.evidence[0]?.detail ?? evidenceList(e.provenance.evidence))])) : notDetected("entry points")].join("\n")
|
|
146
|
+
});
|
|
147
|
+
const cmds = m.commands.slice(0, 40);
|
|
148
|
+
sections.push({
|
|
149
|
+
id: "commands",
|
|
150
|
+
content: ["## Commands", "", cmds.length ? table(["Name", "Command", "Purpose", "Defined in"], cmds.map((c) => [code(c.name), code(c.command.length > 100 ? `${c.command.slice(0, 97)}...` : c.command), c.purpose, code(c.source)])) + truncatedNote(cmds.length, m.commands.length, "commands") : notDetected("scripted commands (package scripts, Makefile, justfile)")].join("\n")
|
|
151
|
+
});
|
|
152
|
+
const env = ["## Environment Requirements", ""];
|
|
153
|
+
const runtimes = m.manifests.map((x) => x.ecosystem).filter((v, i, a) => a.indexOf(v) === i);
|
|
154
|
+
env.push(`**Ecosystems:** ${runtimes.length ? runtimes.join(", ") : "UNKNOWN"}`);
|
|
155
|
+
env.push("", `**Environment variables referenced:** ${m.env.vars.length} (names only \u2014 see \`deployment.md\`)`);
|
|
156
|
+
if (m.env.envFilesPresent.length) env.push("", `**Local env files present (contents never read):** ${m.env.envFilesPresent.map(code).join(", ")}`);
|
|
157
|
+
env.push("", unknown("Required runtime versions", "not inferred; check manifests/tool-version files"));
|
|
158
|
+
sections.push({ id: "environment", content: env.join("\n") });
|
|
159
|
+
const devCmds = m.commands.filter((c) => ["dev", "build", "test", "lint", "start"].includes(c.purpose)).slice(0, 8);
|
|
160
|
+
sections.push({
|
|
161
|
+
id: "workflow",
|
|
162
|
+
content: ["## Development Workflow", "", devCmds.length ? `Detected workflow commands:
|
|
163
|
+
|
|
164
|
+
${bullets(devCmds.map((c) => `${c.purpose}: ${code(c.command)} (${code(c.source)})`))}` : unknown("Development workflow", "no dev/build/test commands detected"), "", "_Branching strategy, review process and release process are not detectable from code \u2014 document them in Developer Notes._"].join("\n")
|
|
165
|
+
});
|
|
166
|
+
if (m.warnings.length) {
|
|
167
|
+
sections.push({ id: "analysis-warnings", content: ["## Analysis Warnings", "", bullets(m.warnings.slice(0, 20).map(esc))].join("\n") });
|
|
168
|
+
} else {
|
|
169
|
+
sections.push({ id: "analysis-warnings", content: "## Analysis Warnings\n\nNone." });
|
|
170
|
+
}
|
|
171
|
+
return sections;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/core/knowledge/renderers/architecture.ts
|
|
175
|
+
function renderArchitecture(m) {
|
|
176
|
+
const s = [];
|
|
177
|
+
const fe = m.frameworks.filter((f) => ["frontend", "fullstack", "mobile", "desktop"].includes(f.category));
|
|
178
|
+
const be = m.frameworks.filter((f) => ["backend", "fullstack"].includes(f.category));
|
|
179
|
+
const summary = ["## System Overview", ""];
|
|
180
|
+
const layers = [];
|
|
181
|
+
if (fe.length) layers.push(`Client/UI layer: ${[...new Set(fe.map((f) => f.name))].map(esc).join(", ")} \u2014 DETECTED`);
|
|
182
|
+
if (be.length) layers.push(`Server/API layer: ${[...new Set(be.map((f) => f.name))].map(esc).join(", ")} \u2014 DETECTED`);
|
|
183
|
+
if (m.databases.length) layers.push(`Data layer: ${m.databases.map((d) => esc(d.name)).join(", ")} \u2014 DETECTED`);
|
|
184
|
+
if (m.caching.length) layers.push(`Caching: ${m.caching.map((d) => esc(d.name)).join(", ")} \u2014 DETECTED`);
|
|
185
|
+
if (m.queues.length) layers.push(`Async/queues: ${m.queues.map((d) => esc(d.name)).join(", ")} \u2014 DETECTED`);
|
|
186
|
+
summary.push(layers.length ? bullets(layers) : unknown("Architecture layers", "no known frameworks or data stores detected"));
|
|
187
|
+
summary.push("", "_Layer membership is based on declared dependencies. How components interact at runtime is not verified by static analysis._");
|
|
188
|
+
s.push({ id: "overview", content: summary.join("\n") });
|
|
189
|
+
const ws = m.workspace;
|
|
190
|
+
if (ws.isMonorepo && ws.packages.length > 1) {
|
|
191
|
+
const edges = ws.packages.flatMap((p) => p.internalDependencies.map((d) => [p.name, d]));
|
|
192
|
+
const lines = ["```mermaid", "graph LR"];
|
|
193
|
+
for (const p of ws.packages) if (p.path !== ".") lines.push(` ${mermaidId(p.name)}["${mermaidLabel(`${p.name} (${p.kind})`)}"]`);
|
|
194
|
+
for (const [from, to] of edges) lines.push(` ${mermaidId(from)} --> ${mermaidId(to)}`);
|
|
195
|
+
lines.push("```");
|
|
196
|
+
s.push({
|
|
197
|
+
id: "modules",
|
|
198
|
+
content: ["## Workspace Modules & Dependencies", "", `Edges are declared internal dependencies between workspace packages (DETECTED from manifests).`, "", lines.join("\n"), "", edges.length ? "" : "_No internal dependencies declared between packages._"].join("\n").trim()
|
|
199
|
+
});
|
|
200
|
+
} else {
|
|
201
|
+
const dirs = m.topLevelDirs.filter((d) => !d.startsWith("."));
|
|
202
|
+
s.push({ id: "modules", content: ["## Modules", "", dirs.length ? `Top-level directories: ${dirs.map(code).join(", ")}` : "_Flat project structure._", "", unknown("Module boundaries and responsibilities", "describe them in Developer Notes")].join("\n") });
|
|
203
|
+
}
|
|
204
|
+
if (m.containers.services.length) {
|
|
205
|
+
const lines = ["```mermaid", "graph TD"];
|
|
206
|
+
for (const svc of m.containers.services) lines.push(` ${mermaidId(svc.name)}["${mermaidLabel(`${svc.name}${svc.image ? ` \xB7 ${svc.image}` : ""}`)}"]`);
|
|
207
|
+
for (const svc of m.containers.services) for (const dep of svc.dependsOn) lines.push(` ${mermaidId(svc.name)} --> ${mermaidId(dep)}`);
|
|
208
|
+
lines.push("```");
|
|
209
|
+
s.push({
|
|
210
|
+
id: "services",
|
|
211
|
+
content: ["## Service Topology (containers)", "", "From compose files. Arrows are `depends_on` relationships (DETECTED).", "", lines.join("\n"), "", table(["Service", "Image", "Ports", "Depends on", "File"], m.containers.services.map((c) => [esc(c.name), esc(c.image ?? "\u2014"), c.ports.map(esc).join(", ") || "\u2014", c.dependsOn.map(esc).join(", ") || "\u2014", code(c.file)]))].join("\n")
|
|
212
|
+
});
|
|
213
|
+
} else {
|
|
214
|
+
s.push({ id: "services", content: "## Service Topology\n\n_No container orchestration (compose) detected._ Runtime service topology: **UNKNOWN**." });
|
|
215
|
+
}
|
|
216
|
+
const flow = ["## Data Flow", ""];
|
|
217
|
+
if (fe.length && be.length && m.databases.length) {
|
|
218
|
+
flow.push("```mermaid", "graph LR", ' client["Client / UI"] --> api["API / Server"]', ' api --> data["Data store"]', "```", "", "_INFERRED from the presence of UI, server and data-layer dependencies. Actual request paths are not verified._");
|
|
219
|
+
} else if (be.length && m.databases.length) {
|
|
220
|
+
flow.push("```mermaid", "graph LR", ' caller["Caller"] --> api["API / Server"]', ' api --> data["Data store"]', "```", "", "_INFERRED from server and data-layer dependencies._");
|
|
221
|
+
} else {
|
|
222
|
+
flow.push(unknown("Data flow", "insufficient evidence to infer a request/data path"));
|
|
223
|
+
}
|
|
224
|
+
s.push({ id: "data-flow", content: flow.join("\n") });
|
|
225
|
+
const external = [...m.auth.filter((a) => a.kind === "provider"), ...m.observability, ...m.infrastructure.filter((i) => i.kind === "hosting")];
|
|
226
|
+
s.push({
|
|
227
|
+
id: "external-services",
|
|
228
|
+
content: ["## External Services", "", external.length ? table(["Service", "Status", "Evidence"], external.map((e) => [esc(e.name), status(e.provenance), evidenceList(e.provenance.evidence, 2)])) : notDetected("external service integrations"), "", "_Only services identifiable from dependencies/config are listed. Third-party APIs called over plain HTTP are not detected in this version._"].join("\n")
|
|
229
|
+
});
|
|
230
|
+
s.push({ id: "decisions", content: "## Architectural Decisions\n\n_Athena does not infer architectural decisions. Record them in Developer Notes (or link ADRs) so agents can respect them._" });
|
|
231
|
+
return s;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/core/knowledge/renderers/data.ts
|
|
235
|
+
var MAX_ENTITIES = 60;
|
|
236
|
+
var MAX_ROUTES = 200;
|
|
237
|
+
function renderDatabase(m) {
|
|
238
|
+
const s = [];
|
|
239
|
+
s.push({
|
|
240
|
+
id: "technology",
|
|
241
|
+
content: ["## Database Technology", "", m.databases.length ? table(["Technology", "Kind", "Status", "Evidence"], m.databases.map((d) => [esc(d.name), d.kind, status(d.provenance), evidenceList(d.provenance.evidence, 2)])) : notDetected("database engines, ORMs or drivers")].join("\n")
|
|
242
|
+
});
|
|
243
|
+
const ents = m.dbEntities.slice(0, MAX_ENTITIES);
|
|
244
|
+
const entityDocs = ents.map((e) => {
|
|
245
|
+
const fields = e.fields.slice(0, 40).map((f) => `${code(f.name)}${f.type ? ` ${esc(f.type)}` : ""}${f.attributes.length ? ` \u2014 ${f.attributes.map(esc).join(" ")}` : ""}`);
|
|
246
|
+
const parts = [`### ${esc(e.name)} (${e.kind})`, "", `Defined in ${evidenceList(e.provenance.evidence, 1)} \xB7 ${status(e.provenance)}`];
|
|
247
|
+
if (fields.length) parts.push("", bullets(fields));
|
|
248
|
+
if (e.relations.length) parts.push("", `**Relations:** ${e.relations.map(esc).join("; ")}`);
|
|
249
|
+
if (e.indexes.length) parts.push("", `**Indexes/constraints:** ${e.indexes.map((i) => code(i)).join(", ")}`);
|
|
250
|
+
return parts.join("\n");
|
|
251
|
+
});
|
|
252
|
+
s.push({
|
|
253
|
+
id: "schema",
|
|
254
|
+
content: ["## Schema & Entities", "", entityDocs.length ? entityDocs.join("\n\n") + truncatedNote(ents.length, m.dbEntities.length, "entities") : notDetected("schema definitions (Prisma, SQL DDL, Django/SQLAlchemy models, Mongoose models)")].join("\n")
|
|
255
|
+
});
|
|
256
|
+
s.push({
|
|
257
|
+
id: "migrations",
|
|
258
|
+
content: ["## Migrations", "", m.migrations.length ? table(["Location", "Tool", "Files"], m.migrations.map((x) => [code(x.path), esc(x.tool), String(x.count)])) : notDetected("migration directories"), "", m.commands.filter((c) => c.purpose === "migrate").length ? `Migration commands: ${m.commands.filter((c) => c.purpose === "migrate").map((c) => code(c.command)).join(", ")}` : ""].join("\n").trim()
|
|
259
|
+
});
|
|
260
|
+
const tenantCols = m.dbEntities.filter((e) => e.fields.some((f) => /^(tenant|organization|org|workspace|account)_?id$/i.test(f.name)));
|
|
261
|
+
const iso = ["## Data Ownership & Tenant Isolation", ""];
|
|
262
|
+
if (tenantCols.length) {
|
|
263
|
+
iso.push(`Entities with tenant/organization-style key columns (DETECTED column names): ${tenantCols.map((e) => code(e.name)).join(", ")}`, "", unknown("Whether tenant isolation is enforced for all queries", "column presence does not prove enforcement"));
|
|
264
|
+
} else {
|
|
265
|
+
iso.push(unknown("Tenancy model", "no tenant/organization key columns detected"));
|
|
266
|
+
}
|
|
267
|
+
s.push({ id: "ownership", content: iso.join("\n") });
|
|
268
|
+
s.push({ id: "constraints", content: "## Important Constraints\n\n_Business-level data constraints are not inferred. Record them in Developer Notes or `rules.md`._" });
|
|
269
|
+
return s;
|
|
270
|
+
}
|
|
271
|
+
function renderApi(m) {
|
|
272
|
+
const s = [];
|
|
273
|
+
const be = m.frameworks.filter((f) => ["backend", "fullstack"].includes(f.category));
|
|
274
|
+
const styles = [];
|
|
275
|
+
if (m.routes.length) styles.push(`HTTP routes (${m.routes.length} detected)`);
|
|
276
|
+
if (m.apiSpecs.some((x) => x.kind === "graphql") || m.frameworks.some((f) => /GraphQL|Apollo/.test(f.name))) styles.push("GraphQL");
|
|
277
|
+
if (m.apiSpecs.some((x) => x.kind === "grpc") || m.frameworks.some((f) => /gRPC|tonic/.test(f.name))) styles.push("gRPC / Protocol Buffers");
|
|
278
|
+
if (m.frameworks.some((f) => f.name === "tRPC")) styles.push("tRPC");
|
|
279
|
+
s.push({
|
|
280
|
+
id: "architecture",
|
|
281
|
+
content: ["## API Architecture", "", be.length ? `**Server frameworks:** ${[...new Set(be.map((f) => f.name))].map(esc).join(", ")} (DETECTED)` : unknown("Server framework", "no known backend framework detected"), "", styles.length ? `**API styles:** ${styles.join(", ")}` : unknown("API style")].join("\n")
|
|
282
|
+
});
|
|
283
|
+
const routes = m.routes.slice(0, MAX_ROUTES);
|
|
284
|
+
s.push({
|
|
285
|
+
id: "endpoints",
|
|
286
|
+
content: [
|
|
287
|
+
"## Endpoints",
|
|
288
|
+
"",
|
|
289
|
+
routes.length ? `Paths are as written in code. Router prefixes applied elsewhere (mounting, include_router, blueprints) may not be reflected.
|
|
290
|
+
|
|
291
|
+
${table(["Method", "Path", "Framework", "Location", "Status"], routes.map((r) => [r.method, code(r.path), esc(r.framework), code(`${r.file}${r.line ? `:${r.line}` : ""}`), status(r.provenance)]))}${truncatedNote(routes.length, m.routes.length, "routes")}` : notDetected("HTTP routes")
|
|
292
|
+
].join("\n")
|
|
293
|
+
});
|
|
294
|
+
s.push({
|
|
295
|
+
id: "specs",
|
|
296
|
+
content: ["## API Specifications", "", m.apiSpecs.length ? bullets(m.apiSpecs.slice(0, 30).map((x) => `${x.kind}: ${code(x.path)}`)) : notDetected("OpenAPI, GraphQL or protobuf specifications")].join("\n")
|
|
297
|
+
});
|
|
298
|
+
s.push({
|
|
299
|
+
id: "auth-requirements",
|
|
300
|
+
content: ["## Authentication & Authorization Requirements", "", unknown("Per-endpoint auth requirements", "static route detection does not determine which middleware/guards apply to each route"), "", "See `auth.md` for detected auth mechanisms."].join("\n")
|
|
301
|
+
});
|
|
302
|
+
s.push({
|
|
303
|
+
id: "errors",
|
|
304
|
+
content: ["## Request/Response & Error Handling", "", unknown("Request/response schemas", "not extracted in this version"), "", unknown("Error response format", "document the convention in Developer Notes or rules.md")].join("\n")
|
|
305
|
+
});
|
|
306
|
+
return s;
|
|
307
|
+
}
|
|
308
|
+
function renderAuth(m) {
|
|
309
|
+
const s = [];
|
|
310
|
+
const by = (k) => m.auth.filter((a) => a.kind === k);
|
|
311
|
+
const row = (a) => [esc(a.name), status(a.provenance), evidenceList(a.provenance.evidence, 3)];
|
|
312
|
+
s.push({
|
|
313
|
+
id: "mechanism",
|
|
314
|
+
content: [
|
|
315
|
+
"## Authentication Mechanism",
|
|
316
|
+
"",
|
|
317
|
+
[...by("library"), ...by("provider")].length ? table(["Library / Provider", "Status", "Evidence"], [...by("library"), ...by("provider")].map(row)) : notDetected("authentication libraries or identity providers"),
|
|
318
|
+
"",
|
|
319
|
+
"_A declared auth dependency shows the capability is available, not that every route is protected._"
|
|
320
|
+
].join("\n")
|
|
321
|
+
});
|
|
322
|
+
s.push({
|
|
323
|
+
id: "tokens-sessions",
|
|
324
|
+
content: ["## Sessions & Tokens", "", by("token").length ? table(["Library", "Status", "Evidence"], by("token").map(row)) : notDetected("token (JWT/JWE) libraries"), "", unknown("Session storage, token lifetime and refresh-token strategy", "not determinable from dependencies alone")].join("\n")
|
|
325
|
+
});
|
|
326
|
+
const rbac = m.auth.filter((a) => a.kind === "middleware");
|
|
327
|
+
s.push({
|
|
328
|
+
id: "authorization",
|
|
329
|
+
content: ["## Authorization, Roles & Permissions", "", rbac.length ? table(["Signal", "Status", "Evidence"], rbac.map(row)) : notDetected("role/permission definitions or auth middleware"), "", unknown("Authorization model (RBAC/ABAC/ownership checks)", "confirm and document in Developer Notes")].join("\n")
|
|
330
|
+
});
|
|
331
|
+
s.push({
|
|
332
|
+
id: "passwords-mfa",
|
|
333
|
+
content: ["## Password Handling, OAuth & MFA", "", by("hashing").length ? `**Password hashing libraries:** ${by("hashing").map((h) => `${esc(h.name)} (${evidenceList(h.provenance.evidence, 1)})`).join(", ")} \u2014 DETECTED` : unknown("Password hashing", "no known hashing library detected (may be delegated to a provider)"), "", unknown("OAuth providers & MFA", "not detected in this version")].join("\n")
|
|
334
|
+
});
|
|
335
|
+
s.push({
|
|
336
|
+
id: "assumptions",
|
|
337
|
+
content: ["## Security Assumptions", "", '_No auth security assumptions are inferred automatically._ Document assumptions (e.g. "all /admin routes require the admin role") in Developer Notes so agents can check changes against them.', "", "> Athena never stores passwords, API keys, tokens or private keys in this file."].join("\n")
|
|
338
|
+
});
|
|
339
|
+
return s;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/core/knowledge/renderers/security.ts
|
|
343
|
+
function renderSecurity(m, extras = {}) {
|
|
344
|
+
const s = [];
|
|
345
|
+
s.push({
|
|
346
|
+
id: "classification",
|
|
347
|
+
content: [
|
|
348
|
+
"## How to Read This Document",
|
|
349
|
+
"",
|
|
350
|
+
bullets([
|
|
351
|
+
"**Detected** \u2014 observed directly in code/config by Athena, with evidence.",
|
|
352
|
+
"**Verified** \u2014 reported by a dedicated security tool (dependency audits below), or confirmed by a human.",
|
|
353
|
+
"**Potential** \u2014 pattern that may indicate a problem; requires human review.",
|
|
354
|
+
"**Unknown** \u2014 not determinable by static analysis."
|
|
355
|
+
]),
|
|
356
|
+
"",
|
|
357
|
+
"_This document improves security awareness. It is not a security audit and does not certify the absence of vulnerabilities._"
|
|
358
|
+
].join("\n")
|
|
359
|
+
});
|
|
360
|
+
const surface = [];
|
|
361
|
+
if (m.routes.length) surface.push(`**Detected:** ${m.routes.length} HTTP route definitions (see \`api.md\`).`);
|
|
362
|
+
if (m.apiSpecs.length) surface.push(`**Detected:** ${m.apiSpecs.length} API specification files.`);
|
|
363
|
+
const exposed = m.containers.services.filter((c) => c.ports.length);
|
|
364
|
+
if (exposed.length) surface.push(`**Detected:** compose services publishing ports: ${exposed.map((c) => `${esc(c.name)} (${c.ports.map(esc).join(", ")})`).join("; ")}.`);
|
|
365
|
+
const exposedDocker = m.containers.dockerfiles.filter((d) => d.exposedPorts.length);
|
|
366
|
+
if (exposedDocker.length) surface.push(`**Detected:** Dockerfile EXPOSE: ${exposedDocker.map((d) => `${code(d.path)} \u2192 ${d.exposedPorts.map(esc).join(", ")}`).join("; ")}.`);
|
|
367
|
+
if (m.frameworks.some((f) => ["frontend", "fullstack", "mobile"].includes(f.category))) surface.push("**Detected:** client-side code exists \u2014 anything bundled into the client is public.");
|
|
368
|
+
if (m.queues.length) surface.push(`**Detected:** message/queue consumers may process untrusted payloads (${m.queues.map((q) => esc(q.name)).join(", ")}).`);
|
|
369
|
+
s.push({ id: "attack-surface", content: ["## Attack Surface", "", surface.length ? bullets(surface) : unknown("Attack surface", "no routes, exposed ports or client code detected")].join("\n") });
|
|
370
|
+
s.push({
|
|
371
|
+
id: "controls",
|
|
372
|
+
content: ["## Security Controls (declared)", "", m.security.controls.length ? table(["Control", "Evidence"], m.security.controls.map((c) => [esc(c.name), evidenceList(c.provenance.evidence, 2)])) : notDetected("security middleware or validation libraries"), "", "_Detected as dependencies. Whether they are applied to every relevant code path is **Unknown**._"].join("\n")
|
|
373
|
+
});
|
|
374
|
+
const secretRows = m.security.secrets.slice(0, 100).map((f) => [esc(f.type), code(`${f.file}:${f.line}`)]);
|
|
375
|
+
const secretBody = ["## Secret Management", ""];
|
|
376
|
+
if (m.security.secrets.length) {
|
|
377
|
+
secretBody.push(`**Potential:** ${m.security.secrets.length} possible hardcoded secret(s). Values are never recorded. Review each and rotate any real credential.`, "", table(["Type", "Location"], secretRows));
|
|
378
|
+
} else {
|
|
379
|
+
secretBody.push("No likely secrets matched Athena's patterns in scanned files. This is **not** proof that no secrets exist (gitignored and oversized files are not scanned).");
|
|
380
|
+
}
|
|
381
|
+
if (m.env.envFilesPresent.length) secretBody.push("", `Local env files present: ${m.env.envFilesPresent.map(code).join(", ")}. Athena does not read their contents. Ensure they are gitignored.`);
|
|
382
|
+
const secretVars = m.env.vars.filter((v) => v.secretLike);
|
|
383
|
+
if (secretVars.length) secretBody.push("", `Secret-like environment variable names (values configured externally): ${secretVars.slice(0, 30).map((v) => code(v.name)).join(", ")}`);
|
|
384
|
+
s.push({ id: "secrets", content: secretBody.join("\n") });
|
|
385
|
+
const risks = [];
|
|
386
|
+
const hasBackend = m.frameworks.some((f) => ["backend", "fullstack"].includes(f.category));
|
|
387
|
+
const names = /* @__PURE__ */ new Set([...m.security.controls.map((c) => c.name)]);
|
|
388
|
+
if (hasBackend && m.routes.length && !m.auth.some((a) => a.kind !== "hashing")) risks.push("**Potential:** routes detected but no authentication library/provider detected. Auth may be custom, external (gateway) or absent \u2014 verify.");
|
|
389
|
+
if (hasBackend && ![...names].some((n) => /zod|joi|yup|validator|pydantic|validation/i.test(n))) risks.push("**Unknown:** input validation approach \u2014 no schema-validation library detected.");
|
|
390
|
+
if (m.frameworks.some((f) => ["Express", "Fastify", "Koa", "Hono"].includes(f.name)) && ![...names].some((n) => /helmet/i.test(n))) risks.push("**Potential:** no security-headers middleware (e.g. helmet) detected for the Node HTTP server.");
|
|
391
|
+
if (hasBackend && ![...names].some((n) => /cors/i.test(n))) risks.push("**Unknown:** CORS policy \u2014 no CORS library detected (may be configured in framework/gateway).");
|
|
392
|
+
if (m.frameworks.some((f) => ["Express", "Fastify", "Koa"].includes(f.name)) && ![...names].some((n) => /rate-limit/i.test(n))) risks.push("**Unknown:** rate limiting \u2014 no rate-limit middleware detected.");
|
|
393
|
+
if (m.databases.some((d) => d.kind === "driver") && !m.databases.some((d) => d.kind === "orm")) risks.push("**Potential:** raw database driver without an ORM detected \u2014 review queries for injection (use parameterized queries).");
|
|
394
|
+
if (m.auth.some((a) => /csurf/.test(a.name)) || names.has("csurf (deprecated)")) risks.push("**Detected:** `csurf` is deprecated; consider a maintained CSRF library.");
|
|
395
|
+
risks.push("**Unknown:** CSRF protection for cookie-based sessions, SSRF protections for server-side fetches, and output encoding \u2014 not analyzed in this version.");
|
|
396
|
+
s.push({ id: "risks", content: ["## Risk Areas", "", bullets(risks)].join("\n") });
|
|
397
|
+
s.push({
|
|
398
|
+
id: "tooling",
|
|
399
|
+
content: ["## Security Tooling", "", m.security.tooling.length ? table(["Tool", "Evidence"], dedupe(m.security.tooling).map((t) => [esc(t.name), evidenceList(t.provenance.evidence, 2)])) : notDetected("security tooling (dependency scanning, SAST, secret scanning)")].join("\n")
|
|
400
|
+
});
|
|
401
|
+
s.push({ id: "dependencies", content: renderDependencyRisks(m, extras.securityScan ?? null) });
|
|
402
|
+
s.push({ id: "known-issues", content: "## Known Unresolved Security Issues\n\n_None recorded by Athena._ Track confirmed issues in Developer Notes (without exploit details or secrets)." });
|
|
403
|
+
return s;
|
|
404
|
+
}
|
|
405
|
+
function renderDependencyRisks(m, scan) {
|
|
406
|
+
const head = ["## Dependency Risks", ""];
|
|
407
|
+
const manifests = `Manifests in scope: ${m.manifests.length ? m.manifests.map((x) => code(x.path)).slice(0, 15).join(", ") : "_none_"}`;
|
|
408
|
+
if (!scan) {
|
|
409
|
+
return [...head, "No dependency audit has been run. Run `athena security` to audit dependencies with the tools installed for this project.", "", manifests].join("\n");
|
|
410
|
+
}
|
|
411
|
+
const lines = [...head, `Last scan: ${scan.scannedAt} (${(scan.durationMs / 1e3).toFixed(1)}s). Findings come from the tools below, not from Athena.`, ""];
|
|
412
|
+
const ran = scan.tools.filter((t) => t.status === "ok");
|
|
413
|
+
const missing = scan.tools.filter((t) => t.status !== "ok");
|
|
414
|
+
const findings = ran.flatMap((t) => t.findings.map((f) => ({ ...f, tool: t.tool, target: t.target })));
|
|
415
|
+
if (findings.length) {
|
|
416
|
+
const rows = findings.slice(0, 60).map((f) => [esc(f.package), f.severity.toUpperCase(), esc(f.title.slice(0, 90)), esc(f.id ?? "\u2014"), esc(f.target ? `${f.tool} (${f.target})` : f.tool), f.fixAvailable ? "yes" : "unknown"]);
|
|
417
|
+
lines.push(`**Detected by tooling:** ${findings.length} vulnerable dependenc${findings.length === 1 ? "y" : "ies"}.`, "", table(["Package", "Severity", "Advisory", "ID", "Tool", "Fix available"], rows));
|
|
418
|
+
if (findings.length > 60) lines.push("", `_Showing 60 of ${findings.length}. See \`.athena/security-scan.json\`._`);
|
|
419
|
+
if (findings.some((f) => f.severity === "unrated")) lines.push("", "_UNRATED: the tool reports the vulnerability without a severity (pip-audit, govulncheck). Treat it as unreviewed, not as low._");
|
|
420
|
+
} else if (ran.length) {
|
|
421
|
+
lines.push(`**Detected:** no known vulnerable dependencies reported by ${ran.map((t) => esc(t.tool)).join(", ")} at the time of the scan.`);
|
|
422
|
+
}
|
|
423
|
+
if (missing.length) {
|
|
424
|
+
lines.push("", "**Unknown** \u2014 these ecosystems were not audited:", "", bullets(missing.map((t) => `${esc(t.tool)} (${esc(t.target ?? t.ecosystem)}): ${esc(t.message ?? t.status)}`)));
|
|
425
|
+
}
|
|
426
|
+
lines.push("", manifests, "", "_Athena does not maintain its own vulnerability database, and a clean audit is not proof that dependencies are safe._");
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
function dedupe(items) {
|
|
430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
431
|
+
return items.filter((i) => seen.has(i.name) ? false : (seen.add(i.name), true));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// src/core/knowledge/renderers/quality.ts
|
|
435
|
+
function renderTesting(m) {
|
|
436
|
+
const t = m.tests;
|
|
437
|
+
const s = [];
|
|
438
|
+
s.push({
|
|
439
|
+
id: "frameworks",
|
|
440
|
+
content: ["## Test Frameworks", "", t.frameworks.length ? table(["Framework", "Evidence"], t.frameworks.map((f) => [esc(f.name), evidenceList(f.provenance.evidence, 3)])) : notDetected("test frameworks")].join("\n")
|
|
441
|
+
});
|
|
442
|
+
const kinds = [];
|
|
443
|
+
const names = t.frameworks.map((f) => f.name);
|
|
444
|
+
if (names.some((n) => /Playwright|Cypress|Detox|integration_test/.test(n))) kinds.push("End-to-end / UI tests: framework DETECTED");
|
|
445
|
+
if (names.some((n) => /Supertest|Testcontainers|pytest-django|Spring Boot Test/.test(n))) kinds.push("Integration tests: supporting library DETECTED");
|
|
446
|
+
if (names.some((n) => /Jest|Vitest|Mocha|pytest|go test|cargo test|JUnit|RSpec|PHPUnit|xUnit|NUnit|flutter_test|Minitest|Pest|testify/.test(n))) kinds.push("Unit tests: runner DETECTED");
|
|
447
|
+
s.push({
|
|
448
|
+
id: "layout",
|
|
449
|
+
content: [
|
|
450
|
+
"## Test Layout",
|
|
451
|
+
"",
|
|
452
|
+
`**Test files detected:** ${t.testFileCount}`,
|
|
453
|
+
"",
|
|
454
|
+
t.testDirs.length ? `**Test directories:** ${t.testDirs.map(code).join(", ")}` : "_No dedicated test directories detected._",
|
|
455
|
+
"",
|
|
456
|
+
kinds.length ? bullets(kinds) : unknown("Unit/integration/E2E split")
|
|
457
|
+
].join("\n")
|
|
458
|
+
});
|
|
459
|
+
const cmds = m.commands.filter((c) => c.purpose === "test");
|
|
460
|
+
s.push({ id: "commands", content: ["## Test Commands", "", cmds.length ? table(["Name", "Command", "Defined in"], cmds.slice(0, 20).map((c) => [code(c.name), code(c.command), code(c.source)])) : notDetected("test commands")].join("\n") });
|
|
461
|
+
s.push({
|
|
462
|
+
id: "coverage",
|
|
463
|
+
content: ["## Coverage", "", t.coverageConfigs.length ? `Coverage configuration found in: ${[...new Set(t.coverageConfigs)].map(code).join(", ")}` : notDetected("coverage configuration"), "", unknown("Actual coverage percentage", "Athena does not run tests or read coverage reports in this version")].join("\n")
|
|
464
|
+
});
|
|
465
|
+
const gaps = [];
|
|
466
|
+
if (t.testFileCount === 0 && m.stats.filesScanned > 0) gaps.push("**Detected:** no test files found in the project.");
|
|
467
|
+
if (t.packagesWithoutTests.length) gaps.push(`**Detected:** workspace packages with source files but no test files: ${t.packagesWithoutTests.map(code).join(", ")}`);
|
|
468
|
+
if (m.routes.length && !names.some((n) => /Supertest|Playwright|Cypress|pytest|Spring Boot Test|Testcontainers/.test(n))) gaps.push("**Potential:** HTTP routes exist but no API/integration testing library was detected.");
|
|
469
|
+
s.push({ id: "gaps", content: ["## Missing Coverage (structural)", "", gaps.length ? bullets(gaps) : "_No structural gaps detected._ This does not measure test quality or coverage."].join("\n") });
|
|
470
|
+
s.push({ id: "critical-areas", content: "## Critical Test Areas\n\n_Not inferred. List business-critical flows (payments, auth, data deletion, migrations) in Developer Notes so agents prioritize tests for them._" });
|
|
471
|
+
return s;
|
|
472
|
+
}
|
|
473
|
+
function renderDebugging(m) {
|
|
474
|
+
const s = [];
|
|
475
|
+
s.push({ id: "known-bugs", content: "## Known Bugs\n\n_None recorded._ Add known bugs and their status in Developer Notes." });
|
|
476
|
+
const cmds = m.commands.filter((c) => ["dev", "start", "test", "lint"].includes(c.purpose)).slice(0, 15);
|
|
477
|
+
s.push({ id: "commands", content: ["## Debugging & Diagnostic Commands", "", cmds.length ? table(["Purpose", "Command", "Defined in"], cmds.map((c) => [c.purpose, code(c.command), code(c.source)])) : notDetected("runnable dev/test commands")].join("\n") });
|
|
478
|
+
s.push({
|
|
479
|
+
id: "logging",
|
|
480
|
+
content: ["## Logging & Observability", "", m.observability.length ? table(["Tool", "Evidence"], m.observability.map((o) => [esc(o.name), evidenceList(o.provenance.evidence, 2)])) : notDetected("logging/monitoring libraries"), "", unknown("Log locations and formats", "document where logs are written and how to access them")].join("\n")
|
|
481
|
+
});
|
|
482
|
+
const hot = m.git.hotspots;
|
|
483
|
+
s.push({
|
|
484
|
+
id: "hotspots",
|
|
485
|
+
content: ["## Change Hotspots", "", hot.length ? `Directories with the most file changes in the last 180 days (git history), most-changed first. **INFERRED** as areas more likely to contain regressions. Exact counts are omitted to keep this document stable; see \`model.json\`.
|
|
486
|
+
|
|
487
|
+
${hot.slice(0, 8).map((h, i) => `${i + 1}. ${code(h.path)}`).join("\n")}` : "_Not available (no git history)._"].join("\n")
|
|
488
|
+
});
|
|
489
|
+
s.push({ id: "common-errors", content: "## Common Errors & Workarounds\n\n_None recorded._ When an agent or developer resolves a recurring error, record the symptom, cause and fix in Developer Notes." });
|
|
490
|
+
return s;
|
|
491
|
+
}
|
|
492
|
+
function renderPerformance(m) {
|
|
493
|
+
const s = [];
|
|
494
|
+
s.push({ id: "caching", content: ["## Caching", "", m.caching.length ? table(["Technology", "Evidence"], m.caching.map((c) => [esc(c.name), evidenceList(c.provenance.evidence, 2)])) : notDetected("caching libraries or cache services"), "", unknown("Cache strategy and invalidation rules")].join("\n") });
|
|
495
|
+
s.push({ id: "queues", content: ["## Background Jobs & Queues", "", m.queues.length ? table(["Technology", "Evidence"], m.queues.map((c) => [esc(c.name), evidenceList(c.provenance.evidence, 2)])) : notDetected("queue or background-job systems")].join("\n") });
|
|
496
|
+
const idx = m.dbEntities.filter((e) => e.indexes.length).length;
|
|
497
|
+
s.push({
|
|
498
|
+
id: "database",
|
|
499
|
+
content: ["## Database Performance", "", m.dbEntities.length ? `${m.dbEntities.length} entities detected; ${idx} with explicit indexes/constraints (see \`database.md\`).` : notDetected("database entities"), "", unknown("Query patterns, N+1 risks and slow queries", "requires runtime data or deeper analysis")].join("\n")
|
|
500
|
+
});
|
|
501
|
+
const fe = m.frameworks.filter((f) => ["frontend", "fullstack"].includes(f.category));
|
|
502
|
+
s.push({ id: "frontend", content: ["## Frontend Performance", "", fe.length ? `Frontend frameworks: ${[...new Set(fe.map((f) => f.name))].map(esc).join(", ")}.` : "_No frontend framework detected._", "", unknown("Bundle size, rendering strategy and Core Web Vitals")].join("\n") });
|
|
503
|
+
s.push({ id: "bottlenecks", content: "## Known Bottlenecks\n\n_None recorded._ Athena does not invent benchmarks. Record measured bottlenecks (with how they were measured) in Developer Notes." });
|
|
504
|
+
return s;
|
|
505
|
+
}
|
|
506
|
+
function renderCodeReview(m) {
|
|
507
|
+
const s = [];
|
|
508
|
+
const general = [
|
|
509
|
+
"Change is limited to the stated task; no unrelated files modified.",
|
|
510
|
+
"Follows every enabled rule in `rules.md`.",
|
|
511
|
+
"No secrets, credentials or tokens added to code, config, logs or tests.",
|
|
512
|
+
"New dependencies are justified and actively maintained.",
|
|
513
|
+
"Breaking changes (API, schema, config) are called out explicitly.",
|
|
514
|
+
"Athena knowledge synchronized (`athena sync`) if structure, routes, schema or infrastructure changed."
|
|
515
|
+
];
|
|
516
|
+
s.push({ id: "general", content: ["## General Checklist", "", bullets(general.map((g) => `[ ] ${g}`))].join("\n") });
|
|
517
|
+
const arch = [];
|
|
518
|
+
if (m.workspace.isMonorepo) arch.push("Cross-package imports go through declared workspace dependencies (no deep relative imports into other packages).");
|
|
519
|
+
if (m.frameworks.some((f) => ["backend", "fullstack"].includes(f.category))) arch.push("Business logic is not added directly to route handlers/controllers when a service layer exists.");
|
|
520
|
+
arch.push("New modules follow the existing directory conventions (see `project.md` \u2192 Structure).");
|
|
521
|
+
s.push({ id: "architecture", content: ["## Architecture", "", bullets(arch.map((a) => `[ ] ${a}`))].join("\n") });
|
|
522
|
+
const sec = [];
|
|
523
|
+
if (m.routes.length) sec.push("New/changed endpoints enforce authentication and authorization as required.", "Request input is validated before use.");
|
|
524
|
+
if (m.databases.length) sec.push("Database access uses parameterized queries / ORM APIs \u2014 no string-built SQL with user input.");
|
|
525
|
+
if (m.frameworks.some((f) => ["frontend", "fullstack", "mobile"].includes(f.category))) sec.push("No server-only secrets or privileged keys exposed to client bundles.", "User-provided content is not rendered as raw HTML without sanitization.");
|
|
526
|
+
sec.push("Errors do not leak stack traces, secrets or internal identifiers to clients.");
|
|
527
|
+
s.push({ id: "security", content: ["## Security", "", bullets(sec.map((x) => `[ ] ${x}`))].join("\n") });
|
|
528
|
+
const testing = [m.tests.frameworks.length ? `New logic has tests using the project's framework(s): ${m.tests.frameworks.map((f) => esc(f.name)).join(", ")}.` : "Consider adding tests \u2014 no test framework was detected."];
|
|
529
|
+
const testCmd = m.commands.find((c) => c.purpose === "test");
|
|
530
|
+
if (testCmd) testing.push(`Tests pass: ${code(testCmd.command)}`);
|
|
531
|
+
const lintCmd = m.commands.find((c) => c.purpose === "lint");
|
|
532
|
+
if (lintCmd) testing.push(`Lint/type checks pass: ${code(lintCmd.command)}`);
|
|
533
|
+
s.push({ id: "testing", content: ["## Testing Expectations", "", bullets(testing.map((x) => `[ ] ${x}`))].join("\n") });
|
|
534
|
+
const perf = [];
|
|
535
|
+
if (m.databases.length) perf.push("New queries on large tables use indexes; no N+1 query patterns introduced.");
|
|
536
|
+
if (m.caching.length) perf.push("Cache invalidation is handled for changed data.");
|
|
537
|
+
if (m.queues.length) perf.push("Long-running work is offloaded to background jobs where appropriate.");
|
|
538
|
+
perf.push("No unbounded loops/fetches over user-controlled sizes.");
|
|
539
|
+
s.push({ id: "performance", content: ["## Performance", "", bullets(perf.map((x) => `[ ] ${x}`))].join("\n") });
|
|
540
|
+
s.push({ id: "common-mistakes", content: "## Common Mistakes in This Project\n\n_None recorded._ Add recurring review findings in Developer Notes." });
|
|
541
|
+
return s;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/core/knowledge/renderers/deployment.ts
|
|
545
|
+
function renderDeployment(m) {
|
|
546
|
+
const s = [];
|
|
547
|
+
const hosting = m.infrastructure.filter((i) => i.kind === "hosting");
|
|
548
|
+
const iac = m.infrastructure.filter((i) => i.kind === "iac");
|
|
549
|
+
const k8s = m.infrastructure.filter((i) => i.kind === "kubernetes");
|
|
550
|
+
const proxy = m.infrastructure.filter((i) => i.kind === "reverse-proxy");
|
|
551
|
+
s.push({
|
|
552
|
+
id: "architecture",
|
|
553
|
+
content: [
|
|
554
|
+
"## Deployment Architecture",
|
|
555
|
+
"",
|
|
556
|
+
hosting.length ? `**Hosting/platform configuration:**
|
|
557
|
+
|
|
558
|
+
${table(["Platform", "File"], hosting.map((h) => [esc(h.name), code(h.file)]))}` : unknown("Hosting platform", "no platform configuration files detected"),
|
|
559
|
+
"",
|
|
560
|
+
iac.length ? `**Infrastructure as code:** ${iac.map((i) => `${esc(i.name)} (${code(i.file)})`).join(", ")}` : "_No infrastructure-as-code detected._",
|
|
561
|
+
"",
|
|
562
|
+
k8s.length ? `**Kubernetes resources:** ${k8s.map((i) => `${esc(i.name)} (${code(i.file)})`).join(", ")}` : "",
|
|
563
|
+
proxy.length ? `**Reverse proxy:** ${proxy.map((i) => `${esc(i.name)} (${code(i.file)})`).join(", ")}` : ""
|
|
564
|
+
].filter((l, i, a) => l !== "" || a[i - 1] !== "").join("\n").trim()
|
|
565
|
+
});
|
|
566
|
+
const df = m.containers.dockerfiles;
|
|
567
|
+
s.push({
|
|
568
|
+
id: "containers",
|
|
569
|
+
content: ["## Containers", "", df.length ? table(["Dockerfile", "Base images", "Exposed ports"], df.map((d) => [code(d.path), d.baseImages.map(esc).join(", ") || "\u2014", d.exposedPorts.map(esc).join(", ") || "\u2014"])) : notDetected("Dockerfiles"), "", m.containers.services.length ? `Compose services: ${m.containers.services.map((c) => code(c.name)).join(", ")} (see \`architecture.md\`).` : ""].join("\n").trim()
|
|
570
|
+
});
|
|
571
|
+
const jobs = m.ci.slice(0, 30);
|
|
572
|
+
s.push({
|
|
573
|
+
id: "ci-cd",
|
|
574
|
+
content: [
|
|
575
|
+
"## CI/CD",
|
|
576
|
+
"",
|
|
577
|
+
jobs.length ? table(["System", "Job", "Key steps", "File"], jobs.map((j) => [esc(j.system), esc(j.name), j.commands.slice(0, 4).map((c) => code(c)).join("<br>") || "\u2014", code(j.file)])) + truncatedNote(jobs.length, m.ci.length, "CI jobs") : notDetected("CI/CD pipelines")
|
|
578
|
+
].join("\n")
|
|
579
|
+
});
|
|
580
|
+
s.push({ id: "environments", content: ["## Environments", "", unknown("Environment list (dev/staging/production), domains and promotion flow", "not determinable from code \u2014 document them in Developer Notes")].join("\n") });
|
|
581
|
+
const vars = m.env.vars.slice(0, 80);
|
|
582
|
+
s.push({
|
|
583
|
+
id: "env-vars",
|
|
584
|
+
content: [
|
|
585
|
+
"## Environment Variables",
|
|
586
|
+
"",
|
|
587
|
+
"Names only. **Values are never read or stored by Athena** \u2014 all values are `<configured externally>`.",
|
|
588
|
+
"",
|
|
589
|
+
vars.length ? table(["Variable", "Value", "Referenced in"], vars.map((v) => [code(v.name), v.secretLike ? "`<secret>`" : "`<configured externally>`", v.references.slice(0, 2).map(code).join(", ") + (v.references.length > 2 ? ` +${v.references.length - 2}` : "")])) + truncatedNote(vars.length, m.env.vars.length, "variables") : notDetected("environment variable references")
|
|
590
|
+
].join("\n")
|
|
591
|
+
});
|
|
592
|
+
const deployCmds = m.commands.filter((c) => ["build", "deploy", "migrate", "start"].includes(c.purpose)).slice(0, 15);
|
|
593
|
+
s.push({
|
|
594
|
+
id: "commands",
|
|
595
|
+
content: ["## Build, Release & Migration Commands", "", deployCmds.length ? table(["Purpose", "Command", "Defined in"], deployCmds.map((c) => [c.purpose, code(c.command), code(c.source)])) : notDetected("build/deploy commands"), "", m.migrations.length ? `Migrations: ${m.migrations.map((x) => `${esc(x.tool)} in ${code(x.path)}`).join(", ")}. ${unknown("When migrations run during deploy")}` : ""].join("\n").trim()
|
|
596
|
+
});
|
|
597
|
+
s.push({ id: "notes", content: ["## Deployment Safety", "", bullets(["Never commit secrets; configure them in the hosting platform or a secret manager.", "Document rollback procedures in Developer Notes."]), "", `_Evidence for this document comes from ${m.infrastructure.length + m.ci.length + df.length} infrastructure/CI files (${evidenceList(m.infrastructure.flatMap((i) => i.provenance.evidence), 3)})._`].join("\n") });
|
|
598
|
+
return s;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/core/knowledge/rules-seed.ts
|
|
602
|
+
function seedRules(m) {
|
|
603
|
+
const hasBackend = m.frameworks.some((f) => ["backend", "fullstack"].includes(f.category));
|
|
604
|
+
const hasFrontend = m.frameworks.some((f) => ["frontend", "fullstack", "mobile"].includes(f.category));
|
|
605
|
+
const hasDb = m.databases.length > 0;
|
|
606
|
+
const hasMigrations = m.migrations.length > 0;
|
|
607
|
+
const hasTests = m.tests.frameworks.length > 0;
|
|
608
|
+
const tenant = m.dbEntities.some((e) => e.fields.some((f) => /^(tenant|organization|org|workspace|account)_?id$/i.test(f.name)));
|
|
609
|
+
const out = [
|
|
610
|
+
"# Athena Project Rules",
|
|
611
|
+
"",
|
|
612
|
+
"<!--",
|
|
613
|
+
"This file is owned by you. Athena creates it once and never regenerates it.",
|
|
614
|
+
"AI agents configured by Athena are instructed to follow every enabled rule.",
|
|
615
|
+
"",
|
|
616
|
+
'Format: one rule per top-level bullet under a "## Section" heading.',
|
|
617
|
+
'Disable a rule without deleting it by writing "- [disabled] ...".',
|
|
618
|
+
"Rules marked [disabled] below are SUGGESTIONS based on the detected stack \u2014 enable the ones that apply.",
|
|
619
|
+
"-->",
|
|
620
|
+
"",
|
|
621
|
+
"## AI Agent Rules",
|
|
622
|
+
"",
|
|
623
|
+
"- Read the relevant Athena knowledge (see the relevance map in your agent instructions) before making significant changes.",
|
|
624
|
+
"- Do not modify files unrelated to the task.",
|
|
625
|
+
"- Do not introduce new dependencies without stating the justification.",
|
|
626
|
+
"- Explain breaking changes (API, schema, configuration) before implementing them.",
|
|
627
|
+
"- Never add secrets, credentials or tokens to code, config, tests or logs.",
|
|
628
|
+
"- Treat statements marked INFERRED or UNKNOWN in Athena knowledge as unverified.",
|
|
629
|
+
"",
|
|
630
|
+
"## Security",
|
|
631
|
+
"",
|
|
632
|
+
"- Validate all external input at trust boundaries."
|
|
633
|
+
];
|
|
634
|
+
if (hasFrontend) out.push("- Never expose server-side secrets to frontend code or client bundles.");
|
|
635
|
+
if (hasDb) out.push("- Use parameterized queries or ORM query APIs; never build SQL from user input.");
|
|
636
|
+
out.push("", "## Architecture", "");
|
|
637
|
+
out.push(hasBackend ? "- [disabled] Keep business logic out of route handlers/controllers; use the service layer." : "- [disabled] Keep modules focused; do not create circular dependencies.");
|
|
638
|
+
if (m.workspace.isMonorepo) out.push("- [disabled] Packages may only import other workspace packages through declared dependencies.");
|
|
639
|
+
if (hasDb) {
|
|
640
|
+
out.push("", "## Database", "");
|
|
641
|
+
if (hasMigrations) out.push("- [disabled] All schema changes must go through migrations; never modify production schema manually.");
|
|
642
|
+
else out.push("- [disabled] Schema changes must be reviewed before applying them to shared environments.");
|
|
643
|
+
if (tenant) out.push("- [disabled] Every tenant-owned table must include the tenant key, and every query must filter by it.");
|
|
644
|
+
}
|
|
645
|
+
if (m.routes.length || hasBackend) {
|
|
646
|
+
out.push("", "## API", "");
|
|
647
|
+
out.push("- [disabled] Return consistent error responses across endpoints.");
|
|
648
|
+
out.push("- [disabled] New endpoints must declare their authentication and authorization requirements.");
|
|
649
|
+
}
|
|
650
|
+
out.push("", "## Testing", "");
|
|
651
|
+
out.push(hasTests ? "- [disabled] New business logic requires tests." : "- [disabled] Add a test framework before adding complex business logic.");
|
|
652
|
+
out.push("");
|
|
653
|
+
return out.join("\n");
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/core/knowledge/generate.ts
|
|
657
|
+
var RENDERERS = {
|
|
658
|
+
project: renderProject,
|
|
659
|
+
architecture: renderArchitecture,
|
|
660
|
+
database: renderDatabase,
|
|
661
|
+
api: renderApi,
|
|
662
|
+
auth: renderAuth,
|
|
663
|
+
security: renderSecurity,
|
|
664
|
+
testing: renderTesting,
|
|
665
|
+
debugging: renderDebugging,
|
|
666
|
+
performance: renderPerformance,
|
|
667
|
+
"code-review": renderCodeReview,
|
|
668
|
+
deployment: renderDeployment
|
|
669
|
+
};
|
|
670
|
+
var DEVELOPER_NOTES_HEADING = "## Developer Notes";
|
|
671
|
+
function renderSections(id, model, extras = {}) {
|
|
672
|
+
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
673
|
+
const about = {
|
|
674
|
+
id: "about",
|
|
675
|
+
content: [
|
|
676
|
+
`> **Purpose:** ${doc.purpose}`,
|
|
677
|
+
">",
|
|
678
|
+
"> Generated by Athena from repository analysis. Content between `athena:generated` markers is refreshed by `athena analyze`; if you edit inside a marker, Athena keeps your edit and stops refreshing that section (use `--force` to regenerate). Everything outside the markers is yours.",
|
|
679
|
+
">",
|
|
680
|
+
`> ${LEGEND}`
|
|
681
|
+
].join("\n")
|
|
682
|
+
};
|
|
683
|
+
return [about, ...RENDERERS[id](model, extras)].map((s) => ({ id: s.id, content: redact(s.content) }));
|
|
684
|
+
}
|
|
685
|
+
function composeNewDocument(id, sections) {
|
|
686
|
+
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
687
|
+
return [
|
|
688
|
+
`# ${doc.title}`,
|
|
689
|
+
"",
|
|
690
|
+
sections.map((s) => renderBlock(s)).join("\n\n"),
|
|
691
|
+
"",
|
|
692
|
+
DEVELOPER_NOTES_HEADING,
|
|
693
|
+
"",
|
|
694
|
+
"<!-- Knowledge Athena cannot detect: decisions, conventions, known issues, gotchas. Athena never modifies this section. -->",
|
|
695
|
+
""
|
|
696
|
+
].join("\n");
|
|
697
|
+
}
|
|
698
|
+
async function planKnowledge(athenaDir2, model, opts = {}) {
|
|
699
|
+
const plans = [];
|
|
700
|
+
const extras = { securityScan: await loadScanFromDir(athenaDir2) };
|
|
701
|
+
const ids = Object.keys(RENDERERS).filter((id) => !opts.only || opts.only.includes(id));
|
|
702
|
+
for (const id of ids) {
|
|
703
|
+
const doc = KNOWLEDGE_DOCS.find((d) => d.id === id);
|
|
704
|
+
const sections = renderSections(id, model, extras);
|
|
705
|
+
const before = await readTextIfExists(path.join(athenaDir2, doc.file));
|
|
706
|
+
if (before === null) {
|
|
707
|
+
plans.push({ id, file: doc.file, status: "created", before, after: composeNewDocument(id, sections), blocks: sections.map((s) => s.id), changedBlocks: sections.map((s) => s.id), preservedModified: [] });
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const merged = mergeDocument(before, sections, { force: opts.force, previouslyKnown: opts.previousBlocks?.[id], insertBefore: DEVELOPER_NOTES_HEADING });
|
|
711
|
+
plans.push({
|
|
712
|
+
id,
|
|
713
|
+
file: doc.file,
|
|
714
|
+
status: merged.changed ? "updated" : "unchanged",
|
|
715
|
+
before,
|
|
716
|
+
after: merged.text,
|
|
717
|
+
blocks: sections.map((s) => s.id),
|
|
718
|
+
changedBlocks: [...merged.updated, ...merged.added],
|
|
719
|
+
preservedModified: merged.preservedModified
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return plans;
|
|
723
|
+
}
|
|
724
|
+
async function writePlannedKnowledge(athenaDir2, plans) {
|
|
725
|
+
const reports = [];
|
|
726
|
+
for (const p of plans) {
|
|
727
|
+
if (p.status !== "unchanged") await writeFileAtomic(path.join(athenaDir2, p.file), p.after);
|
|
728
|
+
reports.push({ id: p.id, file: p.file, status: p.status, blocks: p.blocks, preservedModified: p.preservedModified, contentHash: sha256(p.after).slice(0, 16) });
|
|
729
|
+
}
|
|
730
|
+
return reports;
|
|
731
|
+
}
|
|
732
|
+
async function writeKnowledge(athenaDir2, model, opts = {}) {
|
|
733
|
+
return writePlannedKnowledge(athenaDir2, await planKnowledge(athenaDir2, model, opts));
|
|
734
|
+
}
|
|
735
|
+
async function ensureRules(athenaDir2, model) {
|
|
736
|
+
const file = path.join(athenaDir2, "rules.md");
|
|
737
|
+
const existing = await readTextIfExists(file);
|
|
738
|
+
if (existing !== null) return { id: "rules", file: "rules.md", status: "unchanged", blocks: [], preservedModified: [], contentHash: sha256(existing).slice(0, 16) };
|
|
739
|
+
const text = seedRules(model);
|
|
740
|
+
await writeFileAtomic(file, text);
|
|
741
|
+
return { id: "rules", file: "rules.md", status: "created", blocks: [], preservedModified: [], contentHash: sha256(text).slice(0, 16) };
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// src/services/pipeline.ts
|
|
745
|
+
import { promises as fs } from "fs";
|
|
746
|
+
import path2 from "path";
|
|
747
|
+
import crypto from "crypto";
|
|
748
|
+
var ATHENA_GITIGNORE = [
|
|
749
|
+
"# Local analysis cache \u2014 safe to delete, regenerated by `athena analyze`.",
|
|
750
|
+
"# Commit the *.md knowledge files so your team and Git history share them.",
|
|
751
|
+
"state.json",
|
|
752
|
+
"model.json",
|
|
753
|
+
"graph.json",
|
|
754
|
+
"security-scan.json",
|
|
755
|
+
"ai-suggestions.md",
|
|
756
|
+
".backup/",
|
|
757
|
+
".server.json",
|
|
758
|
+
".sync-ignore.json",
|
|
759
|
+
".agent-events.jsonl",
|
|
760
|
+
".agent-events.1.jsonl",
|
|
761
|
+
".agent-events.rotate.lock",
|
|
762
|
+
"cache/",
|
|
763
|
+
"# Machine-local settings and secrets (never commit).",
|
|
764
|
+
"local.json",
|
|
765
|
+
""
|
|
766
|
+
].join("\n");
|
|
767
|
+
var GITIGNORE_ENTRIES = ["state.json", "model.json", "graph.json", "security-scan.json", "ai-suggestions.md", ".backup/", ".server.json", ".sync-ignore.json", ".agent-events.jsonl", ".agent-events.1.jsonl", ".agent-events.rotate.lock", "cache/", "local.json"];
|
|
768
|
+
async function ensureGitignoreEntries(file) {
|
|
769
|
+
const existing = await readTextIfExists(file);
|
|
770
|
+
if (existing === null) {
|
|
771
|
+
await writeFileAtomic(file, ATHENA_GITIGNORE);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
const lines = existing.split(/\r?\n/);
|
|
775
|
+
const missing = GITIGNORE_ENTRIES.filter((e) => !lines.includes(e));
|
|
776
|
+
if (!missing.length) return;
|
|
777
|
+
await writeFileAtomic(file, `${existing.replace(/\s*$/, "")}
|
|
778
|
+
${missing.join("\n")}
|
|
779
|
+
`);
|
|
780
|
+
}
|
|
781
|
+
async function indexWithIntegrations(root, files, written) {
|
|
782
|
+
const index = buildFileIndex(files);
|
|
783
|
+
for (const rel of written) {
|
|
784
|
+
try {
|
|
785
|
+
const abs = path2.join(root, rel);
|
|
786
|
+
const [st, buf] = await Promise.all([fs.stat(abs), fs.readFile(abs)]);
|
|
787
|
+
index[rel] = { h: sha256(buf), s: st.size, m: Math.floor(st.mtimeMs) };
|
|
788
|
+
} catch {
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return index;
|
|
792
|
+
}
|
|
793
|
+
function composeState(input) {
|
|
794
|
+
const { prevState, analysis, docs } = input;
|
|
795
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
796
|
+
const documents = {};
|
|
797
|
+
for (const d of docs) {
|
|
798
|
+
const before = prevState?.documents[d.id];
|
|
799
|
+
documents[d.id] = {
|
|
800
|
+
file: d.file,
|
|
801
|
+
blocks: d.blocks,
|
|
802
|
+
lastGeneratedAt: now,
|
|
803
|
+
lastChangedAt: d.status === "unchanged" && before ? before.lastChangedAt : now,
|
|
804
|
+
contentHash: d.contentHash,
|
|
805
|
+
preservedModified: d.preservedModified
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return {
|
|
809
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
810
|
+
athenaVersion: ATHENA_VERSION,
|
|
811
|
+
projectName: analysis.model.name,
|
|
812
|
+
createdAt: prevState?.createdAt ?? now,
|
|
813
|
+
analyzedAt: now,
|
|
814
|
+
analysisDurationMs: analysis.durationMs,
|
|
815
|
+
git: { isRepo: analysis.model.git.isRepo, head: analysis.model.git.head, branch: analysis.model.git.branch },
|
|
816
|
+
fileIndex: input.fileIndex,
|
|
817
|
+
documents,
|
|
818
|
+
detectors: analysis.detectorVersions,
|
|
819
|
+
agents: input.agents,
|
|
820
|
+
warnings: analysis.model.warnings
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
async function autoSelectAdapters(root) {
|
|
824
|
+
const selected = [];
|
|
825
|
+
for (const a of ADAPTERS) {
|
|
826
|
+
if (a.id === "agents-md" || (await a.detectPresence(root)).detectedInProject) selected.push(a);
|
|
827
|
+
}
|
|
828
|
+
return selected;
|
|
829
|
+
}
|
|
830
|
+
async function runPipeline(opts) {
|
|
831
|
+
const root = opts.root;
|
|
832
|
+
const finalDir = athenaDir(root);
|
|
833
|
+
const notes = [];
|
|
834
|
+
const prev = await readState(finalDir);
|
|
835
|
+
if (prev.kind === "corrupted" && !opts.dryRun) {
|
|
836
|
+
const backup = await backupCorruptedState(finalDir);
|
|
837
|
+
notes.push(`state.json was corrupted (${prev.reason}); ${backup ? `moved to ${path2.relative(root, backup)}` : "it will be replaced"} and rebuilt.`);
|
|
838
|
+
}
|
|
839
|
+
const prevState = prev.kind === "ok" ? prev.state : null;
|
|
840
|
+
const fresh = !await exists(finalDir);
|
|
841
|
+
const salt = fresh && !opts.dryRun ? crypto.randomBytes(32).toString("hex") : void 0;
|
|
842
|
+
opts.onStage?.("analyze");
|
|
843
|
+
const analysis = await analyzeProject(root, { signal: opts.signal, onStage: (s) => opts.onStage?.(s), fingerprintSalt: salt });
|
|
844
|
+
opts.signal?.throwIfAborted();
|
|
845
|
+
const adapters = opts.agents ?? (prevState ? ADAPTERS.filter((a) => prevState.agents[a.id]?.configured) : await autoSelectAdapters(root));
|
|
846
|
+
if (opts.dryRun) {
|
|
847
|
+
const agentChanges2 = [];
|
|
848
|
+
for (const adapter of adapters) agentChanges2.push({ adapter, changes: await adapter.plan({ root, projectName: analysis.model.name }) });
|
|
849
|
+
return { analysis, docs: [], agentChanges: agentChanges2, state: null, notes };
|
|
850
|
+
}
|
|
851
|
+
const workDir = fresh ? path2.join(root, `${ATHENA_DIR}.tmp-${crypto.randomBytes(4).toString("hex")}`) : finalDir;
|
|
852
|
+
let docs;
|
|
853
|
+
try {
|
|
854
|
+
opts.onStage?.("write");
|
|
855
|
+
await fs.mkdir(workDir, { recursive: true });
|
|
856
|
+
const previousBlocks = prevState ? Object.fromEntries(Object.entries(prevState.documents).map(([k, v]) => [k, v.blocks])) : void 0;
|
|
857
|
+
docs = await writeKnowledge(workDir, analysis.model, { force: opts.force, previousBlocks });
|
|
858
|
+
docs.push(await ensureRules(workDir, analysis.model));
|
|
859
|
+
await writeFileAtomic(path2.join(workDir, "model.json"), `${JSON.stringify(analysis.model, null, 2)}
|
|
860
|
+
`);
|
|
861
|
+
await ensureGitignoreEntries(path2.join(workDir, ".gitignore"));
|
|
862
|
+
if (salt) await writeFileAtomic(path2.join(workDir, LOCAL_CONFIG_FILE), `${JSON.stringify({ salt }, null, 2)}
|
|
863
|
+
`);
|
|
864
|
+
opts.signal?.throwIfAborted();
|
|
865
|
+
if (fresh) await fs.rename(workDir, finalDir);
|
|
866
|
+
} catch (err) {
|
|
867
|
+
if (fresh) await fs.rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
868
|
+
});
|
|
869
|
+
throw err;
|
|
870
|
+
}
|
|
871
|
+
await persistAnalysisCache(analysis, finalDir);
|
|
872
|
+
opts.onStage?.("agents");
|
|
873
|
+
const agentChanges = [];
|
|
874
|
+
const agentsState = { ...prevState?.agents ?? {} };
|
|
875
|
+
for (const adapter of adapters) {
|
|
876
|
+
try {
|
|
877
|
+
const planned = await adapter.plan({ root, projectName: analysis.model.name });
|
|
878
|
+
await applyChanges(root, planned);
|
|
879
|
+
agentChanges.push({ adapter, changes: planned });
|
|
880
|
+
agentsState[adapter.id] = { configured: true, files: planned.map((p) => p.path), configuredAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
881
|
+
} catch (err) {
|
|
882
|
+
notes.push(`${adapter.displayName} integration not configured: ${err.message}`);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const state = composeState({
|
|
886
|
+
prevState,
|
|
887
|
+
analysis,
|
|
888
|
+
docs,
|
|
889
|
+
agents: agentsState,
|
|
890
|
+
fileIndex: await indexWithIntegrations(root, analysis.files, agentChanges.flatMap((a) => a.changes.map((c) => c.path)))
|
|
891
|
+
});
|
|
892
|
+
await writeState(finalDir, state);
|
|
893
|
+
return { analysis, docs, agentChanges, state, notes };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export {
|
|
897
|
+
planKnowledge,
|
|
898
|
+
writePlannedKnowledge,
|
|
899
|
+
ensureRules,
|
|
900
|
+
indexWithIntegrations,
|
|
901
|
+
composeState,
|
|
902
|
+
runPipeline
|
|
903
|
+
};
|
|
904
|
+
//# sourceMappingURL=chunk-SJOOXO34.js.map
|