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.
@@ -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)