@unson/brainbase-mcp 0.2.4 → 0.3.0
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/README.md +17 -12
- package/dist/canonical-edge-builder.d.ts +11 -0
- package/dist/canonical-edge-builder.js +35 -0
- package/dist/canonical-graph.d.ts +9 -0
- package/dist/canonical-graph.js +174 -0
- package/dist/cli.js +134 -32
- package/dist/connected-onboarding.js +45 -11
- package/dist/entity-resolution.d.ts +111 -0
- package/dist/entity-resolution.js +381 -0
- package/dist/graph-diagnosis.d.ts +24 -0
- package/dist/graph-diagnosis.js +129 -0
- package/dist/guided-onboarding.js +4 -4
- package/dist/import-extract.d.ts +8 -3
- package/dist/import-extract.js +142 -17
- package/dist/onboarding.js +5 -5
- package/dist/ontology-migration.d.ts +30 -0
- package/dist/ontology-migration.js +339 -0
- package/dist/ontology-ssot.js +2 -2
- package/dist/ontology.d.ts +101 -3
- package/dist/ontology.js +64 -8
- package/dist/operationalization.js +6 -6
- package/dist/projects.d.ts +3 -1
- package/dist/projects.js +40 -0
- package/dist/relation-registry.d.ts +11 -0
- package/dist/relation-registry.js +66 -0
- package/dist/server.d.ts +86 -2
- package/dist/server.js +158 -9
- package/dist/skills.js +3 -3
- package/dist/ssot.d.ts +16 -0
- package/dist/ssot.js +68 -18
- package/dist/templates.d.ts +110 -2
- package/dist/templates.js +74 -5
- package/dist/tools.d.ts +6 -2
- package/dist/tools.js +184 -147
- package/dist/types.d.ts +52 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Brainbaseは、自分が承認した仕事の前提をCodex、Claude Code、Code
|
|
|
4
4
|
|
|
5
5
|
最初の目標は情報源をすべて接続することではありません。自分、仕事、関係者、判断基準の最小文脈を保存し、10分以内に「同じ前提を説明し直さず役立つ出力」を確認することです。
|
|
6
6
|
|
|
7
|
-
Ontology 1.0.0
|
|
7
|
+
Ontology 2.0.0は、ローカルファイルへ持ち運べる意味契約に、Relation Registryで管理する正規エンティティ間のIDエッジを追加します。ホスト型Brainbaseを必要とせず、型、関係語彙、検証制約、決定論的な判断推論、バージョン移行を定義します。履歴解釈として0.0.0と1.0.0も選択できます。
|
|
8
8
|
|
|
9
9
|
このリポジトリに、社内BrainbaseのUI、セッション実行基盤、xterm転送、ワークフロー管制、SNS運用、ホスト型バックエンド、Infisical設定、雲孫の社内データは含みません。それらは社内版`brainbase-unson`の範囲です。
|
|
10
10
|
|
|
@@ -35,12 +35,12 @@ brainbase onboard:start --target codex
|
|
|
35
35
|
# 表示された onboard:seed を確認して実行
|
|
36
36
|
brainbase onboard:install --target codex --dry-run
|
|
37
37
|
# 設定を承認・反映し、Codexを再起動
|
|
38
|
-
# 新しいCodexでBrainbaseのget_context/searchを使って実際の依頼を試す
|
|
38
|
+
# 新しいCodexでBrainbaseのresolve_entity/get_context/searchを使って実際の依頼を試す
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
リポジトリをcloneした場合も、`npm run onboard:start -- --target codex`から同じ順序で進めます。
|
|
42
42
|
|
|
43
|
-
利用者がBrainbaseの導入を依頼したら、エージェントはチェックリストを返すだけでなく、この公開CLIを実行します。承認された最小文脈を保存し、MCP設定を反映した新しい実エージェントで`get_context
|
|
43
|
+
利用者がBrainbaseの導入を依頼したら、エージェントはチェックリストを返すだけでなく、この公開CLIを実行します。承認された最小文脈を保存し、MCP設定を反映した新しい実エージェントで`resolve_entity`、`get_context`、`search`を使って現実の依頼へ回答します。その実回答を見た本人が「役立った」と判断して初めて初回価値です。`ready: true`、`cli_sample_ready`、CLIの処理時間、合成ペルソナ評価、Skillsやルーティンの生成、`onboard:install --dry-run`だけでは導入完了ではありません。
|
|
44
44
|
|
|
45
45
|
For a Google Workspace / Google Drive / local-notes setup, pass the known answers and let the command surface what still needs approval:
|
|
46
46
|
|
|
@@ -91,7 +91,7 @@ Optionally preview the saved context locally. This is not an onboarding completi
|
|
|
91
91
|
brainbase onboard:demo --scenario "Draft the first note I should send to Key Partner about Current project"
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
`onboard:demo` reads only locally saved, approved facts. It does not call an LLM, an agent, or a hosted backend. Its result is only a preview. Continue through MCP installation, restart the selected agent, make a real request using `get_context
|
|
94
|
+
`onboard:demo` reads only locally saved, approved facts. It does not call an LLM, an agent, or a hosted backend. Its result is only a preview. Continue through MCP installation, restart the selected agent, make a real request using `resolve_entity`, `get_context`, and `search`, and ask the user whether that actual answer was useful.
|
|
95
95
|
|
|
96
96
|
After the demo, keep onboarding open: the preview is not the first-value gate. Continue until a real agent uses Brainbase and the human user confirms that the result was useful.
|
|
97
97
|
|
|
@@ -100,10 +100,10 @@ After seed, install and verify MCP before asking for the human value judgment:
|
|
|
100
100
|
```bash
|
|
101
101
|
brainbase onboard:install --target codex --dry-run
|
|
102
102
|
brainbase doctor
|
|
103
|
-
# restart Codex, use get_context/search for the real request, then ask whether it was useful
|
|
103
|
+
# restart Codex, use resolve_entity/get_context/search for the real request, then ask whether it was useful
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
The recommended order is public skills, `ohayo` / `oyasumi` / `retro` routines registered paused or confirmation-gated, real MCP config merge after approving the dry-run snippet, source allowlist / import / candidate review decisions, then `doctor` plus MCP `get_context` / `search` verification from a fresh agent session.
|
|
106
|
+
The recommended order is public skills, `ohayo` / `oyasumi` / `retro` routines registered paused or confirmation-gated, real MCP config merge after approving the dry-run snippet, source allowlist / import / candidate review decisions, then `doctor` plus MCP `resolve_entity` / `get_context` / `search` verification from a fresh agent session.
|
|
107
107
|
|
|
108
108
|
The commands above are still safe by default. `onboard:skills` and `onboard:routines` generate output unless you provide an explicit `--out`, and `onboard:install --dry-run` is only a preview. Do not treat those generated artifacts as installed until the user approves file writes, scheduler registration, and live config changes.
|
|
109
109
|
|
|
@@ -290,7 +290,7 @@ The default data directory is:
|
|
|
290
290
|
|
|
291
291
|
It contains the canonical local SSOT:
|
|
292
292
|
|
|
293
|
-
- `graph.json`: people, organizations, projects, and
|
|
293
|
+
- `graph.json`: canonical people, organizations, projects, and decisions, plus typed stable-ID edges between them.
|
|
294
294
|
- `personal-kg.jsonl`: values, judgment criteria, experiences, and personal context.
|
|
295
295
|
- `relationships.json`: relationship context that should survive across tools.
|
|
296
296
|
- `decisions.jsonl`: decision records and principles.
|
|
@@ -319,14 +319,15 @@ BRAINBASE_PERSONAL_OS_DIR=/path/to/personal-os brainbase-mcp
|
|
|
319
319
|
- `get_context`: returns initial AI context from the local Graph and Personal KG.
|
|
320
320
|
- `list_entities`: lists `person`, `org`, `project`, `relationship`, and `decision` entities.
|
|
321
321
|
- `search`: searches canonical Graph and Personal KG data.
|
|
322
|
+
- `resolve_entity`: resolves mentions in text to canonical Graph v2 IDs and returns a privacy-safe evidence receipt.
|
|
322
323
|
- `search_personal_kg`: searches owner-local Personal KG only.
|
|
323
324
|
- `onboarding_status`: reports seeded areas, first value demo readiness, missing setup, and local connection status.
|
|
324
|
-
- `get_ontology`: returns the immutable bundled Ontology
|
|
325
|
+
- `get_ontology`: returns the immutable bundled active Ontology 2.0.0 release without reading Personal OS files.
|
|
325
326
|
- `audit_ontology`: audits canonical local files and distinguishes verified violations from unavailable input.
|
|
326
327
|
- `infer_decisions`: derives active, superseded, and conflicting decisions from explicit rules.
|
|
327
328
|
- `ontology_impact`: explains compatibility, migration, and rollback from an earlier ontology version.
|
|
328
329
|
|
|
329
|
-
## Portable Ontology
|
|
330
|
+
## Portable Ontology 2.0.0
|
|
330
331
|
|
|
331
332
|
Inspect the semantic contract and audit your local canonical files:
|
|
332
333
|
|
|
@@ -334,14 +335,18 @@ Inspect the semantic contract and audit your local canonical files:
|
|
|
334
335
|
brainbase ontology:show
|
|
335
336
|
brainbase ontology:audit
|
|
336
337
|
brainbase ontology:audit --ontology-version 0.0.0
|
|
338
|
+
brainbase ontology:audit --ontology-version 1.0.0
|
|
339
|
+
brainbase ontology:migrate
|
|
340
|
+
# previewのexpectedInputDigestを確認してから適用
|
|
341
|
+
brainbase ontology:migrate --write --expected-input-digest '<previewの値>'
|
|
337
342
|
```
|
|
338
343
|
|
|
339
344
|
`ontology:audit` exits non-zero when an error-level violation exists or when a canonical file cannot be verified. It never reports an unavailable or malformed source as zero violations. Warnings, such as a relationship whose person is not yet present in the Graph, remain visible but do not block approved writes.
|
|
340
|
-
Use `--ontology-version 0.0.0` to interpret a pre-kernel snapshot without retroactively applying the
|
|
345
|
+
Use `--ontology-version 0.0.0` to interpret a pre-kernel snapshot without retroactively applying the later `effectiveAt`, supersession, conflict, or validation rules. Use `--ontology-version 1.0.0` for the immutable first portable release. When the flag is omitted, Graph v2 uses its recorded ontology binding; legacy Graph data uses the active 2.0.0 release. The selected version is included in audit and inference results; unsupported versions fail explicitly.
|
|
341
346
|
|
|
342
347
|
Decision evolution is opt-in, read-compatible, and write-gated. Existing decision rows remain readable. New rows may add `topic`, `supersedes`, and `effectiveAt`; only an explicit `supersedes` reference makes an older decision inactive. Multiple active decisions with the same explicit `topic` are reported as a conflict instead of being silently resolved.
|
|
343
348
|
|
|
344
|
-
Before enabling
|
|
349
|
+
Before enabling 2.0.0 writes, back up the Personal OS directory, run the read-only historical audit when upgrading from 1.0.0, and preview `brainbase ontology:migrate`. Apply the migration only with the preview's `expectedInputDigest`; a concurrent input change blocks the write. Existing rows remain readable, but error-level semantic violations must be reviewed before a canonical write. Roll back by restoring the pre-migration backup and reinstalling the recorded last known working package version.
|
|
345
350
|
|
|
346
351
|
## CLI
|
|
347
352
|
|
|
@@ -505,7 +510,7 @@ Keep or pin the internal `brainbase-unson` system when you need:
|
|
|
505
510
|
- Legacy Graph API MCP tools such as `get_entity`.
|
|
506
511
|
- VibePro runtime or internal 31013 operation surfaces.
|
|
507
512
|
|
|
508
|
-
The
|
|
513
|
+
The original MCP surface remains compatible and adds the Ontology tools plus `resolve_entity`. The active Ontology release is 2.0.0; 0.0.0 and 1.0.0 remain available for historical interpretation.
|
|
509
514
|
|
|
510
515
|
## Hosted Backends
|
|
511
516
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CanonicalEdge, CanonicalEntity, CanonicalGraphFile, GraphFileV2 } from './types.js';
|
|
2
|
+
export interface CanonicalWriteSet {
|
|
3
|
+
entities: CanonicalEntity[];
|
|
4
|
+
edges: CanonicalEdge[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Apply explicit canonical writes without deriving relations from labels or free text.
|
|
8
|
+
* Existing records with other IDs keep their order and content; matching IDs are updated in place.
|
|
9
|
+
*/
|
|
10
|
+
export declare function applyCanonicalWrites(graph: CanonicalGraphFile, writes: CanonicalWriteSet): GraphFileV2;
|
|
11
|
+
export declare function buildCanonicalEdge(edge: Omit<CanonicalEdge, 'id'>): CanonicalEdge;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { canonicalEdgeId, validateCanonicalGraph } from './canonical-graph.js';
|
|
2
|
+
/**
|
|
3
|
+
* Apply explicit canonical writes without deriving relations from labels or free text.
|
|
4
|
+
* Existing records with other IDs keep their order and content; matching IDs are updated in place.
|
|
5
|
+
*/
|
|
6
|
+
export function applyCanonicalWrites(graph, writes) {
|
|
7
|
+
if (graph.version !== 2) {
|
|
8
|
+
throw new Error('migration_required: Graph v1 cannot store canonical ID edges; migrate graph.json to Graph v2 before writing');
|
|
9
|
+
}
|
|
10
|
+
const next = {
|
|
11
|
+
...graph,
|
|
12
|
+
entities: upsertById(graph.entities, writes.entities),
|
|
13
|
+
edges: upsertById(graph.edges, writes.edges)
|
|
14
|
+
};
|
|
15
|
+
validateCanonicalGraph(next);
|
|
16
|
+
return next;
|
|
17
|
+
}
|
|
18
|
+
export function buildCanonicalEdge(edge) {
|
|
19
|
+
return { ...edge, id: canonicalEdgeId(edge) };
|
|
20
|
+
}
|
|
21
|
+
function upsertById(existing, additions) {
|
|
22
|
+
const result = [...existing];
|
|
23
|
+
const indexById = new Map(result.map((record, index) => [record.id, index]));
|
|
24
|
+
for (const addition of additions) {
|
|
25
|
+
const index = indexById.get(addition.id);
|
|
26
|
+
if (index === undefined) {
|
|
27
|
+
indexById.set(addition.id, result.length);
|
|
28
|
+
result.push(addition);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
result[index] = addition;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CanonicalEdge, CanonicalGraphFile } from './types.js';
|
|
2
|
+
export declare function canonicalEdgeId(edge: Pick<CanonicalEdge, 'fromId' | 'relation' | 'toId'>): string;
|
|
3
|
+
export declare function validateCanonicalGraph(graph: unknown, options?: {
|
|
4
|
+
allowDuplicateEntityIds?: boolean;
|
|
5
|
+
}): asserts graph is CanonicalGraphFile;
|
|
6
|
+
export declare function isActiveAt(record: {
|
|
7
|
+
validFrom?: string;
|
|
8
|
+
validTo?: string;
|
|
9
|
+
}, asOf: string): boolean;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { getCanonicalRelation } from './relation-registry.js';
|
|
3
|
+
export function canonicalEdgeId(edge) {
|
|
4
|
+
return `edge-${createHash('sha256').update(JSON.stringify([edge.fromId, edge.relation, edge.toId])).digest('hex').slice(0, 24)}`;
|
|
5
|
+
}
|
|
6
|
+
const v1EntityKinds = new Set(['person', 'org', 'project', 'relationship']);
|
|
7
|
+
const canonicalEntityKinds = new Set(['person', 'org', 'project', 'decision']);
|
|
8
|
+
export function validateCanonicalGraph(graph, options = {}) {
|
|
9
|
+
if (!graph || typeof graph !== 'object' || !('version' in graph) || !('entities' in graph)) {
|
|
10
|
+
throw new Error('GRAPH-SHAPE-VALID: graph must be an object with version and entities');
|
|
11
|
+
}
|
|
12
|
+
const candidate = graph;
|
|
13
|
+
if (candidate.version !== 1 && candidate.version !== 2) {
|
|
14
|
+
throw new Error(`GRAPH-VERSION-SUPPORTED: unsupported graph version ${String(candidate.version)}`);
|
|
15
|
+
}
|
|
16
|
+
if (!Array.isArray(candidate.entities)) {
|
|
17
|
+
throw new Error('GRAPH-SHAPE-VALID: entities must be an array');
|
|
18
|
+
}
|
|
19
|
+
const entityIds = new Set();
|
|
20
|
+
for (const [index, entityValue] of candidate.entities.entries()) {
|
|
21
|
+
if (!entityValue || typeof entityValue !== 'object') {
|
|
22
|
+
throw new Error(`GRAPH-ENTITY-SHAPE at entities[${index}]: entity must be an object`);
|
|
23
|
+
}
|
|
24
|
+
const entity = entityValue;
|
|
25
|
+
assertNonEmptyString(entity.id, `entities[${index}].id`);
|
|
26
|
+
assertNonEmptyString(entity.name, `entities[${index}].name`);
|
|
27
|
+
const allowedKinds = candidate.version === 1 ? v1EntityKinds : canonicalEntityKinds;
|
|
28
|
+
if (typeof entity.type !== 'string' || !allowedKinds.has(entity.type)) {
|
|
29
|
+
throw new Error(`GRAPH-ENTITY-TYPE at entities[${index}].type: unsupported entity type ${String(entity.type)}`);
|
|
30
|
+
}
|
|
31
|
+
if (entity.aliases !== undefined && (!Array.isArray(entity.aliases) || entity.aliases.some((alias) => typeof alias !== 'string' || alias.trim() === ''))) {
|
|
32
|
+
throw new Error(`GRAPH-ENTITY-ALIASES at entities[${index}].aliases: aliases must be non-empty strings`);
|
|
33
|
+
}
|
|
34
|
+
assertOptionalString(entity.summary, `entities[${index}].summary`);
|
|
35
|
+
assertOptionalStringArray(entity.tags, `entities[${index}].tags`);
|
|
36
|
+
assertOptionalRecord(entity.metadata, `entities[${index}].metadata`);
|
|
37
|
+
if (entityIds.has(entity.id)) {
|
|
38
|
+
if (!options.allowDuplicateEntityIds) {
|
|
39
|
+
throw new Error(`GRAPH-ENTITY-ID-UNIQUE at entities[${index}].id: duplicate canonical entity ID ${entity.id}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
entityIds.add(entity.id);
|
|
43
|
+
}
|
|
44
|
+
if (candidate.owner !== undefined) {
|
|
45
|
+
assertOptionalRecord(candidate.owner, 'owner');
|
|
46
|
+
const owner = candidate.owner;
|
|
47
|
+
assertOptionalString(owner.id, 'owner.id');
|
|
48
|
+
assertOptionalString(owner.name, 'owner.name');
|
|
49
|
+
assertOptionalString(owner.summary, 'owner.summary');
|
|
50
|
+
}
|
|
51
|
+
if (candidate.version === 1)
|
|
52
|
+
return;
|
|
53
|
+
if (!candidate.ontology || typeof candidate.ontology !== 'object') {
|
|
54
|
+
throw new Error('GRAPH-ONTOLOGY-REQUIRED: Graph v2 requires ontology binding');
|
|
55
|
+
}
|
|
56
|
+
const ontology = candidate.ontology;
|
|
57
|
+
if (ontology.id !== 'brainbase-personal-os') {
|
|
58
|
+
throw new Error('GRAPH-ONTOLOGY-ID: Graph v2 ontology.id must be brainbase-personal-os');
|
|
59
|
+
}
|
|
60
|
+
assertNonEmptyString(ontology.version, 'ontology.version');
|
|
61
|
+
assertNonEmptyString(ontology.releaseDigest, 'ontology.releaseDigest');
|
|
62
|
+
if (!Array.isArray(candidate.edges)) {
|
|
63
|
+
throw new Error('GRAPH-SHAPE-VALID: Graph v2 edges must be an array');
|
|
64
|
+
}
|
|
65
|
+
const typedGraph = graph;
|
|
66
|
+
const entities = new Map();
|
|
67
|
+
for (const [index, entity] of typedGraph.entities.entries()) {
|
|
68
|
+
assertInterval(entity.validFrom, entity.validTo, `entities[${index}]`);
|
|
69
|
+
entities.set(entity.id, entity);
|
|
70
|
+
}
|
|
71
|
+
const edgeIds = new Set();
|
|
72
|
+
const tuples = new Set();
|
|
73
|
+
for (const [index, edge] of typedGraph.edges.entries()) {
|
|
74
|
+
if (!edge || typeof edge !== 'object') {
|
|
75
|
+
throw new Error(`GRAPH-EDGE-SHAPE at edges[${index}]: edge must be an object`);
|
|
76
|
+
}
|
|
77
|
+
assertNonEmptyString(edge.id, `edges[${index}].id`);
|
|
78
|
+
assertNonEmptyString(edge.fromId, `edges[${index}].fromId`);
|
|
79
|
+
assertNonEmptyString(edge.relation, `edges[${index}].relation`);
|
|
80
|
+
assertNonEmptyString(edge.toId, `edges[${index}].toId`);
|
|
81
|
+
assertOptionalString(edge.role, `edges[${index}].role`);
|
|
82
|
+
assertOptionalString(edge.context, `edges[${index}].context`);
|
|
83
|
+
if (edge.provenance !== undefined) {
|
|
84
|
+
assertOptionalRecord(edge.provenance, `edges[${index}].provenance`);
|
|
85
|
+
if (!['user_approved', 'migration', 'import', 'onboarding'].includes(String(edge.provenance.sourceKind))) {
|
|
86
|
+
throw new Error(`GRAPH-EDGE-PROVENANCE at edges[${index}].provenance.sourceKind: unsupported source kind`);
|
|
87
|
+
}
|
|
88
|
+
assertOptionalString(edge.provenance.sourceId, `edges[${index}].provenance.sourceId`);
|
|
89
|
+
assertOptionalString(edge.provenance.evidenceHash, `edges[${index}].provenance.evidenceHash`);
|
|
90
|
+
}
|
|
91
|
+
if (edgeIds.has(edge.id)) {
|
|
92
|
+
throw new Error(`GRAPH-EDGE-ID-UNIQUE at edges[${index}].id: duplicate canonical edge ID ${edge.id}`);
|
|
93
|
+
}
|
|
94
|
+
edgeIds.add(edge.id);
|
|
95
|
+
const tuple = JSON.stringify([edge.fromId, edge.relation, edge.toId]);
|
|
96
|
+
if (tuples.has(tuple)) {
|
|
97
|
+
throw new Error(`GRAPH-EDGE-TUPLE-UNIQUE at edges[${index}]: duplicate canonical edge ${tuple}`);
|
|
98
|
+
}
|
|
99
|
+
tuples.add(tuple);
|
|
100
|
+
const from = entities.get(edge.fromId);
|
|
101
|
+
const to = entities.get(edge.toId);
|
|
102
|
+
if (!from || !to) {
|
|
103
|
+
throw new Error(`GRAPH-EDGE-ENDPOINT-EXISTS at edges[${index}]: missing endpoint for ${tuple}`);
|
|
104
|
+
}
|
|
105
|
+
const expected = getCanonicalRelation(edge.relation);
|
|
106
|
+
if (from.type !== expected.from || to.type !== expected.to) {
|
|
107
|
+
throw new Error(`GRAPH-EDGE-ENDPOINT-TYPE at edges[${index}]: ${edge.relation} requires ${expected.from} -> ${expected.to}`);
|
|
108
|
+
}
|
|
109
|
+
if (edge.id !== canonicalEdgeId(edge)) {
|
|
110
|
+
throw new Error(`GRAPH-EDGE-ID-STABLE at edges[${index}].id: expected ${canonicalEdgeId(edge)}`);
|
|
111
|
+
}
|
|
112
|
+
assertInterval(edge.validFrom, edge.validTo, `edges[${index}]`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function isActiveAt(record, asOf) {
|
|
116
|
+
const instant = parseRfc3339(asOf, 'asOf');
|
|
117
|
+
const from = record.validFrom === undefined ? undefined : parseRfc3339(record.validFrom, 'validFrom');
|
|
118
|
+
const to = record.validTo === undefined ? undefined : parseRfc3339(record.validTo, 'validTo');
|
|
119
|
+
return (from === undefined || from <= instant) && (to === undefined || instant < to);
|
|
120
|
+
}
|
|
121
|
+
function assertInterval(validFrom, validTo, path) {
|
|
122
|
+
const from = validFrom === undefined ? undefined : parseRfc3339(validFrom, `${path}.validFrom`);
|
|
123
|
+
const to = validTo === undefined ? undefined : parseRfc3339(validTo, `${path}.validTo`);
|
|
124
|
+
if (from !== undefined && to !== undefined && from > to) {
|
|
125
|
+
throw new Error(`GRAPH-VALIDITY-ORDER at ${path}: validFrom must not be after validTo`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function parseRfc3339(value, path) {
|
|
129
|
+
if (typeof value !== 'string') {
|
|
130
|
+
throw new Error(`GRAPH-VALIDITY-DATETIME at ${path}: validity must use RFC 3339 date-time values`);
|
|
131
|
+
}
|
|
132
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):(\d{2}))$/u.exec(value);
|
|
133
|
+
if (!match) {
|
|
134
|
+
throw new Error(`GRAPH-VALIDITY-DATETIME at ${path}: validity must use RFC 3339 date-time values`);
|
|
135
|
+
}
|
|
136
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText, offsetHourText, offsetMinuteText] = match;
|
|
137
|
+
const year = Number(yearText);
|
|
138
|
+
const month = Number(monthText);
|
|
139
|
+
const day = Number(dayText);
|
|
140
|
+
const hour = Number(hourText);
|
|
141
|
+
const minute = Number(minuteText);
|
|
142
|
+
const second = Number(secondText);
|
|
143
|
+
const offsetHour = Number(offsetHourText ?? 0);
|
|
144
|
+
const offsetMinute = Number(offsetMinuteText ?? 0);
|
|
145
|
+
const daysInMonth = month >= 1 && month <= 12 ? new Date(Date.UTC(year, month, 0)).getUTCDate() : 0;
|
|
146
|
+
if (day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59 || offsetHour > 23 || offsetMinute > 59) {
|
|
147
|
+
throw new Error(`GRAPH-VALIDITY-DATETIME at ${path}: validity must use a real RFC 3339 date-time value`);
|
|
148
|
+
}
|
|
149
|
+
const parsed = Date.parse(value);
|
|
150
|
+
if (!Number.isFinite(parsed)) {
|
|
151
|
+
throw new Error(`GRAPH-VALIDITY-DATETIME at ${path}: validity must use RFC 3339 date-time values`);
|
|
152
|
+
}
|
|
153
|
+
return parsed;
|
|
154
|
+
}
|
|
155
|
+
function assertNonEmptyString(value, path) {
|
|
156
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
157
|
+
throw new Error(`GRAPH-NONEMPTY-STRING at ${path}: value must be a non-empty string`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function assertOptionalString(value, path) {
|
|
161
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
162
|
+
throw new Error(`GRAPH-OPTIONAL-STRING at ${path}: value must be a string when present`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function assertOptionalStringArray(value, path) {
|
|
166
|
+
if (value !== undefined && (!Array.isArray(value) || value.some((item) => typeof item !== 'string'))) {
|
|
167
|
+
throw new Error(`GRAPH-STRING-ARRAY at ${path}: value must be an array of strings when present`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function assertOptionalRecord(value, path) {
|
|
171
|
+
if (value !== undefined && (!value || typeof value !== 'object' || Array.isArray(value))) {
|
|
172
|
+
throw new Error(`GRAPH-OBJECT at ${path}: value must be an object when present`);
|
|
173
|
+
}
|
|
174
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,8 @@ import { constants, realpathSync } from 'node:fs';
|
|
|
3
3
|
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { delimiter, dirname, isAbsolute, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { initializePersonalOs, loadPersonalOs, mutatePersonalOs } from './ssot.js';
|
|
6
|
+
import { initializePersonalOs, loadPersonalOs, migrateCanonicalGraph, mutatePersonalOs } from './ssot.js';
|
|
7
|
+
import { diagnoseGraph } from './graph-diagnosis.js';
|
|
7
8
|
import { resolveDataDir } from './paths.js';
|
|
8
9
|
import { auditPersonalOsDirectory } from './ontology-ssot.js';
|
|
9
10
|
import { portableOntology, resolveOntologyVersion } from './ontology.js';
|
|
@@ -15,6 +16,7 @@ import { assertPublicSafeSkillBundle, buildSkillBundle, parseSkillIds, parseSkil
|
|
|
15
16
|
import { buildProjectRegistrationPlan, parseProjectSource, parseProjectStakeholder, renderProjectRegistrationMarkdown } from './projects.js';
|
|
16
17
|
import { renderGuidedFirstRun } from './guided-onboarding.js';
|
|
17
18
|
import { blockedJudgmentOutput, processJudgmentHook } from './judgment-host.js';
|
|
19
|
+
import { applyCanonicalWrites, buildCanonicalEdge } from './canonical-edge-builder.js';
|
|
18
20
|
export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
19
21
|
const parsed = parseArgs(argv);
|
|
20
22
|
try {
|
|
@@ -60,6 +62,8 @@ export async function runCli(argv = process.argv.slice(2), io = process) {
|
|
|
60
62
|
return 0;
|
|
61
63
|
case 'ontology:audit':
|
|
62
64
|
return await ontologyAudit(parsed, io);
|
|
65
|
+
case 'ontology:migrate':
|
|
66
|
+
return await ontologyMigrate(parsed, io);
|
|
63
67
|
case 'judgment:hook':
|
|
64
68
|
return await judgmentHook(io);
|
|
65
69
|
case 'judgment:install':
|
|
@@ -166,10 +170,9 @@ async function onboardSeed(parsed, io) {
|
|
|
166
170
|
const personalEntries = [...os.personalKg];
|
|
167
171
|
const decisions = [...os.decisions];
|
|
168
172
|
const relationships = [...os.relationships.relationships];
|
|
169
|
-
const
|
|
173
|
+
const canonicalEntities = [];
|
|
170
174
|
if (name) {
|
|
171
|
-
|
|
172
|
-
upsertGraphEntity(graphEntities, {
|
|
175
|
+
canonicalEntities.push({
|
|
173
176
|
id: 'self',
|
|
174
177
|
type: 'person',
|
|
175
178
|
name,
|
|
@@ -194,7 +197,7 @@ async function onboardSeed(parsed, io) {
|
|
|
194
197
|
});
|
|
195
198
|
}
|
|
196
199
|
for (const project of parsed.values.get('project') ?? []) {
|
|
197
|
-
|
|
200
|
+
canonicalEntities.push({
|
|
198
201
|
id: `project-${hash(project)}`,
|
|
199
202
|
type: 'project',
|
|
200
203
|
name: project,
|
|
@@ -210,12 +213,20 @@ async function onboardSeed(parsed, io) {
|
|
|
210
213
|
});
|
|
211
214
|
}
|
|
212
215
|
for (const value of parsed.values.get('decision-principle') ?? []) {
|
|
213
|
-
|
|
216
|
+
const decision = {
|
|
214
217
|
id: `decision-${hash(value)}`,
|
|
215
218
|
title: 'オンボーディングで登録した判断基準',
|
|
216
219
|
decision: value,
|
|
217
220
|
tags: ['principle', 'onboarding'],
|
|
218
221
|
updatedAt: now
|
|
222
|
+
};
|
|
223
|
+
upsertById(decisions, decision);
|
|
224
|
+
canonicalEntities.push({
|
|
225
|
+
id: decision.id,
|
|
226
|
+
type: 'decision',
|
|
227
|
+
name: decision.title,
|
|
228
|
+
summary: decision.decision,
|
|
229
|
+
tags: decision.tags
|
|
219
230
|
});
|
|
220
231
|
}
|
|
221
232
|
for (const encoded of encodedRelationships) {
|
|
@@ -228,7 +239,7 @@ async function onboardSeed(parsed, io) {
|
|
|
228
239
|
tags: ['relationship'],
|
|
229
240
|
updatedAt: now
|
|
230
241
|
});
|
|
231
|
-
|
|
242
|
+
canonicalEntities.push({
|
|
232
243
|
id: `person-${hash(person)}`,
|
|
233
244
|
type: 'person',
|
|
234
245
|
name: person,
|
|
@@ -236,8 +247,53 @@ async function onboardSeed(parsed, io) {
|
|
|
236
247
|
tags: ['relationship']
|
|
237
248
|
});
|
|
238
249
|
}
|
|
250
|
+
const projects = (parsed.values.get('project') ?? []).map((project) => ({
|
|
251
|
+
id: `project-${hash(project)}`,
|
|
252
|
+
name: project
|
|
253
|
+
}));
|
|
254
|
+
const people = encodedRelationships.map((encoded) => {
|
|
255
|
+
const [person, role, context] = encoded.split('|').map((part) => part.trim());
|
|
256
|
+
return {
|
|
257
|
+
id: `person-${hash(person)}`,
|
|
258
|
+
relationshipId: `relationship-${hash(encoded)}`,
|
|
259
|
+
role: role || undefined,
|
|
260
|
+
context
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
const seedDecisions = (parsed.values.get('decision-principle') ?? []).map((decision) => ({
|
|
264
|
+
id: `decision-${hash(decision)}`,
|
|
265
|
+
decision
|
|
266
|
+
}));
|
|
267
|
+
const canonicalEdges = projects.flatMap((project) => [
|
|
268
|
+
...(name ? [buildCanonicalEdge({
|
|
269
|
+
fromId: 'self',
|
|
270
|
+
relation: 'participates_in',
|
|
271
|
+
toId: project.id,
|
|
272
|
+
context: 'Registered together during onboarding.',
|
|
273
|
+
provenance: { sourceKind: 'onboarding', sourceId: project.id }
|
|
274
|
+
})] : []),
|
|
275
|
+
...people.map((person) => buildCanonicalEdge({
|
|
276
|
+
fromId: person.id,
|
|
277
|
+
relation: 'participates_in',
|
|
278
|
+
toId: project.id,
|
|
279
|
+
role: person.role,
|
|
280
|
+
context: person.context,
|
|
281
|
+
provenance: { sourceKind: 'onboarding', sourceId: person.relationshipId }
|
|
282
|
+
})),
|
|
283
|
+
...seedDecisions.map((decision) => buildCanonicalEdge({
|
|
284
|
+
fromId: decision.id,
|
|
285
|
+
relation: 'governs',
|
|
286
|
+
toId: project.id,
|
|
287
|
+
context: decision.decision,
|
|
288
|
+
provenance: { sourceKind: 'onboarding', sourceId: decision.id }
|
|
289
|
+
}))
|
|
290
|
+
]);
|
|
291
|
+
const graph = applyCanonicalWrites(os.graph, { entities: canonicalEntities, edges: canonicalEdges });
|
|
239
292
|
return proposedPersonalOs(os, {
|
|
240
|
-
graph: {
|
|
293
|
+
graph: {
|
|
294
|
+
...graph,
|
|
295
|
+
owner: { ...graph.owner, ...(name ? { id: 'self', name } : {}) }
|
|
296
|
+
},
|
|
241
297
|
relationships: { version: 1, relationships },
|
|
242
298
|
personalKg: personalEntries,
|
|
243
299
|
decisions
|
|
@@ -257,7 +313,7 @@ async function onboardSeed(parsed, io) {
|
|
|
257
313
|
'',
|
|
258
314
|
'次に実行:',
|
|
259
315
|
`brainbase onboard:install --target codex --dir ${shellArg(dataDir)} --dry-run`,
|
|
260
|
-
'設定を承認・反映してエージェントを再起動した後、実際の依頼でBrainbaseのget_context
|
|
316
|
+
'設定を承認・反映してエージェントを再起動した後、実際の依頼でBrainbaseのresolve_entity、get_context、searchを使います。',
|
|
261
317
|
''
|
|
262
318
|
];
|
|
263
319
|
write(io, summary.join('\n'));
|
|
@@ -464,21 +520,27 @@ async function onboardProjects(parsed, io) {
|
|
|
464
520
|
}
|
|
465
521
|
async function applyProjectRegistrationPlan(dataDir, plan) {
|
|
466
522
|
await mutatePersonalOs(dataDir, (os) => {
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
523
|
+
const graph = applyCanonicalWrites(os.graph, {
|
|
524
|
+
entities: plan.writes.canonicalEntities,
|
|
525
|
+
edges: plan.writes.canonicalEdges
|
|
526
|
+
});
|
|
471
527
|
const relationships = [...os.relationships.relationships];
|
|
472
528
|
for (const relationship of plan.writes.relationships) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
529
|
+
upsertById(relationships, relationship);
|
|
530
|
+
}
|
|
531
|
+
const personalKg = [...os.personalKg];
|
|
532
|
+
for (const entry of plan.writes.personalKg) {
|
|
533
|
+
upsertById(personalKg, entry);
|
|
534
|
+
}
|
|
535
|
+
const decisions = [...os.decisions];
|
|
536
|
+
for (const decision of plan.writes.decisions) {
|
|
537
|
+
upsertById(decisions, decision);
|
|
476
538
|
}
|
|
477
539
|
return proposedPersonalOs(os, {
|
|
478
|
-
graph
|
|
540
|
+
graph,
|
|
479
541
|
relationships: { version: 1, relationships },
|
|
480
|
-
personalKg
|
|
481
|
-
decisions
|
|
542
|
+
personalKg,
|
|
543
|
+
decisions
|
|
482
544
|
});
|
|
483
545
|
});
|
|
484
546
|
}
|
|
@@ -558,25 +620,40 @@ async function onboardApply(parsed, io) {
|
|
|
558
620
|
let result;
|
|
559
621
|
if (willWrite) {
|
|
560
622
|
await mutatePersonalOs(dataDir, (os) => {
|
|
623
|
+
if (os.graph.version !== 2) {
|
|
624
|
+
throw new Error('migration_required: Graph v1 cannot store canonical ID edges; migrate graph.json to Graph v2 before writing');
|
|
625
|
+
}
|
|
561
626
|
result = planApply(candidates, { ids: selectedIds, all }, {
|
|
562
627
|
graphEntities: [...os.graph.entities],
|
|
628
|
+
graphEdges: [...os.graph.edges],
|
|
563
629
|
relationships: [...os.relationships.relationships],
|
|
564
630
|
personalKg: os.personalKg,
|
|
565
631
|
decisions: os.decisions,
|
|
566
632
|
ownerName: os.graph.owner?.name
|
|
567
633
|
}, now);
|
|
634
|
+
const personalKg = [...os.personalKg];
|
|
635
|
+
for (const entry of result.personalKgAdditions)
|
|
636
|
+
upsertById(personalKg, entry);
|
|
637
|
+
const decisions = [...os.decisions];
|
|
638
|
+
for (const decision of result.decisionAdditions)
|
|
639
|
+
upsertById(decisions, decision);
|
|
640
|
+
const graph = applyCanonicalWrites(os.graph, result.canonicalWrites);
|
|
568
641
|
return proposedPersonalOs(os, {
|
|
569
|
-
graph: { ...
|
|
642
|
+
graph: { ...graph, owner: result.ownerName ? { ...graph.owner, name: result.ownerName } : graph.owner },
|
|
570
643
|
relationships: { version: 1, relationships: result.relationships },
|
|
571
|
-
personalKg
|
|
572
|
-
decisions
|
|
644
|
+
personalKg,
|
|
645
|
+
decisions
|
|
573
646
|
});
|
|
574
647
|
});
|
|
575
648
|
}
|
|
576
649
|
else {
|
|
577
650
|
const os = await loadPersonalOs(dataDir);
|
|
651
|
+
if (os.graph.version !== 2) {
|
|
652
|
+
throw new Error('migration_required: Graph v1 cannot store canonical ID edges; migrate graph.json to Graph v2 before writing');
|
|
653
|
+
}
|
|
578
654
|
result = planApply(candidates, { ids: selectedIds, all }, {
|
|
579
655
|
graphEntities: [...os.graph.entities],
|
|
656
|
+
graphEdges: [...os.graph.edges],
|
|
580
657
|
relationships: [...os.relationships.relationships],
|
|
581
658
|
personalKg: os.personalKg,
|
|
582
659
|
decisions: os.decisions,
|
|
@@ -819,12 +896,23 @@ async function writeConfigSnippet(outputPath, payload) {
|
|
|
819
896
|
}
|
|
820
897
|
async function doctor(parsed, io) {
|
|
821
898
|
const dataDir = resolveDataDir(first(parsed, 'dir'));
|
|
822
|
-
const
|
|
823
|
-
|
|
899
|
+
const graphDiagnosis = await diagnoseGraph(dataDir);
|
|
900
|
+
let status;
|
|
901
|
+
try {
|
|
902
|
+
status = onboardingStatus(await loadPersonalOs(dataDir));
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
write(io, `${JSON.stringify({
|
|
906
|
+
graphDiagnosis,
|
|
907
|
+
localBackend: { connected: false, backend: 'local' },
|
|
908
|
+
issue: error instanceof Error ? error.message : String(error)
|
|
909
|
+
}, null, 2)}\n`);
|
|
910
|
+
return graphDiagnosisExitCode(graphDiagnosis.status) || 1;
|
|
911
|
+
}
|
|
824
912
|
const judgmentHooksPath = first(parsed, 'judgment-hooks');
|
|
825
913
|
if (!judgmentHooksPath) {
|
|
826
|
-
write(io, `${JSON.stringify(status, null, 2)}\n`);
|
|
827
|
-
return
|
|
914
|
+
write(io, `${JSON.stringify({ ...status, graphDiagnosis }, null, 2)}\n`);
|
|
915
|
+
return graphDiagnosisExitCode(graphDiagnosis.status);
|
|
828
916
|
}
|
|
829
917
|
const config = JSON.parse(await readFile(judgmentHooksPath, 'utf8'));
|
|
830
918
|
const hooks = config.hooks;
|
|
@@ -841,9 +929,13 @@ async function doctor(parsed, io) {
|
|
|
841
929
|
throw new Error('judgment_hooks_invalid');
|
|
842
930
|
write(io, `${JSON.stringify({
|
|
843
931
|
...status,
|
|
932
|
+
graphDiagnosis,
|
|
844
933
|
judgment_hooks: { status: 'ready', events: requiredEvents, source: judgmentHooksPath }
|
|
845
934
|
}, null, 2)}\n`);
|
|
846
|
-
return
|
|
935
|
+
return graphDiagnosisExitCode(graphDiagnosis.status);
|
|
936
|
+
}
|
|
937
|
+
function graphDiagnosisExitCode(status) {
|
|
938
|
+
return status === 'invalid' || status === 'unavailable' || status === 'migration_required' ? 1 : 0;
|
|
847
939
|
}
|
|
848
940
|
function parseArgs(argv) {
|
|
849
941
|
const [firstToken, ...remaining] = argv;
|
|
@@ -905,9 +997,6 @@ function upsertById(entries, entry) {
|
|
|
905
997
|
entries.push(entry);
|
|
906
998
|
}
|
|
907
999
|
}
|
|
908
|
-
function upsertGraphEntity(entities, entity) {
|
|
909
|
-
upsertById(entities, entity);
|
|
910
|
-
}
|
|
911
1000
|
function hash(value) {
|
|
912
1001
|
let hashValue = 0;
|
|
913
1002
|
for (const char of value) {
|
|
@@ -923,7 +1012,10 @@ function writeError(io, text) {
|
|
|
923
1012
|
}
|
|
924
1013
|
async function ontologyAudit(parsed, io) {
|
|
925
1014
|
const dataDir = resolveDataDir(first(parsed, 'dir'));
|
|
926
|
-
const
|
|
1015
|
+
const requestedVersion = first(parsed, 'ontology-version');
|
|
1016
|
+
const ontologyVersion = requestedVersion === undefined
|
|
1017
|
+
? undefined
|
|
1018
|
+
: resolveOntologyVersion(requestedVersion);
|
|
927
1019
|
const result = await auditPersonalOsDirectory(dataDir, { ontologyVersion });
|
|
928
1020
|
write(io, `${JSON.stringify(result, null, 2)}\n`);
|
|
929
1021
|
if (result.status === 'unverified') {
|
|
@@ -931,6 +1023,15 @@ async function ontologyAudit(parsed, io) {
|
|
|
931
1023
|
}
|
|
932
1024
|
return result.violations.some((violation) => violation.severity === 'error') ? 1 : 0;
|
|
933
1025
|
}
|
|
1026
|
+
async function ontologyMigrate(parsed, io) {
|
|
1027
|
+
const dataDir = resolveDataDir(first(parsed, 'dir'));
|
|
1028
|
+
const result = await migrateCanonicalGraph(dataDir, {
|
|
1029
|
+
write: parsed.flags.has('write'),
|
|
1030
|
+
expectedInputDigest: first(parsed, 'expected-input-digest')
|
|
1031
|
+
});
|
|
1032
|
+
write(io, `${JSON.stringify(result, null, 2)}\n`);
|
|
1033
|
+
return result.status === 'blocked' ? 1 : 0;
|
|
1034
|
+
}
|
|
934
1035
|
function proposedPersonalOs(os, proposed) {
|
|
935
1036
|
return { ...os, ...proposed };
|
|
936
1037
|
}
|
|
@@ -962,7 +1063,8 @@ function usage() {
|
|
|
962
1063
|
brainbase onboard:routines --target codex|claude [--routines ohayo,oyasumi,retro] [--ohayo-hour n] [--oyasumi-hour n] [--retro-dow MON-SUN] [--retro-hour n] [--cwd path] [--out path] [--format markdown|json]
|
|
963
1064
|
brainbase onboard:skills --target codex|claude|portable [--skills id,id] [--out dir] [--format markdown|json]
|
|
964
1065
|
brainbase ontology:show
|
|
965
|
-
brainbase ontology:audit [--dir path] [--ontology-version 0.0.0|1.0.0]
|
|
1066
|
+
brainbase ontology:audit [--dir path] [--ontology-version 0.0.0|1.0.0|2.0.0]
|
|
1067
|
+
brainbase ontology:migrate [--dir path] [--write --expected-input-digest digest]
|
|
966
1068
|
brainbase judgment:install --target codex [--dry-run] [--output path]
|
|
967
1069
|
brainbase judgment:hook
|
|
968
1070
|
brainbase doctor [--dir path] [--judgment-hooks path]
|