diff --git a/backend/elena_app.py b/backend/elena_app.py index 713682d..8f7764d 100644 --- a/backend/elena_app.py +++ b/backend/elena_app.py @@ -877,6 +877,42 @@ def build_spec(): con = db(); con.execute("UPDATE projects SET status='spec_ready' WHERE id=?", (proj["id"],)); con.commit() return jsonify({"spec": result, "usage": usage}) +@app.route("/api/build-spec-client", methods=["POST"]) +def build_spec_client(): + """ТЗ под вариант клиента: полная пересборка с учётом зафиксированных отклонений и орг. слоя.""" + data = request.get_json(force=True) or {} + proj = get_project(data.get("token")) + if not proj: + return jsonify({"error": "project not found"}), 404 + pid = proj["id"] + model_row = db().execute("SELECT blocks_json FROM models WHERE project_id=? ORDER BY id DESC LIMIT 1", (pid,)).fetchone() + if not model_row: + return jsonify({"error": "сначала постройте IDEF0 модель"}), 400 + devs = get_deviations(pid) + if not devs: + return jsonify({"error": "нет зафиксированных отклонений — вариант клиента совпадает с эталоном"}), 400 + idef0 = model_row["blocks_json"] + extra = [] + org = latest_artifact(pid, "orgchart") + if org: + extra.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000]) + jobs = latest_artifact(pid, "jobs") + if jobs: + extra.append("ДОЛЖНОСТНЫЕ ИНСТРУКЦИИ:\n" + json.dumps(jobs, ensure_ascii=False)[:3000]) + extra.append("ЗАФИКСИРОВАННЫЕ ОТКЛОНЕНИЯ КЛИЕНТА (реализуем ИХ вариант, не эталон):\n" + json.dumps(devs, ensure_ascii=False)[:3000]) + instruction = ("Собери ТЗ на программу под РЕАЛЬНЫЙ вариант клиента (с его отклонениями), а не под методологический эталон.\n" + "Базис — интервью и IDEF0, НО там, где клиент настоял на своём (см. отклонения) — проектируй под выбор клиента.\n" + "Например, если клиент оставил совмещение склада и пошива — модули и роли должны это отражать (один человек, общий доступ), " + "но в open_questions честно вынеси риски, о которых предупреждала Елена.\n" + "МАППИНГ: функция → модуль; Input → данные; Output → показ; Control → правила; Mechanism → роли; хранилища → таблицы.\n" + "Думай как проектировщик ПО. Вызови build_tech_spec.\n\n" + f"IDEF0-МОДЕЛЬ:\n{idef0}\n\n" + "\n\n".join(extra)) + result, usage = run_tool(pid, SPEC_TOOL, "build_tech_spec", instruction, max_tokens=8192) + if result is None: + return jsonify({"error": usage}), 500 + save_artifact(pid, "spec_client", result) + return jsonify({"spec_client": result, "usage": usage}) + @app.route("/api/project/crm", methods=["POST"]) def update_crm(): data = request.get_json(force=True) or {} @@ -1298,6 +1334,7 @@ def list_projects(): "has_selection": latest_artifact(pid, "selection") is not None, "has_canvas": latest_artifact(pid, "canvas") is not None, "has_idef0": db().execute("SELECT 1 FROM models WHERE project_id=? LIMIT 1", (pid,)).fetchone() is not None, + "has_org": latest_artifact(pid, "orgchart") is not None, "has_spec": latest_artifact(pid, "spec") is not None, "approvals": latest_artifact(pid, "approvals") or {}, "crm": latest_artifact(pid, "crm") or {"pipeline":"lead","deal_amount":0,"paid_amount":0,"contact":"","source":"","note":""}, @@ -1434,6 +1471,7 @@ def get_project_state(token): "orgchart": latest_artifact(proj["id"], "orgchart"), "jobs": latest_artifact(proj["id"], "jobs"), "spec": latest_artifact(proj["id"], "spec"), + "spec_client": latest_artifact(proj["id"], "spec_client"), "approvals": latest_artifact(proj["id"], "approvals") or {}, "crm": crm, "unlocked": unlocked, diff --git a/docs/cabinet.html b/docs/cabinet.html index 58be971..bbbbd7f 100644 --- a/docs/cabinet.html +++ b/docs/cabinet.html @@ -665,6 +665,7 @@ const EST_STAGES=[ {key:"methods", name:"Методологии", icon:"🎯", desc:"Подбор фреймворков под вашу задачу"}, {key:"canvas", name:"Стратегия", icon:"📊", desc:"Целевая модель процессов (TO-BE)"}, {key:"idef0", name:"Функции IDEF0", icon:"🔧", desc:"Декомпозиция функций + регламенты"}, + {key:"org", name:"Организация", icon:"🏢", desc:"Оргструктура + должностные инструкции"}, {key:"spec", name:"Выдача ТЗ", icon:"📋", desc:"Готовое ТЗ к внедрению"}, ]; function estStageDone(k){ @@ -672,6 +673,7 @@ function estStageDone(k){ if(k==="methods") return !!state.selection; if(k==="canvas") return !!state.canvas; if(k==="idef0") return !!state.model; + if(k==="org") return !!(state.orgchart && state.jobs); if(k==="spec") return !!state.spec; return false; } @@ -732,13 +734,15 @@ function buildTZmd(s){ (s.roles||[]).forEach(r=>m+=`- **${r.name}** — ${r.does} (доступ: ${r.access})\n`); m+=`\n## B. Модули\n`;(s.modules||[]).forEach(x=>{m+=`### ${x.name} [${x.source_node}]\n${x.purpose}\nЭкраны: ${(x.screens||[]).join(', ')}\nДанные: вход — ${x.inputs_data}; выход — ${x.outputs_data}\n`;(x.rules||[]).forEach(r=>m+=`- правило: ${r}\n`);m+=`\n`}); m+=`## C. Модель данных\n`;(s.entities||[]).forEach(e=>{m+=`### ${e.name}\n`;(e.fields||[]).forEach(f=>m+=`- ${f.field}: ${f.type}\n`);if((e.relations||[]).length)m+=`Связи: ${e.relations.join(' · ')}\n`;m+=`Пример: ${e.example}\n\n`}); + if(state.orgchart&&(state.orgchart.units||[]).length){m+=`\n## D. Оргструктура\n${state.orgchart.insight||''}\n`;state.orgchart.units.forEach(u=>{m+=`- **${u.role}** (${(+u.headcount||1)} чел.)${u.reports_to&&u.reports_to!=='—'?` ↑ ${u.reports_to}`:''}${(u.owns_functions||[]).length?` — отвечает: ${u.owns_functions.join(', ')}`:''}${u.note?` ⚠ ${u.note}`:''}\n`})} + if(state.jobs&&(state.jobs.roles||[]).length){m+=`\n## E. Должностные инструкции\n`;state.jobs.roles.forEach(r=>{m+=`### ${r.role}\n${r.purpose||''}${r.reports_to?` (подчинение: ${r.reports_to})`:''}\n`;(r.responsibilities||[]).forEach(x=>m+=`- ${x}\n`);if((r.kpis||[]).length)m+=`KPI: ${r.kpis.join(' · ')}\n`;if((r.authority||[]).length)m+=`Полномочия: ${r.authority.join(' · ')}\n`;if(r.deviation_note)m+=`⚠ Учтено пожелание клиента: ${r.deviation_note}\n`;m+=`\n`})} if((s.open_questions||[]).length){m+=`## Уточнить перед разработкой\n`;s.open_questions.forEach(q=>m+=`- ${q}\n`)} return m; } function dl(name,text,type){const b=new Blob([text],{type:type||'text/plain;charset=utf-8'});const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(a.href),2000)} function printDoc(){if(!state.unlocked)return alert('Доступно после оплаты');window.print()} function downloadTZ(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.md`,buildTZmd(state.spec),'text/markdown;charset=utf-8')} -function exportDev(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.json`,JSON.stringify({client:state.client_name,niche:state.niche,model:state.model,spec:state.spec},null,2),'application/json')} +function exportDev(){if(!state.unlocked)return alert('Доступно после оплаты');dl(`ТЗ_${(state.client_name||'проект').replace(/\s+/g,'_')}.json`,JSON.stringify({client:state.client_name,niche:state.niche,model:state.model,orgchart:state.orgchart,jobs:state.jobs,spec:state.spec},null,2),'application/json')} function runCard(stage,ic,t,d,btn,custom){ const id=stage?`id="rb-${stage}"`:''; const onclick=custom?'':`onclick="runBuild('${stage}')"`; diff --git a/docs/crm.html b/docs/crm.html index 885e726..222e627 100644 --- a/docs/crm.html +++ b/docs/crm.html @@ -199,7 +199,7 @@ function setClientFilter(f){ui.clientFilter=f;saveUi();renderDashboard();} function setTaskFilter(f){ui.taskFilter=f;saveUi();renderDashboard();} function setClientSearch(v){ui.clientSearch=v;renderDashboard();const el=document.getElementById('clientSearch');if(el){el.focus();const L=el.value.length;try{el.setSelectionRange(L,L);}catch(e){}}} function setClientSort(k){if(ui.clientSort===k)ui.clientSortDir=(ui.clientSortDir||1)*-1;else{ui.clientSort=k;ui.clientSortDir=1;}saveUi();renderDashboard();} -function projStageCnt(p){return [p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_spec].filter(Boolean).length;} +function projStageCnt(p){return [p.msg_count>0,!!p.has_selection,!!p.has_canvas,!!p.has_idef0,!!p.has_org,!!p.has_spec].filter(Boolean).length;} function secHead(k,title,right){ const col=!!ui.collapsed[k]; return `
${esc(s.overview)}
`; - body+=`| Поле | Тип |
|---|---|
| ${esc(f.field)} | ${esc(f.type)} |
${esc(s.overview)}
`; + body+=H("Роли пользователей");s.roles.forEach(r=>body+=`| Поле | Тип |
|---|---|
| ${esc(f.field)} | ${esc(f.type)} |