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.
Files changed (144) hide show
  1. cite_citadel-0.1.0/.claude/skills/verify-example/SKILL.md +153 -0
  2. cite_citadel-0.1.0/.claude/skills/verify-example/ground-truth.md +142 -0
  3. cite_citadel-0.1.0/.env.example +4 -0
  4. cite_citadel-0.1.0/.github/copilot-instructions.md +162 -0
  5. cite_citadel-0.1.0/.github/workflows/ci.yml +180 -0
  6. cite_citadel-0.1.0/.github/workflows/pages.yml +54 -0
  7. cite_citadel-0.1.0/.github/workflows/release.yml +89 -0
  8. cite_citadel-0.1.0/.gitignore +240 -0
  9. cite_citadel-0.1.0/AGENT_INGEST.md +6 -0
  10. cite_citadel-0.1.0/CLAUDE.md +176 -0
  11. cite_citadel-0.1.0/LICENSE +21 -0
  12. cite_citadel-0.1.0/PKG-INFO +190 -0
  13. cite_citadel-0.1.0/README.md +164 -0
  14. cite_citadel-0.1.0/SCHEMA.md +5 -0
  15. cite_citadel-0.1.0/citadel/__init__.py +6 -0
  16. cite_citadel-0.1.0/citadel/__main__.py +17 -0
  17. cite_citadel-0.1.0/citadel/cli.py +422 -0
  18. cite_citadel-0.1.0/citadel/config.py +581 -0
  19. cite_citadel-0.1.0/citadel/extract.py +360 -0
  20. cite_citadel-0.1.0/citadel/extract_ole.py +222 -0
  21. cite_citadel-0.1.0/citadel/failures.py +80 -0
  22. cite_citadel-0.1.0/citadel/grammar.py +216 -0
  23. cite_citadel-0.1.0/citadel/ingest.py +1701 -0
  24. cite_citadel-0.1.0/citadel/lint.py +450 -0
  25. cite_citadel-0.1.0/citadel/llm.py +605 -0
  26. cite_citadel-0.1.0/citadel/manifest.py +243 -0
  27. cite_citadel-0.1.0/citadel/okf.py +235 -0
  28. cite_citadel-0.1.0/citadel/progress.py +204 -0
  29. cite_citadel-0.1.0/citadel/repo.py +488 -0
  30. cite_citadel-0.1.0/citadel/rules/README.md +71 -0
  31. cite_citadel-0.1.0/citadel/rules/core.md +131 -0
  32. cite_citadel-0.1.0/citadel/rules/formats/image.md +10 -0
  33. cite_citadel-0.1.0/citadel/rules/formats/office.md +17 -0
  34. cite_citadel-0.1.0/citadel/rules/formats/pdf.md +22 -0
  35. cite_citadel-0.1.0/citadel/rules/formats/repo.md +46 -0
  36. cite_citadel-0.1.0/citadel/rules/genres/email.md +17 -0
  37. cite_citadel-0.1.0/citadel/rules/genres/first-person.md +34 -0
  38. cite_citadel-0.1.0/citadel/rules/genres/meeting-minutes.md +95 -0
  39. cite_citadel-0.1.0/citadel/rules/genres/prose.md +15 -0
  40. cite_citadel-0.1.0/citadel/rules/schema.md +268 -0
  41. cite_citadel-0.1.0/citadel/rules/tasks/delete.md +18 -0
  42. cite_citadel-0.1.0/citadel/rules/tasks/ingest.md +28 -0
  43. cite_citadel-0.1.0/citadel/rules/tasks/reconcile.md +30 -0
  44. cite_citadel-0.1.0/citadel/server.py +229 -0
  45. cite_citadel-0.1.0/citadel/store.py +766 -0
  46. cite_citadel-0.1.0/citadel/templates/env.example +137 -0
  47. cite_citadel-0.1.0/citadel/validate.py +249 -0
  48. cite_citadel-0.1.0/citadel/viewer/__init__.py +363 -0
  49. cite_citadel-0.1.0/citadel/viewer/app.css +191 -0
  50. cite_citadel-0.1.0/citadel/viewer/app.js +1100 -0
  51. cite_citadel-0.1.0/citadel/viewer/template.html +50 -0
  52. cite_citadel-0.1.0/citadel/workspace.py +70 -0
  53. cite_citadel-0.1.0/citadel.cmd +6 -0
  54. cite_citadel-0.1.0/citadel.ps1 +7 -0
  55. cite_citadel-0.1.0/citadel.toml +2 -0
  56. cite_citadel-0.1.0/docs/karpathy-llm-wiki.md +74 -0
  57. cite_citadel-0.1.0/docs/okf-reference.md +70 -0
  58. cite_citadel-0.1.0/docs/refactor-plan.md +576 -0
  59. cite_citadel-0.1.0/pyproject.toml +99 -0
  60. cite_citadel-0.1.0/raw/.gitkeep +0 -0
  61. cite_citadel-0.1.0/raw/aurora-coffee-blog.md +69 -0
  62. cite_citadel-0.1.0/raw/coffee-guide.md +66 -0
  63. cite_citadel-0.1.0/raw/coffee-health-faq.md +81 -0
  64. cite_citadel-0.1.0/raw/cold-brew-notes.md +113 -0
  65. cite_citadel-0.1.0/raw/espresso-and-cafe-culture.md +110 -0
  66. cite_citadel-0.1.0/raw/matcha-and-preparation.md +70 -0
  67. cite_citadel-0.1.0/raw/tea-guide.md +168 -0
  68. cite_citadel-0.1.0/raw/tea-health-faq.md +89 -0
  69. cite_citadel-0.1.0/raw/tea-history-and-trade.md +43 -0
  70. cite_citadel-0.1.0/raw/thornbury-tea-blog.md +59 -0
  71. cite_citadel-0.1.0/tests/conftest.py +350 -0
  72. cite_citadel-0.1.0/tests/test_abbreviations.py +157 -0
  73. cite_citadel-0.1.0/tests/test_cli.py +401 -0
  74. cite_citadel-0.1.0/tests/test_extract.py +361 -0
  75. cite_citadel-0.1.0/tests/test_extract_ole.py +42 -0
  76. cite_citadel-0.1.0/tests/test_failures.py +44 -0
  77. cite_citadel-0.1.0/tests/test_failures_catalog.py +147 -0
  78. cite_citadel-0.1.0/tests/test_grammar.py +168 -0
  79. cite_citadel-0.1.0/tests/test_ingest_chunking.py +110 -0
  80. cite_citadel-0.1.0/tests/test_ingest_core.py +417 -0
  81. cite_citadel-0.1.0/tests/test_ingest_dedup.py +101 -0
  82. cite_citadel-0.1.0/tests/test_ingest_discovery.py +258 -0
  83. cite_citadel-0.1.0/tests/test_ingest_lint_store.py +197 -0
  84. cite_citadel-0.1.0/tests/test_ingest_office_images.py +214 -0
  85. cite_citadel-0.1.0/tests/test_ingest_progress.py +63 -0
  86. cite_citadel-0.1.0/tests/test_ingest_provenance.py +230 -0
  87. cite_citadel-0.1.0/tests/test_ingest_reconcile_delete.py +230 -0
  88. cite_citadel-0.1.0/tests/test_ingest_staging.py +446 -0
  89. cite_citadel-0.1.0/tests/test_llm.py +689 -0
  90. cite_citadel-0.1.0/tests/test_manifest.py +118 -0
  91. cite_citadel-0.1.0/tests/test_netdrive.py +373 -0
  92. cite_citadel-0.1.0/tests/test_okf.py +217 -0
  93. cite_citadel-0.1.0/tests/test_open_points.py +184 -0
  94. cite_citadel-0.1.0/tests/test_packaging.py +46 -0
  95. cite_citadel-0.1.0/tests/test_progress.py +160 -0
  96. cite_citadel-0.1.0/tests/test_repo.py +369 -0
  97. cite_citadel-0.1.0/tests/test_rules.py +249 -0
  98. cite_citadel-0.1.0/tests/test_search.py +256 -0
  99. cite_citadel-0.1.0/tests/test_server.py +379 -0
  100. cite_citadel-0.1.0/tests/test_sources_index.py +67 -0
  101. cite_citadel-0.1.0/tests/test_validate.py +140 -0
  102. cite_citadel-0.1.0/tests/test_viewer.py +413 -0
  103. cite_citadel-0.1.0/tests/test_workspace.py +346 -0
  104. cite_citadel-0.1.0/uv.lock +781 -0
  105. cite_citadel-0.1.0/wiki/.citadel_ingested.json +58 -0
  106. cite_citadel-0.1.0/wiki/abbreviations/ey-extraction-yield.md +29 -0
  107. cite_citadel-0.1.0/wiki/abbreviations/index.md +4 -0
  108. cite_citadel-0.1.0/wiki/abbreviations/tds-total-dissolved-solids.md +27 -0
  109. cite_citadel-0.1.0/wiki/concepts/arabica-and-robusta.md +42 -0
  110. cite_citadel-0.1.0/wiki/concepts/aurora-ritual.md +28 -0
  111. cite_citadel-0.1.0/wiki/concepts/caffeine-in-coffee.md +49 -0
  112. cite_citadel-0.1.0/wiki/concepts/caffeine-in-tea.md +50 -0
  113. cite_citadel-0.1.0/wiki/concepts/caffeine.md +42 -0
  114. cite_citadel-0.1.0/wiki/concepts/coffee-brewing.md +31 -0
  115. cite_citadel-0.1.0/wiki/concepts/coffee-history-and-trade.md +34 -0
  116. cite_citadel-0.1.0/wiki/concepts/coffee-processing.md +28 -0
  117. cite_citadel-0.1.0/wiki/concepts/coffee.md +37 -0
  118. cite_citadel-0.1.0/wiki/concepts/cold-brew.md +88 -0
  119. cite_citadel-0.1.0/wiki/concepts/espresso.md +44 -0
  120. cite_citadel-0.1.0/wiki/concepts/index.md +22 -0
  121. cite_citadel-0.1.0/wiki/concepts/l-theanine.md +28 -0
  122. cite_citadel-0.1.0/wiki/concepts/matcha.md +51 -0
  123. cite_citadel-0.1.0/wiki/concepts/roasting.md +28 -0
  124. cite_citadel-0.1.0/wiki/concepts/tea-antioxidants-and-tannins.md +34 -0
  125. cite_citadel-0.1.0/wiki/concepts/tea-brewing.md +39 -0
  126. cite_citadel-0.1.0/wiki/concepts/tea-growing-and-harvesting.md +26 -0
  127. cite_citadel-0.1.0/wiki/concepts/tea-history-and-trade.md +64 -0
  128. cite_citadel-0.1.0/wiki/concepts/tea-types-and-oxidation.md +33 -0
  129. cite_citadel-0.1.0/wiki/concepts/tea.md +42 -0
  130. cite_citadel-0.1.0/wiki/index.md +273 -0
  131. cite_citadel-0.1.0/wiki/objects/aurora-midnight.md +33 -0
  132. cite_citadel-0.1.0/wiki/objects/canton-mist.md +27 -0
  133. cite_citadel-0.1.0/wiki/objects/index.md +6 -0
  134. cite_citadel-0.1.0/wiki/objects/lin-s-evening.md +26 -0
  135. cite_citadel-0.1.0/wiki/objects/thornbury-lin-breakfast.md +26 -0
  136. cite_citadel-0.1.0/wiki/open-points/index.md +19 -0
  137. cite_citadel-0.1.0/wiki/organizations/caffe-aurora.md +44 -0
  138. cite_citadel-0.1.0/wiki/organizations/index.md +4 -0
  139. cite_citadel-0.1.0/wiki/organizations/thornbury-and-lin.md +48 -0
  140. cite_citadel-0.1.0/wiki/persons/edmund-thornbury.md +28 -0
  141. cite_citadel-0.1.0/wiki/persons/index.md +5 -0
  142. cite_citadel-0.1.0/wiki/persons/lina-marchetti.md +41 -0
  143. cite_citadel-0.1.0/wiki/persons/mei-lin.md +30 -0
  144. 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,4 @@
1
+ # .env.example — moved
2
+
3
+ Moved to [citadel/templates/env.example](citadel/templates/env.example) — packaged with the wheel as the
4
+ `citadel init` .env template; edit there.
@@ -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