zikaron 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.
Files changed (162) hide show
  1. zikaron/__init__.py +1 -0
  2. zikaron/cli/__init__.py +1 -0
  3. zikaron/cli/main.py +114 -0
  4. zikaron/core/__init__.py +1 -0
  5. zikaron/core/clock.py +78 -0
  6. zikaron/core/config/__init__.py +1 -0
  7. zikaron/core/config/keys.py +395 -0
  8. zikaron/core/config/resolution.py +267 -0
  9. zikaron/core/consolidation/__init__.py +1 -0
  10. zikaron/core/consolidation/authorization.py +316 -0
  11. zikaron/core/consolidation/candidates.py +147 -0
  12. zikaron/core/consolidation/context.py +166 -0
  13. zikaron/core/consolidation/grouping.py +383 -0
  14. zikaron/core/consolidation/groups.py +490 -0
  15. zikaron/core/consolidation/payload.py +246 -0
  16. zikaron/core/consolidation/planning.py +192 -0
  17. zikaron/core/consolidation/rowstate.py +68 -0
  18. zikaron/core/consolidation/runs.py +306 -0
  19. zikaron/core/consolidation/serving.py +462 -0
  20. zikaron/core/consolidation/verbs.py +500 -0
  21. zikaron/core/errors.py +355 -0
  22. zikaron/core/events.py +748 -0
  23. zikaron/core/indexing/__init__.py +1 -0
  24. zikaron/core/indexing/acquisition.py +255 -0
  25. zikaron/core/indexing/chunking.py +368 -0
  26. zikaron/core/indexing/encoder.py +537 -0
  27. zikaron/core/indexing/lexical.py +86 -0
  28. zikaron/core/indexing/model_cache.py +93 -0
  29. zikaron/core/indexing/model_pin.py +89 -0
  30. zikaron/core/indexing/vectors.py +223 -0
  31. zikaron/core/indexing/writes.py +461 -0
  32. zikaron/core/knowledge/__init__.py +5 -0
  33. zikaron/core/knowledge/arms.py +104 -0
  34. zikaron/core/knowledge/builds.py +204 -0
  35. zikaron/core/knowledge/candidates.py +130 -0
  36. zikaron/core/knowledge/changes.py +175 -0
  37. zikaron/core/knowledge/chunking.py +376 -0
  38. zikaron/core/knowledge/counters.py +228 -0
  39. zikaron/core/knowledge/database.py +380 -0
  40. zikaron/core/knowledge/ddl.py +196 -0
  41. zikaron/core/knowledge/disposal.py +213 -0
  42. zikaron/core/knowledge/errors.py +166 -0
  43. zikaron/core/knowledge/files.py +202 -0
  44. zikaron/core/knowledge/git.py +385 -0
  45. zikaron/core/knowledge/groups.py +450 -0
  46. zikaron/core/knowledge/lexical.py +64 -0
  47. zikaron/core/knowledge/lifecycle.py +418 -0
  48. zikaron/core/knowledge/lock.py +277 -0
  49. zikaron/core/knowledge/meta.py +393 -0
  50. zikaron/core/knowledge/paths.py +55 -0
  51. zikaron/core/knowledge/pending.py +59 -0
  52. zikaron/core/knowledge/registry.py +264 -0
  53. zikaron/core/knowledge/repair.py +152 -0
  54. zikaron/core/knowledge/reporting.py +436 -0
  55. zikaron/core/knowledge/roots.py +91 -0
  56. zikaron/core/knowledge/scan.py +429 -0
  57. zikaron/core/knowledge/search.py +346 -0
  58. zikaron/core/knowledge/state.py +174 -0
  59. zikaron/core/knowledge/text.py +166 -0
  60. zikaron/core/knowledge/vectors.py +102 -0
  61. zikaron/core/knowledge/walk.py +264 -0
  62. zikaron/core/knowledge/writes.py +127 -0
  63. zikaron/core/records/__init__.py +1 -0
  64. zikaron/core/records/memory.py +961 -0
  65. zikaron/core/records/receipts.py +161 -0
  66. zikaron/core/records/supersession.py +221 -0
  67. zikaron/core/retrieval/__init__.py +1 -0
  68. zikaron/core/retrieval/arms.py +318 -0
  69. zikaron/core/retrieval/block.py +107 -0
  70. zikaron/core/retrieval/eligibility.py +164 -0
  71. zikaron/core/retrieval/query.py +327 -0
  72. zikaron/core/retrieval/ranking.py +260 -0
  73. zikaron/core/retrieval/reads.py +294 -0
  74. zikaron/core/retrieval/retrieve.py +158 -0
  75. zikaron/core/retrieval/similarity.py +87 -0
  76. zikaron/core/signals/__init__.py +34 -0
  77. zikaron/core/signals/contention.py +106 -0
  78. zikaron/core/signals/dedup.py +201 -0
  79. zikaron/core/signals/horizon.py +47 -0
  80. zikaron/core/signals/repair.py +161 -0
  81. zikaron/core/signals/retirement.py +83 -0
  82. zikaron/core/signals/sessions.py +105 -0
  83. zikaron/core/signals/writes.py +200 -0
  84. zikaron/core/store/__init__.py +1 -0
  85. zikaron/core/store/connection.py +202 -0
  86. zikaron/core/store/ddl.py +215 -0
  87. zikaron/core/store/embedder.py +45 -0
  88. zikaron/core/store/meta.py +152 -0
  89. zikaron/core/store/permissions.py +160 -0
  90. zikaron/core/store/store.py +408 -0
  91. zikaron/core/store/transactions.py +181 -0
  92. zikaron/core/write/__init__.py +33 -0
  93. zikaron/core/write/dedup.py +145 -0
  94. zikaron/core/write/tools.py +290 -0
  95. zikaron/doctor/__init__.py +1 -0
  96. zikaron/doctor/checks.py +220 -0
  97. zikaron/doctor/main.py +64 -0
  98. zikaron/harness/__init__.py +1 -0
  99. zikaron/harness/detect.py +92 -0
  100. zikaron/harness/spec.py +320 -0
  101. zikaron/hook/__init__.py +1 -0
  102. zikaron/hook/connect.py +379 -0
  103. zikaron/hook/envelope.py +106 -0
  104. zikaron/hook/failure.py +104 -0
  105. zikaron/hook/limits.py +61 -0
  106. zikaron/hook/main.py +118 -0
  107. zikaron/hook/push.py +183 -0
  108. zikaron/hook/rpc.py +85 -0
  109. zikaron/hook/spawn_warm.py +81 -0
  110. zikaron/hook/subagent_policy.py +57 -0
  111. zikaron/hook/tripwire.py +54 -0
  112. zikaron/hook/warm_helper.py +137 -0
  113. zikaron/hook/write_policy.py +319 -0
  114. zikaron/install/__init__.py +4 -0
  115. zikaron/install/__main__.py +18 -0
  116. zikaron/install/assets.py +394 -0
  117. zikaron/install/entries.py +370 -0
  118. zikaron/install/harness.py +185 -0
  119. zikaron/install/main.py +375 -0
  120. zikaron/install/targets.py +789 -0
  121. zikaron/install/writer.py +973 -0
  122. zikaron/knowledge/__init__.py +1 -0
  123. zikaron/knowledge/__main__.py +17 -0
  124. zikaron/knowledge/indexer/__init__.py +1 -0
  125. zikaron/knowledge/indexer/__main__.py +17 -0
  126. zikaron/knowledge/indexer/detach.py +83 -0
  127. zikaron/knowledge/indexer/main.py +187 -0
  128. zikaron/knowledge/main.py +466 -0
  129. zikaron/knowledge/scope.py +133 -0
  130. zikaron/mcp/__init__.py +6 -0
  131. zikaron/mcp/connection.py +583 -0
  132. zikaron/mcp/consolidator.py +316 -0
  133. zikaron/mcp/errors.py +73 -0
  134. zikaron/mcp/main.py +66 -0
  135. zikaron/mcp/primary.py +420 -0
  136. zikaron/mcp/server.py +96 -0
  137. zikaron/mcp/spill.py +328 -0
  138. zikaron/mcp/tool_names.py +67 -0
  139. zikaron/py.typed +0 -0
  140. zikaron/service/__init__.py +1 -0
  141. zikaron/service/asyncio_compat.py +126 -0
  142. zikaron/service/context.py +251 -0
  143. zikaron/service/dispatch.py +332 -0
  144. zikaron/service/dispatch_consolidation.py +397 -0
  145. zikaron/service/dispatch_knowledge.py +469 -0
  146. zikaron/service/envelope.py +166 -0
  147. zikaron/service/lifecycle.py +467 -0
  148. zikaron/service/log.py +96 -0
  149. zikaron/service/main.py +531 -0
  150. zikaron/service/params.py +168 -0
  151. zikaron/service/paths.py +181 -0
  152. zikaron/service/rpc.py +176 -0
  153. zikaron/service/security.py +156 -0
  154. zikaron/service/serialize.py +204 -0
  155. zikaron/service/serialize_knowledge.py +238 -0
  156. zikaron/service/server.py +416 -0
  157. zikaron-0.1.0.dist-info/METADATA +770 -0
  158. zikaron-0.1.0.dist-info/RECORD +162 -0
  159. zikaron-0.1.0.dist-info/WHEEL +5 -0
  160. zikaron-0.1.0.dist-info/entry_points.txt +4 -0
  161. zikaron-0.1.0.dist-info/licenses/LICENSE +21 -0
  162. zikaron-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,152 @@
