3 Commits

Author SHA1 Message Date
59daebbd38 Template pipeline: docker-compose.yml ja .dockerignore mukaan generointiin
Jokainen generoitu projekti sisältää nyt:
- Dockerfile (oli jo)
- docker-compose.yml (uusi: build + portti 8000 + named volume)
- .dockerignore (uusi: .venv, __pycache__, *.db, .git)

Testattu: docker compose build + kontin käynnistys + API-kutsu OK.
2026-04-13 13:27:50 +03:00
42b71dbf77 Templatejen laatu: declarative_base, ConfigDict, ForeignKey
- models.py: sqlalchemy.ext.declarative → sqlalchemy.orm (poistaa
  MovedIn20Warning-varoituksen)
- schemas.py: class Config → model_config = ConfigDict() (poistaa
  PydanticDeprecatedSince20-varoituksen)
- models.py: _id-kentät saavat ForeignKey("taulu.id") kun speksissä
  on relationship-merkintä

Testattu: 10 erilaista projektia, 78/78 testiä läpi, 0 varoitusta.
2026-04-13 13:18:11 +03:00
b88a741f85 Template pipeline: JS→Python -arvomuunnokset korjattu
Ongelma: generoiduissa Python-tiedostoissa JS-booleanit (false/true)
päätyvät sellaisenaan Python-koodiin, jossa ne eivät ole valideja.
Lisäksi datetime-importit puuttuivat kun LLM antoi extra_imports-kentässä
pelkän "datetime"-merkkijonon eikä kokonaista import-lausetta.

Korjaukset:
- pyLiteral(): muuntaa JS-arvot Python-literaaleiksi (false→False jne.)
- pyJsonLiteral(): testidatan serialisointi Python-dict-muodossa
- tmplSchemas: datetime-importit tunnistetaan automaattisesti kentistä
- tmplModels + tmplSchemas: oletusarvot pyLiteral()-funktion kautta
- tmplTests: JSON.stringify korvattu pyJsonLiteral():lla
- Validaattori: tunnistaa nyt datetime-import-puutteet ja JS-booleanit

Testattu: molemmat aiemmin rikkinäiset speksit generoivat nyt toimivan
koodin — 6/6 pytest-testiä läpi molemmilla.
2026-04-13 12:44:08 +03:00
3 changed files with 80 additions and 10 deletions

View File

