probe-contracts 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 (65) hide show
  1. probe_contracts/__init__.py +51 -0
  2. probe_contracts/agent_surface/DESIGN.md +104 -0
  3. probe_contracts/agent_surface/__init__.py +1 -0
  4. probe_contracts/agent_surface/agent_surface.py +360 -0
  5. probe_contracts/analytics/__init__.py +5 -0
  6. probe_contracts/analytics/analytics.py +116 -0
  7. probe_contracts/analytics/events.json +1019 -0
  8. probe_contracts/client_messages/__init__.py +23 -0
  9. probe_contracts/client_messages/client_messages.py +92 -0
  10. probe_contracts/codegen/__init__.py +5 -0
  11. probe_contracts/codegen/__main__.py +6 -0
  12. probe_contracts/codegen/client_messages.py +125 -0
  13. probe_contracts/codegen/codegen.py +95 -0
  14. probe_contracts/codegen/events.py +108 -0
  15. probe_contracts/codegen/field_types.py +82 -0
  16. probe_contracts/codegen/python.py +96 -0
  17. probe_contracts/codegen/transcript_switch.py +38 -0
  18. probe_contracts/codegen/typescript.py +76 -0
  19. probe_contracts/compat.py +18 -0
  20. probe_contracts/harness/__init__.py +1 -0
  21. probe_contracts/harness/harness.py +533 -0
  22. probe_contracts/harness/harnesses.json +314 -0
  23. probe_contracts/knowledge/DESIGN.md +76 -0
  24. probe_contracts/knowledge/__init__.py +18 -0
  25. probe_contracts/knowledge/knowledge.py +134 -0
  26. probe_contracts/knowledge/limits.py +174 -0
  27. probe_contracts/notes/DESIGN.md +77 -0
  28. probe_contracts/notes/__init__.py +1 -0
  29. probe_contracts/notes/notes.py +59 -0
  30. probe_contracts/overview_csp/__init__.py +5 -0
  31. probe_contracts/overview_csp/allowlist.json +15 -0
  32. probe_contracts/overview_csp/overview_csp.py +17 -0
  33. probe_contracts/probe_contracts.py +75 -0
  34. probe_contracts/records/DESIGN.md +42 -0
  35. probe_contracts/records/__init__.py +29 -0
  36. probe_contracts/records/artifact.py +81 -0
  37. probe_contracts/records/experiment.py +34 -0
  38. probe_contracts/records/lineage.py +71 -0
  39. probe_contracts/records/metric_point.py +31 -0
  40. probe_contracts/records/project.py +68 -0
  41. probe_contracts/records/records.py +50 -0
  42. probe_contracts/records/run.py +75 -0
  43. probe_contracts/records/span.py +38 -0
  44. probe_contracts/scrubber/__init__.py +1 -0
  45. probe_contracts/scrubber/credential_scan/__init__.py +151 -0
  46. probe_contracts/scrubber/credential_scan/credential_scan.py +433 -0
  47. probe_contracts/scrubber/credential_scan/decoding.py +152 -0
  48. probe_contracts/scrubber/credential_scan/events.py +272 -0
  49. probe_contracts/scrubber/credential_scan/quick.py +147 -0
  50. probe_contracts/scrubber/credential_scan/rules.py +138 -0
  51. probe_contracts/scrubber/credential_scan/shapes.py +251 -0
  52. probe_contracts/scrubber/credential_scan/values.py +354 -0
  53. probe_contracts/scrubber/credential_scan/windows.py +119 -0
  54. probe_contracts/scrubber/redaction.py +813 -0
  55. probe_contracts/scrubber/scrubber.py +1087 -0
  56. probe_contracts/scrubber/secrets.py +7 -0
  57. probe_contracts/transcript_switch/__init__.py +5 -0
  58. probe_contracts/transcript_switch/transcript_switch.py +26 -0
  59. probe_contracts/transcripts/__init__.py +1 -0
  60. probe_contracts/transcripts/atif_fragment.py +546 -0
  61. probe_contracts/transcripts/transcripts.py +395 -0
  62. probe_contracts-0.1.0.dist-info/METADATA +29 -0
  63. probe_contracts-0.1.0.dist-info/RECORD +65 -0
  64. probe_contracts-0.1.0.dist-info/WHEEL +4 -0
  65. probe_contracts-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,51 @@
