factoryline-code-factory 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- factoryline/__init__.py +1 -0
- factoryline/app_builder.py +493 -0
- factoryline/assembly.py +263 -0
- factoryline/attribution.py +113 -0
- factoryline/boundary.py +23 -0
- factoryline/challenge.py +53 -0
- factoryline/cli.py +477 -0
- factoryline/contract.py +169 -0
- factoryline/coverage.py +76 -0
- factoryline/meter.py +160 -0
- factoryline/optimizer.py +152 -0
- factoryline/passport.py +148 -0
- factoryline/proof.py +546 -0
- factoryline/protocol.py +74 -0
- factoryline/refinement.py +81 -0
- factoryline_code_factory-0.5.1.dist-info/METADATA +255 -0
- factoryline_code_factory-0.5.1.dist-info/RECORD +24 -0
- factoryline_code_factory-0.5.1.dist-info/WHEEL +5 -0
- factoryline_code_factory-0.5.1.dist-info/entry_points.txt +2 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE +17 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE-APACHE +19 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/LICENSE-MIT +22 -0
- factoryline_code_factory-0.5.1.dist-info/licenses/NOTICE +7 -0
- factoryline_code_factory-0.5.1.dist-info/top_level.txt +1 -0
factoryline/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.1"
|
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""PRD-to-app scaffolding for Factoryline.
|
|
2
|
+
|
|
3
|
+
This module is deliberately deterministic. It does not call a model or claim to
|
|
4
|
+
finish bespoke product logic. It turns a PRD or prompt into a full-stack starter
|
|
5
|
+
repo with a blueprint, handoff plan, smoke hooks, and docs that downstream agents
|
|
6
|
+
can harden through the existing factory gates.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
APP_SCHEMA = "factory.app_blueprint.v1"
|
|
18
|
+
|
|
19
|
+
STACKS = {
|
|
20
|
+
"nextjs-fastapi-postgres": {
|
|
21
|
+
"frontend": "Next.js",
|
|
22
|
+
"backend": "FastAPI",
|
|
23
|
+
"database": "Postgres",
|
|
24
|
+
"test": "pytest + route smoke",
|
|
25
|
+
},
|
|
26
|
+
"react-fastapi-sqlite": {
|
|
27
|
+
"frontend": "React + Vite",
|
|
28
|
+
"backend": "FastAPI",
|
|
29
|
+
"database": "SQLite",
|
|
30
|
+
"test": "pytest + route smoke",
|
|
31
|
+
},
|
|
32
|
+
"react-fastapi-postgres": {
|
|
33
|
+
"frontend": "React",
|
|
34
|
+
"backend": "FastAPI",
|
|
35
|
+
"database": "Postgres",
|
|
36
|
+
"test": "pytest + route smoke",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
PURPOSE_DEFAULTS = {
|
|
41
|
+
"healthcare": {
|
|
42
|
+
"roles": ["patient", "clinician", "admin"],
|
|
43
|
+
"workflows": ["submit_request", "clinical_review", "audit_decision"],
|
|
44
|
+
"logic": ["eligibility_gate", "denial_reason_codes"],
|
|
45
|
+
},
|
|
46
|
+
"fintech": {
|
|
47
|
+
"roles": ["customer", "analyst", "admin"],
|
|
48
|
+
"workflows": ["submit_case", "risk_review", "audit_action"],
|
|
49
|
+
"logic": ["risk_tier_gate", "approval_limit_rules"],
|
|
50
|
+
},
|
|
51
|
+
"saas": {
|
|
52
|
+
"roles": ["user", "manager", "admin"],
|
|
53
|
+
"workflows": ["create_record", "review_record", "export_receipt"],
|
|
54
|
+
"logic": ["plan_limit_gate", "approval_policy"],
|
|
55
|
+
},
|
|
56
|
+
"marketplace": {
|
|
57
|
+
"roles": ["buyer", "seller", "moderator"],
|
|
58
|
+
"workflows": ["create_listing", "request_order", "resolve_dispute"],
|
|
59
|
+
"logic": ["listing_quality_gate", "dispute_routing_rules"],
|
|
60
|
+
},
|
|
61
|
+
"developer": {
|
|
62
|
+
"roles": ["developer", "maintainer", "reviewer"],
|
|
63
|
+
"workflows": ["create_project", "run_checks", "publish_receipt"],
|
|
64
|
+
"logic": ["gate_selection_rules", "release_readiness_rules"],
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class AppBlueprint:
|
|
71
|
+
name: str
|
|
72
|
+
purpose: str
|
|
73
|
+
stack: str
|
|
74
|
+
prompt_source: str
|
|
75
|
+
roles: list[str]
|
|
76
|
+
workflows: list[str]
|
|
77
|
+
deterministic_logic: list[str]
|
|
78
|
+
required_gates: list[str]
|
|
79
|
+
generated_at: str
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict:
|
|
82
|
+
return {
|
|
83
|
+
"schema": APP_SCHEMA,
|
|
84
|
+
"app": {
|
|
85
|
+
"name": self.name,
|
|
86
|
+
"purpose": self.purpose,
|
|
87
|
+
"stack": {
|
|
88
|
+
"key": self.stack,
|
|
89
|
+
**STACKS[self.stack],
|
|
90
|
+
},
|
|
91
|
+
"prompt_source": self.prompt_source,
|
|
92
|
+
"roles": self.roles,
|
|
93
|
+
"workflows": self.workflows,
|
|
94
|
+
"deterministic_logic": self.deterministic_logic,
|
|
95
|
+
"required_gates": self.required_gates,
|
|
96
|
+
},
|
|
97
|
+
"generated_at": self.generated_at,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _slug(value: str) -> str:
|
|
102
|
+
text = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-")
|
|
103
|
+
return text[:48] or "factory-app"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _extract_name(text: str, fallback: str | None = None) -> str:
|
|
107
|
+
if fallback:
|
|
108
|
+
return _slug(fallback)
|
|
109
|
+
heading = re.search(r"^#\s+(.+)$", text, re.M)
|
|
110
|
+
if heading:
|
|
111
|
+
return _slug(heading.group(1))
|
|
112
|
+
words = re.findall(r"[a-zA-Z][a-zA-Z0-9-]+", text.lower())
|
|
113
|
+
stop = {"build", "create", "make", "an", "a", "the", "with", "for", "and"}
|
|
114
|
+
useful = [word for word in words if word not in stop]
|
|
115
|
+
return _slug("-".join(useful[:5]) or "factory-app")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _purpose_from_text(text: str, explicit: str) -> str:
|
|
119
|
+
if explicit != "auto":
|
|
120
|
+
return explicit
|
|
121
|
+
low = text.lower()
|
|
122
|
+
if any(term in low for term in ("clinical", "patient", "hipaa", "prior-auth", "prior auth")):
|
|
123
|
+
return "healthcare"
|
|
124
|
+
if any(term in low for term in ("payment", "invoice", "bank", "risk", "fintech")):
|
|
125
|
+
return "fintech"
|
|
126
|
+
if any(term in low for term in ("marketplace", "seller", "buyer", "listing")):
|
|
127
|
+
return "marketplace"
|
|
128
|
+
if any(term in low for term in ("api", "cli", "developer", "github")):
|
|
129
|
+
return "developer"
|
|
130
|
+
return "saas"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_blueprint(
|
|
134
|
+
text: str,
|
|
135
|
+
*,
|
|
136
|
+
source: str,
|
|
137
|
+
name: str | None = None,
|
|
138
|
+
stack: str = "nextjs-fastapi-postgres",
|
|
139
|
+
purpose: str = "auto",
|
|
140
|
+
) -> AppBlueprint:
|
|
141
|
+
if stack not in STACKS:
|
|
142
|
+
raise ValueError(f"unknown stack: {stack}")
|
|
143
|
+
resolved_purpose = _purpose_from_text(text, purpose)
|
|
144
|
+
defaults = PURPOSE_DEFAULTS.get(resolved_purpose, PURPOSE_DEFAULTS["saas"])
|
|
145
|
+
return AppBlueprint(
|
|
146
|
+
name=_extract_name(text, name),
|
|
147
|
+
purpose=resolved_purpose,
|
|
148
|
+
stack=stack,
|
|
149
|
+
prompt_source=source,
|
|
150
|
+
roles=list(defaults["roles"]),
|
|
151
|
+
workflows=list(defaults["workflows"]),
|
|
152
|
+
deterministic_logic=list(defaults["logic"]),
|
|
153
|
+
required_gates=[
|
|
154
|
+
"prd_optimized",
|
|
155
|
+
"strict_spec",
|
|
156
|
+
"hollow_validators",
|
|
157
|
+
"architecture_gate",
|
|
158
|
+
"hollow_tests",
|
|
159
|
+
"runtime_smoke",
|
|
160
|
+
"design_brief",
|
|
161
|
+
"design_audit",
|
|
162
|
+
"pr_pack",
|
|
163
|
+
],
|
|
164
|
+
generated_at=datetime.now(timezone.utc).isoformat(),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _write(path: Path, content: str) -> None:
|
|
169
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
170
|
+
path.write_text(content.rstrip() + "\n", encoding="utf-8")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _frontend_page(blueprint: AppBlueprint) -> str:
|
|
174
|
+
title = blueprint.name.replace("-", " ").title()
|
|
175
|
+
workflow = blueprint.workflows[0].replace("_", " ")
|
|
176
|
+
return f"""export default function Home() {{
|
|
177
|
+
return (
|
|
178
|
+
<main>
|
|
179
|
+
<section className="hero">
|
|
180
|
+
<p className="eyebrow">{blueprint.purpose} app factory starter</p>
|
|
181
|
+
<h1>{title}</h1>
|
|
182
|
+
<p>Generated from a PRD with gates for spec clarity, architecture, smoke, design, and PR evidence.</p>
|
|
183
|
+
<a className="primary" href="/api/health">Check API health</a>
|
|
184
|
+
</section>
|
|
185
|
+
<section className="panel">
|
|
186
|
+
<h2>Primary workflow</h2>
|
|
187
|
+
<p>{workflow}</p>
|
|
188
|
+
</section>
|
|
189
|
+
</main>
|
|
190
|
+
);
|
|
191
|
+
}}
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _frontend_css() -> str:
|
|
196
|
+
return """:root { color-scheme: light; --ink: #172033; --accent: #2563eb; --paper: #f8fafc; }
|
|
197
|
+
body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, sans-serif; color: var(--ink); background: white; }
|
|
198
|
+
.hero { min-height: 70vh; padding: 6rem 8vw; background: linear-gradient(135deg, #eef2ff, #f8fafc 54%, #ecfeff); }
|
|
199
|
+
.eyebrow { text-transform: uppercase; font-size: .78rem; letter-spacing: .08em; color: #2563eb; font-weight: 700; }
|
|
200
|
+
h1 { font-size: clamp(2.4rem, 6vw, 5rem); line-height: 1; margin: 0 0 1rem; max-width: 840px; }
|
|
201
|
+
p { font-size: 1.12rem; line-height: 1.7; max-width: 720px; }
|
|
202
|
+
.primary { display: inline-block; margin-top: 1.4rem; padding: .95rem 1.15rem; color: white; background: var(--accent); text-decoration: none; font-weight: 700; }
|
|
203
|
+
.panel { padding: 4rem 8vw; background: var(--paper); }
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _backend_main(blueprint: AppBlueprint) -> str:
|
|
208
|
+
return f'''"""FastAPI starter generated by Code Factory."""
|
|
209
|
+
from fastapi import FastAPI
|
|
210
|
+
|
|
211
|
+
app = FastAPI(title="{blueprint.name}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@app.get("/healthz")
|
|
215
|
+
def healthz():
|
|
216
|
+
return {{"ok": True, "app": "{blueprint.name}", "purpose": "{blueprint.purpose}"}}
|
|
217
|
+
'''
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _pytest_health() -> str:
|
|
221
|
+
return '''from fastapi.testclient import TestClient
|
|
222
|
+
from backend.main import app
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_healthz():
|
|
226
|
+
response = TestClient(app).get("/healthz")
|
|
227
|
+
assert response.status_code == 200
|
|
228
|
+
assert response.json()["ok"] is True
|
|
229
|
+
'''
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _schema_sql(blueprint: AppBlueprint) -> str:
|
|
233
|
+
if STACKS[blueprint.stack]["database"] == "SQLite":
|
|
234
|
+
return f"""create table if not exists audit_events (
|
|
235
|
+
id integer primary key autoincrement,
|
|
236
|
+
app_name text not null default '{blueprint.name}',
|
|
237
|
+
actor text not null,
|
|
238
|
+
action text not null,
|
|
239
|
+
created_at text not null default current_timestamp
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
create table if not exists workflow_items (
|
|
243
|
+
id integer primary key autoincrement,
|
|
244
|
+
workflow text not null,
|
|
245
|
+
status text not null default 'draft',
|
|
246
|
+
payload text not null default '{{}}',
|
|
247
|
+
created_at text not null default current_timestamp
|
|
248
|
+
);
|
|
249
|
+
"""
|
|
250
|
+
return f"""create table if not exists audit_events (
|
|
251
|
+
id bigserial primary key,
|
|
252
|
+
app_name text not null default '{blueprint.name}',
|
|
253
|
+
actor text not null,
|
|
254
|
+
action text not null,
|
|
255
|
+
created_at timestamptz not null default now()
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
create table if not exists workflow_items (
|
|
259
|
+
id bigserial primary key,
|
|
260
|
+
workflow text not null,
|
|
261
|
+
status text not null default 'draft',
|
|
262
|
+
payload jsonb not null default '{{}}'::jsonb,
|
|
263
|
+
created_at timestamptz not null default now()
|
|
264
|
+
);
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _workflow_doc(blueprint: AppBlueprint) -> str:
|
|
269
|
+
gates = "\n".join(f"- {gate}" for gate in blueprint.required_gates)
|
|
270
|
+
workflows = "\n".join(f"- {item}" for item in blueprint.workflows)
|
|
271
|
+
logic = "\n".join(f"- {item}" for item in blueprint.deterministic_logic)
|
|
272
|
+
return f"""# PRD-to-App Factory Workflow
|
|
273
|
+
|
|
274
|
+
```mermaid
|
|
275
|
+
flowchart TD
|
|
276
|
+
A["PRD or prompt"] --> B["SpecLine optimize-prd"]
|
|
277
|
+
B --> C["App blueprint"]
|
|
278
|
+
C --> D["Full-stack scaffold"]
|
|
279
|
+
D --> E["Prestige design brief"]
|
|
280
|
+
E --> F["ForgeLine hardening loop"]
|
|
281
|
+
F --> G["HSF deterministic logic"]
|
|
282
|
+
G --> H["Factory PR evidence packet"]
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Workflows
|
|
286
|
+
|
|
287
|
+
{workflows}
|
|
288
|
+
|
|
289
|
+
## Deterministic Logic Candidates
|
|
290
|
+
|
|
291
|
+
{logic}
|
|
292
|
+
|
|
293
|
+
## Required Gates
|
|
294
|
+
|
|
295
|
+
{gates}
|
|
296
|
+
|
|
297
|
+
## Illustrative Readiness Model
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
PRD clarity | NOT RUN
|
|
301
|
+
Architecture | NOT RUN
|
|
302
|
+
Runtime smoke | NOT RUN
|
|
303
|
+
Design fit | NOT RUN
|
|
304
|
+
PR evidence | NOT RUN
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
These bars are not measured gate results. They are a visual map of the readiness
|
|
308
|
+
dimensions this starter is prepared to hand off to the factory. Replace them
|
|
309
|
+
with receipt-backed evidence only after the gates run.
|
|
310
|
+
|
|
311
|
+
## Coverage Gate
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
factory coverage --root .
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Generated starters cover only `RUNTIME_HEALTH` at first. Product requirements
|
|
318
|
+
remain intentionally uncovered until you add non-hollow smoke checks with
|
|
319
|
+
`covers[]` entries.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _readme(blueprint: AppBlueprint) -> str:
|
|
324
|
+
title = blueprint.name.replace("-", " ").title()
|
|
325
|
+
return f"""# {title}
|
|
326
|
+
|
|
327
|
+
Generated by `factory app` as a PRD-to-app starter.
|
|
328
|
+
|
|
329
|
+
## Stack
|
|
330
|
+
|
|
331
|
+
- Frontend: {STACKS[blueprint.stack]["frontend"]}
|
|
332
|
+
- Backend: {STACKS[blueprint.stack]["backend"]}
|
|
333
|
+
- Database: {STACKS[blueprint.stack]["database"]}
|
|
334
|
+
- Purpose: {blueprint.purpose}
|
|
335
|
+
|
|
336
|
+
## Next Commands
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
specline optimize-prd PRD.md
|
|
340
|
+
prestige brief PRD.md --purpose {blueprint.purpose}
|
|
341
|
+
forge verify-tests {blueprint.name} {blueprint.name}.ssat.yaml --root .
|
|
342
|
+
factory coverage --root .
|
|
343
|
+
factory optimize-pr --changed app_blueprint.json --feature {blueprint.name}
|
|
344
|
+
factory pr-pack {blueprint.name}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
`factory coverage` is expected to report `hollow_coverage` on a fresh starter
|
|
348
|
+
until product-specific smoke checks are added.
|
|
349
|
+
|
|
350
|
+
The scaffold is intentionally reviewable: product-specific logic should move
|
|
351
|
+
through SpecLine, ForgeLine, HSF, Prestige, and Factoryline evidence before
|
|
352
|
+
release.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _ssat_yaml(blueprint: AppBlueprint) -> str:
|
|
357
|
+
return f"""name: {blueprint.name}
|
|
358
|
+
modules:
|
|
359
|
+
- name: backend
|
|
360
|
+
path: backend/main.py
|
|
361
|
+
imports: []
|
|
362
|
+
functions:
|
|
363
|
+
- name: healthz
|
|
364
|
+
args: []
|
|
365
|
+
returns: "dict"
|
|
366
|
+
doc: "Return API health and app identity."
|
|
367
|
+
dependencies: []
|
|
368
|
+
invariants:
|
|
369
|
+
- name: no_eval
|
|
370
|
+
forbid_pattern: "\\\\beval\\\\("
|
|
371
|
+
- name: no_hardcoded_secret
|
|
372
|
+
forbid_pattern: "(?i)(api_key|secret|password)\\\\s*=\\\\s*['\\"]"
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _coverage_manifest(blueprint: AppBlueprint) -> str:
|
|
377
|
+
requirements = [{
|
|
378
|
+
"id": "RUNTIME_HEALTH",
|
|
379
|
+
"summary": "The backend exposes a health endpoint that returns ok=true.",
|
|
380
|
+
"source": "generated",
|
|
381
|
+
}]
|
|
382
|
+
for workflow in blueprint.workflows:
|
|
383
|
+
requirements.append({
|
|
384
|
+
"id": f"WORKFLOW_{workflow.upper()}",
|
|
385
|
+
"summary": f"The app supports the {workflow.replace('_', ' ')} workflow.",
|
|
386
|
+
"source": "blueprint.workflow",
|
|
387
|
+
})
|
|
388
|
+
for rule in blueprint.deterministic_logic:
|
|
389
|
+
requirements.append({
|
|
390
|
+
"id": f"LOGIC_{rule.upper()}",
|
|
391
|
+
"summary": f"The deterministic rule {rule.replace('_', ' ')} is enforced.",
|
|
392
|
+
"source": "blueprint.deterministic_logic",
|
|
393
|
+
})
|
|
394
|
+
return json.dumps({
|
|
395
|
+
"schema": "factory.requirement_coverage.v1",
|
|
396
|
+
"app": blueprint.name,
|
|
397
|
+
"requirements": requirements,
|
|
398
|
+
}, indent=2)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _forge_state(blueprint: AppBlueprint) -> str:
|
|
402
|
+
return json.dumps({
|
|
403
|
+
"feature": blueprint.name,
|
|
404
|
+
"state": "blocked",
|
|
405
|
+
"created": blueprint.generated_at,
|
|
406
|
+
"attempts": {},
|
|
407
|
+
"history": [{
|
|
408
|
+
"ts": blueprint.generated_at,
|
|
409
|
+
"state": "blocked",
|
|
410
|
+
"note": "factory app starter generated; run forge verify-tests before smoke",
|
|
411
|
+
}],
|
|
412
|
+
}, indent=2)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def scaffold_app(blueprint: AppBlueprint, *, out_dir: Path, prd_text: str) -> dict:
|
|
416
|
+
out_dir = Path(out_dir)
|
|
417
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
418
|
+
_write(out_dir / "app_blueprint.json", json.dumps(blueprint.to_dict(), indent=2))
|
|
419
|
+
_write(out_dir / "PRD.md", prd_text)
|
|
420
|
+
_write(out_dir / "README.md", _readme(blueprint))
|
|
421
|
+
_write(out_dir / "docs" / "WORKFLOW.md", _workflow_doc(blueprint))
|
|
422
|
+
_write(out_dir / f"{blueprint.name}.ssat.yaml", _ssat_yaml(blueprint))
|
|
423
|
+
_write(out_dir / "coverage" / "requirements.json", _coverage_manifest(blueprint))
|
|
424
|
+
_write(out_dir / ".forge" / blueprint.name / "state.json", _forge_state(blueprint))
|
|
425
|
+
if STACKS[blueprint.stack]["frontend"] == "Next.js":
|
|
426
|
+
_write(out_dir / "frontend" / "app" / "page.tsx", _frontend_page(blueprint))
|
|
427
|
+
frontend_package = {
|
|
428
|
+
"scripts": {"dev": "next dev", "build": "next build"},
|
|
429
|
+
"dependencies": {"next": "^15.0.0", "react": "^19.0.0", "react-dom": "^19.0.0"},
|
|
430
|
+
"devDependencies": {"typescript": "^5.0.0"},
|
|
431
|
+
}
|
|
432
|
+
else:
|
|
433
|
+
_write(out_dir / "frontend" / "src" / "App.tsx", _frontend_page(blueprint))
|
|
434
|
+
_write(out_dir / "frontend" / "src" / "main.tsx", """import React from 'react';
|
|
435
|
+
import ReactDOM from 'react-dom/client';
|
|
436
|
+
import App from './App';
|
|
437
|
+
import '../app/globals.css';
|
|
438
|
+
|
|
439
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);
|
|
440
|
+
""")
|
|
441
|
+
_write(out_dir / "frontend" / "index.html", '<!doctype html><html><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>')
|
|
442
|
+
frontend_package = {
|
|
443
|
+
"scripts": {"dev": "vite", "build": "tsc && vite build"},
|
|
444
|
+
"dependencies": {"@vitejs/plugin-react": "^4.0.0", "vite": "^6.0.0", "react": "^19.0.0", "react-dom": "^19.0.0"},
|
|
445
|
+
"devDependencies": {"typescript": "^5.0.0"},
|
|
446
|
+
}
|
|
447
|
+
_write(out_dir / "frontend" / "app" / "globals.css", _frontend_css())
|
|
448
|
+
_write(out_dir / "frontend" / "package.json", json.dumps(frontend_package, indent=2))
|
|
449
|
+
_write(out_dir / "backend" / "__init__.py", "")
|
|
450
|
+
_write(out_dir / "backend" / "main.py", _backend_main(blueprint))
|
|
451
|
+
_write(out_dir / "backend" / "requirements.txt", "fastapi\nuvicorn\npytest\nhttpx\n")
|
|
452
|
+
_write(out_dir / "tests" / "test_health.py", _pytest_health())
|
|
453
|
+
_write(out_dir / "db" / "schema.sql", _schema_sql(blueprint))
|
|
454
|
+
_write(out_dir / "smoke" / f"{blueprint.name}.json", json.dumps({
|
|
455
|
+
"checks": [{
|
|
456
|
+
"name": "backend_health",
|
|
457
|
+
"kind": "python",
|
|
458
|
+
"run": "from fastapi.testclient import TestClient\nfrom backend.main import app\nassert TestClient(app).get('/healthz').json()['ok'] is True\n",
|
|
459
|
+
"covers": ["RUNTIME_HEALTH"],
|
|
460
|
+
"must_fail_on_stub": True,
|
|
461
|
+
}]
|
|
462
|
+
}, indent=2))
|
|
463
|
+
paths = sorted(str(path.relative_to(out_dir)).replace("\\", "/") for path in out_dir.rglob("*") if path.is_file())
|
|
464
|
+
return {
|
|
465
|
+
"schema": "factory.app_scaffold.v1",
|
|
466
|
+
"app": blueprint.name,
|
|
467
|
+
"out_dir": str(out_dir),
|
|
468
|
+
"files": paths,
|
|
469
|
+
"next_commands": [
|
|
470
|
+
f"specline optimize-prd {out_dir / 'PRD.md'}",
|
|
471
|
+
f"prestige brief {out_dir / 'PRD.md'} --purpose {blueprint.purpose}",
|
|
472
|
+
f"forge verify-tests {blueprint.name} {out_dir / (blueprint.name + '.ssat.yaml')} --root {out_dir}",
|
|
473
|
+
f"factory coverage --root {out_dir}",
|
|
474
|
+
f"factory optimize-pr --root {out_dir} --changed app_blueprint.json --feature {blueprint.name}",
|
|
475
|
+
],
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def app_from_prd(prd_path: Path, *, out_dir: Path | None = None, name: str | None = None,
|
|
480
|
+
stack: str = "nextjs-fastapi-postgres", purpose: str = "auto") -> dict:
|
|
481
|
+
prd_path = Path(prd_path)
|
|
482
|
+
text = prd_path.read_text(encoding="utf-8")
|
|
483
|
+
blueprint = build_blueprint(text, source=str(prd_path), name=name, stack=stack, purpose=purpose)
|
|
484
|
+
target = Path(out_dir) if out_dir else Path.cwd() / blueprint.name
|
|
485
|
+
return scaffold_app(blueprint, out_dir=target, prd_text=text)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def app_from_prompt(prompt: str, *, out_dir: Path | None = None, name: str | None = None,
|
|
489
|
+
stack: str = "nextjs-fastapi-postgres", purpose: str = "auto") -> dict:
|
|
490
|
+
prd_text = f"# {_extract_name(prompt, name).replace('-', ' ').title()}\n\n{prompt}\n"
|
|
491
|
+
blueprint = build_blueprint(prd_text, source="prompt", name=name, stack=stack, purpose=purpose)
|
|
492
|
+
target = Path(out_dir) if out_dir else Path.cwd() / blueprint.name
|
|
493
|
+
return scaffold_app(blueprint, out_dir=target, prd_text=prd_text)
|