@@ -193,6 +193,19 @@ import Settings from "../components/Settings.astro";
if (fname === 'schemas.py') { if (fname === 'schemas.py') {
if (/:\s*date\b/.test(code) && !/from datetime import/.test(code)) if (/:\s*date\b/.test(code) && !/from datetime import/.test(code))
issues.push('ISSUE: schemas.py: käyttää date-tyyppiä mutta "from datetime import date" puuttuu'); issues.push('ISSUE: schemas.py: käyttää date-tyyppiä mutta "from datetime import date" puuttuu');
if (/:\s*datetime\b/.test(code) && !/from datetime import/.test(code))
issues.push('ISSUE: schemas.py: käyttää datetime-tyyppiä mutta "from datetime import datetime" puuttuu');
}
// 4b. Python-syntaksi: JS-booleanit (false/true ilman isoa alkukirjainta)
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/^\s*#/.test(line) || /^\s*$/.test(line)) continue;
// Etsi false/true joita ei ole merkkijonon sisällä ja jotka eivät ole osa isompaa sanaa
if (/(?<!["\w])false(?![\w"])/.test(line))
issues.push(`ISSUE: ${fname}:${i+1}: "false" ei ole Python — pitäisi olla "False"`);
if (/(?<!["\w])true(?![\w"])/.test(line))
issues.push(`ISSUE: ${fname}:${i+1}: "true" ei ole Python — pitäisi olla "True"`);
} }
// 5. test_main.py: ei saa uudelleenmääritellä appia tai modeleita // 5. test_main.py: ei saa uudelleenmääritellä appia tai modeleita
@@ -1121,6 +1134,29 @@ OUTPUT FORMAT:
// === Template Pipeline — rakennuspalaset === // === Template Pipeline — rakennuspalaset ===
// JS-arvo → Python-literaali: false→False, true→True, "teksti"→'"teksti"'
function pyLiteral(val) {
if (val === true) return 'True';
if (val === false) return 'False';
if (val === null || val === undefined) return 'None';
if (typeof val === 'string') return `"${val}"`;
return String(val);
}
// JSON-merkkijono jossa booleanit on Python-muodossa: {"a": True, "b": False}
function pyJsonLiteral(obj) {
const parts = Object.entries(obj).map(([k, v]) => {
let pyVal;
if (v === true) pyVal = 'True';
else if (v === false) pyVal = 'False';
else if (v === null) pyVal = 'None';
else if (typeof v === 'string') pyVal = `"${v}"`;
else pyVal = String(v);
return `"${k}":${pyVal}`;
});
return '{' + parts.join(',') + '}';
}
const SPEC_SYSTEM = `You are a software architect who designs database schemas for Python web applications. const SPEC_SYSTEM = `You are a software architect who designs database schemas for Python web applications.
THINK STEP BY STEP before outputting JSON: THINK STEP BY STEP before outputting JSON:
@@ -1166,20 +1202,29 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
} }
function tmplModels(spec) { function tmplModels(spec) {
// Kerää tarvittavat SA-tyypit + ForeignKey jos relaatioita
const saTypes = new Set(['Integer']); const saTypes = new Set(['Integer']);
for (const e of spec.entities) for (const f of e.fields) saTypes.add(f.sa_type.match(/^(\w+)/)[1]); for (const e of spec.entities) for (const f of e.fields) saTypes.add(f.sa_type.match(/^(\w+)/)[1]);
const relMap = {};
for (const r of (spec.relationships || [])) {
const target = spec.entities.find(e => e.name === r.to);
if (target) relMap[`${r.from}.${r.field}`] = target.table_name;
}
const hasFk = Object.keys(relMap).length > 0;
if (hasFk) saTypes.add('ForeignKey');
const imports = [...saTypes].sort().join(', '); const imports = [...saTypes].sort().join(', ');
let code = `from sqlalchemy import create_engine, Column, ${imports}\n`; let code = `from sqlalchemy import create_engine, Column, ${imports}\n`;
code += `from sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\n`; code += `from sqlalchemy.orm import declarative_base, sessionmaker\n\n`;
code += `DATABASE_URL = "sqlite:///./app.db"\n`; code += `DATABASE_URL = "sqlite:///./app.db"\n`;
code += `engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\n`; code += `engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})\n`;
code += `SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n`; code += `SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)\nBase = declarative_base()\n\n`;
for (const e of spec.entities) { for (const e of spec.entities) {
code += `class ${e.name}(Base):\n __tablename__ = "${e.table_name}"\n id = Column(Integer, primary_key=True, index=True)\n`; code += `class ${e.name}(Base):\n __tablename__ = "${e.table_name}"\n id = Column(Integer, primary_key=True, index=True)\n`;
for (const f of e.fields) { for (const f of e.fields) {
let parts = [`Column(${f.sa_type}`]; const fkTarget = relMap[`${e.name}.${f.name}`];
let parts = fkTarget ? [`Column(${f.sa_type}, ForeignKey("${fkTarget}.id")` ] : [`Column(${f.sa_type}`];
if (!f.nullable) parts.push('nullable=False'); if (!f.nullable) parts.push('nullable=False');
if (f.default !== null && f.default !== undefined) parts.push(typeof f.default === 'string' ? `default="${f.default}"` : `default=${f.default}`); if (f.default !== null && f.default !== undefined) parts.push(`default=${pyLiteral(f.default)}`);
code += ` ${f.name} = ${parts.join(', ')})\n`; code += ` ${f.name} = ${parts.join(', ')})\n`;
} }
code += '\n'; code += '\n';
@@ -1189,17 +1234,31 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
} }
function tmplSchemas(spec) { function tmplSchemas(spec) {
let code = 'from pydantic import BaseModel\n'; // Tunnista tarvittavat datetime-importit kenttätyypeistä
for (const imp of (spec.extra_imports || [])) code += imp + '\n'; const dtTypes = new Set();
for (const e of spec.entities) for (const f of e.fields) {
if (/\bdate\b/i.test(f.py_type) && !/datetime/.test(f.py_type)) dtTypes.add('date');
if (/\bdatetime\b/i.test(f.py_type)) dtTypes.add('datetime');
}
let code = 'from pydantic import BaseModel, ConfigDict\n';
if (dtTypes.size > 0) code += `from datetime import ${[...dtTypes].sort().join(', ')}\n`;
// extra_imports: suodata pois pelkät nimet kuten "datetime" (jo käsitelty yllä)
for (const imp of (spec.extra_imports || [])) {
if (/^(date|datetime)$/.test(imp.trim())) continue; // käsitelty jo
if (/^from\s/.test(imp) || /^import\s/.test(imp)) code += imp + '\n';
}
code += '\n'; code += '\n';
for (const e of spec.entities) { for (const e of spec.entities) {
code += `class ${e.name}Create(BaseModel):\n`; code += `class ${e.name}Create(BaseModel):\n`;
for (const f of e.fields) { for (const f of e.fields) {
if (f.default !== null && f.default !== undefined) code += ` ${f.name}: ${f.py_type} = ${typeof f.default === 'string' ? '"'+f.default+'"' : f.default}\n`; if (f.default !== null && f.default !== undefined) code += ` ${f.name}: ${f.py_type} = ${pyLiteral(f.default)}\n`;
else if (f.nullable && f.py_type.includes('None')) code += ` ${f.name}: ${f.py_type} = None\n`; else if (f.nullable && f.py_type.includes('None')) code += ` ${f.name}: ${f.py_type} = None\n`;
else code += ` ${f.name}: ${f.py_type}\n`; else code += ` ${f.name}: ${f.py_type}\n`;
} }
code += `\nclass ${e.name}Response(${e.name}Create):\n id: int\n\n class Config:\n from_attributes = True\n\n`; code += `\nclass ${e.name}Response(${e.name}Create):\n id: int\n model_config = ConfigDict(from_attributes=True)\n\n`;
} }
return code; return code;
} }
@@ -1253,11 +1312,11 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
else if (f.py_type.includes('bool')) testData[f.name] = true; else if (f.py_type.includes('bool')) testData[f.name] = true;
else if (f.py_type.includes('date')) testData[f.name] = '2024-01-15'; else if (f.py_type.includes('date')) testData[f.name] = '2024-01-15';
} }
const td = JSON.stringify(testData); const td = pyJsonLiteral(testData);
const firstStr = e.fields.find(f => f.py_type.includes('str') && f.name !== 'status'); const firstStr = e.fields.find(f => f.py_type.includes('str') && f.name !== 'status');
const updateData = {...testData}; const updateData = {...testData};
if (firstStr) updateData[firstStr.name] = `Updated ${firstStr.name}`; if (firstStr) updateData[firstStr.name] = `Updated ${firstStr.name}`;
const ud = JSON.stringify(updateData); const ud = pyJsonLiteral(updateData);
code += `def test_create_${lo}():\n response = client.post('/${tb}/', json=${td})\n assert response.status_code == 201\n assert 'id' in response.json()\n\n`; code += `def test_create_${lo}():\n response = client.post('/${tb}/', json=${td})\n assert response.status_code == 201\n assert 'id' in response.json()\n\n`;
code += `def test_list_${lo}s():\n client.post('/${tb}/', json=${td})\n response = client.get('/${tb}/')\n assert response.status_code == 200\n assert len(response.json()) >= 1\n\n`; code += `def test_list_${lo}s():\n client.post('/${tb}/', json=${td})\n response = client.get('/${tb}/')\n assert response.status_code == 200\n assert len(response.json()) >= 1\n\n`;
@@ -1278,6 +1337,15 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
return `FROM python:3.12-slim\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\nENV UV_CACHE_DIR=/tmp/uv-cache\nWORKDIR /app\nCOPY pyproject.toml .\nRUN uv sync\nCOPY *.py .\nRUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache\nUSER appuser\nEXPOSE 8000\nCMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]\n`; return `FROM python:3.12-slim\nCOPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv\nENV UV_CACHE_DIR=/tmp/uv-cache\nWORKDIR /app\nCOPY pyproject.toml .\nRUN uv sync\nCOPY *.py .\nRUN useradd -m appuser && chown -R appuser:appuser /app /tmp/uv-cache\nUSER appuser\nEXPOSE 8000\nCMD ["uv", "run", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]\n`;
} }
function tmplDockerCompose(spec) {
const name = (spec.project_name || 'app').toLowerCase().replace(/[^a-z0-9-]/g, '-');
return `services:\n app:\n build: .\n container_name: ${name}\n ports:\n - "8000:8000"\n volumes:\n - app-data:/app/data\n restart: unless-stopped\n\nvolumes:\n app-data:\n`;
}
function tmplDockerignore() {
return `.venv\n__pycache__\n*.pyc\n*.db\n.pytest_cache\n.git\n`;
}
function tmplGenerate(spec) { function tmplGenerate(spec) {
return { return {
'models.py': tmplModels(spec), 'models.py': tmplModels(spec),
@@ -1286,6 +1354,8 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
'test_main.py': tmplTests(spec), 'test_main.py': tmplTests(spec),
'pyproject.toml': tmplPyproject(spec), 'pyproject.toml': tmplPyproject(spec),
'Dockerfile': tmplDockerfile(), 'Dockerfile': tmplDockerfile(),
'docker-compose.yml': tmplDockerCompose(spec),
'.dockerignore': tmplDockerignore(),
}; };
} }
@@ -1326,7 +1396,7 @@ Blog → Author: name,email,bio(Text|None) / Post: title, content(Text), author_
// === Vaihe 3: Koodigenerointi templateista === // === Vaihe 3: Koodigenerointi templateista ===
const files = tmplGenerate(spec); const files = tmplGenerate(spec);
const fileOrder = Object.keys(files); const fileOrder = Object.keys(files);
const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester' }; const agentMap = { 'models.py': 'data', 'schemas.py': 'coder', 'main.py': 'coder', 'test_main.py': 'qa', 'pyproject.toml': 'coder', 'Dockerfile': 'tester', 'docker-compose.yml': 'tester', '.dockerignore': 'tester' };
const agentNames = { data: 'Data Engineer', coder: 'Coder', qa: 'QA', tester: 'DevOps' }; const agentNames = { data: 'Data Engineer', coder: 'Coder', qa: 'QA', tester: 'DevOps' };
for (let i = 0; i < fileOrder.length; i++) { for (let i = 0; i < fileOrder.length; i++) {

Binary file not shown.

Binary file not shown.