ctxverify 0.1.0__py3-none-any.whl
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.
- ctxverify/__init__.py +47 -0
- ctxverify/__main__.py +8 -0
- ctxverify/adapters/__init__.py +14 -0
- ctxverify/adapters/generic.py +190 -0
- ctxverify/adapters/llmwiki.py +199 -0
- ctxverify/adapters/obsidian.py +253 -0
- ctxverify/checks/__init__.py +203 -0
- ctxverify/checks/actions.py +291 -0
- ctxverify/checks/consistency.py +202 -0
- ctxverify/checks/duplicates.py +43 -0
- ctxverify/checks/freshness.py +102 -0
- ctxverify/checks/hygiene.py +147 -0
- ctxverify/checks/identity.py +137 -0
- ctxverify/checks/links.py +77 -0
- ctxverify/checks/orphans.py +43 -0
- ctxverify/checks/provenance.py +114 -0
- ctxverify/cli.py +357 -0
- ctxverify/frontmatter.py +149 -0
- ctxverify/loader.py +598 -0
- ctxverify/mcp/__init__.py +12 -0
- ctxverify/mcp/server.py +279 -0
- ctxverify/model.py +531 -0
- ctxverify/report.py +252 -0
- ctxverify/resolve.py +118 -0
- ctxverify/rules.py +258 -0
- ctxverify/scaffold.py +85 -0
- ctxverify/version.py +1 -0
- ctxverify/writer.py +118 -0
- ctxverify-0.1.0.dist-info/METADATA +472 -0
- ctxverify-0.1.0.dist-info/RECORD +35 -0
- ctxverify-0.1.0.dist-info/WHEEL +4 -0
- ctxverify-0.1.0.dist-info/entry_points.txt +3 -0
- ctxverify-0.1.0.dist-info/licenses/LICENSE +202 -0
- ctxverify-0.1.0.dist-info/licenses/LICENSE-SPEC +396 -0
- ctxverify-0.1.0.dist-info/licenses/NOTICE +12 -0
ctxverify/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""ctxverify — a linter and verifier for context packs.
|
|
2
|
+
|
|
3
|
+
A context pack is a portable container for the knowledge an agent is given:
|
|
4
|
+
atomic claims, the evidence behind each claim, the entities involved, the
|
|
5
|
+
relations between them, and the contradictions and freshness states that a
|
|
6
|
+
reader needs in order to decide how much to trust it.
|
|
7
|
+
|
|
8
|
+
Compilers produce packs. ctxverify judges them.
|
|
9
|
+
|
|
10
|
+
from ctxverify import load_pack, run_checks
|
|
11
|
+
|
|
12
|
+
pack = load_pack("./pack")
|
|
13
|
+
report = run_checks(pack)
|
|
14
|
+
print(report.to_text())
|
|
15
|
+
raise SystemExit(report.exit_code())
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from .loader import PackLoadError, load_pack
|
|
21
|
+
from .model import FORMAT_VERSION, SUPPORTED_FORMAT_VERSIONS, Claim, Evidence, Pack
|
|
22
|
+
from .report import Finding, Report
|
|
23
|
+
from .rules import RULES, RULES_BY_ID, Rule
|
|
24
|
+
from .version import __version__
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Claim",
|
|
28
|
+
"Evidence",
|
|
29
|
+
"Finding",
|
|
30
|
+
"FORMAT_VERSION",
|
|
31
|
+
"Pack",
|
|
32
|
+
"PackLoadError",
|
|
33
|
+
"RULES",
|
|
34
|
+
"RULES_BY_ID",
|
|
35
|
+
"Report",
|
|
36
|
+
"Rule",
|
|
37
|
+
"SUPPORTED_FORMAT_VERSIONS",
|
|
38
|
+
"__version__",
|
|
39
|
+
"load_pack",
|
|
40
|
+
"run_checks",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_checks(pack, options=None): # re-exported lazily to avoid a cycle
|
|
45
|
+
from .checks import run_checks as _run_checks
|
|
46
|
+
|
|
47
|
+
return _run_checks(pack, options)
|
ctxverify/__main__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Adapters: build a pack from a corpus that already exists.
|
|
2
|
+
|
|
3
|
+
An adapter is deliberately conservative. It reads what is on disk, records
|
|
4
|
+
where every piece came from, and does not try to repair anything. The linter
|
|
5
|
+
then judges the result — that separation is the point of the format.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .generic import export_markdown
|
|
11
|
+
from .llmwiki import export_llmwiki
|
|
12
|
+
from .obsidian import export_obsidian
|
|
13
|
+
|
|
14
|
+
__all__ = ["export_llmwiki", "export_markdown", "export_obsidian"]
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Export a plain markdown directory into a context pack.
|
|
2
|
+
|
|
3
|
+
Not every knowledge base is an Obsidian vault. Karpathy's LLM Wiki pattern
|
|
4
|
+
— a directory of markdown pages compiled from raw sources — has no
|
|
5
|
+
frontmatter, no aliases and no wikilinks by convention, and a tool that only
|
|
6
|
+
reads Obsidian vaults would not touch it.
|
|
7
|
+
|
|
8
|
+
This adapter reads any directory of markdown files: frontmatter if it is
|
|
9
|
+
there, the first heading and first paragraph if it is not. It is the
|
|
10
|
+
lowest-common-denominator adapter, and it is the one to start from when
|
|
11
|
+
writing an adapter for your own corpus.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
import re
|
|
18
|
+
from datetime import datetime, timezone
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from ..frontmatter import as_list, as_text, parse_frontmatter, split_frontmatter
|
|
22
|
+
from ..model import CLAIM_STATUSES, Claim, Entity, Evidence, Manifest, Pack, Source
|
|
23
|
+
from ..resolve import LinkIndex, strip_wikilink
|
|
24
|
+
from ..writer import evidence_record
|
|
25
|
+
|
|
26
|
+
SKIP_DIRS = {".git", ".obsidian", ".trash", "node_modules", ".venv", "__pycache__", "dist", "build"}
|
|
27
|
+
|
|
28
|
+
HEADING_RE = re.compile(r"^#{1,6}\s+(.+?)\s*$", re.MULTILINE)
|
|
29
|
+
WIKILINK_RE = re.compile(r"\[\[([^\[\]]+?)\]\]")
|
|
30
|
+
CODE_FENCE_RE = re.compile(r"```.*?```")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _iter_markdown(root: Path):
|
|
34
|
+
for path in sorted(root.rglob("*.md")):
|
|
35
|
+
parts = set(path.relative_to(root).parts[:-1])
|
|
36
|
+
if parts & SKIP_DIRS:
|
|
37
|
+
continue
|
|
38
|
+
if any(part.startswith(".") for part in parts):
|
|
39
|
+
continue
|
|
40
|
+
yield path
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _first_heading(body: str) -> str:
|
|
44
|
+
match = HEADING_RE.search(body)
|
|
45
|
+
return match.group(1).strip() if match else ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _first_paragraph(body: str) -> str:
|
|
49
|
+
text = CODE_FENCE_RE.sub("", body)
|
|
50
|
+
for block in re.split(r"\n\s*\n", text):
|
|
51
|
+
stripped = block.strip()
|
|
52
|
+
if not stripped or stripped.startswith(("#", "```", "|", "-", "*", ">", "<!--")):
|
|
53
|
+
continue
|
|
54
|
+
return re.sub(r"\s+", " ", stripped)[:600]
|
|
55
|
+
return ""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def export_markdown(
|
|
59
|
+
corpus: str | Path,
|
|
60
|
+
pack_id: str | None = None,
|
|
61
|
+
include: list[str] | None = None,
|
|
62
|
+
exclude: list[str] | None = None,
|
|
63
|
+
) -> Pack:
|
|
64
|
+
"""Read a markdown corpus and return a :class:`Pack`."""
|
|
65
|
+
root = Path(corpus).expanduser().resolve()
|
|
66
|
+
if not root.exists():
|
|
67
|
+
raise FileNotFoundError(f"corpus not found: {root}")
|
|
68
|
+
|
|
69
|
+
include = include or []
|
|
70
|
+
exclude = exclude or []
|
|
71
|
+
|
|
72
|
+
pages: list[tuple[Path, str]] = []
|
|
73
|
+
for path in _iter_markdown(root):
|
|
74
|
+
rel = path.relative_to(root).as_posix()
|
|
75
|
+
if include and not any(pattern in rel for pattern in include):
|
|
76
|
+
continue
|
|
77
|
+
if exclude and any(pattern in rel for pattern in exclude):
|
|
78
|
+
continue
|
|
79
|
+
pages.append((path, rel))
|
|
80
|
+
|
|
81
|
+
index = LinkIndex(paths=[rel for _, rel in pages])
|
|
82
|
+
|
|
83
|
+
claims: list[Claim] = []
|
|
84
|
+
evidence: list[Evidence] = []
|
|
85
|
+
entities: list[Entity] = []
|
|
86
|
+
sources: list[Source] = []
|
|
87
|
+
entity_ids: dict[str, str] = {}
|
|
88
|
+
|
|
89
|
+
for number, (path, rel) in enumerate(pages, start=1):
|
|
90
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
91
|
+
front = parse_frontmatter(text)
|
|
92
|
+
_, body = split_frontmatter(text)
|
|
93
|
+
|
|
94
|
+
source_id = f"src-{number:03d}"
|
|
95
|
+
claim_id = f"clm-{number:03d}"
|
|
96
|
+
evidence_id = f"ev-{number:03d}"
|
|
97
|
+
|
|
98
|
+
title = as_text(front.get("title")).strip() or _first_heading(body) or path.stem
|
|
99
|
+
summary = (
|
|
100
|
+
as_text(front.get("summary")).strip()
|
|
101
|
+
or as_text(front.get("description")).strip()
|
|
102
|
+
or _first_paragraph(body)
|
|
103
|
+
or title
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
raw_status = as_text(front.get("status")).strip().lower()
|
|
107
|
+
status = raw_status if raw_status in CLAIM_STATUSES else "asserted"
|
|
108
|
+
|
|
109
|
+
confidence = front.get("confidence")
|
|
110
|
+
if not isinstance(confidence, (int, float)) or isinstance(confidence, bool):
|
|
111
|
+
confidence = None
|
|
112
|
+
|
|
113
|
+
topics = as_list(front.get("topics")) or as_list(front.get("tags"))
|
|
114
|
+
|
|
115
|
+
links = []
|
|
116
|
+
for raw in WIKILINK_RE.findall(body):
|
|
117
|
+
target = strip_wikilink(raw)
|
|
118
|
+
if not target:
|
|
119
|
+
continue
|
|
120
|
+
links.append({"target": raw, "resolved": index.resolve(raw)})
|
|
121
|
+
|
|
122
|
+
claim_entities: list[str] = []
|
|
123
|
+
for name in as_list(front.get("entities")):
|
|
124
|
+
key = name.strip().lower()
|
|
125
|
+
if not key:
|
|
126
|
+
continue
|
|
127
|
+
if key not in entity_ids:
|
|
128
|
+
entity_id = f"ent-{len(entity_ids) + 1:03d}"
|
|
129
|
+
entity_ids[key] = entity_id
|
|
130
|
+
entities.append(Entity(id=entity_id, name=name.strip()))
|
|
131
|
+
claim_entities.append(entity_ids[key])
|
|
132
|
+
|
|
133
|
+
claims.append(
|
|
134
|
+
Claim(
|
|
135
|
+
id=claim_id,
|
|
136
|
+
text=summary,
|
|
137
|
+
status=status,
|
|
138
|
+
confidence=float(confidence) if confidence is not None else None,
|
|
139
|
+
evidence=[evidence_id],
|
|
140
|
+
entities=claim_entities,
|
|
141
|
+
topics=topics,
|
|
142
|
+
links=links,
|
|
143
|
+
created_at=as_text(front.get("created")).strip() or None,
|
|
144
|
+
updated_at=as_text(front.get("updated")).strip() or None,
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
record = evidence_record(
|
|
149
|
+
evidence_id,
|
|
150
|
+
claim_id,
|
|
151
|
+
source_id,
|
|
152
|
+
summary,
|
|
153
|
+
locator={"path": rel, "line_start": 1, "line_end": 1},
|
|
154
|
+
retrieved_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
155
|
+
)
|
|
156
|
+
evidence.append(Evidence.from_dict(record))
|
|
157
|
+
|
|
158
|
+
sources.append(
|
|
159
|
+
Source(
|
|
160
|
+
id=source_id,
|
|
161
|
+
path=rel,
|
|
162
|
+
title=title,
|
|
163
|
+
kind="markdown",
|
|
164
|
+
sha256=hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
165
|
+
modified_at=datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).isoformat(
|
|
166
|
+
timespec="seconds"
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
manifest = Manifest(
|
|
172
|
+
pack_id=pack_id or root.name,
|
|
173
|
+
title=f"{root.name} (markdown export)",
|
|
174
|
+
description=f"Context pack exported from the markdown corpus at {root}.",
|
|
175
|
+
generated_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
176
|
+
generator={"name": "ctxverify-adapter-markdown", "version": "0.1.0"},
|
|
177
|
+
sources=sources,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return Pack(
|
|
181
|
+
root=str(root),
|
|
182
|
+
manifest=manifest,
|
|
183
|
+
claims=claims,
|
|
184
|
+
evidence=evidence,
|
|
185
|
+
entities=entities,
|
|
186
|
+
relations=[],
|
|
187
|
+
contradictions=[],
|
|
188
|
+
freshness={},
|
|
189
|
+
freshness_policy={"default_ttl_days": 180},
|
|
190
|
+
)
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Export an llmwiki project into a context pack.
|
|
2
|
+
|
|
3
|
+
llmwiki (https://github.com/atomicstrata/llm-wiki-compiler) compiles raw
|
|
4
|
+
sources into a markdown wiki with citation markers such as
|
|
5
|
+
``^[source.md:42-58]`` and ``[[wikilinks]]`` between concept pages.
|
|
6
|
+
|
|
7
|
+
This adapter reads that wiki as it stands. It does not import, does not
|
|
8
|
+
refresh, does not ask for API keys and does not need an LLM: it turns the
|
|
9
|
+
existing artifact into a pack so that any linter — this one included — can
|
|
10
|
+
judge it on the same terms as every other compiler's output.
|
|
11
|
+
|
|
12
|
+
The result is deliberately conservative. A page whose citation markers point
|
|
13
|
+
at a file that is not in ``sources/`` still produces an evidence record: the
|
|
14
|
+
claim is reported as uncited rather than the citation being quietly dropped,
|
|
15
|
+
because a missing citation and a silently dropped one look the same to a
|
|
16
|
+
reader and mean different things to whoever owns the pack.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import hashlib
|
|
22
|
+
import re
|
|
23
|
+
from datetime import datetime, timezone
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from ..frontmatter import as_list, as_text, parse_frontmatter, split_frontmatter
|
|
27
|
+
from ..model import CLAIM_STATUSES, Claim, Entity, Evidence, Manifest, Pack, Source
|
|
28
|
+
from ..resolve import LinkIndex, strip_wikilink
|
|
29
|
+
from ..writer import evidence_record
|
|
30
|
+
|
|
31
|
+
CITATION_RE = re.compile(r"\^\[([^\]]+)\]")
|
|
32
|
+
WIKILINK_RE = re.compile(r"\[\[([^\[\]]+?)\]\]")
|
|
33
|
+
LINE_RANGE_RE = re.compile(r"^(?P<file>[^:]+):(?P<start>\d+)(?:-(?P<end>\d+))?$")
|
|
34
|
+
|
|
35
|
+
WIKI_DIRS = ("wiki/concepts", "wiki/queries", "wiki")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _iter_pages(root: Path):
|
|
39
|
+
seen: set[Path] = set()
|
|
40
|
+
for sub in WIKI_DIRS:
|
|
41
|
+
directory = root / sub
|
|
42
|
+
if not directory.exists():
|
|
43
|
+
continue
|
|
44
|
+
for path in sorted(directory.rglob("*.md")):
|
|
45
|
+
if path in seen:
|
|
46
|
+
continue
|
|
47
|
+
seen.add(path)
|
|
48
|
+
yield path
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _paragraphs(body: str) -> list[str]:
|
|
52
|
+
chunks = re.split(r"\n\s*\n", body)
|
|
53
|
+
return [chunk.strip() for chunk in chunks if chunk.strip()]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def export_llmwiki(project: str | Path) -> Pack:
|
|
57
|
+
"""Read an llmwiki project and return a :class:`Pack`."""
|
|
58
|
+
root = Path(project).expanduser().resolve()
|
|
59
|
+
if not root.exists():
|
|
60
|
+
raise FileNotFoundError(f"llmwiki project not found: {root}")
|
|
61
|
+
|
|
62
|
+
pages = list(_iter_pages(root))
|
|
63
|
+
page_paths = [path.relative_to(root).as_posix() for path in pages]
|
|
64
|
+
index = LinkIndex(paths=page_paths)
|
|
65
|
+
|
|
66
|
+
claims: list[Claim] = []
|
|
67
|
+
evidence: list[Evidence] = []
|
|
68
|
+
entities: list[Entity] = []
|
|
69
|
+
sources: list[Source] = []
|
|
70
|
+
source_ids: dict[str, str] = {}
|
|
71
|
+
entity_ids: dict[str, str] = {}
|
|
72
|
+
evidence_counter = 0
|
|
73
|
+
|
|
74
|
+
def source_id_for(filename: str) -> str:
|
|
75
|
+
key = filename.strip()
|
|
76
|
+
if key not in source_ids:
|
|
77
|
+
source_ids[key] = f"src-{len(source_ids) + 1:03d}"
|
|
78
|
+
sources.append(
|
|
79
|
+
Source(
|
|
80
|
+
id=source_ids[key],
|
|
81
|
+
path=f"sources/{key}",
|
|
82
|
+
title=key,
|
|
83
|
+
kind="file",
|
|
84
|
+
sha256=None,
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
return source_ids[key]
|
|
88
|
+
|
|
89
|
+
for number, path in enumerate(pages, start=1):
|
|
90
|
+
rel = path.relative_to(root).as_posix()
|
|
91
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
92
|
+
front = parse_frontmatter(text)
|
|
93
|
+
_, body = split_frontmatter(text)
|
|
94
|
+
|
|
95
|
+
claim_id = f"clm-{number:03d}"
|
|
96
|
+
title = as_text(front.get("title")).strip() or path.stem
|
|
97
|
+
summary = as_text(front.get("summary")).strip()
|
|
98
|
+
|
|
99
|
+
confidence = front.get("confidence")
|
|
100
|
+
if not isinstance(confidence, (int, float)) or isinstance(confidence, bool):
|
|
101
|
+
confidence = None
|
|
102
|
+
|
|
103
|
+
raw_status = as_text(front.get("status")).strip().lower()
|
|
104
|
+
status = raw_status if raw_status in CLAIM_STATUSES else "asserted"
|
|
105
|
+
|
|
106
|
+
declared_sources = as_list(front.get("sources"))
|
|
107
|
+
evidence_ids: list[str] = []
|
|
108
|
+
|
|
109
|
+
for paragraph in _paragraphs(body):
|
|
110
|
+
citations = CITATION_RE.findall(paragraph)
|
|
111
|
+
if not citations:
|
|
112
|
+
continue
|
|
113
|
+
quote = CITATION_RE.sub("", paragraph).strip()
|
|
114
|
+
quote = re.sub(r"\s+", " ", quote)[:1200]
|
|
115
|
+
for citation in citations:
|
|
116
|
+
match = LINE_RANGE_RE.match(citation.strip())
|
|
117
|
+
filename = match.group("file") if match else citation.strip()
|
|
118
|
+
line_start = int(match.group("start")) if match else None
|
|
119
|
+
line_end = int(match.group("end")) if match and match.group("end") else line_start
|
|
120
|
+
locator = {"path": f"sources/{filename.strip()}"}
|
|
121
|
+
if line_start is not None:
|
|
122
|
+
locator["line_start"] = line_start
|
|
123
|
+
locator["line_end"] = line_end or line_start
|
|
124
|
+
evidence_counter += 1
|
|
125
|
+
evidence_id = f"ev-{evidence_counter:03d}"
|
|
126
|
+
record = evidence_record(
|
|
127
|
+
evidence_id,
|
|
128
|
+
claim_id,
|
|
129
|
+
source_id_for(filename),
|
|
130
|
+
quote or title,
|
|
131
|
+
locator=locator,
|
|
132
|
+
retrieved_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
133
|
+
)
|
|
134
|
+
evidence.append(Evidence.from_dict(record))
|
|
135
|
+
evidence_ids.append(evidence_id)
|
|
136
|
+
|
|
137
|
+
links = []
|
|
138
|
+
for raw in WIKILINK_RE.findall(body):
|
|
139
|
+
target = strip_wikilink(raw)
|
|
140
|
+
if not target:
|
|
141
|
+
continue
|
|
142
|
+
links.append({"target": raw, "resolved": index.resolve(raw)})
|
|
143
|
+
|
|
144
|
+
claim_entities: list[str] = []
|
|
145
|
+
for name in as_list(front.get("entities")):
|
|
146
|
+
key = name.strip().lower()
|
|
147
|
+
if not key:
|
|
148
|
+
continue
|
|
149
|
+
if key not in entity_ids:
|
|
150
|
+
entity_id = f"ent-{len(entity_ids) + 1:03d}"
|
|
151
|
+
entity_ids[key] = entity_id
|
|
152
|
+
entities.append(Entity(id=entity_id, name=name.strip()))
|
|
153
|
+
claim_entities.append(entity_ids[key])
|
|
154
|
+
|
|
155
|
+
topics = as_list(front.get("topics")) or ([as_text(front.get("kind")).strip()] if front.get("kind") else [])
|
|
156
|
+
|
|
157
|
+
claims.append(
|
|
158
|
+
Claim(
|
|
159
|
+
id=claim_id,
|
|
160
|
+
text=summary or title,
|
|
161
|
+
status=status,
|
|
162
|
+
confidence=float(confidence) if confidence is not None else None,
|
|
163
|
+
evidence=evidence_ids,
|
|
164
|
+
entities=claim_entities,
|
|
165
|
+
topics=topics,
|
|
166
|
+
links=links,
|
|
167
|
+
created_at=as_text(front.get("createdAt")).strip() or None,
|
|
168
|
+
updated_at=as_text(front.get("updatedAt")).strip() or None,
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
for filename in declared_sources:
|
|
173
|
+
source_id_for(filename)
|
|
174
|
+
|
|
175
|
+
manifest = Manifest(
|
|
176
|
+
pack_id=root.name,
|
|
177
|
+
title=f"{root.name} (llmwiki export)",
|
|
178
|
+
description=f"Context pack exported from the llmwiki project at {root}.",
|
|
179
|
+
generated_at=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
180
|
+
generator={"name": "ctxverify-adapter-llmwiki", "version": "0.1.0"},
|
|
181
|
+
sources=sources,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
for source in sources:
|
|
185
|
+
candidate = root / source.path
|
|
186
|
+
if candidate.exists():
|
|
187
|
+
source.sha256 = hashlib.sha256(candidate.read_bytes()).hexdigest()
|
|
188
|
+
|
|
189
|
+
return Pack(
|
|
190
|
+
root=str(root),
|
|
191
|
+
manifest=manifest,
|
|
192
|
+
claims=claims,
|
|
193
|
+
evidence=evidence,
|
|
194
|
+
entities=entities,
|
|
195
|
+
relations=[],
|
|
196
|
+
contradictions=[],
|
|
197
|
+
freshness={},
|
|
198
|
+
freshness_policy={"default_ttl_days": 180},
|
|
199
|
+
)
|