@atmem/architect 0.2.0-alpha.4.1 → 0.2.0
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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +19 -21
- package/dist/assets/index-BTLHaa3J.css +1 -0
- package/dist/assets/{index-qtCM2D7O.js → index-CQ2C1gKF.js} +19206 -19139
- package/dist/assets/{index.es-C6C0sneB.js → index.es-DTyqIdaO.js} +1 -1
- package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-Ox0j5xj5.js} +3 -3
- package/dist/index.html +2 -2
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/package.json +2 -2
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- package/server/app.py +1 -1
- package/server/codex_app_server.py +5 -5
- package/server/codex_worker_policy.py +27 -4
- package/server/engineering_artifacts.py +7 -1
- package/server/engineering_handoff.py +9 -0
- package/server/engineering_routes.py +16 -2
- package/server/local_providers.py +7 -3
- package/server/memory_context.py +58 -0
- package/server/memory_routes.py +67 -8
- package/server/memory_service.py +37 -4
- package/server/memory_worker.py +72 -9
- package/server/orca_handoff.py +90 -0
- package/server/pricing.py +106 -0
- package/server/prompts.py +13 -0
- package/server/windows_snapshot.py +119 -0
- package/src/ArchitectureWorkPanel.jsx +0 -7
- package/src/ArtifactChecks.jsx +2 -2
- package/src/ChatMessage.jsx +12 -2
- package/src/EngagementViews.jsx +18 -16
- package/src/EngineeringPanel.jsx +27 -27
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +15 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +52 -16
- package/src/JourneyViews.jsx +19 -16
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +43 -31
- package/src/OpenAIVoicePanel.jsx +15 -14
- package/src/OutputsView.jsx +21 -15
- package/src/ProjectChecks.jsx +44 -0
- package/src/WorkbenchDialogs.jsx +1 -1
- package/src/conversation.mjs +1 -0
- package/src/delivery.mjs +1 -1
- package/src/estimate.mjs +5 -3
- package/src/final-package.mjs +34 -0
- package/src/journey-workspace.css +34 -0
- package/src/journey.mjs +12 -2
- package/src/main.jsx +119 -96
- package/src/output-bundle.mjs +4 -2
- package/src/pricing.mjs +56 -2
- package/src/useChatScroll.jsx +5 -4
- package/src/useEngineeringStatus.mjs +17 -0
- package/src/validation-receipt.mjs +1 -1
- package/dist/assets/index-B97MkVMs.css +0 -1
package/src/JourneyViews.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Review, Estimate and Deploy steps of the AtMem AI Architect journey.
|
|
2
2
|
import React from "react";
|
|
3
|
+
import {Check} from "lucide-react";
|
|
3
4
|
import { ATTRIBUTES, acceptRisk, blockers } from "./review.mjs";
|
|
4
5
|
import { DISCLAIMER, INPUTS, PROJECT_SIZES, setProjectSize, setPrice, setUsage } from "./estimate.mjs";
|
|
5
6
|
import { deploymentSteps } from "./document.mjs";
|
|
@@ -30,7 +31,8 @@ function Accept({ onAccept }) {
|
|
|
30
31
|
);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
export function ReviewView({ project, findings, onChange, onOpen, onAddRequirement }) {
|
|
34
|
+
export function ReviewView({ project, findings, onChange, onOpen, onAddRequirement,expanded=false }) {
|
|
35
|
+
const ChoicesContainer=expanded?'section':'details',ChoicesHeading=expanded?'h3':'summary';
|
|
34
36
|
const names = Object.fromEntries(project.nodes.map((n) => [n.id, n.data.label]));
|
|
35
37
|
const block = blockers(findings);
|
|
36
38
|
const open = findings.filter((f) => f.status === "open");
|
|
@@ -46,7 +48,7 @@ export function ReviewView({ project, findings, onChange, onOpen, onAddRequireme
|
|
|
46
48
|
: "Nothing open. Rules run on every change."}
|
|
47
49
|
</p>
|
|
48
50
|
</header>
|
|
49
|
-
<
|
|
51
|
+
<ChoicesContainer className="extra"><ChoicesHeading>Agreed choices and consequences</ChoicesHeading>{(project.decisions||[]).filter(d=>d.status==='Accepted').map(d=><article key={d.id||d.subject}><strong>{d.title||d.subject}</strong><p>{d.decision}</p>{d.context&&<p>{d.context}</p>}{d.consequences&&<p>{d.consequences}</p>}</article>)}</ChoicesContainer>
|
|
50
52
|
{ATTRIBUTES.map(([id, label]) => {
|
|
51
53
|
const fs = findings.filter((f) => f.attribute === id);
|
|
52
54
|
if (!fs.length) return null;
|
|
@@ -90,24 +92,24 @@ export function ReviewView({ project, findings, onChange, onOpen, onAddRequireme
|
|
|
90
92
|
);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
export function EstimateView({ project, result, onChange, onEstimate, busy=false }) {
|
|
95
|
+
export function EstimateView({ project, result, onChange, onEstimate, busy=false, pricing, onNext }) {
|
|
94
96
|
const e = result;
|
|
95
97
|
return (
|
|
96
98
|
<section className="doc" aria-label="Estimate">
|
|
97
99
|
<header className="doc-head">
|
|
98
|
-
<h2>
|
|
99
|
-
<p className="muted">
|
|
100
|
+
<div className="cost-heading"><h2>1. Cost estimate</h2>{onEstimate&&<button type="button" className="btn" disabled={!!pricing?.busy} onClick={()=>onEstimate(project)}>{pricing?.busy?'Calculating…':'Recalculate'}</button>}</div>
|
|
101
|
+
<p className="muted">Monthly planning estimate. {DISCLAIMER}</p>
|
|
100
102
|
</header>
|
|
101
|
-
<fieldset
|
|
102
|
-
<div className="row">{PROJECT_SIZES.map(size=><button type="button" className="btn" key={size.id} aria-pressed={project.estimate?.projectSize===size.id} onClick={()=>{const next=setProjectSize(project,size.id);onChange(next);onEstimate?.(next);}}>{size.label}</button>)}</div>
|
|
103
|
-
<p className="muted small">Small: 10,000 requests / 10 GB; Medium: 100,000 / 100 GB; Large: 1 million / 1 TB; Extra Large: 10 million / 10 TB per month. Existing usage figures take priority. Model assumptions: 3,000 tokens per request and 730 endpoint hours; training usage needs separate agreement.</p>
|
|
103
|
+
<fieldset className="cost-size-selector"><legend>How large do you expect this project to be?</legend>
|
|
104
|
+
<div className="row">{PROJECT_SIZES.map(size=><button type="button" className={project.estimate?.projectSize===size.id?"btn primary":"btn"} key={size.id} aria-pressed={project.estimate?.projectSize===size.id} onClick={()=>{if(project.estimate?.projectSize===size.id)return;const next=setProjectSize(project,size.id);onChange(next);onEstimate?.(next);}}>{project.estimate?.projectSize===size.id&&<Check size={16} aria-hidden="true"/>}{size.label}</button>)}</div>
|
|
105
|
+
<details className="cost-assumptions"><summary>Planning assumptions</summary><p className="muted small">Small: 10,000 requests / 10 GB; Medium: 100,000 / 100 GB; Large: 1 million / 1 TB; Extra Large: 10 million / 10 TB per month. Existing usage figures take priority. Model assumptions: 3,000 tokens per request and 730 endpoint hours; training usage needs separate agreement.</p></details>
|
|
104
106
|
</fieldset>
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
+
{project.estimate?.projectSize&&<p className="cost-selection-status" role="status">{PROJECT_SIZES.find(size=>size.id===project.estimate.projectSize)?.label} · {pricing?.busy?'Updating prices…':e.complete?'Estimate ready':e.lines.some(line=>line.cost!=null)?'Partial estimate':'Prices unavailable'}</p>}{pricing?.errors?.map((error,i)=><p className="warn-text" key={i}>{error.component&&`${error.component}: `}{error.reason} <button className="link" disabled={busy||pricing?.busy} onClick={()=>onEstimate?.(project)}>Retry pricing</button></p>)}
|
|
108
|
+
|
|
107
109
|
<div className="totals">
|
|
108
110
|
<div>
|
|
109
111
|
<small className="muted">Estimated subtotal / month</small>
|
|
110
|
-
<strong>{e.lines.some((l) => l.cost != null) ? money(e.expected, e.currency) : "—"}</strong>
|
|
112
|
+
<strong>{e.lines.some((l) => l.cost != null) ? e.expected===0&&e.preciseExpected>0?`< ${money(0.01,e.currency)}`:money(e.expected, e.currency) : "—"}</strong>
|
|
111
113
|
</div>
|
|
112
114
|
<div>
|
|
113
115
|
<small className="muted">Range</small>
|
|
@@ -118,7 +120,7 @@ export function EstimateView({ project, result, onChange, onEstimate, busy=false
|
|
|
118
120
|
<div>
|
|
119
121
|
<small className="muted">Status</small>
|
|
120
122
|
<span className={e.complete ? "ok-text" : "warn-text"}>
|
|
121
|
-
{e.complete ? "All entered charges estimated" : e.mixedCurrencies ? "Currency needs review" : "
|
|
123
|
+
{e.complete ? "All entered charges estimated" : e.mixedCurrencies ? "Currency needs review" : "Partial estimate · see exclusions"}
|
|
122
124
|
</span>
|
|
123
125
|
</div>
|
|
124
126
|
{e.budget && (
|
|
@@ -131,14 +133,15 @@ export function EstimateView({ project, result, onChange, onEstimate, busy=false
|
|
|
131
133
|
</div>
|
|
132
134
|
)}
|
|
133
135
|
</div>
|
|
134
|
-
<p className="muted small">
|
|
136
|
+
<p className="muted small">{e.lines.some(line=>line.price?.planningReference)?'US reference rates · cloud region undecided. Unpriced charges excluded.':'Unpriced charges are excluded.'}</p>
|
|
135
137
|
{e.drivers.length > 0 && (
|
|
136
138
|
<p className="small">
|
|
137
139
|
Biggest drivers: {e.drivers.map((d) => `${d.component} (${money(d.cost, d.price.currency)})`).join(", ")}
|
|
138
140
|
</p>
|
|
139
141
|
)}
|
|
140
|
-
{e.lines.some(l=>l.cost==null)&&<p role="status">{e.lines.filter(l=>l.cost==null).length} charges
|
|
141
|
-
|
|
142
|
+
{e.lines.some(l=>l.cost==null)&&<p role="status">{e.lines.filter(l=>l.cost==null).length} charges unpriced.</p>}
|
|
143
|
+
{onNext&&<button type="button" className="btn" onClick={onNext}>Project files ↓</button>}
|
|
144
|
+
<details className="extra"><summary>Cost breakdown and price sources</summary><div className="resource-schedule" aria-label="Estimate breakdown">{e.lines.map(l=><article key={l.id}><strong>{l.component} · {l.driver}</strong><span>{l.cost==null?'Not yet estimated':money(l.cost,l.price.currency)+'/month'}</span><small>{l.note||l.price?.note}</small><small>{l.quantity??'Usage unresolved'} {l.unit}{l.price&&<> · {l.price.source}{l.price.url&&<> · <a href={l.price.url} target="_blank" rel="noreferrer">price source</a></>}</>}</small></article>)}</div></details>
|
|
142
145
|
<details className="extra"><summary>Adjust usage assumptions and prices</summary>
|
|
143
146
|
{e.lines.some((l) => !l.detailed) && <>
|
|
144
147
|
<div className="doc-sub">
|
|
@@ -394,7 +397,7 @@ export function ProjectsDialog({ list, currentId, onOpen, onNew, onBin, onRestor
|
|
|
394
397
|
</ul>
|
|
395
398
|
</details>
|
|
396
399
|
)}
|
|
397
|
-
<p className="modal-fine">
|
|
400
|
+
<p className="modal-fine">{folder || "~/.atforma"}</p>
|
|
398
401
|
</div>
|
|
399
402
|
);
|
|
400
403
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React,{useState,useEffect}from'react';
|
|
2
|
+
import{newDecision}from'./engagement.mjs';import{reviseDecision}from'./records.mjs';
|
|
3
|
+
const latest=(project,subject)=>[...(project.decisions||[])].reverse().find(d=>d.subject===subject&&d.status==='Accepted');
|
|
4
|
+
export function LocationChoices({project,onChange,onDiscuss}){
|
|
5
|
+
const cloud=latest(project,'platform.cloud')?.decision,country=latest(project,'data.country')?.decision,current=latest(project,'cloud.region');
|
|
6
|
+
const [region,setRegion]=useState(current?.decision||''),[error,setError]=useState('');
|
|
7
|
+
useEffect(()=>setRegion(current?.decision||''),[project.id,current?.decision]);
|
|
8
|
+
function apply(event){event.preventDefault();const value=region.trim();if(!cloud||!value)return;if(!/^[a-z][a-z0-9-]{2,39}$/.test(value)){setError('Enter a region code.');return;}if(cloud==='gcp'&&/^(usa|us|united states)$/i.test(country||'')&&!value.startsWith('us-')){setError('Choose a US region for USA residency.');return;}
|
|
9
|
+
const fields={title:'Cloud region',decision:value,status:'Accepted',owner:'user',reason:'Selected in Choices.'};let next=current?reviseDecision(project,current.id,fields,fields.reason):{...project,decisions:[...(project.decisions||[]),newDecision(project,{...fields,subject:'cloud.region'})]};
|
|
10
|
+
next={...next,nodes:next.nodes.map(node=>{const previous=node.data.config?.region;return node.data.provider===cloud&&node.data.origin!=='existing'&&(!previous||/select|unknown|tbd/i.test(previous)||previous===current?.decision)?{...node,data:{...node.data,config:{...node.data.config,region:value}}}:node;})};onChange(next);setError('');}
|
|
11
|
+
return <section className="doc location-choices" aria-label="Country and region"><header className="doc-head"><h2>Country & region</h2></header><p><strong>Data country:</strong> {country||'Not selected'}</p><form className="location-region-form" onSubmit={apply}><label>Cloud region<input aria-label="Cloud region" value={region} maxLength={40} placeholder={cloud==='gcp'?'e.g. us-central1':'Region code'} onChange={event=>setRegion(event.target.value)}/></label><button className="btn primary" disabled={!cloud||!region.trim()||region.trim()===current?.decision}>Apply</button></form>{current&&<p className="ok-text" role="status">✓ {current.decision}</p>}{error&&<p role="alert">{error}</p>}<button className="link" onClick={onDiscuss}>Discuss location</button></section>;
|
|
12
|
+
}
|
package/src/MemoryPanel.jsx
CHANGED
|
@@ -12,35 +12,47 @@ export function ContextUsed({ value }) {
|
|
|
12
12
|
</details>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function MemoryPanel({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
</
|
|
15
|
+
export function MemoryPanel({session,project,onError,onStatus}){
|
|
16
|
+
const [report,setReport]=useState(null),[tab,setTab]=useState('project'),[error,setError]=useState(''),[working,setWorking]=useState(false),[loading,setLoading]=useState(true);
|
|
17
|
+
const [query,setQuery]=useState(''),[search,setSearch]=useState(''),[status,setStatus]=useState('active'),[cursor,setCursor]=useState(null),[history,setHistory]=useState([]),[page,setPage]=useState(null),[revision,setRevision]=useState(0);
|
|
18
|
+
const [selected,setSelected]=useState(null),[preview,setPreview]=useState(null),[field,setField]=useState('preferences'),[value,setValue]=useState(''),[profileName,setProfileName]=useState('Default profile');
|
|
19
|
+
const scope=tab==='profile'?'user':'project',payload={projectId:project.id},date=value=>value?new Date(value).toLocaleString():'Unknown',short=value=>value?value.slice(0,8):'Unknown';
|
|
20
|
+
async function refresh(){
|
|
21
|
+
setWorking(true);setError('');try{
|
|
22
|
+
await memoryCall(session,'sync',{...payload,file:toProjectFile(project)});
|
|
23
|
+
const result=await memoryCall(session,'overview',payload);setReport(result);setProfileName(result.userProfile?.name||'Default profile');
|
|
24
|
+
onStatus?.({enabled:result.enabled,status:result.status,pending:result.pending,rejected:result.rejected,engine:result.engine});
|
|
25
|
+
setRevision(value=>value+1);
|
|
26
|
+
}catch(error){setError(error.message);}finally{setWorking(false);}
|
|
27
|
+
}
|
|
28
|
+
useEffect(()=>{void refresh();},[project.id,session?.token]);
|
|
29
|
+
useEffect(()=>{const timer=setTimeout(()=>{setSearch(query);setCursor(null);setHistory([]);},250);return()=>clearTimeout(timer);},[query]);
|
|
30
|
+
useEffect(()=>{
|
|
31
|
+
if(!['profile','project'].includes(tab))return;
|
|
32
|
+
const controller=new AbortController();setLoading(true);setError('');
|
|
33
|
+
memoryCall(session,'records',{...payload,scope,status,query:search,cursor,limit:50},controller.signal).then(value=>{if(!controller.signal.aborted)setPage(value);}).catch(error=>{if(!controller.signal.aborted)setError(error.message);}).finally(()=>{if(!controller.signal.aborted)setLoading(false);});
|
|
34
|
+
return()=>controller.abort();
|
|
35
|
+
},[project.id,session?.token,scope,tab,status,search,cursor,revision]);
|
|
36
|
+
async function act(operation){setWorking(true);setError('');try{await operation();setCursor(null);setHistory([]);await refresh();}catch(error){setError(error.message);onError?.(error.message);}finally{setWorking(false);}}
|
|
37
|
+
async function openRecord(record){setSelected({record,audit:[]});try{setSelected(await memoryCall(session,'record',{...payload,scope,recordId:record.id}));}catch(error){setError(error.message);}}
|
|
38
|
+
function chooseTab(value){setTab(value);setSelected(null);setPreview(null);setCursor(null);setHistory([]);setQuery('');setSearch('');}
|
|
39
|
+
return <div className="memory-panel memory-browser">
|
|
40
|
+
<header className="memory-heading"><h2>AtMem memory</h2><button className="btn" disabled={working} onClick={refresh}>Refresh</button></header>
|
|
41
|
+
<div className="memory-profile-bar"><label>Profile <input aria-label="Profile name" value={profileName} maxLength={80} onChange={event=>setProfileName(event.target.value)}/></label><button className="btn" disabled={working||!profileName.trim()||profileName===report?.userProfile?.name} onClick={()=>act(async()=>{await memoryCall(session,'user-profile',{name:profileName});})}>Save</button><code title="Local profile user ID">{report?.userProfile?.id||'Loading profile…'}</code><span className="memory-state">{report?.enabled?'On':'Off'}{report?.status==='unavailable'?' · Unavailable':''}</span><button className="link" disabled={working||!report} onClick={()=>act(()=>memoryCall(session,'settings',{...payload,enabled:!report.enabled}))}>{report?.enabled?'Turn off':'Turn on'}</button></div>
|
|
42
|
+
<div className="seg" role="tablist" aria-label="Memory views">{[['project','This project'],['profile','About you'],['skills','Skills'],['activity','Activity'],['providers','Context providers']].map(([id,label])=><button key={id} role="tab" aria-selected={tab===id} className={tab===id?'on':''} onClick={()=>chooseTab(id)}>{label}</button>)}</div>
|
|
43
|
+
{error&&<p className="error-box" role="alert">{error}</p>}
|
|
44
|
+
{(working||loading&&['project','profile'].includes(tab))&&<p className="memory-feedback" role="status"><BrainActivity/> Updating memory…</p>}
|
|
45
|
+
{report&&(report.pending>0||report.rejected>0)&&<p className="warn-text">{report.pending} pending · {report.rejected} rejected</p>}
|
|
46
|
+
{['profile','project'].includes(tab)&&<>
|
|
47
|
+
<div className="memory-toolbar"><input type="search" aria-label="Search memory" placeholder="Search memory or record ID" value={query} maxLength={200} onChange={event=>setQuery(event.target.value)}/><select aria-label="Memory status" value={status} onChange={event=>{setStatus(event.target.value);setCursor(null);setHistory([]);}}><option value="active">Current</option><option value="superseded">Earlier</option><option value="quarantined">Quarantined</option><option value="all">All</option></select><strong>{page?.total?.toLocaleString()??'—'} {page?.total===1?'record':'records'}</strong></div>
|
|
48
|
+
<div className="memory-table-wrap" aria-busy={loading}><table className="memory-table"><thead><tr><th>Key</th><th>Memory</th><th>Status</th><th>User</th><th>Updated</th><th><span className="sr-only">Actions</span></th></tr></thead><tbody>{page?.records?.map(record=><tr key={record.id}><td><button className="link" onClick={()=>openRecord(record)}>{record.fact_key||record.id}</button></td><td><button className="memory-content" onClick={()=>openRecord(record)}>{record.content}</button></td><td><span className={'memory-badge '+record.status}>{record.status==='active'?'Current':record.status==='superseded'?'Earlier':record.status}</span></td><td title={record.governance?.userId||'Creator was not recorded'}>{short(record.governance?.userId)}</td><td>{date(record.updated_at||record.created_at)}</td><td><button className="link" disabled={working} onClick={()=>act(async()=>{setPreview({...await memoryCall(session,'forget-preview',{...payload,scope,recordId:record.id}),scope,recordId:record.id});})}>Forget</button></td></tr>)}</tbody></table>{!loading&&!page?.records?.length&&<p className="memory-empty">{query?'No matching memories.':'No memories yet.'}</p>}</div>
|
|
49
|
+
<footer className="memory-pagination"><span>Page {history.length+1} · 50 per page</span><button className="btn" disabled={loading||!history.length} onClick={()=>{setCursor(history.at(-1));setHistory(value=>value.slice(0,-1));}}>Previous</button><button className="btn" disabled={loading||!page?.hasMore} onClick={()=>{setHistory(value=>[...value,cursor]);setCursor(page.nextCursor);}}>Next</button></footer>
|
|
50
|
+
</>}
|
|
51
|
+
{tab==='profile'&&<form className="memory-preference-form" onSubmit={event=>{event.preventDefault();act(async()=>{await memoryCall(session,'profile',{...payload,field,value});setValue('');});}}><select aria-label="Preference type" value={field} onChange={event=>setField(event.target.value)}><option value="preferences">Preference</option><option value="explanation">Explanation style</option><option value="experience">Role</option><option value="iac">Infrastructure tool</option></select><input aria-label="Remember about me" placeholder="Add a preference" value={value} maxLength={1000} onChange={event=>setValue(event.target.value)}/><button className="btn primary" disabled={working||!report?.enabled||!value.trim()}>Remember</button></form>}
|
|
52
|
+
{selected&&<section className="memory-record-detail" aria-label="Memory details"><header className="memory-heading"><h3>{selected.record.fact_key||'Memory'}</h3><button className="icon" aria-label="Close memory details" onClick={()=>setSelected(null)}>×</button></header><p>{selected.record.content}</p><dl>{[['Record',selected.record.id],['Creator user ID',selected.record.governance?.userId||'Unknown · legacy record'],['Owner user ID',selected.record.governance?.ownerUserId],['Actor',selected.record.governance?.actor||'Unknown'],['Source',selected.record.governance?.source||'Unknown'],['Created',date(selected.record.created_at)],['Updated',date(selected.record.updated_at)]].map(([label,value])=><React.Fragment key={label}><dt>{label}</dt><dd>{value||'Unknown'}</dd></React.Fragment>)}</dl>{selected.audit?.length>0&&<table className="memory-table"><thead><tr><th>Time</th><th>Actor</th><th>Action</th></tr></thead><tbody>{selected.audit.map((event,index)=><tr key={index}><td>{date(event.created_at)}</td><td>{event.actor}</td><td>{event.event_type}</td></tr>)}</tbody></table>}</section>}
|
|
53
|
+
{preview&&<section className="memory-preview" aria-label="Confirm memory removal"><h3>Forget memory?</h3><p>{preview.total??preview.records.length} records, including history. Project files stay.</p><button className="btn danger" disabled={working} onClick={()=>act(async()=>{await memoryCall(session,'forget',{...payload,scope:preview.scope,recordId:preview.recordId});setPreview(null);setSelected(null);})}>Forget</button><button className="btn" onClick={()=>setPreview(null)}>Cancel</button></section>}
|
|
54
|
+
{tab==='skills'&&<table className="memory-table"><thead><tr><th>Skill</th><th>Version</th><th>Integrity</th><th>Source</th><th>Status</th></tr></thead><tbody>{report?.skills?.map(skill=><tr key={skill.id}><td>{skill.name}</td><td>{skill.version}</td><td>{skill.integrity?'Verified':'Failed'}</td><td>{skill.source.startsWith('https:')?<a href={skill.source} target="_blank" rel="noreferrer">Source ↗</a>:'Local'}</td><td><button className="link" disabled={working||!skill.integrity} onClick={()=>act(()=>memoryCall(session,'skill',{...payload,skillId:skill.id,enabled:!skill.enabled}))}>{skill.enabled?'Disable':'Enable'}</button></td></tr>)}</tbody></table>}
|
|
55
|
+
{tab==='providers'&&<section aria-label="Context providers"><table className="memory-table"><thead><tr><th>Provider</th><th>Type</th><th>Status</th></tr></thead><tbody>{report?.contextProviders?.map(provider=><tr key={provider.id}><td>{provider.label}</td><td>{provider.kind}</td><td>{provider.enabled?'Enabled':'Disabled'}</td></tr>)}</tbody></table>{!report?.contextProviders?.length&&<p>No external providers configured.</p>}<p className="muted small">Connect RAG or memory through the server’s MEMORY_CONTEXT_PROVIDERS setting. Retrieved context is never saved automatically.</p></section>}
|
|
56
|
+
{tab==='activity'&&<table className="memory-table"><thead><tr><th>Time</th><th>Result</th><th>Records</th><th>Skills</th></tr></thead><tbody>{report?.activity?.map(activity=><tr key={activity.id}><td>{date(activity.createdAt)}</td><td>{activity.success?'Completed':'Incomplete'}</td><td>{activity.recordIds.length}</td><td>{activity.skillIds.join(', ')||'—'}</td></tr>)}</tbody></table>}
|
|
57
|
+
</div>;
|
|
46
58
|
}
|
package/src/OpenAIVoicePanel.jsx
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {InlineNote} from './HelpNote.jsx';
|
|
2
|
+
import {X,Volume2} from 'lucide-react';
|
|
3
|
+
import {BrainActivity} from './BrainActivity.jsx';
|
|
1
4
|
import React,{useEffect,useRef,useState} from 'react';
|
|
2
5
|
import {OpenAIVoice} from './openai-voice.mjs';
|
|
3
6
|
|
|
@@ -34,29 +37,27 @@ export function OpenAIVoicePanel({session,connection,transportRef,onTranscript,o
|
|
|
34
37
|
}});
|
|
35
38
|
current.readReplies=readReplies;engine.current=current;transportRef.current=current;void current.start().then(()=>{if(live.current&¤t.channel?.readyState==='open')setOpen(false)});
|
|
36
39
|
}
|
|
37
|
-
const caption={off:'
|
|
40
|
+
const caption={off:'Off',connecting:'Connecting OpenAI audio…',listening:automatic?'Listening · auto-send':'Listening',speaking:'Speaking',error:'Stopped · retry'}[phase];
|
|
38
41
|
return <>
|
|
39
|
-
<div className="voice-status"><button type="button" onClick={()=>setOpen(true)}
|
|
42
|
+
<div className="voice-status"><button type="button" className="icon" aria-label="Voice settings" title="Voice settings" onClick={()=>setOpen(true)}><Volume2 size={18}/></button>{active&&<button type="button" onClick={()=>engine.current?.stop({preservePending:true})}>Stop voice</button>}{active&&error.includes('playback')&&<button type="button" onClick={()=>void engine.current?.resumeAudio()}>Enable voice playback</button>}{(active||error)&&<span role="status">{error||caption}</span>}</div>
|
|
40
43
|
{open&&<div className="backdrop" onMouseDown={event=>{if(event.target===event.currentTarget)setOpen(false)}}><section className="modal" role="dialog" aria-modal="true" aria-label="OpenAI voice" onKeyDown={event=>{if(event.key==='Escape'){event.stopPropagation();setOpen(false)}}}>
|
|
41
|
-
<h2>
|
|
44
|
+
<h2>Voice</h2><button type="button" className="icon modal-close" aria-label="Close voice settings" onClick={()=>setOpen(false)}><X size={18}/></button>
|
|
42
45
|
<section aria-label="OpenAI voice settings">
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{status&&<details open={!status.available}><summary>Voice API setup</summary>
|
|
48
|
-
<p>Configure a dedicated OpenAI API key for voice. Your Architect connection stays unchanged. The server keeps this key in memory for its current session. An existing server OpenAI key can also be used.</p>
|
|
46
|
+
<small>AI audio · OpenAI</small><InlineNote>Audio and spoken replies go to OpenAI. Audio is not saved. Live verification pending.</InlineNote>
|
|
47
|
+
{!connection&&<button type="button" className="btn" onClick={()=>{setOpen(false);onConnections()}}>Choose AI provider</button>}
|
|
48
|
+
{status&&<details open={!status.available}><summary>API key</summary>
|
|
49
|
+
|
|
49
50
|
<label className="dialog-field">OpenAI voice API key<input aria-label="OpenAI voice API key" type="password" autoComplete="off" value={key} onChange={event=>setKey(event.target.value)}/></label>
|
|
50
|
-
<button type="button" className="btn" disabled={saving||active||!key.trim()} onClick={configure}>
|
|
51
|
+
<button type="button" className="btn" disabled={saving||active||!key.trim()} onClick={configure}>Use key</button>
|
|
51
52
|
</details>}
|
|
52
|
-
<button type="button" className="link" disabled={saving} onClick={()=>void refresh().catch(e=>setError(e.message))}>
|
|
53
|
+
<button type="button" className="link" disabled={saving} onClick={()=>void refresh().catch(e=>setError(e.message))}>Refresh</button>
|
|
53
54
|
<div className="record-row">
|
|
54
|
-
<button type="button" className="btn" disabled={!status?.available||!connection} onClick={active?()=>engine.current?.stop({preservePending:true}):start}>{active?'Stop
|
|
55
|
+
<button type="button" className="btn" disabled={!status?.available||!connection} onClick={active?()=>engine.current?.stop({preservePending:true}):start}>{active?'Stop voice':'Start voice'}</button>
|
|
55
56
|
{active&&<button type="button" className="btn" onClick={()=>engine.current?.interrupt()}>Stop speaking</button>}
|
|
56
57
|
{active&&error.includes('playback')&&<button type="button" className="btn" onClick={()=>void engine.current?.resumeAudio()}>Enable voice playback</button>}
|
|
57
58
|
</div>
|
|
58
|
-
<p><label><input type="checkbox" checked={automatic} onChange={event=>setAutomatic(event.target.checked)}/>
|
|
59
|
-
<p><label><input type="checkbox" checked={readReplies} onChange={event=>{setReadReplies(event.target.checked);if(engine.current){engine.current.readReplies=event.target.checked;if(!event.target.checked)engine.current.interrupt();}}}/>
|
|
59
|
+
<p><label><input type="checkbox" checked={automatic} onChange={event=>setAutomatic(event.target.checked)}/> Auto-send</label></p>
|
|
60
|
+
<p><label><input type="checkbox" checked={readReplies} onChange={event=>{setReadReplies(event.target.checked);if(engine.current){engine.current.readReplies=event.target.checked;if(!event.target.checked)engine.current.interrupt();}}}/> Read replies</label></p>
|
|
60
61
|
<p role="status">{caption}</p>
|
|
61
62
|
{error&&<p role="alert">{error}</p>}
|
|
62
63
|
</section>
|
package/src/OutputsView.jsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import {FileTree} from './FileTree.jsx';
|
|
2
|
+
import {strFromU8} from 'fflate';
|
|
1
3
|
import {CodePreview} from './CodePreview.jsx';
|
|
2
4
|
import {BrainActivity} from "./BrainActivity.jsx";
|
|
3
5
|
import React,{useEffect,useState} from 'react';
|
|
@@ -33,33 +35,37 @@ function Preview({name,content}){
|
|
|
33
35
|
if(name.endsWith('.md'))return <Document text={content}/>;
|
|
34
36
|
return <CodePreview className="output-code" content={content} name={name}/>;
|
|
35
37
|
}
|
|
36
|
-
export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave}){
|
|
38
|
+
export function OutputsView({project,run,environment,session,onDownload,onBack,emulation,onDiscuss,discussionBusy,onBuild,onSave,onDownloaded,onReview,checkReceipt,onNavigate}){
|
|
39
|
+
const [exploration,setExploration]=useState(null);
|
|
37
40
|
const [bundle,setBundle]=useState(null),[error,setError]=useState(''),[loading,setLoading]=useState(true),[retry,setRetry]=useState(0),[selected,setSelected]=useState('spec-kit/implementation-spec.md');
|
|
38
41
|
useEffect(()=>{
|
|
39
42
|
let active=true;setLoading(true);setError('');
|
|
40
|
-
outputBundle(project,run,environment).then(value=>{if(active){setBundle(value);setLoading(false);}}).catch(e=>{if(active){setError(e.message);setLoading(false);}});
|
|
43
|
+
outputBundle(project,run,environment,checkReceipt).then(value=>{if(active){setBundle(value);setLoading(false);}}).catch(e=>{if(active){setError(e.message);setLoading(false);}});
|
|
41
44
|
return ()=>{active=false;};
|
|
42
|
-
},[project,run,environment,retry]);
|
|
45
|
+
},[project,run,environment,retry,checkReceipt]);
|
|
43
46
|
const readiness=bundle?.files['spec-kit/readiness.json']?JSON.parse(bundle.files['spec-kit/readiness.json']):null;
|
|
44
|
-
const
|
|
47
|
+
const fileValues=exploration?.project===project?exploration.files:bundle?.files||{};
|
|
48
|
+
const selectedPath=fileValues[selected]!==undefined?selected:fileValues['START_HERE.md']?'START_HERE.md':Object.keys(fileValues).find(name=>name.endsWith('implementation-spec.md'))||Object.keys(fileValues)[0];
|
|
49
|
+
const raw=fileValues[selectedPath],content=typeof raw==='string'?raw:raw&&/\.(?:md|txt|json|py|js|mjs|tf|sh|yaml|yml|toml|html|css)$/.test(selectedPath)?strFromU8(raw):raw;
|
|
50
|
+
|
|
45
51
|
return <section className="doc outputs-view" aria-label="Outputs">
|
|
46
|
-
<header className="doc-head"><h2>Delivery</h2
|
|
47
|
-
<button className="link" onClick={onBack}>Back to
|
|
48
|
-
{session?.engineeringEnabled&&<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy}/>}
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
{!session?.engineeringEnabled&&<><p>
|
|
52
|
+
<header className="doc-head"><h2>Delivery</h2></header>
|
|
53
|
+
<button className="link" onClick={onReview||onBack}>Back to Review</button>
|
|
54
|
+
{session?.engineeringEnabled&&<ImplementationDelivery key={project.id} project={project} session={session} onBuild={onBuild} onSave={onSave} contextBusy={discussionBusy} designBundle={loading?null:bundle} onDesignDownload={onDownload} onDownloaded={onDownloaded} checkReceipt={checkReceipt} onExplore={setExploration} onNavigate={onNavigate}/>}
|
|
55
|
+
<section aria-label="Project file explorer"><h3>Explore files</h3>
|
|
56
|
+
|
|
57
|
+
{!session?.engineeringEnabled&&<><p>Design package · no implementation generated.</p><button className="btn" disabled={!bundle||loading||!!error} onClick={()=>onDownload(bundle)}>Download project ZIP</button></>}
|
|
52
58
|
|
|
53
59
|
{loading&&<p role="status"><BrainActivity/> Preparing the current outputs…</p>}
|
|
54
60
|
{error&&<div role="alert"><p>{error}</p><button className="btn" onClick={()=>setRetry(n=>n+1)}>Retry preparing outputs</button></div>}
|
|
55
61
|
{bundle&&!loading&&!error&&<>
|
|
56
|
-
<p className="muted small">
|
|
57
|
-
{readiness&&<aside aria-label="Handoff readiness"><strong>Engineering handoff — decisions and implementation remain</strong><p>{readiness.counts.priorityDecisions} priority decisions · {readiness.counts.generatedFoundations} generated foundations / {readiness.counts.components} logical components</p><p>Next: {readiness.nextAction}</p><button className="link" onClick={()=>setSelected('spec-kit/readiness.md')}>Review handoff gaps</button></aside>}
|
|
62
|
+
<p className="muted small">r{bundle.manifest.designRevision} · {Object.keys(bundle.files).length} design files</p>
|
|
63
|
+
{readiness&&<details><summary>Remaining work</summary><aside aria-label="Handoff readiness"><strong>Engineering handoff — decisions and implementation remain</strong><p>{readiness.counts.priorityDecisions} priority decisions · {readiness.counts.generatedFoundations} generated foundations / {readiness.counts.components} logical components</p><p>Next: {readiness.nextAction}</p><button className="link" onClick={()=>setSelected('spec-kit/readiness.md')}>Review handoff gaps</button></aside></details>}
|
|
58
64
|
<div className="outputs-browser" data-snapshot-id={bundle.manifest.snapshotId}>
|
|
59
|
-
<
|
|
60
|
-
<div className="output-preview" aria-label="Output preview"><h3>{
|
|
65
|
+
<FileTree files={fileValues} selected={selectedPath} onSelect={setSelected}/>
|
|
66
|
+
<div className="output-preview" aria-label="Output preview"><h3>{selectedPath}</h3>{content!==undefined&&(typeof content==='string'||/\.(?:pdf|png|svg)$/.test(selectedPath)?<Preview key={selectedPath} name={selectedPath} content={content}/>:<p>{content.byteLength} bytes</p>)}</div>
|
|
61
67
|
</div>
|
|
62
68
|
</>}
|
|
63
|
-
</
|
|
69
|
+
</section>
|
|
64
70
|
</section>;
|
|
65
71
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React,{useEffect,useMemo,useRef,useState}from'react';
|
|
2
|
+
import{BrainActivity}from'./BrainActivity.jsx';
|
|
3
|
+
import{engineeringCall}from'./engineering-client.mjs';
|
|
4
|
+
import{terraformSketch}from'./terraform.mjs';
|
|
5
|
+
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('');
|
|
8
|
+
const active=useRef(null),locked=useRef(false),files=useMemo(()=>terraformSketch(project,environment),[project,environment]),key=JSON.stringify(files);
|
|
9
|
+
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
|
+
useEffect(()=>{
|
|
11
|
+
if(!session?.engineeringEnabled)return;
|
|
12
|
+
let stopped=false,timer;const controller=new AbortController();
|
|
13
|
+
async function poll(){try{
|
|
14
|
+
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||[]);}}
|
|
16
|
+
}catch(e){if(!stopped&&e.name!=='AbortError'&&e.status!==404)setError(e.message);}finally{if(!stopped)timer=setTimeout(poll,2000);}}
|
|
17
|
+
void poll();return()=>{stopped=true;clearTimeout(timer);controller.abort();};
|
|
18
|
+
},[project.id,session?.token,session?.engineeringEnabled]);
|
|
19
|
+
const running=checks.some(c=>c.state!=='finished'&&c.state!=='recovery_required');
|
|
20
|
+
useEffect(()=>{onBusy?.(working||running);return()=>onBusy?.(false);},[working,running,onBusy]);
|
|
21
|
+
async function run(){
|
|
22
|
+
if(locked.current)return;locked.current=true;setWorking(true);setError('');const controller=new AbortController();active.current=controller;
|
|
23
|
+
try{
|
|
24
|
+
const jobs=[];
|
|
25
|
+
if(report?.artifact)for(const validator of catalog.filter(v=>v.available!==false)){
|
|
26
|
+
const previous=checks.filter(c=>c.validatorId===validator.id&&c.contractSha256===validator.contractSha256).at(-1);
|
|
27
|
+
if(previous&&previous.state!=='finished')continue;
|
|
28
|
+
jobs.push(engineeringCall(session,'validation/start',{projectId:project.id,sessionId:report.session.id,artifactId:report.artifact.id,validatorId:validator.id,retry:!!previous},{signal:controller.signal}));
|
|
29
|
+
}
|
|
30
|
+
const outcomesPending=Promise.allSettled(jobs);
|
|
31
|
+
const response=await fetch('/api/terraform/validate',{method:'POST',headers:{'Content-Type':'application/json',Authorization:`Bearer ${session.token}`},body:JSON.stringify({project,environment}),signal:AbortSignal.any([controller.signal,AbortSignal.timeout(500000)])});const value=await response.json();
|
|
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);}
|
|
37
|
+
}
|
|
38
|
+
const label=status=>({passed:'Passed',failed:'Failed',incomplete:'Incomplete',unavailable:'Not run'}[status]||'Not run');
|
|
39
|
+
return <section className="project-checks" aria-label="Project checks"><div className="section-action"><h3>Checks</h3><button className="btn" disabled={!session?.token||working||running} onClick={run}>{working||running?<><BrainActivity size={22}/> Checking…</>:'Run checks'}</button></div>
|
|
40
|
+
<ul className="check-rows"><li><span>Terraform</span><strong className={result?.status==='passed'?'check-complete':'check-pending'}>{working?'Running…':label(result?.status)}</strong></li>{checks.map(c=><li key={c.id}><span>{c.label}</span><strong className={c.result?.status==='passed'?'check-complete':'check-pending'}>{c.state==='finished'?label(c.result?.status):c.state==='recovery_required'?'Needs recovery':'Running…'}</strong></li>)}{!checks.length&&<li><span>Application checks</span><strong className="check-pending">{report?.artifact?'Not configured':'Generate files first'}</strong></li>}</ul>
|
|
41
|
+
{(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
|
+
{error&&<p role="alert">{error}</p>}
|
|
43
|
+
</section>;
|
|
44
|
+
}
|
package/src/WorkbenchDialogs.jsx
CHANGED
|
@@ -154,7 +154,7 @@ export function Connections({ session, connection, onCheck, onSave, onRetry, wel
|
|
|
154
154
|
(kind === "aws" && !capability?.available);
|
|
155
155
|
return (
|
|
156
156
|
<div className="connect">
|
|
157
|
-
<h2>{welcome ? "
|
|
157
|
+
<h2>{welcome ? "AI providers" : "AI providers"}</h2>
|
|
158
158
|
{!session && (
|
|
159
159
|
<p className="error-box">
|
|
160
160
|
Looks like AtMem AI Architect's helper isn't running. Start it with <code>npm run local</code>, then{" "}
|
package/src/conversation.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export function conversationMessages(project, fallback = []) {
|
|
|
6
6
|
function cleanMessages(messages) {
|
|
7
7
|
return messages.filter(m => m && ['user','assistant'].includes(m.role) && typeof m.text === 'string').map(m => ({
|
|
8
8
|
role:m.role, text:m.text,
|
|
9
|
+
...(typeof m.createdAt==='string'&&Number.isFinite(Date.parse(m.createdAt))?{createdAt:m.createdAt}:{}),
|
|
9
10
|
...(typeof m.activityId==='string'&&/^[a-f0-9-]{36}$/.test(m.activityId)?{activityId:m.activityId}:{}),
|
|
10
11
|
...(m.requestId?{requestId:String(m.requestId).slice(0,100)}:{}),
|
|
11
12
|
...(['queued','processing','complete','superseded','failed','cancelled'].includes(m.workState)?{workState:m.workState}:{}),
|
package/src/delivery.mjs
CHANGED
|
@@ -20,7 +20,7 @@ const json = (v) => JSON.stringify(v, null, 2);
|
|
|
20
20
|
const list = (items) => items.length ? items.map((x) => `- ${x}`).join("\n") : "None recorded.";
|
|
21
21
|
export function solutionSpecification(project) {
|
|
22
22
|
const a = project.assessment;
|
|
23
|
-
return `# ${project.name} — solution specification\n\n## Problem and intended outcome\n\n${implementationOutcome(project)}\n\n## Scope\n\n${list((project.useCases || []).map((u) => `${u.title}: ${u.verdict}${u.reason ? ` — ${u.reason}` : ""}`))}\n\n## Requirements and acceptance criteria\n\n${list((project.needs || []).map((r) => `[${requirementNumber(project,r.id)}] ${r.statement} (${r.priority}, ${r.status}); target: ${r.measure ? `${r.measure.metric} ${r.measure.op} ${r.measure.target}${r.measure.unit || ""}` : "acceptance target needed"}; linked blocks: ${r.links?.join(", ") || "not linked"}${`\n\nDesign explanation: ${requirementExplanation(r)}`}${r.basis ? `\n\nBasis: ${r.basis}` : ""}`))}\n\n${systemDescriptionMarkdown(project)}\n\n## Architecture\n\n\`\`\`mermaid\n${mermaid(project)}\n\`\`\`\n\n${resourceMarkdown(project)}\n\n## Options and tradeoffs\n\n${a ? `Scores are entered judgements (1–5), not provider benchmarks or prices. Unscored options remain open.\n\n| Option | ${CRITERIA.map((c) => c.label).join(" | ")} | Weighted score | Rationale |\n|---|${CRITERIA.map(() => "---|").join("")}---|---|\n${OPTIONS.map((o) => `| ${OPTION_NAMES[o]} | ${CRITERIA.map((c) => a.scores?.[o]?.[c.id] ?? "—").join(" | ")} | ${optionScore(a, o) ?? "—"} | ${text(a.notes?.[o])} |`).join("\n")}` : "Use Design → Options to compare cloud choices and record the rationale before choosing."}\n\n##
|
|
23
|
+
return `# ${project.name} — solution specification\n\n## Problem and intended outcome\n\n${implementationOutcome(project)}\n\n## Scope\n\n${list((project.useCases || []).map((u) => `${u.title}: ${u.verdict}${u.reason ? ` — ${u.reason}` : ""}`))}\n\n## Requirements and acceptance criteria\n\n${list((project.needs || []).map((r) => `[${requirementNumber(project,r.id)}] ${r.statement} (${r.priority}, ${r.status}); target: ${r.measure ? `${r.measure.metric} ${r.measure.op} ${r.measure.target}${r.measure.unit || ""}` : "acceptance target needed"}; linked blocks: ${r.links?.join(", ") || "not linked"}${`\n\nDesign explanation: ${requirementExplanation(r)}`}${r.basis ? `\n\nBasis: ${r.basis}` : ""}`))}\n\n${systemDescriptionMarkdown(project)}\n\n## Architecture\n\n\`\`\`mermaid\n${mermaid(project)}\n\`\`\`\n\n${resourceMarkdown(project)}\n\n## Options and tradeoffs\n\n${a ? `Scores are entered judgements (1–5), not provider benchmarks or prices. Unscored options remain open.\n\n| Option | ${CRITERIA.map((c) => c.label).join(" | ")} | Weighted score | Rationale |\n|---|${CRITERIA.map(() => "---|").join("")}---|---|\n${OPTIONS.map((o) => `| ${OPTION_NAMES[o]} | ${CRITERIA.map((c) => a.scores?.[o]?.[c.id] ?? "—").join(" | ")} | ${optionScore(a, o) ?? "—"} | ${text(a.notes?.[o])} |`).join("\n")}` : "Use Design → Options to compare cloud choices and record the rationale before choosing."}\n\n## Design checks and decisions\n\n${adrMarkdown(project.decisions)}\n\n### Design checks\n\n${review(project).map(f=>`- [${f.status}] ${f.statement}\n Rationale: ${f.rationale}\n Next action: ${f.action}\n Source: ${f.source}${project.reviewAccepted?.[f.key]?`\n Accepted by: ${project.reviewAccepted[f.key].owner}; reason: ${project.reviewAccepted[f.key].reason}`:''}`).join('\n\n')}\n\n## Assumptions\n\n${list((project.assumptions || []).map((a) => `[${a.id}] ${a.statement} (${a.madeAt})`))}\n\n## Questions to resolve\n\n${list((project.openQuestions || []).filter((q) => q.status === "open").map((q) => `[${q.id}] ${q.question}${q.owner ? ` — ${q.owner}` : ""}`))}\n`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function deliveryPack(project, run, environment = "staging", now = new Date()) {
|
package/src/estimate.mjs
CHANGED
|
@@ -79,8 +79,8 @@ export function estimate(project, now = new Date()) {
|
|
|
79
79
|
const priced = validNumber(l.price) && /^[A-Z]{3}$/.test(l.currency || "") && l.source?.trim() && l.unit?.trim();
|
|
80
80
|
lines.push({ id: l.id, component: n.data.label, componentId: n.id, detailed: true,
|
|
81
81
|
driver: l.description || "Charge", unit: l.unit, inputs: [], quantity: missing.length ? null : l.quantity,
|
|
82
|
-
missing, price: priced ? l : null,
|
|
83
|
-
cost: !missing.length && priced ?
|
|
82
|
+
missing, note:l.note, price: priced ? l : null,
|
|
83
|
+
cost: !missing.length && priced ? l.quantity * l.price : null,
|
|
84
84
|
status: missing.length ? "input needed" : priced ? "priced" : "price needed" });
|
|
85
85
|
}
|
|
86
86
|
continue;
|
|
@@ -103,7 +103,7 @@ export function estimate(project, now = new Date()) {
|
|
|
103
103
|
quantity,
|
|
104
104
|
missing,
|
|
105
105
|
price: priced ? price : null,
|
|
106
|
-
cost: quantity != null && priced ?
|
|
106
|
+
cost: quantity != null && priced ? quantity * price.price : null,
|
|
107
107
|
status: missing.length ? "input needed" : priced ? "priced" : "price needed",
|
|
108
108
|
});
|
|
109
109
|
}
|
|
@@ -115,6 +115,7 @@ export function estimate(project, now = new Date()) {
|
|
|
115
115
|
const gaps = [
|
|
116
116
|
...(est.details || []).filter((l) => !project.nodes.some((n) => n.id === l.componentId && n.data.kind !== "note")).map((l) => `${l.description || l.id}: linked component was removed; reassign this charge (excluded from totals).`),
|
|
117
117
|
...lines.filter((l) => l.status !== "priced").map((l) => `${l.component} / ${l.driver}: ${l.status}${l.missing.length ? ` (${l.missing.join(", ")})` : ""}.`),
|
|
118
|
+
...lines.filter(l=>l.price?.coverageIncomplete).map(l=>`${l.component}: additional charges are excluded from this estimate.`),
|
|
118
119
|
...notCosted.map((name) => `${name}: no cost lines; add charges or an explicit zero-cost line with a reason.`),
|
|
119
120
|
...(currencies.length > 1 ? ["Choose one currency; different currencies cannot be added."] : []),
|
|
120
121
|
...lines.filter((l) => l.price && (!l.price.region?.trim() || !l.price.service?.trim() || !l.price.date || !Number.isFinite(Date.parse(l.price.date)))).map((l) => `${l.component} / ${l.driver}: confirm region, service/SKU and price date.`),
|
|
@@ -124,6 +125,7 @@ export function estimate(project, now = new Date()) {
|
|
|
124
125
|
return {
|
|
125
126
|
lines,
|
|
126
127
|
notCosted,
|
|
128
|
+
preciseExpected:costed.length&¤cies.length===1?total:null,
|
|
127
129
|
usage: u,
|
|
128
130
|
currency: currencies.length === 1 ? currencies[0] : null,
|
|
129
131
|
mixedCurrencies: currencies.length > 1,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import{unzipSync,zipSync,strToU8,strFromU8}from'fflate';
|
|
2
|
+
const safe=name=>name&&!name.startsWith('/')&&!name.includes('\\')&&!name.split('/').some(p=>p==='..'||p==='.')&&!name.includes('\0');
|
|
3
|
+
function attributes(bytes){
|
|
4
|
+
const view=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);let end=bytes.length-22;
|
|
5
|
+
while(end>=Math.max(0,bytes.length-65557)&&view.getUint32(end,true)!==0x06054b50)end--;
|
|
6
|
+
if(end<0||end+22+view.getUint16(end+20,true)!==bytes.length||view.getUint16(end+4,true)||view.getUint16(end+6,true))throw Error('Unsupported package directory.');
|
|
7
|
+
const result=new Map(),count=view.getUint16(end+10,true);let offset=view.getUint32(end+16,true);
|
|
8
|
+
for(let i=0;i<count;i++){
|
|
9
|
+
if(offset+46>end||view.getUint32(offset,true)!==0x02014b50)throw Error('Invalid package directory.');
|
|
10
|
+
const length=view.getUint16(offset+28,true),extra=view.getUint16(offset+30,true),comment=view.getUint16(offset+32,true);
|
|
11
|
+
if(offset+46+length+extra+comment>end)throw Error('Invalid package entry.');
|
|
12
|
+
const name=strFromU8(bytes.subarray(offset+46,offset+46+length));if(!safe(name)||result.has(name))throw Error('Unsafe or duplicate package entry.');
|
|
13
|
+
result.set(name,{os:view.getUint16(offset+4,true)>>>8,attrs:view.getUint32(offset+38,true)});offset+=46+length+extra+comment;
|
|
14
|
+
}
|
|
15
|
+
if(offset!==end)throw Error('Package directory does not match its receipt.');return result;
|
|
16
|
+
}
|
|
17
|
+
export async function completeProjectZip(verified,design){
|
|
18
|
+
if(!design?.files)throw Error('Wait for the design documents to finish preparing.');
|
|
19
|
+
const bytes=new Uint8Array(await verified.arrayBuffer()),attrs=attributes(bytes);let expanded=0;
|
|
20
|
+
const original=unzipSync(bytes,{filter:entry=>{expanded+=entry.originalSize;if(expanded>150000000)throw Error('Package exceeds its receiving limit.');return true;}});
|
|
21
|
+
const output=Object.fromEntries(Object.entries(original).map(([name,data])=>[name,[data,attrs.get(name)]]));
|
|
22
|
+
const documents={};for(const[name,value]of Object.entries(design.files)){
|
|
23
|
+
if(!safe(name))throw Error('Invalid design document path.');
|
|
24
|
+
const target='design-documents/'+name;if(output[target])throw Error('Design documents conflict with captured files.');
|
|
25
|
+
const content=typeof value==='string'?strToU8(value):value;expanded+=content.byteLength;if(expanded>150000000)throw Error('Combined package exceeds its receiving limit.');
|
|
26
|
+
output[target]=[content,{os:3,attrs:0o100644<<16}];documents[target]=content;
|
|
27
|
+
}
|
|
28
|
+
const hash=async data=>[...new Uint8Array(await crypto.subtle.digest('SHA-256',data))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
29
|
+
const manifest={version:1,designRevision:design.manifest.designRevision,sourceArtifactPreserved:'evidence/original-implementation.zip',documents:await Promise.all(Object.entries(documents).map(async([path,data])=>({path,bytes:data.byteLength,sha256:await hash(data)})))};
|
|
30
|
+
output['package-contents.json']=strToU8(JSON.stringify(manifest,null,2));
|
|
31
|
+
const readme=strFromU8(original['START_HERE.md']||new Uint8Array());
|
|
32
|
+
output['START_HERE.md']=strToU8('# Open this project\n\nUnzip this package, then open this folder as a project in Orca or your editor. Read this file and the implementation README before running commands.\n\n- implementation/: captured application, infrastructure and tests.\n- design-documents/: requirements, Spec Kit, diagram, cost assumptions and generated design references.\n- evidence/: captured source ZIP and recorded checks.\n\nDesign references are not proof that changed requirements were implemented.\n\n'+readme);
|
|
33
|
+
return new Blob([zipSync(output)],{type:'application/zip'});
|
|
34
|
+
}
|
|
@@ -70,3 +70,37 @@
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
.chat-width-toggle{display:none}@media(min-width:901px){.chat-width-toggle{display:inline-flex;margin-left:auto;padding:5px 8px}}
|
|
73
|
+
|
|
74
|
+
.review-costs,.review-generation{padding:20px;border:1px solid var(--line);border-radius:16px;background:var(--paper);margin:16px 0}
|
|
75
|
+
.review-costs>.doc{padding:0}.review-costs h2,.review-generation h2{font-size:1.25rem;margin-top:0}
|
|
76
|
+
.review-finish{display:flex;align-items:center;justify-content:space-between;gap:20px;padding:20px 0;border-top:1px solid var(--line)}
|
|
77
|
+
.review-finish p{margin:5px 0;color:var(--muted);font-size:.9rem}.review-finish>.btn{flex-shrink:0}
|
|
78
|
+
.chat-working-status{display:flex;align-items:center;gap:10px;padding:12px 16px;border-top:1px solid var(--line);background:var(--paper);font-size:.9rem}
|
|
79
|
+
.chat-working-status .brain-activity{flex-shrink:0}.generation-status{display:flex;gap:8px;align-items:center}
|
|
80
|
+
.engineering-panel .generation-conversation{max-height:none;overflow:visible}.engineering-panel .generation-conversation[open]>.engineering-conversation{max-height:300px;overflow:auto}
|
|
81
|
+
.review-generation .engineering-panel textarea{min-height:72px}.review-generation .engineering-actions{display:block}
|
|
82
|
+
@media(max-width:650px){.review-finish{align-items:stretch;flex-direction:column}.review-finish>.btn{width:100%}}
|
|
83
|
+
.chat-jump-latest{position:absolute;right:24px;bottom:230px;z-index:40;border:1px solid var(--line);border-radius:50%;background:var(--paper);color:var(--ink);box-shadow:0 3px 12px #0002;width:40px;height:40px;font-size:24px;cursor:pointer}
|
|
84
|
+
.project-checks{padding:20px;border:1px solid var(--line);border-radius:16px;background:var(--paper);margin:16px 0}.section-action{display:flex;align-items:center;gap:8px}.section-action h3{margin:0}.section-action>.btn{margin-left:auto}
|
|
85
|
+
.check-rows,.final-checklist ul{padding:0;list-style:none}.check-rows li,.final-checklist li{display:flex;align-items:center;gap:12px;padding:10px 0;border-bottom:1px solid var(--line);font-size:.9rem}.check-rows strong,.final-checklist strong{margin-left:auto;font-size:.85rem}.check-complete{color:#217143}.check-pending{color:#946000}.final-checklist{margin:20px 0}.final-checklist li>span:first-child{width:24px;height:24px;display:grid;place-items:center;border:1px solid currentColor;border-radius:50%;flex-shrink:0}
|
|
86
|
+
[data-theme=dark] .check-complete{color:#80d6a2}[data-theme=dark] .check-pending{color:#f0c575}
|
|
87
|
+
.project-checks pre{max-height:300px;overflow:auto;white-space:pre-wrap}.delivery-state{font-weight:600}
|
|
88
|
+
.message-list>.msg{border-top:1px solid var(--line);padding-top:14px;margin-top:14px}.message-meta{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:.75rem;color:var(--muted);margin-bottom:6px}.message-meta time{font-variant-numeric:tabular-nums}
|
|
89
|
+
.memory-modal{width:min(1280px,96vw)!important;max-width:1280px!important;height:min(88vh,1000px);overflow:auto}.memory-panel{width:100%}
|
|
90
|
+
.file-tree{min-width:0;overflow:auto;border-right:1px solid var(--line);padding:8px 0;max-height:650px;background:var(--paper)}.file-tree ul{list-style:none;padding:0;margin:0}.file-tree button{display:flex;align-items:center;gap:5px;width:100%;min-height:30px;text-align:left;background:transparent;color:var(--ink);border:0;border-radius:0;font:inherit;font-size:.8rem;cursor:pointer;white-space:nowrap}.file-tree button:hover{background:var(--accent-soft)}.file-tree button.selected{background:var(--accent-soft);box-shadow:inset 2px 0 var(--accent)}.file-tree button>svg{flex-shrink:0}.tree-indent{width:13px;flex-shrink:0}.file-tree button>span:last-child{overflow:hidden;text-overflow:ellipsis}
|
|
91
|
+
.concise-answer{min-width:0}.message-details{display:inline-flex;align-items:center;min-height:32px;padding:2px 0;text-decoration:underline;text-underline-offset:3px;cursor:pointer;font-size:.85rem}
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
.final-checklist li{padding:0}.final-checklist li>button{display:flex;align-items:center;gap:12px;width:100%;padding:12px 4px;border:0;border-radius:6px;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer}.final-checklist li>button:hover,.final-checklist li>button:focus-visible{background:var(--accent-soft);outline:2px solid var(--ink);outline-offset:-2px}.final-checklist button>span:first-child{width:24px;height:24px;display:grid;place-items:center;border:1px solid currentColor;border-radius:50%;flex-shrink:0}.checklist-arrow{font-size:1.15em}
|
|
95
|
+
|
|
96
|
+
.inline-note{display:block;max-width:100%;margin:6px 0 10px;line-height:1.5;color:var(--muted);overflow-wrap:anywhere}
|
|
97
|
+
|
|
98
|
+
.cost-size-selector .btn{display:inline-flex;align-items:center;gap:6px}.cost-size-selector .btn[aria-pressed="true"]{font-weight:600}.cost-selection-status{font-size:.9rem;color:var(--muted)}
|
|
99
|
+
|
|
100
|
+
.cost-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.cost-heading h2{margin:0}.cost-heading>.btn{flex-shrink:0}
|
|
101
|
+
|
|
102
|
+
.memory-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.memory-heading h2,.memory-heading h3{margin:0}.memory-profile-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 0;border-bottom:1px solid var(--line);margin-bottom:12px}.memory-profile-bar label{display:flex;align-items:center;gap:8px}.memory-profile-bar input{max-width:190px}.memory-profile-bar code{font-size:.72rem;overflow-wrap:anywhere}.memory-state{margin-left:auto}.memory-toolbar{display:flex;gap:10px;align-items:center;margin:16px 0 10px}.memory-toolbar input{flex:1;min-width:100px}.memory-toolbar strong{white-space:nowrap;font-size:.85rem}.memory-table-wrap{max-height:50vh;overflow:auto;border:1px solid var(--line);border-radius:10px}.memory-table{width:100%;border-collapse:collapse;font-size:.8rem;table-layout:fixed}.memory-table th{position:sticky;top:0;background:var(--paper);z-index:1;font-weight:600;text-align:left}.memory-table td,.memory-table th{padding:10px 12px;border-bottom:1px solid var(--line);vertical-align:top;overflow-wrap:anywhere}.memory-table th:first-child{width:20%}.memory-table th:nth-child(2){width:35%}.memory-table th:last-child{width:60px}.memory-table td:nth-child(5){font-variant-numeric:tabular-nums;font-size:.73rem}.memory-content{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;border:0;padding:0;background:none;color:var(--ink);font:inherit;text-align:left;cursor:pointer}.memory-table .link{padding:0;font:inherit;text-align:left;overflow-wrap:anywhere}.memory-badge{font-size:.72rem;border-radius:20px;padding:3px 6px;background:var(--board)}.memory-badge.active{color:#21633a;background:#e8f4eb}.memory-pagination{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:12px 0}.memory-pagination>span{margin-right:auto;font-size:.8rem}.memory-preference-form{display:flex;gap:10px;flex-wrap:wrap;padding:12px 0}.memory-preference-form input{flex:1;min-width:140px}.memory-record-detail{padding:16px;border:1px solid var(--line);border-radius:10px;margin:12px 0}.memory-record-detail dl{display:grid;grid-template-columns:140px 1fr;gap:8px;font-size:.8rem}.memory-record-detail dd{margin:0;overflow-wrap:anywhere}.memory-record-detail p{white-space:pre-wrap}.memory-feedback{display:flex;align-items:center;gap:6px;font-size:.8rem;margin:8px 0}.memory-empty{padding:32px;text-align:center;color:var(--muted)}.memory-table-wrap[aria-busy=true]{opacity:.65}.memory-preview{padding:16px;border:1px solid var(--line);border-radius:10px}.memory-preview .btn{margin-right:8px}@media(max-width:700px){.memory-table{min-width:800px}.memory-profile-bar code{font-size:.65rem}.memory-record-detail dl{grid-template-columns:1fr}.memory-toolbar{flex-wrap:wrap}}
|
|
103
|
+
|
|
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
|
+
|
|
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)}
|