Reasecify (Reasoning + Security + Graph) connects all of an organization's available data — infrastructure-as-code, design docs, and the decisions behind them — into one place. By combining Graph-Based Infrastructure Ingestion, Dynamic Knowledge Base (Wiki) Linking, and Domain-Specific LoRA Fine-Tuning, Reasecify creates a dedicated AI companion that internalizes your organization's unique architecture and security decisions.
┌───────────────────────────┐
│ Discover │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Clone │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Ingest │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Knowledge Graph + Wiki │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Chat (Claude, grounded) │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Generate training data │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Fine-tune (LoRA) │
└───────────────────────────┘
│
▼
┌───────────────────────────┐
│ Chat (fine-tuned model) │
└───────────────────────────┘
-
Infrastructure Knowledge Graph — Parses IaC (Terraform) to map dependencies, cross-repo references, and resource connections into a unified typed graph.
-
Unified Knowledge Base (Wiki) — Ingests Confluence design docs alongside code, linking architecture decisions directly to live infrastructure nodes without siloed context.
-
Fine-Tuned Domain Intelligence — Generates synthetic training datasets straight from your architecture graph to LoRA fine-tune local open-weight models (Llama 3), turning your team's design rationale into model weights.
-
Dual-Engine Chat — Features grounded retrieval-augmented generation (RAG) for instant, context-verified queries, alongside fine-tuned local model inference for native architecture recall.
Each backbone on its own falls short:
- A plain RAG setup retrieves out of context in many cases — a single pass over unstructured text misses the relationships that live in the structure of the infrastructure itself, not just its words.
- A plain wiki-LLM setup doesn't scale — it needs all the relevant data handed to it fresh for every single question, with nothing carried over or connected between them.
- Fine-tuning alone on raw data can mislead — a model trained straight on undigested text can pick up noise and stale detail as if it were fact.
Reasecify combines all three backbones into one streamlined process, so you get the best of each: real answers grounded in your organization's own facts, a working understanding of your security architecture, and a head start on finding and fixing the next issue.
Reasecify is a guided, one-page pipeline (workflow/server.py) that walks
through all of this in order:
- Discover — starting from one repo you know, find the other repos in its org that are actually relevant to a topic, so you're not manually hunting for what to pull in.
- Clone — pull the relevant repos down locally.
- Ingest — parse every cloned repo's infrastructure-as-code and any linked design docs into one typed knowledge graph — the "wiki" and the "graph" becoming one connected structure, not two separate silos.
- Chat (Claude, grounded) — ask questions against that graph immediately, with real answers grounded in retrieval, before any fine-tuning happens — a sanity check that the ingested knowledge is actually useful.
- Generate training data — turn the graph's design rationale into a real training set: an LLM proposes domain-specific "facts" worth teaching, then expands each into paraphrased training examples.
- Fine-tune — LoRA fine-tune a local model on that training set, so the organization's own reasoning becomes part of the model's weights, not just something it can look up.
- Chat with the fine-tuned model — talk to the result directly, with no retrieval or grounding involved, to see whether the fine-tuned model actually recalls the trained facts on its own.
Steps 1–4 already work standalone as a "know what exists and why" tool. Steps 5–7 are what turn that knowledge into a model that has internalized it — the fine-tuning half of Reasecify's name.
This is a first-pass prototype: the ingest/discovery/query pieces have no LLM calls at all (pure parsing and graph traversal); the workflow module above is what layers chat, training-data generation, and fine-tuning on top of that same graph.
Point ingest.py at a folder. It walks the folder recursively and, by file
shape (not by fixed subfolder names):
- Parses any
.tffile it finds with a small hand-rolled scanner (seeparsers/terraform.py) — nopython-hcl2dependency. It pulls outresource/module/variable/outputblock headers and attribute references (type.name.attr,module.name.attr,var.name). This is a line-oriented scanner, not a full HCL2 grammar parser — it's good enough to build a graph of "what refers to what," but it doesn't evaluate expressions or understanddynamic/for_eachsemantics. - Parses any
.jsonfile that looks like a fetched Confluence page (has abody.storageorbody.viewkey) — seeparsers/confluence.py. Strips the HTML and keeps the plain text as adesign_docnode. - Links Terraform repos to each other via
moduleblocksourcestrings (full significant-word-subset match against known repo directory names, after dropping generic vendor/cloud words like "tf"/"google"/"module" — a registry source string usually doesn't literally contain the repo's directory name, but shares its distinctive words; matching requires every one of a repo's significant words to appear in the source, not just some fixed-count overlap, to avoid confusing two differently-named repos that happen to share a few words). When no cloned repo's words are fully contained in a module's source, the call links to a synthesizedexternal_moduleplaceholder node instead of guessing or silently dropping the edge — the dependency is real even when its target isn't in this ingest's source folder. - Treats each directory under a Terraform repo as its own root
module/workspace (matching how
dev/,stg/,prd/environment folders are typically structured) — so identically-named blocks declared in different environments become distinct nodes (labeled[dev]/[stg]/[prd], etc.) instead of collapsing into one, and avar.x/module.xreference in one environment's files never resolves against another environment's declaration of the same name. - Links design docs to Terraform nodes via keyword overlap (2+ shared keywords, or one shared keyword of 8+ characters, between the doc's text and a node's name/label) — no embeddings/LLM in v1.
Output goes to output/ (gitignored — regenerate anytime):
graph.json— the full node/edge graph. This is the schema/storage format other tooling (query.py, and a futurelintstep) builds on.GRAPH_SUMMARY.md— quick human-readable counts, for sanity-checking a run without opening the viewer.graph.html— a single self-contained viewer. Vanilla JS +<canvas>, a small hand-rolled force-directed layout, no CDN scripts, no build step. The graph data is inlined directly into the HTML at generation time, so you can just double-click the file open — no server, no CORS issues. Each of the 8 node types gets both a distinct marker shape and a color (validated with the dataviz skill's palette checker) so identity never depends on color alone; a legend, hover tooltips, and a table-view fallback are all part of the same reason.
python ingest.py --source /path/to/folder --out output
open output/graph.htmlquery.py is the second operation in the ingest/query/lint model — it
reads graph.json and answers real questions instead of requiring you to pan
around the canvas viewer. --graph defaults to output/graph.json and works
either before or after the subcommand.
Every command takes a loose node reference — an id, an exact name, or a
substring — never the full type:path/to/thing id. An ambiguous or
unresolvable reference is a hard error (non-zero exit), never a silent guess.
# "I don't know the exact name" — substring search over name/label
python query.py find web_service --graph output/graph.json
# Full detail for one node: type, source, attrs, edge counts by type
python query.py show web_service_001 --graph output/graph.json
# What's connected to this node (1 hop out, by default)
python query.py neighbors web_service_001 --graph output/graph.json
# What a design doc covers / what covers a piece of infra — both directions
# of `documents` edges
python query.py documents "stickiness" --graph output/graph.json
# Shortest connection between two nodes, any edge type, either direction
python query.py path web_service_001 "tf-google-foundations-network-gateway-compute-module" --graph output/graph.jsonEvery subcommand also accepts --json for machine-readable output — the seam
for a future free-form natural-language layer (pipe a resolved subgraph to an
LLM), not built yet.
ingest.py/query.py assume you already know which repos to point at.
discovery/ answers a different question: given one repo you already have
locally and a topic, which other repos in its GitHub org are worth pulling
in? It chases the root repo's Terraform module source strings — the same
signal ingest.py's calls_module linking uses — ranks them by relevance to
the topic, and resolves the relevant ones against the org's actual repo
list.
export GITHUB_PAT=... # required — a token with read access to the org
export GITHUB_HOST=git.example.com # omit for public github.com
python discovery/server.py --port 8765
open http://localhost:8765Fill in a local path to an already-cloned repo, how many repos to return, and pick one of two modes:
- By topic (default) — also fill in a topic (free text — "proxy farm
stickiness"). It:
- lists every repo in the org (org auto-detected from the root repo's
.git/configorigin remote, or passorgexplicitly in the form), - reads the root repo's Terraform
moduleblocks, - scores each module against the topic by shared-word overlap — a module
with nothing in common with the topic (a generic
auth/loggingmodule, say) scores zero and drops out on its own; no hardcoded "generic module" list needed, - resolves the surviving modules'
sourcestrings against the org's repo list (same significant-word-subset rule ascalls_module, just matched against the org's live repo list instead of already-cloned directory names), - returns the top N, each with the module that pointed at it and the words that made it relevant — never just a bare score. A "Show all scanned modules (debug)" checkbox reveals every module_call found in the root repo, including the zero-score ones that never made it past step 3 — so "why didn't module X show up" always has a visible answer instead of a silent gap.
- lists every repo in the org (org auto-detected from the root repo's
- All referenced repos — no topic needed. Skips step 3 entirely: every module_call in the root repo is resolved against the org's repo list, regardless of relevance to anything. Useful for "what does this repo reference in this org, period" rather than chasing one topic. In this mode the debug checkbox is redundant — nothing was filtered out before resolution, so the main results already show everything.
You can also hit POST /api/discover directly ({"root_repo", "mode", "topic", "max_repos", "org"} → JSON) without the browser UI — topic is
required when mode is "topic" (the default) and ignored/optional when
mode is "all".
Deliberately out of scope for this pass: discovered repos are only
listed, never cloned; there's no graph/KB generation from them yet (a doc
mentions using this as an input to a per-topic ingest.py run later, but
that wiring doesn't exist); it's Terraform-only (same limitation as
ingest.py's parsing); and it's scoped to one GitHub org, not a
cross-org/dependency-transitive crawl. (The workflow/ module below wires
this into clone+ingest+chat — discovery/ itself is unchanged and works
standalone exactly as described above.)
Code used by more than one of the modules above (or by workflow/) lives in
common/, not duplicated per-module:
common/net.py—build_ssl_context(insecure_env_var), the same verified-by-default/opt-out-by-env-var TLS policydiscovery/already used, factored out socommon/llm_client.pyshares it instead of copy-pasting it.common/llm_client.py— a stdlib (urllib) Anthropic Messages API client:ask(system_prompt, messages, max_tokens=2048) -> str. Hand-rolled rather than adding theanthropicSDK as a dependency, to keep this project stdlib-only outside offinetune/(same spirit asdiscovery/list_github_repos.pyhand-rolling a GitHub client instead of adding a GitHub SDK dependency). ReadsANTHROPIC_AUTH_TOKEN/ANTHROPIC_BASE_URL/ANTHROPIC_MODELdirectly from the environment — point these at whatever gateway or account you use via your own.env, they're never hardcoded here. TLS opt-out (rare, internal-host-only) viaANTHROPIC_INSECURE_TLS=1.common/clone.py—clone_repo(full_name, html_url, dest_dir, pat, host), a smallgit clonewrapper viasubprocess(no GitPython), authenticating against a GitHub Enterprise host via this project's ownGITHUB_PAT/GITHUB_HOSTenv vars. Idempotent — skips withstatus: "already_present"if the destination already has a.gitdirectory.common/context.py—build_context(index, user_message, top_k=15) -> str, the retrieval step for chat. Scores every graph node against the latest message usingdiscovery/relevance.py's existing tokenize/overlap scoring (no new scoring rule, no embeddings), and renders the top matches as text — including a snippet read straight off disk around a node'ssource.linewhen that file is still present locally (cheap, since a repo is already cloned locally by the time chat is reachable).
discovery/server.py only ever answers "which repos are relevant" and
ingest.py/query.py still assume you drive cloning and ingestion by hand.
workflow/ is a separate module that orchestrates all of the above
(plus common/ and finetune/) into one guided page — it imports
discovery/harness.py, ingest.py, query.py, common/, and finetune/
directly as libraries; it does not modify any of them, and
discovery/server.py keeps working standalone, unchanged, on its own port.
export GITHUB_PAT=... # required — same var discovery/ uses
export GITHUB_HOST=git.example.com # omit for public github.com
export ANTHROPIC_AUTH_TOKEN=... # required for the chat step and Generate training data
export ANTHROPIC_BASE_URL=... # optional, has a default
export ANTHROPIC_MODEL=... # optional, has a default
pip install -r requirements.txt # only needed for Fine-tune/Chat-with-fine-tuned-model
python workflow/server.py --port 8766
open http://localhost:8766The page walks through seven steps, gated in order:
- Discover — same
run_discoverycalldiscovery/server.pyuses (by topicorall referenced reposmode), against a root repo and a workspace directory you provide. - Clone — check which discovered repos to keep, then "Clone selected"
clones each into
<workspace>/clones/. Authenticates with this project's ownGITHUB_PAT/GITHUB_HOST. Re-clicking is safe: an already-cloned repo is skipped, not re-cloned. - Ingest — "Run ingest" builds the graph from the root repo plus
every successfully cloned repo (
ingest.run_ingesttakes a list of source roots, not just one) into<workspace>/output/graph.json. This is the step where the "wiki" (linked design docs) and the "graph" (infra-as-code) actually merge into one connected structure. - Chat (Claude, grounded) — once ingest succeeds, ask questions in a
chat box. Each question is answered by retrieving relevant graph nodes
(
common/context.py) and sending them as grounding context to Claude (common/llm_client.py) alongside the conversation so far. The server holds no session state for this step — the browser resends the growinghistoryeach turn, matching every other server in this project's "server does one request, browser holds the state" convention. - Generate training data — asks Claude to propose a handful of
design-rationale "facts" from the ingested graph (design docs + infra
nodes), then expands each into several paraphrased Q&A pairs, repeated
several times, and writes
train_dataset.jsonl+eval_questions.jsonlunder<workspace>/output/finetune/— seefinetune/below. Both files get a clickable link once generated. - Fine-tune — LoRA fine-tune a local base model (default
meta-llama/Llama-3.1-8B-Instruct) ontrain_dataset.jsonl. Shows whether the base model is already in the local Hugging Face cache before you start (finetune/model_cache.py), then runs training in a background thread while the page polls/api/finetune/statusfor live progress (phase/step/loss). - Chat with the fine-tuned model — a second chat panel, stacked
directly below Step 4's, that talks to a fine-tuned adapter instead of
Claude — no retrieval/grounding involved, since the point is testing
whether the fine-tuned weights recall the trained facts. A picker
above the chat log lists every fine-tuning run recorded for the current
workspace (
finetune/runs.py's on-disk manifest under<workspace>/output/finetune/runs/), including runs from previous server processes — not just whatever the current browser session just trained — so you can come back later and pick an older adapter without re-running Step 6.
POST /api/discover, /api/clone, /api/ingest, /api/chat,
/api/finetune/generate_data, /api/finetune/check_model,
/api/finetune/start, and /api/finetune/chat (plus GET /api/finetune/status, /api/finetune/view_file, and /api/finetune/runs)
on this server can also be hit directly with curl — each returns a clean
JSON {"error": "..."} on a bad request rather than a stack trace.
Known v1 limitation: chat retrieval is per-turn and latest-message-only — each new question re-scores the graph from just that message's words, not the whole conversation. A follow-up like "tell me more about that" won't automatically re-pull the prior turn's matched nodes; it relies on the LLM's own memory of what it already said, not on retrieval seeing the earlier question's context again.
This is where the "combine the wiki and the graph to fine-tune" idea
actually turns into a model: it's the one part of this project that isn't
stdlib-only (see requirements.txt: torch, transformers, peft,
accelerate, huggingface_hub), and the one place workflow/server.py
keeps real server-side state (a background job-status dict for the
fine-tune step, and a process-wide loaded-model cache) instead of the
"browser holds all the state" convention used everywhere else — an
8B-parameter model and a multi-minute training run can't be pushed through
a "browser resends everything" request/response cycle.
Design and hyperparameters are ported unchanged from a standalone
proof-of-concept that validated the approach end to end on a real ingested
graph plus a linked Confluence design doc: LoRA (r=16, alpha=32,
dropout=0.05, all attention + MLP projections), num_train_epochs=1 with
repeat-exposure baked into the dataset itself instead (raising epochs would
multiply that exposure again — see finetune/train.py's module
docstring), greedy decoding at inference time for reproducible eval.
finetune/errors.py—FinetuneError, same role asDiscoveryError/IngestError/CloneError/ChatError.finetune/dataset_gen.py—generate_training_data(index, out_dir, num_facts=3, paraphrases=6, repeats=5). The one call in this package that uses an LLM (common/llm_client.ask, not a local model) — proposing which facts are worth teaching and how to paraphrase them is a judgment call, not something worth hand-rolling scoring rules for.finetune/model_cache.py—is_model_cached(model_id), a stdlib-only (nohuggingface_hubimport) check of whether a model is already in the local Hugging Face hub cache, so the UI can warn before kicking off a multi-GB download.finetune/train.py—run_finetune(dataset_path, model_id, out_dir, on_progress), LoRA training with phase/step/loss progress callbacks.workflow/server.pypasses a per-runout_dir(<runs_dir>/<job_id>, seefinetune/runs.py) rather than one fixed path, so every run's adapter survives later runs instead of being overwritten by them.finetune/inference.py—get_or_load(model_id, adapter_dir)(loads + merges the adapter once per process, then caches it) andgenerate_reply(model, tokenizer, message, history)(greedy decode, no retrieval/grounding).finetune/runs.py— stdlib-only durable record of past fine-tuning runs: a JSON manifest at<workspace>/output/finetune/runs/manifest.json, appended to when a run starts and updated in place when it finishes or fails. Backs Step 7's run picker and theGET /api/finetune/runsroute — the one place in this feature that needs to survive aworkflow/server.pyrestart, since_finetune_jobs(the noisy per-step training log a running job's poll loop reads) stays in-memory only.
Generated artifacts (datasets, eval questions, adapters, training logs, the
runs manifest) are gitignored under finetune/ — regenerate anytime;
nothing under <workspace>/output/finetune/ should be committed.
- Terraform parsing is regex/brace-based, not a real HCL parser — it can be
fooled by unusual formatting (e.g. a
resourcekeyword appearing inside a string or comment). Good enough for "what talks to what," not for anything that needs to be semantically exact. - Each directory under a repo is treated as its own flat root module — a
reasonable approximation of real
dev/stg/prd-style layouts, but it will over-split a repo that genuinely shares declarations across directories viaterraform_remote_stateor symlinks (not modeled). calls_modulematching requires every one of a repo's significant words to appear in the module's source string — deliberately strict to avoid cross-linking two differently-named repos, at the cost of missing a real match if a repo's directory name and its registry path diverge more than that (e.g. a repo renamed after publishing). A source with no confident match becomes anexternal_moduleplaceholder node rather than a silent drop or a guess.- Doc-to-infra linking is keyword overlap on node names (2+ shared keywords, or one shared keyword of 8+ characters) — no ranking, no disambiguation between similarly-named things across repos.
- No incremental/re-ingest logic yet — every run rebuilds the graph from scratch. Staleness/drift between a doc and the infra it describes is a real failure mode this doesn't detect yet; that's deliberately out of scope until this first pass is validated.
query.pycovers structured traversal only — no free-form natural-language questions yet (the--jsonflag is the seam for that later) and nolint/ drift detection (e.g. doc-mentions-deleted-infra, infra-with-no-doc).discovery/inheritscalls_module's strict word-subset matching (see above) when resolving a modulesourceagainst the org's repo list, so the same tradeoff applies: a real match is missed if a repo's directory name and its registry path diverge more than expected, rather than risking a wrong guess. It's also single-org and Terraform-only. Used on its own (discovery/server.py) it still stops at "here's the ranked list" — no cloning or KB generation;workflow/server.pyis what takes a discovered list the rest of the way to clone+ingest+chat.workflow/'s chat is retrieval-by-keyword-overlap only (seecommon/context.pyabove) — no embeddings, no re-ranking across turns — and it never fetches file content from GitHub for chat context, only from a repo already cloned to local disk in the same session. Nothing inworkflow/modifiesdiscovery/server.py, which remains fully usable on its own.finetune/'s dataset generation asks Claude to propose facts and paraphrases in one call with no held-out review step — a bad or hallucinated proposal trains straight into the adapter. There's also no UI for actually runningeval_questions.jsonl(it's generated for parity with the proof-of-concept this was ported from, but nothing in the workflow page runs it), and fine-tuning itself is single-GPU/MPS/CPU, one job at a time —_finetune_jobshas no queue or concurrency limit beyond "one background thread per Start click."
