@unson/brainbase-mcp 0.15.1 → 0.15.3
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/dist/canonical-task-service.d.ts +11 -7
- package/dist/canonical-task-service.js +49 -21
- package/dist/cli.js +39 -3
- package/dist/graph-web-http.d.ts +7 -0
- package/dist/graph-web-http.js +14 -0
- package/dist/graph-web.d.ts +36 -6
- package/dist/graph-web.js +45 -4
- package/dist/judgment-value-proof-review.d.ts +24 -0
- package/dist/judgment-value-proof-review.js +120 -9
- package/dist/local-web-host.d.ts +36 -0
- package/dist/local-web-host.js +26 -7
- package/dist/organization-graph-web.d.ts +80 -0
- package/dist/organization-graph-web.js +309 -0
- package/dist/value-proof-review-http.d.ts +5 -0
- package/dist/value-proof-review-http.js +19 -7
- package/dist/world-extension.d.ts +183 -0
- package/dist/world-extension.js +361 -0
- package/package.json +9 -3
- package/ui/graph-projects-view.js +11 -4
- package/ui/graph-registry-view.js +14 -4
- package/ui/graph-view-shared.css +2 -0
- package/ui/graph-view-shared.js +58 -3
- package/ui/local-web-shell.js +74 -12
- package/ui/value-proof-review.js +67 -7
- package/ui/world/world-placement.js +45 -0
- package/ui/world/world-vendor.LICENSE.txt +23 -0
- package/ui/world/world-vendor.js +4220 -0
- package/ui/world/world-view.css +234 -0
- package/ui/world/world-view.js +1092 -0
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { appendFile, mkdir, open, readdir, readFile, stat, unlink } from 'node:fs/promises';
|
|
3
|
-
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { placeJudgmentValueProof, validateJudgmentValueProof } from './judgment-value-proof.js';
|
|
5
5
|
import { defaultDataDir } from './paths.js';
|
|
6
6
|
export const JUDGMENT_VALUE_PROOF_FILE_SUFFIX = '.value-proof.json';
|
|
7
|
+
/** The judgment Host's per-turn final record, next to the turn's value proof. */
|
|
8
|
+
const JUDGMENT_FINAL_FILE_SUFFIX = '.final.json';
|
|
7
9
|
export const JUDGMENT_VALUE_PROOF_FEEDBACK_SCHEMA = 'brainbase-judgment-value-proof-feedback-v1';
|
|
8
10
|
export const JUDGMENT_VALUE_PROOF_FEEDBACK_FILE = 'judgment-value-proof-feedback.jsonl';
|
|
9
11
|
export const JUDGMENT_VALUE_PROOF_ANSWER_SCHEMA = 'brainbase-judgment-value-proof-answer-v1';
|
|
10
12
|
export const JUDGMENT_VALUE_PROOF_ANSWER_FILE = 'judgment-value-proof-answers.jsonl';
|
|
13
|
+
/** Folders stat'ed or listed at once. Node runs file system calls on a small thread pool; this keeps it fed. */
|
|
14
|
+
const JOURNAL_FOLDER_CONCURRENCY = 16;
|
|
15
|
+
/**
|
|
16
|
+
* A folder listed this soon after its last change is listed again on the next read: on file systems with
|
|
17
|
+
* coarse timestamps (HFS+ 1 s, exFAT 2 s) a change right after the listing can keep the same mtime.
|
|
18
|
+
*/
|
|
19
|
+
const JOURNAL_LISTING_SETTLE_NS = 3000000000n;
|
|
11
20
|
const FEEDBACK_SUMMARY_LIMIT = 500;
|
|
12
21
|
const ANSWER_LOCK_TIMEOUT_MS = 2_000;
|
|
13
22
|
/** A lock older than this was left by a write that stopped part way. */
|
|
@@ -61,9 +70,96 @@ async function readProofFile(file) {
|
|
|
61
70
|
return { file, reason: errorMessage(error) };
|
|
62
71
|
}
|
|
63
72
|
}
|
|
73
|
+
/** The `finalized_at` of a turn's final record, or null when it cannot be read as a time. */
|
|
74
|
+
async function readFinalizedAt(file) {
|
|
75
|
+
try {
|
|
76
|
+
const value = JSON.parse(await readFile(file, 'utf8')).finalized_at;
|
|
77
|
+
return typeof value === 'string' && !Number.isNaN(Date.parse(value)) ? value : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The judgment Host keeps each conversation in its own folder, one `<turn>.final.json` per finished turn.
|
|
85
|
+
* Returns when the first later turn of the same conversation was finalized after the waiting turn (compared
|
|
86
|
+
* with the waiting turn's own final, or the proof's recorded time when that is missing). An unreadable final
|
|
87
|
+
* is not evidence that the conversation moved on.
|
|
88
|
+
*/
|
|
89
|
+
async function conversationMovedOnAt(file, proof) {
|
|
90
|
+
const folder = dirname(file);
|
|
91
|
+
const ownFinal = `${basename(file, JUDGMENT_VALUE_PROOF_FILE_SUFFIX)}${JUDGMENT_FINAL_FILE_SUFFIX}`;
|
|
92
|
+
const since = Date.parse(await readFinalizedAt(join(folder, ownFinal)) ?? proof.recorded_at);
|
|
93
|
+
let names;
|
|
94
|
+
try {
|
|
95
|
+
names = await readdir(folder);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
let first = null;
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
if (!name.endsWith(JUDGMENT_FINAL_FILE_SUFFIX) || name === ownFinal)
|
|
103
|
+
continue;
|
|
104
|
+
const at = await readFinalizedAt(join(folder, name));
|
|
105
|
+
if (at && Date.parse(at) > since && (first === null || Date.parse(at) < Date.parse(first)))
|
|
106
|
+
first = at;
|
|
107
|
+
}
|
|
108
|
+
return first;
|
|
109
|
+
}
|
|
110
|
+
/** Maps with at most `limit` calls in flight; results keep the input order. */
|
|
111
|
+
async function mapConcurrently(items, limit, map) {
|
|
112
|
+
const results = new Array(items.length);
|
|
113
|
+
let next = 0;
|
|
114
|
+
const work = async () => {
|
|
115
|
+
while (next < items.length) {
|
|
116
|
+
const index = next++;
|
|
117
|
+
results[index] = await map(items[index]);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, work));
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
123
|
+
async function listValueProofFiles(folder) {
|
|
124
|
+
return (await readdir(folder, { withFileTypes: true }))
|
|
125
|
+
.filter((child) => child.isFile() && child.name.endsWith(JUDGMENT_VALUE_PROOF_FILE_SUFFIX))
|
|
126
|
+
.map((child) => join(folder, child.name));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Remembers which value proofs each folder of a judgment journal held, so that a later read lists only
|
|
130
|
+
* the folders that changed. A folder is listed again when its inode, mtime or ctime differs from the
|
|
131
|
+
* listing, or when it was listed within a few seconds of its last change. The judgment Host creates
|
|
132
|
+
* value proofs and finals by linking a new file and never rewrites them, so every new or removed file
|
|
133
|
+
* changes the folder's mtime. It keeps no proof contents; every read still reads each proof.
|
|
134
|
+
* Use one cache per journal.
|
|
135
|
+
*/
|
|
136
|
+
export class JudgmentValueProofJournalCache {
|
|
137
|
+
#folders = new Map();
|
|
138
|
+
/** The value proof files in `folder`, reusing the last listing while the folder is unchanged. */
|
|
139
|
+
async valueProofFiles(folder) {
|
|
140
|
+
const listedAtNs = BigInt(Date.now()) * 1000000n;
|
|
141
|
+
const { ino, mtimeNs, ctimeNs } = await stat(folder, { bigint: true });
|
|
142
|
+
const listing = this.#folders.get(folder);
|
|
143
|
+
if (listing && listing.ino === ino && listing.mtimeNs === mtimeNs && listing.ctimeNs === ctimeNs
|
|
144
|
+
&& listing.listedAtNs - mtimeNs >= JOURNAL_LISTING_SETTLE_NS) {
|
|
145
|
+
return listing.files;
|
|
146
|
+
}
|
|
147
|
+
const files = await listValueProofFiles(folder);
|
|
148
|
+
this.#folders.set(folder, { ino, mtimeNs, ctimeNs, listedAtNs, files });
|
|
149
|
+
return files;
|
|
150
|
+
}
|
|
151
|
+
/** Forgets folders that are no longer in the journal. */
|
|
152
|
+
retainOnly(folders) {
|
|
153
|
+
for (const folder of this.#folders.keys()) {
|
|
154
|
+
if (!folders.has(folder))
|
|
155
|
+
this.#folders.delete(folder);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
64
159
|
/**
|
|
65
160
|
* Reads saved judgment value proofs from `<root>/*.value-proof.json` and
|
|
66
161
|
* `<root>/<session>/*.value-proof.json`. The journal is read-only here.
|
|
162
|
+
* With `cache`, folders unchanged since an earlier read are not listed again; without it every folder is listed.
|
|
67
163
|
*/
|
|
68
164
|
export async function readJudgmentValueProofJournal(options = {}) {
|
|
69
165
|
const root = resolve(options.root ?? defaultJudgmentJournalRoot());
|
|
@@ -81,23 +177,31 @@ export async function readJudgmentValueProofJournal(options = {}) {
|
|
|
81
177
|
return { status: 'unavailable', root, reason };
|
|
82
178
|
}
|
|
83
179
|
const files = [];
|
|
180
|
+
const folders = [];
|
|
84
181
|
for (const entry of topLevel) {
|
|
85
182
|
const path = join(root, entry.name);
|
|
86
183
|
if (entry.isFile() && entry.name.endsWith(JUDGMENT_VALUE_PROOF_FILE_SUFFIX)) {
|
|
87
184
|
files.push(path);
|
|
88
185
|
}
|
|
89
186
|
else if (entry.isDirectory()) {
|
|
90
|
-
|
|
91
|
-
for (const child of nested) {
|
|
92
|
-
if (child.isFile() && child.name.endsWith(JUDGMENT_VALUE_PROOF_FILE_SUFFIX)) {
|
|
93
|
-
files.push(join(path, child.name));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
187
|
+
folders.push(path);
|
|
96
188
|
}
|
|
97
189
|
}
|
|
190
|
+
const { cache } = options;
|
|
191
|
+
const nested = await mapConcurrently(folders, JOURNAL_FOLDER_CONCURRENCY, (folder) => cache ? cache.valueProofFiles(folder) : listValueProofFiles(folder));
|
|
192
|
+
cache?.retainOnly(new Set(folders));
|
|
193
|
+
for (const folderFiles of nested)
|
|
194
|
+
files.push(...folderFiles);
|
|
98
195
|
const results = await Promise.all(files.sort().map(readProofFile));
|
|
99
|
-
const entries = results
|
|
196
|
+
const entries = (await Promise.all(results
|
|
100
197
|
.filter((result) => 'proof' in result)
|
|
198
|
+
.map(async (entry) => ({
|
|
199
|
+
...entry,
|
|
200
|
+
// Only a judgment inside a conversation folder can tell whether its conversation moved on.
|
|
201
|
+
conversation_moved_on_at: entry.proof.state === 'waiting_human' && dirname(entry.file) !== root
|
|
202
|
+
? await conversationMovedOnAt(entry.file, entry.proof)
|
|
203
|
+
: null
|
|
204
|
+
}))))
|
|
101
205
|
.sort((left, right) => compareRecordedAtDesc(left.proof.recorded_at, right.proof.recorded_at)
|
|
102
206
|
|| left.file.localeCompare(right.file));
|
|
103
207
|
const rejected = results.filter((result) => 'reason' in result);
|
|
@@ -429,7 +533,14 @@ export function buildJudgmentValueProofReviewHome(journal, feedback, options = {
|
|
|
429
533
|
const { proof, history } = applyJudgmentValueProofFeedback(entry.proof, feedback);
|
|
430
534
|
const section = classifyJudgmentValueProof(proof);
|
|
431
535
|
const answer = answers.get(`${proof.intent_id}\u0000${proof.decision_attempt_id}`) ?? null;
|
|
432
|
-
sections[section].push({
|
|
536
|
+
sections[section].push({
|
|
537
|
+
file: entry.file,
|
|
538
|
+
section,
|
|
539
|
+
proof,
|
|
540
|
+
feedback_history: history,
|
|
541
|
+
answer,
|
|
542
|
+
conversation_moved_on_at: entry.conversation_moved_on_at ?? null
|
|
543
|
+
});
|
|
433
544
|
}
|
|
434
545
|
const delegationMap = buildJudgmentDelegationMap([...sections.needs_human, ...sections.blocked, ...sections.continued], feedback);
|
|
435
546
|
const now = (options.now ?? new Date()).getTime();
|
package/dist/local-web-host.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { type IncomingMessage, type Server, type ServerResponse } from 'node:http';
|
|
2
2
|
import { type ObjectiveDefinitionBuilder } from './foundation-http.js';
|
|
3
|
+
import type { GraphWebAnySource, InMemoryGraphReader } from './graph-web.js';
|
|
4
|
+
import { JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
5
|
+
import type { GraphFileV2 } from './types.js';
|
|
3
6
|
/**
|
|
4
7
|
* General local single-owner Web host (`brainbase web:serve`).
|
|
5
8
|
*
|
|
@@ -38,6 +41,10 @@ export interface LocalWebModuleContext {
|
|
|
38
41
|
readonly journalRoot: string;
|
|
39
42
|
readonly token: string;
|
|
40
43
|
readonly now: () => Date;
|
|
44
|
+
/** Present when the host reads the organization Graph (C1). */
|
|
45
|
+
readonly organizationGraph?: LocalWebOrganizationGraph;
|
|
46
|
+
/** The host's one listing cache for the judgment journal, shared by every screen that reads it. */
|
|
47
|
+
readonly journalCache: JudgmentValueProofJournalCache;
|
|
41
48
|
}
|
|
42
49
|
/**
|
|
43
50
|
* Server side of one screen. The host has already rejected a non-loopback
|
|
@@ -59,6 +66,8 @@ export interface LocalWebExtension {
|
|
|
59
66
|
readonly uiFiles: readonly string[];
|
|
60
67
|
/** ES module exporting a `screen` compatible with createLocalWebShell. */
|
|
61
68
|
readonly screenEntry: string;
|
|
69
|
+
/** `first` lists the screen before the public screens and opens it when no screen is named. Defaults to `last`. */
|
|
70
|
+
readonly navPosition?: 'first' | 'last';
|
|
62
71
|
createModule(context: LocalWebModuleContext): LocalWebModule;
|
|
63
72
|
}
|
|
64
73
|
export interface LocalWebHostOptions {
|
|
@@ -71,6 +80,33 @@ export interface LocalWebHostOptions {
|
|
|
71
80
|
readonly uiDir?: string;
|
|
72
81
|
readonly now?: () => Date;
|
|
73
82
|
readonly extensions?: readonly LocalWebExtension[];
|
|
83
|
+
/**
|
|
84
|
+
* Ledger C1: the owner's organization Graph, read only. When given, the Graph
|
|
85
|
+
* screens read it instead of the data directory and the status reports it.
|
|
86
|
+
*/
|
|
87
|
+
readonly organizationGraph?: LocalWebOrganizationGraph;
|
|
88
|
+
}
|
|
89
|
+
export interface LocalWebOrganizationGraph {
|
|
90
|
+
/** Organization server origin shown as the source, without credentials. */
|
|
91
|
+
readonly server: string;
|
|
92
|
+
/** The organization's own web (where its records are corrected), when the owner configured it. */
|
|
93
|
+
readonly webUrl?: string;
|
|
94
|
+
read(): Promise<InMemoryGraphReader<GraphWebAnySource>>;
|
|
95
|
+
/** The same Graph as plain records (the world reads project metadata from it). */
|
|
96
|
+
readGraphFile?(): Promise<GraphFileV2>;
|
|
97
|
+
/**
|
|
98
|
+
* The organization's words for field values (glossary terms with `payload.vocabulary`), or null when
|
|
99
|
+
* they could not be read. The rest of the Graph is read even when these are not.
|
|
100
|
+
*/
|
|
101
|
+
readVocabularyTerms?(): Promise<readonly GraphVocabularyTerm[] | null>;
|
|
102
|
+
}
|
|
103
|
+
/** One organization term that names a field value, e.g. project.kind = product → プロダクト. */
|
|
104
|
+
export interface GraphVocabularyTerm {
|
|
105
|
+
readonly id: string;
|
|
106
|
+
readonly field: string;
|
|
107
|
+
readonly value: string;
|
|
108
|
+
readonly label: string;
|
|
109
|
+
readonly definition: string | null;
|
|
74
110
|
}
|
|
75
111
|
export interface LocalWebHost {
|
|
76
112
|
readonly server: Server;
|
package/dist/local-web-host.js
CHANGED
|
@@ -8,7 +8,7 @@ import { foundationCatalogOrEmpty, foundationKey } from './foundation-catalog.js
|
|
|
8
8
|
import { createFoundationHttpRouter, createObjectiveFoundationRoute, FOUNDATION_HTTP_PREFIX, FOUNDATION_OBJECTIVE_ROUTE_PREFIX } from './foundation-http.js';
|
|
9
9
|
import { createFoundationRevisionStore, FoundationStoreError } from './foundation-store.js';
|
|
10
10
|
import { createGraphWebHttpHandler } from './graph-web-http.js';
|
|
11
|
-
import { defaultJudgmentJournalRoot } from './judgment-value-proof-review.js';
|
|
11
|
+
import { defaultJudgmentJournalRoot, JudgmentValueProofJournalCache } from './judgment-value-proof-review.js';
|
|
12
12
|
import { nodeRequestToFetch, writeFetchResponse } from './local-web-fetch-bridge.js';
|
|
13
13
|
import { assertSameOrigin, isLoopbackHost, LOCAL_WEB_CONTENT_SECURITY_POLICY, LocalWebHttpError, readJsonObjectBody, requestUrl, tokenMatches, writeHttpError, writeJson } from './local-web-security.js';
|
|
14
14
|
import { resolveDataDir } from './paths.js';
|
|
@@ -138,7 +138,9 @@ export function createLocalStatusModule(context) {
|
|
|
138
138
|
writeJson(response, 405, { error: { code: 'method_not_allowed', message: 'Use GET' } });
|
|
139
139
|
return true;
|
|
140
140
|
}
|
|
141
|
-
const
|
|
141
|
+
const local = await readLocalGraphState(context.dataDir);
|
|
142
|
+
// Under C1 the Graph screens read the organization Graph, so they are not gated on the local file.
|
|
143
|
+
const graph = context.organizationGraph ? { ...local, status: 'ready', format: 'v2', message: undefined } : local;
|
|
142
144
|
writeJson(response, 200, {
|
|
143
145
|
version: LOCAL_WEB_HOST_VERSION,
|
|
144
146
|
data_dir: context.dataDir,
|
|
@@ -149,8 +151,14 @@ export function createLocalStatusModule(context) {
|
|
|
149
151
|
...(graph.message ? { message: graph.message } : {}),
|
|
150
152
|
commands: graphCommands(graph, context.dataDir)
|
|
151
153
|
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
organization_graph: context.organizationGraph
|
|
155
|
+
? {
|
|
156
|
+
status: 'connected',
|
|
157
|
+
server: context.organizationGraph.server,
|
|
158
|
+
mode: 'read_only',
|
|
159
|
+
...(context.organizationGraph.webUrl ? { web_url: context.organizationGraph.webUrl } : {})
|
|
160
|
+
}
|
|
161
|
+
: { status: 'not_connected' }
|
|
154
162
|
});
|
|
155
163
|
return true;
|
|
156
164
|
}
|
|
@@ -161,7 +169,8 @@ export function createValueProofModule(context) {
|
|
|
161
169
|
journalRoot: context.journalRoot,
|
|
162
170
|
dataDir: context.dataDir,
|
|
163
171
|
token: context.token,
|
|
164
|
-
now: context.now
|
|
172
|
+
now: context.now,
|
|
173
|
+
journalCache: context.journalCache
|
|
165
174
|
});
|
|
166
175
|
return {
|
|
167
176
|
id: 'value-proofs',
|
|
@@ -674,6 +683,7 @@ export function createGraphWebModule(context) {
|
|
|
674
683
|
dataDir: context.dataDir,
|
|
675
684
|
basePath: LOCAL_WEB_GRAPH_PREFIX,
|
|
676
685
|
now: context.now,
|
|
686
|
+
...(context.organizationGraph ? { readGraph: context.organizationGraph.read } : {}),
|
|
677
687
|
assertWriteAllowed(request) {
|
|
678
688
|
if (isTrustedLocalWrite(request, context.token))
|
|
679
689
|
return;
|
|
@@ -732,6 +742,8 @@ ${stylesheets.map((path) => `<link rel="stylesheet" href="${escapeAttribute(path
|
|
|
732
742
|
}
|
|
733
743
|
function bootstrapJs(extensions) {
|
|
734
744
|
const imports = extensions.map((extension, index) => `import { screen as extensionScreen${index} } from '/ui/extensions/${extension.id}/${extension.screenEntry}';`).join('\n');
|
|
745
|
+
const leading = extensions.flatMap((extension, index) => (extension.navPosition === 'first' ? [`extensionScreen${index}`] : [])).join(', ');
|
|
746
|
+
const trailing = extensions.flatMap((extension, index) => (extension.navPosition === 'first' ? [] : [`extensionScreen${index}`])).join(', ');
|
|
735
747
|
const screens = extensions.map((_, index) => `extensionScreen${index}`).join(', ');
|
|
736
748
|
const ids = JSON.stringify(extensions.map((extension) => extension.id));
|
|
737
749
|
return `import { createLocalWebShell, LOCAL_WEB_SCREENS } from '/ui/local-web-shell.js';
|
|
@@ -749,7 +761,7 @@ const shell = createLocalWebShell({
|
|
|
749
761
|
root: document.getElementById('brainbase-local-web'),
|
|
750
762
|
token: meta('brainbase-web-token'),
|
|
751
763
|
initialScreen: screenFromHash(),
|
|
752
|
-
screens: [...LOCAL_WEB_SCREENS, ...extensionScreens]
|
|
764
|
+
screens: ${leading ? `[${leading}, ...LOCAL_WEB_SCREENS${trailing ? `, ${trailing}` : ''}]` : '[...LOCAL_WEB_SCREENS, ...extensionScreens]'}
|
|
753
765
|
});
|
|
754
766
|
addEventListener('hashchange', () => shell.show(screenFromHash()));
|
|
755
767
|
`;
|
|
@@ -765,7 +777,14 @@ export function createLocalWebHost(options = {}) {
|
|
|
765
777
|
const dataDir = resolveDataDir(options.dataDir);
|
|
766
778
|
const journalRoot = options.journalRoot ?? defaultJudgmentJournalRoot(dataDir);
|
|
767
779
|
const uiDir = options.uiDir ?? fileURLToPath(new URL('../ui/', import.meta.url));
|
|
768
|
-
const context = {
|
|
780
|
+
const context = {
|
|
781
|
+
dataDir,
|
|
782
|
+
journalRoot,
|
|
783
|
+
token,
|
|
784
|
+
now: options.now ?? (() => new Date()),
|
|
785
|
+
journalCache: new JudgmentValueProofJournalCache(),
|
|
786
|
+
...(options.organizationGraph ? { organizationGraph: options.organizationGraph } : {})
|
|
787
|
+
};
|
|
769
788
|
const modules = defaultLocalWebModules(context);
|
|
770
789
|
const extensions = [...options.extensions ?? []];
|
|
771
790
|
const extensionModules = [];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { GraphVocabularyTerm, LocalWebOrganizationGraph } from './local-web-host.js';
|
|
2
|
+
import type { CanonicalEntityKind, GraphFileV2 } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ledger C1 (adopted 2026-09-24): while the owner validates the Web, the host
|
|
5
|
+
* reads the owner's organization Graph read only. The records are projected
|
|
6
|
+
* into an in-memory Graph v2 so the existing Graph screens read them through
|
|
7
|
+
* the same views as a local Graph. Nothing is written locally or remotely.
|
|
8
|
+
*
|
|
9
|
+
* Relations come only from what the organization Graph states:
|
|
10
|
+
* - `member_of` person → project → `participates_in` (role = role_code)
|
|
11
|
+
* - RACI `assigned_to` raci → person, with the RACI's project:
|
|
12
|
+
* accountable / A / lane decider / decision:* → `accountable_for`
|
|
13
|
+
* any other role → `participates_in`
|
|
14
|
+
* Retired or superseded records are counted, not drawn.
|
|
15
|
+
*/
|
|
16
|
+
export declare const ORGANIZATION_GRAPH_WEB_VERSION: "organization-graph-web.v0";
|
|
17
|
+
interface OrganizationRecord {
|
|
18
|
+
id: string;
|
|
19
|
+
entity_type?: string;
|
|
20
|
+
project_id?: string | null;
|
|
21
|
+
project_code?: string | null;
|
|
22
|
+
lifecycle_status?: string;
|
|
23
|
+
payload?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
interface OrganizationEdge {
|
|
26
|
+
id: string;
|
|
27
|
+
from_id: string;
|
|
28
|
+
to_id: string;
|
|
29
|
+
rel_type: string;
|
|
30
|
+
project_id?: string | null;
|
|
31
|
+
lifecycle_status?: string;
|
|
32
|
+
payload?: Record<string, unknown> | null;
|
|
33
|
+
}
|
|
34
|
+
export interface OrganizationGraphRecords {
|
|
35
|
+
readonly entities: Readonly<Record<CanonicalEntityKind | 'raci_assignment', readonly OrganizationRecord[]>>;
|
|
36
|
+
readonly memberOf: readonly OrganizationEdge[];
|
|
37
|
+
readonly assignedTo: readonly OrganizationEdge[];
|
|
38
|
+
}
|
|
39
|
+
export interface OrganizationGraphProjection {
|
|
40
|
+
readonly graph: GraphFileV2;
|
|
41
|
+
readonly excluded: {
|
|
42
|
+
readonly inactive: number;
|
|
43
|
+
readonly unnamed: number;
|
|
44
|
+
readonly danglingRelations: number;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The organization's words for field values: active glossary terms that carry
|
|
49
|
+
* `payload.vocabulary = { field, value }`. The name is the term's label, else
|
|
50
|
+
* its term, else the value itself (pure; tested).
|
|
51
|
+
*/
|
|
52
|
+
export declare function projectVocabularyTerms(records: readonly unknown[]): GraphVocabularyTerm[];
|
|
53
|
+
/** Projects organization Graph records into a canonical Graph v2 (pure; tested). */
|
|
54
|
+
export declare function projectOrganizationGraph(records: OrganizationGraphRecords, owner?: {
|
|
55
|
+
id: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
}): OrganizationGraphProjection;
|
|
58
|
+
interface OrganizationAccess {
|
|
59
|
+
readonly server: string;
|
|
60
|
+
readonly token: string;
|
|
61
|
+
}
|
|
62
|
+
/** The owner's Brainbase server and token, read at request time (tokens rotate hourly). Never logged or returned. */
|
|
63
|
+
export declare function readOrganizationAccess(home?: string): Promise<OrganizationAccess | {
|
|
64
|
+
readonly reason: string;
|
|
65
|
+
}>;
|
|
66
|
+
export interface OrganizationGraphSourceOptions {
|
|
67
|
+
readonly home?: string;
|
|
68
|
+
readonly fetch?: typeof fetch;
|
|
69
|
+
readonly now?: () => Date;
|
|
70
|
+
readonly cacheMs?: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The organization Graph for the local host (C1). Reads are cached briefly and
|
|
74
|
+
* shared while in flight; a failure is reported as `unavailable`, never as an
|
|
75
|
+
* empty Graph.
|
|
76
|
+
*/
|
|
77
|
+
export declare function createOrganizationGraphSource(options?: OrganizationGraphSourceOptions): Promise<LocalWebOrganizationGraph | {
|
|
78
|
+
readonly reason: string;
|
|
79
|
+
}>;
|
|
80
|
+
export {};
|