@atmem/architect 0.2.0-alpha.4.1 → 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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +21 -21
- package/cli/runtime.mjs +3 -1
- package/dist/assets/{index-qtCM2D7O.js → index-D4CZ_n8f.js} +19166 -19094
- package/dist/assets/index-DuveVTIx.css +1 -0
- package/dist/assets/{index.es-C6C0sneB.js → index.es-Fcqg-orY.js} +1 -1
- package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-4PDDTAP7.js} +3 -3
- package/dist/index.html +2 -2
- package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
- package/dist/licenses/memory-dependency-notices.txt +149 -2241
- package/package.json +3 -3
- package/public/licenses/atmem-APACHE-2.0.txt +202 -0
- package/public/licenses/memory-dependency-notices.txt +149 -2241
- 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-requirements.lock +6 -6
- package/server/memory-requirements.txt +2 -2
- package/server/memory_context.py +58 -0
- package/server/memory_routes.py +67 -8
- package/server/memory_service.py +40 -5
- package/server/memory_worker.py +76 -11
- package/server/orca_handoff.py +90 -0
- package/server/pricing.py +161 -1
- package/server/prompts.py +13 -0
- package/server/request_uploads.py +65 -0
- package/server/terraform_routes.py +47 -1
- 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 +35 -28
- package/src/FileTree.jsx +7 -0
- package/src/FinalChecklist.jsx +23 -0
- package/src/HelpNote.jsx +3 -0
- package/src/ImplementationDelivery.jsx +56 -17
- package/src/JourneyViews.jsx +20 -17
- package/src/LocationChoices.jsx +12 -0
- package/src/MemoryPanel.jsx +43 -31
- package/src/OpenAIVoicePanel.jsx +15 -14
- package/src/OutputsView.jsx +28 -16
- package/src/ProjectChecks.jsx +58 -0
- package/src/StarPrompt.jsx +26 -0
- package/src/WorkbenchDialogs.jsx +1 -1
- package/src/conversation.mjs +1 -0
- package/src/delivery.mjs +1 -1
- package/src/engineering.css +10 -0
- package/src/estimate.mjs +5 -3
- package/src/final-package.mjs +34 -0
- package/src/journey-workspace.css +39 -0
- package/src/journey.mjs +12 -2
- package/src/large-request.mjs +25 -0
- package/src/main.jsx +129 -98
- package/src/output-bundle.mjs +18 -4
- package/src/pricing.mjs +59 -3
- package/src/project-file.mjs +1 -1
- 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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Bounded Windows snapshots with held, no-reparse, read-sharing-only handles.
|
|
2
|
+
|
|
3
|
+
All handles stay open until the complete tree is audited. Writers, replacements,
|
|
4
|
+
reparse points, hard links and non-disk objects fail closed; nothing is executed.
|
|
5
|
+
References: Microsoft CreateFileW and GetFileInformationByHandle documentation.
|
|
6
|
+
"""
|
|
7
|
+
import ctypes
|
|
8
|
+
from ctypes import wintypes as w
|
|
9
|
+
import hashlib
|
|
10
|
+
import io
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
import stat
|
|
14
|
+
import zipfile
|
|
15
|
+
from .engineering_sessions import SessionError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def snapshot(workspace, *, excluded, max_bytes, max_files):
|
|
19
|
+
if os.name != 'nt':
|
|
20
|
+
raise SessionError('Windows snapshot handles require Windows')
|
|
21
|
+
kernel = ctypes.WinDLL('kernel32', use_last_error=True)
|
|
22
|
+
class Info(ctypes.Structure):
|
|
23
|
+
_fields_ = [('attributes', w.DWORD), ('created', w.FILETIME),
|
|
24
|
+
('accessed', w.FILETIME), ('written', w.FILETIME),
|
|
25
|
+
('volume', w.DWORD), ('size_hi', w.DWORD), ('size_lo', w.DWORD),
|
|
26
|
+
('links', w.DWORD), ('index_hi', w.DWORD), ('index_lo', w.DWORD)]
|
|
27
|
+
kernel.CreateFileW.argtypes = [w.LPCWSTR,w.DWORD,w.DWORD,w.LPVOID,w.DWORD,w.DWORD,w.HANDLE]
|
|
28
|
+
kernel.CreateFileW.restype = w.HANDLE
|
|
29
|
+
kernel.CloseHandle.argtypes = [w.HANDLE]
|
|
30
|
+
kernel.CloseHandle.restype = w.BOOL
|
|
31
|
+
kernel.GetFileInformationByHandle.argtypes = [w.HANDLE,ctypes.POINTER(Info)]
|
|
32
|
+
kernel.GetFileInformationByHandle.restype = w.BOOL
|
|
33
|
+
kernel.GetFileType.argtypes = [w.HANDLE]
|
|
34
|
+
kernel.GetFileType.restype = w.DWORD
|
|
35
|
+
kernel.GetFinalPathNameByHandleW.argtypes = [w.HANDLE,w.LPWSTR,w.DWORD,w.DWORD]
|
|
36
|
+
kernel.GetFinalPathNameByHandleW.restype = w.DWORD
|
|
37
|
+
kernel.ReadFile.argtypes = [w.HANDLE,w.LPVOID,w.DWORD,ctypes.POINTER(w.DWORD),w.LPVOID]
|
|
38
|
+
kernel.ReadFile.restype = w.BOOL
|
|
39
|
+
invalid = ctypes.c_void_p(-1).value
|
|
40
|
+
held, entries, exclusions, observed = [], [], [], {}
|
|
41
|
+
output, total = io.BytesIO(), 0
|
|
42
|
+
root = Path(workspace).absolute()
|
|
43
|
+
if str(root).startswith('\\\\'):
|
|
44
|
+
raise SessionError('Windows capture requires a local disk workspace')
|
|
45
|
+
def fail():
|
|
46
|
+
raise SessionError('Artifact contains a changed, linked or unavailable Windows entry; no package was saved')
|
|
47
|
+
def information(handle):
|
|
48
|
+
info=Info()
|
|
49
|
+
if not kernel.GetFileInformationByHandle(handle,ctypes.byref(info)):fail()
|
|
50
|
+
if info.attributes & 0x400 or kernel.GetFileType(handle)!=1:fail()
|
|
51
|
+
return info
|
|
52
|
+
def identity(info):
|
|
53
|
+
return (info.volume,info.index_hi,info.index_lo,info.attributes,info.links,
|
|
54
|
+
info.size_hi,info.size_lo,info.created.dwHighDateTime,info.created.dwLowDateTime,
|
|
55
|
+
info.written.dwHighDateTime,info.written.dwLowDateTime)
|
|
56
|
+
def open_entry(path):
|
|
57
|
+
# No FILE_SHARE_WRITE or FILE_SHARE_DELETE: retain every handle through audit.
|
|
58
|
+
handle=kernel.CreateFileW('\\\\?\\'+str(path),0x80000000,1,None,3,0x02200000,None)
|
|
59
|
+
if handle==invalid:fail()
|
|
60
|
+
held.append(handle)
|
|
61
|
+
info=information(handle)
|
|
62
|
+
actual=ctypes.create_unicode_buffer(32768)
|
|
63
|
+
length=kernel.GetFinalPathNameByHandleW(handle,actual,len(actual),0)
|
|
64
|
+
if not length or length>=len(actual) or os.path.normcase(actual.value)!=os.path.normcase('\\\\?\\'+str(path)):fail()
|
|
65
|
+
return handle,info
|
|
66
|
+
def names(path):
|
|
67
|
+
result=[]
|
|
68
|
+
for name in sorted(os.listdir(path)):
|
|
69
|
+
if name in excluded:continue
|
|
70
|
+
if name in {'.','..'} or name.endswith((' ','.')) or any(ord(c)<32 or c in '\\/:"<>|?*' for c in name):fail()
|
|
71
|
+
result.append(name)
|
|
72
|
+
return result
|
|
73
|
+
def walk(path,relative,handle,info,archive):
|
|
74
|
+
nonlocal total
|
|
75
|
+
observed[relative]=(path,handle,identity(info),None)
|
|
76
|
+
if info.attributes & 0x10:
|
|
77
|
+
children=names(path)
|
|
78
|
+
observed[relative]=(path,handle,identity(info),children)
|
|
79
|
+
exclusions.extend((relative+'/' if relative else '')+name for name in sorted(os.listdir(path)) if name in excluded)
|
|
80
|
+
for name in children:
|
|
81
|
+
child_relative=(relative+'/' if relative else '')+name
|
|
82
|
+
child,child_info=open_entry(path/name)
|
|
83
|
+
directory=bool(child_info.attributes & 0x10)
|
|
84
|
+
if len(entries)>=max_files:raise SessionError('Artifact entry limit exceeded; no partial package was saved')
|
|
85
|
+
if directory:
|
|
86
|
+
entries.append({'path':child_relative+'/','kind':'directory'})
|
|
87
|
+
item=zipfile.ZipInfo(child_relative+'/');item.external_attr=(stat.S_IFDIR|0o755)<<16
|
|
88
|
+
archive.writestr(item,b'')
|
|
89
|
+
walk(path/name,child_relative,child,child_info,archive)
|
|
90
|
+
else:
|
|
91
|
+
size=(info.size_hi<<32)|info.size_lo
|
|
92
|
+
if info.links!=1 or total+size>max_bytes:fail()
|
|
93
|
+
content=bytearray()
|
|
94
|
+
while True:
|
|
95
|
+
buffer=ctypes.create_string_buffer(min(1_000_000,max_bytes-total-len(content)+1))
|
|
96
|
+
read=w.DWORD()
|
|
97
|
+
if not kernel.ReadFile(handle,buffer,len(buffer),ctypes.byref(read),None):fail()
|
|
98
|
+
if not read.value:break
|
|
99
|
+
content.extend(buffer.raw[:read.value])
|
|
100
|
+
if total+len(content)>max_bytes:fail()
|
|
101
|
+
if len(content)!=size or identity(information(handle))!=identity(info):fail()
|
|
102
|
+
entries.append({'path':relative,'kind':'file','bytes':size,'sha256':hashlib.sha256(content).hexdigest()})
|
|
103
|
+
item=zipfile.ZipInfo(relative);item.compress_type=zipfile.ZIP_DEFLATED
|
|
104
|
+
mode=0o755 if path.suffix.lower() in {'.sh','.exe'} else 0o644
|
|
105
|
+
item.external_attr=(stat.S_IFREG|mode)<<16
|
|
106
|
+
archive.writestr(item,content)
|
|
107
|
+
total+=size
|
|
108
|
+
try:
|
|
109
|
+
handle,info=open_entry(root)
|
|
110
|
+
if not info.attributes & 0x10:fail()
|
|
111
|
+
with zipfile.ZipFile(output,'w',compression=zipfile.ZIP_DEFLATED)as archive:
|
|
112
|
+
walk(root,'',handle,info,archive)
|
|
113
|
+
for path,handle,expected,children in observed.values():
|
|
114
|
+
if identity(information(handle))!=expected or children is not None and names(path)!=children:fail()
|
|
115
|
+
return output.getvalue(),entries,exclusions
|
|
116
|
+
except OSError:
|
|
117
|
+
fail()
|
|
118
|
+
finally:
|
|
119
|
+
for handle in reversed(held):kernel.CloseHandle(handle)
|
|
@@ -13,12 +13,6 @@ function WorkPanel({project,onChange,busy=false}){
|
|
|
13
13
|
const priority={changed:0,missing:0,unreviewed:1,current:2};
|
|
14
14
|
const mappings=model.entities.filter(e=>['responsibility','delivery-unit','interface','implementation','assumption','decision','evidence','requirement','removed'].includes(e.kind)).sort((a,b)=>priority[a.contextStatus]-priority[b.contextStatus]);
|
|
15
15
|
function apply(change){onChange(change,project.id);setError('');}
|
|
16
|
-
async function importProposal(event){const file=event.target.files?.[0];event.target.value='';if(!file)return;try{
|
|
17
|
-
if(file.size>1_500_000)throw Error('Work proposal exceeds its storage limit. Nothing was imported.');
|
|
18
|
-
const value=JSON.parse(await file.text());
|
|
19
|
-
if(value.version!==1||!Array.isArray(value.records)||Object.keys(value).some(k=>!['version','records'].includes(k)))throw Error('Use a version1 work-record proposal.');
|
|
20
|
-
apply({kind:'propose',records:value.records});
|
|
21
|
-
}catch(e){setError(e.message);}}
|
|
22
16
|
return <details className="architecture-work"><summary>Architecture work context · {mappings.filter(e=>e.contextStatus==='changed'||e.contextStatus==='missing').length} need review</summary>
|
|
23
17
|
<p>Requirements, responsibilities, interfaces and implementation mappings share their change context. A current context is a recorded review; it does not establish that an implementation or test passed.</p>
|
|
24
18
|
<p>{intents} intent {intents===1?'source':'sources'} · {requirements} {requirements===1?'requirement':'requirements'}. Free-form engineering updates affect every mapping until reconciled.</p>
|
|
@@ -30,7 +24,6 @@ function WorkPanel({project,onChange,busy=false}){
|
|
|
30
24
|
{e.missingRefs.length>0&&<p>Missing: {e.missingRefs.map(sourceName).join(', ')}</p>}
|
|
31
25
|
{onChange&&e.contextStatus!=='missing'&&<button disabled={busy} onClick={()=>{try{apply({kind:'review',ref:e.id,expectedContext:workContextIdentity(project,e.id)});}catch(e){setError(e.message);}}}>Record context review: {e.title}</button>}
|
|
32
26
|
</li>)}</ul>:<p>No responsibilities or implementation mappings recorded yet. Engineering can propose them without changing the existing diagram.</p>}
|
|
33
|
-
{onChange&&<label>Import proposed work mappings<input type="file" accept="application/json,.json" disabled={busy} onChange={importProposal}/></label>}
|
|
34
27
|
{result.artifacts.mappings.length>0&&<section aria-label="Linked artifact mappings">
|
|
35
28
|
<h3>Linked artifacts</h3><p>File and context references remain proposed. They do not establish that a test passed or that the implementation meets every requirement.</p>
|
|
36
29
|
<ul>{result.artifacts.mappings.map(m=><li key={m.id} data-artifact-mapping={m.artifactId}>
|
package/src/ArtifactChecks.jsx
CHANGED
|
@@ -25,8 +25,8 @@ export function ArtifactChecks({session,projectId,sessionId,artifact,readOnly=fa
|
|
|
25
25
|
catch(e){if(live.current)setError(e.message);}finally{locked.current=false;if(live.current)setBusy(false);}
|
|
26
26
|
}
|
|
27
27
|
return <section className="artifact-checks" aria-label="Captured package checks">
|
|
28
|
-
<h3>
|
|
29
|
-
{!available&&<p>
|
|
28
|
+
<h3>Check results</h3>
|
|
29
|
+
{!available&&<p>Independent checks have not run: no check runner is configured.</p>}
|
|
30
30
|
{!readOnly&&catalog.map(validator=>{
|
|
31
31
|
const previous=checks.filter(check=>check.validatorId===validator.id&&check.contractSha256===validator.contractSha256).at(-1);
|
|
32
32
|
const active=previous&&previous.state!=='finished';
|
package/src/ChatMessage.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, {useId, useState} from 'react';
|
|
2
2
|
// Small, non-HTML Markdown renderer: no remote images, raw HTML or executable URLs.
|
|
3
3
|
function inline(text){return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\(https?:\/\/[^\s)]+\))/g).map((part,i)=>{
|
|
4
4
|
if(part.startsWith('**')&&part.endsWith('**'))return <strong key={i}>{part.slice(2,-2)}</strong>;
|
|
@@ -6,7 +6,17 @@ function inline(text){return text.split(/(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\(http
|
|
|
6
6
|
const link=part.match(/^\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)$/);if(link)return <a key={i} href={link[2]} target="_blank" rel="noopener noreferrer">{link[1]}</a>;
|
|
7
7
|
return part;
|
|
8
8
|
});}
|
|
9
|
-
export function
|
|
9
|
+
export function messagePreview(text){
|
|
10
|
+
const words=text.trim().split(/\s+/);if(words.length<=20)return text;
|
|
11
|
+
const questions=text.split(/\n+/).map(line=>line.trim()).filter(line=>line.includes('?')).map(line=>line.match(/(?:^|[.!]\s+)([^.!]*\?)/)?.[1]||line.slice(0,line.indexOf('?')+1));
|
|
12
|
+
const question=questions.at(-1);if(question&&question.split(/\s+/).length<=20)return question;
|
|
13
|
+
return words.slice(0,20).join(' ')+'…';
|
|
14
|
+
}
|
|
15
|
+
export function ChatMessage({text='',concise=false}){
|
|
16
|
+
const [expanded,setExpanded]=useState(false);
|
|
17
|
+
const detailId=useId();
|
|
18
|
+
if(concise&&text.trim().split(/\s+/).length>20)return <div className="concise-answer"><div id={detailId}><ChatMessage text={expanded?text:messagePreview(text)}/></div><button type="button" className="link message-details" aria-expanded={expanded} aria-controls={detailId} onClick={()=>setExpanded(value=>!value)}>{expanded?'Less details':'More details'}</button></div>;
|
|
19
|
+
|
|
10
20
|
const lines=text.split('\n'),blocks=[];let i=0;
|
|
11
21
|
while(i<lines.length){
|
|
12
22
|
if(!lines[i].trim()){i++;continue;}
|
package/src/EngagementViews.jsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {InlineNote} from './HelpNote.jsx';
|
|
1
2
|
import {BrainActivity} from "./BrainActivity.jsx";
|
|
2
3
|
import {systemDescription,requirementExplanation} from './system-description.mjs';
|
|
3
4
|
import { resolveCommentRevision } from "./consultation.mjs";
|
|
@@ -136,16 +137,16 @@ export function OptionsView({ project, onDiscuss, onPreview, onChoose, onContinu
|
|
|
136
137
|
const ready = Object.values(notes).some(Boolean);
|
|
137
138
|
const selected = [...(project.decisions||[])].reverse().find(d=>d.subject==='platform.cloud'&&d.status==='Accepted')?.decision;
|
|
138
139
|
return <section className="doc" aria-label="Choices">
|
|
139
|
-
<header className="doc-head"><h2>Choices</h2
|
|
140
|
+
<header className="doc-head"><h2>Choices</h2></header>
|
|
140
141
|
<button className="btn" onClick={() => onDiscuss('Help me compare AWS, Azure, Google Cloud and IBM for this design. Ask one useful question about my priorities, then refine the comparison from my answer. Populate optionAssessment when enough is known. Keep the diagram unchanged.')}>Discuss cloud options</button>
|
|
141
|
-
{selected && <p className="cloud-selection" role="status"><strong>Selected: {OPTION_NAMES[selected]}</strong>
|
|
142
|
+
{selected && <p className="cloud-selection" role="status"><strong>Selected: {OPTION_NAMES[selected]}</strong> <button className="btn primary" disabled={busy} onClick={onContinue}>Review →</button></p>}
|
|
142
143
|
<DecisionsView project={project} onDiscuss={onDiscuss} embedded/>
|
|
143
144
|
<h3>Cloud alternatives</h3>
|
|
144
|
-
|
|
145
|
+
|
|
145
146
|
{['current','aws','azure','gcp','ibm'].filter(id => notes[id]).map(id => <article className="consultation-summary" key={id}>
|
|
146
|
-
<h3>{OPTION_NAMES[id]}{selected===id ? " · Selected" : ""}</h3><
|
|
147
|
+
<h3>{OPTION_NAMES[id]}{selected===id ? " · Selected" : ""}</h3><InlineNote label={`About ${OPTION_NAMES[id]}`}>{notes[id].replace(/^(?:AtMem AI Architect|At[Ff]orma) proposal — confirm: /,'')}</InlineNote>
|
|
147
148
|
<button className="link" onClick={() => onDiscuss(`Explain how ${OPTION_NAMES[id]} fits our design and the main tradeoff. Ask one question to help me decide; keep this as a discussion until I explicitly choose.`)}>Discuss this option</button>
|
|
148
|
-
{id!=="current" && <><button className="link" onClick={()=>onPreview(id)}>Preview service architecture</button><button className="btn primary" disabled={busy||selected===id} onClick={()=>onChoose(id)}>{selected===id ? `${OPTION_NAMES[id]} selected` :
|
|
149
|
+
{id!=="current" && <><button className="link" onClick={()=>onPreview(id)}>Preview service architecture</button><button className="btn primary" disabled={busy||selected===id} onClick={()=>onChoose(id)}>{selected===id ? `${OPTION_NAMES[id]} selected` : `${OPTION_NAMES[id]} →`}</button></>}
|
|
149
150
|
</article>)}
|
|
150
151
|
</section>;
|
|
151
152
|
}
|
|
@@ -157,13 +158,13 @@ export function DecisionsView({ project, onDiscuss, embedded=false }) {
|
|
|
157
158
|
const sketches = all.filter(d => d.subject?.startsWith('design.'));
|
|
158
159
|
const labels = {'platform.cloud':'Cloud provider','cloud.region':'Region','data.country':'Country / data residency','infrastructure.tool':'Infrastructure tool'};
|
|
159
160
|
return <section className={embedded?undefined:"doc"} aria-label="Decisions">
|
|
160
|
-
<header className="doc-head"><h2>
|
|
161
|
+
<header className="doc-head"><h2>Selected choices</h2></header>
|
|
161
162
|
<button className="btn" onClick={() => onDiscuss('Review our current design and explicit choices. Explain the next choice we need to make and ask me one useful question. Do not treat a sketch or a suggestion as an accepted decision.')}>Discuss the next choice</button>
|
|
162
|
-
{!current.length && <p className="muted">No choices
|
|
163
|
+
{!current.length && <p className="muted">No choices selected.</p>}
|
|
163
164
|
{current.map(d => <article className="consultation-summary" key={d.id}>
|
|
164
165
|
<h3>{labels[d.subject] || d.title}</h3><p>{OPTION_NAMES[d.decision] || d.decision || 'Still being discussed'}</p>
|
|
165
166
|
<small className="muted">{d.status === 'Accepted' ? 'Confirmed in our conversation' : 'Proposed — still to discuss'}</small>
|
|
166
|
-
{d.context
|
|
167
|
+
{(d.context||d.consequences)&&<InlineNote label="Decision details"><p>{d.context}</p><p>{d.consequences}</p></InlineNote>}
|
|
167
168
|
<button className="link" onClick={() => onDiscuss(`Let’s revisit our choice about ${labels[d.subject] || d.title}: ${d.decision}. Explain the tradeoff and ask what I would like to change before updating it.`)}>Revisit in chat</button>
|
|
168
169
|
{choices.some(h => h.subject===d.subject && h.status==='Superseded') && <details className="extra"><summary>Earlier choices</summary>{choices.filter(h => h.subject===d.subject && h.status==='Superseded').map(h=><p key={h.id}>{h.decision} {h.reason && `— ${h.reason}`}</p>)}</details>}
|
|
169
170
|
</article>)}
|
|
@@ -297,7 +298,7 @@ function RequirementComments({ requirement, comments, onSave, onRemove, target,
|
|
|
297
298
|
</div> : <><p className="comment-status">{c.reviewMessage || 'Saved feedback. Ask AtMem AI Architect to suggest wording, then review the change here.'}</p><button className="requirement-add-comment" disabled={reviewing} onClick={()=>onReview([c])}>{reviewing?<><BrainActivity/> Preparing suggestions…</>:'Suggest a revision'}</button><button className="link" onClick={()=>onDiscuss(`Help me clarify my feedback on ${number} (${requirement.id}): ${c.text}. Ask one question before proposing a change.`)}>Discuss feedback</button></>}
|
|
298
299
|
<button className="link" onClick={()=>onRemove(c.id)} aria-label={`Remove comment ${c.id}`}>Remove</button>
|
|
299
300
|
</div>)}
|
|
300
|
-
{comments.some(c=>['reviewed','dismissed'].includes(c.status)) && <
|
|
301
|
+
{comments.some(c=>['reviewed','dismissed'].includes(c.status)) && <section className="comment-history"><h4>Feedback history</h4>{comments.filter(c=>['reviewed','dismissed'].includes(c.status)).map(c=><div key={c.id}><p>{c.text}</p><small>{c.status==='reviewed'?'Revision applied':'Original wording kept'}</small>{c.after&&<p>{c.after}</p>}</div>)}</section>}
|
|
301
302
|
{!open ? <button className="requirement-add-comment" onClick={()=>{setQuote('');setOpen(true);}}>Comment or request a change</button> : <div className="comment-composer">
|
|
302
303
|
<strong>Feedback on {number}</strong>{quote && <blockquote>{quote}</blockquote>}
|
|
303
304
|
<textarea ref={editor} aria-label={`Comment on ${requirement.id}`} value={text} maxLength={500} onChange={e=>setText(e.target.value)} placeholder="What doesn’t fit, what’s missing, or what would you like explained?" />
|
|
@@ -309,7 +310,7 @@ function RequirementComments({ requirement, comments, onSave, onRemove, target,
|
|
|
309
310
|
</aside>;
|
|
310
311
|
}
|
|
311
312
|
|
|
312
|
-
export function RequirementsView({ project, onDiscuss, onChange, onReview, reviewing, onUpload, onOpenProject }) {
|
|
313
|
+
export function RequirementsView({checks,onContinue, project, onDiscuss, onChange, onReview, reviewing, onUpload, onOpenProject }) {
|
|
313
314
|
const [commentMenu,setCommentMenu]=React.useState(null), [commentTarget,setCommentTarget]=React.useState(null);
|
|
314
315
|
React.useEffect(()=>{
|
|
315
316
|
if(!commentMenu)return;
|
|
@@ -344,7 +345,7 @@ export function RequirementsView({ project, onDiscuss, onChange, onReview, revie
|
|
|
344
345
|
const purpose = project.brief?.application || project.requirements;
|
|
345
346
|
const system=systemDescription(project);
|
|
346
347
|
return <section className="doc requirements-document" aria-label="Requirements">
|
|
347
|
-
<div className="requirements-actions"
|
|
348
|
+
<div className="requirements-actions">
|
|
348
349
|
{!!pending.length && <button className="btn" disabled={reviewing} onClick={()=>onReview(pending.slice(0,6))}>{reviewing?'Reviewing comments…':'Suggest revisions for saved feedback'} ({Math.min(6,pending.length)})</button>}
|
|
349
350
|
</div>
|
|
350
351
|
{commentMenu && <div className="ctx-menu requirement-text-menu" role="menu" aria-label="Selected text actions" style={{left:commentMenu.x,top:commentMenu.y}} onPointerDown={e=>e.stopPropagation()}>
|
|
@@ -353,7 +354,7 @@ export function RequirementsView({ project, onDiscuss, onChange, onReview, revie
|
|
|
353
354
|
<article className="requirements-paper" aria-label="Requirements document">
|
|
354
355
|
<header className="requirements-cover">
|
|
355
356
|
<p className="document-kicker">AtMem AI Architect · Working requirements</p>
|
|
356
|
-
<h2>{project.name || 'Solution requirements'}</h2
|
|
357
|
+
<h2>{project.name || 'Solution requirements'}</h2>
|
|
357
358
|
<p className="document-subtitle">Where we’ve reached</p>
|
|
358
359
|
<p>This document brings together the requirements developed from our board and discussion. Proposed requirements remain open for review as the design evolves.</p>
|
|
359
360
|
<p className="document-meta">Design revision {project.revision || 1} · Working draft</p>
|
|
@@ -362,9 +363,9 @@ export function RequirementsView({ project, onDiscuss, onChange, onReview, revie
|
|
|
362
363
|
<p>{purpose || 'We are developing the scope together. Describe the outcome you want in chat, and this document will grow with the design.'}</p>
|
|
363
364
|
</section>
|
|
364
365
|
<section className="requirements-chapter"><h3>How the system works</h3><p>{system.overview}</p>{system.flows.map((f,i)=><div className="system-interaction" key={f.id}><h4>{i+1}. {f.title}</h4><p>{f.description}</p><p>{f.contract}</p><p className="document-meta">Proposed verification: {f.validation}</p></div>)}</section>
|
|
365
|
-
<section className="requirements-chapter"><h3>Component responsibilities and design rationale</h3>{system.components.map(c=><section key={c.id} className="system-component"><h4>{c.name}</h4><p className="document-meta">{c.service} · {c.placement}</p><p>{c.purpose}</p><p>{c.boundary}</p><h5>Inputs and outputs</h5><ul>{[...c.inputs,...c.outputs].map((v,i)=><li key={i}>{v}</li>)}</ul>{!c.inputs.length&&!c.outputs.length&&<p>No connections are drawn for this component.</p>}<h5>Contribution to requirements</h5>{c.requirements.length?<ul>{c.requirements.map(r=><li key={r}>{r}</li>)}</ul>:<p>No requirement is explicitly linked to this component yet.</p>}<h5>Implementation and verification</h5><p>{c.implementation}</p><p className="document-meta">Engineering considerations, to be confirmed during implementation:</p><ul>{c.engineering.map(v=><li key={v}>{v}</li>)}</ul></section>)}</section>
|
|
366
|
-
{[...groups].sort(([a],[b])=>(a==='suggested')-(b==='suggested')).map(([key,rows])=>{const Tag=
|
|
367
|
-
{key==='suggested'?<
|
|
366
|
+
<section className="requirements-chapter"><h3>Component responsibilities and design rationale</h3>{system.components.map(c=><section key={c.id} id={`component-spec-${c.id}`} className="system-component"><h4>{c.name}</h4><p className="document-meta">{c.service} · {c.placement}</p><p>{c.purpose}</p><p>{c.boundary}</p><h5>Inputs and outputs</h5><ul>{[...c.inputs,...c.outputs].map((v,i)=><li key={i}>{v}</li>)}</ul>{!c.inputs.length&&!c.outputs.length&&<p>No connections are drawn for this component.</p>}<h5>Contribution to requirements</h5>{c.requirements.length?<ul>{c.requirements.map(r=><li key={r}>{r}</li>)}</ul>:<p>No requirement is explicitly linked to this component yet.</p>}<h5>Implementation and verification</h5><p>{c.implementation}</p><p className="document-meta">Engineering considerations, to be confirmed during implementation:</p><ul>{c.engineering.map(v=><li key={v}>{v}</li>)}</ul></section>)}</section>
|
|
367
|
+
{[...groups].sort(([a],[b])=>(a==='suggested')-(b==='suggested')).map(([key,rows])=>{const Tag='section';return <Tag className="requirements-chapter" key={key}>
|
|
368
|
+
{key==='suggested'?<h4>Suggestions still to discuss ({rows.length})</h4>:<h3>{titles[key] || 'Additional requirements'}</h3>}
|
|
368
369
|
{key==='suggested'&&<p className="document-meta">These are early considerations, not agreed requirements. We’ll discuss the most relevant ones as the design develops.</p>}
|
|
369
370
|
{rows.map(r=><div className="requirement-paragraph" key={r.id}>
|
|
370
371
|
<h4 className="requirement-number">{requirementNumber(project,r.id)}</h4>
|
|
@@ -379,7 +380,8 @@ export function RequirementsView({ project, onDiscuss, onChange, onReview, revie
|
|
|
379
380
|
{!needs.length && <section className="requirements-chapter"><h3>Requirements to develop</h3><p>As we discuss the idea and shape the board, AtMem AI Architect will summarise the functional requirements, constraints and acceptance targets here.</p></section>}
|
|
380
381
|
{!!project.assumptions?.length && <section className="requirements-chapter"><h3>Assumptions to validate</h3>{project.assumptions.map(a=><p key={a.id}>{a.statement}</p>)}</section>}
|
|
381
382
|
{!!questions.length && <section className="requirements-chapter"><h3>Open questions</h3>{questions.map(q=><p key={q.id}>{q.question}</p>)}<button className="link document-discuss" onClick={()=>onDiscuss(`Help resolve the open questions in our requirements document, one at a time. Start with the question that most affects the design.`)}>Let’s work through these in chat</button></section>}
|
|
382
|
-
|
|
383
|
+
{checks&&<section className="requirements-chapter"><h3>Design checks and decisions</h3>{checks}</section>}
|
|
384
|
+
<footer className="document-footer">{onContinue&&<button className="btn primary" disabled={reviewing} onClick={onContinue}>Continue to Choices →</button>}</footer>
|
|
383
385
|
</article>
|
|
384
386
|
</section>;
|
|
385
387
|
}
|
package/src/EngineeringPanel.jsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {BrainActivity} from './BrainActivity.jsx';
|
|
2
2
|
import React,{useEffect,useRef,useState} from 'react';
|
|
3
3
|
import {engineeringCall,engineeringConversation,mergeEngineeringEvents} from './engineering-client.mjs';
|
|
4
4
|
import './engineering.css';
|
|
@@ -6,11 +6,12 @@ import {ArtifactChecks} from './ArtifactChecks.jsx';
|
|
|
6
6
|
import {verifyArtifactWork} from './artifact-work.mjs';
|
|
7
7
|
import {hasArchitectureDesign,buildIntent,BUILD_AND_TEST_REQUEST} from './design-build-flow.mjs';
|
|
8
8
|
|
|
9
|
-
export function EngineeringPanel({projectId,session,connection,saveProject,onConnections,project,onWorkModelChange,onIntentContext,onDesign,onDelivery,buildRequest,onBuildRequestHandled,contextBusy=false}){
|
|
9
|
+
export function EngineeringPanel({projectId,session,connection,saveProject,onConnections,project,onWorkModelChange,onIntentContext,onDesign,onDelivery,buildRequest,onBuildRequestHandled,contextBusy=false,onBusyChange}){
|
|
10
10
|
const draftKey=`atmem-engineering-draft-v1:${projectId}`;
|
|
11
11
|
const [job,setJob]=useState(null),[events,setEvents]=useState([]),[draft,setDraft]=useState(()=>{try{return localStorage.getItem(draftKey)||'';}catch{return '';}}),
|
|
12
12
|
[busy,setBusy]=useState(false),[error,setError]=useState(''),[loading,setLoading]=useState(true),[artifact,setArtifact]=useState(null);
|
|
13
|
-
const [draftError,setDraftError]=useState('');
|
|
13
|
+
const [draftError,setDraftError]=useState(''),[mappingStatus,setMappingStatus]=useState('');
|
|
14
|
+
const mappedArtifacts=useRef(new Set());
|
|
14
15
|
const [restoreBlocked,setRestoreBlocked]=useState(false),[restoreAttempt,setRestoreAttempt]=useState(0);
|
|
15
16
|
useEffect(()=>{try{localStorage.setItem(draftKey,draft);setDraftError('');}catch{setDraftError('This draft could not be saved in your browser. Keep this view open or copy it before leaving.');}},[draftKey,draft]);
|
|
16
17
|
const live=useRef(true),cursor=useRef(0),actionLock=useRef(false);
|
|
@@ -58,8 +59,14 @@ export function EngineeringPanel({projectId,session,connection,saveProject,onCon
|
|
|
58
59
|
actionLock.current=true;setBusy(true);setError('');
|
|
59
60
|
try{await operation();}catch(e){if(live.current)setError(e.message);}finally{actionLock.current=false;if(live.current)setBusy(false);}
|
|
60
61
|
}
|
|
62
|
+
useEffect(()=>{onBusyChange?.(busy||loading||['running','starting'].includes(job?.state));return()=>onBusyChange?.(false);},[busy,loading,job?.state,onBusyChange]);
|
|
61
63
|
const exactSelection=connection?.provider==='codex'&&connection.model&&connection.model!=='default'&&connection.effort;
|
|
62
64
|
const running=job?.state==='running',uncertain=job?.state==='starting';
|
|
65
|
+
const lastProgress=events.filter(event=>event.runId===job?.run&&['progress','message'].includes(event.kind)).at(-1);
|
|
66
|
+
const [lastReceived,setLastReceived]=useState(null),[now,setNow]=useState(Date.now());
|
|
67
|
+
useEffect(()=>{if(lastProgress){const time=Date.now();setLastReceived(time);setNow(time);}},[lastProgress?.sequence]);
|
|
68
|
+
useEffect(()=>{if(!running&&!uncertain)return;const timer=setInterval(()=>setNow(Date.now()),1000);return()=>clearInterval(timer);},[running,uncertain]);
|
|
69
|
+
const activity=lastProgress?.kind==='message'?'Preparing an update':({reasoning:'Thinking through the implementation',fileChange:'Updating project files',commandExecution:'Running a development command',mcpToolCall:'Using a development tool',webSearch:'Checking documentation'}[lastProgress?.text]||'Generating project files');
|
|
63
70
|
const conversation=engineeringConversation(events);
|
|
64
71
|
const hasDesign=hasArchitectureDesign(project),canSubmit=running?!!draft.trim():!!buildIntent(project,draft);
|
|
65
72
|
const consumedBuild=useRef(null);
|
|
@@ -93,58 +100,58 @@ export function EngineeringPanel({projectId,session,connection,saveProject,onCon
|
|
|
93
100
|
},[buildRequest,projectId,loading,restoreBlocked,contextBusy,uncertain,busy,exactSelection,job?.id]);
|
|
94
101
|
async function linkMappings(){
|
|
95
102
|
const result=await call('reconciliation',{artifactId:artifact.id});
|
|
96
|
-
if(result.status==='proposal_missing')
|
|
103
|
+
if(result.status==='proposal_missing'){setMappingStatus('File links missing from package.');return;}
|
|
97
104
|
const verified=await verifyArtifactWork(result);
|
|
98
105
|
if(verified.payload.artifactId!==artifact.id||verified.payload.artifactSha256!==artifact.sha256||verified.payload.sessionId!==job.id)throw Error('The mapping receipt does not match this captured package.');
|
|
99
|
-
onWorkModelChange({kind:'artifact',verified},projectId);
|
|
106
|
+
onWorkModelChange({kind:'artifact',verified},projectId);setMappingStatus('Files connected to design blocks.');
|
|
100
107
|
}
|
|
108
|
+
useEffect(()=>{
|
|
109
|
+
if(!onWorkModelChange||!artifact||running||uncertain||busy||contextBusy||job?.last_status!=='completed'||artifact.runId!==job.run||mappedArtifacts.current.has(artifact.id))return;
|
|
110
|
+
mappedArtifacts.current.add(artifact.id);setMappingStatus('Connecting files to design blocks…');void act(linkMappings);
|
|
111
|
+
},[artifact?.id,job?.run,job?.state,busy,contextBusy]);
|
|
101
112
|
return <div className="engineering-panel">
|
|
102
|
-
<h2>
|
|
103
|
-
<p>Generate the agreed design and run applicable local checks here. Review results and corrections before opening Delivery.</p>
|
|
113
|
+
<h2>2. Project files</h2>
|
|
104
114
|
{onDesign&&<button onClick={onDesign}>Return to Design</button>}
|
|
105
115
|
|
|
106
116
|
{buildRequest?.projectId===projectId&&<p role="status">{restoreBlocked?'Your implementation request is waiting. Retry loading earlier work before starting.':uncertain?'Your implementation request is waiting. Recover the existing request first to avoid duplicate work.':contextBusy?'Your implementation request is waiting for the current design changes.':!job&&!exactSelection?'Your implementation request is waiting here. Choose a model and reasoning level below; it will then start automatically.':'Your implementation request is queued.'} <button onClick={()=>onBuildRequestHandled?.(buildRequest.id)}>Cancel queued request</button></p>}
|
|
107
117
|
{restoreBlocked&&<button disabled={loading||busy} onClick={()=>{setError('');setRestoreAttempt(value=>value+1);}}>Retry loading earlier work</button>}
|
|
108
118
|
{loading?<p role="status">Finding your project’s work…</p>:<>
|
|
109
119
|
{job?<>
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
{(running||uncertain)&&<div className="generation-progress" role="progressbar" aria-label="Project generation" aria-valuetext={activity}><span/></div>}
|
|
121
|
+
{(running||uncertain)&&<p className="generation-activity" role="status">{activity}{lastReceived&&<> · Last update received {Math.max(0,Math.floor((now-lastReceived)/1000))}s ago</>}</p>}
|
|
122
|
+
<p className="generation-status" role="status">{(running||uncertain||busy)&&<BrainActivity size={24}/>} {running?'Working':uncertain?'Recovering acceptance of your last request':finalization?.kind==='finalization_needs_attention'?'Files generated · Checks need attention':job.last_status?({completed:'Work completed',interrupted:'Work stopped',failed:'Work needs attention'}[job.last_status]||job.last_status):'Ready'} · {job.model} · {job.effort}</p>
|
|
123
|
+
|
|
112
124
|
</>:<>
|
|
113
125
|
<p>{exactSelection?`${connection.model} · ${connection.effort}`:'Choose a Codex model and reasoning level to start.'}</p>
|
|
114
126
|
{!exactSelection&&<button onClick={onConnections}>Choose model</button>}
|
|
115
127
|
{contextBusy&&<p role="status">Finish the current design changes before starting new work.</p>}
|
|
116
128
|
</>}
|
|
117
|
-
{conversation.length>0&&<section className="engineering-conversation" role="log" aria-label="Project conversation" aria-live="polite">{conversation.map(entry=><article key={entry.id} className={`engineering-entry ${entry.role}`}>
|
|
118
|
-
<strong>{entry.role==='user'?'You':'Architect'}</strong>
|
|
119
|
-
{entry.status==='pending'&&<span> · Sending update</span>}
|
|
120
|
-
{entry.status==='uncertain'&&<span> · Delivery uncertain; recover before resending</span>}
|
|
121
|
-
<pre>{entry.text}</pre>
|
|
122
|
-
</article>)}</section>}
|
|
123
129
|
{!hasDesign&&!job?<p>Start with the Board or Requirements. Add the components or responsibilities before building an implementation.</p>:<form onSubmit={submitIntent}>
|
|
124
|
-
<label htmlFor="engineering-intent">{running?'Add an implementation update':hasDesign?'Additional instructions (optional)':'Continue implementation'}</label>
|
|
125
|
-
<textarea id="engineering-intent" rows={
|
|
130
|
+
<details open={running||undefined}><summary>{running?'Add an update':'Additional instructions (optional)'}</summary><label htmlFor="engineering-intent">{running?'Add an implementation update':hasDesign?'Additional instructions (optional)':'Continue implementation'}</label>
|
|
131
|
+
<textarea id="engineering-intent" rows={2} value={draft} onChange={e=>setDraft(e.target.value)}/></details>
|
|
126
132
|
{draft.length>80000&&<p role="alert">This update exceeds 80,000 characters. Edit it before sending; your draft is retained.</p>}
|
|
127
|
-
<button disabled={busy||restoreBlocked||uncertain||contextBusy||!canSubmit||draft.length>80000||(!job&&!exactSelection)}>{running?'Send update':hasDesign?'Generate
|
|
133
|
+
<button disabled={busy||restoreBlocked||uncertain||contextBusy||!canSubmit||draft.length>80000||(!job&&!exactSelection)}>{busy?<><BrainActivity size={22}/> Sending…</>:running?'Send update':hasDesign?'Generate project files':'Continue implementation'}</button>
|
|
128
134
|
</form>}
|
|
129
135
|
{job&&<>
|
|
130
|
-
{
|
|
131
|
-
|
|
136
|
+
{running&&<button disabled={busy} onClick={()=>act(async()=>{const next=await call('interrupt');if(live.current)setJob(next);})}>Stop generation</button>}
|
|
137
|
+
|
|
138
|
+
<details className="engineering-actions"><summary>Connection and package details</summary>{finalization&&<p>{finalization.text}</p>}
|
|
132
139
|
<button disabled={busy} onClick={()=>act(async()=>{const next=await call('recover');if(live.current)setJob(next);})}>Refresh connection status</button>
|
|
133
140
|
{running&&<button disabled={busy} onClick={()=>act(async()=>{const next=await call('interrupt');if(live.current)setJob(next);})}>Stop work</button>}
|
|
134
141
|
{!running&&!uncertain&&job.last_status&&<button disabled={busy} onClick={()=>act(async()=>{const captured=await call('capture');if(live.current)setArtifact(captured);})}>Refresh package snapshot</button>}
|
|
135
|
-
</
|
|
136
|
-
<details><summary>Engineering progress ({events.length})</summary><ol>{events.filter(event=>!['message','accepted','steering_submitted'].includes(event.kind)).map(event=><li key={event.sequence}>{event.kind}{event.text?` · ${event.text}`:''}</li>)}</ol></details>
|
|
142
|
+
</details>
|
|
143
|
+
<details><summary>Engineering progress (<span key={events.length} className={`engineering-progress-count${running||uncertain?' updated':''}`}>{events.length}</span>)</summary><ol>{events.filter(event=>!['message','accepted','steering_submitted'].includes(event.kind)).map(event=><li key={event.sequence}>{event.kind}{event.text?` · ${event.text}`:''}</li>)}</ol></details>
|
|
137
144
|
</>}
|
|
138
|
-
{artifact&&<div className="engineering-artifact"><p>
|
|
139
|
-
{artifact.runId!==job?.run&&<p>
|
|
145
|
+
{artifact&&<div className="engineering-artifact"><p>{artifact.runId!==job?.run||running||uncertain?'Previous files':'Project files'} · {artifact.files.filter(file=>file.kind==='file').length} {artifact.files.filter(file=>file.kind==='file').length===1?'file':'files'}</p>
|
|
146
|
+
{artifact.runId!==job?.run&&<p>{running||uncertain?'Generating updated files…':'Generate updated files to include your changes.'}</p>}
|
|
140
147
|
{onDelivery&&<button disabled={busy} onClick={onDelivery}>Review and download handoff</button>}
|
|
141
148
|
|
|
142
|
-
{
|
|
143
|
-
{
|
|
144
|
-
|
|
149
|
+
{onWorkModelChange&&<p className="muted small" role="status">{running||uncertain?'File links update when generation finishes.':artifact.runId!==job?.run?'File links belong to the previous snapshot.':mappingStatus||'Connecting files to design blocks…'}</p>}
|
|
150
|
+
{!running&&!uncertain&&artifact.runId===job?.run&&mappingStatus==='File links missing from package.'&&<button disabled={busy||contextBusy} onClick={()=>submitIntent(null,'Add architecture-work-proposal.json binding the existing implementation files to the saved design blocks and requirements. Preserve the application and requirements. Run applicable local checks; retain failed or unexecuted checks explicitly.')}>Repair file links</button>}
|
|
151
|
+
|
|
152
|
+
|
|
145
153
|
</div>}
|
|
146
154
|
</>}
|
|
147
|
-
{(hasDesign||job)&&<ArchitectureWorkPanel project={project} onChange={onWorkModelChange} busy={busy||contextBusy}/>}
|
|
148
155
|
{error&&<p role="alert">{error}</p>}
|
|
149
156
|
{draftError&&<p role="alert">{draftError}</p>}
|
|
150
157
|
</div>;
|
package/src/FileTree.jsx
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React,{useMemo,useState}from'react';import{Folder,FolderOpen,FileText,FileCode,FileJson,ChevronDown,ChevronRight}from'lucide-react';
|
|
2
|
+
export function FileTree({files,selected,onSelect}){
|
|
3
|
+
const [closed,setClosed]=useState(new Set());
|
|
4
|
+
const tree=useMemo(()=>{const root={};for(const path of Object.keys(files).sort()){if(path.endsWith('/'))continue;let branch=root;const parts=path.split('/');for(let i=0;i<parts.length;i++){const part=parts[i];branch[part]??={path:parts.slice(0,i+1).join('/'),children:i<parts.length-1?{}:null};branch=branch[part].children;}}return root;},[files]);
|
|
5
|
+
const render=(nodes,level=1)=><ul role={level===1?'tree':'group'} aria-label={level===1?'Project files':undefined}>{Object.entries(nodes).map(([name,item])=>{const folder=!!item.children,expanded=!closed.has(item.path),Icon=folder?(expanded?FolderOpen:Folder):/\.json$/.test(name)?FileJson:/\.(?:py|js|mjs|tf|sh|yml|yaml)$/.test(name)?FileCode:FileText;return <li key={item.path} role="treeitem" aria-expanded={folder?expanded:undefined} aria-selected={!folder?selected===item.path:undefined}><button type="button" title={item.path} className={selected===item.path?'selected':''} style={{paddingLeft:8+(level-1)*14}} onClick={()=>folder?setClosed(previous=>{const next=new Set(previous);next.has(item.path)?next.delete(item.path):next.add(item.path);return next;}):onSelect(item.path)}>{folder?(expanded?<ChevronDown size={13}/>:<ChevronRight size={13}/>):<span className="tree-indent"/>}<Icon size={15}/><span>{name}</span></button>{folder&&expanded&&render(item.children,level+1)}</li>;})}</ul>;
|
|
6
|
+
return <nav className="file-tree" aria-label="Output files">{render(tree)}</nav>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React,{useState,useRef} from'react';
|
|
2
|
+
import{BrainActivity}from'./BrainActivity.jsx';
|
|
3
|
+
import{estimate}from'./estimate.mjs';
|
|
4
|
+
import{cloudMapping}from'./journey.mjs';
|
|
5
|
+
import{OPTION_NAMES}from'./pack.mjs';
|
|
6
|
+
export function FinalChecklist({project,implementation,terraform,checks=[],onNavigate}){
|
|
7
|
+
const [opening,setOpening]=useState(null),[error,setError]=useState('');const navigating=useRef(false);
|
|
8
|
+
async function open(name,target){if(navigating.current||!onNavigate)return;navigating.current=true;setError('');setOpening(name);
|
|
9
|
+
try{await onNavigate(target);}catch(e){setError(e.message||'Could not open that section. Try again.');}finally{navigating.current=false;setOpening(null);}
|
|
10
|
+
}
|
|
11
|
+
const cost=estimate(project),mapping=cloudMapping(project),requirements=!!(project.needs?.length||project.brief?.application);
|
|
12
|
+
const selectedRegion=[...(project.decisions||[])].reverse().find(d=>d.subject==='cloud.region'&&d.status==='Accepted')?.decision;
|
|
13
|
+
const country=[...(project.decisions||[])].reverse().find(d=>d.subject==='data.country'&&d.status==='Accepted')?.decision;
|
|
14
|
+
const proposed=project.nodes.filter(n=>n.data.kind!=='note'&&n.data.origin!=='existing');
|
|
15
|
+
const placements=proposed.map(n=>n.data.config?.region).filter(r=>typeof r==='string'&&r.trim()&&!/select|unknown|tbd/i.test(r));
|
|
16
|
+
const region=selectedRegion||(proposed.length&&placements.length===proposed.length?[...new Set(placements)].join(', '):null);
|
|
17
|
+
const hasEstimate=cost.expected!==null&&!cost.mixedCurrencies;
|
|
18
|
+
const excluded=new Set([...cost.lines.filter(line=>line.cost==null).map(line=>line.component),...cost.notCosted]).size;
|
|
19
|
+
const costReason=hasEstimate&&excluded?`${excluded} items excluded · View breakdown`:'';
|
|
20
|
+
const rows=[['Requirements',requirements?'Recorded':'Not recorded',requirements],['Cloud',mapping.provider?(OPTION_NAMES[mapping.provider]||mapping.provider):'Not selected',!!mapping.provider],['Data country',country||'Not selected',!!country],['Cloud region',region||'Choose region',!!region],['Cost estimate',hasEstimate?'Estimated':cost.mixedCurrencies?'Choose one currency':'Not estimated',hasEstimate],['Implementation',implementation?'Included':'Not generated',!!implementation],['Application checks',checks.length?checks.every(c=>c.state==='finished'&&c.result?.status==='passed')?'Passed':'Needs attention':'Not run',checks.length>0&&checks.every(c=>c.state==='finished'&&c.result?.status==='passed')],['Terraform checks',terraform?.status==='passed'?'Passed':terraform?.status==='failed'?'Failed':terraform?.status==='incomplete'?'Incomplete':'Not run',terraform?.status==='passed']];
|
|
21
|
+
const targets=['requirements','location','location','location','cost','implementation','checks','checks'];
|
|
22
|
+
return <section className="final-checklist" aria-label="Final checklist" aria-busy={!!opening}><h3>Final checklist</h3><ul>{rows.map(([name,status,complete],index)=><li key={name}><button type="button" onClick={()=>open(name,targets[index])} disabled={!!opening||!onNavigate} aria-label={`${name}: ${status}. Open ${name}`}><span className={complete?'check-complete':'check-pending'} aria-label={name==='Cost estimate'&&hasEstimate?'Estimate available':complete?'Complete':'Needs attention'}>{complete?'✓':'!'}</span><span className="checklist-label">{name}{name==='Cost estimate'&&costReason&&<small>{costReason}</small>}</span><strong>{opening===name?<>Opening… <BrainActivity/></>:status}</strong><span className="checklist-arrow" aria-hidden="true">→</span></button></li>)}</ul>{opening&&<p role="status" className="checklist-loading">Opening {opening}…</p>}{error&&<p role="alert">{error}</p>}</section>;
|
|
23
|
+
}
|
package/src/HelpNote.jsx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
function textContent(value){return React.Children.toArray(value).map(child=>typeof child==='string'||typeof child==='number'?String(child):React.isValidElement(child)?textContent(child.props.children):'').join(' ');}
|
|
3
|
+
export function InlineNote({children}){const text=textContent(children).trim().replace(/\s+/g,' '),words=text.split(' ');return text?<small className="inline-note">{words.length>20?words.slice(0,20).join(' ')+'…':text}</small>:null;}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {unzipSync,zipSync,strToU8} from 'fflate';
|
|
2
|
+
import {FinalChecklist} from './FinalChecklist.jsx';
|
|
3
|
+
import {BrainActivity} from './BrainActivity.jsx';
|
|
4
|
+
import {completeProjectZip} from './final-package.mjs';
|
|
5
|
+
import React,{useEffect,useState,useRef} from 'react';
|
|
2
6
|
import {engineeringCall,verifiedHandoffDownload} from './engineering-client.mjs';
|
|
3
7
|
import {ArtifactChecks} from './ArtifactChecks.jsx';
|
|
4
8
|
|
|
5
|
-
export function ImplementationDelivery({project,session,onBuild,onSave,contextBusy=false}){
|
|
9
|
+
export function ImplementationDelivery({project,session,onBuild,onSave,contextBusy=false,designBundle,onDesignDownload,onDownloaded,checkReceipt,onExplore,onNavigate}){
|
|
10
|
+
const [orcaResult,setOrcaResult]=useState(null);
|
|
11
|
+
const projectRef=useRef(project);projectRef.current=project;
|
|
12
|
+
const [prepared,setPrepared]=useState(null),[preparing,setPreparing]=useState(false);
|
|
6
13
|
const [loaded,setLoaded]=useState(null),[error,setError]=useState(''),[retry,setRetry]=useState(0),[busy,setBusy]=useState(false);
|
|
7
14
|
const report=loaded?.project===project?loaded.report:null;
|
|
8
15
|
useEffect(()=>{
|
|
@@ -12,7 +19,29 @@ export function ImplementationDelivery({project,session,onBuild,onSave,contextBu
|
|
|
12
19
|
}).catch(e=>{if(!controller.signal.aborted){if(e.status===404&&e.message.startsWith('Save or restore this project'))setLoaded({project,report:{artifact:null}});else setError(e.message);}});
|
|
13
20
|
return()=>controller.abort();
|
|
14
21
|
},[project,session.token,retry]);
|
|
15
|
-
|
|
22
|
+
useEffect(()=>{
|
|
23
|
+
if(!report?.canPackage||designBundle?.project!==project){setPrepared(null);return;}
|
|
24
|
+
let stopped=false;const controller=new AbortController();setPreparing(true);setError('');
|
|
25
|
+
(async()=>{try{
|
|
26
|
+
const captured=await verifiedHandoffDownload(session,{projectId:project.id,sessionId:report.session.id,artifactId:report.artifact.id,savedDesignSha256:report.savedDesignSha256},report.artifact,{signal:controller.signal});
|
|
27
|
+
const files={...unzipSync(new Uint8Array(await captured.arrayBuffer())),...Object.fromEntries(Object.entries(designBundle.files).map(([name,value])=>['design-documents/'+name,typeof value==='string'?strToU8(value):value]))};
|
|
28
|
+
if(!stopped){setPrepared({artifactId:report.artifact.id,designSha256:report.savedDesignSha256});onExplore?.({project,files});}
|
|
29
|
+
}catch(e){if(!stopped&&e.name!=='AbortError')setError(e.message);}finally{if(!stopped)setPreparing(false);}})();
|
|
30
|
+
return()=>{stopped=true;controller.abort();};
|
|
31
|
+
},[report?.artifact?.id,report?.savedDesignSha256,designBundle,project,session.token,retry]);
|
|
32
|
+
async function launchOrca(blob){
|
|
33
|
+
if(blob.size>32000000)throw Error('Orca handoff supports ZIPs up to 32 MB. Download ZIP instead.');
|
|
34
|
+
const zipBase64=await new Promise((resolve,reject)=>{const reader=new FileReader();reader.onload=()=>resolve(reader.result.split(',')[1]);reader.onerror=()=>reject(Error('Could not prepare the project ZIP.'));reader.readAsDataURL(blob);});
|
|
35
|
+
const result=await engineeringCall(session,'orca/open',{projectId:project.id,zipBase64});setOrcaResult(result);if(!result.opened)throw Error(result.message);
|
|
36
|
+
}
|
|
37
|
+
async function downloadDesign(){if(busy||contextBusy)return;setBusy(true);setError('');try{await onDesignDownload?.(designBundle);}catch(error){setError(error.message);}finally{setBusy(false);}}
|
|
38
|
+
async function openOrca(){
|
|
39
|
+
if(busy||contextBusy)return;
|
|
40
|
+
setOrcaResult(null);
|
|
41
|
+
if(report?.artifact)return download(true);
|
|
42
|
+
setBusy(true);setError('');try{if(!designBundle||designBundle.project!==project)throw Error('Project files are still preparing.');if(onSave)await onSave(project);const full=await(designBundle.prepare?.()||designBundle);if(projectRef.current!==project)throw Error('Design changed while preparing files. Try again.');await launchOrca(new Blob([zipSync(Object.fromEntries(Object.entries(full.files).map(([name,value])=>[name,typeof value==='string'?strToU8(value):value])))]));}catch(error){setError(error.message);}finally{setBusy(false);}
|
|
43
|
+
}
|
|
44
|
+
async function download(openInOrca=false){
|
|
16
45
|
if(!report?.artifact||busy||contextBusy)return;
|
|
17
46
|
setBusy(true);setError('');
|
|
18
47
|
try{
|
|
@@ -29,27 +58,37 @@ export function ImplementationDelivery({project,session,onBuild,onSave,contextBu
|
|
|
29
58
|
throw Error('The saved design or package changed. Review the refreshed Delivery information before downloading.');
|
|
30
59
|
}
|
|
31
60
|
const payload={projectId:project.id,sessionId:current.session.id,artifactId:current.artifact.id};
|
|
32
|
-
const
|
|
61
|
+
const captured=await verifiedHandoffDownload(session,{...payload,savedDesignSha256:current.savedDesignSha256},current.artifact);
|
|
62
|
+
if(designBundle?.project!==project)throw Error('The design documents are refreshing. Try downloading again when preparation finishes.');
|
|
63
|
+
const full=await(designBundle.prepare?.()||designBundle);
|
|
64
|
+
if(projectRef.current!==project)throw Error('Design changed while preparing files. Try again.');
|
|
65
|
+
const blob=await completeProjectZip(captured,full);
|
|
66
|
+
if(openInOrca){await launchOrca(blob);return;}
|
|
33
67
|
const url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;
|
|
34
|
-
link.download=`architect-handoff-${report.artifact.id}.zip`;link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
|
68
|
+
link.download=`architect-handoff-${report.artifact.id}.zip`;link.click();onDownloaded?.();setTimeout(()=>URL.revokeObjectURL(url),1000);
|
|
35
69
|
}catch(e){setError(e.message)}finally{setBusy(false)}
|
|
36
70
|
}
|
|
37
71
|
return <section aria-label="Implementation delivery">
|
|
38
|
-
<h3>
|
|
39
|
-
{!report&&!error&&<p role="status">
|
|
40
|
-
{report&&!report.artifact
|
|
72
|
+
<h3>Your project ZIP</h3>
|
|
73
|
+
{!report&&!error&&<p role="status">Preparing package…</p>}
|
|
74
|
+
{report&&!report.artifact&&<p>Design package · implementation not generated.</p>}
|
|
75
|
+
{report&&!report.canPackage&&<button className="btn primary" disabled={busy||!designBundle||designBundle.project!==project||contextBusy} onClick={downloadDesign}>{busy?'Preparing ZIP…':'Download project ZIP'}</button>}
|
|
41
76
|
{report?.artifact&&<>
|
|
42
|
-
<p>{report.relation==='matches_saved_design'?'
|
|
77
|
+
<p className="delivery-state">{report.relation==='matches_saved_design'?'✓ Current design':report.relation==='design_changed'?'! Design changed since generation':'! Earlier or unverified implementation'}</p>
|
|
78
|
+
|
|
43
79
|
<p>{report.artifact.files.filter(file=>file.kind==='file').length} files · {report.artifact.model} · {report.artifact.effort}</p>
|
|
44
|
-
{report.canPackage&&<button className="btn primary" disabled={busy||contextBusy} onClick={()=>download()}>Download project
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
{report.canPackage&&<button className="btn primary" disabled={busy||preparing||!prepared||contextBusy||!designBundle||designBundle.project!==project} onClick={()=>download()}>Download project ZIP</button>}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
49
85
|
</>}
|
|
50
|
-
{busy
|
|
51
|
-
{
|
|
86
|
+
<button className="btn" disabled={busy||preparing||contextBusy||!report||!designBundle||(!!report?.artifact&&!prepared)} onClick={openOrca}>Open in Orca ↗</button> <a className="link orca-website" href="https://orca.dev" target="_blank" rel="noopener noreferrer">Orca website ↗</a>
|
|
87
|
+
{orcaResult&&<p role="status">{orcaResult.opened?'Opened in Orca':''} <code>{orcaResult.path}</code></p>}
|
|
88
|
+
<FinalChecklist project={project} implementation={report?.canPackage&&report?.artifact} checks={report?.checks} terraform={checkReceipt} onNavigate={onNavigate}/>
|
|
89
|
+
{(busy||preparing)&&<p role="status"><BrainActivity/> Preparing ZIP…</p>}
|
|
90
|
+
{contextBusy&&<p role="status">Waiting for current changes…</p>}
|
|
52
91
|
{error&&<p role="alert">{error}</p>}
|
|
53
|
-
<button className="
|
|
92
|
+
<button className="icon" aria-label="Refresh package" title="Refresh package" disabled={busy||preparing} onClick={()=>setRetry(value=>value+1)}>↻</button>
|
|
54
93
|
</section>;
|
|
55
94
|
}
|