graphvault-studio 0.1.2 → 0.1.4

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.
@@ -5,17 +5,71 @@ export interface StorageAdminClientOptions {
5
5
  storageTarget?: StorageTarget;
6
6
  channelCount?: number;
7
7
  allowMutations?: boolean;
8
+ lockTimeoutMs?: number;
9
+ staleLockTimeoutMs?: number;
10
+ transactionLog?: "full" | "off";
11
+ }
12
+ export interface AdminStorageHardening {
13
+ mutationsAllowed: boolean;
14
+ transactionLog: "full" | "off";
15
+ writerLock: "enabled";
16
+ fencingTokens: "used-when-supported";
17
+ staleLockRecovery: boolean;
8
18
  }
9
19
  export interface AdminSummary {
10
20
  storageDirectory: string;
11
21
  transactionId: number;
12
22
  currentSnapshot?: string;
13
23
  objectCount: number;
24
+ library: AdminLibraryCompatibility;
14
25
  latestTransaction?: TransactionRecord;
15
26
  typeDictionary?: TypeDictionary;
27
+ hardening: AdminStorageHardening;
28
+ operations: AdminOperationalStatus;
29
+ productionSafety: AdminProductionSafety;
16
30
  verification?: VerificationResult;
17
31
  verificationSkipped?: boolean;
18
32
  }
