openkos 0.2.3__tar.gz → 0.2.4__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.
- {openkos-0.2.3 → openkos-0.2.4}/PKG-INFO +2 -1
- {openkos-0.2.3 → openkos-0.2.4}/README.md +1 -0
- {openkos-0.2.3 → openkos-0.2.4}/pyproject.toml +1 -1
- {openkos-0.2.3 → openkos-0.2.4}/pyproject.toml.orig +1 -1
- openkos-0.2.4/src/openkos/bundle/decisions.py +195 -0
- openkos-0.2.4/src/openkos/bundle/ledger.py +448 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/merge.py +116 -24
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/provenance.py +40 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/curate.py +139 -17
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/main.py +2877 -268
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/next_action.py +190 -8
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/config.py +13 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/concept.py +1051 -43
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/sqlite_graph.py +23 -4
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/lint.py +51 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/okf.py +131 -6
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/relations.py +25 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/types.py +39 -7
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/contradiction.py +102 -6
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/edge_typing.py +42 -5
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/similarity.py +34 -3
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/answer.py +37 -2
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/source_title.py +37 -3
- openkos-0.2.4/src/openkos/state/findings.py +212 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/reindex.py +55 -4
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/vcs/git.py +19 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/bundle.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/index.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/links.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/listing.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/log.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/references.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/relations.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/source_titles.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/observability.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/judge.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/fsio.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/analysis.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/base.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/proximity.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/summary.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/lifecycle.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/base.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/ollama.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/parsing.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/py.typed +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/adjudication.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/candidates.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/normalize.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/volatility_typing.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/fusion.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/pool.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/sensitivity.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/__init__.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/derived.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/fts.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/vectorstore.py +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/templates/agents.md.template +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/templates/openkos.yaml.template +0 -0
- {openkos-0.2.3 → openkos-0.2.4}/src/openkos/vcs/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openkos
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Local-first engine that turns your scattered text into a living, portable knowledge base in the Open Knowledge Format (OKF)
|
|
5
5
|
Keywords: okf,open-knowledge-format,knowledge-base,knowledge-graph,local-first,rag,llm,second-brain,cli
|
|
6
6
|
Author: Jason
|
|
@@ -151,6 +151,7 @@ Beyond that: a desktop app, graph visualization, richer memory, and federation
|
|
|
151
151
|
- [`docs/philosophy.md`](https://github.com/jasonssdev/openkos/blob/main/docs/philosophy.md) — the foundational essay: what knowledge is and why OpenKOS matters
|
|
152
152
|
- [`docs/knowledge-object-model.md`](https://github.com/jasonssdev/openkos/blob/main/docs/knowledge-object-model.md) — how knowledge is represented (OKF + the OpenKOS layer)
|
|
153
153
|
- [`docs/roadmap.md`](https://github.com/jasonssdev/openkos/blob/main/docs/roadmap.md) — the MVP roadmap
|
|
154
|
+
- [`docs/ideas.md`](https://github.com/jasonssdev/openkos/blob/main/docs/ideas.md) — ideas under consideration, none of them committed
|
|
154
155
|
- [`docs/tech_stack.md`](https://github.com/jasonssdev/openkos/blob/main/docs/tech_stack.md) — technology choices
|
|
155
156
|
- [`docs/architecture.md`](https://github.com/jasonssdev/openkos/blob/main/docs/architecture.md) — repository and bundle structure, and source versioning
|
|
156
157
|
- [`docs/okf-alignment.md`](https://github.com/jasonssdev/openkos/blob/main/docs/okf-alignment.md) — how OpenKOS relates to OKF
|
|
@@ -118,6 +118,7 @@ Beyond that: a desktop app, graph visualization, richer memory, and federation
|
|
|
118
118
|
- [`docs/philosophy.md`](https://github.com/jasonssdev/openkos/blob/main/docs/philosophy.md) — the foundational essay: what knowledge is and why OpenKOS matters
|
|
119
119
|
- [`docs/knowledge-object-model.md`](https://github.com/jasonssdev/openkos/blob/main/docs/knowledge-object-model.md) — how knowledge is represented (OKF + the OpenKOS layer)
|
|
120
120
|
- [`docs/roadmap.md`](https://github.com/jasonssdev/openkos/blob/main/docs/roadmap.md) — the MVP roadmap
|
|
121
|
+
- [`docs/ideas.md`](https://github.com/jasonssdev/openkos/blob/main/docs/ideas.md) — ideas under consideration, none of them committed
|
|
121
122
|
- [`docs/tech_stack.md`](https://github.com/jasonssdev/openkos/blob/main/docs/tech_stack.md) — technology choices
|
|
122
123
|
- [`docs/architecture.md`](https://github.com/jasonssdev/openkos/blob/main/docs/architecture.md) — repository and bundle structure, and source versioning
|
|
123
124
|
- [`docs/okf-alignment.md`](https://github.com/jasonssdev/openkos/blob/main/docs/okf-alignment.md) — how OpenKOS relates to OKF
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Operator decision sidecar store for pending contradiction findings
|
|
2
|
+
(pending-work design, Decisions 3-4; ADR-0014).
|
|
3
|
+
|
|
4
|
+
An irreplaceable human verdict on a machine proposal -- exactly one kind
|
|
5
|
+
extends ADR-0013's `bundle/.state/` mechanism, deliberately excluding the
|
|
6
|
+
findings themselves (`state.findings`, recomputable machine inference kept
|
|
7
|
+
out of `bundle/`). Reuses ADR-0013's storage shape verbatim: one file per
|
|
8
|
+
sorted-first concept id, mirroring `bundle.ledger.ledger_path_for`;
|
|
9
|
+
`okf.concept_path_for`'s `(root, suffix)` generalization for id-to-path
|
|
10
|
+
mapping; `okf.dump_frontmatter`/`load_frontmatter` for a frontmatter
|
|
11
|
+
container with an empty body (ADR-0002 invariant 3); and a non-`.md`
|
|
12
|
+
suffix (`.decisions.okf`) for free structural exclusion from every
|
|
13
|
+
`rglob("*.md")` EXCLUDE walk, already policed unmodified by
|
|
14
|
+
`lint.check_state_dir_contains_no_markdown`.
|
|
15
|
+
|
|
16
|
+
A single record is ids + verdict only -- no rationale, no body text -- which
|
|
17
|
+
is what keeps this store non-confidential (Decision 4).
|
|
18
|
+
|
|
19
|
+
Leaf module: mirrors `bundle/ledger.py`/`bundle/relations.py`/
|
|
20
|
+
`bundle/links.py` -- MUST NOT import `openkos.graph` (canonical-layer rule,
|
|
21
|
+
AGENTS.md:41; guarded by `tests/unit/bundle/test_layering.py`).
|
|
22
|
+
|
|
23
|
+
Deliberately UNWIRED to any CLI verb in this slice (tasks.md slicing
|
|
24
|
+
rationale, maintainer decision D6): no operator action can produce a
|
|
25
|
+
`bundle/.state/decisions/**` file through the shipped CLI yet -- only a
|
|
26
|
+
direct unit-test call to `write_decisions` can."""
|
|
27
|
+
|
|
28
|
+
import hashlib
|
|
29
|
+
from dataclasses import dataclass
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Final, Literal
|
|
32
|
+
|
|
33
|
+
from openkos import fsio
|
|
34
|
+
from openkos.model import okf
|
|
35
|
+
|
|
36
|
+
DECISIONS_DIRNAME: Final = "decisions"
|
|
37
|
+
"""The subdirectory of `okf.STATE_DIRNAME` holding every decision sidecar,
|
|
38
|
+
mirroring `bundle.ledger.LEDGER_DIRNAME`'s own precedent."""
|
|
39
|
+
|
|
40
|
+
DECISIONS_SUFFIX: Final = ".decisions.okf"
|
|
41
|
+
"""Never `.md` -- see this module's docstring and `bundle.ledger.
|
|
42
|
+
LEDGER_SUFFIX`'s own rationale, which applies identically here."""
|
|
43
|
+
|
|
44
|
+
DECISIONS_SCHEMA: Final = "openkos.contradiction_decisions/v1"
|
|
45
|
+
"""The committed sidecar's own `schema` key, versioned independently of any
|
|
46
|
+
single `DecisionRecord`'s shape -- mirrors `bundle.ledger.
|
|
47
|
+
LEDGER_SIDECAR_SCHEMA`."""
|
|
48
|
+
|
|
49
|
+
_DECISION_KEY_HEX_CHARS: Final = 32
|
|
50
|
+
"""Mirrors `model.okf._ORIGIN_KEY_HEX_CHARS` (Decision 3): 128 bits of the
|
|
51
|
+
digest, unambiguous for any realistic workspace."""
|
|
52
|
+
|
|
53
|
+
DecisionState = Literal["declined", "open"]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def decision_key_for(pair_ids: tuple[str, str], merged_absorbed_id: str | None) -> str:
|
|
57
|
+
"""The stable identity a decision is keyed on (Decision 3):
|
|
58
|
+
`sha256("contradiction/v1\\n" + pair_ids[0] + "\\n" + pair_ids[1] + "\\n"
|
|
59
|
+
+ (merged_absorbed_id or ""))[:32]`.
|
|
60
|
+
|
|
61
|
+
Never a findings row id -- a findings row is recomputed on every
|
|
62
|
+
`curate` run and its row id is not stable across recomputation, so
|
|
63
|
+
keying on it would silently evaporate every declination on the next
|
|
64
|
+
run (proposal's Critical risk). `merged_absorbed_id` is mandatory in
|
|
65
|
+
the digest, not optional: it is the SOLE discriminator between a
|
|
66
|
+
typed-edge candidate and a merged-body candidate sharing the same
|
|
67
|
+
`pair_ids` (`resolution.contradiction.ContradictionVerdict.
|
|
68
|
+
merged_absorbed_id`'s own warning) -- `pair_ids` shape alone is not a
|
|
69
|
+
safe stand-in."""
|
|
70
|
+
payload = (
|
|
71
|
+
"contradiction/v1\n"
|
|
72
|
+
+ pair_ids[0]
|
|
73
|
+
+ "\n"
|
|
74
|
+
+ pair_ids[1]
|
|
75
|
+
+ "\n"
|
|
76
|
+
+ (merged_absorbed_id or "")
|
|
77
|
+
)
|
|
78
|
+
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
79
|
+
return digest[:_DECISION_KEY_HEX_CHARS]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class DecisionRecord:
|
|
84
|
+
"""One operator decision on one contradiction proposal (Decision 4):
|
|
85
|
+
ids and verdict only -- no rationale, no body text."""
|
|
86
|
+
|
|
87
|
+
decision_key: str
|
|
88
|
+
pair_ids: tuple[str, str]
|
|
89
|
+
merged_absorbed_id: str | None
|
|
90
|
+
state: DecisionState
|
|
91
|
+
decided_at: str
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def decisions_root(bundle_dir: Path) -> Path:
|
|
95
|
+
"""`bundle_dir/.state/decisions` -- the root every decision sidecar
|
|
96
|
+
lives under (Decision 4), mirroring `bundle.ledger.ledger_root`."""
|
|
97
|
+
return bundle_dir / okf.STATE_DIRNAME / DECISIONS_DIRNAME
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def decisions_path_for(concept_id: str, bundle_dir: Path) -> Path:
|
|
101
|
+
"""The committed sidecar path for `concept_id` -- the SAME
|
|
102
|
+
NFC/NFD-tolerant resolver `okf.concept_path_for` uses for concept files
|
|
103
|
+
and `bundle.ledger.ledger_path_for` reuses for the merge ledger,
|
|
104
|
+
generalized to this store's `(root, suffix)` (Decision 4: "do not
|
|
105
|
+
invent a second id-to-path mapping")."""
|
|
106
|
+
return okf.concept_path_for(
|
|
107
|
+
concept_id, decisions_root(bundle_dir), suffix=DECISIONS_SUFFIX
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _encode_container(
|
|
112
|
+
concept_id: str, records: list[DecisionRecord]
|
|
113
|
+
) -> dict[str, object]:
|
|
114
|
+
return {
|
|
115
|
+
"schema": DECISIONS_SCHEMA,
|
|
116
|
+
"concept_id": concept_id,
|
|
117
|
+
"decisions": [
|
|
118
|
+
{
|
|
119
|
+
"decision_key": record.decision_key,
|
|
120
|
+
"pair_ids": list(record.pair_ids),
|
|
121
|
+
"merged_absorbed_id": record.merged_absorbed_id,
|
|
122
|
+
"state": record.state,
|
|
123
|
+
"decided_at": record.decided_at,
|
|
124
|
+
}
|
|
125
|
+
for record in records
|
|
126
|
+
],
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _decode_record(raw: dict[str, object]) -> DecisionRecord:
|
|
131
|
+
pair_ids_raw = raw["pair_ids"]
|
|
132
|
+
if not isinstance(pair_ids_raw, list) or len(pair_ids_raw) != 2:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"malformed decision record: pair_ids must be a 2-item list, got {pair_ids_raw!r}"
|
|
135
|
+
)
|
|
136
|
+
return DecisionRecord(
|
|
137
|
+
decision_key=str(raw["decision_key"]),
|
|
138
|
+
pair_ids=(str(pair_ids_raw[0]), str(pair_ids_raw[1])),
|
|
139
|
+
merged_absorbed_id=(
|
|
140
|
+
None
|
|
141
|
+
if raw.get("merged_absorbed_id") is None
|
|
142
|
+
else str(raw["merged_absorbed_id"])
|
|
143
|
+
),
|
|
144
|
+
state="declined" if raw["state"] == "declined" else "open",
|
|
145
|
+
decided_at=str(raw["decided_at"]),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def read_decisions(concept_id: str, bundle_dir: Path) -> list[DecisionRecord]:
|
|
150
|
+
"""Read every `DecisionRecord` recorded for `concept_id`'s sidecar. No
|
|
151
|
+
sidecar on disk (no decision ever declined/reopened under this concept
|
|
152
|
+
id) returns `[]` -- mirrors `bundle.ledger.read_entries`'s own
|
|
153
|
+
"absent file" contract."""
|
|
154
|
+
path = decisions_path_for(concept_id, bundle_dir)
|
|
155
|
+
if not path.is_file():
|
|
156
|
+
return []
|
|
157
|
+
metadata, _ = okf.load_frontmatter(path.read_text(encoding="utf-8"))
|
|
158
|
+
raw_decisions = metadata.get("decisions")
|
|
159
|
+
if not isinstance(raw_decisions, list):
|
|
160
|
+
return []
|
|
161
|
+
return [_decode_record(raw) for raw in raw_decisions if isinstance(raw, dict)]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def write_decisions(
|
|
165
|
+
concept_id: str, bundle_dir: Path, *, records: list[DecisionRecord]
|
|
166
|
+
) -> Path:
|
|
167
|
+
"""(Re)write `concept_id`'s committed sidecar to hold EXACTLY `records`,
|
|
168
|
+
replacing whatever it held before -- mirrors `bundle.ledger.
|
|
169
|
+
write_entries`'s full-replace contract. An empty `records` list removes
|
|
170
|
+
the sidecar file entirely; removing an already-absent sidecar is a
|
|
171
|
+
no-op, not an error.
|
|
172
|
+
|
|
173
|
+
Written via `fsio.write_atomic`, over `okf.dump_frontmatter`'s output
|
|
174
|
+
with an empty body (ADR-0002 invariant 3, preserved literally)."""
|
|
175
|
+
path = decisions_path_for(concept_id, bundle_dir)
|
|
176
|
+
if not records:
|
|
177
|
+
if path.is_file():
|
|
178
|
+
path.unlink()
|
|
179
|
+
return path
|
|
180
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
container = _encode_container(concept_id, records)
|
|
182
|
+
fsio.write_atomic(path, okf.dump_frontmatter(container, body=""))
|
|
183
|
+
return path
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def iter_decisions(bundle_dir: Path) -> list[Path]:
|
|
187
|
+
"""Every committed decision sidecar under `bundle_dir`'s decisions
|
|
188
|
+
root, sorted -- the ONE shared INCLUDE-walk primitive `purge`/`forget`
|
|
189
|
+
reuse for their privacy sweep (Decision 4), mirroring `bundle.ledger.
|
|
190
|
+
iter_ledgers`. A missing decisions root (no decision has ever been
|
|
191
|
+
written) returns `[]` rather than raising."""
|
|
192
|
+
root = decisions_root(bundle_dir)
|
|
193
|
+
if not root.is_dir():
|
|
194
|
+
return []
|
|
195
|
+
return sorted(root.rglob(f"*{DECISIONS_SUFFIX}"))
|