In developmentBuilding Nuvakin2026 — ongoing

OASIS

Structured, traceable course data from inconsistent official web pages, with a local LLM kept on a short leash.

Role
Designed and built
Data
Public university web pages
Stack
Python · FastAPI · Ollama (local LLM) · Playwright fallback · unittest · GitHub Actions
Source
Private

Source code is private. This page shows a sanitised architecture, documented decisions and test evidence.

Problem

Problem

Course information is spread across official university pages that are each structured differently and change without notice. Comparing programmes usually means trusting data someone copied by hand, which is often already out of date.

Scraping is the easy part. The hard part is knowing which extracted facts you can trust, and keeping that true as sites change.

Constraints

Constraints

Local-first
Runs on my own machine with a local model through Ollama, with no paid API dependency.
Facts beat fluency
A model may fill gaps but must never overwrite a value the rules already extracted.
Traceable
Official sources only. Raw pages are kept as evidence so every field can be traced back.
One developer
Safety has to come from automated tests and gates, not from someone remembering to check.
Hostile inputs
Some sites render client-side, some block automated access, and none share a structure.

Architecture

Architecture

Ingestion: page to structured recordServing: recommendations through an APIpromote: manual registry editnulls onlyRegistry config per uni (Stored data)Onboarding run sample pipeline pass (Deterministic step)Gap report P0–P3 issues (Deterministic step)Config suggestion human approves diff (Human decision)Regression gate promote/hold/reject (Deterministic step)Discover find course pages (Deterministic step)Fetch static first (Deterministic step)Validate page drop non-course (Deterministic step)Extract evidence saved (Deterministic step)9 field parsers deterministic (Deterministic step)LLM fill only empty fields (Model call)Merge deterministic wins (Deterministic step)Normalise schema + canonical (Deterministic step)Change detection diff vs last run (Deterministic step)Structured records one per programme (Stored data)Matching engine explainable scoring (Deterministic step)Document intake transcript → profile (Deterministic step)API FastAPI (Deterministic step)Review dashboard internal (Deterministic step)End-user product not built yet (Not built)
  1. Registryconfig per uni

    Stored data Per-university settings live in configuration, not code. A university's status (candidate or active) is a field here.

  2. Onboarding runsample pipeline pass

    Deterministic step A new candidate gets a sample run through the real pipeline before anything is trusted.

  3. Gap reportP0–P3 issues

    Deterministic step Missing or suspect fields are classified by severity, so effort goes to what blocks correctness first.

  4. Config suggestionhuman approves diff

    Human decision The system drafts configuration changes. A person approves them; nothing is applied automatically.

  5. Regression gatepromote/hold/reject

    Deterministic step Promotion to active is a manual registry edit, made only after a PROMOTE verdict. Current state: 0 promote, 3 hold, the rest reject.

  6. Discoverfind course pages

    Deterministic step Finds candidate course pages from listing pages using clustering and scoring heuristics.

  7. Fetchstatic first

    Deterministic step Plain HTTP first. A headless browser is a scoped fallback, switched on per configuration, not the default.

  8. Validate pagedrop non-course

    Deterministic step Pages that aren't real course pages are skipped before any evidence is saved.

  9. Extractevidence saved

    Deterministic step Raw HTML and extracted text are stored as evidence, so every field can be traced back to its source page.

  10. 9 field parsersdeterministic

    Deterministic step Config-driven parsers for degree, duration, fees, modules, intakes, campus, study mode, language and entry requirements.

  11. LLM fillonly empty fields

    Model call One narrowly scoped local-model call, given only the fields the parsers left empty.

  12. Mergedeterministic wins

    Deterministic step If a parser found a value, the model's answer is discarded. The LLM can add facts; it can never overwrite them.

  13. Normaliseschema + canonical

    Deterministic step Schema validation plus canonical degree names, durations, intakes, campus names and currency.

  14. Change detectiondiff vs last run

    Deterministic step Each run is compared with the stored record, so changes on a university's site show up as a report.

  15. Structured recordsone per programme

    Stored data One JSON record per programme. Duplicates are quarantined (moved, not deleted) with a manifest.

  16. Matching engineexplainable scoring

    Deterministic step Mandatory criteria plus weighted scoring, with every result explained. It never imports the ingestion code.

  17. Document intaketranscript → profile

    Deterministic step Turns an uploaded transcript into the same profile shape a manual form produces. Isolated from every other module.

  18. APIFastAPI

    Deterministic step Serves universities, courses and recommendations, with OpenAPI docs. It reuses the engine and never duplicates logic.

  19. Review dashboardinternal

    Deterministic step Internal UI for testing profiles against real records. It talks to the API over HTTP only.

  20. End-user productnot built yet

    Not built No public product runs on OASIS yet. It is shown here so the diagram doesn't overstate what exists.

Selected stepSelect a step in the diagram, or focus it and press Enter, to see what it does.
  • Stored data
  • Deterministic step
  • Human decision
  • Model call
  • Not built

Simplified from the repository's architecture document. Module names are generalised and internal paths omitted. Select a node, or use the arrow keys, to read what it does.

  1. Review dashboardtalks to the API over HTTP only
  2. APIFastAPI; reuses the engine, never duplicates it
  3. Recommendation · Document intakeisolated peers; neither imports ingestion
  4. Ingestion pipelinepage → evidence → parsers → LLM gaps → record
  5. Registry and onboarding gatedecides what is trusted
Code dependencies only point downward. Upper layers read the pipeline's output; none of them imports its code, and tests enforce that.