33
+ export interface AdminOperationalStatus {
34
+ transactionLog: "full" | "off";
35
+ mutationsAllowed: boolean;
36
+ lockTimeoutMs: number;
37
+ staleLockTimeoutMs?: number;
38
+ walPrepareFiles: number;
39
+ walCommitFiles: number;
40
+ latestWalTransactionId: number;
41
+ latestJournalTransactionId: number;
42
+ publishedTransactionId: number;
43
+ pendingWalCommits: number;
44
+ checkedIntegrityHashes?: number;
45
+ status: "healthy" | "recovery-pending";
46
+ }
47
+ export interface AdminLibraryCompatibility {
48
+ packageName: "@sprengmeister/graphvault";
49
+ installedVersion?: string;
50
+ recommendedVersion: string;
51
+ status: "ok" | "warning";
52
+ warnings: string[];
53
+ }
54
+ export type AdminProductionSafetyStatus = "production-ready" | "warning" | "unsafe";
55
+ export type AdminProductionSafetySeverity = "info" | "warning" | "critical";
56
+ export interface AdminProductionSafetyIssue {
57
+ code: string;
58
+ severity: AdminProductionSafetySeverity;
59
+ message: string;
60
+ recommendation: string;
61
+ }
62
+ export interface AdminProductionSafety {
63
+ status: AdminProductionSafetyStatus;
64
+ score: number;
65
+ summary: string;
66
+ transactionLog: "full" | "off";
67
+ mutationsAllowed: boolean;
68
+ staleLockRecovery: boolean;
69
+ pendingRecovery: boolean;
70
+ hashChain: "present" | "missing";
71
+ issues: AdminProductionSafetyIssue[];
72
+ }
19
73
  export interface AdminObjectListItem {
20
74
  objectId: string;
21
75
  kind: EncodedNode["kind"];
@@ -70,8 +124,12 @@ export interface AdminSearchResult {
70
124
  }
71
125
  export interface AdminGraph {
72
126
  root: EncodedValue;
127
+ depth?: number;
128
+ complete?: boolean;
129
+ rootObjectId?: string;
73
130
  nodes: AdminGraphNode[];
74
131
  edges: AdminGraphEdge[];
132
+ truncatedReferences?: AdminSubtreeReference[];
75
133
  }
76
134
  export interface AdminGraphNode {
77
135
  objectId: string;
@@ -83,10 +141,43 @@ export interface AdminGraphEdge {
83
141
  to: string;
84
142
  path: string;
85
143
  }
144
+ export interface AdminSubtreeReference {
145
+ fromObjectId: string;
146
+ toObjectId: string;
147
+ path: string;
148
+ depth: number;
149
+ }
150
+ export interface AdminSubtree {
151
+ root: EncodedValue;
152
+ rootObjectId?: string;
153
+ transactionId: number;
154
+ depth: number;
155
+ complete: boolean;
156
+ objectIds: string[];
157
+ nodes: AdminGraphNode[];
158
+ edges: AdminGraphEdge[];
159
+ truncatedReferences: AdminSubtreeReference[];
160
+ envelope: {
161
+ format: "graphvault";
162
+ version: 1;
163
+ createdAt: string;
164
+ root: EncodedValue;
165
+ nodes: Record<string, EncodedNode>;
166
+ };
167
+ }
86
168
  export interface AdminMutation {
87
169
  objectId: string;
88
170
  path: string;
89
171
  value: unknown;
172
+ metadata?: AdminTransactionMetadata;
173
+ }
174
+ export interface AdminTransactionMetadata {
175
+ actor?: string;
176
+ reason?: string;
177
+ source?: string;
178
+ traceId?: string;
179
+ tags?: string[];
180
+ attributes?: Record<string, string | number | boolean | null>;
90
181
  }
91
182
  export interface AdminMutationPreview {
92
183
  objectId: string;
package/dist/admin-ui.js CHANGED
@@ -45,6 +45,8 @@ export const ADMIN_HTML = `<!doctype html>
45
45
  .sidebar-card { display: grid; align-content: start; gap: 8px; padding: 10px; border-radius: 8px; background: rgba(255, 255, 255, 0.06); }
46
46
  nav .hint { align-self: end; }
47
47
  .row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 8px; }
48
+ .row-split { grid-template-columns: minmax(0, 1fr) 82px; }
49
+ .mini-labels { color: #9fb1b9; font-size: 12px; padding: 2px 2px 0; }
48
50
  .topbar { min-height: 74px; padding: 16px 24px; display: grid; grid-template-columns: 1fr auto auto; gap: 14px; align-items: center; background: linear-gradient(180deg, #ffffff 0%, #fbfdfd 100%); border-bottom: 1px solid var(--line); }
49
51
  .topbar h1 { margin: 0; font-family: "Avenir Next", "SF Pro Display", "Aptos Display", "Inter", ui-sans-serif, system-ui, sans-serif; font-size: 24px; letter-spacing: 0; font-weight: 600; }
50
52
  .auth { display: none; grid-template-columns: minmax(160px, 280px) auto; gap: 8px; }
@@ -62,6 +64,7 @@ export const ADMIN_HTML = `<!doctype html>
62
64
  .mutation summary { cursor: pointer; font-weight: 600; }
63
65
  .mutation[open] { display: grid; gap: 8px; }
64
66
  .mutation-grid { display: grid; grid-template-columns: 90px 1fr 1fr 1fr; gap: 8px; }
67
+ .audit-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
65
68
  .gvql-box { display: none; padding: 12px 14px; border-bottom: 1px solid var(--line); background: #f8fafb; gap: 8px; }
66
69
  .gvql-box.active { display: grid; }
67
70
  .gvql-examples { display: flex; flex-wrap: wrap; gap: 6px; }
@@ -115,7 +118,7 @@ export const ADMIN_HTML = `<!doctype html>
115
118
  .field-actions { grid-area: actions; justify-content: flex-start; }
116
119
  }
117
120
  @media (max-width: 980px) {
118
- .shell, .kpis, .mutation-grid, .topbar, .fields-head { grid-template-columns: 1fr; }
121
+ .shell, .kpis, .mutation-grid, .audit-grid, .topbar, .fields-head { grid-template-columns: 1fr; }
119
122
  .gvql-actions, .gvql-parameter-row { grid-template-columns: 1fr; }
120
123
  nav { grid-template-rows: auto; gap: 10px; padding: 12px; }
121
124
  .brand { grid-template-columns: 42px 1fr; padding-bottom: 2px; }
@@ -141,6 +144,7 @@ export const ADMIN_HTML = `<!doctype html>
141
144
  <button class="nav-btn" data-view="objects" onclick="showObjects()">Objects</button>
142
145
  <button class="nav-btn" data-view="graph" onclick="showGraph()">Graph</button>
143
146
  <button class="nav-btn" data-view="gvql" onclick="showGvql()">GVQL</button>
147
+ <button class="nav-btn" data-view="operations" onclick="showOperations()">Operations</button>
144
148
  <button class="nav-btn" data-view="types" onclick="showTypes()">Type Dictionary</button>
145
149
  <button class="nav-btn" data-view="transactions" onclick="showTransactions()">Transactions</button>
146
150
  <button class="nav-btn" data-view="journal" onclick="showJournal()">Journal</button>
@@ -148,6 +152,9 @@ export const ADMIN_HTML = `<!doctype html>
148
152
  </div>
149
153
  <div class="sidebar-card">
150
154
  <div class="row"><input id="q" placeholder="Search paths, values, types, IDs" /><button onclick="showSearch()">Search</button></div>
155
+ <div class="row row-split mini-labels"><span>Graph root ID</span><span>Depth</span></div>
156
+ <div class="row row-split"><input id="graphRoot" placeholder="Graph root object ID" /><input id="graphDepth" type="number" min="0" max="12" value="2" /></div>
157
+ <button onclick="showGraph()">Load Graph Slice</button>
151
158
  <div class="row"><input id="backupPath" placeholder="Backup destination" /><button onclick="runBackup()">Backup</button></div>
152
159
  <button onclick="runMaintenance()">Run Maintenance</button>
153
160
  </div>
@@ -172,6 +179,11 @@ export const ADMIN_HTML = `<!doctype html>
172
179
  <input id="mvalue" placeholder='New JSON value, e.g. "active"' />
173
180
  <input id="confirmToken" placeholder="Confirm token" />
174
181
  </div>
182
+ <div class="audit-grid">
183
+ <input id="auditActor" placeholder="Actor, e.g. ops@example.com" />
184
+ <input id="auditReason" placeholder="Reason for change" />
185
+ <input id="auditTraceId" placeholder="Trace ID" />
186
+ </div>
175
187
  <div class="actions"><button onclick="preview()">Preview</button><button class="danger" onclick="mutate()">Commit Change</button></div>
176
188
  </details>
177
189
  <div class="gvql-box" id="gvqlPanel">
@@ -185,6 +197,8 @@ OFFSET 0</textarea>
185
197
  <input id="gvqlParams" type="hidden" />
186
198
  <div class="gvql-actions">
187
199
  <input id="gvqlConfirmToken" placeholder="Confirm token" />
200
+ <input id="gvqlAuditActor" placeholder="Actor" />
201
+ <input id="gvqlAuditReason" placeholder="Reason" />
188
202
  <button onclick="runGvql(true)">Run / Preview</button>
189
203
  <button class="danger" onclick="runGvql(false)">Commit GVQL</button>
190
204
  </div>
@@ -223,8 +237,9 @@ OFFSET 0</textarea>
223
237
  hierarchy: ['Object Hierarchy', 'Root-first graph view with references you can follow.'],
224
238
  overview: ['Storage Overview', 'Health, object count, latest transaction, and current snapshot.'],
225
239
  objects: ['Objects', 'Browse graph records with type, preview, and transaction metadata.'],
226
- graph: ['Object Graph', 'Click nodes or edges to inspect referenced records.'],
240
+ graph: ['Object Graph', 'Depth-limited graph slice for large stores and API-style inspection.'],
227
241
  gvql: ['GVQL Query', 'Run graph pattern queries and preview batch updates.'],
242
+ operations: ['Operations', 'Storage hardening, WAL state, and recovery readiness.'],
228
243
  types: ['Type Dictionary', 'Registered runtime types and schema metadata.'],
229
244
  transactions: ['Transactions', 'Newest commits first.'],
230
245
  journal: ['Journal', 'Append-only storage activity log.'],
@@ -308,7 +323,14 @@ OFFSET 0</textarea>
308
323
  }
309
324
  async function refreshKpis() {
310
325
  const summary = await requestJson('/api/summary?verify=false');
311
- kpis.innerHTML = kpi('Objects', summary.objectCount) + kpi('Transaction', summary.transactionId) + kpi('Verify', summary.verification ? (summary.verification.ok ? 'OK' : 'FAIL') : 'manual') + kpi('Snapshot', summary.currentSnapshot || '-');
326
+ const hardening = summary.hardening || {};
327
+ const ops = summary.operations || {};
328
+ const library = summary.library || {};
329
+ const safety = summary.productionSafety || {};
330
+ const walLabel = (hardening.transactionLog || '-') + (typeof ops.pendingWalCommits === 'number' ? ' / ' + ops.pendingWalCommits + ' pending' : '');
331
+ const libraryLabel = (library.installedVersion || '-') + (library.status === 'warning' ? ' warning' : '');
332
+ const safetyLabel = (safety.status || '-') + (typeof safety.score === 'number' ? ' / ' + safety.score : '');
333
+ kpis.innerHTML = kpi('Objects', summary.objectCount) + kpi('Transaction', summary.transactionId) + kpi('Safety', safetyLabel) + kpi('Library', libraryLabel) + kpi('WAL', walLabel) + kpi('Lock', hardening.writerLock || '-') + kpi('Ops', ops.status || '-') + kpi('Snapshot', summary.currentSnapshot || '-');
312
334
  return summary;
313
335
  }
314
336
  async function showHierarchy() {
@@ -326,6 +348,7 @@ OFFSET 0</textarea>
326
348
  show({ root: root.rootObjectId, note: 'Only the visible branch is loaded. Click a row to drill into its children.' });
327
349
  }
328
350
  async function showObjectWithChildren(id, label, parentPath) {
351
+ document.getElementById('graphRoot').value = id;
329
352
  const record = await apiJson('/api/objects/' + encodeURIComponent(id));
330
353
  hierarchyPath = parentPath.concat([{ id, label, record }]);
331
354
  renderEditableFields(record);
@@ -345,6 +368,7 @@ OFFSET 0</textarea>
345
368
  setRows(rows, 'No children');
346
369
  }
347
370
  async function showObjectInHierarchyPath(id) {
371
+ document.getElementById('graphRoot').value = id;
348
372
  setView('hierarchy');
349
373
  listTitle.textContent = 'Object hierarchy';
350
374
  listHint.textContent = 'Search context';
@@ -396,7 +420,20 @@ OFFSET 0</textarea>
396
420
  listHint.textContent = 'Overview';
397
421
  const summary = await refreshKpis();
398
422
  show(summary);
399
- setRows(summary.latestTransaction ? [{ columns: ['#' + summary.latestTransaction.transactionId, summary.latestTransaction.mode, summary.latestTransaction.snapshotFile, summary.latestTransaction.objectIds.length + ' ids'], onclick: () => show(summary.latestTransaction) }] : [], 'No transactions yet');
423
+ const rows = [];
424
+ if (summary.operations) {
425
+ rows.push({ columns: [summary.operations.status, 'operations', summary.operations.pendingWalCommits + ' pending WAL commits', summary.operations.walCommitFiles + ' WAL commits'], onclick: () => show(summary.operations) });
426
+ }
427
+ if (summary.productionSafety) {
428
+ rows.push({ columns: [summary.productionSafety.status, 'production safety', summary.productionSafety.score + ' score', summary.productionSafety.issues.length + ' issues'], onclick: () => show(summary.productionSafety) });
429
+ }
430
+ if (summary.library) {
431
+ rows.push({ columns: [summary.library.installedVersion || '-', 'library', 'recommended ' + summary.library.recommendedVersion, summary.library.status], onclick: () => show(summary.library) });
432
+ }
433
+ if (summary.latestTransaction) {
434
+ rows.push({ columns: ['#' + summary.latestTransaction.transactionId, summary.latestTransaction.mode, summary.latestTransaction.snapshotFile, summary.latestTransaction.objectIds.length + ' ids'], onclick: () => show(summary.latestTransaction) });
435
+ }
436
+ setRows(rows, 'No transactions yet');
400
437
  }
401
438
  async function showObjects() {
402
439
  setView('objects');
@@ -427,6 +464,7 @@ OFFSET 0</textarea>
427
464
  list.appendChild(pager);
428
465
  }
429
466
  async function showObject(id) {
467
+ document.getElementById('graphRoot').value = id;
430
468
  document.getElementById('mid').value = id;
431
469
  document.getElementById('detailHint').textContent = 'Object #' + id;
432
470
  const record = await requestJson('/api/objects/' + encodeURIComponent(id));
@@ -565,11 +603,39 @@ OFFSET 0</textarea>
565
603
  }
566
604
  async function showGraph() {
567
605
  setView('graph');
568
- listTitle.textContent = 'Graph edges';
569
- listHint.textContent = 'Follow references';
570
- const graph = await requestJson('/api/graph');
606
+ listTitle.textContent = 'Graph slice';
607
+ const depth = graphDepth();
608
+ const rootObjectId = document.getElementById('graphRoot').value.trim();
609
+ listHint.textContent = (rootObjectId ? 'Root #' + rootObjectId : 'Store root') + ', depth ' + depth;
610
+ const graph = await requestJson(graphSubtreeUrl(rootObjectId, depth));
571
611
  renderGraph(graph);
572
- setRows(graph.edges.map(e => ({ columns: [e.from + ' -> ' + e.to, 'edge', e.path, 'ref'], onclick: () => showObject(e.to) })), 'No edges found');
612
+ const rows = [
613
+ {
614
+ columns: [graph.complete ? 'complete' : 'partial', 'subtree', graph.objectIds.length + ' objects', graph.truncatedReferences.length + ' boundary refs'],
615
+ onclick: () => show(graph)
616
+ },
617
+ ...graph.nodes.map(node => ({
618
+ columns: ['#' + node.objectId, node.type || node.kind, node.objectId === graph.rootObjectId ? 'subtree root' : 'loaded node', 'node'],
619
+ onclick: () => showObject(node.objectId)
620
+ })),
621
+ ...graph.edges.map(e => ({
622
+ columns: [e.from + ' -> ' + e.to, 'edge', e.path, 'loaded'],
623
+ onclick: () => showObject(e.to)
624
+ })),
625
+ ...(graph.truncatedReferences || []).map(ref => ({
626
+ columns: [ref.fromObjectId + ' -> ' + ref.toObjectId, 'boundary', ref.path, 'depth ' + ref.depth],
627
+ onclick: () => showObjectInHierarchyPath(ref.toObjectId)
628
+ }))
629
+ ];
630
+ setRows(rows, 'No graph objects found');
631
+ }
632
+ function graphDepth() {
633
+ const value = Number.parseInt(document.getElementById('graphDepth').value || '2', 10);
634
+ return Number.isInteger(value) && value >= 0 ? Math.min(value, 12) : 2;
635
+ }
636
+ function graphSubtreeUrl(rootObjectId, depth) {
637
+ const query = '?depth=' + encodeURIComponent(String(depth));
638
+ return rootObjectId ? '/api/objects/' + encodeURIComponent(rootObjectId) + '/subtree' + query : '/api/subtree' + query;
573
639
  }
574
640
  function renderGraph(graph) {
575
641
  const width = 900;
@@ -607,8 +673,8 @@ OFFSET 0</textarea>
607
673
  circle.setAttribute('cx', pos.x);
608
674
  circle.setAttribute('cy', pos.y);
609
675
  circle.setAttribute('r', '26');
610
- circle.setAttribute('fill', node.objectId === 'root' ? '#f3c969' : '#d9f1ef');
611
- circle.setAttribute('stroke', node.objectId === 'root' ? '#ab7622' : '#0f8b8d');
676
+ circle.setAttribute('fill', node.objectId === graph.rootObjectId ? '#f3c969' : '#d9f1ef');
677
+ circle.setAttribute('stroke', node.objectId === graph.rootObjectId ? '#ab7622' : '#0f8b8d');
612
678
  circle.setAttribute('stroke-width', '2');
613
679
  const label = document.createElementNS(ns, 'text');
614
680
  label.setAttribute('x', pos.x);
@@ -624,6 +690,23 @@ OFFSET 0</textarea>
624
690
  }
625
691
  const showTypes = async () => { setView('types'); listTitle.textContent = 'Types'; listHint.textContent = 'Dictionary'; const value = await requestJson('/api/types'); setRows((value && value.entries || []).map(e => ({ columns: [String(e.id || '-'), e.name || 'type', e.handler || e.name || '-', 'type'], onclick: () => show(e) })), 'No type dictionary found'); };
626
692
  const showTransactions = async () => { setView('transactions'); listTitle.textContent = 'Transactions'; listHint.textContent = 'Newest first'; const rows = await requestJson('/api/transactions'); setRows(rows.map(t => ({ columns: ['#' + t.transactionId, t.mode, t.snapshotFile, t.objectIds.length + ' ids'], onclick: () => show(t) })), 'No transactions found'); };
693
+ const showOperations = async () => {
694
+ setView('operations');
695
+ listTitle.textContent = 'Storage operations';
696
+ listHint.textContent = 'WAL, hardening, and production safety';
697
+ const summary = await requestJson('/api/summary?verify=false');
698
+ const ops = summary.operations || {};
699
+ const safety = summary.productionSafety || {};
700
+ setRows([
701
+ { columns: [safety.status || '-', 'production safety', typeof safety.score === 'number' ? safety.score + ' score' : '-', (safety.issues || []).length + ' issues'], onclick: () => show(safety) },
702
+ { columns: [ops.status, 'health', ops.pendingWalCommits + ' pending WAL commits', ops.latestWalTransactionId ? 'wal tx ' + ops.latestWalTransactionId : 'no wal'], onclick: () => show(ops) },
703
+ { columns: [ops.transactionLog, 'wal', ops.walPrepareFiles + ' prepares', ops.walCommitFiles + ' commits'], onclick: () => show(ops) },
704
+ { columns: [String(ops.checkedIntegrityHashes || 0), 'integrity hashes', 'transaction chain', ops.checkedIntegrityHashes ? 'present' : 'not recorded'], onclick: () => show(ops) },
705
+ { columns: [ops.mutationsAllowed ? 'enabled' : 'disabled', 'mutations', 'lock timeout ' + ops.lockTimeoutMs + 'ms', ops.staleLockTimeoutMs ? 'stale ' + ops.staleLockTimeoutMs + 'ms' : 'no stale recovery'], onclick: () => show(ops) },
706
+ { columns: ['#' + ops.publishedTransactionId, 'published', ops.latestJournalTransactionId ? 'journal tx ' + ops.latestJournalTransactionId : 'no journal', 'manifest'], onclick: () => show(ops) }
707
+ ], 'No operations status');
708
+ return ops;
709
+ };
627
710
  const showJournal = async () => {
628
711
  setView('journal');
629
712
  listTitle.textContent = 'Journal';
@@ -637,7 +720,8 @@ OFFSET 0</textarea>
637
720
  listHint.textContent = 'Integrity';
638
721
  const result = await requestJson('/api/verify');
639
722
  setRows([
640
- { columns: [result.ok ? 'OK' : 'FAIL', 'health', result.errors.length ? result.errors.join('; ') : 'No errors', result.checkedObjects + ' objects'], onclick: () => show(result) }
723
+ { columns: [result.ok ? 'OK' : 'FAIL', 'health', result.errors.length ? result.errors.join('; ') : 'No errors', result.checkedObjects + ' objects'], onclick: () => show(result) },
724
+ { columns: [String(result.checkedIntegrityHashes || 0), 'integrity hashes', result.checkedWalRecords + ' WAL checks', result.pendingWalCommits + ' pending WAL'], onclick: () => show(result) }
641
725
  ], 'Verification has not run');
642
726
  return result;
643
727
  };
@@ -821,6 +905,7 @@ OFFSET 0</textarea>
821
905
  dryRun
822
906
  };
823
907
  if (!dryRun && confirmRequired) payload.confirmToken = document.getElementById('gvqlConfirmToken').value;
908
+ if (!dryRun) payload.metadata = readAuditMetadata('gvql');
824
909
  const result = await postJson('/api/gvql', payload);
825
910
  const rows = [];
826
911
  if (result.plan) {
@@ -872,8 +957,19 @@ OFFSET 0</textarea>
872
957
  }
873
958
  const confirmRequired = __CONFIRM_REQUIRED__;
874
959
  if (!confirmRequired) document.getElementById('confirmToken').style.display = 'none';
960
+ if (!confirmRequired) document.getElementById('gvqlConfirmToken').style.display = 'none';
961
+ function readAuditMetadata(prefix) {
962
+ const actor = document.getElementById(prefix === 'gvql' ? 'gvqlAuditActor' : 'auditActor')?.value.trim();
963
+ const reason = document.getElementById(prefix === 'gvql' ? 'gvqlAuditReason' : 'auditReason')?.value.trim();
964
+ const traceId = prefix === 'gvql' ? '' : document.getElementById('auditTraceId')?.value.trim();
965
+ const metadata = { source: prefix === 'gvql' ? 'graphvault-studio:gvql' : 'graphvault-studio:direct-edit' };
966
+ if (actor) metadata.actor = actor;
967
+ if (reason) metadata.reason = reason;
968
+ if (traceId) metadata.traceId = traceId;
969
+ return metadata;
970
+ }
875
971
  function mutationPayload(includeConfirm) {
876
- const payload = { objectId: document.getElementById('mid').value, path: document.getElementById('mpath').value, value: JSON.parse(document.getElementById('mvalue').value) };
972
+ const payload = { objectId: document.getElementById('mid').value, path: document.getElementById('mpath').value, value: JSON.parse(document.getElementById('mvalue').value), metadata: readAuditMetadata('direct') };
877
973
  if (includeConfirm) payload.confirmToken = document.getElementById('confirmToken').value;
878
974
  return payload;
879
975
  }
@@ -1 +1 @@
1
- {"version":3,"file":"admin-ui.js","sourceRoot":"","sources":["../src/admin-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAqNC,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA8pBvD,CAAC"}
1
+ {"version":3,"file":"admin-ui.js","sourceRoot":"","sources":["../src/admin-ui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE1D,MAAM,CAAC,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BAmOC,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAgvBvD,CAAC"}
@@ -18,9 +18,12 @@ npm ci
18
18
  npm test
19
19
  npm run demo:store
20
20
  npm run pack:dry-run
21
+ npm run package:smoke
21
22
  ```
22
23
 
23
- Inspect the dry-run file list and confirm it contains `README.md`, `LICENSE`, `CHANGELOG.md`, `CONTRIBUTING.md`, `docs`, `dist`, `examples`, logo/screenshot assets, and the `graphvault-studio` CLI entry point.
24
+ `npm test` includes `npm run compat:check`, which verifies that Studio's `@sprengmeister/graphvault` dependency range accepts the runtime version recommended by the current Studio build. If the check warns that the lockfile resolves an older GraphVault Library, publish or install the newer library before a production Studio release.
25
+
26
+ Inspect the dry-run file list and confirm it contains `README.md`, `LICENSE`, `CHANGELOG.md`, `CONTRIBUTING.md`, `docs`, `dist`, `examples`, logo/screenshot assets, and the `graphvault-studio` CLI entry point. `npm run package:smoke` installs the generated tarball into a fresh temporary project, verifies the CLI help output, imports the public programmatic API, creates a real GraphVault store, and exercises search plus bounded subtree loading through the installed package.
24
27
 
25
28
  ## Tagging
26
29
 
@@ -35,7 +38,7 @@ git push origin v0.1.0
35
38
 
36
39
  Use the GitHub Actions `Release` workflow with the matching tag input, for example `v0.1.0`.
37
40
 
38
- The workflow checks out the tag, installs with `npm ci`, runs tests, creates the demo store, validates the npm tarball with `npm run pack:dry-run`, and publishes with npm provenance.
41
+ The workflow checks out the tag, installs with `npm ci`, runs tests, creates the demo store, validates the npm tarball with `npm run pack:dry-run`, performs the fresh-install package smoke test, and publishes with npm provenance.
39
42
 
40
43
  ## Repository Visibility
41
44
 
@@ -8,8 +8,10 @@ GraphVault Studio 0.1.0 is the first public graphical admin client for GraphVaul
8
8
  - Search across paths, values, ids, types, references, and encoded object data.
9
9
  - Parent-path lookup from an object back toward the root, including multiple direct parents.
10
10
  - Paged object browser for large stores.
11
+ - Depth-limited subtree API and graph slice view for large stores.
11
12
  - GVQL console for graph queries and dry-run batch mutation previews.
12
13
  - Controlled primitive-field editing with confirmation-token safety.
14
+ - Visible GraphVault Library compatibility status.
13
15
  - Verification, maintenance, backup, transaction, journal, graph, type dictionary, and object detail views.
14
16
  - CLI and programmatic server startup.
15
17
  - Local filesystem, custom, remote, S3-compatible, HTTP, and SQL-backed storage through GraphVault storage targets.
@@ -53,4 +53,22 @@ await startAdminServer({
53
53
  });
54
54
  ```
55
55
 
56
- For mutation endpoints, always set `allowMutations: true` and a `mutationConfirmToken`. For exposed or shared environments, also set `authToken`.
56
+ For mutation endpoints, always set `allowMutations: true` and a `mutationConfirmToken`. For exposed or shared environments, also set authentication. The legacy `authToken` option grants admin access. Prefer role tokens when several people or tools use Studio:
57
+
58
+ ```ts
59
+ await startAdminServer({
60
+ storageDirectory: "main",
61
+ storageTarget,
62
+ allowMutations: true,
63
+ mutationConfirmToken: process.env.GRAPHVAULT_ADMIN_CONFIRM_TOKEN,
64
+ accessTokens: [
65
+ { token: process.env.GRAPHVAULT_VIEWER_TOKEN!, role: "viewer" },
66
+ { token: process.env.GRAPHVAULT_OPERATOR_TOKEN!, role: "operator" },
67
+ { token: process.env.GRAPHVAULT_ADMIN_ROLE_TOKEN!, role: "admin" },
68
+ ],
69
+ });
70
+ ```
71
+
72
+ - `viewer`: read-only inspection, search, verification, transactions, journal, and GVQL dry-runs.
73
+ - `operator`: viewer plus maintenance and backup.
74
+ - `admin`: operator plus committed direct edits and GVQL mutations. Admin writes should still use a confirmation token.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphvault-studio",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Graphical admin client for GraphVault object graph stores.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,9 +37,11 @@
37
37
  ],
38
38
  "scripts": {
39
39
  "build": "tsc -p tsconfig.json",
40
- "test": "npm run build && node tests/smoke.mjs",
40
+ "compat:check": "node scripts/check-compatibility.mjs",
41
+ "test": "npm run compat:check && npm run build && node tests/smoke.mjs",
41
42
  "demo:store": "npm run build && node examples/create-demo-store.mjs",
42
43
  "pack:dry-run": "npm pack --dry-run --cache ./.npm-cache",
44
+ "package:smoke": "node scripts/package-smoke.mjs",
43
45
  "prepack": "npm run build"
44
46
  },
45
47
  "keywords": [
@@ -58,7 +60,7 @@
58
60
  "node": ">=20"
59
61
  },
60
62
  "dependencies": {
61
- "@sprengmeister/graphvault": "0.1.0"
63
+ "@sprengmeister/graphvault": ">=0.1.0 <0.3.0"
62
64
  },
63
65
  "devDependencies": {
64
66
  "@types/node": "^22.15.3",