agentforge-framework 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.
- agentforge_framework/.claude-plugin/plugin.json +4 -0
- agentforge_framework/__init__.py +3 -0
- agentforge_framework/agents/__init__.py +92 -0
- agentforge_framework/agents/architect.py +146 -0
- agentforge_framework/agents/implementer.py +162 -0
- agentforge_framework/agents/orchestrator.py +588 -0
- agentforge_framework/agents/reviewer.py +335 -0
- agentforge_framework/agents/security.py +138 -0
- agentforge_framework/agents/tester.py +125 -0
- agentforge_framework/cli.py +461 -0
- agentforge_framework/context/__init__.py +1 -0
- agentforge_framework/context/extractors/__init__.py +76 -0
- agentforge_framework/context/extractors/base.py +47 -0
- agentforge_framework/context/extractors/python.py +65 -0
- agentforge_framework/context/extractors/sql.py +121 -0
- agentforge_framework/context/extractors/yaml.py +59 -0
- agentforge_framework/context/prompt.py +104 -0
- agentforge_framework/context/resolver.py +185 -0
- agentforge_framework/core/__init__.py +1 -0
- agentforge_framework/core/commands.py +170 -0
- agentforge_framework/core/config.py +90 -0
- agentforge_framework/core/contracts.py +875 -0
- agentforge_framework/core/gates.py +333 -0
- agentforge_framework/core/issues.py +697 -0
- agentforge_framework/core/plan_format.py +272 -0
- agentforge_framework/core/process.py +141 -0
- agentforge_framework/core/project.py +262 -0
- agentforge_framework/core/registry.py +455 -0
- agentforge_framework/core/repo.py +185 -0
- agentforge_framework/core/router.py +1 -0
- agentforge_framework/core/runtime.py +639 -0
- agentforge_framework/core/skills.py +255 -0
- agentforge_framework/core/workflow.py +215 -0
- agentforge_framework/plugins/__init__.py +35 -0
- agentforge_framework/plugins/databricks/__init__.py +86 -0
- agentforge_framework/plugins/pyspark/__init__.py +57 -0
- agentforge_framework/plugins/python/__init__.py +45 -0
- agentforge_framework/plugins/sql/__init__.py +377 -0
- agentforge_framework/providers/__init__.py +48 -0
- agentforge_framework/providers/base.py +248 -0
- agentforge_framework/providers/claude.py +159 -0
- agentforge_framework/providers/codex.py +139 -0
- agentforge_framework/skills/MANIFEST.yaml +157 -0
- agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework/skills/domain-modeling/ADR-FORMAT.md +47 -0
- agentforge_framework/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
- agentforge_framework/skills/domain-modeling/SKILL.md +74 -0
- agentforge_framework/skills/domain-modeling/agents/openai.yaml +3 -0
- agentforge_framework/skills/grill-with-docs/SKILL.md +76 -0
- agentforge_framework/skills/grilling/SKILL.md +28 -0
- agentforge_framework/skills/grilling/agents/openai.yaml +3 -0
- agentforge_framework/skills/to-spec/SKILL.md +75 -0
- agentforge_framework/skills/to-spec/agents/openai.yaml +5 -0
- agentforge_framework/skills/to-tickets/SKILL.md +105 -0
- agentforge_framework/skills/to-tickets/agents/openai.yaml +5 -0
- agentforge_framework/skills/unslop/SKILL.md +131 -0
- agentforge_framework/skills/unslop/evals/fixtures/silhouette/human_reference.json +66 -0
- agentforge_framework/skills/unslop/scripts/_lang.py +106 -0
- agentforge_framework/skills/unslop/scripts/banned_phrase_scan.py +784 -0
- agentforge_framework/skills/unslop/scripts/calibrate_pairs.py +580 -0
- agentforge_framework/skills/unslop/scripts/calibrate_score.py +273 -0
- agentforge_framework/skills/unslop/scripts/check_packs.py +80 -0
- agentforge_framework/skills/unslop/scripts/check_suggestions.py +225 -0
- agentforge_framework/skills/unslop/scripts/contribute.py +373 -0
- agentforge_framework/skills/unslop/scripts/diff_check.py +139 -0
- agentforge_framework/skills/unslop/scripts/extract_constraints.py +201 -0
- agentforge_framework/skills/unslop/scripts/harvest_classify.py +223 -0
- agentforge_framework/skills/unslop/scripts/harvest_samples.py +534 -0
- agentforge_framework/skills/unslop/scripts/readability_metrics.py +295 -0
- agentforge_framework/skills/unslop/scripts/refresh_status.py +154 -0
- agentforge_framework/skills/unslop/scripts/silhouette_scan.py +390 -0
- agentforge_framework/skills/unslop/scripts/structure_scan.py +322 -0
- agentforge_framework/skills/unslop/scripts/suggest.py +211 -0
- agentforge_framework/skills/unslop/scripts/validate_preservation.py +409 -0
- agentforge_framework/skills/unslop/scripts/voice_card.py +496 -0
- agentforge_framework/skills/unslop/scripts/voice_profile.py +194 -0
- agentforge_framework/skills/unslop/scripts/voice_score.py +271 -0
- agentforge_framework/skills/unslop/scripts/wiki_sync.py +479 -0
- agentforge_framework/skills/write-plainly/SKILL.md +94 -0
- agentforge_framework/workflows/bugfix.yaml +8 -0
- agentforge_framework/workflows/feature.yaml +16 -0
- agentforge_framework/workflows/review.yaml +10 -0
- agentforge_framework-0.2.0.dist-info/METADATA +321 -0
- agentforge_framework-0.2.0.dist-info/RECORD +89 -0
- agentforge_framework-0.2.0.dist-info/WHEEL +5 -0
- agentforge_framework-0.2.0.dist-info/entry_points.txt +3 -0
- agentforge_framework-0.2.0.dist-info/licenses/LICENSE +202 -0
- agentforge_framework-0.2.0.dist-info/licenses/src/agentforge_framework/skills/NOTICE +49 -0
- agentforge_framework-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""The Python Plugin: what a Python change is held to here.
|
|
2
|
+
|
|
3
|
+
Deliberately narrow. This ships one Fragment, aimed at the Roles that write and
|
|
4
|
+
check code, and says only things that change what an Agent produces. A
|
|
5
|
+
convention an Agent would have followed anyway is tokens spent on agreement.
|
|
6
|
+
|
|
7
|
+
No root markers: this Plugin answers for the blast radius alone. A repository
|
|
8
|
+
with a `pyproject.toml` and a Plan that touches only SQL is not doing Python
|
|
9
|
+
work, and holding that Run to Python conventions would be the first way this
|
|
10
|
+
mechanism starts costing more than it returns.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from ...core.contracts import Fragment, Plugin
|
|
16
|
+
|
|
17
|
+
#: Aimed at the three Roles that produce or judge code. The Security Role is
|
|
18
|
+
#: absent on purpose: it audits against production standards, and a style
|
|
19
|
+
#: convention in its prompt competes with that rather than supporting it.
|
|
20
|
+
_CONVENTIONS = """\
|
|
21
|
+
Follow these Python conventions unless the file you are editing plainly does otherwise:
|
|
22
|
+
|
|
23
|
+
- Match the module you are editing. Its import grouping, quote style, and naming
|
|
24
|
+
are the convention here, and a file that reads as two styles costs every
|
|
25
|
+
future reader more than either style saves.
|
|
26
|
+
- Type-annotate new public functions and dataclass fields. Leave existing
|
|
27
|
+
unannotated signatures alone unless the Plan names them.
|
|
28
|
+
- Raise a specific exception with a message naming what was wrong and what was
|
|
29
|
+
expected. A bare `raise Exception` or a swallowed `except: pass` is a defect,
|
|
30
|
+
not a shortcut.
|
|
31
|
+
- Prefer a standard-library answer to a new dependency. Adding one is a decision
|
|
32
|
+
the Plan has to have made.
|
|
33
|
+
- Tests assert on behaviour through the public surface, not on private helpers.
|
|
34
|
+
A test that breaks on a rename was testing the rename.\
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
PYTHON = Plugin(
|
|
38
|
+
name="python",
|
|
39
|
+
suffixes=(".py", ".pyi"),
|
|
40
|
+
fragments=(
|
|
41
|
+
Fragment(text=_CONVENTIONS, roles=("implementer", "tester", "reviewer")),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = ["PYTHON"]
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""The SQL Plugin: what a `.sql` file means when dbt is what builds it.
|
|
2
|
+
|
|
3
|
+
Deliberately two Extractors, one Gate kind, and no Fragment. The conventions a
|
|
4
|
+
SQL change is held to are a `sql` Fragment's job and nobody has written one
|
|
5
|
+
worth a Role's tokens yet; what this Plugin has that nothing else does is
|
|
6
|
+
knowledge of what a dbt model *depends on* and of what it means for a project to
|
|
7
|
+
still parse — one belongs in the pack and the other in a Gate, and neither
|
|
8
|
+
belongs in a prompt.
|
|
9
|
+
|
|
10
|
+
The built-in SQL extractor reads tables out of a statement. In a dbt project
|
|
11
|
+
that reading is not wrong so much as beside the point: a model says
|
|
12
|
+
`{{ ref('stg_orders') }}` and compiles to a table name this repository does not
|
|
13
|
+
contain, so a generic read finds either nothing or the wrong thing. What breaks
|
|
14
|
+
when the model changes is the models that `ref()` it, and that is what a Role
|
|
15
|
+
needs to be told.
|
|
16
|
+
|
|
17
|
+
The `dbt` Gate is the worked example of a validator. A Workflow in a dbt project
|
|
18
|
+
writes `gate: dbt` after the Step that edits models, and the Run holds there
|
|
19
|
+
until the project parses — the same YAML a Workflow writes for `tests`, and a
|
|
20
|
+
Workflow in a repository this Plugin does not answer for is refused at load
|
|
21
|
+
time, because nothing there would evaluate it.
|
|
22
|
+
|
|
23
|
+
Detection and reading are separate, which is why `suffixes` here is `.sql`
|
|
24
|
+
alone. A repository is a dbt project because of `dbt_project.yml`, and a Plan
|
|
25
|
+
touching a `.sql` file is doing SQL work wherever it is done. Neither of those
|
|
26
|
+
is a reason to activate on every `.yml` in every repository — but once this
|
|
27
|
+
Plugin *is* active, the schema files beside the models are worth reading as
|
|
28
|
+
what they are, so the YAML Extractor claims those suffixes and hands back
|
|
29
|
+
anything that is not dbt-shaped to the reader that already handles it.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
|
|
36
|
+
import yaml as pyyaml
|
|
37
|
+
|
|
38
|
+
from ...context.extractors import sql as builtin_sql
|
|
39
|
+
from ...context.extractors import yaml as builtin_yaml
|
|
40
|
+
from ...context.extractors.base import Extraction, ordered
|
|
41
|
+
from ...core.contracts import (
|
|
42
|
+
Command,
|
|
43
|
+
Extractor,
|
|
44
|
+
FileTemplate,
|
|
45
|
+
GateEntry,
|
|
46
|
+
GateVerdict,
|
|
47
|
+
Plugin,
|
|
48
|
+
Validator,
|
|
49
|
+
)
|
|
50
|
+
from ...core.gates import GateContext, command_tail
|
|
51
|
+
from ...core.process import MissingBinary
|
|
52
|
+
|
|
53
|
+
#: `ref('model')`, `ref("model")`, and the two-argument `ref('package', 'model')`
|
|
54
|
+
#: that a model in an installed package is reached by. The last argument is the
|
|
55
|
+
#: model either way, which is why the first is captured and discarded.
|
|
56
|
+
_REF = re.compile(
|
|
57
|
+
r"""\bref\s*\(\s*['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"]\s*)?\)""",
|
|
58
|
+
re.IGNORECASE,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
#: `source('name', 'table')`. Both halves matter and both are carried: the
|
|
62
|
+
#: source name alone would not say which table, and the table alone would not
|
|
63
|
+
#: say which source declared it.
|
|
64
|
+
_SOURCE = re.compile(
|
|
65
|
+
r"""\bsource\s*\(\s*['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]\s*\)""",
|
|
66
|
+
re.IGNORECASE,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
#: How many entries of a list in a schema file are read. The same bound the
|
|
70
|
+
#: built-in YAML extractor applies, and for the same reason: a `models:` list of
|
|
71
|
+
#: two hundred has the shape of a list of three.
|
|
72
|
+
MAX_ITEMS = builtin_yaml.MAX_ITEMS
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_model(text: str) -> Extraction:
|
|
76
|
+
"""A dbt model's dependencies as references, on top of the generic read.
|
|
77
|
+
|
|
78
|
+
Composed rather than replacing: a model is still SQL, and the columns the
|
|
79
|
+
built-in extractor finds are still where a change lands. What this adds is
|
|
80
|
+
the edges — `ref()` and `source()` targets, carried in front of the compiled
|
|
81
|
+
table names, because they are the names the repository actually contains and
|
|
82
|
+
so the ones a Role can go and read.
|
|
83
|
+
|
|
84
|
+
A file with no `ref()` and no `source()` in it extracts exactly what the
|
|
85
|
+
built-in extractor extracts. That is the ordinary case in a repository that
|
|
86
|
+
has one `.sql` file and no dbt, and it costs that repository nothing.
|
|
87
|
+
"""
|
|
88
|
+
dependencies: list[str] = []
|
|
89
|
+
|
|
90
|
+
for package, model in _REF.findall(text):
|
|
91
|
+
# Two-argument `ref` captures ('package', 'model'); one-argument
|
|
92
|
+
# captures ('model', ''). The model is the last non-empty group.
|
|
93
|
+
dependencies.append(f"ref:{model or package}")
|
|
94
|
+
|
|
95
|
+
for source, table in _SOURCE.findall(text):
|
|
96
|
+
dependencies.append(f"source:{source}.{table}")
|
|
97
|
+
|
|
98
|
+
generic = builtin_sql.extract(text)
|
|
99
|
+
return Extraction(
|
|
100
|
+
symbols=generic.symbols,
|
|
101
|
+
references=ordered([*dependencies, *generic.references]),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def extract_schema(text: str) -> Extraction:
|
|
106
|
+
"""A dbt schema file read as dbt, or as ordinary YAML when it is not one.
|
|
107
|
+
|
|
108
|
+
A `schema.yml` and a CI config are both YAML and are not both worth the same
|
|
109
|
+
reading. Read generically, a model's name, one of its column's names, and
|
|
110
|
+
the name of a test on that column are three keys at three depths and nothing
|
|
111
|
+
distinguishes them. Read as dbt they are three different kinds of thing:
|
|
112
|
+
|
|
113
|
+
- a model or a source table is a **symbol**, because it is the thing a Plan
|
|
114
|
+
sends somebody to change
|
|
115
|
+
- a column is a **symbol**, qualified by the model it belongs to, so that
|
|
116
|
+
two models with an `id` are two symbols rather than one
|
|
117
|
+
- a **test** is neither. It is carried as a reference, because a test is what
|
|
118
|
+
breaks when the column changes, which is the question `references` answers
|
|
119
|
+
|
|
120
|
+
Anything without a dbt shape falls through to the built-in YAML extractor.
|
|
121
|
+
That fall-through is what makes claiming `.yml` safe: this Plugin activates
|
|
122
|
+
on a `dbt_project.yml` at the root, and a repository that has one still has
|
|
123
|
+
ordinary YAML in it that nobody wants read as dbt.
|
|
124
|
+
"""
|
|
125
|
+
document = pyyaml.safe_load(text)
|
|
126
|
+
if not _is_dbt_schema(document):
|
|
127
|
+
return builtin_yaml.extract(text)
|
|
128
|
+
|
|
129
|
+
symbols: list[str] = []
|
|
130
|
+
references: list[str] = []
|
|
131
|
+
|
|
132
|
+
for key in ("models", "seeds", "snapshots"):
|
|
133
|
+
for node in _entries(document.get(key)):
|
|
134
|
+
name = _name(node)
|
|
135
|
+
if not name:
|
|
136
|
+
continue
|
|
137
|
+
symbols.append(name)
|
|
138
|
+
_read_columns(node, name, symbols, references)
|
|
139
|
+
|
|
140
|
+
for source in _entries(document.get("sources")):
|
|
141
|
+
source_name = _name(source)
|
|
142
|
+
if not source_name:
|
|
143
|
+
continue
|
|
144
|
+
for table in _entries(source.get("tables")):
|
|
145
|
+
table_name = _name(table)
|
|
146
|
+
if not table_name:
|
|
147
|
+
continue
|
|
148
|
+
# A source table is a symbol the same way a model is — it is a named
|
|
149
|
+
# thing in this file — and its `source:` reference is what a model
|
|
150
|
+
# reaching for it will have written.
|
|
151
|
+
qualified = f"{source_name}.{table_name}"
|
|
152
|
+
symbols.append(qualified)
|
|
153
|
+
references.append(f"source:{qualified}")
|
|
154
|
+
_read_columns(table, qualified, symbols, references)
|
|
155
|
+
|
|
156
|
+
return Extraction(symbols=ordered(symbols), references=ordered(references))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _is_dbt_schema(document) -> bool:
|
|
160
|
+
"""Whether this document is a dbt schema file rather than any other YAML.
|
|
161
|
+
|
|
162
|
+
A mapping carrying at least one of dbt's node lists. `version: 2` is not the
|
|
163
|
+
test: plenty of YAML declares a version, and a schema file that omits it is
|
|
164
|
+
still a schema file.
|
|
165
|
+
"""
|
|
166
|
+
return isinstance(document, dict) and any(
|
|
167
|
+
isinstance(document.get(key), list)
|
|
168
|
+
for key in ("models", "sources", "seeds", "snapshots")
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _read_columns(node, owner: str, symbols: list[str], references: list[str]) -> None:
|
|
173
|
+
"""A node's columns as `owner.column`, and its tests as references."""
|
|
174
|
+
for test in _tests(node):
|
|
175
|
+
references.append(f"test:{test} on {owner}")
|
|
176
|
+
|
|
177
|
+
for column in _entries(node.get("columns")):
|
|
178
|
+
name = _name(column)
|
|
179
|
+
if not name:
|
|
180
|
+
continue
|
|
181
|
+
symbols.append(f"{owner}.{name}")
|
|
182
|
+
for test in _tests(column):
|
|
183
|
+
references.append(f"test:{test} on {owner}.{name}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _tests(node) -> list[str]:
|
|
187
|
+
"""The tests declared on one node, under either spelling.
|
|
188
|
+
|
|
189
|
+
dbt renamed `tests:` to `data_tests:` and reads both, so this reads both. A
|
|
190
|
+
test is a mapping when it takes arguments (`relationships:` with a `to:`)
|
|
191
|
+
and a string when it does not, and the name is what matters either way.
|
|
192
|
+
"""
|
|
193
|
+
named: list[str] = []
|
|
194
|
+
for key in ("tests", "data_tests"):
|
|
195
|
+
for test in _entries(node.get(key)) if isinstance(node, dict) else ():
|
|
196
|
+
if isinstance(test, str):
|
|
197
|
+
named.append(test)
|
|
198
|
+
elif isinstance(test, dict) and test:
|
|
199
|
+
named.append(str(next(iter(test))))
|
|
200
|
+
return named
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _entries(value) -> list:
|
|
204
|
+
"""The first `MAX_ITEMS` of a list, or nothing at all if it is not one."""
|
|
205
|
+
return value[:MAX_ITEMS] if isinstance(value, list) else []
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _name(node) -> str:
|
|
209
|
+
"""A node's `name:`, or empty where it has none."""
|
|
210
|
+
if not isinstance(node, dict):
|
|
211
|
+
return ""
|
|
212
|
+
name = node.get("name")
|
|
213
|
+
return str(name).strip() if name is not None else ""
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
#: What the `dbt` Gate runs. `parse` rather than `build` or `test`: parsing
|
|
217
|
+
#: resolves every `ref()` and `source()` and compiles every model without
|
|
218
|
+
#: touching a warehouse, so the Gate holds a Run on a project that no longer
|
|
219
|
+
#: hangs together without needing a connection, a profile, or data.
|
|
220
|
+
DBT_PARSE = ("dbt", "parse")
|
|
221
|
+
|
|
222
|
+
#: The status dbt spends on "it ran and the project has a problem", which is a
|
|
223
|
+
#: report on the repository. It spends 2 on a usage error — no project here, a
|
|
224
|
+
#: flag it does not know — and that is a report on the invocation instead.
|
|
225
|
+
DBT_FAILED = 1
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def parses(context: GateContext) -> GateEntry:
|
|
229
|
+
"""The dbt Gate: parse the project, and read the exit status.
|
|
230
|
+
|
|
231
|
+
The same three answers the test-suite Gate gives, for the same reasons. It
|
|
232
|
+
parsed. It ran and found the project broken — a model referencing one that
|
|
233
|
+
was renamed, a macro that no longer resolves — which the next commit can
|
|
234
|
+
fix, so the Run suspends rather than halting. Or it never reached a verdict,
|
|
235
|
+
and a Gate with nothing to clear halts the Run rather than inviting a resume
|
|
236
|
+
that suspends again.
|
|
237
|
+
|
|
238
|
+
It names nobody in `invalidates`. This verdict comes from re-running dbt
|
|
239
|
+
against the working tree rather than from reading what a Role said about it,
|
|
240
|
+
so no Step's output has been judged and every Step behind this Gate stays
|
|
241
|
+
behind it (ADR-0008).
|
|
242
|
+
|
|
243
|
+
A Plugin's Gate degrades a Run and never ends it: dbt missing from the
|
|
244
|
+
machine, or refusing to start, is an errored verdict rather than an
|
|
245
|
+
exception, which is what the runtime has a way of reporting.
|
|
246
|
+
"""
|
|
247
|
+
if not context.runner.has_binary(DBT_PARSE[0]):
|
|
248
|
+
return _cannot_parse(f"{DBT_PARSE[0]!r} is not installed or not on PATH")
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
result = context.runner.run(DBT_PARSE, cwd=context.root)
|
|
252
|
+
except MissingBinary as exc:
|
|
253
|
+
return _cannot_parse(str(exc))
|
|
254
|
+
|
|
255
|
+
if result.ok:
|
|
256
|
+
return GateEntry(
|
|
257
|
+
kind="",
|
|
258
|
+
verdict=GateVerdict.CLEARED,
|
|
259
|
+
summary="`dbt parse` resolved the project.",
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if result.returncode == DBT_FAILED:
|
|
263
|
+
return GateEntry(
|
|
264
|
+
kind="",
|
|
265
|
+
verdict=GateVerdict.BLOCKED,
|
|
266
|
+
summary=(
|
|
267
|
+
"`dbt parse` failed, so the project does not resolve. The Run stops "
|
|
268
|
+
f"here rather than carrying that to Sign-off.\n\n{command_tail(result)}"
|
|
269
|
+
),
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return GateEntry(
|
|
273
|
+
kind="",
|
|
274
|
+
verdict=GateVerdict.ERRORED,
|
|
275
|
+
summary=(
|
|
276
|
+
f"`dbt parse` exited {result.returncode}, which is not a report on the "
|
|
277
|
+
"project: it did not run to a verdict, so there is nothing here for a "
|
|
278
|
+
f"later Run to clear.\n\n{command_tail(result)}"
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _cannot_parse(reason: str) -> GateEntry:
|
|
284
|
+
"""dbt never started, which says nothing about the project.
|
|
285
|
+
|
|
286
|
+
Errored rather than blocked, for the reason the test-suite Gate errors:
|
|
287
|
+
waiting clears nothing, and what has to change is the machine rather than
|
|
288
|
+
the repository.
|
|
289
|
+
"""
|
|
290
|
+
return GateEntry(
|
|
291
|
+
kind="",
|
|
292
|
+
verdict=GateVerdict.ERRORED,
|
|
293
|
+
summary=(
|
|
294
|
+
f"the dbt Gate cannot run `{' '.join(DBT_PARSE)}`: {reason}. Install dbt "
|
|
295
|
+
"where the Run executes, or drop the `dbt` Gate from this Workflow."
|
|
296
|
+
),
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
#: The model a scaffold writes. Deliberately a shape rather than a guess: a
|
|
301
|
+
#: staging CTE and a final select is what a reviewer expects to read, and every
|
|
302
|
+
#: decision that needs a person — what it selects from, what it filters, what it
|
|
303
|
+
#: is materialized as — is left where they will see it rather than filled in
|
|
304
|
+
#: with something plausible. `$$name` in a template is a literal dollar; `$name`
|
|
305
|
+
#: is the argument.
|
|
306
|
+
_MODEL_SQL = """with source as (
|
|
307
|
+
|
|
308
|
+
select * from {{ ref('stg_$name') }}
|
|
309
|
+
|
|
310
|
+
),
|
|
311
|
+
|
|
312
|
+
renamed as (
|
|
313
|
+
|
|
314
|
+
select
|
|
315
|
+
-- Name the columns this model exposes. `select *` here is how a
|
|
316
|
+
-- downstream break becomes a surprise.
|
|
317
|
+
*
|
|
318
|
+
|
|
319
|
+
from source
|
|
320
|
+
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
select * from renamed
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
#: The schema entry beside it. A model with no description and no test is the
|
|
327
|
+
#: thing `dbt parse` is happy with and a reviewer is not, so the scaffold writes
|
|
328
|
+
#: the places both belong and fills in neither.
|
|
329
|
+
_MODEL_YML = """version: 2
|
|
330
|
+
|
|
331
|
+
models:
|
|
332
|
+
- name: $name
|
|
333
|
+
description: ""
|
|
334
|
+
columns:
|
|
335
|
+
- name: id
|
|
336
|
+
description: ""
|
|
337
|
+
data_tests:
|
|
338
|
+
- unique
|
|
339
|
+
- not_null
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
#: The one chore this Plugin knows: two files, in the places dbt looks for them,
|
|
343
|
+
#: with no inference anywhere. Writing them by asking a Role at `standard` tier
|
|
344
|
+
#: is the most expensive way to produce a file whose shape was never in question.
|
|
345
|
+
SCAFFOLD_MODEL = Command(
|
|
346
|
+
name="scaffold-dbt-model",
|
|
347
|
+
summary="Write a dbt model and the schema entry beside it.",
|
|
348
|
+
arguments=("name",),
|
|
349
|
+
templates=(
|
|
350
|
+
FileTemplate(path="models/$name.sql", text=_MODEL_SQL),
|
|
351
|
+
FileTemplate(path="models/$name.yml", text=_MODEL_YML),
|
|
352
|
+
),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
SQL = Plugin(
|
|
357
|
+
name="sql",
|
|
358
|
+
suffixes=(".sql",),
|
|
359
|
+
root_markers=("dbt_project.yml", "dbt_project.yaml"),
|
|
360
|
+
extractors=(
|
|
361
|
+
Extractor(suffixes=(".sql",), read=extract_model),
|
|
362
|
+
Extractor(suffixes=(".yml", ".yaml"), read=extract_schema),
|
|
363
|
+
),
|
|
364
|
+
validators=(Validator(kind="dbt", check=parses),),
|
|
365
|
+
commands=(SCAFFOLD_MODEL,),
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
__all__ = [
|
|
369
|
+
"DBT_FAILED",
|
|
370
|
+
"DBT_PARSE",
|
|
371
|
+
"MAX_ITEMS",
|
|
372
|
+
"SCAFFOLD_MODEL",
|
|
373
|
+
"SQL",
|
|
374
|
+
"extract_model",
|
|
375
|
+
"extract_schema",
|
|
376
|
+
"parses",
|
|
377
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Provider interfaces and integrations.
|
|
2
|
+
|
|
3
|
+
Selecting a Provider is the only place a user names a coding-agent CLI.
|
|
4
|
+
Everything downstream of `get_provider` speaks in Model Tiers (ADR-0004).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from ..core.config import Config
|
|
10
|
+
from ..core.process import CommandRunner
|
|
11
|
+
from .base import CliProvider, Provider, ProviderError, ProviderOutput
|
|
12
|
+
from .claude import ClaudeProvider
|
|
13
|
+
from .codex import CodexProvider
|
|
14
|
+
|
|
15
|
+
PROVIDERS: dict[str, type[CliProvider]] = {
|
|
16
|
+
ClaudeProvider.name: ClaudeProvider,
|
|
17
|
+
CodexProvider.name: CodexProvider,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
DEFAULT_PROVIDER = ClaudeProvider.name
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_provider(
|
|
24
|
+
name: str,
|
|
25
|
+
runner: CommandRunner,
|
|
26
|
+
allow_commands: bool = False,
|
|
27
|
+
config: Config | None = None,
|
|
28
|
+
) -> Provider:
|
|
29
|
+
"""Build an adapter. `allow_commands` is ADR-0007's gate, closed by default."""
|
|
30
|
+
try:
|
|
31
|
+
provider = PROVIDERS[name]
|
|
32
|
+
except KeyError as exc:
|
|
33
|
+
known = ", ".join(sorted(PROVIDERS))
|
|
34
|
+
raise ProviderError(f"unknown provider {name!r}; available: {known}") from exc
|
|
35
|
+
return provider(runner, allow_commands=allow_commands, config=config)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"DEFAULT_PROVIDER",
|
|
40
|
+
"PROVIDERS",
|
|
41
|
+
"ClaudeProvider",
|
|
42
|
+
"CliProvider",
|
|
43
|
+
"CodexProvider",
|
|
44
|
+
"Provider",
|
|
45
|
+
"ProviderError",
|
|
46
|
+
"ProviderOutput",
|
|
47
|
+
"get_provider",
|
|
48
|
+
]
|