@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.
Files changed (66) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +6 -0
  3. package/README.md +21 -21
  4. package/cli/runtime.mjs +3 -1
  5. package/dist/assets/{index-qtCM2D7O.js → index-D4CZ_n8f.js} +19166 -19094
  6. package/dist/assets/index-DuveVTIx.css +1 -0
  7. package/dist/assets/{index.es-C6C0sneB.js → index.es-Fcqg-orY.js} +1 -1
  8. package/dist/assets/{visual-export-DjbjgQbA.js → visual-export-4PDDTAP7.js} +3 -3
  9. package/dist/index.html +2 -2
  10. package/dist/licenses/atmem-APACHE-2.0.txt +202 -0
  11. package/dist/licenses/memory-dependency-notices.txt +149 -2241
  12. package/package.json +3 -3
  13. package/public/licenses/atmem-APACHE-2.0.txt +202 -0
  14. package/public/licenses/memory-dependency-notices.txt +149 -2241
  15. package/server/app.py +1 -1
  16. package/server/codex_app_server.py +5 -5
  17. package/server/codex_worker_policy.py +27 -4
  18. package/server/engineering_artifacts.py +7 -1
  19. package/server/engineering_handoff.py +9 -0
  20. package/server/engineering_routes.py +16 -2
  21. package/server/local_providers.py +7 -3
  22. package/server/memory-requirements.lock +6 -6
  23. package/server/memory-requirements.txt +2 -2
  24. package/server/memory_context.py +58 -0
  25. package/server/memory_routes.py +67 -8
  26. package/server/memory_service.py +40 -5
  27. package/server/memory_worker.py +76 -11
  28. package/server/orca_handoff.py +90 -0
  29. package/server/pricing.py +161 -1
  30. package/server/prompts.py +13 -0
  31. package/server/request_uploads.py +65 -0
  32. package/server/terraform_routes.py +47 -1
  33. package/server/windows_snapshot.py +119 -0
  34. package/src/ArchitectureWorkPanel.jsx +0 -7
  35. package/src/ArtifactChecks.jsx +2 -2
  36. package/src/ChatMessage.jsx +12 -2
  37. package/src/EngagementViews.jsx +18 -16
  38. package/src/EngineeringPanel.jsx +35 -28
  39. package/src/FileTree.jsx +7 -0
  40. package/src/FinalChecklist.jsx +23 -0
  41. package/src/HelpNote.jsx +3 -0
  42. package/src/ImplementationDelivery.jsx +56 -17
  43. package/src/JourneyViews.jsx +20 -17
  44. package/src/LocationChoices.jsx +12 -0
  45. package/src/MemoryPanel.jsx +43 -31
  46. package/src/OpenAIVoicePanel.jsx +15 -14
  47. package/src/OutputsView.jsx +28 -16
  48. package/src/ProjectChecks.jsx +58 -0
  49. package/src/StarPrompt.jsx +26 -0
  50. package/src/WorkbenchDialogs.jsx +1 -1
  51. package/src/conversation.mjs +1 -0
  52. package/src/delivery.mjs +1 -1
  53. package/src/engineering.css +10 -0
  54. package/src/estimate.mjs +5 -3
  55. package/src/final-package.mjs +34 -0
  56. package/src/journey-workspace.css +39 -0
  57. package/src/journey.mjs +12 -2
  58. package/src/large-request.mjs +25 -0
  59. package/src/main.jsx +129 -98
  60. package/src/output-bundle.mjs +18 -4
  61. package/src/pricing.mjs +59 -3
  62. package/src/project-file.mjs +1 -1
  63. package/src/useChatScroll.jsx +5 -4
  64. package/src/useEngineeringStatus.mjs +17 -0
  65. package/src/validation-receipt.mjs +1 -1
  66. 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}>
