scriptoria 0.9.0__tar.gz → 0.10.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.
- {scriptoria-0.9.0 → scriptoria-0.10.0}/PKG-INFO +5 -3
- {scriptoria-0.9.0 → scriptoria-0.10.0}/README.md +3 -1
- {scriptoria-0.9.0 → scriptoria-0.10.0}/pyproject.toml +1 -1
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/__init__.py +1 -1
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/blocks.py +29 -27
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/cli.py +80 -6
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/facts.py +5 -8
- scriptoria-0.10.0/tests/test_init_cmd.py +102 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/uv.lock +1 -1
- {scriptoria-0.9.0 → scriptoria-0.10.0}/.gitignore +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/pyrightconfig.json +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/anchors.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/embeddings.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/errors.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/frontmatter.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/graph.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/hashing.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/ingest.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/lock.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/manifest.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/ontology.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/pageindex_adapter.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/query.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/retrieval.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/src/scrip/similar.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/conftest.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_anchor_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_anchors.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_blocks.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_embeddings.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_fact_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_facts.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_frontmatter.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_graph_status.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_hashing.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_index_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_ingest.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_json_shapes.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_lock.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_manifest.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_new_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_pageindex_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_query.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_retrieval.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_similar_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_span_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_stamp.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_status_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_unlock_cmd.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_verify.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_version.py +0 -0
- {scriptoria-0.9.0 → scriptoria-0.10.0}/tests/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: scriptoria
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
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
|
|
@@ -51,6 +51,7 @@ uv run --project scrip scrip --help
|
|
|
51
51
|
|
|
52
52
|
| Command | What it does |
|
|
53
53
|
|---|---|
|
|
54
|
+
| `scrip init [DIR]` | Create the root skeleton — `vault/{raw,facts,wiki}/` plus the `.kb/` cache and its manifest — in `DIR` (default: the current directory). Idempotent in an existing root; refuses to nest a new instance inside one. |
|
|
54
55
|
| `scrip status` | Report `STALE` / `OK` / `UNCOMPILED` artifacts from the dependency graph. `--no-cache` recomputes from files; `--rebuild-manifest` regenerates the cache. |
|
|
55
56
|
| `scrip verify` | Check every provenance anchor still resolves to text in its source; check referenced sources exist and `claim_id`s are unique. Fails on `BROKEN` and `AMBIGUOUS` by default; `--allow-ambiguous` downgrades `AMBIGUOUS` to a warning. |
|
|
56
57
|
| `scrip query [claims\|entities\|edges\|contradictions\|reconciliations]` | Structured query over `vault/facts/*.ndjson` via DuckDB. `--sql "<duckdb>"`, `--where`, `--limit`. |
|
|
@@ -67,7 +68,8 @@ uv run --project scrip scrip --help
|
|
|
67
68
|
| `scrip pageindex search "<question>" [--source raw/<slug>]` | Search cached PageIndex sections, returning only snippets mapped back to canonical `vault/raw/` text. |
|
|
68
69
|
| `scrip watch` / `scrip unlock` | Watch vault health in a poll loop; clear a stale advisory write lock. |
|
|
69
70
|
|
|
70
|
-
Every command accepts `--root DIR` and `--json
|
|
71
|
+
Every command accepts `--root DIR` and `--json` (`init` takes its target as a
|
|
72
|
+
positional path instead of `--root`).
|
|
71
73
|
|
|
72
74
|
The optional `scrip-harness` package adds the model-bearing commands, including
|
|
73
75
|
`scrip-harness answer "<question>"`, which gathers evidence with these primitives
|
|
@@ -20,6 +20,7 @@ uv run --project scrip scrip --help
|
|
|
20
20
|
|
|
21
21
|
| Command | What it does |
|
|
22
22
|
|---|---|
|
|
23
|
+
| `scrip init [DIR]` | Create the root skeleton — `vault/{raw,facts,wiki}/` plus the `.kb/` cache and its manifest — in `DIR` (default: the current directory). Idempotent in an existing root; refuses to nest a new instance inside one. |
|
|
23
24
|
| `scrip status` | Report `STALE` / `OK` / `UNCOMPILED` artifacts from the dependency graph. `--no-cache` recomputes from files; `--rebuild-manifest` regenerates the cache. |
|
|
24
25
|
| `scrip verify` | Check every provenance anchor still resolves to text in its source; check referenced sources exist and `claim_id`s are unique. Fails on `BROKEN` and `AMBIGUOUS` by default; `--allow-ambiguous` downgrades `AMBIGUOUS` to a warning. |
|
|
25
26
|
| `scrip query [claims\|entities\|edges\|contradictions\|reconciliations]` | Structured query over `vault/facts/*.ndjson` via DuckDB. `--sql "<duckdb>"`, `--where`, `--limit`. |
|
|
@@ -36,7 +37,8 @@ uv run --project scrip scrip --help
|
|
|
36
37
|
| `scrip pageindex search "<question>" [--source raw/<slug>]` | Search cached PageIndex sections, returning only snippets mapped back to canonical `vault/raw/` text. |
|
|
37
38
|
| `scrip watch` / `scrip unlock` | Watch vault health in a poll loop; clear a stale advisory write lock. |
|
|
38
39
|
|
|
39
|
-
Every command accepts `--root DIR` and `--json
|
|
40
|
+
Every command accepts `--root DIR` and `--json` (`init` takes its target as a
|
|
41
|
+
positional path instead of `--root`).
|
|
40
42
|
|
|
41
43
|
The optional `scrip-harness` package adds the model-bearing commands, including
|
|
42
44
|
`scrip-harness answer "<question>"`, which gathers evidence with these primitives
|
|
@@ -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.10.0"
|
|
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.10.0"
|
|
17
17
|
|
|
18
18
|
# --- canonical vault layout ------------------------------------------------
|
|
19
19
|
# ``root`` is the repo/instance root: the directory containing ``vault/``.
|
|
@@ -38,43 +38,45 @@ def split_blocks(text: str) -> list[dict]:
|
|
|
38
38
|
# Use splitlines(keepends=True): its boundary set (form feed, NEL, U+2028/9,
|
|
39
39
|
# …) is part of the deterministic segmentation contract — io.StringIO only
|
|
40
40
|
# splits on \r/\n and would re-hash existing blocks that contain those chars.
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
# Emit blocks in a single pass: a running [cur_start, cur_end] paragraph is
|
|
42
|
+
# flushed at each boundary, so no intermediate `ranges` list is allocated.
|
|
43
|
+
out: list[dict] = []
|
|
44
|
+
seen: dict[str, int] = {}
|
|
45
|
+
|
|
46
|
+
def emit(s: int, e: int) -> None:
|
|
47
|
+
slice_text = text[s:e]
|
|
48
|
+
out.append(
|
|
49
|
+
{
|
|
50
|
+
"block_id": _block_id(slice_text, seen),
|
|
51
|
+
"span": [s, e],
|
|
52
|
+
"hash": hashing.sha256_text(slice_text),
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cur_start: int | None = None
|
|
57
|
+
cur_end = 0
|
|
43
58
|
start = 0
|
|
44
59
|
|
|
45
60
|
for line in text.splitlines(keepends=True):
|
|
46
61
|
end = start + len(line)
|
|
47
62
|
if _is_blank(line):
|
|
48
|
-
if
|
|
49
|
-
|
|
50
|
-
|
|
63
|
+
if cur_start is not None:
|
|
64
|
+
emit(cur_start, cur_end)
|
|
65
|
+
cur_start = None
|
|
51
66
|
elif _is_heading(line):
|
|
52
|
-
if
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
if cur_start is not None:
|
|
68
|
+
emit(cur_start, cur_end)
|
|
69
|
+
cur_start = None
|
|
70
|
+
emit(start, end) # a heading is its own block
|
|
56
71
|
else:
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
cur[1] = end
|
|
72
|
+
if cur_start is None:
|
|
73
|
+
cur_start = start
|
|
74
|
+
cur_end = end
|
|
61
75
|
start = end
|
|
62
76
|
|
|
63
|
-
if
|
|
64
|
-
|
|
77
|
+
if cur_start is not None:
|
|
78
|
+
emit(cur_start, cur_end)
|
|
65
79
|
|
|
66
|
-
out: list[dict] = []
|
|
67
|
-
seen: dict[str, int] = {}
|
|
68
|
-
for s, e in ranges:
|
|
69
|
-
slice_text = text[s:e]
|
|
70
|
-
block_id = _block_id(slice_text, seen)
|
|
71
|
-
out.append(
|
|
72
|
-
{
|
|
73
|
-
"block_id": block_id,
|
|
74
|
-
"span": [s, e],
|
|
75
|
-
"hash": hashing.sha256_text(slice_text),
|
|
76
|
-
}
|
|
77
|
-
)
|
|
78
80
|
return out
|
|
79
81
|
|
|
80
82
|
|
|
@@ -35,12 +35,20 @@ def _safe_slug(slug: str, what: str = "slug") -> str:
|
|
|
35
35
|
# --------------------------------------------------------------------------- #
|
|
36
36
|
# Root resolution
|
|
37
37
|
# --------------------------------------------------------------------------- #
|
|
38
|
+
def _looks_like_root(cand: Path) -> bool:
|
|
39
|
+
"""True if ``cand`` looks like a scriptorium instance root: a directory
|
|
40
|
+
containing ``vault/`` plus either ``SPEC.md`` or ``.kb/``."""
|
|
41
|
+
return (cand / "vault").is_dir() and (
|
|
42
|
+
(cand / "SPEC.md").exists() or (cand / ".kb").is_dir()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
38
46
|
def resolve_root(root_arg: str | None) -> Path:
|
|
39
47
|
"""Return the scriptorium root.
|
|
40
48
|
|
|
41
49
|
With ``--root`` the path is used verbatim (must be a directory). Otherwise we
|
|
42
|
-
walk up from the cwd for the nearest ancestor that looks like an instance
|
|
43
|
-
|
|
50
|
+
walk up from the cwd for the nearest ancestor that looks like an instance
|
|
51
|
+
(see :func:`_looks_like_root`).
|
|
44
52
|
"""
|
|
45
53
|
if root_arg:
|
|
46
54
|
root = Path(root_arg).expanduser().resolve()
|
|
@@ -50,13 +58,12 @@ def resolve_root(root_arg: str | None) -> Path:
|
|
|
50
58
|
|
|
51
59
|
cur = Path.cwd().resolve()
|
|
52
60
|
for cand in (cur, *cur.parents):
|
|
53
|
-
if (cand
|
|
54
|
-
(cand / "SPEC.md").exists() or (cand / ".kb").is_dir()
|
|
55
|
-
):
|
|
61
|
+
if _looks_like_root(cand):
|
|
56
62
|
return cand
|
|
57
63
|
raise errors.UsageError(
|
|
58
64
|
"could not locate a scriptorium root (looked for a parent with vault/ and "
|
|
59
|
-
"SPEC.md or .kb/).
|
|
65
|
+
"SPEC.md or .kb/). Run `scrip init` to start one here, or pass --root "
|
|
66
|
+
"explicitly."
|
|
60
67
|
)
|
|
61
68
|
|
|
62
69
|
|
|
@@ -68,6 +75,58 @@ def _emit(payload: dict) -> None:
|
|
|
68
75
|
# --------------------------------------------------------------------------- #
|
|
69
76
|
# Subcommand handlers -> return an int exit code
|
|
70
77
|
# --------------------------------------------------------------------------- #
|
|
78
|
+
def cmd_init(args: argparse.Namespace) -> int:
|
|
79
|
+
from . import graph, manifest_path
|
|
80
|
+
|
|
81
|
+
target = Path(args.path or ".").expanduser().resolve()
|
|
82
|
+
if target.exists() and not target.is_dir():
|
|
83
|
+
raise errors.UsageError(f"init target is not a directory: {target}")
|
|
84
|
+
if _looks_like_root(target):
|
|
85
|
+
if args.json:
|
|
86
|
+
_emit({"root": str(target), "status": "exists", "created": []})
|
|
87
|
+
else:
|
|
88
|
+
print(f"already a scriptorium root: {target} (nothing to do)")
|
|
89
|
+
return 0
|
|
90
|
+
# Refuse to create a nested instance: an inner vault/ would shadow the outer
|
|
91
|
+
# root for every command run beneath it (resolve_root stops at the nearest
|
|
92
|
+
# ancestor).
|
|
93
|
+
for cand in target.parents:
|
|
94
|
+
if _looks_like_root(cand):
|
|
95
|
+
raise errors.UsageError(
|
|
96
|
+
f"refusing to create a nested scriptorium: {target} is inside the "
|
|
97
|
+
f"root at {cand}. Run `scrip init` at that root, or pick a "
|
|
98
|
+
f"directory outside it."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
created: list[str] = []
|
|
102
|
+
for rel in ("vault/raw", "vault/facts", "vault/wiki", ".kb"):
|
|
103
|
+
d = target / rel
|
|
104
|
+
if d.is_dir():
|
|
105
|
+
continue
|
|
106
|
+
if d.exists():
|
|
107
|
+
raise errors.UsageError(
|
|
108
|
+
f"cannot create {rel}/: a file already exists at {d}"
|
|
109
|
+
)
|
|
110
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
created.append(rel + "/")
|
|
112
|
+
# Same as `scrip status --rebuild-manifest`: scan the (empty) vault and write
|
|
113
|
+
# the .kb/manifest.json cache, so the instance starts warm and detectable.
|
|
114
|
+
graph.compute_status(target, rebuild=True)
|
|
115
|
+
created.append(str(manifest_path(target).relative_to(target)))
|
|
116
|
+
|
|
117
|
+
if args.json:
|
|
118
|
+
_emit({"root": str(target), "status": "initialized", "created": created})
|
|
119
|
+
else:
|
|
120
|
+
print(f"initialized scriptorium root at {target}")
|
|
121
|
+
for c in created:
|
|
122
|
+
print(f" + {c}")
|
|
123
|
+
print(
|
|
124
|
+
" next: add a source (`scrip ingest <url|file>`), compile a page "
|
|
125
|
+
"(`scrip new` + `scrip anchor` + `scrip stamp`), then `scrip verify`"
|
|
126
|
+
)
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
71
130
|
def cmd_status(args: argparse.Namespace) -> int:
|
|
72
131
|
from . import graph
|
|
73
132
|
|
|
@@ -555,6 +614,21 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
555
614
|
p.add_argument("--version", action="version", version=f"scrip {__version__}")
|
|
556
615
|
sub = p.add_subparsers(dest="command", required=True, metavar="<command>")
|
|
557
616
|
|
|
617
|
+
pinit = sub.add_parser(
|
|
618
|
+
"init",
|
|
619
|
+
help="create the root skeleton (vault/{raw,facts,wiki}/ + .kb/ cache) and its manifest",
|
|
620
|
+
)
|
|
621
|
+
pinit.add_argument(
|
|
622
|
+
"path",
|
|
623
|
+
nargs="?",
|
|
624
|
+
metavar="DIR",
|
|
625
|
+
help="directory to initialize (default: current directory; created if absent)",
|
|
626
|
+
)
|
|
627
|
+
pinit.add_argument(
|
|
628
|
+
"--json", action="store_true", help="machine-readable JSON output"
|
|
629
|
+
)
|
|
630
|
+
pinit.set_defaults(func=cmd_init)
|
|
631
|
+
|
|
558
632
|
ps = sub.add_parser(
|
|
559
633
|
"status",
|
|
560
634
|
parents=[common],
|
|
@@ -412,10 +412,7 @@ def _load_meta(root: Path) -> dict:
|
|
|
412
412
|
|
|
413
413
|
def _write_meta(root: Path, data: dict, new_sources: list[str]) -> None:
|
|
414
414
|
derived = list(data.get("derived-from") or [])
|
|
415
|
-
|
|
416
|
-
if sid not in derived:
|
|
417
|
-
derived.append(sid)
|
|
418
|
-
data["derived-from"] = derived
|
|
415
|
+
data["derived-from"] = list(dict.fromkeys(derived + new_sources))
|
|
419
416
|
# Drop the stamp on EVERY append: with an unchanged derived-from the
|
|
420
417
|
# recomputed input-hash would still match the stored one, and status would
|
|
421
418
|
# report OK over facts nobody has blessed. Removing input-hash forces STALE
|
|
@@ -596,12 +593,12 @@ def add(root: Path, table: str, proposals: list[dict]) -> dict:
|
|
|
596
593
|
if not ends_with_newline:
|
|
597
594
|
f.write("\n")
|
|
598
595
|
f.write(payload)
|
|
599
|
-
|
|
596
|
+
new_sources_dict: dict[str, None] = {}
|
|
600
597
|
if table in ("claims", "edges"):
|
|
601
598
|
for rec in appended:
|
|
602
599
|
sid = rec.get("source_id") # cited edges carry one; bare edges don't
|
|
603
|
-
if sid
|
|
604
|
-
|
|
605
|
-
_write_meta(root, meta,
|
|
600
|
+
if sid:
|
|
601
|
+
new_sources_dict[sid] = None
|
|
602
|
+
_write_meta(root, meta, list(new_sources_dict.keys()))
|
|
606
603
|
|
|
607
604
|
return {"table": table, "appended": appended, "skipped": skipped, "failures": []}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`scrip init` — create the root skeleton (vault/ layers + .kb/ cache) so a new
|
|
2
|
+
instance starts detectable and green. Idempotent in an existing root; refuses to
|
|
3
|
+
nest a new instance inside an existing one."""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from scrip import cli, manifest_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_init_creates_skeleton_and_manifest(tmp_path, capsys):
|
|
11
|
+
root = tmp_path / "kb"
|
|
12
|
+
assert cli.main(["init", str(root)]) == 0
|
|
13
|
+
for rel in ("vault/raw", "vault/facts", "vault/wiki", ".kb"):
|
|
14
|
+
assert (root / rel).is_dir()
|
|
15
|
+
# same effect as `scrip status --rebuild-manifest`: the cache exists
|
|
16
|
+
assert manifest_path(root).exists()
|
|
17
|
+
out = capsys.readouterr().out
|
|
18
|
+
assert f"initialized scriptorium root at {root}" in out
|
|
19
|
+
assert "scrip ingest" in out # next-steps hint: ingest -> compile -> verify
|
|
20
|
+
assert "scrip verify" in out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_init_defaults_to_cwd(tmp_path, monkeypatch):
|
|
24
|
+
monkeypatch.chdir(tmp_path)
|
|
25
|
+
assert cli.main(["init"]) == 0
|
|
26
|
+
assert (tmp_path / "vault" / "raw").is_dir()
|
|
27
|
+
assert (tmp_path / ".kb").is_dir()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_init_then_commands_resolve_root_without_flags(tmp_path, monkeypatch):
|
|
31
|
+
"""The dogfooding gap this command closes: after init, `scrip status` and
|
|
32
|
+
`scrip verify` run green from inside the new root with no --root."""
|
|
33
|
+
root = tmp_path / "kb"
|
|
34
|
+
assert cli.main(["init", str(root)]) == 0
|
|
35
|
+
monkeypatch.chdir(root)
|
|
36
|
+
assert cli.main(["status"]) == 0
|
|
37
|
+
assert cli.main(["verify"]) == 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_init_existing_root_is_noop(tmp_path, capsys):
|
|
41
|
+
root = tmp_path / "kb"
|
|
42
|
+
assert cli.main(["init", str(root)]) == 0
|
|
43
|
+
manifest_before = manifest_path(root).read_text(encoding="utf-8")
|
|
44
|
+
capsys.readouterr()
|
|
45
|
+
assert cli.main(["init", str(root)]) == 0
|
|
46
|
+
out = capsys.readouterr().out
|
|
47
|
+
assert f"already a scriptorium root: {root}" in out
|
|
48
|
+
# a true no-op: the manifest was not rewritten
|
|
49
|
+
assert manifest_path(root).read_text(encoding="utf-8") == manifest_before
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_init_detects_spec_marker_root_as_existing(kb, capsys):
|
|
53
|
+
"""A root marked by SPEC.md (no .kb/ needed) counts as already initialized."""
|
|
54
|
+
assert cli.main(["init", str(kb.root)]) == 0
|
|
55
|
+
assert "already a scriptorium root" in capsys.readouterr().out
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_init_inside_existing_root_refused_exit_2(tmp_path):
|
|
59
|
+
root = tmp_path / "kb"
|
|
60
|
+
assert cli.main(["init", str(root)]) == 0
|
|
61
|
+
nested = root / "vault" / "raw" / "sub"
|
|
62
|
+
assert cli.main(["init", str(nested)]) == 2
|
|
63
|
+
# nothing was created for the refused nested instance
|
|
64
|
+
assert not nested.exists()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_init_target_is_a_file_exit_2(tmp_path):
|
|
68
|
+
f = tmp_path / "notes.md"
|
|
69
|
+
f.write_text("hi\n", encoding="utf-8")
|
|
70
|
+
assert cli.main(["init", str(f)]) == 2
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_init_fills_in_missing_layers(tmp_path):
|
|
74
|
+
"""A partial layout (vault/ alone is not a detectable root) is completed
|
|
75
|
+
rather than refused."""
|
|
76
|
+
root = tmp_path / "kb"
|
|
77
|
+
(root / "vault" / "raw").mkdir(parents=True)
|
|
78
|
+
assert cli.main(["init", str(root)]) == 0
|
|
79
|
+
for rel in ("vault/facts", "vault/wiki", ".kb"):
|
|
80
|
+
assert (root / rel).is_dir()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_init_layer_path_is_a_file_refused_exit_2(tmp_path):
|
|
84
|
+
"""A file squatting a layer path is refused cleanly (exit 2), not an
|
|
85
|
+
uncaught FileExistsError mapped to the internal-error exit code."""
|
|
86
|
+
root = tmp_path / "kb"
|
|
87
|
+
(root / "vault").mkdir(parents=True)
|
|
88
|
+
(root / "vault" / "facts").write_text("oops\n", encoding="utf-8")
|
|
89
|
+
assert cli.main(["init", str(root)]) == 2
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_init_json_shape(tmp_path, capsys):
|
|
93
|
+
root = tmp_path / "kb"
|
|
94
|
+
assert cli.main(["init", "--json", str(root)]) == 0
|
|
95
|
+
data = json.loads(capsys.readouterr().out)
|
|
96
|
+
assert set(data) == {"root", "status", "created"}
|
|
97
|
+
assert data["status"] == "initialized"
|
|
98
|
+
assert ".kb/manifest.json" in data["created"]
|
|
99
|
+
assert cli.main(["init", "--json", str(root)]) == 0
|
|
100
|
+
data = json.loads(capsys.readouterr().out)
|
|
101
|
+
assert data["status"] == "exists"
|
|
102
|
+
assert data["created"] == []
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|