atif-sql 0.2.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.
- atif_analytics/__init__.py +9 -0
- atif_analytics/application/__init__.py +3 -0
- atif_analytics/application/analyze.py +158 -0
- atif_analytics/application/prompts.py +750 -0
- atif_analytics/application/use_cases/__init__.py +3 -0
- atif_analytics/application/use_cases/_shared.py +264 -0
- atif_analytics/application/use_cases/classify.py +430 -0
- atif_analytics/application/use_cases/conflicts.py +448 -0
- atif_analytics/application/use_cases/friction.py +634 -0
- atif_analytics/application/use_cases/perceived.py +451 -0
- atif_analytics/domain/__init__.py +3 -0
- atif_analytics/domain/authorship.py +166 -0
- atif_analytics/domain/config.py +30 -0
- atif_analytics/domain/costs.py +50 -0
- atif_analytics/domain/friction.py +117 -0
- atif_analytics/domain/layout.py +94 -0
- atif_analytics/domain/models.py +385 -0
- atif_analytics/domain/ports.py +100 -0
- atif_analytics/domain/transcript.py +359 -0
- atif_analytics/infrastructure/__init__.py +3 -0
- atif_analytics/infrastructure/corpus_reader.py +546 -0
- atif_analytics/infrastructure/parquet_cache.py +186 -0
- atif_analytics/infrastructure/settings.py +114 -0
- atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
- atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
- atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
- atif_analytics/py.typed +0 -0
- atif_cli/__init__.py +3 -0
- atif_cli/__main__.py +8 -0
- atif_cli/app.py +2459 -0
- atif_cli/converter_adapter.py +151 -0
- atif_cli/corpus.py +409 -0
- atif_cli/cron.py +311 -0
- atif_cli/duck_errors.py +97 -0
- atif_cli/embed_lake.py +101 -0
- atif_cli/errors.py +89 -0
- atif_cli/lake.py +373 -0
- atif_cli/lake_sessions.py +270 -0
- atif_cli/output.py +297 -0
- atif_cli/py.typed +0 -0
- atif_converter/__init__.py +3 -0
- atif_converter/application/__init__.py +3 -0
- atif_converter/application/convert_and_audit.py +294 -0
- atif_converter/application/convert_codex.py +210 -0
- atif_converter/domain/__init__.py +9 -0
- atif_converter/domain/agents.py +40 -0
- atif_converter/domain/atif/__init__.py +55 -0
- atif_converter/domain/atif/agent.py +37 -0
- atif_converter/domain/atif/content.py +161 -0
- atif_converter/domain/atif/final_metrics.py +45 -0
- atif_converter/domain/atif/metrics.py +49 -0
- atif_converter/domain/atif/observation.py +21 -0
- atif_converter/domain/atif/observation_result.py +47 -0
- atif_converter/domain/atif/step.py +144 -0
- atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
- atif_converter/domain/atif/tool_call.py +36 -0
- atif_converter/domain/atif/trajectory.py +211 -0
- atif_converter/domain/atif/trajectory_validator.py +299 -0
- atif_converter/domain/blobs.py +510 -0
- atif_converter/domain/claude_code_conversion.py +1077 -0
- atif_converter/domain/codex_conversion.py +1036 -0
- atif_converter/domain/codex_edges.py +192 -0
- atif_converter/domain/codex_enrichment.py +419 -0
- atif_converter/domain/codex_fidelity.py +124 -0
- atif_converter/domain/codex_nested_calls.py +165 -0
- atif_converter/domain/edges.py +113 -0
- atif_converter/domain/enrichment.py +412 -0
- atif_converter/domain/errors.py +60 -0
- atif_converter/domain/fidelity.py +171 -0
- atif_converter/domain/model_prices.json +8538 -0
- atif_converter/domain/pricing.py +790 -0
- atif_converter/domain/result_signals.py +774 -0
- atif_converter/domain/schema_version.py +90 -0
- atif_converter/domain/session_events.py +408 -0
- atif_converter/infrastructure/__init__.py +3 -0
- atif_converter/infrastructure/census.py +89 -0
- atif_converter/infrastructure/claude_code_converter.py +127 -0
- atif_converter/infrastructure/codex_adapter.py +132 -0
- atif_converter/infrastructure/codex_census.py +100 -0
- atif_converter/infrastructure/codex_converter.py +82 -0
- atif_converter/infrastructure/harbor_adapter.py +187 -0
- atif_converter/infrastructure/raw_records.py +426 -0
- atif_converter/infrastructure/source_archive.py +154 -0
- atif_converter/py.typed +0 -0
- atif_corpus/__init__.py +13 -0
- atif_corpus/application/__init__.py +7 -0
- atif_corpus/application/materialize.py +1826 -0
- atif_corpus/domain/__init__.py +9 -0
- atif_corpus/domain/agents.py +33 -0
- atif_corpus/domain/generation.py +34 -0
- atif_corpus/domain/layout.py +168 -0
- atif_corpus/domain/ports.py +175 -0
- atif_corpus/domain/session_id.py +68 -0
- atif_corpus/domain/sessions.py +228 -0
- atif_corpus/domain/slug.py +72 -0
- atif_corpus/domain/source_layout.py +143 -0
- atif_corpus/domain/watermark.py +76 -0
- atif_corpus/infrastructure/__init__.py +7 -0
- atif_corpus/infrastructure/atomic.py +266 -0
- atif_corpus/infrastructure/compress_artifacts.py +161 -0
- atif_corpus/infrastructure/fake_converter.py +174 -0
- atif_corpus/infrastructure/scanner.py +316 -0
- atif_corpus/infrastructure/settings.py +130 -0
- atif_corpus/infrastructure/source_archive.py +202 -0
- atif_corpus/py.typed +0 -0
- atif_duck/__init__.py +43 -0
- atif_duck/domain/__init__.py +32 -0
- atif_duck/domain/artifacts.py +66 -0
- atif_duck/domain/authorship.py +165 -0
- atif_duck/domain/catalog.py +626 -0
- atif_duck/domain/columnar.py +149 -0
- atif_duck/domain/embedding_guard.py +88 -0
- atif_duck/domain/examples.py +265 -0
- atif_duck/domain/lake.py +427 -0
- atif_duck/domain/lake_sessions.py +97 -0
- atif_duck/domain/raw_readers.py +140 -0
- atif_duck/domain/session_id.py +69 -0
- atif_duck/domain/sql_literal.py +50 -0
- atif_duck/infrastructure/__init__.py +34 -0
- atif_duck/infrastructure/analytics.py +469 -0
- atif_duck/infrastructure/authorship.py +139 -0
- atif_duck/infrastructure/columnar.py +641 -0
- atif_duck/infrastructure/lake.py +1383 -0
- atif_duck/infrastructure/lake_sessions.py +194 -0
- atif_duck/infrastructure/lake_settings.py +63 -0
- atif_duck/infrastructure/lake_steps.py +225 -0
- atif_duck/infrastructure/projections.py +221 -0
- atif_duck/infrastructure/registry.py +2194 -0
- atif_duck/infrastructure/stored_artifacts.py +115 -0
- atif_duck/py.typed +0 -0
- atif_embed/__init__.py +12 -0
- atif_embed/application/__init__.py +3 -0
- atif_embed/application/embed.py +293 -0
- atif_embed/application/prune.py +80 -0
- atif_embed/domain/__init__.py +3 -0
- atif_embed/domain/discovery.py +70 -0
- atif_embed/domain/embedding_guard.py +83 -0
- atif_embed/domain/errors.py +94 -0
- atif_embed/domain/ports.py +223 -0
- atif_embed/domain/sql_literal.py +49 -0
- atif_embed/domain/text_stamp.py +71 -0
- atif_embed/infrastructure/__init__.py +3 -0
- atif_embed/infrastructure/cohere_bedrock.py +433 -0
- atif_embed/infrastructure/corpus_text_rows.py +273 -0
- atif_embed/infrastructure/gemma_local.py +355 -0
- atif_embed/infrastructure/lake_text_rows.py +203 -0
- atif_embed/infrastructure/lance_store.py +440 -0
- atif_embed/infrastructure/providers.py +30 -0
- atif_embed/infrastructure/settings.py +154 -0
- atif_embed/py.typed +0 -0
- atif_models/__init__.py +14 -0
- atif_models/domain/__init__.py +9 -0
- atif_models/domain/ports.py +143 -0
- atif_models/domain/registry.py +151 -0
- atif_models/domain/schema.py +109 -0
- atif_models/infrastructure/__init__.py +8 -0
- atif_models/infrastructure/openai_bedrock.py +314 -0
- atif_models/infrastructure/settings.py +85 -0
- atif_models/py.typed +0 -0
- atif_sql-0.2.0.dist-info/METADATA +272 -0
- atif_sql-0.2.0.dist-info/RECORD +164 -0
- atif_sql-0.2.0.dist-info/WHEEL +4 -0
- atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
- atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
atif_duck/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""atif-duck: DuckDB views over the materialized ATIF corpus.
|
|
4
|
+
|
|
5
|
+
Layered: ``infrastructure`` (DuckDB registry) > ``domain`` (static
|
|
6
|
+
view/macro catalog). ``register(con, corpus_root)`` wires a connection to a
|
|
7
|
+
CONTRACT-shaped corpus (``<root>/sessions/<id>/{trajectory.json,
|
|
8
|
+
edges.jsonl, loss_report.json, meta.json}``) and exposes the core query
|
|
9
|
+
surface: the views in :data:`VIEW_NAMES` (sessions / steps / messages /
|
|
10
|
+
tool_calls / user_steps / human_turns / session_outcomes / ...) plus the
|
|
11
|
+
macros in :data:`MACRO_NAMES`. The v2
|
|
12
|
+
analytics surface (``ANALYTICS_VIEW_NAMES``,
|
|
13
|
+
``ANALYTICS_MACRO_SIGNATURES``) registers separately and needs
|
|
14
|
+
``atif-sql analyze`` to have run.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from atif_duck.domain.catalog import (
|
|
18
|
+
DEFAULT_PRICING,
|
|
19
|
+
MACRO_NAMES,
|
|
20
|
+
MACRO_SIGNATURES,
|
|
21
|
+
VIEW_NAMES,
|
|
22
|
+
VIEW_SCHEMA,
|
|
23
|
+
)
|
|
24
|
+
from atif_duck.infrastructure.registry import (
|
|
25
|
+
RawSources,
|
|
26
|
+
register,
|
|
27
|
+
register_macros,
|
|
28
|
+
register_raw,
|
|
29
|
+
register_views,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"DEFAULT_PRICING",
|
|
34
|
+
"MACRO_NAMES",
|
|
35
|
+
"MACRO_SIGNATURES",
|
|
36
|
+
"VIEW_NAMES",
|
|
37
|
+
"VIEW_SCHEMA",
|
|
38
|
+
"RawSources",
|
|
39
|
+
"register",
|
|
40
|
+
"register_macros",
|
|
41
|
+
"register_raw",
|
|
42
|
+
"register_views",
|
|
43
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Domain layer: the static view/macro catalog for atif-duck.
|
|
4
|
+
|
|
5
|
+
Pure value objects — no duckdb import, no I/O. The catalog is the
|
|
6
|
+
agent-facing contract (`atif-sql schema` answers from here in <50ms with
|
|
7
|
+
zero DuckDB bind cost); drift against the actual DDL is caught by the
|
|
8
|
+
tests in ``tests/test_duck_views.py``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from atif_duck.domain.catalog import (
|
|
12
|
+
DEFAULT_PRICING,
|
|
13
|
+
DESCRIPTIONS,
|
|
14
|
+
MACRO_NAMES,
|
|
15
|
+
MACRO_SIGNATURES,
|
|
16
|
+
TABLE_MACRO_NAMES,
|
|
17
|
+
VIEW_NAMES,
|
|
18
|
+
VIEW_SCHEMA,
|
|
19
|
+
)
|
|
20
|
+
from atif_duck.domain.examples import ExampleQuery, build_examples
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"DEFAULT_PRICING",
|
|
24
|
+
"DESCRIPTIONS",
|
|
25
|
+
"MACRO_NAMES",
|
|
26
|
+
"MACRO_SIGNATURES",
|
|
27
|
+
"TABLE_MACRO_NAMES",
|
|
28
|
+
"VIEW_NAMES",
|
|
29
|
+
"VIEW_SCHEMA",
|
|
30
|
+
"ExampleQuery",
|
|
31
|
+
"build_examples",
|
|
32
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""How the per-session JSON artifacts are stored: their names and their compression.
|
|
4
|
+
|
|
5
|
+
Three of the artifacts a session directory holds are the bulk of a corpus's
|
|
6
|
+
bytes: ``trajectory.json`` (the ATIF document), ``edges.jsonl`` and
|
|
7
|
+
``session_events.jsonl``. materialize stores each of them zstd-compressed,
|
|
8
|
+
as ``<name>.zst``, and a corpus written before that keeps the plain file until
|
|
9
|
+
``atif-sql corpus slim`` compresses it. Either spelling holds the same bytes
|
|
10
|
+
once decompressed, so every reader resolves the stored file per session
|
|
11
|
+
(:func:`stored_names`, the compressed one first) and nothing else about it
|
|
12
|
+
changes. ``meta.json`` and ``loss_report.json`` stay plain: they're small, and
|
|
13
|
+
``meta.json`` is what the corpus writer and every reader check first.
|
|
14
|
+
|
|
15
|
+
A path a view reports (``sessions.trajectory_path``) names the logical
|
|
16
|
+
artifact, ``<session>/trajectory.json``, whichever spelling is on disk, so a
|
|
17
|
+
row reads the same before and after a corpus is slimmed.
|
|
18
|
+
|
|
19
|
+
atif-corpus writes these names and carries the twin constants; the cross-package
|
|
20
|
+
pin lives in atif-cli's tests, the one package that may import both.
|
|
21
|
+
|
|
22
|
+
Pure constants: no filesystem access.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
#: The ATIF document.
|
|
28
|
+
TRAJECTORY_JSON: str = "trajectory.json"
|
|
29
|
+
#: One line per raw transcript record.
|
|
30
|
+
EDGES_JSONL: str = "edges.jsonl"
|
|
31
|
+
#: One line per kept non-message record.
|
|
32
|
+
SESSION_EVENTS_JSONL: str = "session_events.jsonl"
|
|
33
|
+
#: The completion marker every reader gates on (always plain).
|
|
34
|
+
META_JSON: str = "meta.json"
|
|
35
|
+
#: The fidelity report (always plain).
|
|
36
|
+
LOSS_REPORT_JSON: str = "loss_report.json"
|
|
37
|
+
|
|
38
|
+
#: The suffix a compressed artifact carries after its logical name.
|
|
39
|
+
COMPRESSED_SUFFIX: str = ".zst"
|
|
40
|
+
|
|
41
|
+
#: The artifacts materialize stores compressed.
|
|
42
|
+
COMPRESSED_ARTIFACTS: tuple[str, ...] = (TRAJECTORY_JSON, EDGES_JSONL, SESSION_EVENTS_JSONL)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def stored_names(name: str) -> tuple[str, ...]:
|
|
46
|
+
"""The file names ``name`` may be stored under, in the order a reader tries them.
|
|
47
|
+
|
|
48
|
+
The compressed spelling comes first: a session caught between ``corpus
|
|
49
|
+
slim`` writing ``<name>.zst`` and removing ``<name>`` holds both, with the
|
|
50
|
+
same content, and either answer is right.
|
|
51
|
+
"""
|
|
52
|
+
if name in COMPRESSED_ARTIFACTS:
|
|
53
|
+
return (f"{name}{COMPRESSED_SUFFIX}", name)
|
|
54
|
+
return (name,)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"COMPRESSED_ARTIFACTS",
|
|
59
|
+
"COMPRESSED_SUFFIX",
|
|
60
|
+
"EDGES_JSONL",
|
|
61
|
+
"LOSS_REPORT_JSON",
|
|
62
|
+
"META_JSON",
|
|
63
|
+
"SESSION_EVENTS_JSONL",
|
|
64
|
+
"TRAJECTORY_JSON",
|
|
65
|
+
"stored_names",
|
|
66
|
+
]
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
"""Who wrote a user-role step: the ``author`` rule table and its SQL form.
|
|
4
|
+
|
|
5
|
+
Claude Code and Codex deliver a lot of machine-written text in the USER role:
|
|
6
|
+
Stop hook feedback, ``<task-notification>`` blocks, retry and continuation
|
|
7
|
+
nudges, skill bodies, slash-command wrappers, image metadata from a screenshot
|
|
8
|
+
the agent read, and the prompts of automated review sessions. Counted as
|
|
9
|
+
user turns they drown the human ones (on a busy agent corpus about half of
|
|
10
|
+
the main-chain user steps were machine-written), so every analytics reader
|
|
11
|
+
classifies a user step first and treats only ``human`` as the user
|
|
12
|
+
speaking.
|
|
13
|
+
|
|
14
|
+
The classification is a PREFIX match over the message with leading
|
|
15
|
+
whitespace stripped, first rule wins, in :data:`AUTHOR_PREFIX_RULES` order.
|
|
16
|
+
Prefixes only, so the SQL macro (``starts_with``) and the Python twin
|
|
17
|
+
(``str.startswith``) cannot disagree on a regex dialect. An empty message is
|
|
18
|
+
``harness``: nothing a human typed. One rule sits outside the text: a step the
|
|
19
|
+
converter flagged ``is_compact_summary`` is ``harness`` whatever it says
|
|
20
|
+
(``user_steps.author`` and the Python twin both apply it; the bare
|
|
21
|
+
``step_author`` macro sees only source and text).
|
|
22
|
+
|
|
23
|
+
This table is the ONE definition. atif-analytics may not import atif-duck
|
|
24
|
+
(independence contract), so ``atif_analytics.domain.authorship`` carries a
|
|
25
|
+
twin of the two tuples below, and ``tests/test_authorship_twin_pin.py`` reads
|
|
26
|
+
that twin as source text and fails on any difference (the ``session_id``
|
|
27
|
+
precedent).
|
|
28
|
+
|
|
29
|
+
Pure domain module: stdlib only, no duckdb.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from atif_duck.domain.sql_literal import SqlFragment, sql_literal
|
|
35
|
+
|
|
36
|
+
#: The author labels, in the order the docs list them.
|
|
37
|
+
AUTHOR_VALUES: tuple[str, ...] = (
|
|
38
|
+
"human",
|
|
39
|
+
"stop_hook",
|
|
40
|
+
"task_notification",
|
|
41
|
+
"harness",
|
|
42
|
+
"audit_prompt",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
#: Characters stripped from the left of a message before matching.
|
|
46
|
+
AUTHOR_STRIP_CHARS: str = " \t\r\n"
|
|
47
|
+
|
|
48
|
+
#: ``(author, prefix)``, first match wins. Every prefix was read off real
|
|
49
|
+
#: Claude Code and Codex corpora in September 2026 as a most-common opening of
|
|
50
|
+
#: user-role steps; the comment beside each says what writes it.
|
|
51
|
+
AUTHOR_PREFIX_RULES: tuple[tuple[str, str], ...] = (
|
|
52
|
+
# An automated review session: a hook that audits another agent's turn
|
|
53
|
+
# opens a fresh session whose whole content is its prompt and a verdict.
|
|
54
|
+
("audit_prompt", "You are auditing an agent turn"),
|
|
55
|
+
# Claude Code Stop / SubagentStop hooks that blocked the agent's stop.
|
|
56
|
+
("stop_hook", "Stop hook feedback"),
|
|
57
|
+
("stop_hook", "SubagentStop hook feedback"),
|
|
58
|
+
# Background task completion notices.
|
|
59
|
+
("task_notification", "<task-notification>"),
|
|
60
|
+
("task_notification", "[SYSTEM NOTIFICATION"),
|
|
61
|
+
# Claude Code bookkeeping: interrupts, continuation and retry nudges,
|
|
62
|
+
# compaction summaries, skill bodies, image metadata for a screenshot the
|
|
63
|
+
# agent read, workflow-computed tasks, reminders and hook output.
|
|
64
|
+
("harness", "[Request interrupted"),
|
|
65
|
+
("harness", "Continue from where you left off"),
|
|
66
|
+
("harness", "Your previous attempt hit a transient"),
|
|
67
|
+
("harness", "This session is being continued from a previous conversation"),
|
|
68
|
+
("harness", "Base directory for this skill:"),
|
|
69
|
+
("harness", "[Image: original "),
|
|
70
|
+
("harness", '{"type": "image"'),
|
|
71
|
+
("harness", "[Workflow harness"),
|
|
72
|
+
("harness", "[structured-output-enforce]"),
|
|
73
|
+
("harness", "<system-reminder>"),
|
|
74
|
+
("harness", "<user-prompt-submit-hook>"),
|
|
75
|
+
("harness", "UserPromptSubmit operation blocked by hook"),
|
|
76
|
+
("harness", "<fork-boilerplate>"),
|
|
77
|
+
("harness", "Another Claude session sent a message"),
|
|
78
|
+
("harness", "The coordinator sent a message"),
|
|
79
|
+
# Slash-command and local-shell wrappers.
|
|
80
|
+
("harness", "<command-name>"),
|
|
81
|
+
("harness", "<command-message>"),
|
|
82
|
+
("harness", "<command-args>"),
|
|
83
|
+
("harness", "<local-command-stdout>"),
|
|
84
|
+
("harness", "<local-command-stderr>"),
|
|
85
|
+
("harness", "<local-command-caveat>"),
|
|
86
|
+
("harness", "Caveat: The messages below were generated"),
|
|
87
|
+
("harness", "<bash-input>"),
|
|
88
|
+
("harness", "<bash-stdout>"),
|
|
89
|
+
("harness", "<bash-stderr>"),
|
|
90
|
+
# Codex CLI context injections.
|
|
91
|
+
("harness", "<environment_context>"),
|
|
92
|
+
("harness", "<codex_internal_context"),
|
|
93
|
+
("harness", "<user_instructions>"),
|
|
94
|
+
("harness", "# AGENTS.md instructions"),
|
|
95
|
+
("harness", "<turn_aborted>"),
|
|
96
|
+
("harness", "The following is the Codex agent history"),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
#: Prefixes that mark an interrupted turn (``session_outcomes.interrupts``).
|
|
100
|
+
#: Both are also ``harness`` rules above, so an interrupt never counts as a
|
|
101
|
+
#: human turn.
|
|
102
|
+
INTERRUPT_PREFIXES: tuple[str, ...] = ("[Request interrupted", "<turn_aborted>")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
#: ``ltrim`` argument naming :data:`AUTHOR_STRIP_CHARS` by code point (space,
|
|
106
|
+
#: tab, CR, LF); ``test_authorship.py`` pins the two against each other.
|
|
107
|
+
_STRIP_CHARS_SQL = "chr(32) || chr(9) || chr(13) || chr(10)"
|
|
108
|
+
|
|
109
|
+
#: The stripped message inside the ``step_author(src, msg)`` macro body.
|
|
110
|
+
_MACRO_MSG_STRIPPED = f"ltrim(msg, {_STRIP_CHARS_SQL})"
|
|
111
|
+
|
|
112
|
+
#: The stripped ``message`` column, for predicates over ``steps`` rows.
|
|
113
|
+
_COLUMN_MSG_STRIPPED = f"ltrim(message, {_STRIP_CHARS_SQL})"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _author_arms() -> SqlFragment:
|
|
117
|
+
"""One ``WHEN starts_with(...) THEN '<author>'`` arm per rule, in order."""
|
|
118
|
+
return SqlFragment(
|
|
119
|
+
"\n".join(
|
|
120
|
+
f" WHEN starts_with({_MACRO_MSG_STRIPPED}, {sql_literal(prefix)}) "
|
|
121
|
+
f"THEN {sql_literal(author)}"
|
|
122
|
+
for author, prefix in AUTHOR_PREFIX_RULES
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _step_author_case() -> SqlFragment:
|
|
128
|
+
"""The ``CASE`` expression behind the ``step_author(src, msg)`` macro.
|
|
129
|
+
|
|
130
|
+
``NULL`` for a step whose source is not ``user``; otherwise ``harness`` for
|
|
131
|
+
an empty message, the first matching rule's author, and ``human`` when
|
|
132
|
+
nothing matches.
|
|
133
|
+
"""
|
|
134
|
+
return SqlFragment(
|
|
135
|
+
"CASE\n WHEN src IS DISTINCT FROM 'user' THEN NULL\n"
|
|
136
|
+
f" WHEN msg IS NULL OR {_MACRO_MSG_STRIPPED} = '' THEN 'harness'\n"
|
|
137
|
+
f"{_author_arms()}\n ELSE 'human'\nEND"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _interrupt_predicate() -> SqlFragment:
|
|
142
|
+
"""``starts_with`` over :data:`INTERRUPT_PREFIXES` for the ``message`` column."""
|
|
143
|
+
return SqlFragment(
|
|
144
|
+
"("
|
|
145
|
+
+ " OR ".join(
|
|
146
|
+
f"starts_with({_COLUMN_MSG_STRIPPED}, {sql_literal(prefix)})"
|
|
147
|
+
for prefix in INTERRUPT_PREFIXES
|
|
148
|
+
)
|
|
149
|
+
+ ")"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#: The macro body and the interrupt test, rendered once from the tables above.
|
|
154
|
+
STEP_AUTHOR_CASE_SQL: SqlFragment = _step_author_case()
|
|
155
|
+
INTERRUPT_PREDICATE_SQL: SqlFragment = _interrupt_predicate()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
__all__ = [
|
|
159
|
+
"AUTHOR_PREFIX_RULES",
|
|
160
|
+
"AUTHOR_STRIP_CHARS",
|
|
161
|
+
"AUTHOR_VALUES",
|
|
162
|
+
"INTERRUPT_PREDICATE_SQL",
|
|
163
|
+
"INTERRUPT_PREFIXES",
|
|
164
|
+
"STEP_AUTHOR_CASE_SQL",
|
|
165
|
+
]
|