ultra-builder-pro-cli 0.25.1 → 0.26.1
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.
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/contexts/TEMPLATE.md +94 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +107 -1
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +328 -0
- package/bin/delegate.cjs +463 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +125 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +77 -87
- package/docs/PHILOSOPHY.md +208 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +65 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +140 -155
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +97 -189
- package/hooks/README.md +19 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +201 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +115 -50
- package/skills/ultra-change/references/change-contract.md +68 -3
- package/skills/ultra-delegate/SKILL.md +89 -0
- package/skills/ultra-delegate/references/delegation-contract.md +59 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +98 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +115 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +118 -0
- package/skills/ultra-grilling/references/reframing.md +72 -0
- package/skills/ultra-init/SKILL.md +102 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +101 -68
- package/skills/ultra-research/SKILL.md +68 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +84 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +75 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Immutable Worker Packet
|
|
2
|
+
|
|
3
|
+
Create `.ultra/reviews/<session>/WORKER-PACKET.json` once, before starting any lens.
|
|
4
|
+
Serialize it as UTF-8 JSON with a trailing newline, compute SHA-256 over those exact
|
|
5
|
+
bytes, and never edit the file. A changed scope requires a new session and packet.
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"$schema": "ultra-review-worker-packet-v1",
|
|
10
|
+
"session": "<session-id>",
|
|
11
|
+
"mode": "task | change | plan",
|
|
12
|
+
"created_at": "<ISO-8601>",
|
|
13
|
+
"head": "<full-git-head>",
|
|
14
|
+
"range": "<exact-diff-range>",
|
|
15
|
+
"change_id": "<active-change-id>",
|
|
16
|
+
"task_ids": ["<task-id>"],
|
|
17
|
+
"acceptance": ["<criterion with source path and heading>"],
|
|
18
|
+
"public_seams": ["<entry point or behavior>"],
|
|
19
|
+
"context_files": [
|
|
20
|
+
{"path": ".ultra/contexts/<task-id>.md", "sha256": "<digest>"}
|
|
21
|
+
],
|
|
22
|
+
"diff_files": ["src/example.ts"],
|
|
23
|
+
"output_directory": ".ultra/reviews/<session>"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
All paths are repository-relative. `mode`, `head`, `range`, `change_id`, `task_ids`,
|
|
28
|
+
`acceptance`, `public_seams`, `context_files`, `diff_files`, and `output_directory` are
|
|
29
|
+
required. Arrays may be empty only when the selected mode makes the absence explicit
|
|
30
|
+
in the packet.
|
|
31
|
+
|
|
32
|
+
Give a selected worker only:
|
|
33
|
+
|
|
34
|
+
1. this packet path and exact digest;
|
|
35
|
+
2. `unified-schema.md`;
|
|
36
|
+
3. its one lens reference.
|
|
37
|
+
|
|
38
|
+
The worker writes only its assigned JSON artifact. Its `packet_digest` must equal the
|
|
39
|
+
expected digest passed to `review_wait.py`; a merely well-formed digest is insufficient.
|
|
40
|
+
The coordinator owns worker selection, synthesis, repair, and the summary.
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"""Wait for and validate Ultra review v2 artifacts.
|
|
3
3
|
|
|
4
4
|
Usage:
|
|
5
|
-
python3 review_wait.py <session_path> agents <artifact-stem> [<artifact-stem> ...]
|
|
6
|
-
python3 review_wait.py <session_path> summary
|
|
5
|
+
python3 review_wait.py <session_path> agents --packet-digest <sha256> <artifact-stem> [<artifact-stem> ...]
|
|
6
|
+
python3 review_wait.py <session_path> summary --packet-digest <sha256>
|
|
7
7
|
|
|
8
8
|
The agents mode validates the exact named JSON files. The summary mode validates the
|
|
9
9
|
two-axis verdict contract. Timeout and poll intervals can be shortened in tests with
|
|
@@ -96,13 +96,15 @@ def validate_finding(finding, artifact_axis: str, seen_ids):
|
|
|
96
96
|
return None
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
def validate_specialist(data):
|
|
99
|
+
def validate_specialist(data, expected_packet_digest: str):
|
|
100
100
|
if data.get("$schema") != FINDINGS_SCHEMA:
|
|
101
101
|
return f"$schema must be {FINDINGS_SCHEMA}"
|
|
102
102
|
if not nonempty_string(data.get("agent")):
|
|
103
103
|
return "agent must be a non-empty string"
|
|
104
104
|
if not digest_string(data.get("packet_digest")):
|
|
105
105
|
return "packet_digest must be a lowercase SHA-256 digest"
|
|
106
|
+
if data["packet_digest"] != expected_packet_digest:
|
|
107
|
+
return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
|
|
106
108
|
axis = data.get("axis")
|
|
107
109
|
if axis not in AXES:
|
|
108
110
|
return "axis must be spec_fidelity or engineering_standards"
|
|
@@ -137,7 +139,7 @@ def validate_specialist(data):
|
|
|
137
139
|
return None
|
|
138
140
|
|
|
139
141
|
|
|
140
|
-
def evaluate_artifacts(session_path: Path, expected):
|
|
142
|
+
def evaluate_artifacts(session_path: Path, expected, expected_packet_digest: str):
|
|
141
143
|
done = []
|
|
142
144
|
missing = []
|
|
143
145
|
invalid = []
|
|
@@ -149,7 +151,7 @@ def evaluate_artifacts(session_path: Path, expected):
|
|
|
149
151
|
continue
|
|
150
152
|
data, error = read_json(path)
|
|
151
153
|
if error is None:
|
|
152
|
-
error = validate_specialist(data)
|
|
154
|
+
error = validate_specialist(data, expected_packet_digest)
|
|
153
155
|
if error:
|
|
154
156
|
invalid.append(stem)
|
|
155
157
|
errors[stem] = error
|
|
@@ -158,10 +160,12 @@ def evaluate_artifacts(session_path: Path, expected):
|
|
|
158
160
|
return done, missing, invalid, errors
|
|
159
161
|
|
|
160
162
|
|
|
161
|
-
def wait_for_agents(session_path: Path, expected, timeout: float) -> bool:
|
|
163
|
+
def wait_for_agents(session_path: Path, expected, expected_packet_digest: str, timeout: float) -> bool:
|
|
162
164
|
deadline = time.monotonic() + timeout
|
|
163
165
|
while True:
|
|
164
|
-
done, missing, invalid, errors = evaluate_artifacts(
|
|
166
|
+
done, missing, invalid, errors = evaluate_artifacts(
|
|
167
|
+
session_path, expected, expected_packet_digest
|
|
168
|
+
)
|
|
165
169
|
if len(done) == len(expected):
|
|
166
170
|
print(json.dumps({
|
|
167
171
|
"status": "complete",
|
|
@@ -195,7 +199,7 @@ def expected_overall_verdict(data) -> str:
|
|
|
195
199
|
return "APPROVE"
|
|
196
200
|
|
|
197
201
|
|
|
198
|
-
def validate_summary(data):
|
|
202
|
+
def validate_summary(data, expected_packet_digest: str):
|
|
199
203
|
if data.get("$schema") != SUMMARY_SCHEMA:
|
|
200
204
|
return f"$schema must be {SUMMARY_SCHEMA}"
|
|
201
205
|
if data.get("mode") not in REVIEW_MODES:
|
|
@@ -205,6 +209,8 @@ def validate_summary(data):
|
|
|
205
209
|
return f"{field} must be a non-empty string"
|
|
206
210
|
if not digest_string(data.get("packet_digest")):
|
|
207
211
|
return "packet_digest must be a lowercase SHA-256 digest"
|
|
212
|
+
if data["packet_digest"] != expected_packet_digest:
|
|
213
|
+
return f"packet_digest does not match expected immutable packet {expected_packet_digest}"
|
|
208
214
|
if data.get("worktree_digest") is not None and not nonempty_string(data.get("worktree_digest")):
|
|
209
215
|
return "worktree_digest must be null or a non-empty string"
|
|
210
216
|
task_ids = data.get("task_ids")
|
|
@@ -282,7 +288,7 @@ def validate_summary(data):
|
|
|
282
288
|
return None
|
|
283
289
|
|
|
284
290
|
|
|
285
|
-
def wait_for_summary(session_path: Path, timeout: float) -> bool:
|
|
291
|
+
def wait_for_summary(session_path: Path, expected_packet_digest: str, timeout: float) -> bool:
|
|
286
292
|
summary_path = session_path / "SUMMARY.json"
|
|
287
293
|
deadline = time.monotonic() + timeout
|
|
288
294
|
last_error = "SUMMARY.json is missing"
|
|
@@ -290,7 +296,7 @@ def wait_for_summary(session_path: Path, timeout: float) -> bool:
|
|
|
290
296
|
if summary_path.exists():
|
|
291
297
|
data, error = read_json(summary_path)
|
|
292
298
|
if error is None:
|
|
293
|
-
error = validate_summary(data)
|
|
299
|
+
error = validate_summary(data, expected_packet_digest)
|
|
294
300
|
if error is None:
|
|
295
301
|
counts = {severity: 0 for severity in sorted(SEVERITIES)}
|
|
296
302
|
for finding in data["findings"]:
|
|
@@ -319,19 +325,24 @@ def main():
|
|
|
319
325
|
print(f"Error: session directory not found: {session_path}", file=sys.stderr)
|
|
320
326
|
sys.exit(2)
|
|
321
327
|
|
|
328
|
+
if len(sys.argv) < 5 or sys.argv[3] != "--packet-digest" or not digest_string(sys.argv[4]):
|
|
329
|
+
print("Error: --packet-digest requires the expected lowercase SHA-256 digest", file=sys.stderr)
|
|
330
|
+
sys.exit(2)
|
|
331
|
+
expected_packet_digest = sys.argv[4]
|
|
332
|
+
|
|
322
333
|
if mode == "agents":
|
|
323
|
-
expected = sys.argv[
|
|
334
|
+
expected = sys.argv[5:]
|
|
324
335
|
if not expected or len(expected) != len(set(expected)) or not all(
|
|
325
336
|
ARTIFACT_STEM.fullmatch(stem) for stem in expected
|
|
326
337
|
):
|
|
327
338
|
print("Error: agents mode requires unique safe artifact stems", file=sys.stderr)
|
|
328
339
|
sys.exit(2)
|
|
329
|
-
ok = wait_for_agents(session_path, expected, DEFAULT_TIMEOUT)
|
|
340
|
+
ok = wait_for_agents(session_path, expected, expected_packet_digest, DEFAULT_TIMEOUT)
|
|
330
341
|
elif mode == "summary":
|
|
331
|
-
if len(sys.argv) !=
|
|
332
|
-
print("Error: summary mode accepts
|
|
342
|
+
if len(sys.argv) != 5:
|
|
343
|
+
print("Error: summary mode accepts only --packet-digest", file=sys.stderr)
|
|
333
344
|
sys.exit(2)
|
|
334
|
-
ok = wait_for_summary(session_path, DEFAULT_TIMEOUT)
|
|
345
|
+
ok = wait_for_summary(session_path, expected_packet_digest, DEFAULT_TIMEOUT)
|
|
335
346
|
else:
|
|
336
347
|
print(f"Error: unknown mode '{mode}'. Use 'agents' or 'summary'.", file=sys.stderr)
|
|
337
348
|
sys.exit(2)
|
|
@@ -1,40 +1,65 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra-status
|
|
3
|
-
description:
|
|
3
|
+
description: Infer the current Ultra workflow position, stale evidence, task risk, and installation health from repository files and host manifests. Use when the owner asks what is complete, blocked, stale, unhealthy, or useful to do next.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
This
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
6
|
+
# Route from artifacts without changing them
|
|
7
|
+
|
|
8
|
+
This router is read-only. It reports verified files, model inference and unavailable
|
|
9
|
+
evidence separately, then recommends one explicit capability without invoking it.
|
|
10
|
+
|
|
11
|
+
## Before you start
|
|
12
|
+
|
|
13
|
+
1. Read `.ultra/tasks.json` if present; read the active task's `context_file` and
|
|
14
|
+
closing `## Resume Note`.
|
|
15
|
+
2. Read `CONTEXT.md` and relevant `.ultra/decisions/` entries when they exist.
|
|
16
|
+
3. Inspect Git `HEAD`, worktree status, active/archive Change directories and installed
|
|
17
|
+
host manifests without repairing them.
|
|
18
|
+
|
|
19
|
+
## Definition of done
|
|
20
|
+
|
|
21
|
+
- The artifact route below is evaluated in order and the matching evidence is named.
|
|
22
|
+
- Stale test evidence, dirty worktree state and task graph risks are visible.
|
|
23
|
+
- Installation health reports exact missing or drifted files without mutation.
|
|
24
|
+
- The recommendation says why it follows; it does not launch the capability.
|
|
25
|
+
|
|
26
|
+
## Infer the route
|
|
27
|
+
|
|
28
|
+
| Observable files | Recommendation |
|
|
29
|
+
|---|---|
|
|
30
|
+
| No `.ultra/` | `ultra-init` |
|
|
31
|
+
| Product specification missing or contains `[NEEDS CLARIFICATION]` | `ultra-research` |
|
|
32
|
+
| A new request exists and no active Change exists | `ultra-change` |
|
|
33
|
+
| An active Change exists and `.ultra/tasks.json` is missing or `tasks` is empty | `ultra-plan` |
|
|
34
|
+
| A frontier task is pending or in progress | `ultra-dev`, naming the task |
|
|
35
|
+
| All tasks completed and no current report | `ultra-test` |
|
|
36
|
+
| `test-report.passed` is false | `ultra-dev`, with blocking findings |
|
|
37
|
+
| Report passed and `git_commit` equals `HEAD` | `ultra-deliver` |
|
|
38
|
+
| No active Change and no new request | Report the idle state; do not manufacture work |
|
|
39
|
+
|
|
40
|
+
A report whose `git_commit` differs from `HEAD`, or whose recorded `worktree.diff_digest`
|
|
41
|
+
differs from the result of resolving the installed `ultra-test` Skill and running
|
|
42
|
+
`node <ultra-test-skill-dir>/scripts/worktree_digest.cjs --project <repository-root>`, is stale. A dirty
|
|
43
|
+
worktree is a warning, not evidence of failure.
|
|
44
|
+
|
|
45
|
+
## Surface risk and installation health
|
|
46
|
+
|
|
47
|
+
List unmet task dependencies, any `in_progress` task older than 3 days, complexity
|
|
48
|
+
concentration and broken `trace_to` or `context_file` paths. These are sensors.
|
|
49
|
+
|
|
50
|
+
For installation health, compare the selected host's plugin path, skill inventory,
|
|
51
|
+
version, provenance and hook manifest against the installed files. Report broken links
|
|
52
|
+
and missing registrations. Recommend `ubp install` or `ubp update` as the reachable
|
|
53
|
+
repair; this read-only skill never performs it.
|
|
54
|
+
|
|
55
|
+
## When the owner decides
|
|
56
|
+
|
|
57
|
+
The owner chooses the next workflow, risk disposition and any reinstall. Contradictory
|
|
58
|
+
file facts are reported as contradictions rather than silently selecting one.
|
|
59
|
+
|
|
60
|
+
## References
|
|
61
|
+
|
|
62
|
+
- `../ultra-think/SKILL.md` — read only when contradictory evidence creates a real
|
|
63
|
+
decision rather than a mechanical repair.
|
|
64
|
+
- `../ultra-think/references/autonomy-boundary.md` — read before recommending a route
|
|
65
|
+
that would reduce an accepted commitment.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ultra-tdd
|
|
3
|
+
description: Drive one slice of behavior through red then green — a failing test on a confirmed seam, then the smallest code that passes it. Use when another skill is about to write or change behavior and needs the feedback loop that proves the code actually runs, including a small change that goes straight to implementation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Write one failing test on a seam, then just enough code to pass it
|
|
7
|
+
|
|
8
|
+
Without feedback from code that actually ran, the work is blind flying. This is
|
|
9
|
+
the loop that supplies it, and it serves Goal 3, Production-Ready.
|
|
10
|
+
|
|
11
|
+
## Before you start
|
|
12
|
+
|
|
13
|
+
1. Read the current task context for the seams already confirmed during
|
|
14
|
+
planning. Tests live on those, and choosing a new one is a planning decision.
|
|
15
|
+
2. Run the repository's existing test command once, unchanged, to see what
|
|
16
|
+
passing currently looks like. Read `references/test-execution.md` before recording
|
|
17
|
+
that run or any red/green evidence.
|
|
18
|
+
3. Read `CONTEXT.md` for vocabulary — test names carry the domain words.
|
|
19
|
+
|
|
20
|
+
## Definition of done
|
|
21
|
+
|
|
22
|
+
- One slice is complete: one seam, one test, one minimal implementation.
|
|
23
|
+
- The test was observed failing before the implementation existed, and observed
|
|
24
|
+
passing after. Both runs are real command output.
|
|
25
|
+
- The test reads as a specification of behavior through a public interface, and
|
|
26
|
+
survives replacing the implementation underneath it.
|
|
27
|
+
|
|
28
|
+
## Seams, and what a good test on one looks like
|
|
29
|
+
|
|
30
|
+
A seam is a place where behavior can change without editing at that place — the
|
|
31
|
+
module interface. Tests live on seams and never reach for internals. Prefer a
|
|
32
|
+
seam that already exists, take the highest one that works, and one is the ideal
|
|
33
|
+
number. Verify behavior through that public interface: a name like "user can
|
|
34
|
+
checkout with a valid cart" shows at a glance what capability exists, and one
|
|
35
|
+
logical assertion per test keeps it readable.
|
|
36
|
+
|
|
37
|
+
## Red, then green
|
|
38
|
+
|
|
39
|
+
Write the failing test first and run it, so that red is observed rather than
|
|
40
|
+
assumed. Then write the smallest code that turns it green. Predict no later
|
|
41
|
+
tests and add no speculative capability. Then take the next slice.
|
|
42
|
+
|
|
43
|
+
Refactoring is not part of this loop; `ultra-review` owns it, because the
|
|
44
|
+
restructuring worth doing only becomes visible after three or four slices.
|
|
45
|
+
|
|
46
|
+
## Three patterns that produce tests worth deleting
|
|
47
|
+
|
|
48
|
+
| Pattern | How you recognize it |
|
|
49
|
+
|---|---|
|
|
50
|
+
| Implementation-coupled | Mocks an internal collaborator, tests a private method, asserts on call counts or ordering, or reaches past the interface into the database. The check: behavior is unchanged after restructuring, yet the test breaks. |
|
|
51
|
+
| Tautological | Recomputes the expected value the same way the implementation does, so it can never fail. Expected values come from an independent source — a known literal, a hand-worked example, the specification. |
|
|
52
|
+
| All tests first | Writing every test before any implementation tests imagined behavior. It captures shape rather than user-visible behavior, is insensitive to real change, and freezes the test structure before the implementation is understood. Take one test, one implementation, and repeat. |
|
|
53
|
+
|
|
54
|
+
## Mock only at the system boundary
|
|
55
|
+
|
|
56
|
+
External APIs, time and randomness, sometimes the filesystem. Prefer a real test
|
|
57
|
+
library over mocking the database. Your own classes, modules and internal
|
|
58
|
+
collaborators stay real.
|
|
59
|
+
|
|
60
|
+
Design for that boundary: inject dependencies, and give each external operation
|
|
61
|
+
its own named function rather than one generic fetcher — mocking then needs no
|
|
62
|
+
conditional logic, and which endpoints a test touches stays visible. When a real
|
|
63
|
+
dependency is the right answer, copy
|
|
64
|
+
`references/templates/testcontainer-postgres.ts` or
|
|
65
|
+
`references/templates/testcontainer-postgres.py`, and
|
|
66
|
+
`references/templates/vertical-slice.ts` for a path that runs end to end.
|
|
67
|
+
|
|
68
|
+
## When the owner decides
|
|
69
|
+
|
|
70
|
+
Weakening an assertion, skipping a relevant test, or adjusting an expected value
|
|
71
|
+
to obtain green output crosses the C5 boundary — stop and surface it. A test that
|
|
72
|
+
is genuinely wrong about the specification is a specification question, so it
|
|
73
|
+
goes to the owner as one.
|
|
74
|
+
|
|
75
|
+
## References
|
|
76
|
+
|
|
77
|
+
- `../ultra-think/references/autonomy-boundary.md` — read before replacing a real
|
|
78
|
+
dependency with a fake, or when a test stands between the work and green.
|
|
79
|
+
- `references/templates/README.md` — read when choosing which enabling template
|
|
80
|
+
fits the dependency you need to make real.
|
|
81
|
+
- `references/test-execution.md` — exact command, failure and result evidence contract.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Enabling templates
|
|
2
|
+
|
|
3
|
+
These runnable examples make the real integration path cheaper than a fake. Read this
|
|
4
|
+
index when `ultra-tdd` reaches a database, feature flag, or multi-layer seam, then copy
|
|
5
|
+
only the smallest relevant file into the project and adapt it to repository conventions.
|
|
6
|
+
|
|
7
|
+
| Template | When to copy |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [testcontainer-postgres.ts](testcontainer-postgres.ts) | A TypeScript/Node test needs real Postgres instead of an internal Repository mock |
|
|
10
|
+
| [testcontainer-postgres.py](testcontainer-postgres.py) | A pytest test needs real Postgres instead of a `MagicMock` Repository |
|
|
11
|
+
| [vertical-slice.ts](vertical-slice.ts) | One test must prove HTTP → use case → database → response |
|
|
12
|
+
| [persistence-real.ts](persistence-real.ts) | A Repository needs real storage rather than an in-memory `Map` |
|
|
13
|
+
| [feature-flag-default-audit.sh](feature-flag-default-audit.sh) | A Change must surface default-off flags that could hide unfinished work |
|
|
14
|
+
|
|
15
|
+
The templates travel with the `ultra-tdd` skill; they are rule-side examples, not
|
|
16
|
+
project authority. Do not copy this directory wholesale into `.ultra/`.
|
|
17
|
+
|
|
18
|
+
Each template must remain runnable after its documented dependencies are installed,
|
|
19
|
+
name the exact verification command in its header, stay short enough to inspect before
|
|
20
|
+
use, and state which Ultra principle makes the example preferable.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# feature-flag-default-audit.sh
|
|
3
|
+
#
|
|
4
|
+
# Surface every `default off` / `enabled: false` / commented-out feature so the
|
|
5
|
+
# user knows what is *quietly* disabled. Catches the "agent shipped task by
|
|
6
|
+
# turning the feature off" failure mode v7 fixes.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# bash feature-flag-default-audit.sh [path]
|
|
10
|
+
#
|
|
11
|
+
# Default path: src/ (override with first arg)
|
|
12
|
+
#
|
|
13
|
+
# PHILOSOPHY: enables C5 (Bounded Autonomy) — default-off without rationale is
|
|
14
|
+
# scope reduction in disguise; this surfaces it so the user can decide.
|
|
15
|
+
|
|
16
|
+
set -euo pipefail
|
|
17
|
+
|
|
18
|
+
ROOT="${1:-src}"
|
|
19
|
+
if [ ! -d "$ROOT" ]; then
|
|
20
|
+
echo "[audit] path not found: $ROOT" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
echo "## Feature-flag default audit"
|
|
25
|
+
echo "Path: $ROOT"
|
|
26
|
+
echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
27
|
+
echo ""
|
|
28
|
+
|
|
29
|
+
# Patterns we care about. Adjust per project conventions.
|
|
30
|
+
patterns=(
|
|
31
|
+
'\benabled:\s*false\b'
|
|
32
|
+
'\bdefault\s*[:=]\s*false\b'
|
|
33
|
+
'\bDISABLED\s*=\s*true\b'
|
|
34
|
+
'\bif\s*\(\s*false\s*\)'
|
|
35
|
+
'FEATURE_[A-Z_]+\s*=\s*false'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
found_any=0
|
|
39
|
+
for p in "${patterns[@]}"; do
|
|
40
|
+
hits=$(grep -REn "$p" "$ROOT" \
|
|
41
|
+
--include='*.ts' --include='*.tsx' \
|
|
42
|
+
--include='*.js' --include='*.jsx' \
|
|
43
|
+
--include='*.py' --include='*.go' \
|
|
44
|
+
--include='*.rs' --include='*.sol' \
|
|
45
|
+
--include='*.yaml' --include='*.yml' \
|
|
46
|
+
--include='*.json' --include='*.toml' 2>/dev/null || true)
|
|
47
|
+
|
|
48
|
+
if [ -n "$hits" ]; then
|
|
49
|
+
found_any=1
|
|
50
|
+
echo "### Pattern: $p"
|
|
51
|
+
echo '```'
|
|
52
|
+
echo "$hits"
|
|
53
|
+
echo '```'
|
|
54
|
+
echo ""
|
|
55
|
+
fi
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
# Also surface TODO-style "off until later" markers
|
|
59
|
+
todo_hits=$(grep -REn '(TODO|FIXME).*(enable|turn on|flip on)' "$ROOT" 2>/dev/null || true)
|
|
60
|
+
if [ -n "$todo_hits" ]; then
|
|
61
|
+
found_any=1
|
|
62
|
+
echo "### TODOs to re-enable later"
|
|
63
|
+
echo '```'
|
|
64
|
+
echo "$todo_hits"
|
|
65
|
+
echo '```'
|
|
66
|
+
echo ""
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
if [ "$found_any" -eq 0 ]; then
|
|
70
|
+
echo "_No default-off features detected._"
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
echo "---"
|
|
75
|
+
echo ""
|
|
76
|
+
echo "**Action**: For each entry above, confirm with the user whether it is:"
|
|
77
|
+
echo "- (A) Pre-launch: feature is built but gated until release — fine"
|
|
78
|
+
echo "- (B) Genuine kill-switch: stays off in normal operation — fine, document why"
|
|
79
|
+
echo "- (C) Hiding incomplete work: agent shipped task by turning it off — **must fix**"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-persistence Repository skeleton (TypeScript)
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: in-memory `Map` Repositories are a v7 anti-pattern. They
|
|
5
|
+
* pass tests, don't survive restart, hide migration bugs, and silently break
|
|
6
|
+
* production. This template shows the minimum real Repository.
|
|
7
|
+
*
|
|
8
|
+
* Required deps:
|
|
9
|
+
* pnpm add pg
|
|
10
|
+
* # Optional ORM: drizzle / prisma / kysely
|
|
11
|
+
*
|
|
12
|
+
* PHILOSOPHY: enables C4 progress.json `persistence_real` dimension. Pair with
|
|
13
|
+
* testcontainer-postgres.ts for the test side.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Pool, PoolClient } from 'pg';
|
|
17
|
+
|
|
18
|
+
// Domain entity — keep separate from row shape
|
|
19
|
+
export type User = {
|
|
20
|
+
id: number;
|
|
21
|
+
email: string;
|
|
22
|
+
createdAt: Date;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Optional: row→entity mapper if column names differ from field names
|
|
26
|
+
function rowToUser(row: { id: number; email: string; created_at: Date }): User {
|
|
27
|
+
return { id: row.id, email: row.email, createdAt: row.created_at };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class UserRepo {
|
|
31
|
+
constructor(private readonly pool: Pool) {}
|
|
32
|
+
|
|
33
|
+
async create(input: { email: string }): Promise<User> {
|
|
34
|
+
const { rows } = await this.pool.query<{
|
|
35
|
+
id: number;
|
|
36
|
+
email: string;
|
|
37
|
+
created_at: Date;
|
|
38
|
+
}>(
|
|
39
|
+
`INSERT INTO users (email) VALUES ($1)
|
|
40
|
+
RETURNING id, email, created_at`,
|
|
41
|
+
[input.email],
|
|
42
|
+
);
|
|
43
|
+
return rowToUser(rows[0]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async findById(id: number): Promise<User | null> {
|
|
47
|
+
const { rows } = await this.pool.query<{
|
|
48
|
+
id: number;
|
|
49
|
+
email: string;
|
|
50
|
+
created_at: Date;
|
|
51
|
+
}>(
|
|
52
|
+
`SELECT id, email, created_at FROM users WHERE id = $1`,
|
|
53
|
+
[id],
|
|
54
|
+
);
|
|
55
|
+
return rows[0] ? rowToUser(rows[0]) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Multi-statement work — pass in a client to keep it on a single connection
|
|
59
|
+
async withTransaction<T>(work: (client: PoolClient) => Promise<T>): Promise<T> {
|
|
60
|
+
const client = await this.pool.connect();
|
|
61
|
+
try {
|
|
62
|
+
await client.query('BEGIN');
|
|
63
|
+
const result = await work(client);
|
|
64
|
+
await client.query('COMMIT');
|
|
65
|
+
return result;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
await client.query('ROLLBACK');
|
|
68
|
+
throw e;
|
|
69
|
+
} finally {
|
|
70
|
+
client.release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// What this template intentionally does NOT do (and why):
|
|
76
|
+
// - No `Map<number, User>` field. That is the in-memory anti-pattern v7 surfaces.
|
|
77
|
+
// - No silent error swallowing (catch + return-null). Errors propagate; callers handle.
|
|
78
|
+
// - No mock-friendly singleton. Inject the Pool so testcontainers work cleanly.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Testcontainer-Postgres starter (Python / pytest)
|
|
3
|
+
|
|
4
|
+
Why this exists: Real DB in tests catches migration drift, schema bugs, and
|
|
5
|
+
concurrency issues that mocked Repositories silently miss. Prefer this to
|
|
6
|
+
`MagicMock(spec=Repo)` for any Repository or Service that touches data.
|
|
7
|
+
|
|
8
|
+
Required deps:
|
|
9
|
+
pip install testcontainers[postgres] psycopg[binary] pytest
|
|
10
|
+
|
|
11
|
+
Verify:
|
|
12
|
+
pytest tests/repo/test_user_repo_testcontainer.py -v
|
|
13
|
+
|
|
14
|
+
PHILOSOPHY: enables C2 (Enabling > Defensive) — the cheaper path becomes the
|
|
15
|
+
right path, so agents stop reaching for mocks under pressure.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import psycopg
|
|
21
|
+
import pytest
|
|
22
|
+
from testcontainers.postgres import PostgresContainer
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(scope="module")
|
|
26
|
+
def pg():
|
|
27
|
+
"""Module-scoped real Postgres. Slow on first pull, fast after."""
|
|
28
|
+
with PostgresContainer("postgres:16-alpine") as c:
|
|
29
|
+
with psycopg.connect(c.get_connection_url()) as conn:
|
|
30
|
+
with conn.cursor() as cur:
|
|
31
|
+
cur.execute("""
|
|
32
|
+
CREATE TABLE users (
|
|
33
|
+
id SERIAL PRIMARY KEY,
|
|
34
|
+
email TEXT UNIQUE NOT NULL,
|
|
35
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
36
|
+
);
|
|
37
|
+
""")
|
|
38
|
+
conn.commit()
|
|
39
|
+
yield c
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def conn(pg):
|
|
44
|
+
"""Per-test connection with truncate-reset so each test is independent."""
|
|
45
|
+
with psycopg.connect(pg.get_connection_url()) as c:
|
|
46
|
+
with c.cursor() as cur:
|
|
47
|
+
cur.execute("TRUNCATE users RESTART IDENTITY CASCADE")
|
|
48
|
+
c.commit()
|
|
49
|
+
yield c
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_user_repo(conn):
|
|
53
|
+
"""Replace with import from your real Repository."""
|
|
54
|
+
class UserRepo:
|
|
55
|
+
def create(self, email: str) -> dict:
|
|
56
|
+
with conn.cursor() as cur:
|
|
57
|
+
cur.execute(
|
|
58
|
+
"INSERT INTO users (email) VALUES (%s) RETURNING id, email, created_at",
|
|
59
|
+
(email,),
|
|
60
|
+
)
|
|
61
|
+
row = cur.fetchone()
|
|
62
|
+
conn.commit()
|
|
63
|
+
return {"id": row[0], "email": row[1], "created_at": row[2]}
|
|
64
|
+
|
|
65
|
+
def find_by_id(self, user_id: int) -> dict | None:
|
|
66
|
+
with conn.cursor() as cur:
|
|
67
|
+
cur.execute("SELECT id, email, created_at FROM users WHERE id = %s", (user_id,))
|
|
68
|
+
row = cur.fetchone()
|
|
69
|
+
return None if row is None else {"id": row[0], "email": row[1], "created_at": row[2]}
|
|
70
|
+
|
|
71
|
+
return UserRepo()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_insert_and_fetch_round_trips(conn):
|
|
75
|
+
repo = make_user_repo(conn)
|
|
76
|
+
created = repo.create("a@example.com")
|
|
77
|
+
found = repo.find_by_id(created["id"])
|
|
78
|
+
assert found == created
|