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.
- zikaron/__init__.py +1 -0
- zikaron/cli/__init__.py +1 -0
- zikaron/cli/main.py +114 -0
- zikaron/core/__init__.py +1 -0
- zikaron/core/clock.py +78 -0
- zikaron/core/config/__init__.py +1 -0
- zikaron/core/config/keys.py +395 -0
- zikaron/core/config/resolution.py +267 -0
- zikaron/core/consolidation/__init__.py +1 -0
- zikaron/core/consolidation/authorization.py +316 -0
- zikaron/core/consolidation/candidates.py +147 -0
- zikaron/core/consolidation/context.py +166 -0
- zikaron/core/consolidation/grouping.py +383 -0
- zikaron/core/consolidation/groups.py +490 -0
- zikaron/core/consolidation/payload.py +246 -0
- zikaron/core/consolidation/planning.py +192 -0
- zikaron/core/consolidation/rowstate.py +68 -0
- zikaron/core/consolidation/runs.py +306 -0
- zikaron/core/consolidation/serving.py +462 -0
- zikaron/core/consolidation/verbs.py +500 -0
- zikaron/core/errors.py +355 -0
- zikaron/core/events.py +748 -0
- zikaron/core/indexing/__init__.py +1 -0
- zikaron/core/indexing/acquisition.py +255 -0
- zikaron/core/indexing/chunking.py +368 -0
- zikaron/core/indexing/encoder.py +537 -0
- zikaron/core/indexing/lexical.py +86 -0
- zikaron/core/indexing/model_cache.py +93 -0
- zikaron/core/indexing/model_pin.py +89 -0
- zikaron/core/indexing/vectors.py +223 -0
- zikaron/core/indexing/writes.py +461 -0
- zikaron/core/knowledge/__init__.py +5 -0
- zikaron/core/knowledge/arms.py +104 -0
- zikaron/core/knowledge/builds.py +204 -0
- zikaron/core/knowledge/candidates.py +130 -0
- zikaron/core/knowledge/changes.py +175 -0
- zikaron/core/knowledge/chunking.py +376 -0
- zikaron/core/knowledge/counters.py +228 -0
- zikaron/core/knowledge/database.py +380 -0
- zikaron/core/knowledge/ddl.py +196 -0
- zikaron/core/knowledge/disposal.py +213 -0
- zikaron/core/knowledge/errors.py +166 -0
- zikaron/core/knowledge/files.py +202 -0
- zikaron/core/knowledge/git.py +385 -0
- zikaron/core/knowledge/groups.py +450 -0
- zikaron/core/knowledge/lexical.py +64 -0
- zikaron/core/knowledge/lifecycle.py +418 -0
- zikaron/core/knowledge/lock.py +277 -0
- zikaron/core/knowledge/meta.py +393 -0
- zikaron/core/knowledge/paths.py +55 -0
- zikaron/core/knowledge/pending.py +59 -0
- zikaron/core/knowledge/registry.py +264 -0
- zikaron/core/knowledge/repair.py +152 -0
- zikaron/core/knowledge/reporting.py +436 -0
- zikaron/core/knowledge/roots.py +91 -0
- zikaron/core/knowledge/scan.py +429 -0
- zikaron/core/knowledge/search.py +346 -0
- zikaron/core/knowledge/state.py +174 -0
- zikaron/core/knowledge/text.py +166 -0
- zikaron/core/knowledge/vectors.py +102 -0
- zikaron/core/knowledge/walk.py +264 -0
- zikaron/core/knowledge/writes.py +127 -0
- zikaron/core/records/__init__.py +1 -0
- zikaron/core/records/memory.py +961 -0
- zikaron/core/records/receipts.py +161 -0
- zikaron/core/records/supersession.py +221 -0
- zikaron/core/retrieval/__init__.py +1 -0
- zikaron/core/retrieval/arms.py +318 -0
- zikaron/core/retrieval/block.py +107 -0
- zikaron/core/retrieval/eligibility.py +164 -0
- zikaron/core/retrieval/query.py +327 -0
- zikaron/core/retrieval/ranking.py +260 -0
- zikaron/core/retrieval/reads.py +294 -0
- zikaron/core/retrieval/retrieve.py +158 -0
- zikaron/core/retrieval/similarity.py +87 -0
- zikaron/core/signals/__init__.py +34 -0
- zikaron/core/signals/contention.py +106 -0
- zikaron/core/signals/dedup.py +201 -0
- zikaron/core/signals/horizon.py +47 -0
- zikaron/core/signals/repair.py +161 -0
- zikaron/core/signals/retirement.py +83 -0
- zikaron/core/signals/sessions.py +105 -0
- zikaron/core/signals/writes.py +200 -0
- zikaron/core/store/__init__.py +1 -0
- zikaron/core/store/connection.py +202 -0
- zikaron/core/store/ddl.py +215 -0
- zikaron/core/store/embedder.py +45 -0
- zikaron/core/store/meta.py +152 -0
- zikaron/core/store/permissions.py +160 -0
- zikaron/core/store/store.py +408 -0
- zikaron/core/store/transactions.py +181 -0
- zikaron/core/write/__init__.py +33 -0
- zikaron/core/write/dedup.py +145 -0
- zikaron/core/write/tools.py +290 -0
- zikaron/doctor/__init__.py +1 -0
- zikaron/doctor/checks.py +220 -0
- zikaron/doctor/main.py +64 -0
- zikaron/harness/__init__.py +1 -0
- zikaron/harness/detect.py +92 -0
- zikaron/harness/spec.py +320 -0
- zikaron/hook/__init__.py +1 -0
- zikaron/hook/connect.py +379 -0
- zikaron/hook/envelope.py +106 -0
- zikaron/hook/failure.py +104 -0
- zikaron/hook/limits.py +61 -0
- zikaron/hook/main.py +118 -0
- zikaron/hook/push.py +183 -0
- zikaron/hook/rpc.py +85 -0
- zikaron/hook/spawn_warm.py +81 -0
- zikaron/hook/subagent_policy.py +57 -0
- zikaron/hook/tripwire.py +54 -0
- zikaron/hook/warm_helper.py +137 -0
- zikaron/hook/write_policy.py +319 -0
- zikaron/install/__init__.py +4 -0
- zikaron/install/__main__.py +18 -0
- zikaron/install/assets.py +394 -0
- zikaron/install/entries.py +370 -0
- zikaron/install/harness.py +185 -0
- zikaron/install/main.py +375 -0
- zikaron/install/targets.py +789 -0
- zikaron/install/writer.py +973 -0
- zikaron/knowledge/__init__.py +1 -0
- zikaron/knowledge/__main__.py +17 -0
- zikaron/knowledge/indexer/__init__.py +1 -0
- zikaron/knowledge/indexer/__main__.py +17 -0
- zikaron/knowledge/indexer/detach.py +83 -0
- zikaron/knowledge/indexer/main.py +187 -0
- zikaron/knowledge/main.py +466 -0
- zikaron/knowledge/scope.py +133 -0
- zikaron/mcp/__init__.py +6 -0
- zikaron/mcp/connection.py +583 -0
- zikaron/mcp/consolidator.py +316 -0
- zikaron/mcp/errors.py +73 -0
- zikaron/mcp/main.py +66 -0
- zikaron/mcp/primary.py +420 -0
- zikaron/mcp/server.py +96 -0
- zikaron/mcp/spill.py +328 -0
- zikaron/mcp/tool_names.py +67 -0
- zikaron/py.typed +0 -0
- zikaron/service/__init__.py +1 -0
- zikaron/service/asyncio_compat.py +126 -0
- zikaron/service/context.py +251 -0
- zikaron/service/dispatch.py +332 -0
- zikaron/service/dispatch_consolidation.py +397 -0
- zikaron/service/dispatch_knowledge.py +469 -0
- zikaron/service/envelope.py +166 -0
- zikaron/service/lifecycle.py +467 -0
- zikaron/service/log.py +96 -0
- zikaron/service/main.py +531 -0
- zikaron/service/params.py +168 -0
- zikaron/service/paths.py +181 -0
- zikaron/service/rpc.py +176 -0
- zikaron/service/security.py +156 -0
- zikaron/service/serialize.py +204 -0
- zikaron/service/serialize_knowledge.py +238 -0
- zikaron/service/server.py +416 -0
- zikaron-0.1.0.dist-info/METADATA +770 -0
- zikaron-0.1.0.dist-info/RECORD +162 -0
- zikaron-0.1.0.dist-info/WHEEL +5 -0
- zikaron-0.1.0.dist-info/entry_points.txt +4 -0
- zikaron-0.1.0.dist-info/licenses/LICENSE +21 -0
- zikaron-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""`python -m zikaron.knowledge` — manage this project's knowledge bases from a shell."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""`python -m zikaron.knowledge`.
|
|
2
|
+
|
|
3
|
+
A separate module from `main.py` because `python -m zikaron.knowledge` — the documented invocation
|
|
4
|
+
— resolves a *package*, and a package needs `__main__` to be executable. `main.py`
|
|
5
|
+
returns a status rather than exiting so a test can drive it in-process; turning that status into an
|
|
6
|
+
exit is this file's only job.
|
|
7
|
+
|
|
8
|
+
**Its coverage reads 0% and that is not a gap.** The test that exercises it runs
|
|
9
|
+
`python -m zikaron.knowledge` as a subprocess, which is the only way to exercise an entry point at
|
|
10
|
+
all, and coverage does not follow a child process.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from zikaron.knowledge.main import main
|
|
16
|
+
|
|
17
|
+
sys.exit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""`python -m zikaron.knowledge.indexer` — build one knowledge base's index."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""`python -m zikaron.knowledge.indexer`.
|
|
2
|
+
|
|
3
|
+
A separate module from `main.py` because `python -m zikaron.knowledge.indexer` resolves a
|
|
4
|
+
*package*, and a package needs `__main__` to be executable. `main.py` returns a status rather than
|
|
5
|
+
exiting so a test can drive it in-process; turning that status into an exit is this file's only
|
|
6
|
+
job.
|
|
7
|
+
|
|
8
|
+
**Its coverage reads 0% and that is not a gap.** The test that exercises it runs the module as a
|
|
9
|
+
subprocess, which is the only way to exercise an entry point at all, and coverage does not follow
|
|
10
|
+
a child process.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from zikaron.knowledge.indexer.main import main
|
|
16
|
+
|
|
17
|
+
sys.exit(main())
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Starting a build that outlives whoever asked for it.
|
|
2
|
+
|
|
3
|
+
A build is minutes of saturated CPU over a whole directory tree, and every surface that can ask for
|
|
4
|
+
one — a person at a shell, and an agent through a tool call — needs to be free the moment it has
|
|
5
|
+
asked. So the build runs as its own process, in its own session, with no controlling terminal and
|
|
6
|
+
nothing connected to its streams.
|
|
7
|
+
|
|
8
|
+
**This lives beside the command it starts, not in the library.** The argv is the indexer's own, and
|
|
9
|
+
a second module holding a copy of it is how a renamed option comes to fail only in production: the
|
|
10
|
+
spawn does not import the thing it runs, so nothing but a shared definition keeps the two agreeing.
|
|
11
|
+
|
|
12
|
+
**What the child is told, and what it is left to work out.** The project is passed explicitly,
|
|
13
|
+
because the parent has already resolved which store it means and the child must act on that one
|
|
14
|
+
rather than on whatever it would resolve for itself. Everything else it inherits — the environment,
|
|
15
|
+
so that it reads the same configuration layers the parent read, and the working directory, which
|
|
16
|
+
nothing it does depends on but which is cheaper to leave alone than to argue about.
|
|
17
|
+
|
|
18
|
+
**Its output goes nowhere, and that is the trade.** A build that fails after detaching is visible as
|
|
19
|
+
a fact rather than as a reason: its corpus keeps reporting that it has not been built, and the lock
|
|
20
|
+
it took names a process that is no longer running. The reason is recovered by running the same
|
|
21
|
+
command in the foreground, which is what the indexer's own entry point is for. The alternative — a
|
|
22
|
+
log file per knowledge base — is several concurrent writers and a retention policy, bought for a
|
|
23
|
+
diagnostic that one re-run produces on demand.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
import threading
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
#: The command the child runs. Spelled once, as the module path a build is documented under, so the
|
|
32
|
+
#: spawn and the entry point cannot come to disagree about which one exists.
|
|
33
|
+
_MODULE = "zikaron.knowledge.indexer"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def command(name: str, *, project: Path, full: bool) -> list[str]:
|
|
37
|
+
"""The argv a detached build is started with, for a caller that wants to show it to somebody.
|
|
38
|
+
|
|
39
|
+
The knowledge-base name goes after `--`, because a name is free-form: nothing stops one
|
|
40
|
+
beginning with a dash, and without the separator such a name would be read as an option and the
|
|
41
|
+
build would die on its own arguments before opening anything.
|
|
42
|
+
"""
|
|
43
|
+
argv = [sys.executable, "-m", _MODULE, "--project", str(project)]
|
|
44
|
+
if full:
|
|
45
|
+
argv.append("--full")
|
|
46
|
+
argv.extend(["--", name])
|
|
47
|
+
return argv
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def spawn(name: str, *, project: Path, full: bool = False) -> list[str]:
|
|
51
|
+
"""Start a build of `name` and return without waiting for it.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
name: which knowledge base to build, as the registry stores it.
|
|
55
|
+
project: the project whose store to act on, already resolved by the caller.
|
|
56
|
+
full: reindex every admitted file rather than only what changed.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The argv the child was started with — what a caller shows somebody who has to reproduce
|
|
60
|
+
this build in the foreground. Returned rather than rebuilt at the call site so that what is
|
|
61
|
+
printed is what actually ran.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
OSError: the child could not be started at all — no interpreter, no permission. Raised
|
|
65
|
+
rather than swallowed, because the caller has just told somebody a build is running and
|
|
66
|
+
nothing else would ever correct that.
|
|
67
|
+
"""
|
|
68
|
+
argv = command(name, project=project, full=full)
|
|
69
|
+
process = subprocess.Popen( # noqa: S603 — a fixed argv this process constructed, whose one
|
|
70
|
+
# variable part is passed as an argument vector with no shell involved.
|
|
71
|
+
argv,
|
|
72
|
+
start_new_session=True,
|
|
73
|
+
stdin=subprocess.DEVNULL,
|
|
74
|
+
stdout=subprocess.DEVNULL,
|
|
75
|
+
stderr=subprocess.DEVNULL,
|
|
76
|
+
)
|
|
77
|
+
# A new session gives the child no controlling terminal and its own process group; it does not
|
|
78
|
+
# reparent it, so this process stays its parent for as long as it lives. A short-lived caller
|
|
79
|
+
# exits first and the child is reparented then — but a long-lived one that never waited would
|
|
80
|
+
# accumulate a zombie per build, so the wait happens on a daemon thread: it reaps without
|
|
81
|
+
# blocking here, and without keeping the caller alive past its own work.
|
|
82
|
+
threading.Thread(target=process.wait, daemon=True).start()
|
|
83
|
+
return argv
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""Build one knowledge base's index, and say what the build did.
|
|
2
|
+
|
|
3
|
+
The whole of the command is here; `__main__` only turns its status into an exit. The management
|
|
4
|
+
command **spawns this module** rather than calling `build`, so a build means one thing however it
|
|
5
|
+
was asked for — and `detach.command` is the argv it spawns, which is also what it prints for
|
|
6
|
+
anybody who has to run the same build where its output can be seen.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import asyncio
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import aiosqlite
|
|
16
|
+
|
|
17
|
+
from zikaron.core.config.resolution import EffectiveConfig
|
|
18
|
+
from zikaron.core.indexing.encoder import FastEmbedEncoder
|
|
19
|
+
from zikaron.core.knowledge import builds, disposal, lifecycle, scan
|
|
20
|
+
from zikaron.core.knowledge.counters import SkipReason
|
|
21
|
+
from zikaron.core.knowledge.meta import GitMode
|
|
22
|
+
from zikaron.knowledge import scope
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parser() -> argparse.ArgumentParser:
|
|
26
|
+
parser = argparse.ArgumentParser(
|
|
27
|
+
# Derived rather than written out, so the usage text stays true through a rename — this is
|
|
28
|
+
# the same module path the spawn builds its argv from, and two spellings of it would drift.
|
|
29
|
+
prog=f"python -m {__package__}",
|
|
30
|
+
description="Build one knowledge base's index: walk its root, detect what changed, and "
|
|
31
|
+
"record it.",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"name", help="which knowledge base to build, as `zikaron knowledge list` names it"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--project",
|
|
38
|
+
type=Path,
|
|
39
|
+
default=None,
|
|
40
|
+
help="the project whose store to act on. The default is the harness's own project "
|
|
41
|
+
"directory where it names one, else the current directory.",
|
|
42
|
+
)
|
|
43
|
+
parser.add_argument(
|
|
44
|
+
"--full",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="reindex every file rather than only what changed.",
|
|
47
|
+
)
|
|
48
|
+
return parser
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
52
|
+
"""Build the named knowledge base. Returns a process exit status."""
|
|
53
|
+
args = _parser().parse_args(sys.argv[1:] if argv is None else argv)
|
|
54
|
+
return scope.execute(lambda: _run(args), printer=lambda line: print(line, file=sys.stderr))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def _run(args: argparse.Namespace) -> int:
|
|
58
|
+
async with scope.open_store(args.project) as store:
|
|
59
|
+
return await build(
|
|
60
|
+
store.directory, store.connection, store.config, name=args.name, full=args.full
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def build(
|
|
65
|
+
store_dir: Path,
|
|
66
|
+
db: aiosqlite.Connection,
|
|
67
|
+
config: EffectiveConfig,
|
|
68
|
+
*,
|
|
69
|
+
name: str,
|
|
70
|
+
full: bool = False,
|
|
71
|
+
) -> int:
|
|
72
|
+
"""Run one build and print an account of it. Returns a process exit status.
|
|
73
|
+
|
|
74
|
+
**The refusals are decided before the model is loaded**, which is a second of CPU and the most
|
|
75
|
+
expensive thing this command does. It is wasted either way when a build is refused — in the
|
|
76
|
+
foreground it is the operator's second, and detached it is spent on a refusal nobody will ever
|
|
77
|
+
see. The build itself re-establishes each refusal, since the check and the build are not one
|
|
78
|
+
transaction.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
store_dir: the `.zikaron` directory this store lives in.
|
|
82
|
+
db: an open connection to `memory.db`, which carries the registry.
|
|
83
|
+
config: the effective configuration: which encoder to load, how many chunks to embed per
|
|
84
|
+
pass, and what to compare the built corpus's identity against afterwards.
|
|
85
|
+
name: which knowledge base to build.
|
|
86
|
+
full: reindex every admitted file rather than only what changed.
|
|
87
|
+
"""
|
|
88
|
+
await builds.prepare(store_dir, db, config, name=name)
|
|
89
|
+
refreshed = await lifecycle.refresh(
|
|
90
|
+
store_dir, db, config, name=name, build=await build_settings(config, full=full)
|
|
91
|
+
)
|
|
92
|
+
_print_report(refreshed)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
async def build_settings(config: EffectiveConfig, *, full: bool = False) -> disposal.BuildSettings:
|
|
97
|
+
"""Load the configured encoder and read the batch size, for one build.
|
|
98
|
+
|
|
99
|
+
The load costs roughly a second and runs on a worker thread, which keeps it off the event loop
|
|
100
|
+
rather than making it cheaper — a build is a batch job with no interactive path to protect, so
|
|
101
|
+
paying it up front and once is the right trade. A corpus whose recorded identity disagrees with
|
|
102
|
+
what this loads is not refused: it is rebuilt at the new identity, which is the only repair
|
|
103
|
+
available once the width of a stored vector has stopped matching what the model emits.
|
|
104
|
+
"""
|
|
105
|
+
return disposal.BuildSettings(
|
|
106
|
+
encoder=await asyncio.to_thread(FastEmbedEncoder.load, config.get_str("embed_model")),
|
|
107
|
+
embed_batch=config.get_int("knowledge_embed_batch"),
|
|
108
|
+
full=full,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _print_report(refreshed: lifecycle.Refreshed) -> None:
|
|
113
|
+
result = refreshed.result
|
|
114
|
+
counters = result.counters
|
|
115
|
+
print(f"built {refreshed.knowledge_base.name!r}")
|
|
116
|
+
# "in the index" rather than "indexed", because the count describes the corpus this build
|
|
117
|
+
# leaves behind rather than the work it did: a build that found nothing changed still reports
|
|
118
|
+
# every file it confirmed is there.
|
|
119
|
+
print(
|
|
120
|
+
f"files {counters.files_indexed} in the index, {result.files_deleted} deleted, "
|
|
121
|
+
f"{counters.files_skipped} skipped, {counters.files_seen} seen"
|
|
122
|
+
)
|
|
123
|
+
print(f"bytes {counters.bytes_indexed}")
|
|
124
|
+
print(f"skipped {_by_reason(counters.skipped)}")
|
|
125
|
+
print(f"pruned {counters.pruned_directories} directories")
|
|
126
|
+
print(f"state {refreshed.status.summary.state.value}")
|
|
127
|
+
_print_caveats(result)
|
|
128
|
+
_print_rebuild(result)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _print_rebuild(result: scan.ScanResult) -> None:
|
|
132
|
+
"""Say when a build was a rebuild, because nothing in its numbers would show it.
|
|
133
|
+
|
|
134
|
+
A rebuild reindexes every file and deletes none, which is what a corpus's first build looks
|
|
135
|
+
like too — and what changed is not in the corpus at all but in what embedded it.
|
|
136
|
+
|
|
137
|
+
The wording names the outcome rather than the cause, because there are two and the numbers
|
|
138
|
+
distinguish neither: the model may have moved under the index, or the vector table may have
|
|
139
|
+
been found declared at a width the corpus's own record does not name, which nothing this
|
|
140
|
+
program does can produce and a restored or hand-edited database can.
|
|
141
|
+
"""
|
|
142
|
+
rebuilt = result.rebuilt_identity
|
|
143
|
+
if rebuilt is None:
|
|
144
|
+
return
|
|
145
|
+
print(
|
|
146
|
+
f"\nThis corpus's stored vectors did not match the model it records, so its index was "
|
|
147
|
+
f"discarded and made again with {rebuilt.embed_model!r} at {rebuilt.embed_dim} dimensions."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _by_reason(counts: dict[SkipReason, int]) -> str:
|
|
152
|
+
"""Every reason that fired, with its count, or a marker when nothing was skipped.
|
|
153
|
+
|
|
154
|
+
Only the reasons that fired, because the question this answers is *why is a file missing from
|
|
155
|
+
the corpus* and a wall of zeroes buries the one that is not. The all-zero case says so
|
|
156
|
+
explicitly, since "nothing was skipped" and "this was not measured" are different answers.
|
|
157
|
+
"""
|
|
158
|
+
fired = [f"{reason.value} {count}" for reason, count in sorted(counts.items()) if count]
|
|
159
|
+
return ", ".join(fired) if fired else "none"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _print_caveats(result: scan.ScanResult) -> None:
|
|
163
|
+
"""Whatever made this build weaker than it was asked to be, or nothing at all.
|
|
164
|
+
|
|
165
|
+
Each of these is a corpus shaped by something other than its own configuration, and a build
|
|
166
|
+
that did not say so would leave the difference to be discovered as missing files.
|
|
167
|
+
"""
|
|
168
|
+
if result.git_mode_effective is not result.git_mode:
|
|
169
|
+
# Both causes are named because the code cannot tell them apart here — the work-tree probe
|
|
170
|
+
# collapses every failure to "no" — and because the *ordinary* one is first: indexing a
|
|
171
|
+
# documentation tree outside any repository is an endorsed use, and a message naming only
|
|
172
|
+
# a git failure would send that operator to debug `PATH` on every build.
|
|
173
|
+
print(
|
|
174
|
+
f"\ngit-mode {result.git_mode.value!r} did not take effect: this root is not inside a "
|
|
175
|
+
f"git work tree, or git could not answer for it, so the build ran as "
|
|
176
|
+
f"{GitMode.OFF.value!r} and no git rule was applied."
|
|
177
|
+
)
|
|
178
|
+
if not result.attributes_available:
|
|
179
|
+
print(
|
|
180
|
+
"\n.gitattributes could not be read, so files this repository marks as binary were "
|
|
181
|
+
"judged by their content alone."
|
|
182
|
+
)
|
|
183
|
+
if result.files_remaining:
|
|
184
|
+
print(
|
|
185
|
+
f"\n{result.files_remaining} file(s) could not be read and are still pending; they "
|
|
186
|
+
"are reported as stale until a later build reaches them."
|
|
187
|
+
)
|