docir 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 (115) hide show
  1. docir/__init__.py +25 -0
  2. docir/__main__.py +7 -0
  3. docir/config/__init__.py +0 -0
  4. docir/config/settings.py +157 -0
  5. docir/entry_points/__init__.py +0 -0
  6. docir/entry_points/cli/__init__.py +0 -0
  7. docir/entry_points/cli/app.py +516 -0
  8. docir/entry_points/cli/body_input.py +32 -0
  9. docir/entry_points/cli/rendering.py +209 -0
  10. docir/entry_points/cli/runner.py +103 -0
  11. docir/entry_points/composition.py +208 -0
  12. docir/entry_points/daemon/__init__.py +0 -0
  13. docir/entry_points/daemon/cmds.py +69 -0
  14. docir/entry_points/daemon/lifecycle.py +138 -0
  15. docir/entry_points/daemon/socket_executor.py +32 -0
  16. docir/entry_points/dispatch.py +225 -0
  17. docir/modules/__init__.py +0 -0
  18. docir/modules/agents/CONTRACT.md +50 -0
  19. docir/modules/agents/__init__.py +0 -0
  20. docir/modules/agents/api.py +46 -0
  21. docir/modules/agents/application/__init__.py +0 -0
  22. docir/modules/agents/application/ports.py +31 -0
  23. docir/modules/agents/application/service.py +146 -0
  24. docir/modules/agents/domain/__init__.py +0 -0
  25. docir/modules/agents/domain/rendering.py +84 -0
  26. docir/modules/agents/domain/results.py +35 -0
  27. docir/modules/agents/domain/targets.py +66 -0
  28. docir/modules/agents/infra/__init__.py +0 -0
  29. docir/modules/agents/infra/file_sink.py +19 -0
  30. docir/modules/agents/infra/template_provider.py +22 -0
  31. docir/modules/agents/infra/templates/__init__.py +0 -0
  32. docir/modules/agents/infra/templates/skill.md +201 -0
  33. docir/modules/documents/CONTRACT.md +54 -0
  34. docir/modules/documents/__init__.py +0 -0
  35. docir/modules/documents/api.py +45 -0
  36. docir/modules/documents/application/__init__.py +0 -0
  37. docir/modules/documents/application/dto.py +209 -0
  38. docir/modules/documents/application/services/__init__.py +0 -0
  39. docir/modules/documents/application/services/document_service.py +398 -0
  40. docir/modules/documents/application/services/id_generator.py +40 -0
  41. docir/modules/documents/application/services/maintenance_service.py +174 -0
  42. docir/modules/documents/domain/__init__.py +0 -0
  43. docir/modules/documents/domain/entities/__init__.py +0 -0
  44. docir/modules/documents/domain/entities/document.py +83 -0
  45. docir/modules/documents/domain/entities/relation.py +23 -0
  46. docir/modules/documents/domain/schema.py +163 -0
  47. docir/modules/documents/domain/services/__init__.py +0 -0
  48. docir/modules/documents/domain/services/graph_checks.py +223 -0
  49. docir/modules/documents/domain/services/markdown_sections.py +66 -0
  50. docir/modules/documents/domain/services/similarity_lint.py +69 -0
  51. docir/modules/documents/domain/services/slugify.py +21 -0
  52. docir/modules/documents/domain/services/validation.py +93 -0
  53. docir/modules/documents/domain/value_objects/__init__.py +0 -0
  54. docir/modules/documents/domain/value_objects/identifiers.py +76 -0
  55. docir/modules/documents/domain/value_objects/queries.py +24 -0
  56. docir/modules/documents/domain/value_objects/relations.py +45 -0
  57. docir/modules/documents/infra/__init__.py +0 -0
  58. docir/modules/documents/infra/default_schema.py +43 -0
  59. docir/modules/documents/infra/profiles.py +176 -0
  60. docir/modules/documents/infra/schema_loader.py +177 -0
  61. docir/modules/indexing/CONTRACT.md +31 -0
  62. docir/modules/indexing/__init__.py +0 -0
  63. docir/modules/indexing/api.py +44 -0
  64. docir/modules/indexing/application/__init__.py +0 -0
  65. docir/modules/indexing/application/ports/__init__.py +0 -0
  66. docir/modules/indexing/application/ports/scheduler.py +37 -0
  67. docir/modules/indexing/domain/__init__.py +0 -0
  68. docir/modules/indexing/domain/results.py +13 -0
  69. docir/modules/indexing/domain/scoring.py +73 -0
  70. docir/modules/indexing/infra/__init__.py +0 -0
  71. docir/modules/indexing/infra/scheduler.py +115 -0
  72. docir/modules/tags/CONTRACT.md +32 -0
  73. docir/modules/tags/__init__.py +0 -0
  74. docir/modules/tags/api.py +12 -0
  75. docir/modules/tags/application/__init__.py +0 -0
  76. docir/modules/tags/application/dto.py +13 -0
  77. docir/modules/tags/application/services/__init__.py +0 -0
  78. docir/modules/tags/application/services/tag_service.py +109 -0
  79. docir/modules/tags/domain/__init__.py +0 -0
  80. docir/modules/tags/domain/entities/__init__.py +0 -0
  81. docir/modules/tags/domain/entities/tag.py +19 -0
  82. docir/platform/__init__.py +0 -0
  83. docir/platform/clock/__init__.py +6 -0
  84. docir/platform/clock/port.py +19 -0
  85. docir/platform/clock/system.py +14 -0
  86. docir/platform/embedding/__init__.py +10 -0
  87. docir/platform/embedding/deterministic.py +49 -0
  88. docir/platform/embedding/fastembed.py +51 -0
  89. docir/platform/embedding/port.py +30 -0
  90. docir/platform/embedding/vector.py +49 -0
  91. docir/platform/errors/__init__.py +123 -0
  92. docir/platform/filesystem/__init__.py +7 -0
  93. docir/platform/filesystem/markdown_store.py +185 -0
  94. docir/platform/filesystem/ports.py +62 -0
  95. docir/platform/filesystem/tag_store.py +35 -0
  96. docir/platform/persistence/__init__.py +0 -0
  97. docir/platform/persistence/alembic/env.py +45 -0
  98. docir/platform/persistence/alembic/script.py.mako +26 -0
  99. docir/platform/persistence/alembic/versions/0001_initial_index.py +106 -0
  100. docir/platform/persistence/alembic/versions/0002_typed_edges_and_staleness.py +46 -0
  101. docir/platform/persistence/engine.py +45 -0
  102. docir/platform/persistence/models.py +96 -0
  103. docir/platform/persistence/ports.py +134 -0
  104. docir/platform/persistence/repositories.py +362 -0
  105. docir/platform/persistence/sqlalchemy_uow.py +61 -0
  106. docir/platform/persistence/unit_of_work.py +51 -0
  107. docir/platform/transport/__init__.py +0 -0
  108. docir/platform/transport/client.py +47 -0
  109. docir/platform/transport/messages.py +70 -0
  110. docir/platform/transport/protocol.py +45 -0
  111. docir/platform/transport/server.py +70 -0
  112. docir-0.1.0.dist-info/METADATA +205 -0
  113. docir-0.1.0.dist-info/RECORD +115 -0
  114. docir-0.1.0.dist-info/WHEEL +4 -0
  115. docir-0.1.0.dist-info/entry_points.txt +3 -0
