devtorch-core 3.0.1__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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,228 @@
1
+ """Reasoning template library for DevTorch.
2
+
3
+ Provides built-in reasoning templates and a documented extension API for
4
+ project-level custom templates under ``.devtorch/templates/`` or ``templates/``.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ __all__ = [
13
+ "list_builtin_templates",
14
+ "load_builtin_template",
15
+ "discover_custom_templates",
16
+ "apply_template",
17
+ "render_template",
18
+ ]
19
+
20
+ _BUILTIN_DIR = Path(__file__).resolve().parent
21
+ _PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")
22
+
23
+
24
+ def _parse_front_matter(text: str) -> tuple[dict[str, Any], str]:
25
+ """Parse a minimal YAML-like front-matter block at the top of ``text``.
26
+
27
+ Returns a ``(metadata, body)`` tuple. Only the scalar keys required by
28
+ the library (``name``, ``description``, ``tags``) are parsed, and ``tags``
29
+ is normalised to a list of strings.
30
+ """
31
+ lines = text.splitlines()
32
+ if not lines or lines[0].strip() != "---":
33
+ return {}, text
34
+
35
+ end = -1
36
+ for i in range(1, len(lines)):
37
+ if lines[i].strip() == "---":
38
+ end = i
39
+ break
40
+
41
+ if end == -1:
42
+ return {}, text
43
+
44
+ meta_text = "\n".join(lines[1:end])
45
+ body = "\n".join(lines[end + 1 :])
46
+ metadata: dict[str, Any] = {}
47
+
48
+ for line in meta_text.splitlines():
49
+ if ":" not in line:
50
+ continue
51
+ key, _, value = line.partition(":")
52
+ key = key.strip()
53
+ value = value.strip()
54
+ if not key:
55
+ continue
56
+
57
+ # Strip matching quotes.
58
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
59
+ value = value[1:-1]
60
+
61
+ if key == "tags":
62
+ value = _parse_tags(value)
63
+
64
+ metadata[key] = value
65
+
66
+ return metadata, body
67
+
68
+
69
+ def _parse_tags(value: str) -> list[str]:
70
+ """Parse ``tags`` as a YAML list ``[a, b]`` or a comma-separated string."""
71
+ value = value.strip()
72
+ if value.startswith("[") and value.endswith("]"):
73
+ inner = value[1:-1].strip()
74
+ if not inner:
75
+ return []
76
+ return [
77
+ item.strip().strip('"\'')
78
+ for item in inner.split(",")
79
+ if item.strip()
80
+ ]
81
+ if value:
82
+ return [item.strip() for item in value.split(",") if item.strip()]
83
+ return []
84
+
85
+
86
+ def list_builtin_templates() -> list[dict[str, Any]]:
87
+ """Return metadata for every built-in reasoning template.
88
+
89
+ Each entry contains at least ``name``, ``description``, ``tags``, and
90
+ ``filename``.
91
+ """
92
+ templates: list[dict[str, Any]] = []
93
+ if not _BUILTIN_DIR.exists():
94
+ return templates
95
+
96
+ for path in sorted(_BUILTIN_DIR.glob("*.md")):
97
+ text = path.read_text(encoding="utf-8")
98
+ meta, _ = _parse_front_matter(text)
99
+ templates.append(
100
+ {
101
+ "name": meta.get("name", path.stem),
102
+ "description": meta.get("description", ""),
103
+ "tags": meta.get("tags", []),
104
+ "filename": path.name,
105
+ }
106
+ )
107
+
108
+ return templates
109
+
110
+
111
+ def load_builtin_template(name: str) -> str:
112
+ """Return the raw Markdown text of a built-in template.
113
+
114
+ Raises:
115
+ ValueError: if the template is not built-in.
116
+ """
117
+ path = _BUILTIN_DIR / f"{name}.md"
118
+ if not path.exists():
119
+ raise ValueError(f"Unknown built-in template: {name}")
120
+ return path.read_text(encoding="utf-8")
121
+
122
+
123
+ def discover_custom_templates(project_root: Path | str) -> list[dict[str, Any]]:
124
+ """Discover custom templates under ``.devtorch/templates/`` and ``templates/``.
125
+
126
+ Templates are returned in discovery order: ``.devtorch/templates/`` first,
127
+ then project-level ``templates/``.
128
+ """
129
+ root = Path(project_root).resolve()
130
+ origins = [root / ".devtorch" / "templates", root / "templates"]
131
+ templates: list[dict[str, Any]] = []
132
+
133
+ for origin_dir in origins:
134
+ if not origin_dir.is_dir():
135
+ continue
136
+ for path in sorted(origin_dir.glob("*.md")):
137
+ text = path.read_text(encoding="utf-8")
138
+ meta, _ = _parse_front_matter(text)
139
+ try:
140
+ origin = str(path.parent.relative_to(root))
141
+ except ValueError:
142
+ origin = str(path.parent)
143
+ templates.append(
144
+ {
145
+ "name": meta.get("name", path.stem),
146
+ "description": meta.get("description", ""),
147
+ "tags": meta.get("tags", []),
148
+ "path": str(path),
149
+ "origin": origin,
150
+ }
151
+ )
152
+
153
+ return templates
154
+
155
+
156
+ def render_template(template_text: str, variables: dict[str, Any]) -> str:
157
+ """Replace ``{{variable}}`` placeholders with values from ``variables``.
158
+
159
+ Unknown placeholders are left untouched so callers can see what is still
160
+ required.
161
+ """
162
+ string_vars = {str(k): str(v) for k, v in variables.items()}
163
+
164
+ def _replace(match: re.Match[str]) -> str:
165
+ key = match.group(1)
166
+ return string_vars.get(key, match.group(0))
167
+
168
+ return _PLACEHOLDER_RE.sub(_replace, template_text)
169
+
170
+
171
+ def _resolve_template(name: str, project_root: Path | str | None = None) -> str:
172
+ """Load template text from built-ins or, optionally, custom directories.
173
+
174
+ Custom templates take precedence over built-ins when a name collision
175
+ occurs, so projects can extend or override the default reasoning library.
176
+ """
177
+ if project_root is not None:
178
+ for custom in discover_custom_templates(project_root):
179
+ if custom["name"] == name:
180
+ return Path(custom["path"]).read_text(encoding="utf-8")
181
+
182
+ builtin_path = _BUILTIN_DIR / f"{name}.md"
183
+ if builtin_path.exists():
184
+ return builtin_path.read_text(encoding="utf-8")
185
+
186
+ raise ValueError(f"Unknown template: {name}")
187
+
188
+
189
+ def _default_output_path(name: str, project_root: Path | str | None = None) -> Path:
190
+ """Default render target inside ``.GCC/templates/rendered/``."""
191
+ root = Path(project_root) if project_root else Path.cwd()
192
+ return root / ".GCC" / "templates" / "rendered" / f"{name}.md"
193
+
194
+
195
+ def apply_template(
196
+ name: str,
197
+ variables: dict[str, Any],
198
+ output_path: Path | str | None = None,
199
+ project_root: Path | str | None = None,
200
+ ) -> Path:
201
+ """Render a template (built-in or custom) and write it to ``output_path``.
202
+
203
+ If ``output_path`` is not provided, the rendered output is written to
204
+ ``.GCC/templates/rendered/<name>.md`` relative to ``project_root`` (or the
205
+ current working directory if ``project_root`` is not given).
206
+
207
+ Args:
208
+ name: Template name (matches the filename stem, or the ``name`` field
209
+ in the front matter).
210
+ variables: Mapping of placeholder names to values.
211
+ output_path: Destination path for the rendered template.
212
+ project_root: Optional project root used to resolve custom templates
213
+ and default output paths.
214
+
215
+ Returns:
216
+ The absolute path where the rendered template was written.
217
+
218
+ Raises:
219
+ ValueError: if the template cannot be found.
220
+ """
221
+ template_text = _resolve_template(name, project_root)
222
+ rendered = render_template(template_text, variables)
223
+
224
+ out = Path(output_path) if output_path else _default_output_path(name, project_root)
225
+ out = out.resolve()
226
+ out.parent.mkdir(parents=True, exist_ok=True)
227
+ out.write_text(rendered, encoding="utf-8")
228
+ return out
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: security_review
3
+ description: Structured reasoning for a security review or threat assessment
4
+ tags: [reasoning, security, review]
5
+ ---
6
+ # Security Review: {{title}}
7
+
8
+ ## Scope
9
+ {{scope}}
10
+
11
+ ## Threat model
12
+ {{threat_model}}
13
+
14
+ ## Controls evaluated
15
+ {{controls}}
16
+
17
+ ## Findings
18
+ {{findings}}
19
+
20
+ ## Risk assessment
21
+ {{reasoning}}
22
+
23
+ ## Remediation
24
+ {{remediation}}
25
+
26
+ ## Verification
27
+ {{verification}}
28
+
29
+ ## Disclosure level
30
+ {{disclosure_level}}
@@ -0,0 +1,19 @@
1
+ """Built-in Python project template."""
2
+ from __future__ import annotations
3
+
4
+ from devtorch_core.templates.engine import Template
5
+
6
+ TEMPLATE = Template(
7
+ name="python",
8
+ display_name="Python",
9
+ project_type="python",
10
+ concepts=[
11
+ ("auth", "Authentication, authorization, and identity management."),
12
+ ("schema", "Data models, validation schemas, and serialization."),
13
+ ("api", "Public interfaces, endpoints, and service contracts."),
14
+ ("data", "Persistence, queries, and data lifecycle."),
15
+ ("cli", "Command-line interface and entry points."),
16
+ ("tests", "Automated tests, fixtures, and coverage."),
17
+ ],
18
+ governance_note="Python projects default to explicit schemas, tested APIs, and clear separation between CLI, business logic, and data layers.",
19
+ )
@@ -0,0 +1,18 @@
1
+ """Built-in React project template."""
2
+ from __future__ import annotations
3
+
4
+ from devtorch_core.templates.engine import Template
5
+
6
+ TEMPLATE = Template(
7
+ name="react",
8
+ display_name="React",
9
+ project_type="react",
10
+ concepts=[
11
+ ("auth", "Authentication, authorization, and session handling."),
12
+ ("schema", "Prop types, data shapes, and API response schemas."),
13
+ ("api", "Backend integration, hooks, and service clients."),
14
+ ("ui", "Screens, layouts, and user interaction design."),
15
+ ("components", "Reusable components, composition, and state ownership."),
16
+ ],
17
+ governance_note="React projects default to component-driven UI, explicit data schemas, and isolated auth/session concerns separated from presentation logic.",
18
+ )
@@ -0,0 +1,18 @@
1
+ """Built-in TypeScript project template."""
2
+ from __future__ import annotations
3
+
4
+ from devtorch_core.templates.engine import Template
5
+
6
+ TEMPLATE = Template(
7
+ name="typescript",
8
+ display_name="TypeScript",
9
+ project_type="typescript",
10
+ concepts=[
11
+ ("auth", "Authentication, authorization, and identity management."),
12
+ ("schema", "Type definitions, validation schemas, and runtime checks."),
13
+ ("api", "HTTP, RPC, and library interfaces."),
14
+ ("ui", "User-facing components and interaction patterns."),
15
+ ("types", "Shared type definitions and generic contracts."),
16
+ ],
17
+ governance_note="TypeScript projects default to strict type contracts, schema-first APIs, and shared type definitions across UI and service boundaries.",
18
+ )
devtorch_core/theta.py ADDED
@@ -0,0 +1,221 @@
1
+ """
2
+ Sprint 3 – Coordination vector Θ and pluggable Aggφ aggregation.
3
+
4
+ ThetaStore persists Θ (a concept → summary mapping) at `.GCC/theta.json`.
5
+ AggPhi is an abstract interface; RulesBasedAggPhi is the default implementation
6
+ that averages confidence with recency weighting and escalates disclosure levels.
7
+
8
+ Callers invoke `ThetaStore.ripple(events)` to merge a batch of sensitivity
9
+ event dicts into the stored Θ, using the configured AggPhi.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import abc
15
+ import datetime as _dt
16
+ import json
17
+ import os
18
+ from pathlib import Path
19
+ from typing import Dict, List, Optional
20
+
21
+
22
+ THETA_FILE = "theta.json"
23
+
24
+ # Ordered disclosure level ranks (higher index = more restrictive)
25
+ _LEVEL_RANK: Dict[str, int] = {"PUBLIC": 0, "PROTECTED": 1, "PRIVATE": 2}
26
+ _RANK_LEVEL: Dict[int, str] = {0: "PUBLIC", 1: "PROTECTED", 2: "PRIVATE"}
27
+
28
+
29
+ class AggPhi(abc.ABC):
30
+ """
31
+ Pluggable aggregation interface (Aggφ).
32
+
33
+ Implementations receive a list of raw sensitivity event dicts and return a
34
+ *partial theta delta*: a mapping of concept → aggregated entry containing
35
+ at minimum:
36
+ mean_confidence : float
37
+ disclosure_level : str ("PUBLIC" | "PROTECTED" | "PRIVATE")
38
+ event_count : int
39
+ """
40
+
41
+ @abc.abstractmethod
42
+ def aggregate(self, events: List[dict]) -> Dict[str, dict]:
43
+ """Aggregate *events* into a per-concept summary delta."""
44
+
45
+
46
+ class RulesBasedAggPhi(AggPhi):
47
+ """
48
+ Default rules-based Aggφ.
49
+
50
+ Aggregation rules:
51
+ 1. Group events by `target_concept`.
52
+ 2. Within each group, sort by `created_at` descending (newest first).
53
+ 3. Apply recency weighting: the newest event has weight 2.0; all others 1.0.
54
+ 4. Compute weighted mean confidence.
55
+ 5. Escalate disclosure level to the maximum seen in the group.
56
+ """
57
+
58
+ def aggregate(self, events: List[dict]) -> Dict[str, dict]:
59
+ grouped: Dict[str, List[dict]] = {}
60
+ for ev in events:
61
+ concept = ev.get("target_concept", "unknown")
62
+ grouped.setdefault(concept, []).append(ev)
63
+
64
+ result: Dict[str, dict] = {}
65
+ for concept, group in grouped.items():
66
+ sorted_group = sorted(
67
+ group,
68
+ key=lambda e: e.get("created_at", ""),
69
+ reverse=True,
70
+ )
71
+ total_weight = 0.0
72
+ weighted_conf = 0.0
73
+ max_rank = 0
74
+
75
+ for i, ev in enumerate(sorted_group):
76
+ weight = 2.0 if i == 0 else 1.0
77
+ conf = float(ev.get("confidence", 0.5))
78
+ weighted_conf += weight * conf
79
+ total_weight += weight
80
+
81
+ rank = _LEVEL_RANK.get(ev.get("disclosure_level", "PUBLIC"), 0)
82
+ if rank > max_rank:
83
+ max_rank = rank
84
+
85
+ mean_conf = weighted_conf / total_weight if total_weight else 0.0
86
+ result[concept] = {
87
+ "mean_confidence": round(mean_conf, 6),
88
+ "disclosure_level": _RANK_LEVEL[max_rank],
89
+ "event_count": len(group),
90
+ }
91
+ return result
92
+
93
+
94
+ class ThetaStore:
95
+ """
96
+ Persistence layer for the coordination vector Θ.
97
+
98
+ The on-disk format is a single JSON object at `.GCC/theta.json`:
99
+ {
100
+ "version": 1,
101
+ "updated_at": "<ISO UTC>",
102
+ "coordination_vector": {
103
+ "<concept>": {
104
+ "mean_confidence": 0.85,
105
+ "disclosure_level": "PROTECTED",
106
+ "event_count": 5,
107
+ "last_updated": "<ISO UTC>"
108
+ }
109
+ }
110
+ }
111
+ """
112
+
113
+ def __init__(self, gcc_dir: Path, aggregator: Optional[AggPhi] = None) -> None:
114
+ self.gcc_dir = gcc_dir
115
+ self.theta_path = gcc_dir / THETA_FILE
116
+ self.aggregator: AggPhi = aggregator if aggregator is not None else RulesBasedAggPhi()
117
+
118
+ # ------------------------------------------------------------------
119
+ # Load / save
120
+ # ------------------------------------------------------------------
121
+
122
+ def load(self) -> dict:
123
+ """Load the current Θ from disk; return a fresh structure if absent."""
124
+ if not self.theta_path.exists():
125
+ return {"version": 1, "updated_at": None, "coordination_vector": {}}
126
+ try:
127
+ return json.loads(self.theta_path.read_text(encoding="utf-8"))
128
+ except (json.JSONDecodeError, OSError):
129
+ return {"version": 1, "updated_at": None, "coordination_vector": {}}
130
+
131
+ def save(self, theta: dict) -> None:
132
+ self.gcc_dir.mkdir(parents=True, exist_ok=True)
133
+ self.theta_path.write_text(
134
+ json.dumps(theta, indent=2, sort_keys=True) + "\n",
135
+ encoding="utf-8",
136
+ )
137
+
138
+ # ------------------------------------------------------------------
139
+ # Ripple propagation
140
+ # ------------------------------------------------------------------
141
+
142
+ def ripple(self, events: List[dict]) -> dict:
143
+ """
144
+ Aggregate *events* via Aggφ and merge the delta into the stored Θ.
145
+
146
+ Merge rules per concept:
147
+ - confidence: weighted blend of existing + new (weighted by event counts).
148
+ - disclosure_level: escalate to the maximum of existing and incoming.
149
+ - event_count: cumulative sum.
150
+
151
+ Returns the updated Θ dict.
152
+ """
153
+ delta = self.aggregator.aggregate(events)
154
+ theta = self.load()
155
+ cv: dict = theta.setdefault("coordination_vector", {})
156
+ now = _dt.datetime.now(tz=_dt.timezone.utc).isoformat()
157
+
158
+ for concept, entry in delta.items():
159
+ existing = cv.get(concept, {})
160
+ old_count = int(existing.get("event_count", 0))
161
+ new_count = int(entry["event_count"])
162
+ total = old_count + new_count
163
+
164
+ if total:
165
+ old_conf = float(existing.get("mean_confidence", entry["mean_confidence"]))
166
+ new_conf = float(entry["mean_confidence"])
167
+ blended = (old_conf * old_count + new_conf * new_count) / total
168
+ else:
169
+ blended = float(entry["mean_confidence"])
170
+
171
+ existing_rank = _LEVEL_RANK.get(existing.get("disclosure_level", "PUBLIC"), 0)
172
+ new_rank = _LEVEL_RANK.get(entry["disclosure_level"], 0)
173
+ merged_level = _RANK_LEVEL[max(existing_rank, new_rank)]
174
+
175
+ cv[concept] = {
176
+ "mean_confidence": round(blended, 6),
177
+ "disclosure_level": merged_level,
178
+ "event_count": total,
179
+ "last_updated": now,
180
+ }
181
+
182
+ theta["updated_at"] = now
183
+ self.save(theta)
184
+
185
+ # S8: near-duplicate concept pairs for I3 handshake (Textual Mode Aggφ only)
186
+ if getattr(self.aggregator, "records_i3_merge_candidates", False):
187
+ try:
188
+ from .aggphi_textual import merge_i3_records, near_duplicate_pairs
189
+
190
+ thr = float(getattr(self.aggregator, "similarity_threshold", 0.82))
191
+ theta = self.load()
192
+ cv_keys = list(theta.setdefault("coordination_vector", {}).keys())
193
+ pairs = near_duplicate_pairs(cv_keys, threshold=thr)
194
+ if pairs:
195
+ prev = theta.get("pending_i3_merges")
196
+ if not isinstance(prev, list):
197
+ prev = []
198
+ theta["pending_i3_merges"] = merge_i3_records(prev, pairs)
199
+ self.save(theta)
200
+ except Exception:
201
+ pass
202
+
203
+ return self.load()
204
+
205
+
206
+ def make_theta_store(gcc_dir: Path) -> ThetaStore:
207
+ """
208
+ Construct a :class:`ThetaStore` for *gcc_dir*, respecting ``DEVTORCH_AGGPHI``:
209
+
210
+ - ``rules`` (default): :class:`RulesBasedAggPhi`
211
+ - ``textual`` / ``s8`` / ``text``: :class:`TextualModeAggPhi` (Sprint 8)
212
+ """
213
+ mode = os.environ.get("DEVTORCH_AGGPHI", "rules").strip().lower()
214
+ if mode in ("textual", "s8", "text"):
215
+ try:
216
+ from .aggphi_textual import TextualModeAggPhi
217
+
218
+ return ThetaStore(gcc_dir, aggregator=TextualModeAggPhi())
219
+ except Exception:
220
+ return ThetaStore(gcc_dir)
221
+ return ThetaStore(gcc_dir)