1
+ """`meta`'s five required keys, and the `reindexing` sentinel that is deliberately not one of them.
2
+
3
+ `schema.md` §"`meta` — the store-coupled values, and the initialization contract" is normative.
4
+ Two facts from it drive this module's shape: validation happens **twice** — written at creation,
5
+ parsed and range-checked on **every** open — and a required key that is missing, unparseable or
6
+ out of range on open is a fatal `bad_config`, never a silent fall-back to a default. `reindexing`
7
+ is a transient key whose *absence* is normal, so it is exempt from that required-key validation
8
+ and handled by its own present/absent check instead.
9
+ """
10
+
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass
13
+ from typing import Final
14
+ from uuid import UUID
15
+
16
+ from zikaron.core.errors import BadConfigSource, ErrorCode, ZikaronError
17
+
18
+ #: The `meta` row key naming the schema this build's tables and invariants implement. Compared
19
+ #: against `zikaron.core.errors.ERROR_SPECS[SCHEMA_INCOMPATIBLE].data_fields`'s fixed `supported`
20
+ #: value by a test, so this module's schema version is never a second, driftable statement of it.
21
+ SCHEMA_VERSION_KEY: Final = "schema_version"
22
+ STORE_ID_KEY: Final = "store_id"
23
+ EMBED_MODEL_KEY: Final = "embed_model"
24
+ EMBED_DIM_KEY: Final = "embed_dim"
25
+ CHUNK_MAX_TOKENS_KEY: Final = "chunk_max_tokens"
26
+
27
+ #: The transient sentinel `schema.md` explicitly excludes from required-key validation: normal
28
+ #: absence, abnormal presence. Its value is the ISO-8601 instant the unavailable window opened.
29
+ REINDEXING_KEY: Final = "reindexing"
30
+
31
+ #: The five keys required on every open, in the order this module reads and validates them.
32
+ REQUIRED_KEYS: Final[tuple[str, ...]] = (
33
+ SCHEMA_VERSION_KEY,
34
+ STORE_ID_KEY,
35
+ EMBED_MODEL_KEY,
36
+ EMBED_DIM_KEY,
37
+ CHUNK_MAX_TOKENS_KEY,
38
+ )
39
+
40
+ _CHUNK_MAX_TOKENS_MIN: Final = 64
41
+ _CHUNK_MAX_TOKENS_MAX: Final = 8192
42
+ _UUID4_STRING_LENGTH: Final = 36
43
+ _UUID_VERSION_4: Final = 4
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class StoreMeta:
48
+ """The five required `meta` values, parsed and range-checked.
49
+
50
+ Constructing one *is* passing validation: every field has already been read as its typed
51
+ value and shown to lie in its stated range, so a caller holding a `StoreMeta` need not
52
+ re-check any of them.
53
+ """
54
+
55
+ schema_version: int
56
+ store_id: str
57
+ embed_model: str
58
+ embed_dim: int
59
+ chunk_max_tokens: int
60
+
61
+
62
+ def _bad_meta(key: str, value: str, expected: str) -> ZikaronError:
63
+ return ZikaronError(
64
+ ErrorCode.BAD_CONFIG,
65
+ source=BadConfigSource.META,
66
+ key=key,
67
+ value=value,
68
+ expected=expected,
69
+ )
70
+
71
+
72
+ def _require_int(raw: Mapping[str, str], key: str, expected: str) -> int:
73
+ if key not in raw:
74
+ raise _bad_meta(key, "<missing>", expected)
75
+ text = raw[key]
76
+ try:
77
+ return int(text)
78
+ except ValueError as error:
79
+ raise _bad_meta(key, text, expected) from error
80
+
81
+
82
+ def _require_string(raw: Mapping[str, str], key: str, expected: str) -> str:
83
+ if key not in raw:
84
+ raise _bad_meta(key, "<missing>", expected)
85
+ return raw[key]
86
+
87
+
88
+ def parse_and_validate(raw: Mapping[str, str]) -> StoreMeta:
89
+ """Turn every `meta` row's text value into `StoreMeta`, or raise naming the first bad key.
90
+
91
+ Args:
92
+ raw: every `meta` row's text value, keyed by `key`. Extra keys (an unknown setting a
93
+ newer writer left behind) are tolerated silently, per `schema.md`'s forward-
94
+ compatibility rule for a *supported* schema version.
95
+
96
+ Raises:
97
+ ZikaronError: `BAD_CONFIG` with `source='meta'`, naming the first required key found
98
+ missing, unparseable, or out of its stated range. There is no partial `StoreMeta` to
99
+ return on failure, per `schema.md`'s "fatal store error, not a fall-back" rule.
100
+ """
101
+ schema_version = _require_int(raw, SCHEMA_VERSION_KEY, "int >= 1")
102
+ if schema_version < 1:
103
+ raise _bad_meta(SCHEMA_VERSION_KEY, str(schema_version), "int >= 1")
104
+ store_id = _require_string(raw, STORE_ID_KEY, "uuid4 string, 36 chars")
105
+ if len(store_id) != _UUID4_STRING_LENGTH:
106
+ raise _bad_meta(STORE_ID_KEY, store_id, "uuid4 string, 36 chars")
107
+ try:
108
+ parsed_store_id = UUID(store_id)
109
+ except ValueError as error:
110
+ raise _bad_meta(STORE_ID_KEY, store_id, "uuid4 string, 36 chars") from error
111
+ if parsed_store_id.version != _UUID_VERSION_4:
112
+ raise _bad_meta(STORE_ID_KEY, store_id, "uuid4 string, 36 chars")
113
+ embed_model = _require_string(raw, EMBED_MODEL_KEY, "non-empty string")
114
+ if embed_model == "":
115
+ raise _bad_meta(EMBED_MODEL_KEY, embed_model, "non-empty string")
116
+ embed_dim = _require_int(raw, EMBED_DIM_KEY, "int >= 1")
117
+ if embed_dim < 1:
118
+ raise _bad_meta(EMBED_DIM_KEY, str(embed_dim), "int >= 1")
119
+ chunk_max_tokens = _require_int(
120
+ raw, CHUNK_MAX_TOKENS_KEY, f"int {_CHUNK_MAX_TOKENS_MIN}-{_CHUNK_MAX_TOKENS_MAX}"
121
+ )
122
+ if not (_CHUNK_MAX_TOKENS_MIN <= chunk_max_tokens <= _CHUNK_MAX_TOKENS_MAX):
123
+ raise _bad_meta(
124
+ CHUNK_MAX_TOKENS_KEY,
125
+ str(chunk_max_tokens),
126
+ f"int {_CHUNK_MAX_TOKENS_MIN}-{_CHUNK_MAX_TOKENS_MAX}",
127
+ )
128
+ return StoreMeta(
129
+ schema_version=schema_version,
130
+ store_id=store_id,
131
+ embed_model=embed_model,
132
+ embed_dim=embed_dim,
133
+ chunk_max_tokens=chunk_max_tokens,
134
+ )
135
+
136
+
137
+ def defaults_at_creation(
138
+ schema_version: int, store_id: str, embed_model: str, embed_dim: int, chunk_max_tokens: int
139
+ ) -> Mapping[str, str]:
140
+ """Every required `meta` row's text value at store creation, in `REQUIRED_KEYS` order.
141
+
142
+ `schema.md` requires a fresh store to be "fully described" so nothing on open reads a
143
+ missing key — this is the write side of that promise, called once by `Store.create` after
144
+ every value it takes has already been validated by whoever assembled them.
145
+ """
146
+ return {
147
+ SCHEMA_VERSION_KEY: str(schema_version),
148
+ STORE_ID_KEY: store_id,
149
+ EMBED_MODEL_KEY: embed_model,
150
+ EMBED_DIM_KEY: str(embed_dim),
151
+ CHUNK_MAX_TOKENS_KEY: str(chunk_max_tokens),
152
+ }
@@ -0,0 +1,160 @@
1
+ """`0700`/`0600` enforcement for the store directory and the files inside it.
2
+
3
+ `architecture.md` §"Filesystem security" is normative: `.zikaron/` and `.zikaron/knowledge/` are
4
+ mode `0700`, and `memory.db`, every `knowledge/<uuid4>.db`, and all their `-wal`/`-shm` siblings
5
+ are `0600`, created with an explicit umask rather
6
+ than inherited — SQLite creates the WAL/SHM files itself and will otherwise pick up whatever
7
+ umask the process happened to have. A directory found wider than `0700` is tightened rather than
8
+ refused, per the same table.
9
+
10
+ `core/knowledge/database.py` imports this module so a knowledge base gets the same modes, which
11
+ matters at least as much there: a corpus holds the verbatim text of every file indexed into it.
12
+ """
13
+
14
+ import os
15
+ from collections.abc import Iterator
16
+ from contextlib import contextmanager
17
+ from pathlib import Path
18
+
19
+ from zikaron.core.errors import BadConfigSource, ErrorCode, ZikaronError
20
+
21
+ _STORE_DIR_MODE = 0o700
22
+ _STORE_FILE_MODE = 0o600
23
+ _STORE_FILE_UMASK = 0o077
24
+
25
+
26
+ def _reject_symlinked_store_dir(store_dir: Path) -> None:
27
+ """Refuse a store reached through a symlink anywhere in `store_dir`'s own path.
28
+
29
+ `architecture.md` §"Filesystem security": "`realpath` the store directory and require the
30
+ resolved parent to be the **scope directory** (D17 as amended — under Claude Code deliberately
31
+ not the spawning process's cwd)." *This quoted the pre-amendment wording — "the cwd" — which
32
+ D17 replaced, and then glossed the word the amendment had removed. One side of a two-way
33
+ pointer was updated and the other kept quoting the superseded text; the pointer guard cannot
34
+ see it, because the section name it resolves is intact.* That sentence states the rule from
35
+ `zikaron-service`'s own point of view, where the store path is *derived* from the caller's
36
+ scope directory in the first place (D17: the harness's own project directory where it names
37
+ one, else the working directory; the vetting below is unaffected either way, since it never
38
+ compares against an ambient cwd) — deriving `store_dir` is that caller's job, not this
39
+ function's, since `Store.create`/`Store.open` take `store_dir` as an explicit parameter and
40
+ have no way to know whether the caller's own cwd is the concept the caller meant by it. A
41
+ caller that resolved `store_dir` from somewhere other than its own cwd — a test, a tool
42
+ walking several stores — would be wrongly refused by comparing against the *ambient*
43
+ process cwd here, which is a fact about this function's environment rather than about the
44
+ path it was actually asked to open.
45
+
46
+ What this function *can* check, and what carries the rule's real security content
47
+ regardless of who derived the path: whether `store_dir` is reached through a symlink at
48
+ all, anywhere along its own length — `store_dir` itself, or any directory between it and
49
+ the filesystem root. A bare `store_dir.is_symlink()` check misses the case a symlinked
50
+ *ancestor* creates, e.g. `some-symlinked-parent/.zikaron`, where `.zikaron` itself is a
51
+ genuine directory but the path leading to it is not what it appears to be.
52
+ """
53
+ current = store_dir if store_dir.is_absolute() else Path.cwd() / store_dir
54
+ while True:
55
+ if current.is_symlink():
56
+ raise ZikaronError(
57
+ ErrorCode.BAD_CONFIG,
58
+ source=BadConfigSource.DERIVED,
59
+ key="store_dir",
60
+ value=str(store_dir),
61
+ expected=f"no symlink anywhere in the path to it (found one at {current})",
62
+ )
63
+ parent = current.parent
64
+ if parent == current:
65
+ return
66
+ current = parent
67
+
68
+
69
+ def ensure_store_dir(path: Path) -> None:
70
+ """Create `path` at `0700` if absent, or tighten it to `0700` if it exists wider.
71
+
72
+ Explicit `mkdir(mode=...)` rather than a bare `mkdir()` followed by `chmod`, because a
73
+ process umask can otherwise narrow the mode `mkdir` was asked for and leave a gap between
74
+ what this function requested and what actually landed on disk.
75
+
76
+ Raises:
77
+ ZikaronError: `BAD_CONFIG` if `path`, once resolved, is not reached through the current
78
+ directory alone — `architecture.md` §"Filesystem security" requires a store reached
79
+ through a symlinked `.zikaron`, or through a symlinked ancestor of it, to be
80
+ refused rather than followed, since the store's own `0700`/`0600` modes protect the
81
+ directory they are set on, not whatever a symlink happens to point at.
82
+ """
83
+ _reject_symlinked_store_dir(path)
84
+ if path.is_dir():
85
+ current = path.stat().st_mode & 0o777
86
+ if current != _STORE_DIR_MODE:
87
+ path.chmod(_STORE_DIR_MODE)
88
+ return
89
+ path.mkdir(mode=_STORE_DIR_MODE, parents=True)
90
+ path.chmod(_STORE_DIR_MODE)
91
+
92
+
93
+ @contextmanager
94
+ def restrictive_umask() -> Iterator[None]:
95
+ """Force every file created in this block to land at `0600`, regardless of the process umask.
96
+
97
+ SQLite creates `memory.db`'s `-wal` and `-shm` siblings itself, outside any `open()` call
98
+ this module makes, so a umask set only around an explicit file creation would not reach
99
+ them. Wrapping the whole store-creation sequence in this umask is what does.
100
+ """
101
+ previous = os.umask(_STORE_FILE_UMASK)
102
+ try:
103
+ yield
104
+ finally:
105
+ os.umask(previous)
106
+
107
+
108
+ def database_files(db_path: Path) -> tuple[Path, Path, Path]:
109
+ """`db_path` and its two WAL siblings — the three files one open database actually is.
110
+
111
+ In WAL mode a database at rest is one file and a database in use is three, and every operation
112
+ that moves, removes or re-permissions one has to handle all of them. Enumerating them in one
113
+ place rather than at each call site is what keeps a caller from tightening `memory.db` while
114
+ leaving its `-wal` world-readable, or from unlinking a knowledge base and leaving its journal
115
+ behind for the next create at that path to inherit.
116
+
117
+ Returned in a fixed order — database, `-wal`, `-shm` — so a caller that unlinks them does so
118
+ in an order a reader can reason about, and named by suffix rather than by a hardcoded
119
+ `memory.db` so a knowledge-base database is the same three files by the same rule.
120
+ """
121
+ return (
122
+ db_path,
123
+ db_path.with_name(db_path.name + "-wal"),
124
+ db_path.with_name(db_path.name + "-shm"),
125
+ )
126
+
127
+
128
+ def enforce_store_file_mode(path: Path) -> None:
129
+ """Tighten an existing store file to `0600` if it was created wider than that.
130
+
131
+ Called after store creation as a belt-and-suspenders check: `restrictive_umask` is what
132
+ should have produced `0600` in the first place, and this is what makes a wider mode a
133
+ correction rather than a silent gap if some code path ever creates the file outside it.
134
+ """
135
+ if path.is_file():
136
+ current = path.stat().st_mode & 0o777
137
+ if current != _STORE_FILE_MODE:
138
+ path.chmod(_STORE_FILE_MODE)
139
+
140
+
141
+ def enforce_existing_store_permissions(store_dir: Path) -> None:
142
+ """Tighten a pre-existing store's directory and database files, without creating anything.
143
+
144
+ `Store.open` calls this before touching the connection, so a store whose directory was left
145
+ at a wider mode by an older build — or by an operator's own `chmod` — is corrected on the
146
+ very next open rather than only ever being corrected at creation time, which is the one
147
+ moment `Store.create`'s own checks previously ran.
148
+
149
+ Raises:
150
+ ZikaronError: `BAD_CONFIG` if `store_dir`, once resolved, is not reached through the
151
+ current directory alone, for the same reason `ensure_store_dir` refuses that shape
152
+ at creation.
153
+ """
154
+ _reject_symlinked_store_dir(store_dir)
155
+ if store_dir.is_dir():
156
+ current = store_dir.stat().st_mode & 0o777
157
+ if current != _STORE_DIR_MODE:
158
+ store_dir.chmod(_STORE_DIR_MODE)
159
+ for path in database_files(store_dir / "memory.db"):
160
+ enforce_store_file_mode(path)