Key decisions

Key decisions

Deterministic first, the LLM last

Nine config-driven parsers run first. One narrowly scoped local-model call sees only the fields they left empty, and the merge step always keeps the deterministic value.

Show reasoning
Context
Field values have to be right, not plausible. LLMs are good at reading messy pages and bad at admitting they don't know.
Options considered
LLM extraction for every field · rules only · rules first, with an LLM for what they miss
Trade-off accepted
Slower to build, field by field, and some fields stay empty rather than guessed.

Isolation enforced by tests, not convention

Static import-analysis tests fail if, for example, the recommendation engine ever imports ingestion code. The dashboard talks to the API over HTTP only.

Show reasoning
Context
Five modules (ingestion, recommendation, document intake, API and dashboard) could easily start reaching into each other.
Options considered
One package with good intentions · separate services · one repo with enforced boundaries
Trade-off accepted
Interfaces have to be designed explicitly, even where a direct import would be quicker.

A gate between “works on a sample” and “live”

An onboarding run produces a severity-ranked gap report. The system drafts config fixes that a person approves, and a regression gate returns PROMOTE, HOLD or REJECT. Promotion itself is a manual edit.

Show reasoning
Context
A new university that parses well on five pages can still be wrong on the fiftieth.
Options considered
Add universities once they run · manual review only · an automated verdict plus a human decision
Trade-off accepted
Growth is slow on purpose: 4 universities are live, and none of the 46 candidates has been promoted yet.

Revisited: a decision I changed or corrected after the evidence moved.

Revisited: no headless browser, then a scoped one

As the candidate list grew and many returned zero courses, the argument for headless got stronger, so I changed my mind. I added it as a config-gated fallback and then measured it against 18 zero-discovery candidates.

Show reasoning
Context
The original design deliberately excluded headless rendering. Pages that only render client-side were out of scope, and one university was left out for exactly that reason.
Options considered
Keep the exclusion · switch to headless everywhere · add a fallback behind configuration
Trade-off accepted
Result: 5 of 18 newly produced courses (3 because of headless, 2 through static discovery on a fresh run), 4 were confirmed blocked, and 9 still found nothing despite a real browser render. The assumption behind the fix was only partly right, and the measurement showed which part.

Quarantine, don't delete

Confirmed duplicates (12 groups, 17 files) were moved to a quarantine folder with a manifest. Two suspected groups turned out to be an extraction bug, which was fixed instead.

Show reasoning
Context
Duplicate programme records appeared after file-naming drift between runs.
Options considered
Delete duplicates · ignore them · move them aside with a manifest
Trade-off accepted
Permanent deletion is still a pending follow-up, but nothing was lost on a false positive.

Implementation

Implementation

  • Python pipeline: discover → fetch (static, with a headless fallback) → validate → extract and save evidence → 9 field parsers → LLM gap-fill → merge → validate and normalise → change detection → store.
  • Explainable matching engine with mandatory criteria and weighted scoring, exposed through a FastAPI service with OpenAPI docs.
  • Document intake that turns a transcript into the same profile shape a manual form produces, isolated from every other module.
  • University registry and onboarding framework: per-university JSON config, gap reports, human-approved config suggestions and the regression gate.

Validation

Validation

858 offline tests
Test methods across 46 files, run with Python's unittest. No real network or LLM calls, and fixtures copied from real fetched pages.
CI on every change
GitHub Actions runs the suite on pushes and pull requests. The most recent run, on 17 Aug 2026, passed.
Boundaries are tests
Layer isolation is checked by static import analysis, not code review.
Where it started
An early internal audit recorded 24 tests in 2 files and no CI. Writing that down is what turned a working demo into a project with a reliability bar.

Results

Results

  • 858offline regression tests across 46 files, run in CICodebase
  • 5isolated layers, with isolation enforced by static import testsCodebase
  • 4 / 50universities live; none of the 46 candidates has passed the promotion gate yetObserved in use
  • Codebase 5 isolated modules, 9 deterministic field parsers, and an API with documented endpoints.
  • Observed in use 4 universities live. 46 candidates in onboarding, of which the gate currently holds 3 and rejects the rest.

Limitations

Limitations

  • No public end-user product runs on OASIS yet.
  • Only 4 universities are live, so every “general” heuristic has been proven against a small set of sites.
  • 9 candidate sites still yield nothing even with a headless browser; the cause is not yet diagnosed.
  • Some sites block automated access outright, and the system respects that rather than working around it.
  • Quarantined duplicates are still awaiting permanent deletion.

Lessons

Lessons

  • Put the model where rules run out, and make sure it can't overwrite them.
  • A gate that says “no” most of the time is doing its job.
  • When you reverse a decision, measure the assumption behind the reversal too.
What I'd do differently
  • Write decision records as decisions happen. The repository's decisions folder is still empty, and the reasoning lives in design documents instead.
  • Test the most heuristic code first. The early audit found that discovery and layout detection, the riskiest parts, had no tests at all.

Where the numbers come from

  • 858 offline regression tests across 46 files, run in CI. Test methods at the latest commit; last CI run passed 17 Aug 2026. Source: private repository, verified 18 Oct 2026
  • 5 isolated layers, with isolation enforced by static import tests. ingestion · recommendation · API · document intake · dashboard. Source: private repository, verified 18 Oct 2026
  • 4 / 50 universities live; none of the 46 candidates has passed the promotion gate yet. Registry status at the latest commit (4 active, 46 candidate). Source: private repository, verified 18 Oct 2026