hex-audit 0.5.4 → 0.5.6
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/DEMO.md +5 -5
- package/README.md +31 -10
- package/dashboard/.next/BUILD_ID +1 -1
- package/dashboard/.next/app-build-manifest.json +22 -22
- package/dashboard/.next/app-path-routes-manifest.json +1 -1
- package/dashboard/.next/build-manifest.json +4 -4
- package/dashboard/.next/next-server.js.nft.json +1 -1
- package/dashboard/.next/prerender-manifest.js +1 -1
- package/dashboard/.next/prerender-manifest.json +1 -1
- package/dashboard/.next/required-server-files.json +1 -1
- package/dashboard/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/_not-found.html +1 -1
- package/dashboard/.next/server/app/_not-found.rsc +1 -1
- package/dashboard/.next/server/app/access/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/api/notes/done/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/done/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/lead/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/lead/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/question/route.js +1 -0
- package/dashboard/.next/server/app/api/notes/question/route.js.nft.json +1 -0
- package/dashboard/.next/server/app/api/notes/record/route.js +3 -3
- package/dashboard/.next/server/app/api/notes/route.js +2 -2
- package/dashboard/.next/server/app/calls/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/conformance/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/diagram.html +1 -1
- package/dashboard/.next/server/app/diagram.rsc +1 -1
- package/dashboard/.next/server/app/flows/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/flows.html +1 -1
- package/dashboard/.next/server/app/flows.rsc +1 -1
- package/dashboard/.next/server/app/functions/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/issues/page.js +5 -5
- package/dashboard/.next/server/app/issues/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/notes/page.js +3 -3
- package/dashboard/.next/server/app/notes/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/overleaf/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/page.js +2 -2
- package/dashboard/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/progress/page.js +1 -1
- package/dashboard/.next/server/app/progress/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app/stats/page_client-reference-manifest.js +1 -1
- package/dashboard/.next/server/app-paths-manifest.json +11 -8
- package/dashboard/.next/server/chunks/42.js +1 -1
- package/dashboard/.next/server/chunks/530.js +1 -1
- package/dashboard/.next/server/chunks/622.js +3 -0
- package/dashboard/.next/server/functions-config-manifest.json +1 -1
- package/dashboard/.next/server/middleware-build-manifest.js +1 -1
- package/dashboard/.next/server/pages/404.html +1 -1
- package/dashboard/.next/server/pages/500.html +1 -1
- package/dashboard/.next/server/server-reference-manifest.js +1 -1
- package/dashboard/.next/server/server-reference-manifest.json +1 -1
- package/dashboard/.next/static/chunks/app/issues/page-d1607f57783782ae.js +1 -0
- package/dashboard/.next/static/chunks/app/notes/page-17af94c67ba79c00.js +1 -0
- package/dashboard/.next/static/chunks/app/page-24bac51dfe007c66.js +1 -0
- package/dashboard/.next/static/chunks/app/progress/page-427fb18da3afb061.js +1 -0
- package/dashboard/.next/static/chunks/{main-a6cc6e599cefa893.js → main-9a811c95265dbd11.js} +1 -1
- package/dashboard/.next/static/css/4a38d8f6f6b7a68f.css +3 -0
- package/dashboard/app/api/notes/done/route.ts +26 -0
- package/dashboard/app/api/notes/lead/route.ts +40 -0
- package/dashboard/app/api/notes/question/route.ts +30 -0
- package/dashboard/app/api/notes/record/route.ts +10 -2
- package/dashboard/app/issues/IssuesBoardClient.tsx +18 -2
- package/dashboard/app/notes/NotesClient.tsx +494 -77
- package/dashboard/app/notes/page.tsx +5 -2
- package/dashboard/app/progress/ProgressClient.tsx +29 -8
- package/dashboard/app/progress/page.tsx +2 -0
- package/dashboard/components/MarkdownRenderer.tsx +31 -0
- package/dashboard/next.config.mjs +11 -0
- package/dashboard/whisper-server.py +85 -0
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +23 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/issue.js +2 -2
- package/dist/commands/issue.js.map +1 -1
- package/dist/commands/note.d.ts.map +1 -1
- package/dist/commands/note.js +55 -2
- package/dist/commands/note.js.map +1 -1
- package/dist/core/issues.d.ts +1 -1
- package/dist/core/issues.d.ts.map +1 -1
- package/dist/core/issues.js +2 -1
- package/dist/core/issues.js.map +1 -1
- package/dist/core/notes.d.ts +68 -0
- package/dist/core/notes.d.ts.map +1 -1
- package/dist/core/notes.js +167 -0
- package/dist/core/notes.js.map +1 -1
- package/dist/core/review.d.ts +14 -0
- package/dist/core/review.d.ts.map +1 -0
- package/dist/core/review.js +80 -0
- package/dist/core/review.js.map +1 -0
- package/dist/core/whisper.d.ts +8 -0
- package/dist/core/whisper.d.ts.map +1 -1
- package/dist/core/whisper.js +55 -0
- package/dist/core/whisper.js.map +1 -1
- package/dist/skills/diane.md +49 -67
- package/dist/skills/ingest-aa-report.md +6 -6
- package/dist/skills/init-audit.md +1 -1
- package/dist/skills/validate-all-findings.md +23 -9
- package/dist/skills/{validate-issue.md → validate-finding.md} +71 -25
- package/dist/skills/write-finding.md +3 -3
- package/package.json +3 -2
- package/dashboard/.next/static/chunks/app/issues/page-abb062e4cf118340.js +0 -1
- package/dashboard/.next/static/chunks/app/notes/page-bc458d3e1639c526.js +0 -1
- package/dashboard/.next/static/chunks/app/page-1226422e170d8895.js +0 -1
- package/dashboard/.next/static/chunks/app/progress/page-f07ac39d1d27b480.js +0 -1
- package/dashboard/.next/static/css/23b3f575e87bea75.css +0 -3
- /package/dashboard/.next/static/{JIjye0HUTWALF3gbtaZF8 → hV9GBuhq9vm6hXiHOWEfE}/_buildManifest.js +0 -0
- /package/dashboard/.next/static/{JIjye0HUTWALF3gbtaZF8 → hV9GBuhq9vm6hXiHOWEfE}/_ssgManifest.js +0 -0
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import { useRouter } from 'next/navigation';
|
|
5
|
-
import { MarkdownRenderer } from '@/components/MarkdownRenderer';
|
|
6
|
-
|
|
7
|
-
interface ContractNote {
|
|
8
|
-
name: string;
|
|
9
|
-
body: string;
|
|
10
|
-
}
|
|
5
|
+
import { MarkdownRenderer, InlineMarkdown } from '@/components/MarkdownRenderer';
|
|
6
|
+
import type { ContractNote, FnEntry, Question, Lead } from '../../../src/core/notes';
|
|
11
7
|
|
|
12
8
|
interface NotesClientProps {
|
|
13
9
|
general: string;
|
|
@@ -19,6 +15,7 @@ interface NotesClientProps {
|
|
|
19
15
|
|
|
20
16
|
type RecState = 'idle' | 'recording' | 'transcribing';
|
|
21
17
|
const OTHER = '__other__';
|
|
18
|
+
const GENERAL = 'general';
|
|
22
19
|
|
|
23
20
|
function fmt(sec: number): string {
|
|
24
21
|
const m = Math.floor(sec / 60);
|
|
@@ -35,42 +32,18 @@ export function NotesClient({
|
|
|
35
32
|
}: NotesClientProps) {
|
|
36
33
|
const router = useRouter();
|
|
37
34
|
|
|
38
|
-
// ──
|
|
39
|
-
const
|
|
40
|
-
() => [
|
|
41
|
-
|
|
35
|
+
// ── Left rail: General + each contract note ──
|
|
36
|
+
const railItems = useMemo(
|
|
37
|
+
() => [
|
|
38
|
+
{ key: GENERAL, label: 'General' },
|
|
39
|
+
...contractNotes.map((n) => ({ key: n.contract, label: n.contract })),
|
|
40
|
+
],
|
|
41
|
+
[contractNotes],
|
|
42
42
|
);
|
|
43
43
|
const initialKey =
|
|
44
|
-
activeContract &&
|
|
44
|
+
activeContract && railItems.some((d) => d.key === activeContract) ? activeContract : GENERAL;
|
|
45
45
|
const [selectedKey, setSelectedKey] = useState<string>(initialKey);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
// ── Editing ──
|
|
49
|
-
const [editing, setEditing] = useState(false);
|
|
50
|
-
const [draft, setDraft] = useState('');
|
|
51
|
-
const [saving, setSaving] = useState(false);
|
|
52
|
-
|
|
53
|
-
const startEdit = () => {
|
|
54
|
-
setDraft(current.body);
|
|
55
|
-
setEditing(true);
|
|
56
|
-
};
|
|
57
|
-
const save = async () => {
|
|
58
|
-
setSaving(true);
|
|
59
|
-
try {
|
|
60
|
-
const res = await fetch('/api/notes', {
|
|
61
|
-
method: 'PUT',
|
|
62
|
-
headers: { 'Content-Type': 'application/json' },
|
|
63
|
-
body: JSON.stringify({ target: current.key, body: draft }),
|
|
64
|
-
});
|
|
65
|
-
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'save failed');
|
|
66
|
-
setEditing(false);
|
|
67
|
-
router.refresh();
|
|
68
|
-
} catch (err) {
|
|
69
|
-
setStatusMsg(err instanceof Error ? err.message : 'Save failed');
|
|
70
|
-
} finally {
|
|
71
|
-
setSaving(false);
|
|
72
|
-
}
|
|
73
|
-
};
|
|
46
|
+
const selectedNote = contractNotes.find((n) => n.contract === selectedKey) ?? null;
|
|
74
47
|
|
|
75
48
|
// ── Recording ──
|
|
76
49
|
const [recState, setRecState] = useState<RecState>('idle');
|
|
@@ -209,18 +182,15 @@ export function NotesClient({
|
|
|
209
182
|
)}
|
|
210
183
|
</div>
|
|
211
184
|
|
|
212
|
-
{/* ── Notes (rail +
|
|
185
|
+
{/* ── Notes (rail + structured view) ── */}
|
|
213
186
|
<div className="flex gap-sp-5">
|
|
214
187
|
<nav className="w-44 shrink-0 space-y-0.5">
|
|
215
|
-
{
|
|
188
|
+
{railItems.map((d) => (
|
|
216
189
|
<button
|
|
217
190
|
key={d.key}
|
|
218
191
|
type="button"
|
|
219
|
-
onClick={() =>
|
|
220
|
-
|
|
221
|
-
setEditing(false);
|
|
222
|
-
}}
|
|
223
|
-
className={`block w-full truncate rounded-md px-3 py-1.5 text-left text-body ${
|
|
192
|
+
onClick={() => setSelectedKey(d.key)}
|
|
193
|
+
className={`block w-full truncate rounded-md px-3 py-1.5 text-left text-body transition-colors duration-200 ${
|
|
224
194
|
d.key === selectedKey
|
|
225
195
|
? 'bg-accent-subtle font-medium text-accent'
|
|
226
196
|
: 'text-text-secondary hover:bg-surface-2 hover:text-text-primary'
|
|
@@ -231,54 +201,501 @@ export function NotesClient({
|
|
|
231
201
|
))}
|
|
232
202
|
</nav>
|
|
233
203
|
|
|
234
|
-
<div className="min-w-0 flex-1
|
|
235
|
-
|
|
236
|
-
<
|
|
237
|
-
|
|
204
|
+
<div className="min-w-0 flex-1">
|
|
205
|
+
{selectedKey === GENERAL ? (
|
|
206
|
+
<GeneralDoc body={general} />
|
|
207
|
+
) : (
|
|
208
|
+
<ContractView
|
|
209
|
+
note={selectedNote}
|
|
210
|
+
contract={selectedKey}
|
|
211
|
+
onMutate={() => router.refresh()}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── General (freeform markdown) ────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
function GeneralDoc({ body }: { body: string }) {
|
|
223
|
+
return (
|
|
224
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-5">
|
|
225
|
+
<div className="mb-3 text-heading font-medium text-text-primary">General</div>
|
|
226
|
+
{body.trim() ? (
|
|
227
|
+
<MarkdownRenderer content={body} />
|
|
228
|
+
) : (
|
|
229
|
+
<p className="text-body text-text-tertiary">
|
|
230
|
+
No protocol-wide notes yet. Record a narration above against{' '}
|
|
231
|
+
<span className="font-medium">Other…</span> as <code className="font-mono">general</code> and
|
|
232
|
+
run <code className="font-mono">/diane</code>.
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Contract (structured view) ─────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
const SECTIONS = [
|
|
242
|
+
{ id: 'leads', label: 'Leads' },
|
|
243
|
+
{ id: 'description', label: 'Description' },
|
|
244
|
+
{ id: 'questions', label: 'Questions' },
|
|
245
|
+
] as const;
|
|
246
|
+
|
|
247
|
+
function scrollToSection(id: string) {
|
|
248
|
+
document.getElementById(`note-${id}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function ContractView({
|
|
252
|
+
note,
|
|
253
|
+
contract,
|
|
254
|
+
onMutate,
|
|
255
|
+
}: {
|
|
256
|
+
note: ContractNote | null;
|
|
257
|
+
contract: string;
|
|
258
|
+
onMutate: () => void;
|
|
259
|
+
}) {
|
|
260
|
+
const [busy, setBusy] = useState(false);
|
|
261
|
+
|
|
262
|
+
const post = useCallback(
|
|
263
|
+
async (url: string, body: unknown) => {
|
|
264
|
+
setBusy(true);
|
|
265
|
+
try {
|
|
266
|
+
const res = await fetch(url, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify(body),
|
|
270
|
+
});
|
|
271
|
+
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error ?? 'request failed');
|
|
272
|
+
onMutate();
|
|
273
|
+
} catch {
|
|
274
|
+
/* surface via refresh; keep UI resilient */
|
|
275
|
+
} finally {
|
|
276
|
+
setBusy(false);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[onMutate],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const isEmpty =
|
|
283
|
+
!note ||
|
|
284
|
+
(note.leads.length === 0 &&
|
|
285
|
+
note.questions.length === 0 &&
|
|
286
|
+
note.description.purpose.length === 0 &&
|
|
287
|
+
note.description.inheritance.length === 0 &&
|
|
288
|
+
note.description.storage.length === 0 &&
|
|
289
|
+
note.description.roles.length === 0 &&
|
|
290
|
+
note.description.functions.length === 0);
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className="rounded-lg border border-border-subtle bg-surface-1 p-5">
|
|
294
|
+
{/* Title + mark-done + section nav */}
|
|
295
|
+
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
|
|
296
|
+
<div className="flex items-center gap-2">
|
|
297
|
+
<span className="text-heading font-medium text-text-primary">{contract}</span>
|
|
298
|
+
{note?.file && (
|
|
299
|
+
<span className="font-mono text-caption text-text-tertiary">{note.file}</span>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
<label className="flex cursor-pointer items-center gap-2 text-caption text-text-secondary">
|
|
303
|
+
<input
|
|
304
|
+
type="checkbox"
|
|
305
|
+
checked={Boolean(note?.marked_done)}
|
|
306
|
+
disabled={busy || !note}
|
|
307
|
+
onChange={(e) => post('/api/notes/done', { contract, done: e.target.checked })}
|
|
308
|
+
className="h-4 w-4 rounded border-border-emphasis bg-surface-1 accent-[var(--accent)]"
|
|
309
|
+
/>
|
|
310
|
+
Mark done reading
|
|
311
|
+
</label>
|
|
312
|
+
</div>
|
|
313
|
+
|
|
314
|
+
{isEmpty ? (
|
|
315
|
+
<p className="text-body text-text-tertiary">
|
|
316
|
+
No notes yet — record a narration above and run <code className="font-mono">/diane</code>.
|
|
317
|
+
</p>
|
|
318
|
+
) : (
|
|
319
|
+
<>
|
|
320
|
+
<nav className="mb-5 flex flex-wrap items-center gap-1 border-b border-border-subtle pb-3 text-caption">
|
|
321
|
+
{SECTIONS.map((s) => (
|
|
322
|
+
<button
|
|
323
|
+
key={s.id}
|
|
324
|
+
type="button"
|
|
325
|
+
onClick={() => scrollToSection(s.id)}
|
|
326
|
+
className="rounded-md px-2 py-1 text-text-secondary transition-colors duration-200 hover:bg-surface-2 hover:text-text-primary"
|
|
327
|
+
>
|
|
328
|
+
{s.label}
|
|
329
|
+
</button>
|
|
330
|
+
))}
|
|
331
|
+
</nav>
|
|
332
|
+
|
|
333
|
+
<div className="space-y-6">
|
|
334
|
+
<LeadsSection
|
|
335
|
+
leads={note!.leads}
|
|
336
|
+
contract={contract}
|
|
337
|
+
busy={busy}
|
|
338
|
+
post={post}
|
|
339
|
+
/>
|
|
340
|
+
<DescriptionSection note={note!} />
|
|
341
|
+
<QuestionsSection
|
|
342
|
+
questions={note!.questions}
|
|
343
|
+
contract={contract}
|
|
344
|
+
busy={busy}
|
|
345
|
+
post={post}
|
|
346
|
+
/>
|
|
347
|
+
</div>
|
|
348
|
+
</>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ─── Leads ──────────────────────────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
function LeadsSection({
|
|
357
|
+
leads,
|
|
358
|
+
contract,
|
|
359
|
+
busy,
|
|
360
|
+
post,
|
|
361
|
+
}: {
|
|
362
|
+
leads: Lead[];
|
|
363
|
+
contract: string;
|
|
364
|
+
busy: boolean;
|
|
365
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
366
|
+
}) {
|
|
367
|
+
const open = leads.filter((l) => l.status === 'open');
|
|
368
|
+
const closed = leads.filter((l) => l.status !== 'open');
|
|
369
|
+
const [showClosed, setShowClosed] = useState(false);
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<section id="note-leads">
|
|
373
|
+
<h3 className="mb-2 text-heading font-medium text-text-primary">Leads</h3>
|
|
374
|
+
{open.length === 0 ? (
|
|
375
|
+
<p className="text-body text-text-tertiary">No open leads.</p>
|
|
376
|
+
) : (
|
|
377
|
+
<ul className="space-y-2">
|
|
378
|
+
{open.map((lead) => (
|
|
379
|
+
<li
|
|
380
|
+
key={lead.id}
|
|
381
|
+
className="flex items-start justify-between gap-3 rounded-md border border-border-subtle bg-surface-2 px-3 py-2"
|
|
382
|
+
>
|
|
383
|
+
<span className="min-w-0 flex-1 text-body text-text-primary">
|
|
384
|
+
<InlineMarkdown>{lead.text}</InlineMarkdown>
|
|
385
|
+
</span>
|
|
386
|
+
<button
|
|
387
|
+
type="button"
|
|
388
|
+
disabled={busy}
|
|
389
|
+
onClick={() =>
|
|
390
|
+
post('/api/notes/lead', { contract, id: lead.id, action: 'close', status: 'dismissed' })
|
|
391
|
+
}
|
|
392
|
+
className="shrink-0 rounded-md border border-border-default px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:bg-surface-3 hover:text-text-primary disabled:opacity-50"
|
|
393
|
+
>
|
|
394
|
+
Close
|
|
395
|
+
</button>
|
|
396
|
+
</li>
|
|
397
|
+
))}
|
|
398
|
+
</ul>
|
|
399
|
+
)}
|
|
400
|
+
|
|
401
|
+
{closed.length > 0 && (
|
|
402
|
+
<div className="mt-3">
|
|
403
|
+
<button
|
|
404
|
+
type="button"
|
|
405
|
+
onClick={() => setShowClosed((v) => !v)}
|
|
406
|
+
className="flex items-center gap-1.5 text-caption text-text-tertiary transition-colors duration-200 hover:text-text-secondary"
|
|
407
|
+
>
|
|
408
|
+
<svg
|
|
409
|
+
className={`h-3.5 w-3.5 transition-transform duration-200 ${showClosed ? 'rotate-90' : ''}`}
|
|
410
|
+
fill="none"
|
|
411
|
+
viewBox="0 0 24 24"
|
|
412
|
+
stroke="currentColor"
|
|
413
|
+
strokeWidth={2}
|
|
414
|
+
>
|
|
415
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
416
|
+
</svg>
|
|
417
|
+
Closed ({closed.length})
|
|
418
|
+
</button>
|
|
419
|
+
{showClosed && (
|
|
420
|
+
<ul className="mt-2 space-y-1.5">
|
|
421
|
+
{closed.map((lead) => (
|
|
422
|
+
<li
|
|
423
|
+
key={lead.id}
|
|
424
|
+
className="flex items-start justify-between gap-3 rounded-md px-3 py-1.5"
|
|
425
|
+
>
|
|
426
|
+
<span className="min-w-0 flex-1 text-caption text-text-tertiary">
|
|
427
|
+
<span className="line-through"><InlineMarkdown>{lead.text}</InlineMarkdown></span>
|
|
428
|
+
{lead.status === 'logged' && lead.ref && (
|
|
429
|
+
<span className="ml-2 font-mono text-text-secondary">→ {lead.ref}</span>
|
|
430
|
+
)}
|
|
431
|
+
{lead.status === 'dismissed' && (
|
|
432
|
+
<span className="ml-2 text-text-tertiary">dismissed</span>
|
|
433
|
+
)}
|
|
434
|
+
</span>
|
|
435
|
+
<button
|
|
436
|
+
type="button"
|
|
437
|
+
disabled={busy}
|
|
438
|
+
onClick={() => post('/api/notes/lead', { contract, id: lead.id, action: 'reopen' })}
|
|
439
|
+
className="shrink-0 rounded-md px-2 py-0.5 text-caption text-text-secondary transition-colors duration-200 hover:text-accent disabled:opacity-50"
|
|
440
|
+
>
|
|
441
|
+
Reopen
|
|
442
|
+
</button>
|
|
443
|
+
</li>
|
|
444
|
+
))}
|
|
445
|
+
</ul>
|
|
446
|
+
)}
|
|
447
|
+
</div>
|
|
448
|
+
)}
|
|
449
|
+
</section>
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ─── Description ────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
function BulletList({ label, items }: { label: string; items: string[] }) {
|
|
456
|
+
if (items.length === 0) return null;
|
|
457
|
+
return (
|
|
458
|
+
<div>
|
|
459
|
+
<h4 className="mb-1 text-body font-medium text-text-secondary">{label}</h4>
|
|
460
|
+
<ul className="list-disc space-y-0.5 pl-5 text-body text-text-primary">
|
|
461
|
+
{items.map((it, i) => (
|
|
462
|
+
<li key={i}>
|
|
463
|
+
<InlineMarkdown>{it}</InlineMarkdown>
|
|
464
|
+
</li>
|
|
465
|
+
))}
|
|
466
|
+
</ul>
|
|
467
|
+
</div>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function DescriptionSection({ note }: { note: ContractNote }) {
|
|
472
|
+
const { purpose, inheritance, storage, roles, functions } = note.description;
|
|
473
|
+
const hasAny =
|
|
474
|
+
purpose.length > 0 ||
|
|
475
|
+
inheritance.length > 0 ||
|
|
476
|
+
storage.length > 0 ||
|
|
477
|
+
roles.length > 0 ||
|
|
478
|
+
functions.length > 0;
|
|
479
|
+
|
|
480
|
+
return (
|
|
481
|
+
<section id="note-description">
|
|
482
|
+
<h3 className="mb-3 text-heading font-medium text-text-primary">Description</h3>
|
|
483
|
+
{!hasAny ? (
|
|
484
|
+
<p className="text-body text-text-tertiary">No description recorded yet.</p>
|
|
485
|
+
) : (
|
|
486
|
+
<div className="space-y-4">
|
|
487
|
+
<BulletList label="Purpose" items={purpose} />
|
|
488
|
+
<BulletList label="Inheritance" items={inheritance} />
|
|
489
|
+
<BulletList label="Storage" items={storage} />
|
|
490
|
+
<BulletList label="Roles & modifiers" items={roles} />
|
|
491
|
+
{functions.length > 0 && (
|
|
492
|
+
<div>
|
|
493
|
+
<h4 className="mb-1.5 text-body font-medium text-text-secondary">Functions</h4>
|
|
494
|
+
<div className="space-y-1.5">
|
|
495
|
+
{functions.map((fn) => (
|
|
496
|
+
<FunctionAccordion key={fn.id} fn={fn} />
|
|
497
|
+
))}
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
)}
|
|
501
|
+
</div>
|
|
502
|
+
)}
|
|
503
|
+
</section>
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function FunctionAccordion({ fn }: { fn: FnEntry }) {
|
|
508
|
+
const [open, setOpen] = useState(false);
|
|
509
|
+
const meta: [string, string | undefined][] = [
|
|
510
|
+
['Purpose', fn.purpose],
|
|
511
|
+
['Access', fn.access],
|
|
512
|
+
['Effects', fn.effects],
|
|
513
|
+
];
|
|
514
|
+
return (
|
|
515
|
+
<div className="rounded-md border border-border-subtle">
|
|
516
|
+
<button
|
|
517
|
+
type="button"
|
|
518
|
+
onClick={() => setOpen((v) => !v)}
|
|
519
|
+
className="flex w-full items-center gap-2 rounded-md bg-surface-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-surface-3"
|
|
520
|
+
>
|
|
521
|
+
<svg
|
|
522
|
+
className={`h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`}
|
|
523
|
+
fill="none"
|
|
524
|
+
viewBox="0 0 24 24"
|
|
525
|
+
stroke="currentColor"
|
|
526
|
+
strokeWidth={2}
|
|
527
|
+
>
|
|
528
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
529
|
+
</svg>
|
|
530
|
+
<span className="min-w-0 truncate font-mono text-body text-text-primary">{fn.sig}</span>
|
|
531
|
+
{fn.warnings.length > 0 && (
|
|
532
|
+
<span className="ml-auto shrink-0 text-caption text-[var(--high)]">
|
|
533
|
+
⚠ {fn.warnings.length}
|
|
534
|
+
</span>
|
|
535
|
+
)}
|
|
536
|
+
</button>
|
|
537
|
+
{open && (
|
|
538
|
+
<div className="space-y-2 px-3 py-2.5">
|
|
539
|
+
{meta
|
|
540
|
+
.filter(([, v]) => v && v.trim())
|
|
541
|
+
.map(([label, v]) => (
|
|
542
|
+
<p key={label} className="text-body text-text-primary">
|
|
543
|
+
<span className="text-text-secondary">{label}: </span>
|
|
544
|
+
<InlineMarkdown>{v as string}</InlineMarkdown>
|
|
545
|
+
</p>
|
|
546
|
+
))}
|
|
547
|
+
{fn.notes.length > 0 && (
|
|
548
|
+
<ul className="list-disc space-y-0.5 pl-5 text-body text-text-primary">
|
|
549
|
+
{fn.notes.map((n, i) => (
|
|
550
|
+
<li key={i}>
|
|
551
|
+
<InlineMarkdown>{n}</InlineMarkdown>
|
|
552
|
+
</li>
|
|
553
|
+
))}
|
|
554
|
+
</ul>
|
|
555
|
+
)}
|
|
556
|
+
{fn.warnings.length > 0 && (
|
|
557
|
+
<ul className="space-y-1">
|
|
558
|
+
{fn.warnings.map((w, i) => (
|
|
559
|
+
<li
|
|
560
|
+
key={i}
|
|
561
|
+
className="flex items-start gap-1.5 rounded-md bg-[var(--high)]/10 px-2 py-1 text-body text-[var(--high)]"
|
|
562
|
+
>
|
|
563
|
+
<span aria-hidden>⚠</span>
|
|
564
|
+
<span className="min-w-0 flex-1">
|
|
565
|
+
<InlineMarkdown>{w}</InlineMarkdown>
|
|
566
|
+
</span>
|
|
567
|
+
</li>
|
|
568
|
+
))}
|
|
569
|
+
</ul>
|
|
570
|
+
)}
|
|
571
|
+
</div>
|
|
572
|
+
)}
|
|
573
|
+
</div>
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ─── Questions ──────────────────────────────────────────────────────
|
|
578
|
+
|
|
579
|
+
function QuestionsSection({
|
|
580
|
+
questions,
|
|
581
|
+
contract,
|
|
582
|
+
busy,
|
|
583
|
+
post,
|
|
584
|
+
}: {
|
|
585
|
+
questions: Question[];
|
|
586
|
+
contract: string;
|
|
587
|
+
busy: boolean;
|
|
588
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
589
|
+
}) {
|
|
590
|
+
return (
|
|
591
|
+
<section id="note-questions">
|
|
592
|
+
<h3 className="mb-2 text-heading font-medium text-text-primary">Questions</h3>
|
|
593
|
+
{questions.length === 0 ? (
|
|
594
|
+
<p className="text-body text-text-tertiary">No questions recorded yet.</p>
|
|
595
|
+
) : (
|
|
596
|
+
<div className="space-y-1.5">
|
|
597
|
+
{questions.map((q) => (
|
|
598
|
+
<QuestionAccordion
|
|
599
|
+
key={q.id}
|
|
600
|
+
question={q}
|
|
601
|
+
contract={contract}
|
|
602
|
+
busy={busy}
|
|
603
|
+
post={post}
|
|
604
|
+
/>
|
|
605
|
+
))}
|
|
606
|
+
</div>
|
|
607
|
+
)}
|
|
608
|
+
</section>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function QuestionAccordion({
|
|
613
|
+
question,
|
|
614
|
+
contract,
|
|
615
|
+
busy,
|
|
616
|
+
post,
|
|
617
|
+
}: {
|
|
618
|
+
question: Question;
|
|
619
|
+
contract: string;
|
|
620
|
+
busy: boolean;
|
|
621
|
+
post: (url: string, body: unknown) => Promise<void>;
|
|
622
|
+
}) {
|
|
623
|
+
const answered = Boolean(question.answer && question.answer.trim());
|
|
624
|
+
const [open, setOpen] = useState(false);
|
|
625
|
+
const [editing, setEditing] = useState(false);
|
|
626
|
+
const [draft, setDraft] = useState(question.answer ?? '');
|
|
627
|
+
|
|
628
|
+
return (
|
|
629
|
+
<div className="rounded-md border border-border-subtle">
|
|
630
|
+
<button
|
|
631
|
+
type="button"
|
|
632
|
+
onClick={() => setOpen((v) => !v)}
|
|
633
|
+
className="flex w-full items-center gap-2 rounded-md bg-surface-2 px-3 py-2 text-left transition-colors duration-200 hover:bg-surface-3"
|
|
634
|
+
>
|
|
635
|
+
<svg
|
|
636
|
+
className={`h-4 w-4 shrink-0 text-text-tertiary transition-transform duration-200 ${open ? 'rotate-90' : ''}`}
|
|
637
|
+
fill="none"
|
|
638
|
+
viewBox="0 0 24 24"
|
|
639
|
+
stroke="currentColor"
|
|
640
|
+
strokeWidth={2}
|
|
641
|
+
>
|
|
642
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="m9 5 7 7-7 7" />
|
|
643
|
+
</svg>
|
|
644
|
+
<span className={`min-w-0 flex-1 text-body ${answered ? 'text-text-primary' : 'text-text-tertiary'}`}>
|
|
645
|
+
<InlineMarkdown>{question.q}</InlineMarkdown>
|
|
646
|
+
</span>
|
|
647
|
+
{!answered && <span className="shrink-0 text-caption text-text-tertiary">open</span>}
|
|
648
|
+
</button>
|
|
649
|
+
{open && (
|
|
650
|
+
<div className="space-y-2 px-3 py-2.5">
|
|
651
|
+
{editing ? (
|
|
652
|
+
<div className="space-y-2">
|
|
653
|
+
<textarea
|
|
654
|
+
value={draft}
|
|
655
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
656
|
+
rows={3}
|
|
657
|
+
className="w-full resize-y rounded-md border border-border-default bg-surface-0 p-2 text-body text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
|
|
658
|
+
/>
|
|
238
659
|
<div className="flex gap-2">
|
|
239
660
|
<button
|
|
240
661
|
type="button"
|
|
241
|
-
onClick={() =>
|
|
242
|
-
|
|
662
|
+
onClick={() => {
|
|
663
|
+
setEditing(false);
|
|
664
|
+
setDraft(question.answer ?? '');
|
|
665
|
+
}}
|
|
666
|
+
className="rounded-md px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:text-text-primary"
|
|
243
667
|
>
|
|
244
668
|
Cancel
|
|
245
669
|
</button>
|
|
246
670
|
<button
|
|
247
671
|
type="button"
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
672
|
+
disabled={busy || !draft.trim()}
|
|
673
|
+
onClick={async () => {
|
|
674
|
+
await post('/api/notes/question', { contract, id: question.id, answer: draft.trim() });
|
|
675
|
+
setEditing(false);
|
|
676
|
+
}}
|
|
677
|
+
className="rounded-md bg-accent px-2.5 py-1 text-caption font-medium text-surface-0 transition-opacity duration-200 hover:opacity-90 disabled:opacity-50"
|
|
251
678
|
>
|
|
252
|
-
|
|
679
|
+
Save answer
|
|
253
680
|
</button>
|
|
254
681
|
</div>
|
|
255
|
-
|
|
682
|
+
</div>
|
|
683
|
+
) : (
|
|
684
|
+
<div className="flex items-start justify-between gap-3">
|
|
685
|
+
<p className={`min-w-0 flex-1 text-body ${answered ? 'text-text-primary' : 'text-text-tertiary'}`}>
|
|
686
|
+
{answered ? <InlineMarkdown>{question.answer as string}</InlineMarkdown> : '— open'}
|
|
687
|
+
</p>
|
|
256
688
|
<button
|
|
257
689
|
type="button"
|
|
258
|
-
onClick={
|
|
259
|
-
className="rounded-md border border-border-default px-
|
|
690
|
+
onClick={() => setEditing(true)}
|
|
691
|
+
className="shrink-0 rounded-md border border-border-default px-2.5 py-1 text-caption text-text-secondary transition-colors duration-200 hover:bg-surface-3 hover:text-text-primary"
|
|
260
692
|
>
|
|
261
|
-
Edit
|
|
693
|
+
{answered ? 'Edit' : 'Answer'}
|
|
262
694
|
</button>
|
|
263
|
-
|
|
264
|
-
</div>
|
|
265
|
-
|
|
266
|
-
{editing ? (
|
|
267
|
-
<textarea
|
|
268
|
-
value={draft}
|
|
269
|
-
onChange={(e) => setDraft(e.target.value)}
|
|
270
|
-
className="h-[60vh] w-full resize-y rounded-md border border-border-default bg-surface-0 p-3 font-mono text-body text-text-primary focus:outline-none focus:ring-2 focus:ring-accent"
|
|
271
|
-
/>
|
|
272
|
-
) : current.body.trim() ? (
|
|
273
|
-
<MarkdownRenderer content={current.body} />
|
|
274
|
-
) : (
|
|
275
|
-
<p className="text-body text-text-tertiary">
|
|
276
|
-
No notes yet for {current.label}. Record a narration above and run{' '}
|
|
277
|
-
<code className="font-mono">/diane</code>, or click Edit to write notes by hand.
|
|
278
|
-
</p>
|
|
695
|
+
</div>
|
|
279
696
|
)}
|
|
280
697
|
</div>
|
|
281
|
-
|
|
698
|
+
)}
|
|
282
699
|
</div>
|
|
283
700
|
);
|
|
284
701
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getOutputDirPath, readJsonFile } from '@/lib/data';
|
|
2
|
-
import { readNotesIndex, readNote } from '../../../src/core/notes';
|
|
2
|
+
import { readNotesIndex, readNote, readContractNote, type ContractNote } from '../../../src/core/notes';
|
|
3
3
|
import { NotesClient } from './NotesClient';
|
|
4
4
|
|
|
5
5
|
// Read .hex/notes at request time (see app/stats/page.tsx for the rationale).
|
|
@@ -18,7 +18,6 @@ export default function NotesPage() {
|
|
|
18
18
|
const dir = getOutputDirPath();
|
|
19
19
|
const index = readNotesIndex(dir);
|
|
20
20
|
const general = readNote(dir, 'general');
|
|
21
|
-
const contractNotes = index.contracts.map((name) => ({ name, body: readNote(dir, name) }));
|
|
22
21
|
|
|
23
22
|
// Dropdown options: contracts in scope (from stats) ∪ contracts that already
|
|
24
23
|
// have notes, so the auditor can record against any contract immediately.
|
|
@@ -27,6 +26,10 @@ export default function NotesPage() {
|
|
|
27
26
|
.filter((c) => c.type === 'contract')
|
|
28
27
|
.map((c) => c.contract);
|
|
29
28
|
const options = Array.from(new Set([...scope, ...index.contracts])).sort();
|
|
29
|
+
|
|
30
|
+
// Load the structured note for each contract that has one (the index tracks
|
|
31
|
+
// every contract a note doc has been written for).
|
|
32
|
+
const contractNotes: ContractNote[] = index.contracts.map((name) => readContractNote(dir, name));
|
|
30
33
|
const unprocessed = index.sessions.filter((s) => !s.processed).length;
|
|
31
34
|
|
|
32
35
|
return (
|