loki-mode 7.12.0 → 7.14.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/SKILL.md +4 -2
- package/VERSION +1 -1
- package/autonomy/lib/wiki-ask.py +137 -0
- package/autonomy/lib/wiki-generator.py +322 -0
- package/autonomy/lib/wiki_index.py +258 -0
- package/autonomy/lib/wiki_llm.py +140 -0
- package/autonomy/loki +304 -11
- package/autonomy/run.sh +62 -12
- package/bin/loki +1 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +202 -0
- package/dashboard/static/index.html +405 -329
- package/docs/INSTALLATION.md +1 -1
- package/docs/R5-AUTO-WIKI-DESIGN.md +137 -0
- package/docs/R6-ROLLBACK-CHECKPOINT-PLAN.md +107 -0
- package/loki-ts/dist/loki.js +245 -206
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
package/SKILL.md
CHANGED
|
@@ -3,7 +3,7 @@ name: loki-mode
|
|
|
3
3
|
description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.
|
|
6
|
+
# Loki Mode v7.14.0
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
@@ -160,6 +160,8 @@ GROWTH ──[continuous improvement loop]──> GROWTH
|
|
|
160
160
|
| `.loki/signals/HUMAN_REVIEW_NEEDED` | Never | When human decision required |
|
|
161
161
|
| `.loki/state/checkpoints/` | After task completion | Automatic + manual via `loki checkpoint` |
|
|
162
162
|
|
|
163
|
+
One-command rollback (v7.5.2+): `loki rollback latest` or `loki rollback to <id>` restores `.loki/` state from a checkpoint. It first captures a forced pre-rollback snapshot of the current state and prints its id, so a rollback is itself undoable (`loki rollback to <that-id>`). Use `loki rollback list` to see checkpoints.
|
|
164
|
+
|
|
163
165
|
---
|
|
164
166
|
|
|
165
167
|
## Module Loading Protocol (Skills)
|
|
@@ -381,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
|
|
|
381
383
|
|
|
382
384
|
---
|
|
383
385
|
|
|
384
|
-
**v7.
|
|
386
|
+
**v7.14.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.
|
|
1
|
+
7.14.0
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""wiki-ask.py -- grounded, cited codebase Q&A for the R5 auto-wiki.
|
|
3
|
+
|
|
4
|
+
Answers a natural-language question about the project, grounded in the indexed
|
|
5
|
+
codebase. Citations are file:line and ALWAYS point at real code:
|
|
6
|
+
|
|
7
|
+
1. Deterministically retrieve the top-K relevant chunks (token overlap).
|
|
8
|
+
2. Show the LLM NUMBERED chunks and tell it to cite by index only ([1]..[K]).
|
|
9
|
+
3. Map each [n] back to the real {file,start_line} of the chunk we supplied.
|
|
10
|
+
4. Validate every citation against the filesystem; drop non-resolving ones.
|
|
11
|
+
|
|
12
|
+
A fabricated citation is structurally impossible: the model can only reference
|
|
13
|
+
chunks we handed it, and only those that resolve on disk survive.
|
|
14
|
+
|
|
15
|
+
LLM is mocked via LOKI_WIKI_LLM_STUB (see wiki_llm.py) so CI makes no paid calls.
|
|
16
|
+
With no provider and no stub, an EXTRACTIVE answer is returned (top chunk
|
|
17
|
+
snippets with their real citations).
|
|
18
|
+
|
|
19
|
+
Invoked as a subprocess (hyphen in filename):
|
|
20
|
+
python3 wiki-ask.py --root <project> --question "..." [--k N] [--json] [--quiet]
|
|
21
|
+
Exit codes: 0 answered, 2 usage/error, 3 no relevant code found.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import sys
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
33
|
+
if _HERE not in sys.path:
|
|
34
|
+
sys.path.insert(0, _HERE)
|
|
35
|
+
|
|
36
|
+
import wiki_index # noqa: E402
|
|
37
|
+
from wiki_llm import invoke_llm, map_and_validate_citations # noqa: E402
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_or_build_index(root):
|
|
41
|
+
"""Reuse the persisted code-index when present; else build in-memory.
|
|
42
|
+
|
|
43
|
+
The persisted index lacks chunk text (kept small), so for retrieval we
|
|
44
|
+
rebuild the full in-memory index. This stays cheap because build_index is
|
|
45
|
+
dependency-free and capped.
|
|
46
|
+
"""
|
|
47
|
+
return wiki_index.build_index(root)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _build_prompt(question, chunks):
|
|
51
|
+
parts = [
|
|
52
|
+
"Answer the question using ONLY the numbered code excerpts below. "
|
|
53
|
+
"Cite the excerpts you use with their index in square brackets, like "
|
|
54
|
+
"[1] or [3]. Cite by index ONLY. Do NOT write file paths yourself. Do "
|
|
55
|
+
"NOT use emojis. Do NOT use em dashes or en dashes. If the excerpts do "
|
|
56
|
+
"not contain the answer, say so plainly.",
|
|
57
|
+
"",
|
|
58
|
+
"QUESTION: " + question,
|
|
59
|
+
"",
|
|
60
|
+
"CODE EXCERPTS:",
|
|
61
|
+
]
|
|
62
|
+
for i, ch in enumerate(chunks, start=1):
|
|
63
|
+
parts.append("[%d] %s (lines %d-%d):" % (
|
|
64
|
+
i, ch["file"], ch["start_line"], ch["end_line"]))
|
|
65
|
+
parts.append(ch["text"])
|
|
66
|
+
parts.append("")
|
|
67
|
+
return "\n".join(parts)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _extractive_answer(question, chunks):
|
|
71
|
+
"""Deterministic fallback when no LLM is available.
|
|
72
|
+
|
|
73
|
+
Returns prose that references the top chunks by index so the same
|
|
74
|
+
citation-mapping path applies (every [n] resolves to a real chunk).
|
|
75
|
+
"""
|
|
76
|
+
lines = [
|
|
77
|
+
"No language model was available, so here are the most relevant code "
|
|
78
|
+
"locations for your question:",
|
|
79
|
+
"",
|
|
80
|
+
]
|
|
81
|
+
for i, ch in enumerate(chunks, start=1):
|
|
82
|
+
snippet = ch["text"].strip().splitlines()[:3]
|
|
83
|
+
preview = " ".join(s.strip() for s in snippet)[:160]
|
|
84
|
+
lines.append("- [%d] %s" % (i, preview))
|
|
85
|
+
return "\n".join(lines)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv=None):
|
|
89
|
+
ap = argparse.ArgumentParser(description="Grounded cited codebase Q&A.")
|
|
90
|
+
ap.add_argument("--root", default=".")
|
|
91
|
+
ap.add_argument("--question", required=True)
|
|
92
|
+
ap.add_argument("--k", type=int, default=6)
|
|
93
|
+
ap.add_argument("--json", action="store_true", help="emit JSON")
|
|
94
|
+
ap.add_argument("--quiet", action="store_true")
|
|
95
|
+
args = ap.parse_args(argv)
|
|
96
|
+
|
|
97
|
+
root = Path(args.root).resolve()
|
|
98
|
+
if not root.is_dir():
|
|
99
|
+
print("error: not a directory: %s" % root, file=sys.stderr)
|
|
100
|
+
return 2
|
|
101
|
+
|
|
102
|
+
index = _load_or_build_index(root)
|
|
103
|
+
chunks = wiki_index.retrieve(index, args.question, k=max(1, args.k))
|
|
104
|
+
if not chunks:
|
|
105
|
+
if args.json:
|
|
106
|
+
print(json.dumps({"answer": "", "citations": [],
|
|
107
|
+
"note": "no relevant code found"}))
|
|
108
|
+
else:
|
|
109
|
+
print("No relevant code found for that question.")
|
|
110
|
+
return 3
|
|
111
|
+
|
|
112
|
+
prompt = _build_prompt(args.question, chunks)
|
|
113
|
+
raw = invoke_llm(prompt)
|
|
114
|
+
if raw is None or not raw.strip():
|
|
115
|
+
raw = _extractive_answer(args.question, chunks)
|
|
116
|
+
|
|
117
|
+
answer, citations = map_and_validate_citations(raw, chunks, root)
|
|
118
|
+
|
|
119
|
+
if args.json:
|
|
120
|
+
print(json.dumps({
|
|
121
|
+
"question": args.question,
|
|
122
|
+
"answer": answer.strip(),
|
|
123
|
+
"citations": citations,
|
|
124
|
+
}, indent=2))
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
print(answer.strip())
|
|
128
|
+
if citations:
|
|
129
|
+
print("")
|
|
130
|
+
print("Citations:")
|
|
131
|
+
for c in citations:
|
|
132
|
+
print(" %s:%d" % (c["file"], c["line"]))
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__":
|
|
137
|
+
sys.exit(main())
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""wiki-generator.py -- build a per-project cited wiki under .loki/wiki/ (R5).
|
|
3
|
+
|
|
4
|
+
Loki's answer to Devin DeepWiki: an architecture overview, key-modules guide,
|
|
5
|
+
and data-flow section generated from the codebase, where EVERY section cites the
|
|
6
|
+
real source files it was built from.
|
|
7
|
+
|
|
8
|
+
Grounding contract: section citations are CODE-DERIVED. The scanner picks the
|
|
9
|
+
real files and the definition-extractor reads the real def/class/function line
|
|
10
|
+
numbers from those files. The LLM (or template fallback) writes prose only; it
|
|
11
|
+
never invents a citation. Citations are validated against the filesystem before
|
|
12
|
+
being written, so a citation can never point at a file/line that does not exist.
|
|
13
|
+
|
|
14
|
+
Incremental: a signature over (git HEAD + per-file content hashes) is stored in
|
|
15
|
+
wiki-manifest.json. Re-running skips regeneration when the signature is unchanged
|
|
16
|
+
(unless --force). Same cheap-incremental idea as the proof/docs manifests.
|
|
17
|
+
|
|
18
|
+
CI-safe: LOKI_WIKI_LLM_STUB mocks the LLM (see wiki_llm.py); with no provider and
|
|
19
|
+
no stub, a deterministic template wiki is written (citations still real).
|
|
20
|
+
|
|
21
|
+
Invoked as a subprocess (hyphen in filename, like proof-generator.py):
|
|
22
|
+
python3 wiki-generator.py --root <project> [--force] [--quiet]
|
|
23
|
+
Exit codes: 0 generated or up-to-date, 2 usage/error.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import argparse
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
from datetime import datetime, timezone
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
36
|
+
if _HERE not in sys.path:
|
|
37
|
+
sys.path.insert(0, _HERE)
|
|
38
|
+
|
|
39
|
+
import wiki_index # noqa: E402
|
|
40
|
+
from wiki_llm import invoke_llm # noqa: E402
|
|
41
|
+
|
|
42
|
+
# Optional reuse of the proof redactor for a final PII sweep. Repo-relative
|
|
43
|
+
# paths already avoid /Users/<name>, but this is belt-and-suspenders.
|
|
44
|
+
try:
|
|
45
|
+
import proof_redact # noqa: E402
|
|
46
|
+
_HAVE_REDACT = True
|
|
47
|
+
except Exception: # pragma: no cover - redactor optional
|
|
48
|
+
_HAVE_REDACT = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _utc_now():
|
|
52
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _top_modules(index, root, limit=10):
|
|
56
|
+
"""Pick the most significant source files as 'key modules'.
|
|
57
|
+
|
|
58
|
+
Significance = chunk count (proxy for size/centrality). Returns a list of
|
|
59
|
+
{file, defs:[{name,line}], line_count} with REAL definition citations.
|
|
60
|
+
"""
|
|
61
|
+
counts = {}
|
|
62
|
+
for ch in index["chunks"]:
|
|
63
|
+
counts[ch["file"]] = counts.get(ch["file"], 0) + 1
|
|
64
|
+
ranked = sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))[:limit]
|
|
65
|
+
modules = []
|
|
66
|
+
for rel, n in ranked:
|
|
67
|
+
defs = wiki_index.extract_definitions(root, rel, limit=8)
|
|
68
|
+
modules.append({
|
|
69
|
+
"file": rel,
|
|
70
|
+
"approx_lines": n * wiki_index.CHUNK_LINES,
|
|
71
|
+
"defs": defs,
|
|
72
|
+
})
|
|
73
|
+
return modules
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _entry_points(root, files):
|
|
77
|
+
"""Detect likely entry-point files from the scanned set (real files only)."""
|
|
78
|
+
candidates = [
|
|
79
|
+
"src/index.ts", "src/index.js", "src/main.ts", "src/main.js",
|
|
80
|
+
"index.ts", "index.js", "main.ts", "main.js", "server.ts", "server.js",
|
|
81
|
+
"src/app.ts", "src/app.js", "app.py", "main.py", "manage.py",
|
|
82
|
+
"src/main.py", "__main__.py", "main.go", "cmd/main.go",
|
|
83
|
+
"src/main.rs", "src/lib.rs", "bin/loki",
|
|
84
|
+
]
|
|
85
|
+
fileset = set(files)
|
|
86
|
+
return [c for c in candidates if c in fileset]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _llm_prose(section, context, fallback):
|
|
90
|
+
"""Get prose for a section from the LLM, or use the deterministic fallback."""
|
|
91
|
+
prompt = (
|
|
92
|
+
"You are documenting a software project for a team wiki. Write a clear, "
|
|
93
|
+
"factual " + section + " section in markdown. Be concise (under 250 words). "
|
|
94
|
+
"Do NOT use emojis. Do NOT use em dashes or en dashes. Do NOT invent file "
|
|
95
|
+
"paths. Output ONLY the markdown prose, no headings, no citations.\n\n"
|
|
96
|
+
"PROJECT CONTEXT:\n" + context
|
|
97
|
+
)
|
|
98
|
+
out = invoke_llm(prompt)
|
|
99
|
+
if out and out.strip():
|
|
100
|
+
return out.strip()
|
|
101
|
+
return fallback
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_context(root, index, modules, entries):
|
|
105
|
+
name = Path(root).name
|
|
106
|
+
lines = [
|
|
107
|
+
"PROJECT: %s" % name,
|
|
108
|
+
"SOURCE FILES INDEXED: %d" % len(index["files"]),
|
|
109
|
+
"ENTRY POINTS: %s" % (", ".join(entries) or "none detected"),
|
|
110
|
+
"TOP MODULES:",
|
|
111
|
+
]
|
|
112
|
+
for m in modules[:8]:
|
|
113
|
+
lines.append(" - %s (~%d lines)" % (m["file"], m["approx_lines"]))
|
|
114
|
+
# Top-level dirs.
|
|
115
|
+
dirs = {}
|
|
116
|
+
for f in index["files"]:
|
|
117
|
+
top = f.split("/")[0] if "/" in f else "(root)"
|
|
118
|
+
dirs[top] = dirs.get(top, 0) + 1
|
|
119
|
+
lines.append("DIRECTORIES:")
|
|
120
|
+
for d, n in sorted(dirs.items(), key=lambda kv: -kv[1])[:12]:
|
|
121
|
+
lines.append(" - %s/ (%d files)" % (d, n))
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _section_overview(root, index, modules, entries, context):
|
|
126
|
+
name = Path(root).name
|
|
127
|
+
fallback = (
|
|
128
|
+
"%s is a software project with %d indexed source files. "
|
|
129
|
+
"Entry points: %s. The largest modules by size are listed below."
|
|
130
|
+
% (name, len(index["files"]), ", ".join(entries) or "not detected")
|
|
131
|
+
)
|
|
132
|
+
prose = _llm_prose("architecture overview", context, fallback)
|
|
133
|
+
# Code-derived citations: the entry points + top modules (real files).
|
|
134
|
+
citations = []
|
|
135
|
+
for e in entries[:3]:
|
|
136
|
+
citations.append({"file": e, "line": 1})
|
|
137
|
+
for m in modules[:4]:
|
|
138
|
+
line = m["defs"][0]["line"] if m["defs"] else 1
|
|
139
|
+
citations.append({"file": m["file"], "line": line})
|
|
140
|
+
return {"id": "architecture", "title": "Architecture Overview",
|
|
141
|
+
"body": prose, "citations": citations}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _section_modules(root, modules, context):
|
|
145
|
+
fallback_lines = ["The following modules carry most of the codebase:"]
|
|
146
|
+
citations = []
|
|
147
|
+
body_parts = []
|
|
148
|
+
prose = _llm_prose(
|
|
149
|
+
"key modules summary",
|
|
150
|
+
context,
|
|
151
|
+
"Key modules of this project, with their primary definitions:",
|
|
152
|
+
)
|
|
153
|
+
body_parts.append(prose)
|
|
154
|
+
body_parts.append("")
|
|
155
|
+
for m in modules:
|
|
156
|
+
defs = m["defs"]
|
|
157
|
+
def_str = ", ".join("%s (L%d)" % (d["name"], d["line"]) for d in defs[:6])
|
|
158
|
+
body_parts.append(
|
|
159
|
+
"- **%s** (~%d lines)%s"
|
|
160
|
+
% (m["file"], m["approx_lines"], (": " + def_str) if def_str else "")
|
|
161
|
+
)
|
|
162
|
+
line = defs[0]["line"] if defs else 1
|
|
163
|
+
citations.append({"file": m["file"], "line": line})
|
|
164
|
+
return {"id": "modules", "title": "Key Modules",
|
|
165
|
+
"body": "\n".join(body_parts), "citations": citations}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _section_data_flow(root, index, entries, context):
|
|
169
|
+
fallback = (
|
|
170
|
+
"Execution begins at the entry point(s) (%s) and flows through the "
|
|
171
|
+
"key modules. Trace a request from the entry file into the modules it "
|
|
172
|
+
"imports to follow the data path." % (", ".join(entries) or "not detected")
|
|
173
|
+
)
|
|
174
|
+
prose = _llm_prose("data flow", context, fallback)
|
|
175
|
+
citations = [{"file": e, "line": 1} for e in entries[:4]]
|
|
176
|
+
if not citations and index["files"]:
|
|
177
|
+
citations = [{"file": index["files"][0], "line": 1}]
|
|
178
|
+
return {"id": "data-flow", "title": "Data Flow",
|
|
179
|
+
"body": prose, "citations": citations}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _validate_citations(root, citations):
|
|
183
|
+
"""Drop any citation that does not resolve to a real file/line on disk."""
|
|
184
|
+
root = Path(root)
|
|
185
|
+
clean = []
|
|
186
|
+
for c in citations:
|
|
187
|
+
rel = c.get("file")
|
|
188
|
+
line = int(c.get("line", 1))
|
|
189
|
+
if not rel:
|
|
190
|
+
continue
|
|
191
|
+
abs_path = root / rel
|
|
192
|
+
try:
|
|
193
|
+
if not abs_path.is_file():
|
|
194
|
+
continue
|
|
195
|
+
with open(abs_path, "r", encoding="utf-8", errors="replace") as f:
|
|
196
|
+
nlines = sum(1 for _ in f)
|
|
197
|
+
except OSError:
|
|
198
|
+
continue
|
|
199
|
+
if 1 <= line <= max(nlines, 1):
|
|
200
|
+
clean.append({"file": rel, "line": line})
|
|
201
|
+
return clean
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _render_md(section):
|
|
205
|
+
lines = ["## %s" % section["title"], "", section["body"], ""]
|
|
206
|
+
if section["citations"]:
|
|
207
|
+
lines.append("**Sources:**")
|
|
208
|
+
for c in section["citations"]:
|
|
209
|
+
lines.append("- `%s:%d`" % (c["file"], c["line"]))
|
|
210
|
+
lines.append("")
|
|
211
|
+
return "\n".join(lines)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv=None):
|
|
215
|
+
ap = argparse.ArgumentParser(description="Generate a cited project wiki.")
|
|
216
|
+
ap.add_argument("--root", default=".", help="project root")
|
|
217
|
+
ap.add_argument("--force", action="store_true", help="regenerate even if unchanged")
|
|
218
|
+
ap.add_argument("--quiet", action="store_true")
|
|
219
|
+
args = ap.parse_args(argv)
|
|
220
|
+
|
|
221
|
+
root = Path(args.root).resolve()
|
|
222
|
+
if not root.is_dir():
|
|
223
|
+
print("error: not a directory: %s" % root, file=sys.stderr)
|
|
224
|
+
return 2
|
|
225
|
+
|
|
226
|
+
def log(msg):
|
|
227
|
+
if not args.quiet:
|
|
228
|
+
print(msg)
|
|
229
|
+
|
|
230
|
+
wiki_dir = root / ".loki" / "wiki"
|
|
231
|
+
manifest_path = wiki_dir / "wiki-manifest.json"
|
|
232
|
+
|
|
233
|
+
signature = wiki_index.compute_signature(root)
|
|
234
|
+
|
|
235
|
+
if manifest_path.is_file() and not args.force:
|
|
236
|
+
try:
|
|
237
|
+
prev = json.loads(manifest_path.read_text())
|
|
238
|
+
except (OSError, json.JSONDecodeError):
|
|
239
|
+
prev = {}
|
|
240
|
+
if prev.get("signature") == signature:
|
|
241
|
+
log("wiki up to date (codebase unchanged); use --force to regenerate")
|
|
242
|
+
return 0
|
|
243
|
+
|
|
244
|
+
log("scanning codebase ...")
|
|
245
|
+
index = wiki_index.build_index(root)
|
|
246
|
+
if not index["files"]:
|
|
247
|
+
print("error: no source files found to document", file=sys.stderr)
|
|
248
|
+
return 2
|
|
249
|
+
|
|
250
|
+
modules = _top_modules(index, root)
|
|
251
|
+
entries = _entry_points(root, index["files"])
|
|
252
|
+
context = _build_context(root, index, modules, entries)
|
|
253
|
+
|
|
254
|
+
log("generating sections (%d files indexed) ..." % len(index["files"]))
|
|
255
|
+
sections = [
|
|
256
|
+
_section_overview(root, index, modules, entries, context),
|
|
257
|
+
_section_modules(root, modules, context),
|
|
258
|
+
_section_data_flow(root, index, entries, context),
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
# Enforce the grounding contract: validate every citation against disk.
|
|
262
|
+
for sec in sections:
|
|
263
|
+
sec["citations"] = _validate_citations(root, sec["citations"])
|
|
264
|
+
|
|
265
|
+
# Optional PII sweep on prose (paths are already repo-relative).
|
|
266
|
+
if _HAVE_REDACT:
|
|
267
|
+
try:
|
|
268
|
+
proof_redact.set_context(repo_root=str(root))
|
|
269
|
+
for sec in sections:
|
|
270
|
+
sec["body"] = proof_redact.redact_value(sec["body"])
|
|
271
|
+
proof_redact.reset_context()
|
|
272
|
+
except Exception:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
wiki_dir.mkdir(parents=True, exist_ok=True)
|
|
276
|
+
generated_at = _utc_now()
|
|
277
|
+
wiki = {
|
|
278
|
+
"schema_version": "1.0",
|
|
279
|
+
"project": root.name,
|
|
280
|
+
"generated_at": generated_at,
|
|
281
|
+
"file_count": len(index["files"]),
|
|
282
|
+
"sections": sections,
|
|
283
|
+
}
|
|
284
|
+
(wiki_dir / "wiki.json").write_text(json.dumps(wiki, indent=2))
|
|
285
|
+
|
|
286
|
+
# Rendered markdown.
|
|
287
|
+
index_md = ["# %s -- Project Wiki" % root.name, "",
|
|
288
|
+
"Auto-generated by Loki (R5). %d source files indexed. "
|
|
289
|
+
"Generated %s." % (len(index["files"]), generated_at), ""]
|
|
290
|
+
for sec in sections:
|
|
291
|
+
index_md.append(_render_md(sec))
|
|
292
|
+
# Per-section file too.
|
|
293
|
+
(wiki_dir / (sec["id"] + ".md")).write_text(_render_md(sec))
|
|
294
|
+
(wiki_dir / "index.md").write_text("\n".join(index_md))
|
|
295
|
+
|
|
296
|
+
# Persist the code index (used by ask + dashboard).
|
|
297
|
+
(wiki_dir / "code-index.json").write_text(json.dumps({
|
|
298
|
+
"root_relative": True,
|
|
299
|
+
"files": index["files"],
|
|
300
|
+
"chunks": [
|
|
301
|
+
{"id": c["id"], "file": c["file"],
|
|
302
|
+
"start_line": c["start_line"], "end_line": c["end_line"]}
|
|
303
|
+
for c in index["chunks"]
|
|
304
|
+
],
|
|
305
|
+
}, indent=2))
|
|
306
|
+
|
|
307
|
+
manifest = {
|
|
308
|
+
"schema_version": "1.0",
|
|
309
|
+
"signature": signature,
|
|
310
|
+
"generated_at": generated_at,
|
|
311
|
+
"file_count": len(index["files"]),
|
|
312
|
+
"sections": [s["id"] for s in sections],
|
|
313
|
+
}
|
|
314
|
+
wiki_dir.mkdir(parents=True, exist_ok=True)
|
|
315
|
+
manifest_path.write_text(json.dumps(manifest, indent=2))
|
|
316
|
+
|
|
317
|
+
log("wiki written to %s/.loki/wiki/ (%d sections)" % (root.name, len(sections)))
|
|
318
|
+
return 0
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
sys.exit(main())
|