1
+ """probe-contracts: what the server, the SDK/CLI and the dashboard must agree on, defined once.
2
+
3
+ The front door is lazy: `import probe_contracts.scrubber` must not pay for
4
+ pydantic and every record class. `from probe_contracts import Run` loads them.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib
10
+ from typing import Any
11
+
12
+ _RECORDS = (
13
+ "AnchorLevel",
14
+ "Artifact",
15
+ "ArtifactStatus",
16
+ "Experiment",
17
+ "LineageEntityType",
18
+ "LineageRelation",
19
+ "MetricNonFinite",
20
+ "MetricPoint",
21
+ "ParentRelation",
22
+ "Project",
23
+ "ProjectKind",
24
+ "Record",
25
+ "Run",
26
+ "RunCounts",
27
+ "RunStatus",
28
+ "Span",
29
+ )
30
+
31
+ _CLIENT_MESSAGES = (
32
+ "Action",
33
+ "Audience",
34
+ "ClientMessage",
35
+ "ClientMessageList",
36
+ "ClientRefusalBody",
37
+ "MessageCode",
38
+ "Severity",
39
+ )
40
+
41
+ __all__ = [*_RECORDS, *_CLIENT_MESSAGES, "CONTRACTS", "Contracts"]
42
+
43
+
44
+ def __getattr__(name: str) -> Any:
45
+ if name in _RECORDS:
46
+ return getattr(importlib.import_module(".records", __name__), name)
47
+ if name in _CLIENT_MESSAGES:
48
+ return getattr(importlib.import_module(".client_messages", __name__), name)
49
+ if name in ("CONTRACTS", "Contracts"):
50
+ return getattr(importlib.import_module(".probe_contracts", __name__), name)
51
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,104 @@
1
+ # contracts/probe_contracts/agent_surface: the view vocabulary both agent surfaces speak
2
+
3
+ ## Why one vocabulary, in a module that imports nothing (`agent_surface.py`)
4
+
5
+ Probe has two agent surfaces over the same research record: the MCP server
6
+ (`agent/src/probe/mcp/`), which external coding agents and the dashboard assistant both read
7
+ through, and the page-generation agent (`app/generation/agent/`). Both answer the same question
8
+ ("open this run and show me its trajectory"). If they answer to different words, a researcher who
9
+ learns one surface has learned nothing about the other, and a fix to one silently diverges from
10
+ the other. The MCP already fixed a lineage bug by making that view return ancestry and edges; a
11
+ second, independent matrix would re-break it.
12
+
13
+ So the vocabulary lives here with no dependencies: the backend reads it directly, and it is
14
+ generated into the agent package (`agent/tools/codegen/gen_views.py`, `make gen-views`). That is the
15
+ direction `agent/schema/openapi.json` already flows: the agent pulls from the backend, the backend
16
+ never writes into the agent.
17
+
18
+ Coverage may differ, spelling may not. `VIEW_MATRIX` is the union of what the surfaces offer,
19
+ because they genuinely differ. Each surface declares the subset it serves and a test holds it to
20
+ that. Neither may invent a second word for a read the other already named.
21
+
22
+ ## trial (`ViewKind.TRIAL`, 0135)
23
+
24
+ A rollout as an entity, not a span. It is reached by its rollout span id, which is both the URL
25
+ identity the dashboard uses and the durable metric-point -> rollout -> manifest join, so an agent
26
+ handed a trial link opens the same record a person is looking at. It carries an authored title,
27
+ description and notes, which is what makes it an entity rather than a filter on a run's spans: a
28
+ caveat written on one rollout is invisible from the run that contains 500.
29
+
30
+ `View.TRIALS` lists them under a run, named like `GROUPS` on an experiment: a trial only ever
31
+ exists under one run, so enumerating them is a view on the container, not a listing with a
32
+ mandatory parent. Before it, a run's trials were reachable only by reading every span the run
33
+ logged and filtering for `rollout`, which returns the span, not the authored Trial, so it cannot
34
+ see a title, a description or a note.
35
+
36
+ ## trial views (the `ViewKind.TRIAL` row of `VIEW_MATRIX`)
37
+
38
+ A trial is not a notes carrier, the only research entity that is not: a rollout ran once and is
39
+ immutable afterwards, so `description` holds what a later reader needs, and a notes document
40
+ beside it is one more place to look. So it gets identity plus the one read that is its own.
41
+
42
+ `trajectory` is not reachable by narrowing the run's: the rollout's descendants are a subtree,
43
+ and the spans route filters by direct `parent_span_id` only, so an agent asking a run for "the
44
+ spans of trial X" gets one level and reads it as the whole rollout.
45
+
46
+ No `artifacts` and no `diff`, both on purpose:
47
+
48
+ - Run artifacts carry a `span_id`, but the list route has no `span_id` filter, so the view could
49
+ only narrow a bounded page client-side and would report "no artifacts" for a trial whose
50
+ manifest sat past the cap.
51
+ - `diff` stays on the run because the sandbox-state route is keyed by the provider's trial name,
52
+ which a researcher-customized `Trial.name` no longer matches; pointing it at a trial id would
53
+ 404 exactly on the trials someone had bothered to rename.
54
+
55
+ ## Other kinds and views
56
+
57
+ - `ViewKind.PAPER` (0152): a kind, not a filter on the project's `papers` view, so "open this
58
+ paper" is a read of a thing, not a narrowing of a list.
59
+ - `ViewKind.TEAM_NOTE`: MCP-only as a readable entity. The assistant is handed this document in
60
+ its system prompt every turn (`render_team_note`), so a read tool for it would be a second copy
61
+ of something already in context, and its cap is exactly the assistant's whole tool-result
62
+ budget.
63
+ - `View.LINEAGE`: both relations, not one or the other (see `_view_run_lineage` in the MCP). The
64
+ ancestry walk alone answered "no lineage" for a run that had plainly consumed a dataset and
65
+ produced three artifacts, which an agent reads as fact. On an artifact (0255) it is the
66
+ file-side half of "what produced this number".
67
+ - `View.VIEWS`: the assistant could create a saved metric view and, before this, not list one
68
+ back, so a view it saved last week was unfindable by the agent that saved it.
69
+ - `View.CONTENT`: the artifact-content lane (1.36) as a view rather than a generation-only
70
+ expansion. Publish means publish, so an unpublished artifact refuses this view rather than
71
+ returning bytes.
72
+ - `View.SUMMARY_HISTORY`: the page's history was a dashboard-only surface, and a recorder that
73
+ rewrites the summary needs to see what it is replacing and what a person last wrote.
74
+ - `View.PICKS` (#3624): the current pick rode the branches header alone, so pick churn (who
75
+ overwrote whose pick, and how often) was unreadable by agents.
76
+
77
+ ## VIEW_PURPOSE
78
+
79
+ One agent-facing line per view, for the error a wrong view raises: it lists the kind's views with
80
+ what each returns. `available_views` on a response stays a bare list (see `_views_offered` in the
81
+ agent's MCP service for why).
82
+
83
+ Separate from the `#:` comments on `View`, deliberately. Those are for whoever maintains the file
84
+ and say why a view exists, which incident shaped it, what it used to do wrong. The purpose lines
85
+ say what an agent gets if it asks, in one line, and nothing else: a maintainer's note shipped into
86
+ a tool response is context spent on history the caller cannot act on.
87
+
88
+ Kept beside `VIEW_MATRIX` rather than in the MCP package for the same reason as the matrix: two
89
+ agent surfaces answer with the same words, and a second copy of the vocabulary goes stale. A view
90
+ absent here still works; the list surfaces print its name bare, as they all did before this
91
+ existed (`tests/unit/core/test_agent_view_vocabulary_is_generated.py` still requires a line for every
92
+ view).
93
+
94
+ ## GENERATION_VIEWS
95
+
96
+ The subset the generation surface serves, declared rather than discovered. It is a third surface,
97
+ and the one that cannot afford a silent miss: an unserved (kind, view) that falls through to a
98
+ card returns a confident answer to a different question, and the summary says it checked
99
+ something it never read. So the pairs are enumerated, a structural test holds the dispatcher to
100
+ exactly this table, and anything outside it refuses by naming what is valid.
101
+
102
+ It is narrower than `VIEW_MATRIX` where a read has no in-process reader yet (`reproduce`,
103
+ `events`, `diff`, `views`, `handoff`, `code`, `readme`). Those are gaps to close, not words to
104
+ invent: the spelling is already agreed.
@@ -0,0 +1 @@
1
+ """The view vocabulary both agent surfaces speak (`agent_surface.py`)."""
@@ -0,0 +1,360 @@
1
+ """The view vocabulary both agent surfaces (MCP, page generation) speak; generated into agent/.
2
+
3
+ Imports nothing. Coverage may differ per surface, spelling may not. Why: DESIGN.md.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from ..compat import StrEnum
9
+
10
+
11
+ class ViewKind(StrEnum):
12
+ """Entities a view can be taken on.
13
+
14
+ The shared noun list. A surface serves a subset of it and a contract test
15
+ holds each surface to its subset, so one surface adding a kind never
16
+ silently changes another's vocabulary.
17
+ """
18
+
19
+ PROJECT = "project"
20
+ EXPERIMENT = "experiment"
21
+ RUN = "run"
22
+ #: One titled sub-note (0146), by its own id (the one catalog rows and sub-note lists carry).
23
+ #: Card-only: the card is the document (title, body, headroom); history stays on the dashboard.
24
+ SUB_NOTE = "sub_note"
25
+ #: One rollout as an entity, not a span (0135): it carries an authored title, description and notes.
26
+ #: Reached by its rollout span id, the dashboard's URL identity. Why: DESIGN.md (trial).
27
+ TRIAL = "trial"
28
+ ARTIFACT = "artifact"
29
+ #: One paper (0152) a research project was built from. A kind, not a filter on `papers`: it has its
30
+ #: own row, distilled record and citation identity (`source_ref.paper_id`).
31
+ PAPER = "paper"
32
+ GROUP = "group"
33
+ SESSION = "session"
34
+ #: The tenant's one shared note. MCP-only: the assistant gets it in its system prompt every turn
35
+ #: (`render_team_note`), and its cap is the assistant's whole tool-result budget.
36
+ TEAM_NOTE = "team-note"
37
+ #: The data catalog (C3/C4): a team dataset and its versions, an external source, a checkpoint.
38
+ #: Reached by prefix ref only (`app/catalog/refs.py`). MCP-only today.
39
+ DATASET = "dataset"
40
+ SOURCE = "source"
41
+ CHECKPOINT = "checkpoint"
42
+
43
+
44
+ class View(StrEnum):
45
+ """One word per purpose-shaped read.
46
+
47
+ THIS IS THE THIN HARNESS. Capability lives in this parameter rather than in
48
+ extra tools, so reading a trajectory costs a `view` value and not a
49
+ `read_run_spans` entry in every turn's catalog. The MCP's own contract module
50
+ says the same thing in the same words, which is the point.
51
+ """
52
+
53
+ #: The cheap identity/status glance, and the default everywhere.
54
+ CARD = "card"
55
+ #: Explicit complete-record read on MCP; supports selecting one nested field.
56
+ #: Delivery is bounded separately from the stored record's shape.
57
+ RECORD = "record"
58
+ #: The spans themselves, not span-type COUNTS.
59
+ TRAJECTORY = "trajectory"
60
+ #: Both relations on a run: the parent/child walk and artifact provenance edges (`_view_run_lineage`).
61
+ #: The walk alone said "no lineage" for a run that consumed a dataset and produced 3 artifacts.
62
+ LINEAGE = "lineage"
63
+ #: Everything needed to run it again, plus an honest completeness verdict.
64
+ REPRODUCE = "reproduce"
65
+ #: The append-only lifecycle log.
66
+ EVENTS = "events"
67
+ #: Saved metric views. Without it an agent could save a view but never list it back.
68
+ VIEWS = "views"
69
+ #: One trial's begin/end filesystem diff. Needs a `trial`, which comes off
70
+ #: the run's own files.
71
+ DIFF = "diff"
72
+ #: Which coding sessions produced this thing.
73
+ SESSIONS = "sessions"
74
+ #: The files registered under this entity. MCP-only as a VIEW.
75
+ ARTIFACTS = "artifacts"
76
+ #: Free-text notes as a document of their own.
77
+ NOTES = "notes"
78
+ #: Sweeps and ensembles under an experiment.
79
+ GROUPS = "groups"
80
+ #: Rollouts under a run (like `GROUPS` on an experiment: a trial has one parent run). Filtering the
81
+ #: run's spans for `rollout` returns spans, not authored Trials, so no title, description or note.
82
+ TRIALS = "trials"
83
+ #: Immutable published manifests.
84
+ VERSIONS = "versions"
85
+ #: A session's normalized transcript, in bounded sections.
86
+ TRANSCRIPT = "transcript"
87
+ #: A project's rendered README.
88
+ README = "readme"
89
+ #: Dashboard-visible authored Markdown on a project, experiment or run.
90
+ SUMMARY = "summary"
91
+ #: Complete unified saved Overview, including generated and authored content.
92
+ OVERVIEW = "overview"
93
+ #: Series summaries. MCP-only; the MCP's `metrics` tool is the richer
94
+ #: per-grain read.
95
+ METRICS = "metrics"
96
+ #: Everything a new session needs to continue. MCP-only today.
97
+ HANDOFF = "handoff"
98
+ #: GitHub resolution per grain: a project's code sources and proxied timeline; an experiment's per-run
99
+ #: `owner/repo@sha` and stored commit window; a run's own (compare diffs another). MCP-only today.
100
+ CODE = "code"
101
+ #: A published document's own text, bounded (the artifact-content lane, 1.36, as a view). An
102
+ #: unpublished artifact refuses it; everything else about an artifact stays metadata.
103
+ CONTENT = "content"
104
+ #: A project's papers (0152), one row each: the paper's idea, the repo read alongside it, where they
105
+ #: disagree. MCP-only today; the dashboard shows the same rows as a tab.
106
+ PAPERS = "papers"
107
+ #: What a fork reads live from its source: its runs as they were at the fork, or their files. Same
108
+ #: word in the API and CLI (`GET .../experiments/{E}/inherited`, `probe experiment inherited`).
109
+ INHERITED = "inherited"
110
+ #: A project's or experiment's summary versions, newest first (who, when, via what), or one by number.
111
+ #: MCP-only; a recorder rewriting the summary must see what it replaces and what a person last wrote.
112
+ SUMMARY_HISTORY = "summary_history"
113
+ #: Lines of work: a project's tree, or an experiment's line and those it split from; best value on the
114
+ #: declared metric, effort, state, links (swarm D6). MCP-only; workspace: `browse mode=branches`.
115
+ BRANCHES = "branches"
116
+ #: A project's best-pick history, newest first: each pick and clear, who and why (#3624). The branches
117
+ #: header shows only the current pick, hiding churn. MCP-only; workspace: rides `browse mode=branches`.
118
+ PICKS = "picks"
119
+
120
+
121
+ #: What a view MEANS on a kind. The union across both surfaces; each declares the
122
+ #: subset it serves. A pair absent here is a pair neither surface may invent.
123
+ VIEW_MATRIX: dict[ViewKind, frozenset[View]] = {
124
+ ViewKind.SUB_NOTE: frozenset({View.CARD}),
125
+ ViewKind.PROJECT: frozenset(
126
+ {
127
+ View.CARD,
128
+ View.RECORD,
129
+ View.NOTES,
130
+ View.SUMMARY,
131
+ View.README,
132
+ View.OVERVIEW,
133
+ View.SESSIONS,
134
+ View.ARTIFACTS,
135
+ View.CODE,
136
+ View.PAPERS,
137
+ # lineage plan 2 (L12/L18): its OWN links, in and out, and its
138
+ # children each with a derived origin (GET /v1/projects/{id}/lineage).
139
+ View.LINEAGE,
140
+ # The summary's versions (GET /v1/projects/{P}/summary/versions[/{v}]).
141
+ View.SUMMARY_HISTORY,
142
+ # Its tree's lines of work (GET /v1/projects/{P}/branches).
143
+ View.BRANCHES,
144
+ # Its best pick's history (GET /v1/projects/{P}/best?history=N).
145
+ View.PICKS,
146
+ }
147
+ ),
148
+ ViewKind.EXPERIMENT: frozenset(
149
+ {
150
+ View.CARD,
151
+ View.RECORD,
152
+ View.NOTES,
153
+ View.GROUPS,
154
+ View.VERSIONS,
155
+ View.REPRODUCE,
156
+ View.LINEAGE,
157
+ View.SESSIONS,
158
+ View.ARTIFACTS,
159
+ View.SUMMARY,
160
+ View.OVERVIEW,
161
+ # integrations-v2 phase 6: repo changes between this experiment's runs
162
+ # (stored rows only; GET /v1/experiments/{ref}/code).
163
+ View.CODE,
164
+ # A fork's runs and files read live from its source
165
+ # (GET /v1/projects/{P}/experiments/{E}/inherited).
166
+ View.INHERITED,
167
+ # The summary's versions
168
+ # (GET /v1/projects/{P}/experiments/{E}/summary/versions[/{v}]).
169
+ View.SUMMARY_HISTORY,
170
+ # Its own line of work and the ones it split from
171
+ # (GET /v1/projects/{P}/experiments/{E}/branch).
172
+ View.BRANCHES,
173
+ }
174
+ ),
175
+ ViewKind.RUN: frozenset(
176
+ {
177
+ View.CARD,
178
+ View.RECORD,
179
+ View.NOTES,
180
+ View.TRAJECTORY,
181
+ View.LINEAGE,
182
+ View.REPRODUCE,
183
+ View.EVENTS,
184
+ View.VIEWS,
185
+ View.DIFF,
186
+ View.SESSIONS,
187
+ View.METRICS,
188
+ View.HANDOFF,
189
+ View.ARTIFACTS,
190
+ View.TRIALS,
191
+ # integrations-v2 phase 6: the run's owner/repo@sha, and a diff against another run
192
+ # (GET /v1/runs/{ref}/code and /code/compare).
193
+ View.CODE,
194
+ }
195
+ ),
196
+ # The one entity with no notes (immutable; `description` holds it). Its `trajectory` is its whole
197
+ # subtree, which narrowing the run's cannot give; no `artifacts` or `diff`: DESIGN.md (trial views).
198
+ ViewKind.TRIAL: frozenset({View.CARD, View.RECORD, View.TRAJECTORY}),
199
+ #: `lineage` (0255): which run wrote this file and which read it. No `notes` here or on a group: since
200
+ #: 2026-10-16 a file is described in its run's notes, a sweep in its experiment's.
201
+ ViewKind.ARTIFACT: frozenset(
202
+ {
203
+ View.CARD,
204
+ View.RECORD,
205
+ View.VERSIONS,
206
+ View.SESSIONS,
207
+ View.CONTENT,
208
+ View.LINEAGE,
209
+ }
210
+ ),
211
+ ViewKind.GROUP: frozenset({View.CARD, View.RECORD}),
212
+ #: `card` is the distilled record (row plus digest, what comparisons use). No separate `digest`
213
+ #: view: the useful read would always be the second call.
214
+ ViewKind.PAPER: frozenset({View.CARD}),
215
+ ViewKind.SESSION: frozenset({View.CARD, View.RECORD, View.TRANSCRIPT}),
216
+ ViewKind.TEAM_NOTE: frozenset({View.CARD, View.RECORD}),
217
+ #: `versions`: a dataset's versions, a checkpoint's run's other saves. `lineage`: the provenance walk
218
+ #: (`GET /v1/provenance`), which reaches sources. A source has no versions: its revision is in its key.
219
+ ViewKind.DATASET: frozenset({View.CARD, View.RECORD, View.VERSIONS, View.LINEAGE}),
220
+ ViewKind.CHECKPOINT: frozenset({View.CARD, View.RECORD, View.VERSIONS, View.LINEAGE}),
221
+ ViewKind.SOURCE: frozenset({View.CARD, View.RECORD, View.LINEAGE}),
222
+ }
223
+
224
+
225
+ #: One agent-facing line per view (what it returns, no history), for the error a wrong view raises.
226
+ #: `available_views` stays bare (`_views_offered`, MCP service); a view absent here prints bare. DESIGN.md.
227
+ VIEW_PURPOSE: dict[View, str] = {
228
+ View.CARD: "identity, status, and this kind's available_views",
229
+ View.RECORD: "the raw source record, pageable; one field via view_options.field",
230
+ View.SUMMARY: "the person-written block of the Overview page (`document`)",
231
+ View.OVERVIEW: "the complete saved Overview page: generated content, authored edits and chart data",
232
+ View.NOTES: "hidden operational notes: decisions, caveats, handoffs",
233
+ View.ARTIFACTS: "the files the work produced at this grain",
234
+ View.VERSIONS: "an artifact's or dataset's versions (a checkpoint: its run's saves); the reuse check",
235
+ View.METRICS: "series summaries; view_options.key drills to raw points",
236
+ View.TRAJECTORY: "the spans themselves -- ask a TRIAL for one rollout's subtree",
237
+ View.TRIALS: "the run's authored trials, newest step first",
238
+ View.EVENTS: "the append-only lifecycle log",
239
+ View.LINEAGE: "what it came from: parents, files read, and who wrote them",
240
+ View.REPRODUCE: "everything needed to run it again, plus a completeness verdict",
241
+ View.HANDOFF: "what a new session needs to continue the work",
242
+ View.CODE: "the GitHub resolution at this grain; diff with view_options.compare_to",
243
+ View.GROUPS: "sweeps and ensembles under an experiment",
244
+ View.PAPERS: "the literature attached to this project",
245
+ View.TRANSCRIPT: "a captured session's text; view_options.grep or view_options.start_line",
246
+ View.SESSIONS: "the captured agent sessions that touched this entity",
247
+ View.CONTENT: "the artifact's own bytes",
248
+ View.README: "the repository's README at this grain",
249
+ View.VIEWS: "saved metric views on this entity",
250
+ View.DIFF: "what changed, against the comparison this kind defines",
251
+ View.INHERITED: "what a fork reads live from its source: its files, or its runs with runs=true",
252
+ View.SUMMARY_HISTORY: "the summary's versions, newest first; view_options.version=N reads one's markdown",
253
+ View.BRANCHES: "lines of work: best value, effort, state; view_options.branch for one in full",
254
+ View.PICKS: "the best pick's history, newest first: who picked or cleared what, and why",
255
+ }
256
+
257
+
258
+ class MissingMarker(StrEnum):
259
+ """Why a payload is incomplete, in words a model can act on.
260
+
261
+ A view that clips and says nothing is worse than one that refuses: the model
262
+ reads a short list as a complete list and answers confidently from it. Every
263
+ projection that can clip must emit one of these.
264
+ """
265
+
266
+ #: The server capped the rows before we saw them, and the route takes no
267
+ #: offset -- so paging here would page an already-truncated list.
268
+ ROWS_CAPPED_BY_SERVER = "rows_capped_by_server"
269
+ #: We asked for a window and the window filled. Move the window, do not page.
270
+ WINDOW_FULL = "window_full"
271
+ #: The source route answered, but with a degraded body (legacy rows).
272
+ SOURCE_DEGRADED = "source_degraded"
273
+
274
+
275
+ class Admits(StrEnum):
276
+ """What citing power a GRANTED view hands the summary writer.
277
+
278
+ THE HALF THAT MAKES CITING SAFE, as a table rather than as a branch. A
279
+ Tier-2 grant widens the context and the allowed-ref set together, and the
280
+ two must move as one -- so the rule lives beside the view it belongs to
281
+ rather than inside an `elif` chain that a new view forgets to extend.
282
+
283
+ Route-specific ON PURPOSE. A generic "admit whatever came back" reads a
284
+ session digest's incidental run mention as a citeable measurement, and a
285
+ card's `project_id` as something a metric block may point at.
286
+ """
287
+
288
+ #: Prose about a thing, not a reading of it. Nothing here is a source.
289
+ NOTHING = "nothing"
290
+ #: The rows the DATABASE returned become citeable -- never the ids asked
291
+ #: for, which is how a scoped-out request stops being an existence oracle.
292
+ RETURNED_IDS = "returned_ids"
293
+ #: A metric read: the run AND the (run, key) pairs it actually carried.
294
+ RUN_AND_METRIC_KEYS = "run_and_metric_keys"
295
+
296
+
297
+ #: Per view, not per kind: a `card` admits its own row's id, run or paper. A view absent here admits
298
+ #: NOTHING, asserted by a test so a new view cannot silently inherit citing power.
299
+ VIEW_ADMISSION: dict[View, Admits] = {
300
+ View.CARD: Admits.RETURNED_IDS,
301
+ View.RECORD: Admits.NOTHING,
302
+ View.ARTIFACTS: Admits.RETURNED_IDS,
303
+ View.TRIALS: Admits.RETURNED_IDS,
304
+ View.GROUPS: Admits.RETURNED_IDS,
305
+ View.VERSIONS: Admits.RETURNED_IDS,
306
+ View.LINEAGE: Admits.RETURNED_IDS,
307
+ View.PAPERS: Admits.RETURNED_IDS,
308
+ View.INHERITED: Admits.RETURNED_IDS,
309
+ View.TRAJECTORY: Admits.RETURNED_IDS,
310
+ View.METRICS: Admits.RUN_AND_METRIC_KEYS,
311
+ # Prose and documents: readable evidence, never a citation source.
312
+ View.NOTES: Admits.NOTHING,
313
+ View.SESSIONS: Admits.NOTHING,
314
+ View.TRANSCRIPT: Admits.NOTHING,
315
+ View.SUMMARY: Admits.NOTHING,
316
+ View.SUMMARY_HISTORY: Admits.NOTHING,
317
+ View.OVERVIEW: Admits.NOTHING,
318
+ View.README: Admits.NOTHING,
319
+ View.CONTENT: Admits.NOTHING,
320
+ View.REPRODUCE: Admits.NOTHING,
321
+ View.EVENTS: Admits.NOTHING,
322
+ View.HANDOFF: Admits.NOTHING,
323
+ View.CODE: Admits.NOTHING,
324
+ View.DIFF: Admits.NOTHING,
325
+ View.VIEWS: Admits.NOTHING,
326
+ # Computed standings and effort: a reading of many records, not one.
327
+ View.BRANCHES: Admits.NOTHING,
328
+ # Who chose what, and when: a judgement about results, never a reading of one.
329
+ View.PICKS: Admits.NOTHING,
330
+ }
331
+
332
+
333
+ #: The pairs the generation surface serves; a test holds its dispatcher to exactly this, and any other
334
+ #: pair refuses, naming what is valid. Narrower than VIEW_MATRIX: DESIGN.md (GENERATION_VIEWS).
335
+ GENERATION_VIEWS: dict[ViewKind, frozenset[View]] = {
336
+ ViewKind.PROJECT: frozenset(
337
+ {View.CARD, View.NOTES, View.SESSIONS, View.PAPERS, View.ARTIFACTS, View.SUMMARY}
338
+ ),
339
+ ViewKind.EXPERIMENT: frozenset(
340
+ {View.CARD, View.NOTES, View.SESSIONS, View.LINEAGE, View.ARTIFACTS, View.SUMMARY}
341
+ ),
342
+ ViewKind.RUN: frozenset(
343
+ {
344
+ View.CARD,
345
+ View.NOTES,
346
+ View.SESSIONS,
347
+ View.LINEAGE,
348
+ View.METRICS,
349
+ View.ARTIFACTS,
350
+ View.TRIALS,
351
+ # No `summary`: 0220 removed a run's authored document (a column, never on the
352
+ # Overview page lane), so there is nothing to read.
353
+ }
354
+ ),
355
+ ViewKind.TRIAL: frozenset({View.CARD, View.TRAJECTORY}),
356
+ # No `notes`: an artifact carries a `description`, not a notes document.
357
+ ViewKind.ARTIFACT: frozenset({View.CARD, View.VERSIONS, View.CONTENT}),
358
+ ViewKind.SESSION: frozenset({View.CARD}),
359
+ ViewKind.PAPER: frozenset({View.CARD}),
360
+ }
@@ -0,0 +1,5 @@
1
+ """The product-analytics event catalog (events.json) and its checked loader."""
2
+
3
+ from .analytics import CATALOG_PATH, CatalogError, CatalogEvent, CatalogGroup, EventCatalog, Runtime
4
+
5
+ __all__ = ["CATALOG_PATH", "CatalogError", "CatalogEvent", "CatalogGroup", "EventCatalog", "Runtime"]
@@ -0,0 +1,116 @@
1
+ """EventCatalog: the one list of product-analytics events (events.json beside it), checked on load.
2
+
3
+ Codegen draws each runtime's `Event` enum from it; nothing reads the JSON at run time.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import re
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from ..compat import StrEnum
14
+
15
+ #: The catalog this package ships.
16
+ CATALOG_PATH = Path(__file__).with_name("events.json")
17
+
18
+ _KEY = re.compile(r"[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*")
19
+
20
+
21
+ class Runtime(StrEnum):
22
+ """Who sends an event. `plugin` is the hooks, which keep their own constants (no generated enum)."""
23
+
24
+ SERVER = "server"
25
+ AGENT = "agent"
26
+ PLUGIN = "plugin"
27
+ DASHBOARD = "dashboard"
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class CatalogEvent:
32
+ """One event: its enum key (UPPER_SNAKE), wire name, who sends it and a one-line why."""
33
+
34
+ key: str
35
+ name: str
36
+ runtimes: tuple[Runtime, ...]
37
+ doc: str = ""
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class CatalogGroup:
42
+ """A titled run of related events, drawn as a comment block in each generated enum."""
43
+
44
+ title: str
45
+ events: tuple[CatalogEvent, ...]
46
+ doc: str = ""
47
+
48
+
49
+ class CatalogError(ValueError):
50
+ """events.json breaks a catalog rule (named in the message)."""
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class EventCatalog:
55
+ about: str
56
+ groups: tuple[CatalogGroup, ...]
57
+
58
+ @classmethod
59
+ def load(cls, path: Path = CATALOG_PATH) -> EventCatalog:
60
+ """Read and check a catalog: unique keys and names, UPPER_SNAKE keys, known runtimes."""
61
+ data = json.loads(path.read_text(encoding="utf-8"))
62
+ groups = tuple(
63
+ CatalogGroup(
64
+ title=group["title"],
65
+ doc=group.get("doc", ""),
66
+ events=tuple(
67
+ CatalogEvent(
68
+ key=event["key"],
69
+ name=event["name"],
70
+ runtimes=tuple(Runtime(r) for r in event["runtimes"]),
71
+ doc=event.get("doc", ""),
72
+ )
73
+ for event in group["events"]
74
+ ),
75
+ )
76
+ for group in data["groups"]
77
+ )
78
+ catalog = cls(about=data["about"], groups=groups)
79
+ catalog.check()
80
+ return catalog
81
+
82
+ @property
83
+ def events(self) -> tuple[CatalogEvent, ...]:
84
+ return tuple(event for group in self.groups for event in group.events)
85
+
86
+ def check(self) -> None:
87
+ """Raise CatalogError on the first broken rule. A name is unique in the whole catalog; a key
88
+ only within a runtime (the server's `onboarding.completed` and the browser's
89
+ `onboarding_completed` are both ONBOARDING_COMPLETED in their own enum)."""
90
+ keys: set[tuple[Runtime, str]] = set()
91
+ names: set[str] = set()
92
+ for event in self.events:
93
+ if not _KEY.fullmatch(event.key):
94
+ raise CatalogError(f"{event.name}: key {event.key!r} is not UPPER_SNAKE")
95
+ if event.name in names:
96
+ raise CatalogError(f"name {event.name!r} is used twice")
97
+ if not event.runtimes or len(set(event.runtimes)) != len(event.runtimes):
98
+ raise CatalogError(f"{event.key}: runtimes must be a non-empty list without repeats")
99
+ for runtime in event.runtimes:
100
+ if (runtime, event.key) in keys:
101
+ raise CatalogError(f"{runtime} key {event.key} is used twice")
102
+ keys.add((runtime, event.key))
103
+ names.add(event.name)
104
+
105
+ def get_groups(self, runtime: Runtime) -> tuple[CatalogGroup, ...]:
106
+ """The groups with an event `runtime` sends, holding only those events, in catalog order."""
107
+ kept = []
108
+ for group in self.groups:
109
+ events = tuple(e for e in group.events if runtime in e.runtimes)
110
+ if events:
111
+ kept.append(CatalogGroup(title=group.title, events=events, doc=group.doc))
112
+ return tuple(kept)
113
+
114
+ def get_names(self, runtime: Runtime) -> list[str]:
115
+ """Every wire name `runtime` sends, in catalog order."""
116
+ return [event.name for group in self.get_groups(runtime) for event in group.events]