@atmem/architect 0.2.0 → 0.2.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/README.md +3 -1
- package/cli/runtime.mjs +3 -1
- package/dist/assets/{index-CQ2C1gKF.js → index-D4CZ_n8f.js} +20473 -20468
- package/dist/assets/index-DuveVTIx.css +1 -0
- package/dist/assets/{index.es-DTyqIdaO.js → index.es-Fcqg-orY.js} +1 -1
- package/dist/assets/{visual-export-Ox0j5xj5.js → visual-export-4PDDTAP7.js} +3 -3
- package/dist/index.html +2 -2
- package/dist/licenses/memory-dependency-notices.txt +149 -2241
- package/package.json +2 -2
- package/public/licenses/memory-dependency-notices.txt +149 -2241
- package/server/memory-requirements.lock +6 -6
- package/server/memory-requirements.txt +2 -2
- package/server/memory_service.py +3 -1
- package/server/memory_worker.py +4 -2
- package/server/pricing.py +55 -1
- package/server/request_uploads.py +65 -0
- package/server/terraform_routes.py +47 -1
- package/src/EngineeringPanel.jsx +14 -7
- package/src/FinalChecklist.jsx +11 -3
- package/src/ImplementationDelivery.jsx +10 -7
- package/src/JourneyViews.jsx +4 -4
- package/src/OutputsView.jsx +14 -8
- package/src/ProjectChecks.jsx +35 -21
- package/src/StarPrompt.jsx +26 -0
- package/src/engineering.css +10 -0
- package/src/journey-workspace.css +5 -0
- package/src/large-request.mjs +25 -0
- package/src/main.jsx +17 -9
- package/src/output-bundle.mjs +16 -4
- package/src/pricing.mjs +6 -4
- package/src/project-file.mjs +1 -1
- package/dist/assets/index-BTLHaa3J.css +0 -1
package/src/ProjectChecks.jsx
CHANGED
|
@@ -1,43 +1,57 @@
|
|
|
1
|
-
import React,{useEffect,useMemo,useRef,useState}from'react';
|
|
1
|
+
import React,{useEffect,useMemo,useRef,useState,useId}from'react';
|
|
2
|
+
import{postLargeJson}from'./large-request.mjs';
|
|
2
3
|
import{BrainActivity}from'./BrainActivity.jsx';
|
|
3
4
|
import{engineeringCall}from'./engineering-client.mjs';
|
|
4
5
|
import{terraformSketch}from'./terraform.mjs';
|
|
5
6
|
import{verifiedReceipt}from'./validation-receipt.mjs';
|
|
6
|
-
export function ProjectChecks({project,environment,session,onReceipt,onBusy,receipt}){
|
|
7
|
-
const [report,setReport]=useState(null),[catalog,setCatalog]=useState([]),[checks,setChecks]=useState([]),[result,setResult]=useState(null),[working,setWorking]=useState(false),[error,setError]=useState('');
|
|
7
|
+
export function ProjectChecks({project,environment,session,onReceipt,onBusy,receipt,generationPending=false}){
|
|
8
|
+
const [report,setReport]=useState(null),[catalogReady,setCatalogReady]=useState(false),[catalog,setCatalog]=useState([]),[checks,setChecks]=useState([]),[result,setResult]=useState(null),[working,setWorking]=useState(false),[error,setError]=useState('');
|
|
9
|
+
const hintId=useId();
|
|
10
|
+
const [uploadProgress,setUploadProgress]=useState(null);
|
|
8
11
|
const active=useRef(null),locked=useRef(false),files=useMemo(()=>terraformSketch(project,environment),[project,environment]),key=JSON.stringify(files);
|
|
9
12
|
useEffect(()=>{let stopped=false;setResult(null);active.current?.abort();let previous=receipt;try{previous=previous||JSON.parse(localStorage.getItem('atmem-tf-receipt:'+project.id)||'null');}catch{}void verifiedReceipt(files,previous).then(value=>{if(!stopped){setResult(value);onReceipt?.(value);}});return()=>{stopped=true;active.current?.abort();};},[key]);
|
|
10
13
|
useEffect(()=>{
|
|
14
|
+
setReport(null);setCatalog([]);setChecks([]);setCatalogReady(false);
|
|
11
15
|
if(!session?.engineeringEnabled)return;
|
|
12
16
|
let stopped=false,timer;const controller=new AbortController();
|
|
13
17
|
async function poll(){try{
|
|
14
18
|
const value=await engineeringCall(session,'delivery',{projectId:project.id},{signal:controller.signal});if(stopped)return;setReport(value);
|
|
15
|
-
if(value.artifact){const payload={projectId:project.id,sessionId:value.session.id};const [definitions,results]=await Promise.all([engineeringCall(session,'validators',payload,{signal:controller.signal}),engineeringCall(session,'validation/list',{...payload,artifactId:value.artifact.id},{signal:controller.signal})]);if(!stopped){setCatalog(definitions.validators||[]);setChecks(results.checks||[]);}}
|
|
19
|
+
if(value.artifact){const payload={projectId:project.id,sessionId:value.session.id};const [definitions,results]=await Promise.all([engineeringCall(session,'validators',payload,{signal:controller.signal}),engineeringCall(session,'validation/list',{...payload,artifactId:value.artifact.id},{signal:controller.signal})]);if(!stopped){setCatalog(definitions.validators||[]);setChecks(results.checks||[]);setCatalogReady(true);}}else{setCatalog([]);setChecks([]);setCatalogReady(false);}
|
|
16
20
|
}catch(e){if(!stopped&&e.name!=='AbortError'&&e.status!==404)setError(e.message);}finally{if(!stopped)timer=setTimeout(poll,2000);}}
|
|
17
21
|
void poll();return()=>{stopped=true;clearTimeout(timer);controller.abort();};
|
|
18
22
|
},[project.id,session?.token,session?.engineeringEnabled]);
|
|
19
23
|
const running=checks.some(c=>c.state!=='finished'&&c.state!=='recovery_required');
|
|
20
|
-
useEffect(()=>{onBusy?.(working||running);return()=>onBusy?.(false);},[working,running,onBusy]);
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
useEffect(()=>{onBusy?.(!!working||running);return()=>onBusy?.(false);},[working,running,onBusy]);
|
|
25
|
+
const available=catalog.filter(v=>v.available!==false);
|
|
26
|
+
async function run(kind){
|
|
27
|
+
if(generationPending||locked.current||!session?.token||kind==='application'&&(!report?.artifact||!available.length))return;
|
|
28
|
+
locked.current=true;setWorking(kind);setError('');const controller=new AbortController();active.current=controller;
|
|
23
29
|
try{
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
if(kind==='application'){
|
|
31
|
+
const payload={projectId:project.id,sessionId:report.session.id,artifactId:report.artifact.id};
|
|
32
|
+
await Promise.all(available.map(validator=>{
|
|
33
|
+
const previous=checks.filter(c=>c.validatorId===validator.id&&c.contractSha256===validator.contractSha256).at(-1);
|
|
34
|
+
if(previous&&previous.state!=='finished')return;
|
|
35
|
+
return engineeringCall(session,'validation/start',{...payload,validatorId:validator.id,retry:!!previous},{signal:controller.signal});
|
|
36
|
+
}));
|
|
37
|
+
const results=await engineeringCall(session,'validation/list',payload,{signal:controller.signal});
|
|
38
|
+
if(!controller.signal.aborted)setChecks(results.checks||[]);
|
|
39
|
+
}else{
|
|
40
|
+
const value=await postLargeJson('/api/terraform/validate',{project,environment},{token:session.token,signal:AbortSignal.any([controller.signal,AbortSignal.timeout(500000)]),onProgress:setUploadProgress});
|
|
41
|
+
const verified=await verifiedReceipt(files,value);if(!controller.signal.aborted){setResult(value);onReceipt?.(verified);try{if(verified)localStorage.setItem('atmem-tf-receipt:'+project.id,JSON.stringify(verified));else localStorage.removeItem('atmem-tf-receipt:'+project.id);}catch{}}
|
|
29
42
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if(!response.ok)throw Error(value.detail||'Terraform checks could not start.');
|
|
33
|
-
const verified=await verifiedReceipt(files,value);if(!controller.signal.aborted){setResult(value);onReceipt?.(verified);try{if(verified)localStorage.setItem('atmem-tf-receipt:'+project.id,JSON.stringify(verified));else localStorage.removeItem('atmem-tf-receipt:'+project.id);}catch{}}
|
|
34
|
-
const outcomes=await outcomesPending;const failed=outcomes.find(o=>o.status==='rejected');if(failed)throw failed.reason;
|
|
35
|
-
}catch(e){if(!controller.signal.aborted){setResult({status:'failed',message:e.message});setError(e.message);}}
|
|
36
|
-
finally{locked.current=false;setWorking(false);}
|
|
43
|
+
}catch(e){if(!controller.signal.aborted){setError(e.message);}}
|
|
44
|
+
finally{locked.current=false;setWorking(false);setUploadProgress(null);}
|
|
37
45
|
}
|
|
38
46
|
const label=status=>({passed:'Passed',failed:'Failed',incomplete:'Incomplete',unavailable:'Not run'}[status]||'Not run');
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
const terraformHint=generationPending?'Project files are being generated. Wait until generation finishes.':'Validate the Terraform draft from your saved design.';
|
|
48
|
+
const applicationHint=generationPending?'Project files are being generated. Wait until generation finishes.':!report?.artifact?'Generate project files first.':!catalogReady?'Loading available application tests.':!available.length?'No application tests are configured.':'Run the generated application tests.';
|
|
49
|
+
const applicationStatus=generationPending?'Generating files…':!report?.artifact?'Generate files first':working==='application'||running?'Running…':checks.length?checks.every(c=>c.state==='finished'&&c.result?.status==='passed')?'Passed':'Needs attention':!catalogReady?'Loading…':available.length?'Not run':'Not configured';
|
|
50
|
+
return <section className="project-checks" aria-label="Project checks"><div className="section-action"><h3>Checks</h3></div>
|
|
51
|
+
<ul className="check-rows">
|
|
52
|
+
<li><span>Terraform validation</span><strong className={result?.status==='passed'?'check-complete':'check-pending'}>{working==='terraform'?uploadProgress!==null?`Uploading ${uploadProgress}%`:'Running…':label(result?.status)}</strong><span className="check-run-action" title={terraformHint} tabIndex={generationPending?0:undefined} aria-label={terraformHint}><button className="btn" aria-label="Run Terraform validation" title={terraformHint} disabled={generationPending||!session?.token||!!working||running} onClick={()=>run('terraform')}>{working==='terraform'?<><BrainActivity size={18}/> Running…</>:'Run'}</button></span></li>
|
|
53
|
+
<li><span>Application tests</span><strong id={hintId} className={applicationStatus==='Passed'?'check-complete':'check-pending'}>{applicationStatus}</strong><span className="check-run-action" title={applicationHint} tabIndex={generationPending||!report?.artifact||!available.length?0:undefined} aria-label={applicationHint}><button className="btn" aria-label="Run application tests" aria-describedby={hintId} title={applicationHint} disabled={generationPending||!session?.token||!!working||running||!report?.artifact||!available.length} onClick={()=>run('application')}>{working==='application'||running?<><BrainActivity size={18}/> Running…</>:'Run'}</button></span></li>
|
|
54
|
+
</ul>
|
|
41
55
|
{(result||checks.length>0)&&<details><summary>Check details</summary>{result&&<><p>{result.message}</p>{result.checks?.map((c,i)=><details key={i}><summary>{c.name} · {c.passed?'Passed':'Failed'}</summary><pre>{c.output}</pre></details>)}</>}{checks.map(c=><details key={c.id}><summary>{c.label}</summary><pre>{c.result?.output||c.result?.reason||c.state}</pre></details>)}</details>}
|
|
42
56
|
{error&&<p role="alert">{error}</p>}
|
|
43
57
|
</section>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React,{useEffect,useRef}from'react';
|
|
2
|
+
import{Star,X}from'lucide-react';
|
|
3
|
+
export const STAR_URL='https://github.com/aetna000/atmem-ai-architect';
|
|
4
|
+
const KEY='atmem-architect-star-prompt-v1',SNOOZE=30*24*60*60*1000;
|
|
5
|
+
export function shouldOfferStar(value,now=Date.now()){
|
|
6
|
+
if(!value)return true;
|
|
7
|
+
try{const record=JSON.parse(value);return record.choice!=='opened'&&(!Number.isFinite(record.after)||record.after<=now);}catch{return true;}
|
|
8
|
+
}
|
|
9
|
+
export function StarPrompt({offer=0}){
|
|
10
|
+
const dialog=useRef(null),choice=useRef('later');
|
|
11
|
+
function dismiss(value='later'){
|
|
12
|
+
choice.current=value;
|
|
13
|
+
try{localStorage.setItem(KEY,JSON.stringify({choice:value,after:Date.now()+SNOOZE}));}catch{}
|
|
14
|
+
dialog.current?.close();
|
|
15
|
+
}
|
|
16
|
+
useEffect(()=>{
|
|
17
|
+
if(!offer)return;
|
|
18
|
+
let allowed=true;try{allowed=shouldOfferStar(localStorage.getItem(KEY));}catch{}
|
|
19
|
+
if(allowed&&!dialog.current.open){choice.current='later';dialog.current.showModal();}
|
|
20
|
+
},[offer]);
|
|
21
|
+
return <dialog ref={dialog} className="star-prompt" aria-labelledby="star-prompt-title" onCancel={event=>{event.preventDefault();dismiss();}}>
|
|
22
|
+
<header><h2 id="star-prompt-title"><Star size={22} aria-hidden="true"/> Enjoying Architect?</h2><button type="button" className="icon" aria-label="Close star invitation" onClick={()=>dismiss()}><X size={20}/></button></header>
|
|
23
|
+
<p>If Architect helped, a GitHub star helps others find it.</p>
|
|
24
|
+
<div className="star-prompt-actions"><a className="btn primary" href={STAR_URL} target="_blank" rel="noopener noreferrer" onClick={()=>dismiss('opened')}><Star size={18} aria-hidden="true"/> Star on GitHub</a><button type="button" className="btn" onClick={()=>dismiss()}>Later</button></div>
|
|
25
|
+
</dialog>;
|
|
26
|
+
}
|
package/src/engineering.css
CHANGED
|
@@ -29,3 +29,13 @@
|
|
|
29
29
|
.architecture-work > ul { max-height: 24rem; overflow: auto; padding-left: 1.3rem; }
|
|
30
30
|
.architecture-work pre { white-space: pre-wrap; overflow-wrap: anywhere; font-size: .8rem; }
|
|
31
31
|
.architecture-work small { display: block; }
|
|
32
|
+
|
|
33
|
+
.generation-progress { height: 8px; overflow: hidden; border-radius: 999px; background: var(--line, #e1e6ee); margin: 1rem 0 .5rem; }
|
|
34
|
+
.generation-progress > span { display: block; height: 100%; width: 35%; border-radius: inherit; background: var(--action, #f3a2b4); animation: generation-sweep 1.8s ease-in-out infinite; }
|
|
35
|
+
@keyframes generation-sweep { from { transform: translateX(-80%); } to { transform: translateX(266%); } }
|
|
36
|
+
@media (prefers-reduced-motion: reduce) { .generation-progress > span { animation: none; width: 100%; opacity: .65; } }
|
|
37
|
+
|
|
38
|
+
.engineering-progress-count { display: inline-block; font-variant-numeric: tabular-nums; transform-origin: center; }
|
|
39
|
+
.engineering-progress-count.updated { animation: progress-count-pulse 360ms ease-out; }
|
|
40
|
+
@keyframes progress-count-pulse { 0% { opacity: .55; transform: scale(.92); } 45% { opacity: 1; transform: scale(1.1); } 100% { transform: scale(1); } }
|
|
41
|
+
@media (prefers-reduced-motion: reduce) { .engineering-progress-count.updated { animation: none; } }
|
|
@@ -104,3 +104,8 @@
|
|
|
104
104
|
.memory-modal{height:auto!important;max-height:88vh!important}.memory-profile-bar input,.memory-toolbar input,.memory-toolbar select,.memory-preference-form input,.memory-preference-form select{font:inherit;font-size:.85rem;min-height:36px;box-sizing:border-box;padding:7px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink)}.memory-table th:last-child{width:84px;white-space:nowrap}.memory-table td:last-child{white-space:nowrap}.memory-table th:nth-child(4){width:90px}.memory-table th:nth-child(5){width:130px}.memory-table th:nth-child(3){width:84px}.memory-toolbar>strong{color:var(--muted);font-weight:500}.memory-pagination .btn{min-height:32px}.memory-record-detail .memory-table{table-layout:auto}.memory-table .memory-content:hover{text-decoration:underline;text-underline-offset:3px}
|
|
105
105
|
|
|
106
106
|
.location-region-form{display:flex;align-items:flex-end;gap:12px}.location-region-form label{display:flex;flex-direction:column;gap:6px}.location-region-form input{font:inherit;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink)}
|
|
107
|
+
|
|
108
|
+
.final-checklist li>button:not(:disabled),.final-checklist li>button:not(:disabled) *{cursor:pointer}.final-checklist[aria-busy="true"] li>button,.final-checklist[aria-busy="true"] li>button *{cursor:wait}.final-checklist li>button:disabled{opacity:1}.checklist-label{display:flex;flex-direction:column;gap:4px;min-width:0}.checklist-label small{font-size:.75rem;color:var(--muted);font-weight:400;overflow-wrap:anywhere}.checklist-loading{font-size:.85rem;color:var(--muted)}
|
|
109
|
+
.star-prompt{width:min(460px,calc(100vw - 32px));max-width:460px;border:1px solid var(--line);border-radius:16px;padding:22px;background:var(--paper);color:var(--ink);box-shadow:0 18px 60px #0003}.star-prompt::backdrop{background:#0005}.star-prompt header,.star-prompt h2,.star-prompt-actions{display:flex;align-items:center;gap:10px}.star-prompt header{justify-content:space-between}.star-prompt h2{font-size:1.2rem}.star-prompt h2>svg{color:var(--accent);fill:var(--accent-soft)}.star-prompt p{margin:18px 0;line-height:1.5}.star-prompt-actions>a{flex:1;text-decoration:none}.star-prompt-actions>button{min-width:90px}
|
|
110
|
+
|
|
111
|
+
.check-run-action{display:inline-flex;margin-left:12px;flex-shrink:0}.check-run-action>button:disabled{pointer-events:none}.check-run-action:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Preserve the entire JSON payload; chunk transport changes no project content.
|
|
2
|
+
export async function postLargeJson(url,payload,{token,signal,onProgress,fetcher=fetch}={}){
|
|
3
|
+
const data=new TextEncoder().encode(JSON.stringify(payload));
|
|
4
|
+
if(data.length>64*1024*1024)throw Error('Project exceeds the 64 MiB validation upload limit.');
|
|
5
|
+
const post=async(path,value,requestSignal=signal)=>{
|
|
6
|
+
const response=await fetcher(path,{method:'POST',headers:{'Content-Type':'application/json',Authorization:`Bearer ${token}`},body:JSON.stringify(value),signal:requestSignal});
|
|
7
|
+
const result=await response.json();if(!response.ok)throw Error(result.detail||'Validation request could not start.');return result;
|
|
8
|
+
};
|
|
9
|
+
if(data.length<=192000)return post(url,payload);
|
|
10
|
+
const sha256=[...new Uint8Array(await crypto.subtle.digest('SHA-256',data))].map(x=>x.toString(16).padStart(2,'0')).join('');
|
|
11
|
+
const {uploadId,chunkBytes}=await post('/api/terraform/uploads/start',{bytes:data.length,sha256});
|
|
12
|
+
try{
|
|
13
|
+
if(chunkBytes!==131072)throw Error('The upload service returned an unsupported chunk size.');
|
|
14
|
+
for(let offset=0,index=0;offset<data.length;offset+=chunkBytes,index++){
|
|
15
|
+
const chunk=data.subarray(offset,offset+chunkBytes);let text='';
|
|
16
|
+
for(let i=0;i<chunk.length;i+=8192)text+=String.fromCharCode(...chunk.subarray(i,i+8192));
|
|
17
|
+
await post('/api/terraform/uploads/chunk',{uploadId,index,data:btoa(text)});
|
|
18
|
+
onProgress?.(Math.round(Math.min(offset+chunkBytes,data.length)/data.length*100));
|
|
19
|
+
}
|
|
20
|
+
onProgress?.(null);
|
|
21
|
+
return await post(url+'-upload',{uploadId});
|
|
22
|
+
}finally{
|
|
23
|
+
await post('/api/terraform/uploads/cancel',{uploadId},AbortSignal.timeout(5000)).catch(()=>{});
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/main.jsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {StarPrompt,STAR_URL} from './StarPrompt.jsx';
|
|
2
|
+
import {prefillAcceptedSettings} from './engineering-defaults.mjs';
|
|
1
3
|
import {LocationChoices} from './LocationChoices.jsx';
|
|
2
4
|
import {InlineNote} from './HelpNote.jsx';
|
|
3
5
|
import {ProjectChecks} from './ProjectChecks.jsx';
|
|
@@ -663,6 +665,7 @@ document.documentElement.dataset.theme = savedTheme();
|
|
|
663
665
|
function readChatView(id){try{const value=JSON.parse(localStorage.getItem('atmem-chat-view:'+id)||'null');return value&&Number.isSafeInteger(value.start)&&value.start>=0&&Array.isArray(value.turnIds)?value:{start:0,turnIds:[]};}catch{return {start:0,turnIds:[]};}}
|
|
664
666
|
const pricingSignature=p=>JSON.stringify([p.id,p.estimate?.projectSize||'small',p.estimate?.usage,p.facts,p.decisions?.filter(d=>d.status==='Accepted'&&['cloud.region','data.country'].includes(d.subject)),[...storageNodes(p),...lambdaNodes(p),...googleNodes(p)].map(n=>[n.id,n.data.provider,n.data.service,n.data.config])]);
|
|
665
667
|
function App() {
|
|
668
|
+
const [starOffer,setStarOffer]=useState(0);
|
|
666
669
|
const [project, setProject] = useState(readSaved);
|
|
667
670
|
const projectRef = useRef(project);
|
|
668
671
|
projectRef.current = project;
|
|
@@ -749,7 +752,8 @@ function App() {
|
|
|
749
752
|
const pricingKey=pricingSignature(project);
|
|
750
753
|
async function refreshEstimate(selected){
|
|
751
754
|
if(!session?.token)return;
|
|
752
|
-
const
|
|
755
|
+
const resolved=prefillAcceptedSettings(selected);
|
|
756
|
+
const requested=resolved.estimate?.projectSize?resolved:setProjectSize(resolved,'small');
|
|
753
757
|
if(requested!==selected)updateEngagement(requested);
|
|
754
758
|
const request=++pricingRequest.current;pricingAttempt.current.add(pricingSignature(requested));
|
|
755
759
|
const baseline=JSON.stringify([requested.estimate?.prices,requested.estimate?.details]);
|
|
@@ -1578,15 +1582,17 @@ function App() {
|
|
|
1578
1582
|
const exportFiles = (files, suffix) => {
|
|
1579
1583
|
download(zipSync(Object.fromEntries(Object.entries(files).map(([name, content]) => [name, typeof content === "string" ? strToU8(content) : content]))), `${slug}-r${project.revision}-${suffix}.zip`, "application/zip");
|
|
1580
1584
|
};
|
|
1581
|
-
const downloadBundle = bundle => {
|
|
1585
|
+
const downloadBundle = async bundle => {
|
|
1586
|
+
bundle=await(bundle.prepare?.()||bundle);
|
|
1587
|
+
if(bundle.project!==projectRef.current)throw Error('Design changed while preparing files. Download again.');
|
|
1582
1588
|
download(zipSync(Object.fromEntries(Object.entries(bundle.files).map(([name,content])=>[name,typeof content === "string"?strToU8(content):content]))),bundle.filename,"application/zip");
|
|
1583
1589
|
if(bundle.project?.id===projectRef.current.id)updateEngagement(acknowledgeStage(projectRef.current,'delivery'));
|
|
1584
|
-
notify("ZIP downloaded.");
|
|
1590
|
+
notify("ZIP downloaded.");setStarOffer(value=>value+1);
|
|
1585
1591
|
};
|
|
1586
1592
|
const exportPack = async () => {
|
|
1587
1593
|
try {
|
|
1588
1594
|
notify("Preparing documents and diagram exports…");
|
|
1589
|
-
downloadBundle(await outputBundle(project,run,environment));
|
|
1595
|
+
await downloadBundle(await outputBundle(project,run,environment));
|
|
1590
1596
|
} catch(e) { notify(e.message); }
|
|
1591
1597
|
};
|
|
1592
1598
|
const exportTerraform = async (validation = null) => {
|
|
@@ -1887,6 +1893,7 @@ function App() {
|
|
|
1887
1893
|
<hr />
|
|
1888
1894
|
<button role="menuitem" onClick={() => setModal("connections")}><Waypoints size={16}/>AI providers</button>
|
|
1889
1895
|
<button role="menuitem" onClick={() => setModal("licenses")}><ShieldCheck size={16}/>Licences</button>
|
|
1896
|
+
<a role="menuitem" href={STAR_URL} target="_blank" rel="noopener noreferrer">Star on GitHub ↗</a>
|
|
1890
1897
|
</div>
|
|
1891
1898
|
)}
|
|
1892
1899
|
</header>
|
|
@@ -2164,12 +2171,12 @@ function App() {
|
|
|
2164
2171
|
<header className="review-intro"><h2>Review</h2></header>
|
|
2165
2172
|
<div className="review-design-status"><ShieldCheck size={18}/><span>{findings.filter(f=>f.status==='open').length} design items</span><button className="icon" aria-label="Open design checks in specification" title="Specification" onClick={()=>visitJourney('scope')}><Code2 size={16}/></button></div>
|
|
2166
2173
|
<section className="review-costs" aria-label="Project costs">
|
|
2167
|
-
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} pricing={pricingState} onNext={()=>{const section=document.querySelector('.review-generation');const target=section?.querySelector('button')||section;if(target){target.setAttribute('tabindex','-1');target.focus({preventScroll:true});target.scrollIntoView({block:'center'});}}} />}
|
|
2174
|
+
{["review","estimate"].includes(mode) && <EstimateView project={project} result={costs} onChange={updateEngagement} busy={busy||agentBusy||!!pending} onEstimate={refreshEstimate} onChooseRegion={()=>{visitJourney('cloud');requestAnimationFrame(()=>{const input=document.querySelector('.location-region-form input');input?.focus();input?.scrollIntoView({block:'center',behavior:'instant'});});}} pricing={pricingState} onNext={()=>{const section=document.querySelector('.review-generation');const target=section?.querySelector('button')||section;if(target){target.setAttribute('tabindex','-1');target.focus({preventScroll:true});target.scrollIntoView({block:'center'});}}} />}
|
|
2168
2175
|
</section>
|
|
2169
2176
|
{mode==='review'&&session?.engineeringEnabled&&<section className="review-generation" aria-label="Generation and verification"><EngineeringPanel key={project.id} project={project} onBusyChange={setGenerationPending} onWorkModelChange={changeArchitectureWork} onIntentContext={syncEngineeringIntent} projectId={project.id} session={session} connection={connection} contextBusy={busy||agentBusy||!!pending} saveProject={()=>saveEngineeringProject(project.id)} buildRequest={buildRequest} onBuildRequestHandled={id=>setBuildRequest(current=>current?.id===id?null:current)} onConnections={()=>setModal('connections')}/></section>}
|
|
2170
2177
|
{mode==='review'&&!session?.engineeringEnabled&&<p>Generation and local checks require the engineering service. Your design is retained; Delivery can package the design and remaining tasks.</p>}
|
|
2171
|
-
{mode==='review'&&<ProjectChecks receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
|
|
2172
|
-
{mode==='review'&&<footer className="review-finish"><div><strong>3. Package and download</strong><p>Explore all files and download one project ZIP in Delivery.</p></div><button className="btn primary" onClick={()=>{const next=acknowledgeStage(projectRef.current,'review');updateEngagement(costs.
|
|
2178
|
+
{mode==='review'&&<ProjectChecks generationPending={generationPending} receipt={checkReceipt} project={project} environment={environment} session={session} onReceipt={setCheckReceipt} onBusy={setChecking}/>}
|
|
2179
|
+
{mode==='review'&&<footer className="review-finish"><div><strong>3. Package and download</strong><p>Explore all files and download one project ZIP in Delivery.</p></div><button className="btn primary" onClick={()=>{const next=acknowledgeStage(projectRef.current,'review');updateEngagement(costs.expected!==null&&!costs.mixedCurrencies?{...next,extensions:{...next.extensions,journey:{...next.extensions.journey,estimate:journey(next).keys.estimate}}}:next);visitJourney('delivery');}}>Complete →</button></footer>}
|
|
2173
2180
|
</div>
|
|
2174
2181
|
|
|
2175
2182
|
|
|
@@ -2177,7 +2184,7 @@ function App() {
|
|
|
2177
2184
|
|
|
2178
2185
|
{onBoard&&emulationOpen&&<div className="emulation-dock"><EmulationPanel project={project} session={session} emulation={emulation} onDiscuss={discussEmulation} onOpenComponent={id=>{reviewBlock(id);setEmulationOpen(false);}} discussionBusy={busy||agentBusy||!!pending} onClose={()=>setEmulationOpen(false)} onBuild={session?.engineeringEnabled?()=>{setEmulationOpen(false);buildCurrentDesign('test');}:undefined}/></div>}
|
|
2179
2186
|
|
|
2180
|
-
{mode === "design" && designView === "outputs" && <OutputsView onNavigate={target=>{const [stage,selector]={requirements:['scope','.requirements-document'],location:['cloud','section[aria-label="Country and region"]'],cost:['review','.review-costs'],implementation:['review','.review-generation'],checks:['review','.project-checks']}[target];visitJourney(stage);
|
|
2187
|
+
{mode === "design" && designView === "outputs" && <OutputsView onNavigate={async target=>{const [stage,selector]={requirements:['scope','.requirements-document'],location:['cloud','section[aria-label="Country and region"]'],cost:['review','.review-costs'],implementation:['review','.review-generation'],checks:['review','.project-checks']}[target];visitJourney(stage);await new Promise((resolve,reject)=>{const started=performance.now();function focus(){const element=document.querySelector(selector);if(element&&element.getClientRects().length){element.setAttribute('tabindex','-1');element.focus({preventScroll:true});element.scrollIntoView({block:'start',behavior:'instant'});resolve();}else if(performance.now()-started<5000)requestAnimationFrame(focus);else reject(Error('Section is unavailable. Use the stage tabs to continue.'));}requestAnimationFrame(focus);});}} checkReceipt={checkReceipt} onDownloaded={()=>{updateEngagement(acknowledgeStage(projectRef.current,'delivery'));setStarOffer(value=>value+1);}} onReview={()=>visitJourney('review')} onSave={expected=>saveEngineeringProject(project.id,expected)} project={project} run={run} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDownload={downloadBundle} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2181
2188
|
{mode === "design" && designView === "terraform" && <TerraformView project={project} environment={environment} session={session} emulation={emulation} onDiscuss={discussEmulation} discussionBusy={busy||agentBusy||!!pending} onDelivery={()=>visitJourney("delivery")} onBuild={session?.engineeringEnabled?()=>visitJourney('review'):undefined} onBack={()=>setDesignView("board")} />}
|
|
2182
2189
|
<div className="choices-workspace" hidden={!(mode==="design"&&["options","decisions","location"].includes(designView))}>
|
|
2183
2190
|
{mode === "design" && ["options","decisions"].includes(designView) && (
|
|
@@ -2324,7 +2331,7 @@ function App() {
|
|
|
2324
2331
|
</button>
|
|
2325
2332
|
))}
|
|
2326
2333
|
</div>
|
|
2327
|
-
{(agentBusy||generationPending||['starting','running'].includes(engineeringStatus?.state)||pricingState.busy||checking)&&<div className="chat-working-status" role="status" aria-live="polite"><BrainActivity size={26}/><span>{agentBusy?architectAction:generationPending?'
|
|
2334
|
+
{(agentBusy||generationPending||['starting','running'].includes(engineeringStatus?.state)||pricingState.busy||checking)&&<div className="chat-working-status" role="status" aria-live="polite"><BrainActivity size={26}/><span>{agentBusy?architectAction:generationPending?'Generating project files…':['starting','running'].includes(engineeringStatus?.state)?'Generating project files…':checking?'Running checks…':'Calculating costs…'}</span></div>}
|
|
2328
2335
|
{readingAttachments&&<p className="attachment-status" role="status"><BrainActivity/> Reading files…</p>}
|
|
2329
2336
|
{attachmentError&&<p className="attachment-status" role="alert">{attachmentError}</p>}
|
|
2330
2337
|
{!!attachments.length&&<div className="attachment-tray" aria-label="Attachments ready to send">{attachments.map(a=><div className="attachment-chip" key={a.id}>{a.data&&a.mime.startsWith('image/')?<img src={`data:${a.mime};base64,${a.data}`} alt=""/>:<Paperclip size={16}/>}<span>{a.name}<small>{!a.data?'Original missing · attach again':a.mime.startsWith('image/')?'Image':a.mime==='application/pdf'||a.mime.includes('wordprocessingml')?'Text will be extracted; embedded images omitted':'Document text'}</small></span><button className="icon" aria-label={`Remove ${a.name}`} onClick={()=>setAttachments(list=>list.filter(item=>item.id!==a.id))}><X size={14}/></button></div>)}<p>Sent to your selected architect with this message. Original files are not stored in project files.</p></div>}
|
|
@@ -2387,6 +2394,7 @@ function App() {
|
|
|
2387
2394
|
{toast === "Preparing documents and diagram exports…"&&<BrainActivity/>}{toast}
|
|
2388
2395
|
</div>
|
|
2389
2396
|
)}
|
|
2397
|
+
<StarPrompt offer={starOffer}/>
|
|
2390
2398
|
{modal && (
|
|
2391
2399
|
<div className="backdrop" onMouseDown={(e) => e.target === e.currentTarget && !(modal === "welcome" && !connection) && setModal(null)}>
|
|
2392
2400
|
<section className={`modal ${modal==="memory"?"memory-modal":""}`} role="dialog" aria-modal="true" aria-label={modal}>
|
package/src/output-bundle.mjs
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import {verifiedReceipt} from './validation-receipt.mjs';
|
|
2
2
|
import {deliveryPack} from './delivery.mjs';
|
|
3
|
-
export
|
|
3
|
+
export const VISUAL_PATHS=['spec-kit/implementation-spec.pdf','specification.pdf','diagram.svg','diagram.png','diagram.pdf'];
|
|
4
|
+
export async function outputBundle(project,run,environment,checks,{deferVisuals=false}={}){
|
|
4
5
|
const snapshot=structuredClone(project);
|
|
5
6
|
const files=deliveryPack(snapshot,run,environment);
|
|
6
|
-
const {visualFiles}=await import('./visual-export.mjs');
|
|
7
|
-
Object.assign(files,await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md']));
|
|
8
7
|
if(checks){const terraform=Object.fromEntries(Object.entries(files).filter(([name])=>name.startsWith('terraform/')).map(([name,value])=>[name.slice(10),value]));const receipt=await verifiedReceipt(terraform,checks);if(receipt)files['terraform/validation-receipt.json']=JSON.stringify(receipt,null,2);}
|
|
9
8
|
const manifest=JSON.parse(files['manifest.json']);manifest.files=Object.keys(files);
|
|
10
9
|
files['manifest.json']=JSON.stringify(manifest,null,2);
|
|
11
10
|
const slug=snapshot.name.toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'')||'atflow';
|
|
12
|
-
|
|
11
|
+
const base={project,files,manifest,filename:`${slug}-r${snapshot.revision}-delivery.zip`,visualPaths:VISUAL_PATHS};
|
|
12
|
+
let pending;
|
|
13
|
+
async function prepare(){
|
|
14
|
+
if(!pending)pending=(async()=>{
|
|
15
|
+
const {visualFiles}=await import('./visual-export.mjs');
|
|
16
|
+
const fullFiles={...files,...await visualFiles(snapshot,files['specification.md'],files['spec-kit/implementation-spec.md'])};
|
|
17
|
+
const fullManifest={...manifest,files:Object.keys(fullFiles)};
|
|
18
|
+
fullFiles['manifest.json']=JSON.stringify(fullManifest,null,2);
|
|
19
|
+
const full={...base,files:fullFiles,manifest:fullManifest,visualPaths:[]};
|
|
20
|
+
return {...full,prepare:()=>Promise.resolve(full)};
|
|
21
|
+
})().catch(error=>{pending=null;throw error;});
|
|
22
|
+
return pending;
|
|
23
|
+
}
|
|
24
|
+
return deferVisuals?{...base,prepare}:prepare();
|
|
13
25
|
}
|
package/src/pricing.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import {prefillAcceptedSettings} from './engineering-defaults.mjs';
|
|
1
2
|
import { setPrice, usage } from './estimate.mjs';
|
|
2
3
|
const patterns = {aws:/\bs3\b/i,azure:/blob|data lake|azure storage/i,gcp:/cloud storage|storage bucket/i,ibm:/object storage|\bcos\b/i};
|
|
3
4
|
export const storageNodes = p => p.nodes.filter(n=>patterns[n.data.provider]?.test(`${n.data.label} ${n.data.service || ''}`));
|
|
4
|
-
const sources = {'AWS public Price List':'https://pricing.us-east-1.amazonaws.com/','Azure Retail Prices API':'https://prices.azure.com/','Google Cloud Billing Catalog':'https://cloud.google.com/'};
|
|
5
|
+
const sources = {'AWS public Price List':'https://pricing.us-east-1.amazonaws.com/','Azure Retail Prices API':'https://prices.azure.com/','Google Cloud Billing Catalog':'https://cloud.google.com/','Google Cloud public pricing':'https://cloud.google.com/storage/pricing'};
|
|
5
6
|
export function applyPriceQuotes(project, quotes) {
|
|
6
7
|
let p=project;
|
|
7
8
|
for(const quote of quotes||[]){
|
|
@@ -11,7 +12,7 @@ export function applyPriceQuotes(project, quotes) {
|
|
|
11
12
|
if(n.data.provider!==quote.provider || n.data.config?.region!==quote.region || existing?.source && !sources[existing.source])continue;
|
|
12
13
|
// Shared usage is decimal GB; preserve the conversion in the quote note.
|
|
13
14
|
const factor=quote.unit==='GiB-month'?1e9/(1024**3):1;
|
|
14
|
-
p=setPrice(p,n.id,{price:quote.price*factor,currency:quote.currency,source:quote.source,url:quote.url,date:quote.date,region:quote.region,service:quote.service,tierLimit:quote.tierLimit,coverageIncomplete:true,note:quote.coverage+(factor!==1?`; provider GiB converted from decimal GB using ${factor}`:'')});
|
|
15
|
+
p=setPrice(p,n.id,{price:quote.price*factor,currency:quote.currency,source:quote.source,url:quote.url,date:quote.date,region:quote.region,service:quote.service,tierLimit:quote.tierLimit,coverageIncomplete:true,sourceSha256:quote.sourceSha256,hourlyPrice:quote.hourlyPrice,hoursPerMonth:quote.hoursPerMonth,note:quote.coverage+(factor!==1?`; provider GiB converted from decimal GB using ${factor}`:'')});
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
return p;
|
|
@@ -49,9 +50,10 @@ export function applyLambdaQuote(project,node,quote){
|
|
|
49
50
|
return {...project,estimate:{...project.estimate,details:[...current.filter(line=>!line.id.startsWith(prefix)),...generated]}};
|
|
50
51
|
}
|
|
51
52
|
export async function fetchPlanningPrices(session,project){
|
|
53
|
+
project=prefillAcceptedSettings(project);
|
|
52
54
|
const storage=await fetchStoragePrices(session,project);let next=applyPriceQuotes(project,storage.quotes);const errors=[...storage.errors];
|
|
53
55
|
for(const node of lambdaNodes(project)){
|
|
54
|
-
const region=node.data.config?.region;if(!region||!/^[a-z]{2}-(?:gov-)?[a-z]+-\d$/.test(region)){errors.push({
|
|
56
|
+
const region=node.data.config?.region;if(!region||!/^[a-z]{2}-(?:gov-)?[a-z]+-\d$/.test(region)){errors.push({provider:'aws',action:'choose-region',reason:'Choose an AWS region in Choices.'});continue;}
|
|
55
57
|
try{
|
|
56
58
|
const architecture=node.data.config?.architecture==='arm64'?'arm64':'x86_64';
|
|
57
59
|
const response=await fetch('/api/pricing/lambda',{method:'POST',headers:{'Content-Type':'application/json',Authorization:`Bearer ${session.token}`},body:JSON.stringify({projectId:project.id,region,architecture}),signal:AbortSignal.timeout(20000)}),quote=await response.json();
|
|
@@ -63,7 +65,7 @@ export async function fetchPlanningPrices(session,project){
|
|
|
63
65
|
const region=node.data.config?.region||'',service=/cloud run/i.test(`${node.data.service||''} ${node.data.label}`)?'cloud-run':'cloud-sql';
|
|
64
66
|
try{const response=await fetch('/api/pricing/google',{method:'POST',headers:{'Content-Type':'application/json',Authorization:`Bearer ${session.token}`},body:JSON.stringify({projectId:project.id,region,service,planningReference:!region||/select|unknown|tbd/i.test(region)}),signal:AbortSignal.timeout(20000)}),quote=await response.json();if(!response.ok)throw Error(quote.detail||'Google pricing could not be verified.');next=applyGoogleQuote(next,node,quote);}catch(error){errors.push({component:node.data.label,reason:error.message});}
|
|
65
67
|
}
|
|
66
|
-
return {project:next,errors};
|
|
68
|
+
return {project:next,errors:[...new Map(errors.map(error=>[`${error.provider||''}:${error.reason}:${error.action||''}`,error])).values()]};
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
export const googleNodes=p=>p.nodes.filter(n=>n.data.provider==='gcp'&&n.data.origin!=='existing'&&/cloud run|cloud sql/i.test(`${n.data.service||''} ${n.data.label}`));
|
package/src/project-file.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export const FORMAT_VERSION = "2.3.0";
|
|
|
10
10
|
export const APPROVAL_FORMAT_VERSION = "3.0.0";
|
|
11
11
|
const approvalFormat=nodes=>Array.isArray(nodes)&&nodes.some(n=>n.data?.implementation==='approval-workflow'||n.data?.approvalProfile!=null);
|
|
12
12
|
export const MAX_FILE_BYTES = 2_000_000;
|
|
13
|
-
const GENERATOR = "atmem-ai-architect 0.2.
|
|
13
|
+
const GENERATOR = "atmem-ai-architect 0.2.1";
|
|
14
14
|
|
|
15
15
|
// Section keys the 2.0 reader understands; anything else is kept in extensions.
|
|
16
16
|
const KNOWN_TOP = ["format", "formatVersion", "meta", "design", "discovery", "requirements", "assumptions", "openQuestions", "decisions", "options", "estimate", "review", "extensions"];
|