scriptoria 0.7.0__tar.gz → 0.8.1__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.
- {scriptoria-0.7.0 → scriptoria-0.8.1}/.gitignore +4 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/PKG-INFO +1 -1
- {scriptoria-0.7.0 → scriptoria-0.8.1}/pyproject.toml +1 -1
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/__init__.py +1 -1
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/anchors.py +21 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/facts.py +47 -11
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_fact_cmd.py +73 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_facts.py +14 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_verify.py +38 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/uv.lock +1 -1
- {scriptoria-0.7.0 → scriptoria-0.8.1}/README.md +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/pyrightconfig.json +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/blocks.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/cli.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/embeddings.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/errors.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/frontmatter.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/graph.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/hashing.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/ingest.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/lock.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/manifest.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/pageindex_adapter.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/query.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/retrieval.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/src/scrip/similar.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/conftest.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_anchor_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_anchors.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_blocks.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_embeddings.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_frontmatter.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_graph_status.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_hashing.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_index_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_ingest.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_json_shapes.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_lock.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_manifest.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_new_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_pageindex_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_query.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_retrieval.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_similar_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_span_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_stamp.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_status_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_unlock_cmd.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_version.py +0 -0
- {scriptoria-0.7.0 → scriptoria-0.8.1}/tests/test_watch.py +0 -0
|
@@ -15,6 +15,10 @@ build/
|
|
|
15
15
|
# OS
|
|
16
16
|
.DS_Store
|
|
17
17
|
|
|
18
|
+
# Secrets: harness wiki-publish credentials (gh_username/password/wikirepo).
|
|
19
|
+
# Never commit. The real file lives with the private KB, not in this repo.
|
|
20
|
+
.env
|
|
21
|
+
|
|
18
22
|
# Embeddings index: a large, regenerable binary cache (rebuild with `scrip index`)
|
|
19
23
|
/.kb/embeddings/
|
|
20
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scriptoria
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: Deterministic scriptorium-keeper (the `scrip` CLI): staleness, provenance integrity, and fact queries for an agent-compiled knowledge base
|
|
5
5
|
Project-URL: Homepage, https://github.com/coredipper/scriptorium
|
|
6
6
|
Project-URL: Changelog, https://github.com/coredipper/scriptorium/blob/main/CHANGELOG.md
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Distribution name on PyPI is `scriptoria` (scrip/scriptorium were taken); the
|
|
3
3
|
# CLI command and the import package both remain `scrip`.
|
|
4
4
|
name = "scriptoria"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.8.1"
|
|
6
6
|
description = "Deterministic scriptorium-keeper (the `scrip` CLI): staleness, provenance integrity, and fact queries for an agent-compiled knowledge base"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
requires-python = ">=3.10"
|
|
@@ -13,7 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
|
-
__version__ = "0.
|
|
16
|
+
__version__ = "0.8.1"
|
|
17
17
|
|
|
18
18
|
# --- canonical vault layout ------------------------------------------------
|
|
19
19
|
# ``root`` is the repo/instance root: the directory containing ``vault/``.
|
|
@@ -199,6 +199,27 @@ def verify_vault(root: Path) -> dict:
|
|
|
199
199
|
text = _source_text(root, rec["source_id"], src_cache)
|
|
200
200
|
record(f"claim:{cid}", rec, resolve(text, rec["anchor"]))
|
|
201
201
|
|
|
202
|
+
# cited edges (graph.ndjson): an edge that carries an anchor is verified like
|
|
203
|
+
# a claim; a bare {src,dst,kind} edge has no citation to resolve and is skipped.
|
|
204
|
+
edges_path = facts_dir(root) / "graph.ndjson"
|
|
205
|
+
if edges_path.exists():
|
|
206
|
+
with edges_path.open(encoding="utf-8") as f:
|
|
207
|
+
for lineno, raw_line in enumerate(f, start=1):
|
|
208
|
+
line = raw_line.strip()
|
|
209
|
+
if not line:
|
|
210
|
+
continue
|
|
211
|
+
try:
|
|
212
|
+
rec = json.loads(line)
|
|
213
|
+
except json.JSONDecodeError as e:
|
|
214
|
+
raise DataError(f"graph.ndjson:{lineno}: invalid JSON: {e}") from e
|
|
215
|
+
if "anchor" not in rec:
|
|
216
|
+
continue
|
|
217
|
+
if "source_id" not in rec:
|
|
218
|
+
raise DataError(f"graph.ndjson:{lineno}: cited edge missing 'source_id'")
|
|
219
|
+
text = _source_text(root, rec["source_id"], src_cache)
|
|
220
|
+
where = f"edge:{rec.get('src')}->{rec.get('dst')} ({rec.get('kind')})"
|
|
221
|
+
record(where, rec, resolve(text, rec["anchor"]))
|
|
222
|
+
|
|
202
223
|
wd = wiki_dir(root)
|
|
203
224
|
if wd.is_dir():
|
|
204
225
|
for path in sorted(wd.rglob("*.md")):
|
|
@@ -21,6 +21,7 @@ from __future__ import annotations
|
|
|
21
21
|
|
|
22
22
|
import json
|
|
23
23
|
import re
|
|
24
|
+
from collections.abc import Iterator
|
|
24
25
|
from datetime import datetime, timezone
|
|
25
26
|
from pathlib import Path
|
|
26
27
|
|
|
@@ -53,7 +54,10 @@ _CLAIM_ALLOWED = frozenset((*_CLAIM_REQUIRED, "claim_text", "tags"))
|
|
|
53
54
|
_ENTITY_REQUIRED = ("entity_id", "name", "kind")
|
|
54
55
|
_ENTITY_ALLOWED = frozenset((*_ENTITY_REQUIRED, "tags"))
|
|
55
56
|
_EDGE_REQUIRED = ("src", "dst", "kind")
|
|
56
|
-
|
|
57
|
+
# An edge may optionally be *cited*: a verbatim ``quote`` + ``source_id`` whose
|
|
58
|
+
# ``anchor`` scrip mints+verifies exactly as for a claim. Bare edges stay
|
|
59
|
+
# structural. ``anchor`` remains scrip-owned (proposing it is a schema error).
|
|
60
|
+
_EDGE_ALLOWED = frozenset((*_EDGE_REQUIRED, "quote", "source_id"))
|
|
57
61
|
_DECISIONS = ("supersede", "qualify", "keep-both")
|
|
58
62
|
_RECON_REQUIRED = ("decision", "claim_a", "claim_b")
|
|
59
63
|
_RECON_ALLOWED = frozenset((*_RECON_REQUIRED, "winner", "rationale"))
|
|
@@ -73,6 +77,18 @@ def _now() -> str:
|
|
|
73
77
|
# --------------------------------------------------------------------------- #
|
|
74
78
|
# Input parsing & structural validation (DataError, exit 3)
|
|
75
79
|
# --------------------------------------------------------------------------- #
|
|
80
|
+
def _iter_lf_lines(text: str) -> Iterator[str]:
|
|
81
|
+
"""Yield ``text.split("\n")``-equivalent lines without allocating the list."""
|
|
82
|
+
start = 0
|
|
83
|
+
while True:
|
|
84
|
+
end = text.find("\n", start)
|
|
85
|
+
if end == -1:
|
|
86
|
+
yield text[start:]
|
|
87
|
+
return
|
|
88
|
+
yield text[start:end]
|
|
89
|
+
start = end + 1
|
|
90
|
+
|
|
91
|
+
|
|
76
92
|
def parse_ndjson(text: str) -> list[dict]:
|
|
77
93
|
"""Parse proposed records (one JSON object per line). Malformed input is a
|
|
78
94
|
:class:`DataError` with its line number; an empty input is a usage error."""
|
|
@@ -81,7 +97,7 @@ def parse_ndjson(text: str) -> list[dict]:
|
|
|
81
97
|
# delimited and may legally contain U+2028/U+2029/NEL inside a JSON string,
|
|
82
98
|
# which splitlines() would wrongly treat as record breaks. trailing \r (from
|
|
83
99
|
# \r\n) and the trailing empty element from a final newline are dropped below.
|
|
84
|
-
for lineno, raw_line in enumerate(text
|
|
100
|
+
for lineno, raw_line in enumerate(_iter_lf_lines(text), start=1):
|
|
85
101
|
line = raw_line.strip()
|
|
86
102
|
if not line:
|
|
87
103
|
continue
|
|
@@ -161,6 +177,17 @@ def _validate(table: str, rec: dict, index: int) -> None:
|
|
|
161
177
|
_check_shape(rec, index, _EDGE_REQUIRED, _EDGE_ALLOWED)
|
|
162
178
|
for key in _EDGE_REQUIRED:
|
|
163
179
|
_check_str(rec, key, index)
|
|
180
|
+
if "quote" in rec or "source_id" in rec:
|
|
181
|
+
# both or neither: a quote needs a source to anchor against
|
|
182
|
+
missing = [k for k in ("quote", "source_id") if k not in rec]
|
|
183
|
+
if missing:
|
|
184
|
+
raise DataError(
|
|
185
|
+
f"record {index}: a cited edge needs both 'quote' and 'source_id' "
|
|
186
|
+
f"(missing: {', '.join(missing)})"
|
|
187
|
+
)
|
|
188
|
+
_check_str(rec, "source_id", index)
|
|
189
|
+
# quote emptiness is a per-record finding (EMPTY_QUOTE), not a schema error
|
|
190
|
+
_check_str(rec, "quote", index, allow_blank=True)
|
|
164
191
|
else: # reconciliations
|
|
165
192
|
_check_shape(rec, index, _RECON_REQUIRED, _RECON_ALLOWED, owned=_RECON_OWNED)
|
|
166
193
|
for key in ("decision", "claim_a", "claim_b"):
|
|
@@ -390,13 +417,16 @@ def add(root: Path, table: str, proposals: list[dict]) -> dict:
|
|
|
390
417
|
resolved: list[dict | None] = [None] * len(proposals)
|
|
391
418
|
path = facts_dir(root) / _FILES[table]
|
|
392
419
|
with lock.write_lock(root):
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
420
|
+
# Resolve quotes INSIDE the lock: raw/ only changes via a *locked*
|
|
421
|
+
# `ingest --reingest`, so holding the lock from verification through
|
|
422
|
+
# append closes the window where a re-ingest could land between the
|
|
423
|
+
# two and silently break the just-minted anchors. Claims always carry a
|
|
424
|
+
# quote; an edge does only when it is *cited*.
|
|
425
|
+
if table == "claims" or table == "edges":
|
|
398
426
|
src_cache: dict[str, str | None] = {}
|
|
399
427
|
for i, rec in enumerate(proposals):
|
|
428
|
+
if table == "edges" and "quote" not in rec:
|
|
429
|
+
continue # bare edge: nothing to anchor
|
|
400
430
|
fail, res = _resolve_claim(root, rec, i, src_cache)
|
|
401
431
|
if fail:
|
|
402
432
|
failures.append(fail)
|
|
@@ -478,7 +508,12 @@ def add(root: Path, table: str, proposals: list[dict]) -> dict:
|
|
|
478
508
|
skipped.append({"index": i, "reason": "duplicate", "existing_id": None})
|
|
479
509
|
continue
|
|
480
510
|
seen_edges.add(key)
|
|
481
|
-
|
|
511
|
+
full = {"src": rec["src"], "dst": rec["dst"], "kind": rec["kind"]}
|
|
512
|
+
res = resolved[i]
|
|
513
|
+
if res is not None: # cited edge: carry the minted provenance
|
|
514
|
+
full["source_id"] = res["source_id"]
|
|
515
|
+
full["anchor"] = res["anchor"]
|
|
516
|
+
appended.append(full)
|
|
482
517
|
else: # reconciliations
|
|
483
518
|
claim_ids = {c.get("claim_id") for c in _read_table(facts_dir(root) / "claims.ndjson")[0]}
|
|
484
519
|
for i, rec in enumerate(proposals):
|
|
@@ -528,10 +563,11 @@ def add(root: Path, table: str, proposals: list[dict]) -> dict:
|
|
|
528
563
|
f.write("\n")
|
|
529
564
|
f.write(payload)
|
|
530
565
|
new_sources: list[str] = []
|
|
531
|
-
if table
|
|
566
|
+
if table in ("claims", "edges"):
|
|
532
567
|
for rec in appended:
|
|
533
|
-
|
|
534
|
-
|
|
568
|
+
sid = rec.get("source_id") # cited edges carry one; bare edges don't
|
|
569
|
+
if sid and sid not in new_sources:
|
|
570
|
+
new_sources.append(sid)
|
|
535
571
|
_write_meta(root, meta, new_sources)
|
|
536
572
|
|
|
537
573
|
return {"table": table, "appended": appended, "skipped": skipped, "failures": []}
|
|
@@ -31,6 +31,12 @@ def _claim(quote, source="raw/s", **kw):
|
|
|
31
31
|
return rec
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _edge(src="entity/a", dst="entity/b", kind="relates-to", **kw):
|
|
35
|
+
rec = {"src": src, "dst": dst, "kind": kind}
|
|
36
|
+
rec.update(kw)
|
|
37
|
+
return rec
|
|
38
|
+
|
|
39
|
+
|
|
34
40
|
def _ndjson(*recs):
|
|
35
41
|
return "".join(json.dumps(r, ensure_ascii=False) + "\n" for r in recs)
|
|
36
42
|
|
|
@@ -55,6 +61,13 @@ def _recs_lines(kb):
|
|
|
55
61
|
return [json.loads(s) for s in p.read_text(encoding="utf-8").splitlines() if s.strip()]
|
|
56
62
|
|
|
57
63
|
|
|
64
|
+
def _graph_lines(kb):
|
|
65
|
+
p = kb.root / "vault" / "facts" / "graph.ndjson"
|
|
66
|
+
if not p.exists():
|
|
67
|
+
return []
|
|
68
|
+
return [json.loads(s) for s in p.read_text(encoding="utf-8").splitlines() if s.strip()]
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
def _two_claims(kb):
|
|
59
72
|
"""Seed a contradiction pair to reconcile."""
|
|
60
73
|
kb.add_raw("s", SRC)
|
|
@@ -476,3 +489,63 @@ def test_fact_add_edges_appends_and_skips_duplicates(kb):
|
|
|
476
489
|
assert _run_add(kb, _ndjson(edge), "--table", "edges") == 0
|
|
477
490
|
lines = (kb.root / "vault" / "facts" / "graph.ndjson").read_text(encoding="utf-8")
|
|
478
491
|
assert len(lines.splitlines()) == 1
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
# --------------------------------------------------------------------------- #
|
|
495
|
+
# Cited edges: an edge may carry a verbatim quote + source_id; scrip mints and
|
|
496
|
+
# verifies an anchor for it exactly as it does for a claim. Bare edges still work.
|
|
497
|
+
# --------------------------------------------------------------------------- #
|
|
498
|
+
def test_fact_add_bare_edge_carries_no_provenance(kb):
|
|
499
|
+
# additivity guard: an edge with no quote stays purely structural
|
|
500
|
+
assert _run_add(kb, _ndjson(_edge()), "--table", "edges") == 0
|
|
501
|
+
[edge] = _graph_lines(kb)
|
|
502
|
+
assert set(edge) == {"src", "dst", "kind"}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def test_fact_add_cited_edge_mints_and_verifies_anchor(kb):
|
|
506
|
+
kb.add_raw("s", SRC)
|
|
507
|
+
rc = _run_add(
|
|
508
|
+
kb,
|
|
509
|
+
_ndjson(_edge(quote="Caching answers beats recomputing them.", source_id="raw/s")),
|
|
510
|
+
"--table", "edges",
|
|
511
|
+
)
|
|
512
|
+
assert rc == 0
|
|
513
|
+
[edge] = _graph_lines(kb)
|
|
514
|
+
assert (edge["src"], edge["dst"], edge["kind"]) == ("entity/a", "entity/b", "relates-to")
|
|
515
|
+
assert edge["source_id"] == "raw/s"
|
|
516
|
+
# the minted anchor resolves uniquely in the source, and scrip verify confirms it
|
|
517
|
+
assert anchors.resolve(SRC, edge["anchor"]) == "OK"
|
|
518
|
+
assert cli.main(["verify", "--root", str(kb.root)]) == 0
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def test_fact_add_cited_edge_broken_quote_fails_batch(kb, capsys):
|
|
522
|
+
kb.add_raw("s", SRC)
|
|
523
|
+
rc = _run_add(
|
|
524
|
+
kb,
|
|
525
|
+
_ndjson(_edge(quote="this sentence is absent from the source", source_id="raw/s")),
|
|
526
|
+
"--table", "edges", "--json",
|
|
527
|
+
)
|
|
528
|
+
assert rc == 1
|
|
529
|
+
assert _graph_lines(kb) == [] # all-or-nothing: nothing written
|
|
530
|
+
[failure] = json.loads(capsys.readouterr().out)["failures"]
|
|
531
|
+
assert failure["status"] == "BROKEN"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def test_fact_add_cited_edge_requires_both_quote_and_source_id(kb):
|
|
535
|
+
kb.add_raw("s", SRC)
|
|
536
|
+
# a quote needs a source to anchor against, and vice versa — both or neither
|
|
537
|
+
assert _run_add(kb, _ndjson(_edge(quote="Caching answers beats recomputing them.")),
|
|
538
|
+
"--table", "edges") == 3
|
|
539
|
+
assert _run_add(kb, _ndjson(_edge(source_id="raw/s")), "--table", "edges") == 3
|
|
540
|
+
assert _graph_lines(kb) == []
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def test_fact_add_cited_edge_dedups_on_triple(kb, capsys):
|
|
544
|
+
kb.add_raw("s", SRC)
|
|
545
|
+
cited = _ndjson(_edge(quote="Caching answers beats recomputing them.", source_id="raw/s"))
|
|
546
|
+
assert _run_add(kb, cited, "--table", "edges") == 0
|
|
547
|
+
capsys.readouterr()
|
|
548
|
+
# same (src,dst,kind) again — even bare — is the same relation, so it's skipped
|
|
549
|
+
assert _run_add(kb, _ndjson(_edge()), "--table", "edges", "--json") == 0
|
|
550
|
+
assert len(_graph_lines(kb)) == 1
|
|
551
|
+
assert json.loads(capsys.readouterr().out)["skipped"][0]["reason"] == "duplicate"
|
|
@@ -4,6 +4,20 @@ from scrip.errors import DataError, UsageError
|
|
|
4
4
|
from scrip import facts
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
def test_iter_lf_lines_matches_split_newline_semantics():
|
|
8
|
+
samples = [
|
|
9
|
+
"",
|
|
10
|
+
"one",
|
|
11
|
+
"one\n",
|
|
12
|
+
"one\n\n",
|
|
13
|
+
"one\r\ntwo\r\n",
|
|
14
|
+
"one\rtwo",
|
|
15
|
+
"one\u2028two\ntwo\u2029three",
|
|
16
|
+
]
|
|
17
|
+
for text in samples:
|
|
18
|
+
assert list(facts._iter_lf_lines(text)) == text.split("\n")
|
|
19
|
+
|
|
20
|
+
|
|
7
21
|
def test_parse_ndjson_basic():
|
|
8
22
|
text = '{"a": 1}\n{"b": 2}\n'
|
|
9
23
|
assert facts.parse_ndjson(text) == [{"a": 1}, {"b": 2}]
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
import pytest
|
|
2
4
|
from scrip.errors import DataError
|
|
3
5
|
|
|
4
6
|
from scrip import anchors, cli
|
|
5
7
|
|
|
6
8
|
|
|
9
|
+
def _write_edge(kb, rec):
|
|
10
|
+
p = kb.root / "vault" / "facts" / "graph.ndjson"
|
|
11
|
+
with open(p, "a", encoding="utf-8") as f:
|
|
12
|
+
f.write(json.dumps(rec, ensure_ascii=False) + "\n")
|
|
13
|
+
|
|
14
|
+
|
|
7
15
|
def test_cli_verify_fails_on_ambiguous_by_default(kb):
|
|
8
16
|
# a quote that occurs twice in the source -> AMBIGUOUS
|
|
9
17
|
kb.add_raw("a", "# A\n\nalpha beta gamma. alpha beta gamma.\n")
|
|
@@ -62,3 +70,33 @@ def test_verify_wiki_footnote_anchor(kb):
|
|
|
62
70
|
res = anchors.verify_vault(kb.root)
|
|
63
71
|
assert res["ok"] == 1
|
|
64
72
|
assert res["broken"] == []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_verify_cited_edge_anchor_resolves(kb):
|
|
76
|
+
src = "# A\n\nThe sky is blue today over the hills.\n"
|
|
77
|
+
kb.add_raw("a", src)
|
|
78
|
+
anchor = anchors.make_anchor(src, "the sky is blue today")
|
|
79
|
+
_write_edge(kb, {"src": "entity/sky", "dst": "entity/color", "kind": "is",
|
|
80
|
+
"source_id": "raw/a", "anchor": anchor})
|
|
81
|
+
res = anchors.verify_vault(kb.root)
|
|
82
|
+
assert res["ok"] == 1
|
|
83
|
+
assert res["broken"] == []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_verify_broken_cited_edge_anchor_is_listed(kb):
|
|
87
|
+
kb.add_raw("a", "# A\n\nThe sky is blue.\n")
|
|
88
|
+
bogus = "qh:" + ("0" * 64) + "|loc:0.0|len:10"
|
|
89
|
+
_write_edge(kb, {"src": "entity/a", "dst": "entity/b", "kind": "rel",
|
|
90
|
+
"source_id": "raw/a", "anchor": bogus})
|
|
91
|
+
res = anchors.verify_vault(kb.root)
|
|
92
|
+
assert len(res["broken"]) == 1
|
|
93
|
+
assert res["broken"][0]["where"].startswith("edge:")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_verify_bare_edge_is_not_checked(kb):
|
|
97
|
+
# an uncited edge has nothing to resolve, so verify ignores it (additive)
|
|
98
|
+
kb.add_raw("a", "# A\n\nThe sky is blue.\n")
|
|
99
|
+
_write_edge(kb, {"src": "entity/a", "dst": "entity/b", "kind": "rel"})
|
|
100
|
+
res = anchors.verify_vault(kb.root)
|
|
101
|
+
assert res["checked"] == 0
|
|
102
|
+
assert res["broken"] == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|