docir/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ # docir — Doc-Index CLI.
2
+ #
3
+ # A git-backed markdown document system with a derived, read-optimized index.
4
+ # The markdown files under the docs root are the source of truth; the SQLite
5
+ # index (metadata + FTS5 + relation graph + semantic embeddings) is a
6
+ # rebuildable projection on top of them.
7
+ #
8
+ # The package is split into four Clean Architecture layers, each in its own
9
+ # sub-package with dependencies pointing strictly inward:
10
+ #
11
+ # presentation -> application -> domain
12
+ # infrastructure ------------^ (implements domain ports)
13
+ #
14
+ # * domain — enterprise rules: entities, value objects, ports
15
+ # (interfaces), domain services, errors. Depends on
16
+ # nothing else in the package.
17
+ # * application — use cases orchestrating the domain via its ports,
18
+ # plus the DTOs that cross the boundary.
19
+ # * infrastructure — concrete adapters implementing the domain ports
20
+ # (SQLAlchemy index, filesystem store, embedder,
21
+ # scheduler, daemon transport).
22
+ # * presentation — the Typer/Rich CLI and the composition root that
23
+ # wires infrastructure adapters into the use cases.
24
+
25
+ __version__ = "0.1.0"
docir/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ # Module entry point: enables `python -m docir ...`, which is how the daemon
2
+ # lifecycle spawns its detached background process.
3
+
4
+ from docir.entry_points.cli.app import main
5
+
6
+ if __name__ == "__main__":
7
+ main()
File without changes
@@ -0,0 +1,157 @@
1
+ """Runtime settings and the ``~/.docir`` path layout.
2
+
3
+ Backed by ``pydantic-settings``: the ``home`` and ``idle_timeout`` fields are
4
+ populated from ``DOCIR_HOME`` / ``DOCIR_IDLE_TIMEOUT`` (the ``DOCIR_`` env
5
+ prefix) or their defaults. Everything the application persists lives under the
6
+ single home directory; pointing ``DOCIR_HOME`` at a temp dir is what makes the
7
+ whole system hermetic and testable — no global state leaks between runs.
8
+
9
+ The derived paths are plain ``@property`` computations over ``home``; the
10
+ settings object is frozen (immutable), which those read-only properties are
11
+ unaffected by.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import os
18
+ import tempfile
19
+ from pathlib import Path
20
+
21
+ from pydantic import Field, field_validator
22
+ from pydantic_settings import BaseSettings, SettingsConfigDict
23
+
24
+ #: Environment variable naming the root data directory (``DOCIR_`` + ``HOME``).
25
+ HOME_ENV = "DOCIR_HOME"
26
+ #: Force in-process execution (bypass the daemon) — used by tests and CI.
27
+ NO_DAEMON_ENV = "DOCIR_NO_DAEMON"
28
+ #: Idle timeout (seconds) before the daemon shuts itself down.
29
+ DEFAULT_IDLE_TIMEOUT = 900.0
30
+ #: The per-project store directory name, discovered by walking up from the CWD
31
+ #: (the ``.git`` model). ``docir init`` creates one; commands then scope to it.
32
+ PROJECT_STORE_DIRNAME = ".docir"
33
+
34
+
35
+ def discover_project_home(start: Path | None = None) -> Path | None:
36
+ """Walk up from ``start`` (default CWD) for a ``.docir`` store directory.
37
+
38
+ Returns the first ``.docir`` directory found on the path to the filesystem
39
+ root, or ``None`` if there is none — mirroring how git locates ``.git``. This
40
+ is what makes a project-local store (created by ``docir init``) take effect
41
+ without setting ``DOCIR_HOME`` in every shell.
42
+ """
43
+ current = (start or Path.cwd()).resolve()
44
+ for directory in (current, *current.parents):
45
+ candidate = directory / PROJECT_STORE_DIRNAME
46
+ if candidate.is_dir():
47
+ return candidate
48
+ return None
49
+
50
+
51
+ class Settings(BaseSettings):
52
+ """Resolved paths and tunables for one docir installation.
53
+
54
+ Field sources, highest precedence first: constructor kwargs → ``DOCIR_*``
55
+ environment variables → field defaults.
56
+ """
57
+
58
+ model_config = SettingsConfigDict(
59
+ env_prefix="DOCIR_",
60
+ case_sensitive=False,
61
+ extra="ignore",
62
+ frozen=True,
63
+ validate_default=True,
64
+ )
65
+
66
+ home: Path = Field(default_factory=lambda: Path.home() / ".docir")
67
+ idle_timeout: float = DEFAULT_IDLE_TIMEOUT
68
+ use_daemon: bool = False
69
+
70
+ @field_validator("home")
71
+ @classmethod
72
+ def _normalize_home(cls, value: Path) -> Path:
73
+ """Expand ``~`` and resolve to an absolute path however home was set."""
74
+ return Path(value).expanduser().resolve()
75
+
76
+ @classmethod
77
+ def resolve(
78
+ cls,
79
+ home: str | os.PathLike[str] | None = None,
80
+ *,
81
+ use_daemon: bool | None = None,
82
+ ) -> Settings:
83
+ """Build settings, applying the inverted ``DOCIR_NO_DAEMON`` semantics.
84
+
85
+ The daemon is used by default; a set ``DOCIR_NO_DAEMON`` env var (or an
86
+ explicit ``use_daemon=False``) forces in-process execution.
87
+
88
+ Home precedence, highest first: an explicit ``home`` argument (the
89
+ ``--home`` flag) → the ``DOCIR_HOME`` env var → a project-local
90
+ ``.docir`` discovered by walking up from the CWD → the global
91
+ ``~/.docir`` default. The discovery step is what lets ``docir init``
92
+ scope a repo's docs to the repo without exporting ``DOCIR_HOME``.
93
+ """
94
+ if use_daemon is None:
95
+ use_daemon = os.environ.get(NO_DAEMON_ENV, "") == ""
96
+ if home is not None:
97
+ return cls(home=Path(home), use_daemon=use_daemon)
98
+ if os.environ.get(HOME_ENV):
99
+ # Let pydantic read DOCIR_HOME (env_prefix DOCIR_ + field ``home``).
100
+ return cls(use_daemon=use_daemon)
101
+ discovered = discover_project_home()
102
+ if discovered is not None:
103
+ return cls(home=discovered, use_daemon=use_daemon)
104
+ return cls(use_daemon=use_daemon)
105
+
106
+ # -- derived paths ------------------------------------------------------
107
+
108
+ @property
109
+ def docs_root(self) -> Path:
110
+ """Where the canonical markdown files live."""
111
+ return self.home / "docs"
112
+
113
+ @property
114
+ def db_path(self) -> Path:
115
+ """The derived SQLite index file."""
116
+ return self.home / "index.db"
117
+
118
+ @property
119
+ def schema_path(self) -> Path:
120
+ """The per-type schema config."""
121
+ return self.home / "docs-schema.yaml"
122
+
123
+ @property
124
+ def tags_path(self) -> Path:
125
+ """The canonical tag registry file."""
126
+ return self.docs_root / "tags.yaml"
127
+
128
+ @property
129
+ def socket_path(self) -> Path:
130
+ """The daemon's Unix domain socket.
131
+
132
+ Placed under the system temp dir with a short, home-derived name rather
133
+ than inside ``home`` — a deep home path would blow past the platform's
134
+ ~104-char ``AF_UNIX`` limit. The name is stable per home, so every
135
+ client for the same installation targets the same socket.
136
+ """
137
+ digest = hashlib.sha1(str(self.home).encode("utf-8")).hexdigest()[:12]
138
+ return Path(tempfile.gettempdir()) / f"docir-{digest}.sock"
139
+
140
+ @property
141
+ def pid_path(self) -> Path:
142
+ """The daemon's PID file."""
143
+ return self.home / "daemon.pid"
144
+
145
+ @property
146
+ def log_path(self) -> Path:
147
+ """The daemon's log file."""
148
+ return self.home / "daemon.log"
149
+
150
+ @property
151
+ def database_url(self) -> str:
152
+ """The SQLAlchemy URL for the index database."""
153
+ return f"sqlite:///{self.db_path}"
154
+
155
+ def ensure_directories(self) -> None:
156
+ """Create the home and docs directories if they do not yet exist."""
157
+ self.docs_root.mkdir(parents=True, exist_ok=True)
File without changes
File without changes