@@ -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>Package checks</h3><p>Results cover this captured package. Full project acceptance remains pending.</p>
29
- {!available&&<p>Local checks are unavailable on this server. Your captured package is retained.</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';
@@ -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 ChatMessage({text=''}){
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;}
@@ -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><p className="muted">Compare the approaches below, then choose a cloud to continue. Next we’ll discuss country and data residency.</p></header>
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> · You can change your choice below. <button className="btn primary" disabled={busy} onClick={onContinue}>Continue with {OPTION_NAMES[selected]} →</button></p>}
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
- {!ready && <p className="muted">We’ll clarify what matters to you and explain the tradeoffs here as we go.</p>}
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><p>{notes[id].replace(/^(?:AtMem AI Architect|At[Ff]orma) proposal — confirm: /,'')}</p>
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` : `Choose ${OPTION_NAMES[id]} & continue →`}</button></>}
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>Choices we’ve made</h2><p className="muted">AtMem AI Architect records your choices and explains why they fit. You can revisit them in chat.</p></header>
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 confirmed yet. We’ll work through them together as the design develops.</p>}
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 && <p>{d.context}</p>}{d.consequences && <p>{d.consequences}</p>}
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)) && <details className="comment-history"><summary>Feedback history</summary>{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>)}</details>}
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"><button className="btn" onClick={() => onDiscuss('Summarise where we have reached from the current board and our discussion. Distinguish what I have confirmed from your proposals and unresolved questions. Then ask ONE useful question to develop the requirements further, and update the summary as we discuss.')}>Let’s discuss more in chat</button>
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><div className="row">{onUpload&&<button className="btn" onClick={onUpload}>Upload requirements or diagram</button>}{onOpenProject&&<button className="btn" onClick={onOpenProject}>Open saved project file</button>}</div><p className="muted small">Start from Word (.docx), PDF, text, Markdown, draw.io or images. Send the attached material in chat; Architect extracts requirements, proposes the diagram and asks about unclear points.</p>
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=key==='suggested'?'details':'section';return <Tag className="requirements-chapter" key={key}>
367
- {key==='suggested'?<summary>Suggestions still to discuss ({rows.length})</summary>:<h3>{titles[key] || 'Additional requirements'}</h3>}
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
- <footer className="document-footer">AtMem AI Architect · Requirements derived from the current design and discussion</footer>
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
  }
@@ -1,4 +1,4 @@
1
- import {ArchitectureWorkPanel} from './ArchitectureWorkPanel.jsx';
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')throw Error(result.message);
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>Generate and verify</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
- <p role="status">{running?'Working':uncertain?'Recovering acceptance of your last request':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>
111
- <p className="muted">Each request includes your saved design. Earlier versions remain in the work history.</p>
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={4} value={draft} onChange={e=>setDraft(e.target.value)}/>
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 and verify':'Continue implementation'}</button>
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
- {finalization&&<p role="status"><strong>Recorded verification update: </strong>{finalization.text}</p>}
131
- <div className="engineering-actions">
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
- </div>
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>Implementation draft · {artifact.files.filter(file=>file.kind==='file').length} {artifact.files.filter(file=>file.kind==='file').length===1?'file':'files'} · work {artifact.turnStatus}. Capture does not establish full project acceptance.</p>
139
- {artifact.runId!==job?.run&&<p>This package is from an earlier turn. It does not cover the current work.</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
- {!running&&!uncertain&&job?.last_status==='completed'&&artifact.runId===job.run&&<button disabled={busy} onClick={()=>act(async()=>{await call('recheck',{runId:job.run,artifactId:artifact.id});const next=await call('state');if(live.current)setJob(next);})}>Recheck and repair</button>}
143
- {onWorkModelChange&&!running&&!uncertain&&job?.last_status==='completed'&&artifact.runId===job.run&&<button disabled={busy||contextBusy} onClick={()=>act(linkMappings)}>Link proposed mappings</button>}
144
- <ArtifactChecks key={artifact.id} session={session} projectId={projectId} sessionId={job.id} artifact={artifact}/>
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>;
@@ -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
+ }
@@ -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 React,{useEffect,useState} from 'react';
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
- async function download(){
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 blob=await verifiedHandoffDownload(session,{...payload,savedDesignSha256:current.savedDesignSha256},current.artifact);
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>Project package</h3>
39
- {!report&&!error&&<p role="status">Finding the captured implementation and its checks…</p>}
40
- {report&&!report.artifact&&<><p>No implementation package has been captured for this project yet. Next: open Review to generate the project and inspect its checks. Return here for the final package.</p>{onBuild&&<button className="btn primary" disabled={contextBusy} onClick={onBuild}>Go to Review</button>}</>}
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'?'This package used the latest saved design.':report.relation==='design_changed'?'The saved design has changed since this implementation. New or changed requirements need implementation and checks.':report.relation==='earlier_turn'?'This is an earlier package. A later implementation turn has not produced a completed package yet.':'The submitted design receipt is unavailable. Its relationship to the current design is unknown.'} Capture and local checks do not establish full project acceptance.</p>
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 package (ZIP)</button>}
45
- {report.relation!=='matches_saved_design'&&onBuild&&<button className="btn" disabled={busy||contextBusy} onClick={onBuild}>Review latest design</button>}
46
- <p>The handoff includes the captured files, their original ZIP, submitted and latest saved requirements/design, and recorded local checks. Missing implementation or Spec Kit content remains work to complete.</p>
47
- <ArtifactChecks key={report.artifact.id} session={session} projectId={project.id} sessionId={report.session.id} artifact={report.artifact} readOnly/>
48
- <details><summary>Package files and source receipt</summary><p>Artifact SHA-256: {report.artifact.sha256}</p><ul>{report.artifact.files.filter(file=>file.kind==='file').map(file=><li key={file.path}>{file.path}</li>)}</ul></details>
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&&<p role="status">Preparing and verifying your handoff download…</p>}
51
- {contextBusy&&<p role="status">Finish the current design changes before downloading a combined handoff.</p>}
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="link" disabled={busy} onClick={()=>setRetry(value=>value+1)}>Refresh delivery</button>
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
  }