cite-citadel 0.1.0__tar.gz
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.
- cite_citadel-0.1.0/.claude/skills/verify-example/SKILL.md +153 -0
- cite_citadel-0.1.0/.claude/skills/verify-example/ground-truth.md +142 -0
- cite_citadel-0.1.0/.env.example +4 -0
- cite_citadel-0.1.0/.github/copilot-instructions.md +162 -0
- cite_citadel-0.1.0/.github/workflows/ci.yml +180 -0
- cite_citadel-0.1.0/.github/workflows/pages.yml +54 -0
- cite_citadel-0.1.0/.github/workflows/release.yml +89 -0
- cite_citadel-0.1.0/.gitignore +240 -0
- cite_citadel-0.1.0/AGENT_INGEST.md +6 -0
- cite_citadel-0.1.0/CLAUDE.md +176 -0
- cite_citadel-0.1.0/LICENSE +21 -0
- cite_citadel-0.1.0/PKG-INFO +190 -0
- cite_citadel-0.1.0/README.md +164 -0
- cite_citadel-0.1.0/SCHEMA.md +5 -0
- cite_citadel-0.1.0/citadel/__init__.py +6 -0
- cite_citadel-0.1.0/citadel/__main__.py +17 -0
- cite_citadel-0.1.0/citadel/cli.py +422 -0
- cite_citadel-0.1.0/citadel/config.py +581 -0
- cite_citadel-0.1.0/citadel/extract.py +360 -0
- cite_citadel-0.1.0/citadel/extract_ole.py +222 -0
- cite_citadel-0.1.0/citadel/failures.py +80 -0
- cite_citadel-0.1.0/citadel/grammar.py +216 -0
- cite_citadel-0.1.0/citadel/ingest.py +1701 -0
- cite_citadel-0.1.0/citadel/lint.py +450 -0
- cite_citadel-0.1.0/citadel/llm.py +605 -0
- cite_citadel-0.1.0/citadel/manifest.py +243 -0
- cite_citadel-0.1.0/citadel/okf.py +235 -0
- cite_citadel-0.1.0/citadel/progress.py +204 -0
- cite_citadel-0.1.0/citadel/repo.py +488 -0
- cite_citadel-0.1.0/citadel/rules/README.md +71 -0
- cite_citadel-0.1.0/citadel/rules/core.md +131 -0
- cite_citadel-0.1.0/citadel/rules/formats/image.md +10 -0
- cite_citadel-0.1.0/citadel/rules/formats/office.md +17 -0
- cite_citadel-0.1.0/citadel/rules/formats/pdf.md +22 -0
- cite_citadel-0.1.0/citadel/rules/formats/repo.md +46 -0
- cite_citadel-0.1.0/citadel/rules/genres/email.md +17 -0
- cite_citadel-0.1.0/citadel/rules/genres/first-person.md +34 -0
- cite_citadel-0.1.0/citadel/rules/genres/meeting-minutes.md +95 -0
- cite_citadel-0.1.0/citadel/rules/genres/prose.md +15 -0
- cite_citadel-0.1.0/citadel/rules/schema.md +268 -0
- cite_citadel-0.1.0/citadel/rules/tasks/delete.md +18 -0
- cite_citadel-0.1.0/citadel/rules/tasks/ingest.md +28 -0
- cite_citadel-0.1.0/citadel/rules/tasks/reconcile.md +30 -0
- cite_citadel-0.1.0/citadel/server.py +229 -0
- cite_citadel-0.1.0/citadel/store.py +766 -0
- cite_citadel-0.1.0/citadel/templates/env.example +137 -0
- cite_citadel-0.1.0/citadel/validate.py +249 -0
- cite_citadel-0.1.0/citadel/viewer/__init__.py +363 -0
- cite_citadel-0.1.0/citadel/viewer/app.css +191 -0
- cite_citadel-0.1.0/citadel/viewer/app.js +1100 -0
- cite_citadel-0.1.0/citadel/viewer/template.html +50 -0
- cite_citadel-0.1.0/citadel/workspace.py +70 -0
- cite_citadel-0.1.0/citadel.cmd +6 -0
- cite_citadel-0.1.0/citadel.ps1 +7 -0
- cite_citadel-0.1.0/citadel.toml +2 -0
- cite_citadel-0.1.0/docs/karpathy-llm-wiki.md +74 -0
- cite_citadel-0.1.0/docs/okf-reference.md +70 -0
- cite_citadel-0.1.0/docs/refactor-plan.md +576 -0
- cite_citadel-0.1.0/pyproject.toml +99 -0
- cite_citadel-0.1.0/raw/.gitkeep +0 -0
- cite_citadel-0.1.0/raw/aurora-coffee-blog.md +69 -0
- cite_citadel-0.1.0/raw/coffee-guide.md +66 -0
- cite_citadel-0.1.0/raw/coffee-health-faq.md +81 -0
- cite_citadel-0.1.0/raw/cold-brew-notes.md +113 -0
- cite_citadel-0.1.0/raw/espresso-and-cafe-culture.md +110 -0
- cite_citadel-0.1.0/raw/matcha-and-preparation.md +70 -0
- cite_citadel-0.1.0/raw/tea-guide.md +168 -0
- cite_citadel-0.1.0/raw/tea-health-faq.md +89 -0
- cite_citadel-0.1.0/raw/tea-history-and-trade.md +43 -0
- cite_citadel-0.1.0/raw/thornbury-tea-blog.md +59 -0
- cite_citadel-0.1.0/tests/conftest.py +350 -0
- cite_citadel-0.1.0/tests/test_abbreviations.py +157 -0
- cite_citadel-0.1.0/tests/test_cli.py +401 -0
- cite_citadel-0.1.0/tests/test_extract.py +361 -0
- cite_citadel-0.1.0/tests/test_extract_ole.py +42 -0
- cite_citadel-0.1.0/tests/test_failures.py +44 -0
- cite_citadel-0.1.0/tests/test_failures_catalog.py +147 -0
- cite_citadel-0.1.0/tests/test_grammar.py +168 -0
- cite_citadel-0.1.0/tests/test_ingest_chunking.py +110 -0
- cite_citadel-0.1.0/tests/test_ingest_core.py +417 -0
- cite_citadel-0.1.0/tests/test_ingest_dedup.py +101 -0
- cite_citadel-0.1.0/tests/test_ingest_discovery.py +258 -0
- cite_citadel-0.1.0/tests/test_ingest_lint_store.py +197 -0
- cite_citadel-0.1.0/tests/test_ingest_office_images.py +214 -0
- cite_citadel-0.1.0/tests/test_ingest_progress.py +63 -0
- cite_citadel-0.1.0/tests/test_ingest_provenance.py +230 -0
- cite_citadel-0.1.0/tests/test_ingest_reconcile_delete.py +230 -0
- cite_citadel-0.1.0/tests/test_ingest_staging.py +446 -0
- cite_citadel-0.1.0/tests/test_llm.py +689 -0
- cite_citadel-0.1.0/tests/test_manifest.py +118 -0
- cite_citadel-0.1.0/tests/test_netdrive.py +373 -0
- cite_citadel-0.1.0/tests/test_okf.py +217 -0
- cite_citadel-0.1.0/tests/test_open_points.py +184 -0
- cite_citadel-0.1.0/tests/test_packaging.py +46 -0
- cite_citadel-0.1.0/tests/test_progress.py +160 -0
- cite_citadel-0.1.0/tests/test_repo.py +369 -0
- cite_citadel-0.1.0/tests/test_rules.py +249 -0
- cite_citadel-0.1.0/tests/test_search.py +256 -0
- cite_citadel-0.1.0/tests/test_server.py +379 -0
- cite_citadel-0.1.0/tests/test_sources_index.py +67 -0
- cite_citadel-0.1.0/tests/test_validate.py +140 -0
- cite_citadel-0.1.0/tests/test_viewer.py +413 -0
- cite_citadel-0.1.0/tests/test_workspace.py +346 -0
- cite_citadel-0.1.0/uv.lock +781 -0
- cite_citadel-0.1.0/wiki/.citadel_ingested.json +58 -0
- cite_citadel-0.1.0/wiki/abbreviations/ey-extraction-yield.md +29 -0
- cite_citadel-0.1.0/wiki/abbreviations/index.md +4 -0
- cite_citadel-0.1.0/wiki/abbreviations/tds-total-dissolved-solids.md +27 -0
- cite_citadel-0.1.0/wiki/concepts/arabica-and-robusta.md +42 -0
- cite_citadel-0.1.0/wiki/concepts/aurora-ritual.md +28 -0
- cite_citadel-0.1.0/wiki/concepts/caffeine-in-coffee.md +49 -0
- cite_citadel-0.1.0/wiki/concepts/caffeine-in-tea.md +50 -0
- cite_citadel-0.1.0/wiki/concepts/caffeine.md +42 -0
- cite_citadel-0.1.0/wiki/concepts/coffee-brewing.md +31 -0
- cite_citadel-0.1.0/wiki/concepts/coffee-history-and-trade.md +34 -0
- cite_citadel-0.1.0/wiki/concepts/coffee-processing.md +28 -0
- cite_citadel-0.1.0/wiki/concepts/coffee.md +37 -0
- cite_citadel-0.1.0/wiki/concepts/cold-brew.md +88 -0
- cite_citadel-0.1.0/wiki/concepts/espresso.md +44 -0
- cite_citadel-0.1.0/wiki/concepts/index.md +22 -0
- cite_citadel-0.1.0/wiki/concepts/l-theanine.md +28 -0
- cite_citadel-0.1.0/wiki/concepts/matcha.md +51 -0
- cite_citadel-0.1.0/wiki/concepts/roasting.md +28 -0
- cite_citadel-0.1.0/wiki/concepts/tea-antioxidants-and-tannins.md +34 -0
- cite_citadel-0.1.0/wiki/concepts/tea-brewing.md +39 -0
- cite_citadel-0.1.0/wiki/concepts/tea-growing-and-harvesting.md +26 -0
- cite_citadel-0.1.0/wiki/concepts/tea-history-and-trade.md +64 -0
- cite_citadel-0.1.0/wiki/concepts/tea-types-and-oxidation.md +33 -0
- cite_citadel-0.1.0/wiki/concepts/tea.md +42 -0
- cite_citadel-0.1.0/wiki/index.md +273 -0
- cite_citadel-0.1.0/wiki/objects/aurora-midnight.md +33 -0
- cite_citadel-0.1.0/wiki/objects/canton-mist.md +27 -0
- cite_citadel-0.1.0/wiki/objects/index.md +6 -0
- cite_citadel-0.1.0/wiki/objects/lin-s-evening.md +26 -0
- cite_citadel-0.1.0/wiki/objects/thornbury-lin-breakfast.md +26 -0
- cite_citadel-0.1.0/wiki/open-points/index.md +19 -0
- cite_citadel-0.1.0/wiki/organizations/caffe-aurora.md +44 -0
- cite_citadel-0.1.0/wiki/organizations/index.md +4 -0
- cite_citadel-0.1.0/wiki/organizations/thornbury-and-lin.md +48 -0
- cite_citadel-0.1.0/wiki/persons/edmund-thornbury.md +28 -0
- cite_citadel-0.1.0/wiki/persons/index.md +5 -0
- cite_citadel-0.1.0/wiki/persons/lina-marchetti.md +41 -0
- cite_citadel-0.1.0/wiki/persons/mei-lin.md +30 -0
- cite_citadel-0.1.0/wiki/sources/index.md +16 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: verify-example
|
|
3
|
+
description: End-to-end test of the whole citadel ingest pipeline on the bundled coffee+tea example corpus — ingest raw/ into a fresh wiki, run the structural gates (citadel check + lint), then grade the result against the ground-truth answer key (planted contradictions, repetitions, single-source facts, the one deliberately-false fact, cross-topic links). Use this whenever the user wants to run the e2e or example test, verify or grade the example corpus, (re)build the demo wiki, prove that citations and contradictions still surface, or check that a change to ingest, the rules tree (citadel/rules/), the ingest prompts, or the store still folds the corpus correctly — even if they do not say the word "skill".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Verify the example corpus end-to-end
|
|
7
|
+
|
|
8
|
+
The `raw/` corpus (5 coffee + 5 tea files) is **designed** to stress the three guarantees: facts
|
|
9
|
+
repeat, contradict, hide in one place, vary in style, name fictional people, and include one
|
|
10
|
+
flat-out-false claim. The answer key is `.claude/skills/verify-example/ground-truth.md` — the ingest
|
|
11
|
+
never sees it (it lives outside `raw/`/`wiki/`/`docs/`). This skill runs the real pipeline, then grades
|
|
12
|
+
the wiki against that key. All paths are relative to the repo root.
|
|
13
|
+
|
|
14
|
+
This is a **heavy, intentional** test: Mode A shells out to the LLM ingest CLI (slow, uses your
|
|
15
|
+
subscription). For fast iteration on the grader, use Mode B.
|
|
16
|
+
|
|
17
|
+
## Preconditions
|
|
18
|
+
|
|
19
|
+
- The ingest CLI is installed and logged in (default `claude`; run `claude` once and `/login`). Mode B
|
|
20
|
+
needs no CLI.
|
|
21
|
+
- The 10 example files are present: `ls raw/*.md | wc -l` → should be **10**.
|
|
22
|
+
- Prefer a clean-ish git tree so a stray edit is easy to see (`git status`).
|
|
23
|
+
|
|
24
|
+
## Mode A · Full E2E (ingest + grade)
|
|
25
|
+
|
|
26
|
+
Regenerates the wiki from scratch, so it both **rebuilds the showcase wiki** and tests the pipeline.
|
|
27
|
+
|
|
28
|
+
**1 · Start from an empty wiki** (so every source is ingested fresh — the manifest makes ingest
|
|
29
|
+
idempotent, so a leftover wiki/manifest would skip everything). Move the current wiki aside rather than
|
|
30
|
+
deleting it:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
[ -d wiki ] && mv wiki "/tmp/citadel-wiki-bak.$(date +%s)" || true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**2 · Ingest the whole corpus** (one agentic session per file; minutes, not seconds):
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
time uv run python -m citadel ingest
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Expected: a report ending `... created, ... updated, 0 errors` and **no** "WARNING — broken
|
|
43
|
+
cross-links". 10 sources processed. If a source errored (missing/again-not-logged-in CLI, timeout), fix
|
|
44
|
+
that first — the grade is meaningless on a partial wiki.
|
|
45
|
+
|
|
46
|
+
**3 · Structural gates** (hard pass/fail, pure code — see ground-truth §G):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv run python -m citadel check # expect: "OK — no validation issues."
|
|
50
|
+
uv run python -m citadel lint # expect: final line "OK"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A non-zero `lint` (missing type / broken link / fabricated source / `[[wikilink]]`) or any `check`
|
|
54
|
+
error is an automatic FAIL — the pipeline produced a structurally invalid wiki.
|
|
55
|
+
|
|
56
|
+
**4 · Grade against the answer key** — read `ground-truth.md` in full, then judge each section against
|
|
57
|
+
the wiki using the greps below as evidence (do not grade from memory). Then go to **Grading**.
|
|
58
|
+
|
|
59
|
+
**5 · Keep or restore the wiki.** The freshly built `wiki/` is the new showcase. To keep it, leave it
|
|
60
|
+
(and `git add wiki/` when committing). To revert: `rm -rf wiki && mv /tmp/citadel-wiki-bak.* wiki`.
|
|
61
|
+
|
|
62
|
+
## Mode B · Grade-only (no re-ingest)
|
|
63
|
+
|
|
64
|
+
Grades the wiki that is already on disk — for iterating on the grader/ground-truth or re-checking after
|
|
65
|
+
a manual wiki edit. Skip steps 1–2; run **3** (gates) and **4** (grade) only.
|
|
66
|
+
|
|
67
|
+
## Grading — evidence commands
|
|
68
|
+
|
|
69
|
+
Run these and judge the output against `ground-truth.md`. Page **names** are LLM-chosen and vary run to
|
|
70
|
+
run — **grade by content, not by filename**.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# C · contradictions surfaced (target >=2 of 4; stretch 4/4)
|
|
74
|
+
grep -rn "CONTRADICTION" wiki/ | grep -v index.md
|
|
75
|
+
# the four subjects to confirm by eye (one value vs the other, or a callout):
|
|
76
|
+
grep -rni "green tea" wiki/ | grep -iE "28|50 ?mg"
|
|
77
|
+
grep -rni "half-life" wiki/ | grep -iE "3 ?h|5 ?h|hour"
|
|
78
|
+
grep -rni "aurora" wiki/ | grep -iE "198[57]"
|
|
79
|
+
grep -rni "thornbury\|1650\|1657" wiki/
|
|
80
|
+
|
|
81
|
+
# D · the false fact present, attributed, and questioned (not silently fixed/dropped)
|
|
82
|
+
grep -rni "caffeine-free\|midnight\|burns off" wiki/ # the claim must appear
|
|
83
|
+
grep -rn "\[\^llm" wiki/ # ideally an LLM caveat near it
|
|
84
|
+
uv run python -m citadel lint | grep -A20 "model-supplied" # pages carrying [^llm] facts
|
|
85
|
+
|
|
86
|
+
# E · the subtle "don't drop me" fact survived
|
|
87
|
+
grep -rni "cold brew" wiki/ | grep -iE "higher|more caffeine|ratio|steep"
|
|
88
|
+
|
|
89
|
+
# A · single-source facts survived
|
|
90
|
+
grep -rni "l-theanine" wiki/ ; grep -rni "ceremonial\|culinary" wiki/ ; grep -rni "9 ?bar\|63 ?mg" wiki/
|
|
91
|
+
|
|
92
|
+
# B · repetitions merged, not duplicated — there should NOT be one isolated page per raw file
|
|
93
|
+
grep -rln "95 ?mg" wiki/ # the 95 mg fact should live on ~one page, co-cited, not many
|
|
94
|
+
grep -rln "twice the caffeine\|2x\|2× caffeine" wiki/
|
|
95
|
+
|
|
96
|
+
# F · cross-topic bridge: coffee<->tea connected, Thornbury reachable from both
|
|
97
|
+
grep -rni "caffeine" wiki/ | grep -i "tea" | grep -i "coffee"
|
|
98
|
+
grep -rni "thornbury" wiki/
|
|
99
|
+
|
|
100
|
+
# H · abbreviations: TDS/EGCG spelled-out-once carried in (defined); EY flagged undefined
|
|
101
|
+
grep -rni "total dissolved solids\|(TDS)\|(EGCG)\|epigallocatechin" wiki/ # expansions preserved
|
|
102
|
+
grep -rln "type: Abbreviation" wiki/ 2>/dev/null || true # bonus: a glossary page
|
|
103
|
+
uv run python -m citadel lint | grep -A12 -i "undefined abbrev" # EY should appear; TDS/EGCG should NOT
|
|
104
|
+
|
|
105
|
+
# provenance density (every fact cited)
|
|
106
|
+
grep -rno "\[\^s[0-9]" wiki/ | wc -l # many raw citations
|
|
107
|
+
uv run python -m citadel search "caffeine" # the search seam works
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Optional visual check: `uv run python -m citadel view --no-open` then open the printed `file://` path.
|
|
111
|
+
|
|
112
|
+
## Pass criteria
|
|
113
|
+
|
|
114
|
+
Report a table of: **hard gates** (all must hold) and **soft checks** (report caught/partial/missed,
|
|
115
|
+
don't hard-fail a single miss). From `ground-truth.md`:
|
|
116
|
+
|
|
117
|
+
- **Hard:** `check` 0 errors · `lint` OK · all §A single-source facts present · §D false claim present +
|
|
118
|
+
attributed (NOT silently corrected) · §E subtle fact present · §F coffee and tea not two disconnected
|
|
119
|
+
islands · §B not one-isolated-page-per-raw-file.
|
|
120
|
+
- **Soft:** §C contradictions surfaced (≥2 good, 4/4 great) · §D carries an explicit `[^llm]` caveat ·
|
|
121
|
+
§B merges maximally tidy · §H abbreviations (TDS/EGCG carried in with their expansion and not flagged
|
|
122
|
+
undefined; EY surfaced by lint's undefined-abbreviations check).
|
|
123
|
+
|
|
124
|
+
End with a one-line verdict and, if anything failed, the specific guarantee it breaks (organized /
|
|
125
|
+
links / provenance) and the file/fact involved.
|
|
126
|
+
|
|
127
|
+
## Gotchas
|
|
128
|
+
|
|
129
|
+
- **Ingest is non-deterministic.** Page filenames, exact wording, and which contradictions get a
|
|
130
|
+
`> [!CONTRADICTION]` callout vary between runs and models. Grade semantics (is the fact present, cited,
|
|
131
|
+
merged?), never exact paths. A contradiction missed on one run that was caught before is a *soft*
|
|
132
|
+
regression worth noting, not a hard fail.
|
|
133
|
+
- **The manifest makes ingest skip unchanged sources.** If you forget step 1 (empty wiki) the run does
|
|
134
|
+
nothing and the grade reflects the *old* wiki. Always start Mode A from a moved-aside wiki.
|
|
135
|
+
- **A wiki outside the repo root breaks citations.** Do NOT point `CITADEL_WIKI_DIR` at `/tmp` for this —
|
|
136
|
+
the `[^s..]` links are `../../raw/...` relative and must resolve to the repo's `raw/`. Keep `wiki/` at
|
|
137
|
+
the repo root (sibling of `raw/`); only the *backup* goes to `/tmp`.
|
|
138
|
+
- **The false fact is supposed to be in the wiki.** Do not "fix" it. A wiki that silently states the
|
|
139
|
+
truth instead, with no `aurora-coffee-blog.md` citation, is a provenance FAIL, not a pass.
|
|
140
|
+
- **Fictional entities are not errors.** Caffè Aurora, Lina Marchetti, Thornbury & Lin etc. are invented
|
|
141
|
+
on purpose; the wiki recording them is correct.
|
|
142
|
+
- **Model matters.** A weaker `CITADEL_INGEST_MODEL` catches fewer contradictions and adds fewer `[^llm]`
|
|
143
|
+
caveats. Note the model (it is recorded per source in `wiki/.citadel_ingested.json` and the report) so
|
|
144
|
+
soft-score comparisons are apples-to-apples.
|
|
145
|
+
|
|
146
|
+
## Troubleshooting
|
|
147
|
+
|
|
148
|
+
- `ingest` reports a per-source error about the CLI → it is missing or not logged in; `claude` then
|
|
149
|
+
`/login`, or set `CITADEL_LLM_CLI`/`*_CLI_PATH`. Re-run from step 1.
|
|
150
|
+
- `check`/`lint` fail right after a green ingest → the agent introduced a broken cross-link or skipped a
|
|
151
|
+
required field; read the issue, it names the page. This is a real pipeline finding, report it.
|
|
152
|
+
- Grade looks empty / everything "missing" → you are likely grading a stale or empty `wiki/`; confirm
|
|
153
|
+
`ls wiki/**/*.md` shows pages and that step 2 actually processed 10 sources.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Ground truth — the coffee+tea example corpus
|
|
2
|
+
|
|
3
|
+
This is the **answer key** for the `verify-example` end-to-end test. The `raw/` corpus is fed to
|
|
4
|
+
`citadel ingest`; this file is **not** — it lives under `.claude/` (outside `raw/`/`wiki/`/`docs/`), so
|
|
5
|
+
the ingest pipeline never sees it. The skill reads it to grade the wiki the pipeline produced.
|
|
6
|
+
|
|
7
|
+
The corpus is deliberately messy — facts repeat, contradict, hide in one place, vary in writing style,
|
|
8
|
+
and include invented people and one flat-out-false claim — so a clean pass exercises all three of the
|
|
9
|
+
project's guarantees: **stays organized**, **links keep working**, **honest provenance**.
|
|
10
|
+
|
|
11
|
+
> Some people/companies below are **fictional by design** (Caffè Aurora, Lina Marchetti, Thornbury & Lin,
|
|
12
|
+
> Sir Edmund Thornbury, Mei Lin). They are *not* errors — the wiki should record them faithfully as the
|
|
13
|
+
> sources state them.
|
|
14
|
+
|
|
15
|
+
## The 10 raw files
|
|
16
|
+
|
|
17
|
+
| file | topic | register | gist |
|
|
18
|
+
| ---- | ----- | -------- | ---- |
|
|
19
|
+
| `raw/coffee-guide.md` | coffee | structured reference | species, origin, processing, roast, ratios, caffeine |
|
|
20
|
+
| `raw/espresso-and-cafe-culture.md` | coffee | prose essay | espresso mechanics; Lina Marchetti founds Caffè Aurora (1987) |
|
|
21
|
+
| `raw/cold-brew-notes.md` | coffee | lab notebook | cold-brew method; "cold brew higher caffeine"; half-life ~3 h |
|
|
22
|
+
| `raw/coffee-health-faq.md` | coffee | FAQ | half-life ~5 h, 95 mg, robusta 2×, adenosine, pregnancy |
|
|
23
|
+
| `raw/aurora-coffee-blog.md` | coffee | brand blog | Caffè Aurora (1985); **the false "dark roast = caffeine-free" claim** |
|
|
24
|
+
| `raw/tea-guide.md` | tea | structured reference | Camellia sinensis, oxidation, temps, caffeine (green 28 mg) |
|
|
25
|
+
| `raw/tea-history-and-trade.md` | tea | prose narrative | tea trade; Thornbury & Lin (1657); green tea ~50 mg |
|
|
26
|
+
| `raw/matcha-and-preparation.md` | tea | how-to | matcha prep; 60–70 mg; ceremonial vs culinary grade |
|
|
27
|
+
| `raw/tea-health-faq.md` | tea | FAQ | L-theanine; EGCG; tea-vs-coffee caffeine compare |
|
|
28
|
+
| `raw/thornbury-tea-blog.md` | tea | brand blog | Thornbury & Lin (1650); tea-vs-coffee rivalry |
|
|
29
|
+
|
|
30
|
+
## A · Known facts that MUST appear in the wiki (cited to the right source)
|
|
31
|
+
|
|
32
|
+
Single-source facts (only one file states them — they must **survive** ingest, not be dropped):
|
|
33
|
+
|
|
34
|
+
| fact | source file | note |
|
|
35
|
+
| ---- | ----------- | ---- |
|
|
36
|
+
| Espresso pulled ~9 bar, ~25–30 s, ~18 g in → ~36 g out, crema; ~63 mg/shot | `espresso-and-cafe-culture.md` | espresso mechanics |
|
|
37
|
+
| Cold brew often **higher** caffeine than hot drip (ratio + long steep) | `cold-brew-notes.md` | the "easy to drop" fact — see §D |
|
|
38
|
+
| Matcha grades: ceremonial vs culinary | `matcha-and-preparation.md` | |
|
|
39
|
+
| Matcha ~60–70 mg (whole leaf consumed) | `matcha-and-preparation.md` | |
|
|
40
|
+
| **L-theanine** (calm-alert, ~unique to tea) | `tea-health-faq.md` | |
|
|
41
|
+
| Coffee 3–4 cups/day *associated with* lower type-2-diabetes / Parkinson's risk | `coffee-health-faq.md` | must stay an *association* |
|
|
42
|
+
| White tea contains caffeine (~30–55 mg) | `tea-guide.md` | |
|
|
43
|
+
| Caffè Aurora founder Lina Marchetti, Trieste | `espresso-and-cafe-culture.md` + `aurora-coffee-blog.md` | year contradicts (§C) |
|
|
44
|
+
| Thornbury & Lin (Edmund Thornbury, Mei Lin), London/Canton, traded tea **and** coffee | `tea-history-and-trade.md` + `thornbury-tea-blog.md` | year contradicts (§C) |
|
|
45
|
+
|
|
46
|
+
## B · Repetitions — must MERGE / co-cite, never duplicate one-page-per-file
|
|
47
|
+
|
|
48
|
+
| fact | files | expectation |
|
|
49
|
+
| ---- | ----- | ----------- |
|
|
50
|
+
| Robusta ≈ 2× the caffeine of Arabica | `coffee-guide.md` + `coffee-health-faq.md` | one statement, both cited (`[^s..]` ×2) |
|
|
51
|
+
| Drip coffee (8 oz) ≈ 95 mg | `coffee-guide.md` + `coffee-health-faq.md` | one statement, both cited |
|
|
52
|
+
| Green-tea brew temp 70–80 °C | `tea-guide.md` + `matcha-and-preparation.md` | merged |
|
|
53
|
+
| All true tea = *Camellia sinensis* | `tea-guide.md` + `matcha-and-preparation.md` (+ history) | merged |
|
|
54
|
+
|
|
55
|
+
Failure mode to catch: a `concepts/coffee-guide.md` AND a `concepts/coffee-health-faq.md` that each
|
|
56
|
+
restate the 95 mg / robusta facts in isolation = the pipeline made one-page-per-file instead of routing
|
|
57
|
+
by fit. (Page **names** are LLM-chosen and may differ — judge by content, not filename.)
|
|
58
|
+
|
|
59
|
+
## C · Contradictions — must surface as `> [!CONTRADICTION]` (or both conflicting values co-cited on one page)
|
|
60
|
+
|
|
61
|
+
| id | subject | source A | source B |
|
|
62
|
+
| -- | ------- | -------- | -------- |
|
|
63
|
+
| `green-tea-caffeine` | green-tea caffeine per cup | `tea-guide.md`: ~28 mg | `tea-history-and-trade.md`: ~50 mg |
|
|
64
|
+
| `half-life` | caffeine half-life | `coffee-health-faq.md`: ~5 h | `cold-brew-notes.md`: ~3 h |
|
|
65
|
+
| `aurora-year` | Caffè Aurora founding year | `espresso-and-cafe-culture.md`: 1987 | `aurora-coffee-blog.md`: 1985 |
|
|
66
|
+
| `thornbury-year` | first English tea import | `tea-history-and-trade.md`: 1657 | `thornbury-tea-blog.md`: 1650 |
|
|
67
|
+
|
|
68
|
+
A passing wiki surfaces these rather than silently picking one value. Catching all four is the
|
|
69
|
+
**stretch** goal; catching ≥2 and never *silently* overwriting is the **hard** goal. Report each as
|
|
70
|
+
caught / not-caught.
|
|
71
|
+
|
|
72
|
+
## D · The one blatantly false fact (single source → must stand, but flagged)
|
|
73
|
+
|
|
74
|
+
`aurora-coffee-blog.md` asserts: **"Aurora Midnight is a dark roast, and the roasting fire burns off
|
|
75
|
+
the caffeine, so Midnight is caffeine-free — the darker the roast, the less caffeine."** This is false
|
|
76
|
+
in reality, but it is the blog's own word and no other source makes the same Aurora-Midnight claim.
|
|
77
|
+
|
|
78
|
+
Required wiki behaviour (honest provenance):
|
|
79
|
+
1. The claim **appears**, cited to `aurora-coffee-blog.md` (`[^s..]`) — not silently dropped.
|
|
80
|
+
2. It is **not** presented as unqualified truth: ideally an `[^llm]` model-knowledge note questions it,
|
|
81
|
+
or a `> [!CONTRADICTION]` ties it to `coffee-guide.md`'s "roast barely changes caffeine / dark roast
|
|
82
|
+
is not decaffeinated."
|
|
83
|
+
3. It is **not** rewritten into the correct fact without attribution (that would be inventing/erasing).
|
|
84
|
+
|
|
85
|
+
`citadel lint` lists pages carrying `[^llm]` facts — a good signal this was handled.
|
|
86
|
+
|
|
87
|
+
## E · The subtle "must-not-be-dropped" fact
|
|
88
|
+
|
|
89
|
+
`cold-brew-notes.md` states cold brew often ends up **higher** in caffeine than hot drip (high
|
|
90
|
+
grounds-to-water ratio + long steep), against the "cold = less caffeine" assumption. It is mentioned
|
|
91
|
+
once, in a notebook among other detail — easy to lose. It **must be present** in the wiki.
|
|
92
|
+
|
|
93
|
+
## F · Cross-topic bridges (coffee ↔ tea)
|
|
94
|
+
|
|
95
|
+
- **Caffeine** is the strongest link: both topics give numbers (drip 95 mg vs black tea 47 mg; tea
|
|
96
|
+
generally less than coffee). Expect either a shared caffeine `Concept` page cited by both topics, or
|
|
97
|
+
dense cross-links between the coffee and tea caffeine material. The two topics must **not** end up as
|
|
98
|
+
two disconnected islands.
|
|
99
|
+
- **Thornbury & Lin** traded tea **and** coffee → its page (likely `type: Organization`) should be
|
|
100
|
+
reachable from both the tea trade material and a coffee-trade mention.
|
|
101
|
+
|
|
102
|
+
## G · Structural gates (hard pass/fail — pure code, no judgement)
|
|
103
|
+
|
|
104
|
+
- `citadel check` → **0 errors** (required fields, honest/defined citations, relative non-broken links).
|
|
105
|
+
- `citadel lint` → **OK** (no missing-type, no broken links, no fabricated sources, no `[[wikilinks]]`).
|
|
106
|
+
- Every factual sentence carries a `[^s..]` (raw) or `[^llm..]` (model) marker; every `[^s..]` resolves
|
|
107
|
+
to a real `raw/` file.
|
|
108
|
+
- Pages routed by `type` into the right folder (`concepts/`, `organizations/`, `persons/`, …).
|
|
109
|
+
|
|
110
|
+
## H · Abbreviations (glossary + the undefined-abbreviation lint check)
|
|
111
|
+
|
|
112
|
+
The corpus seeds the abbreviation machinery on purpose:
|
|
113
|
+
|
|
114
|
+
- **Spelled out once → defined.** `TDS` (Total Dissolved Solids) and `EGCG` (epigallocatechin gallate)
|
|
115
|
+
are each expanded **exactly once**, in parenthetical form, then used bare elsewhere (`TDS` in
|
|
116
|
+
`cold-brew-notes.md` + `coffee-guide.md`; `EGCG` in `tea-guide.md` + `tea-health-faq.md`). The wiki
|
|
117
|
+
should carry that expansion — inline (`Total Dissolved Solids (TDS)`) and/or as a `type:
|
|
118
|
+
Abbreviation` glossary page — so `citadel lint` does **not** list TDS/EGCG as undefined, and they
|
|
119
|
+
may appear in the generated `## Abbreviations` table in `index.md`.
|
|
120
|
+
- **Never spelled out in raw → defined honestly OR flagged.** `EY` (extraction yield) is used across
|
|
121
|
+
the coffee brewing material (`cold-brew-notes.md` + `coffee-guide.md`) but **never** expanded in any
|
|
122
|
+
raw file. Two honest outcomes are acceptable: (a) the wiki leaves it bare and `citadel lint`'s
|
|
123
|
+
*Undefined abbreviations* check surfaces `EY`; or (b) — observed, and preferable — the agent creates a
|
|
124
|
+
`type: Abbreviation` page that expands it (`EY — Extraction Yield`) and labels the expansion `[^llm]`,
|
|
125
|
+
since that expansion is model knowledge, not from a raw file. Either way `citadel lint` should surface
|
|
126
|
+
**at least one** undefined abbreviation (`EY` and/or `FAQ`); the build never fails on it (advisory).
|
|
127
|
+
|
|
128
|
+
This is what the user means by "abbreviations should appear, at least one spelled out once": TDS/EGCG
|
|
129
|
+
are the spelled-out-once ones (→ defined, ideally a glossary page); EY is never spelled out in raw, so
|
|
130
|
+
the pipeline must either flag it (lint) or define it with an honest `[^llm]` expansion — and never just
|
|
131
|
+
silently invent the expansion as if it came from the source.
|
|
132
|
+
|
|
133
|
+
## Scoring
|
|
134
|
+
|
|
135
|
+
**Hard gates** (must all hold): §G structural, §A single-source facts all present, §D claim present +
|
|
136
|
+
attributed (not silently corrected), §E subtle fact present, §F not two disconnected islands.
|
|
137
|
+
|
|
138
|
+
**Soft / probabilistic** (LLM-dependent — report, don't hard-fail on a single miss): §C contradiction
|
|
139
|
+
callouts (target ≥2 of 4, stretch 4/4), §B merges being maximally tidy, §D carrying an explicit `[^llm]`
|
|
140
|
+
caveat, §H abbreviations (TDS/EGCG carried in with their expansion and NOT flagged undefined; EY
|
|
141
|
+
surfaced by lint's undefined-abbreviations check). Report each soft check as caught/partial/missed so
|
|
142
|
+
regressions are visible across runs.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# GitHub Copilot instructions — cite-citadel
|
|
2
|
+
|
|
3
|
+
Repository guidance for GitHub Copilot. This mirrors [`CLAUDE.md`](../CLAUDE.md); keep the two in
|
|
4
|
+
sync when either changes.
|
|
5
|
+
|
|
6
|
+
## What this is
|
|
7
|
+
|
|
8
|
+
`cite-citadel` (CLI: `citadel`, PyPI package: `cite-citadel`) is an LLM-maintained, fully-cited
|
|
9
|
+
personal wiki in Google's [Open Knowledge Format](../docs/okf-reference.md), with an MCP server so an
|
|
10
|
+
AI can search and read it. It implements Karpathy's LLM-Wiki pattern: drop arbitrary text-bearing
|
|
11
|
+
files into `raw/`, and one agentic CLI session per source folds each into a cross-linked OKF wiki
|
|
12
|
+
under `wiki/`. Pure Python 3.12, KISS. Runtime deps are only `mcp` and `pyyaml` — **there is no LLM
|
|
13
|
+
SDK and no API key**: ingest shells out to a coding-agent CLI you already have logged in
|
|
14
|
+
(`claude`/`copilot`/`gemini`).
|
|
15
|
+
|
|
16
|
+
## Commands
|
|
17
|
+
|
|
18
|
+
Setup: `uv sync` (creates `.venv`, installs deps + the `dev` group + the `citadel` script).
|
|
19
|
+
|
|
20
|
+
Use the **portable** invocation everywhere — it works identically on Linux/macOS/Windows and needs
|
|
21
|
+
no `.exe` (the `uv run citadel …` shorthand often breaks on Windows because AV quarantines uv's
|
|
22
|
+
generated `citadel.exe`):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv run python -m citadel <subcommand>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Subcommands: `init [DIR]` (scaffold a workspace: `citadel.toml` marker, `.env`, `raw/`, `wiki/`;
|
|
29
|
+
idempotent), `ingest [paths…]` (fold raw/ into the wiki; `--verbose`/`-v` streams the agent
|
|
30
|
+
session, `--log-dir DIR` writes a transcript per source, `--quiet` drops the progress spinner),
|
|
31
|
+
`serve` (MCP stdio server), `search <query> [--tag T] [--limit N]`, `tags [tag]`,
|
|
32
|
+
`lint [--stale-days N]`, `check [paths…]`, `view [--out PATH] [--no-open] [--obsidian]`.
|
|
33
|
+
`citadel --version` prints the version and (like `--help`) needs no workspace.
|
|
34
|
+
|
|
35
|
+
Tests (pytest, all offline — no CLI/network is ever spawned):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
uv run pytest -q # whole suite (~420 tests, ~3s)
|
|
39
|
+
uv run pytest tests/test_ingest_core.py -q # one file
|
|
40
|
+
uv run pytest tests/test_ingest_core.py::test_ingest_creates_pages # one test
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
New tests build on the shared fixtures in `tests/conftest.py` — that layer is THE pattern:
|
|
44
|
+
`tmp_citadel` (a temp repo/wiki/raw/docs layout wired into `config.*`; `tmp_citadel_external`
|
|
45
|
+
for the out-of-repo mounted-drive layout, `make_citadel` for custom ones), `seed_page` (write a
|
|
46
|
+
canonical OKF page into the configured wiki), and `fake_agent` (a recording `FakeAgent`
|
|
47
|
+
installed over `llm.run_ingest_session` — pages to write, an error to raise, or a
|
|
48
|
+
`side_effect`). Don't re-create per-file `_wire*`/fake-session copies.
|
|
49
|
+
|
|
50
|
+
Lint and format with **ruff** (config in `pyproject.toml`; CI gates both, alongside pytest):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
uv run ruff check . # lint
|
|
54
|
+
uv run ruff format . # auto-format (CI runs `ruff format --check .`)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Python 3.12+ is required. There is no separate build step — `pytest` and `ruff` are the checks.
|
|
58
|
+
|
|
59
|
+
## Architecture
|
|
60
|
+
|
|
61
|
+
**The `wiki/` directory _is_ the database.** No SQLite, no vector store, no second source of truth.
|
|
62
|
+
Pages are markdown files with YAML frontmatter; everything (search, index, graph, provenance) is
|
|
63
|
+
recomputed from them in memory.
|
|
64
|
+
|
|
65
|
+
**Three layers** (the README and `citadel/rules/schema.md` are authoritative):
|
|
66
|
+
1. `raw/` — immutable sources the agent reads but never edits.
|
|
67
|
+
2. `wiki/` — the LLM-owned OKF bundle: pages routed *by kind* into `concepts/`, `objects/`,
|
|
68
|
+
`systems/`, `persons/`, `organizations/`, `projects/`, `abbreviations/`, `misc/` (see
|
|
69
|
+
`okf.folder_for_type`), cross-linked with relative markdown links, each fact carrying a footnote
|
|
70
|
+
citation.
|
|
71
|
+
3. `citadel/rules/` — the schema/rules tree, packaged with the wheel (index:
|
|
72
|
+
`citadel/rules/README.md`; the repo-root `SCHEMA.md`/`AGENT_INGEST.md` are thin pointers):
|
|
73
|
+
`schema.md` (format contract) + `core.md` (agent behavior) are read every session, plus one
|
|
74
|
+
lifecycle brief from `tasks/`, any file-type brief from `formats/`, and the agent-judged
|
|
75
|
+
`genres/` briefs. These are **read by the ingest agent at run time** (referenced by path in the
|
|
76
|
+
prompt), so editing them changes how the wiki is built with **no code change**. Treat them as
|
|
77
|
+
part of the program.
|
|
78
|
+
|
|
79
|
+
**Everything operates on a WORKSPACE**, not the repo checkout: a directory holding a
|
|
80
|
+
`citadel.toml` marker (a pure marker, never config — scaffold one with `citadel init [DIR]`).
|
|
81
|
+
Discovery order: `CITADEL_WORKSPACE` env var > nearest marker walking up from the CWD (nested
|
|
82
|
+
markers shadow outer ones) > an env-dirs workspace (`CITADEL_WIKI_DIR`+`CITADEL_RAW_DIR` both
|
|
83
|
+
set) > otherwise none: `config.WORKSPACE_FOUND` is False, `WORKSPACE_ROOT` falls back to the
|
|
84
|
+
bare CWD, and every subcommand except `init` fails loud. The dev checkout carries a marker, so
|
|
85
|
+
it is itself a workspace.
|
|
86
|
+
|
|
87
|
+
**Ingest is the heart of the system** (`ingest.py` → `llm.py`). The flow per source:
|
|
88
|
+
- `ingest.ingest()` partitions candidates into pending / already-ingested (sha match) / reorganized
|
|
89
|
+
(moved-or-duplicate) / unreadable (binary) / deleted (vanished from disk, full runs only).
|
|
90
|
+
- For each pending source it runs the agent against a **per-source staging copy** of the wiki (a
|
|
91
|
+
sibling dir, never the live wiki), then snapshots before/after and **diffs by content hash** to
|
|
92
|
+
learn what the agent created/updated/deleted — the agent has no return value, its file edits *are*
|
|
93
|
+
the result.
|
|
94
|
+
- It then re-imposes invariants on every changed page (`validate.validate_page` + `store.write_page`
|
|
95
|
+
to canonicalize YAML and stamp the timestamp), repairs renamed-page links, and **only on a fully
|
|
96
|
+
clean session promotes staging onto the live wiki** with a non-destructive copy-over-then-prune.
|
|
97
|
+
Any failure/timeout/Ctrl+C leaves the live wiki exactly as it was; the source is retried next run.
|
|
98
|
+
This all-or-nothing + network-share-hardened machinery (`_robust_*`, `robust_mkdir`) is load-bearing
|
|
99
|
+
— don't simplify it away.
|
|
100
|
+
|
|
101
|
+
**`llm.py` is the ONLY place that talks to an LLM**, and it does so by shelling out to a CLI in
|
|
102
|
+
agentic mode (`cwd` = workspace root, autonomous file tools). The prompt is **paths-only** — it references
|
|
103
|
+
the source and rules by path, never embeds file content — which keeps argv tiny (the Windows
|
|
104
|
+
`WinError 206` fix). `kind` selects the propagation: `ingest` (new), `reconcile` (changed source —
|
|
105
|
+
update/remove stale facts, don't just append), `delete` (source removed — strip its provenance),
|
|
106
|
+
`repo`/`repo-reconcile` (a whole git repo folded as one digest). `run_ingest_session` is the single
|
|
107
|
+
seam tests monkeypatch.
|
|
108
|
+
|
|
109
|
+
**Two checking layers, one implementation** (`validate.py`):
|
|
110
|
+
- `citadel check` / `wiki_validate` — the **strict per-page gate** (required fields, honest/defined
|
|
111
|
+
citations, relative non-broken links, no `[[wikilinks]]`). The ingest agent self-runs it; ingest
|
|
112
|
+
re-runs it and fails the source on any error.
|
|
113
|
+
- `citadel lint` (`lint.py`) — a **pure offline health check** (contradictions, orphans, missing
|
|
114
|
+
cites, broken links, stale, fabricated sources, undefined abbreviations). Only *structural*
|
|
115
|
+
problems (missing type, broken links, bad sources, wikilinks) flip its non-zero exit; the rest are
|
|
116
|
+
advisory. Both layers parse citations/links/fences through `grammar.py`, so lint and `citadel
|
|
117
|
+
check` agree by construction: a citation into `raw/` or `docs/` is legal provenance (never a
|
|
118
|
+
broken link), and a link inside a ``` code fence is literal text.
|
|
119
|
+
|
|
120
|
+
**Other modules:** `okf.py` is the OKF format core (parse/dump, type→folder routing, link math, and
|
|
121
|
+
the non-negotiable `safe_join` path guard — reuse it for any wiki-relative path). `grammar.py` is
|
|
122
|
+
the **single home of the markdown grammar** (link/footnote/fence/Sources-heading parsing and the
|
|
123
|
+
source-citation predicates) that `store`, `validate`, `lint`, and the viewer all parse through;
|
|
124
|
+
never re-define any of it locally. `store.py` is the
|
|
125
|
+
"database": `load()`, the single swappable `search()` seam, `rebuild_indexes()` (regenerates
|
|
126
|
+
`index.md`, per-folder `index.md`, and `sources/index.md` mechanically from frontmatter +
|
|
127
|
+
manifest), and the deterministic link-rewrite safety nets (`rewrite_links`, `rewrite_raw_references`,
|
|
128
|
+
`find_raw_references`, `find_broken_links`), all fence-aware via `grammar.py`. `manifest.py` tracks idempotency in
|
|
129
|
+
`wiki/.citadel_ingested.json` (per source: sha256 or git commit + importing model). `repo.py` builds
|
|
130
|
+
the digest for git-repo sources. `extract.py` pulls text from Office files (stdlib-only); the legacy
|
|
131
|
+
OLE/CFBF salvage lives in `extract_ole.py`, imported lazily only when a legacy `.ppt`/`.doc`/`.xls`
|
|
132
|
+
is dispatched. `server.py` is the FastMCP stdio server (7 tools; only `wiki_ingest` mutates; tools
|
|
133
|
+
never raise — they return error strings). The `viewer/` subpackage builds the self-contained offline
|
|
134
|
+
HTML viewer (`template.html`/`app.css`/`app.js` are package-data assets loaded via `importlib.resources`). `config.py`
|
|
135
|
+
resolves all paths/settings. `cli.py` mirrors the MCP tools as subcommands.
|
|
136
|
+
|
|
137
|
+
## Conventions specific to this codebase
|
|
138
|
+
|
|
139
|
+
- **`config.*` is read at call time** (`from . import config` then `config.WIKI_DIR`), never imported
|
|
140
|
+
by value — so tests can monkeypatch the whole filesystem layout. Honor this when adding code.
|
|
141
|
+
- **Tests redirect everything to `tmp_path`** by monkeypatching `config.*` (including
|
|
142
|
+
`WORKSPACE_ROOT`, which the agent's `cwd` reads) and replace `llm.run_ingest_session` with a fake
|
|
143
|
+
that writes files into the temp wiki. No test spawns a real CLI. Follow that pattern; keep tests
|
|
144
|
+
offline.
|
|
145
|
+
- **Never hand-edit generated files** — `index.md`, `log.md`, any `*/index.md`, `sources/index.md`,
|
|
146
|
+
`.citadel_viewer.html`, and `.citadel_ingested.json` are regenerated. The ingest agent prompt and
|
|
147
|
+
`store.delete_page` both refuse to touch them.
|
|
148
|
+
- **Provenance grammar is load-bearing:** raw facts cite `[^sN]` → a real `raw/` file; model-supplied
|
|
149
|
+
facts use `[^llmN]` (source: `LLM`) and must never be disguised as raw citations. A `[^sN]` to a
|
|
150
|
+
missing file fails lint/check.
|
|
151
|
+
- **`wiki/`, `raw/`, `docs/` can live outside the workspace** (e.g. a mounted network drive) via
|
|
152
|
+
`CITADEL_*_DIR`. Path handling distinguishes workspace-relative keys from absolute out-of-workspace
|
|
153
|
+
keys (`config.rel_or_abs_posix` / `source_path_for_key`) — preserve that when touching path logic.
|
|
154
|
+
- **Cross-platform robustness is intentional**, not over-engineering: UTF-8 forcing, BOM stripping,
|
|
155
|
+
ASCII-only progress output, read-only-bit clearing, and network-share retry loops all fix real
|
|
156
|
+
Windows/SMB failures.
|
|
157
|
+
- Config knobs live in the workspace-root `.env` (auto-loaded, gitignored; template:
|
|
158
|
+
`citadel/templates/env.example`): `CITADEL_LLM_CLI`,
|
|
159
|
+
`CITADEL_INGEST_MODEL`, `CITADEL_LLM_TIMEOUT`, `CITADEL_LLM_VERBOSE`, `CITADEL_LLM_LOG_DIR`,
|
|
160
|
+
`CITADEL_REPO_SUPPORT`, `CITADEL_WIKI_LANG` (target language of all wiki prose, default `en`),
|
|
161
|
+
`CITADEL_PDF_MODE` (`text` | `images`), `CITADEL_STYLE_PROFILES` (opt-in style capture, default
|
|
162
|
+
`0`), the `CITADEL_*_DIR` path overrides, and `*_CLI_PATH` binary overrides.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ci-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
name: test (${{ matrix.os }}, py${{ matrix.python-version }})
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
# 3.12 is the declared minimum (pyproject `requires-python`); 3.13/3.14 guard forward-compat.
|
|
23
|
+
# Full Python spread on ubuntu; windows/macos run only the edges (3.12 + 3.14) to keep the
|
|
24
|
+
# job count sane. Windows in particular is load-bearing: the SMB/network-share hardening
|
|
25
|
+
# (_robust_*, read-only-bit clearing, UTF-8 forcing) is exercised nowhere else in CI.
|
|
26
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
27
|
+
python-version: ["3.12", "3.13", "3.14"]
|
|
28
|
+
exclude:
|
|
29
|
+
- os: windows-latest
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
- os: macos-latest
|
|
32
|
+
python-version: "3.13"
|
|
33
|
+
defaults:
|
|
34
|
+
run:
|
|
35
|
+
# One shell on every OS (git-bash on Windows) so steps stay byte-identical across the matrix.
|
|
36
|
+
shell: bash
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v4
|
|
39
|
+
|
|
40
|
+
- name: Install uv
|
|
41
|
+
uses: astral-sh/setup-uv@v6
|
|
42
|
+
with:
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
enable-cache: true
|
|
45
|
+
|
|
46
|
+
- name: Install dependencies
|
|
47
|
+
run: uv sync --dev
|
|
48
|
+
|
|
49
|
+
- name: Run tests
|
|
50
|
+
run: uv run pytest -q
|
|
51
|
+
|
|
52
|
+
- name: Lint the bundled wiki (structural health check)
|
|
53
|
+
# The checkout root carries a citadel.toml marker, so the repo itself is a valid workspace.
|
|
54
|
+
run: uv run python -m citadel lint
|
|
55
|
+
|
|
56
|
+
lint:
|
|
57
|
+
runs-on: ubuntu-latest
|
|
58
|
+
steps:
|
|
59
|
+
- uses: actions/checkout@v4
|
|
60
|
+
|
|
61
|
+
- name: Install uv
|
|
62
|
+
uses: astral-sh/setup-uv@v6
|
|
63
|
+
with:
|
|
64
|
+
python-version: "3.12"
|
|
65
|
+
enable-cache: true
|
|
66
|
+
|
|
67
|
+
- name: Install dependencies
|
|
68
|
+
run: uv sync --dev
|
|
69
|
+
|
|
70
|
+
- name: Ruff lint
|
|
71
|
+
run: uv run ruff check .
|
|
72
|
+
|
|
73
|
+
- name: Ruff format check
|
|
74
|
+
run: uv run ruff format --check .
|
|
75
|
+
|
|
76
|
+
# Regression gate for the phantom-workspace bug class: prove the WHEEL ALONE works — installed
|
|
77
|
+
# into a clean venv (no checkout on sys.path, no dev deps) and driven from a temp CWD outside
|
|
78
|
+
# the checkout (no citadel.toml above it, no CITADEL_* env). Each step checks exactly one layer,
|
|
79
|
+
# so the first red step names what broke:
|
|
80
|
+
# uv build -> build backend / project metadata
|
|
81
|
+
# twine check -> distribution metadata (PyPI would reject it)
|
|
82
|
+
# venv + pip install -> wheel installability (runtime deps mcp + pyyaml only)
|
|
83
|
+
# packaged data -> rules/ + templates/ actually shipped INSIDE the wheel
|
|
84
|
+
# citadel --version -> console entry point + version wiring
|
|
85
|
+
# fail-loud guard -> workspace discovery must NOT invent a phantom workspace
|
|
86
|
+
# init / check / search -> workspace scaffold + read paths of the installed package
|
|
87
|
+
wheel-smoke:
|
|
88
|
+
name: wheel-smoke (${{ matrix.os }})
|
|
89
|
+
runs-on: ${{ matrix.os }}
|
|
90
|
+
strategy:
|
|
91
|
+
fail-fast: false
|
|
92
|
+
matrix:
|
|
93
|
+
os: [ubuntu-latest, windows-latest]
|
|
94
|
+
defaults:
|
|
95
|
+
run:
|
|
96
|
+
# git-bash on Windows: globs, mktemp and `source` behave the same as on ubuntu.
|
|
97
|
+
shell: bash
|
|
98
|
+
steps:
|
|
99
|
+
- uses: actions/checkout@v4
|
|
100
|
+
|
|
101
|
+
- name: Install uv
|
|
102
|
+
uses: astral-sh/setup-uv@v6
|
|
103
|
+
with:
|
|
104
|
+
python-version: "3.12"
|
|
105
|
+
enable-cache: true
|
|
106
|
+
|
|
107
|
+
- name: Build sdist + wheel
|
|
108
|
+
run: uv build
|
|
109
|
+
|
|
110
|
+
- name: Check distribution metadata (twine)
|
|
111
|
+
run: uvx twine check dist/*
|
|
112
|
+
|
|
113
|
+
- name: Install ONLY the wheel into a clean venv
|
|
114
|
+
# --seed puts pip into the venv; the activate glob resolves bin/ (POSIX) or Scripts/ (Windows).
|
|
115
|
+
# The venv's script dir goes onto $GITHUB_PATH once (pwd -W yields the native Windows path
|
|
116
|
+
# under git-bash; plain pwd elsewhere), so every later step runs citadel/python directly —
|
|
117
|
+
# from a smoke dir OUTSIDE the checkout, via per-step working-directory.
|
|
118
|
+
run: |
|
|
119
|
+
uv venv --seed smoke-venv
|
|
120
|
+
source smoke-venv/*/activate
|
|
121
|
+
python -m pip install --no-cache-dir dist/*.whl
|
|
122
|
+
echo "$(cd "$(dirname smoke-venv/*/activate)" && { pwd -W 2>/dev/null || pwd; })" >> "$GITHUB_PATH"
|
|
123
|
+
mkdir -p "${{ runner.temp }}/smoke"
|
|
124
|
+
|
|
125
|
+
- name: Wheel completeness — packaged rules + templates
|
|
126
|
+
# From a CWD with no ./citadel directory, imports MUST resolve to site-packages; the agent
|
|
127
|
+
# rules and the .env template are package data and must travel inside the wheel. The
|
|
128
|
+
# expected set is enumerated from the CHECKOUT (citadel/rules/**/*.md) — self-maintaining,
|
|
129
|
+
# so a newly added rules file that fails to ship in the wheel turns this step red.
|
|
130
|
+
working-directory: ${{ runner.temp }}/smoke
|
|
131
|
+
env:
|
|
132
|
+
CHECKOUT: ${{ github.workspace }}
|
|
133
|
+
run: |
|
|
134
|
+
python - <<'EOF'
|
|
135
|
+
import os
|
|
136
|
+
from importlib import resources
|
|
137
|
+
from pathlib import Path
|
|
138
|
+
import citadel
|
|
139
|
+
print("citadel imported from:", citadel.__file__)
|
|
140
|
+
src = Path(os.environ["CHECKOUT"]) / "citadel"
|
|
141
|
+
expected = sorted(p.relative_to(src).as_posix() for p in (src / "rules").rglob("*.md") if p.is_file())
|
|
142
|
+
if not expected:
|
|
143
|
+
raise SystemExit(f"no rules files found under {src / 'rules'} — wrong CHECKOUT path?")
|
|
144
|
+
expected.append("templates/env.example")
|
|
145
|
+
pkg = resources.files("citadel")
|
|
146
|
+
missing = [rel for rel in expected if not pkg.joinpath(rel).is_file()]
|
|
147
|
+
if missing:
|
|
148
|
+
raise SystemExit(f"wheel is missing packaged data: {missing} — check the hatchling wheel config / package layout")
|
|
149
|
+
print(f"packaged rules + templates present in the installed wheel ({len(expected)} files)")
|
|
150
|
+
EOF
|
|
151
|
+
|
|
152
|
+
- name: Smoke — citadel --version (console entry point)
|
|
153
|
+
working-directory: ${{ runner.temp }}/smoke
|
|
154
|
+
run: citadel --version
|
|
155
|
+
|
|
156
|
+
- name: Smoke — fail loud outside any workspace (no phantom workspace)
|
|
157
|
+
working-directory: ${{ runner.temp }}/smoke
|
|
158
|
+
run: |
|
|
159
|
+
if citadel check; then
|
|
160
|
+
echo "::error::'citadel check' succeeded in a CWD with no citadel.toml marker and no CITADEL_* dirs — workspace discovery invented a phantom workspace"
|
|
161
|
+
exit 1
|
|
162
|
+
fi
|
|
163
|
+
echo "OK: workspace-less 'citadel check' failed loud, as designed"
|
|
164
|
+
|
|
165
|
+
- name: Smoke — citadel init demo (workspace scaffold)
|
|
166
|
+
working-directory: ${{ runner.temp }}/smoke
|
|
167
|
+
run: |
|
|
168
|
+
citadel init demo
|
|
169
|
+
test -f demo/citadel.toml
|
|
170
|
+
test -f demo/.env
|
|
171
|
+
test -d demo/raw
|
|
172
|
+
test -d demo/wiki
|
|
173
|
+
|
|
174
|
+
- name: Smoke — citadel check on the fresh workspace (must be clean)
|
|
175
|
+
working-directory: ${{ runner.temp }}/smoke/demo
|
|
176
|
+
run: citadel check
|
|
177
|
+
|
|
178
|
+
- name: Smoke — citadel search on the empty wiki (must exit 0, no matches)
|
|
179
|
+
working-directory: ${{ runner.temp }}/smoke/demo
|
|
180
|
+
run: citadel search x
|