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.
Files changed (69) hide show
  1. {openkos-0.2.3 → openkos-0.2.4}/PKG-INFO +2 -1
  2. {openkos-0.2.3 → openkos-0.2.4}/README.md +1 -0
  3. {openkos-0.2.3 → openkos-0.2.4}/pyproject.toml +1 -1
  4. {openkos-0.2.3 → openkos-0.2.4}/pyproject.toml.orig +1 -1
  5. openkos-0.2.4/src/openkos/bundle/decisions.py +195 -0
  6. openkos-0.2.4/src/openkos/bundle/ledger.py +448 -0
  7. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/merge.py +116 -24
  8. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/provenance.py +40 -0
  9. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/curate.py +139 -17
  10. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/main.py +2877 -268
  11. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/next_action.py +190 -8
  12. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/config.py +13 -0
  13. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/concept.py +1051 -43
  14. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/sqlite_graph.py +23 -4
  15. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/lint.py +51 -0
  16. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/okf.py +131 -6
  17. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/relations.py +25 -0
  18. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/types.py +39 -7
  19. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/contradiction.py +102 -6
  20. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/edge_typing.py +42 -5
  21. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/similarity.py +34 -3
  22. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/answer.py +37 -2
  23. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/source_title.py +37 -3
  24. openkos-0.2.4/src/openkos/state/findings.py +212 -0
  25. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/reindex.py +55 -4
  26. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/vcs/git.py +19 -0
  27. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/__init__.py +0 -0
  28. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/__init__.py +0 -0
  29. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/bundle.py +0 -0
  30. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/index.py +0 -0
  31. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/links.py +0 -0
  32. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/listing.py +0 -0
  33. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/log.py +0 -0
  34. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/references.py +0 -0
  35. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/relations.py +0 -0
  36. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/bundle/source_titles.py +0 -0
  37. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/__init__.py +0 -0
  38. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/cli/observability.py +0 -0
  39. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/__init__.py +0 -0
  40. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/extraction/judge.py +0 -0
  41. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/fsio.py +0 -0
  42. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/__init__.py +0 -0
  43. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/analysis.py +0 -0
  44. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/base.py +0 -0
  45. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/proximity.py +0 -0
  46. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/graph/summary.py +0 -0
  47. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/lifecycle.py +0 -0
  48. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/__init__.py +0 -0
  49. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/base.py +0 -0
  50. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/ollama.py +0 -0
  51. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/llm/parsing.py +0 -0
  52. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/model/__init__.py +0 -0
  53. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/py.typed +0 -0
  54. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/__init__.py +0 -0
  55. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/adjudication.py +0 -0
  56. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/candidates.py +0 -0
  57. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/normalize.py +0 -0
  58. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/resolution/volatility_typing.py +0 -0
  59. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/__init__.py +0 -0
  60. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/fusion.py +0 -0
  61. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/retrieval/pool.py +0 -0
  62. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/sensitivity.py +0 -0
  63. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/__init__.py +0 -0
  64. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/derived.py +0 -0
  65. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/fts.py +0 -0
  66. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/state/vectorstore.py +0 -0
  67. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/templates/agents.md.template +0 -0
  68. {openkos-0.2.3 → openkos-0.2.4}/src/openkos/templates/openkos.yaml.template +0 -0
  69. {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
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openkos"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Local-first engine that turns your scattered text into a living, portable knowledge base in the Open Knowledge Format (OKF)"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openkos"
3
- version = "0.2.3"
3
+ version = "0.2.4"
4
4
  description = "Local-first engine that turns your scattered text into a living, portable knowledge base in the Open Knowledge Format (OKF)"
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -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}"))