Skip to content

Commit d1dc637

Browse files
committed
feat(entities): shared page-dir constants + surface entities in list/lint/status/skill-gate/linter
Add PAGE_CONTENT_DIRS and INDEX_SEED to openkb/schema.py as the single source of truth; replace duplicated index-seed literals in cli init and compiler._update_index with INDEX_SEED. - openkb list / chat /list: add an Entities section (#2) - lint.check_index_sync: iterate PAGE_CONTENT_DIRS so entities/ pages missing from index.md are flagged (#4) - skill-new gate: count entities/ as compiled content (#5) - status last-compile: derive from summaries/concepts/entities mtimes (#12) - semantic linter: read entities/, check contradictions/redundancy/ coverage/orphans (#3)
1 parent b882ee9 commit d1dc637

8 files changed

Lines changed: 108 additions & 30 deletions

File tree

openkb/agent/compiler.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import yaml
3131

3232
from openkb.lint import list_existing_wiki_targets, strip_ghost_wikilinks
33-
from openkb.schema import get_agents_md
33+
from openkb.schema import INDEX_SEED, get_agents_md
3434

3535
logger = logging.getLogger(__name__)
3636

@@ -1219,11 +1219,7 @@ def _update_index(
12191219

12201220
index_path = wiki_dir / "index.md"
12211221
if not index_path.exists():
1222-
index_path.write_text(
1223-
"# Knowledge Base Index\n\n## Documents\n\n## Concepts\n\n"
1224-
"## Entities\n\n## Explorations\n",
1225-
encoding="utf-8",
1226-
)
1222+
index_path.write_text(INDEX_SEED, encoding="utf-8")
12271223

12281224
lines = index_path.read_text(encoding="utf-8").split("\n")
12291225

openkb/agent/linter.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@
2424
4. **Redundancy** — Are there multiple pages that cover the same content and
2525
could be merged?
2626
5. **Concept coverage** — Are important themes in the summaries missing concept pages?
27+
6. **Entity coverage** — Are important named things (people, organizations, places,
28+
products, works, events) in the summaries missing entity pages, or are existing
29+
entity pages contradictory, redundant, or orphaned (unlinked from any source)?
2730
2831
## Process
2932
1. Start with index.md to understand scope.
3033
2. Read summary pages to understand document content.
3134
3. Read concept pages to check for contradictions and gaps.
32-
4. Produce a structured Markdown report listing issues found with references
35+
4. Read entity pages to check for contradictions, redundancy, coverage, and orphans.
36+
5. Produce a structured Markdown report listing issues found with references
3337
to the specific pages where each issue occurs.
3438
3539
Be thorough but concise. If the wiki is small or sparse, say so.
@@ -99,9 +103,9 @@ async def run_knowledge_lint(kb_dir: Path, model: str) -> str:
99103

100104
prompt = (
101105
"Please audit this knowledge base wiki for semantic quality issues: "
102-
"contradictions, gaps, staleness, redundancy, and missing concept pages. "
103-
"Start with index.md, then read summaries and concepts as needed. "
104-
"Produce a structured Markdown report."
106+
"contradictions, gaps, staleness, redundancy, and missing concept and "
107+
"entity pages. Start with index.md, then read summaries, concepts, and "
108+
"entities as needed. Produce a structured Markdown report."
105109
)
106110

107111
result = await Runner.run(agent, prompt, max_turns=MAX_TURNS)

openkb/cli.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def filter(self, record: logging.LogRecord) -> bool:
4343
from openkb.config import DEFAULT_CONFIG, load_config, save_config, load_global_config, register_kb
4444
from openkb.converter import convert_document
4545
from openkb.log import append_log
46-
from openkb.schema import AGENTS_MD
46+
from openkb.schema import AGENTS_MD, INDEX_SEED, PAGE_CONTENT_DIRS
4747

4848
# Suppress warnings after all imports — markitdown overrides filters at import time
4949
import warnings
@@ -217,7 +217,7 @@ def _preflight_skill_new(kb_dir: Path, name: str) -> str | None:
217217
Checks (in order):
218218
* skill name is a valid kebab-case slug
219219
* ``<kb>/wiki`` exists
220-
* ``<kb>/wiki/concepts`` or ``<kb>/wiki/summaries`` has at least
220+
* any of ``<kb>/wiki/{summaries,concepts,entities}`` has at least
221221
one file (i.e. some document has been ingested + compiled)
222222
223223
Returns ``None`` if all gates pass, else a single-line error message
@@ -239,7 +239,7 @@ def _preflight_skill_new(kb_dir: Path, name: str) -> str | None:
239239

240240
has_content = any(
241241
(wiki / sub).is_dir() and any((wiki / sub).iterdir())
242-
for sub in ("concepts", "summaries")
242+
for sub in PAGE_CONTENT_DIRS
243243
)
244244
if not has_content:
245245
return (
@@ -542,10 +542,7 @@ def init(model, language):
542542

543543
# Write wiki files
544544
Path("wiki/AGENTS.md").write_text(AGENTS_MD, encoding="utf-8")
545-
Path("wiki/index.md").write_text(
546-
"# Knowledge Base Index\n\n## Documents\n\n## Concepts\n\n## Entities\n\n## Explorations\n",
547-
encoding="utf-8",
548-
)
545+
Path("wiki/index.md").write_text(INDEX_SEED, encoding="utf-8")
549546
Path("wiki/log.md").write_text("# Operations Log\n\n", encoding="utf-8")
550547

551548
# Create .openkb/ state directory
@@ -1288,6 +1285,15 @@ def print_list(kb_dir: Path) -> None:
12881285
for c in concepts:
12891286
click.echo(f" - {c}")
12901287

1288+
# Display entities
1289+
entities_dir = kb_dir / "wiki" / "entities"
1290+
if entities_dir.exists():
1291+
entities = sorted(p.stem for p in entities_dir.glob("*.md"))
1292+
if entities:
1293+
click.echo(f"\nEntities ({len(entities)}):")
1294+
for e in entities:
1295+
click.echo(f" - {e}")
1296+
12911297
# Display reports
12921298
reports_dir = kb_dir / "wiki" / "reports"
12931299
if reports_dir.exists():
@@ -1343,15 +1349,19 @@ def print_status(kb_dir: Path) -> None:
13431349
hashes = json.loads(hashes_file.read_text(encoding="utf-8"))
13441350
click.echo(f"\n Total indexed: {len(hashes)} document(s)")
13451351

1346-
# Last compile time: newest file in wiki/summaries/
1347-
summaries_dir = wiki_dir / "summaries"
1348-
if summaries_dir.exists():
1349-
summaries = list(summaries_dir.glob("*.md"))
1350-
if summaries:
1351-
newest_summary = max(summaries, key=lambda p: p.stat().st_mtime)
1352-
import datetime
1353-
mtime = datetime.datetime.fromtimestamp(newest_summary.stat().st_mtime)
1354-
click.echo(f" Last compile: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
1352+
# Last compile time: newest compiled page across summaries/, concepts/,
1353+
# and entities/ (an entity-only compile must still bump the shown time).
1354+
compiled_pages = [
1355+
p
1356+
for sub in PAGE_CONTENT_DIRS
1357+
for p in (wiki_dir / sub).glob("*.md")
1358+
if (wiki_dir / sub).exists()
1359+
]
1360+
if compiled_pages:
1361+
newest_page = max(compiled_pages, key=lambda p: p.stat().st_mtime)
1362+
import datetime
1363+
mtime = datetime.datetime.fromtimestamp(newest_page.stat().st_mtime)
1364+
click.echo(f" Last compile: {mtime.strftime('%Y-%m-%d %H:%M:%S')}")
13551365

13561366
# Last lint time: newest file in wiki/reports/
13571367
reports_dir = wiki_dir / "reports"

openkb/lint.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
import yaml
1717

18+
from openkb.schema import PAGE_CONTENT_DIRS
19+
1820
# Matches [[wikilink]] or [[subdir/link]]
1921
_WIKILINK_RE = re.compile(r"\[\[([^\]]+)\]\]")
2022

@@ -368,7 +370,7 @@ def check_index_sync(wiki: Path) -> list[str]:
368370
369371
Returns issues for:
370372
- Links in index.md pointing to non-existent pages
371-
- Pages in summaries/ or concepts/ not mentioned in index.md
373+
- Pages in summaries/, concepts/, or entities/ not mentioned in index.md
372374
373375
Args:
374376
wiki: Path to the wiki root directory.
@@ -392,11 +394,11 @@ def check_index_sync(wiki: Path) -> list[str]:
392394
if lnk_norm not in pages:
393395
issues.append(f"index.md links to missing page: [[{lnk}]]")
394396

395-
# Check that summaries and concepts pages are mentioned in index
397+
# Check that summaries, concepts, and entities pages are mentioned in index
396398
index_stems = {Path(lnk.strip()).stem for lnk in index_links}
397399
index_text_lower = index_text.lower()
398400

399-
for subdir in ("summaries", "concepts"):
401+
for subdir in PAGE_CONTENT_DIRS:
400402
subdir_path = wiki / subdir
401403
if not subdir_path.exists():
402404
continue

openkb/schema.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
from pathlib import Path
44

5+
# The compiled page-type subdirectories under wiki/. Shared source of truth
6+
# for surfaces that enumerate page content (list, lint, status, skill gate).
7+
PAGE_CONTENT_DIRS = ("summaries", "concepts", "entities")
8+
9+
# Canonical empty index.md seed. Used by `openkb init` and the compiler's
10+
# lazy-create path so they never drift.
11+
INDEX_SEED = "# Knowledge Base Index\n\n## Documents\n\n## Concepts\n\n## Entities\n\n## Explorations\n"
12+
513
AGENTS_MD = """\
614
# Wiki Schema
715
@@ -26,9 +34,10 @@
2634
- **Index Page** (index.md): One-liner summary of every page in the wiki. Auto-maintained.
2735
2836
## Index Page Format
29-
index.md lists all documents, concepts, and explorations with metadata:
37+
index.md lists all documents, concepts, entities, and explorations with metadata:
3038
- Documents: name, one-liner description, type (short|pageindex), detail access path
3139
- Concepts: name, one-liner description
40+
- Entities: name, type, one-liner description
3241
- Explorations: name, one-liner description
3342
3443
## Log Format

tests/test_lint.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,22 @@ def test_page_not_in_index(self, tmp_path):
185185

186186
assert any("unlisted" in issue for issue in result)
187187

188+
def test_entity_page_not_in_index(self, tmp_path):
189+
wiki = _make_wiki(tmp_path)
190+
(wiki / "entities").mkdir()
191+
(wiki / "entities" / "ada-lovelace.md").write_text("# Ada Lovelace")
192+
# index.md has no mention of the entity
193+
(wiki / "index.md").write_text(
194+
"# Index\n\n## Documents\n\n## Concepts\n\n## Entities\n"
195+
)
196+
197+
result = check_index_sync(wiki)
198+
199+
assert any(
200+
"entities/ada-lovelace.md not mentioned in index.md" in issue
201+
for issue in result
202+
)
203+
188204
def test_missing_index_md(self, tmp_path):
189205
wiki = tmp_path / "wiki"
190206
wiki.mkdir()

tests/test_list_status.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,34 @@ def test_list_no_concepts_section_when_empty(self, tmp_path):
8888
# No concepts in output since none exist
8989
assert "Concepts:" not in result.output
9090

91+
def test_list_shows_entities(self, tmp_path):
92+
kb_dir = _setup_kb(tmp_path)
93+
hashes = {"abc": {"name": "paper.pdf", "type": "pdf"}}
94+
(kb_dir / ".openkb" / "hashes.json").write_text(json.dumps(hashes))
95+
(kb_dir / "wiki" / "entities" / "ada-lovelace.md").write_text("# Ada")
96+
(kb_dir / "wiki" / "entities" / "openai.md").write_text("# OpenAI")
97+
98+
runner = CliRunner()
99+
with patch("openkb.cli._find_kb_dir", return_value=kb_dir):
100+
result = runner.invoke(cli, ["list"])
101+
102+
assert "Entities (2):" in result.output
103+
assert "ada-lovelace" in result.output
104+
assert "openai" in result.output
105+
106+
def test_list_no_entities_section_when_empty(self, tmp_path):
107+
kb_dir = _setup_kb(tmp_path)
108+
hashes = {"abc": {"name": "paper.pdf", "type": "pdf"}}
109+
(kb_dir / ".openkb" / "hashes.json").write_text(json.dumps(hashes))
110+
111+
runner = CliRunner()
112+
with patch("openkb.cli._find_kb_dir", return_value=kb_dir):
113+
result = runner.invoke(cli, ["list"])
114+
115+
assert result.exit_code == 0
116+
assert "Entities:" not in result.output
117+
assert "Entities (" not in result.output
118+
91119

92120
class TestStatusCommand:
93121
def test_status_no_kb(self, tmp_path):

tests/test_skill_chat_slash.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,19 @@ async def test_slash_skill_new_rejects_empty_wiki(tmp_path):
8787
assert not (kb / "output").exists()
8888

8989

90+
def test_preflight_gate_counts_entities(tmp_path):
91+
"""The wiki-content gate must accept a KB whose only compiled content
92+
lives in entities/ (no concept or summary pages yet)."""
93+
from openkb.cli import _preflight_skill_new
94+
95+
kb = tmp_path
96+
(kb / "wiki" / "entities").mkdir(parents=True)
97+
(kb / "wiki" / "entities" / "ada.md").write_text("# Ada\n")
98+
99+
# No error means the gate passed.
100+
assert _preflight_skill_new(kb, "demo") is None
101+
102+
90103
@pytest.mark.asyncio
91104
async def test_slash_skill_new_rejects_when_target_exists(tmp_path):
92105
"""Chat / slash command must not silently overwrite an existing skill."""

0 commit comments

Comments
 (0)