@jstn-sdk/ma 0.1.12 → 0.1.13
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/.agents/plugins/marketplace.json +0 -0
- package/.codex/agents/Architect.toml +0 -2
- package/.codex/agents/Auditor.toml +0 -2
- package/.codex/agents/Builder.toml +0 -2
- package/.codex/agents/Flow.toml +0 -2
- package/.codex/agents/Sage.toml +0 -2
- package/.codex/agents/Vibe.toml +0 -2
- package/.codex/hooks.json +14 -13
- package/.codex/prompts/enforcement.md +4 -0
- package/.codex/prompts/skill-contract.md +4 -0
- package/COVERAGE.md +211 -0
- package/DEMO.md +265 -0
- package/README.md +159 -16
- package/bin/ma.js +219 -14
- package/data/clone-data.ledger.json +41 -0
- package/data/clone-data.proof.json +50 -0
- package/data/clone-data.rvf +37 -0
- package/docs/README.md +4 -1
- package/docs/assets/image/Screenshot(1).png +0 -0
- package/docs/assets/image/Screenshot(2).png +0 -0
- package/docs/assets/image/Screenshot(3).png +0 -0
- package/docs/assets/image/Screenshot(4).png +0 -0
- package/docs/assets/image/Screenshot(5).png +0 -0
- package/docs/assets/image/Screenshot(6).png +0 -0
- package/docs/assets/image/Screenshot(7).png +0 -0
- package/docs/assets/image/Screenshot(8).png +0 -0
- package/docs/assets/image/Screenshot(9).png +0 -0
- package/docs/assets/meta-architect-logo.png +0 -0
- package/docs/assets/meta-architect-logo.svg +0 -0
- package/docs/getting-started.md +38 -22
- package/docs/installed-sdk.md +0 -0
- package/docs/mcp-setup.md +35 -0
- package/docs/onboarding.md +18 -2
- package/docs/prompt-guidance-contract.md +17 -0
- package/docs/prompt-guidance-fragments/active-autonomy-core.md +7 -0
- package/docs/qa/release-issue-gates-0.1.13.json +644 -0
- package/docs/qa/{release-readiness-0.1.12.md → release-readiness-0.1.13.md} +25 -9
- package/docs/qa/release-readiness-0.1.5.md +1 -1
- package/docs/reference/native-engineering-patterns.md +0 -0
- package/docs/reference/native-security-playbooks.md +6 -0
- package/docs/reference/native-source-selection.md +6 -0
- package/docs/reference/native-style-and-deslop.md +0 -0
- package/docs/release-spec.md +25 -9
- package/docs/skills.md +14 -1
- package/index.js +222 -2
- package/mcp/collections.json +23 -6
- package/mcp/local/code-intel.js +0 -0
- package/mcp/local/memory.js +0 -0
- package/mcp/local/playbooks.js +0 -0
- package/mcp/local/state.js +0 -0
- package/mcp/local/team-run.js +48 -1
- package/mcp/local/trace.js +0 -0
- package/mcp/local-capabilities.json +0 -0
- package/mcp/native-playbooks.json +0 -0
- package/mcp/servers.json +24 -0
- package/package.json +5 -2
- package/plugins/meta-architect/.app.json +1 -1
- package/plugins/meta-architect/.codex-plugin/plugin.json +1 -1
- package/plugins/meta-architect/.mcp.json +1 -1
- package/plugins/meta-architect/README.md +8 -1
- package/plugins/meta-architect/obsidian/main.js +401 -0
- package/plugins/meta-architect/obsidian/manifest.json +9 -0
- package/plugins/meta-architect/obsidian/styles.css +7 -0
- package/plugins/meta-architect/skills/align/SKILL.md +0 -0
- package/plugins/meta-architect/skills/align/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/align/references/shared-language.md +0 -0
- package/plugins/meta-architect/skills/arch/SKILL.md +2 -0
- package/plugins/meta-architect/skills/arch/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/build/SKILL.md +3 -0
- package/plugins/meta-architect/skills/build/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/SKILL.md +0 -0
- package/plugins/meta-architect/skills/cleanup/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/cleanup/references/style-and-deslop.md +0 -0
- package/plugins/meta-architect/skills/diagnose/SKILL.md +0 -0
- package/plugins/meta-architect/skills/diagnose/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/flow/SKILL.md +2 -0
- package/plugins/meta-architect/skills/flow/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/SKILL.md +0 -0
- package/plugins/meta-architect/skills/maestro/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/maestro/references/core-release-rules.md +0 -0
- package/plugins/meta-architect/skills/maestro/references/native-ingest-map.md +0 -0
- package/plugins/meta-architect/skills/sage/SKILL.md +2 -0
- package/plugins/meta-architect/skills/sage/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/sage/references/source-selection.md +8 -0
- package/plugins/meta-architect/skills/tdd/SKILL.md +0 -0
- package/plugins/meta-architect/skills/tdd/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/SKILL.md +2 -0
- package/plugins/meta-architect/skills/vet/agents/openai.yaml +0 -0
- package/plugins/meta-architect/skills/vet/references/security-playbooks.md +7 -0
- package/plugins/meta-architect/skills/vibe/SKILL.md +2 -0
- package/plugins/meta-architect/skills/vibe/agents/openai.yaml +0 -0
- package/scripts/active-autonomy-hook.mjs +96 -0
- package/scripts/biome-staged.js +0 -0
- package/scripts/build-linux-packages.mjs +0 -0
- package/scripts/doctor.js +17 -4
- package/scripts/install.sh +28 -0
- package/scripts/linux-package-lib.mjs +0 -0
- package/scripts/linux-package-smoke.mjs +0 -0
- package/scripts/plugin-sync.js +0 -0
- package/scripts/postinstall.js +0 -0
- package/scripts/ralph/prompt.md +35 -0
- package/scripts/release-metadata.js +0 -0
- package/scripts/release-sync.js +0 -0
- package/scripts/release-verify.js +166 -1
- package/scripts/setup-npmrc.js +0 -0
- package/skills/align/SKILL.md +0 -0
- package/skills/align/agents/openai.yaml +0 -0
- package/skills/align/references/shared-language.md +0 -0
- package/skills/arch/SKILL.md +2 -0
- package/skills/arch/agents/openai.yaml +0 -0
- package/skills/build/SKILL.md +3 -0
- package/skills/build/agents/openai.yaml +0 -0
- package/skills/cleanup/SKILL.md +0 -0
- package/skills/cleanup/agents/openai.yaml +0 -0
- package/skills/cleanup/references/style-and-deslop.md +0 -0
- package/skills/diagnose/SKILL.md +0 -0
- package/skills/diagnose/agents/openai.yaml +0 -0
- package/skills/flow/SKILL.md +2 -0
- package/skills/flow/agents/openai.yaml +0 -0
- package/skills/maestro/SKILL.md +0 -0
- package/skills/maestro/agents/openai.yaml +0 -0
- package/skills/maestro/references/core-release-rules.md +0 -0
- package/skills/maestro/references/native-ingest-map.md +0 -0
- package/skills/sage/SKILL.md +2 -0
- package/skills/sage/agents/openai.yaml +0 -0
- package/skills/sage/references/source-selection.md +8 -0
- package/skills/tdd/SKILL.md +0 -0
- package/skills/tdd/agents/openai.yaml +0 -0
- package/skills/vet/SKILL.md +2 -0
- package/skills/vet/agents/openai.yaml +0 -0
- package/skills/vet/references/security-playbooks.md +7 -0
- package/skills/vibe/SKILL.md +2 -0
- package/skills/vibe/agents/openai.yaml +0 -0
- package/src/bootstrap.js +17 -0
- package/src/build-gate.js +2 -2
- package/src/launcher.js +4 -0
- package/src/mcp-live-client.js +289 -3
- package/src/paths.js +4 -0
- package/src/policy.js +1 -1
- package/src/release-issue-gates.js +190 -0
- package/src/runtime/active-autonomy-core.js +208 -0
- package/src/runtime/alignment-sentinel.js +165 -0
- package/src/runtime/architect-review.js +88 -0
- package/src/runtime/build-readiness.js +28 -4
- package/src/runtime/code-graph-rehearse.js +113 -0
- package/src/runtime/context-economy-core.js +276 -0
- package/src/runtime/continuity-notes.js +0 -0
- package/src/runtime/core-source-ingest.js +379 -0
- package/src/runtime/detached-provider.js +74 -0
- package/src/runtime/environment-awareness-core.js +460 -0
- package/src/runtime/exposure-catalog.js +276 -0
- package/src/runtime/guidance-stack.js +0 -0
- package/src/runtime/helper-orchestration-core.js +307 -0
- package/src/runtime/learning-loop-core.js +238 -0
- package/src/runtime/maestro-events.js +18 -0
- package/src/runtime/maestro-manager.js +19 -0
- package/src/runtime/maestro-state.js +125 -0
- package/src/runtime/mcp-policy.js +192 -0
- package/src/runtime/obsidian-integration-core.js +851 -0
- package/src/runtime/obsidian-plugin-bridge.js +739 -0
- package/src/runtime/orchestrator.js +0 -0
- package/src/runtime/prompt-strategy-core.js +230 -0
- package/src/runtime/quorum-review.js +90 -0
- package/src/runtime/ralph-execution-core.js +217 -0
- package/src/runtime/redaction-gateway.js +174 -0
- package/src/runtime/runtime-state.js +843 -2
- package/src/runtime/semantic-recording-core.js +147 -0
- package/src/runtime/signal-hooks.js +15 -0
- package/src/runtime/skills-registry-export.js +670 -0
- package/src/runtime/startup-path.js +0 -0
- package/src/runtime/universal-plugin-broker-core.js +575 -0
- package/src/runtime/workspace-intelligence-runtime.js +674 -0
- package/src/runtime/workspace-virtualizer.js +102 -0
- package/src/runtime/workspaces.js +0 -0
- package/src/runtime-artifacts.js +225 -62
- package/src/skill-installer.js +0 -0
- package/src/skills.js +876 -84
|
@@ -0,0 +1,851 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { ensureDir, readJson, writeFileIfMissing, writeJson } from "../fs-utils.js";
|
|
5
|
+
import { getRuntimeSubsystemPath } from "../paths.js";
|
|
6
|
+
import { createDefaultObsidianPluginBridgeManifest } from "./obsidian-plugin-bridge.js";
|
|
7
|
+
|
|
8
|
+
export const obsidianIntegrationSchemaVersion = "0.1.0";
|
|
9
|
+
export const obsidianGraphMapNotePath = "Meta-Architect/Map of Content.md";
|
|
10
|
+
const obsidianGraphSectionHeading = "## Obsidian Graph Links";
|
|
11
|
+
|
|
12
|
+
export function getObsidianBridgePath() {
|
|
13
|
+
return getRuntimeSubsystemPath("context", "obsidian-bridge.json");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getObsidianVaultIndexPath() {
|
|
17
|
+
return getRuntimeSubsystemPath("context", "obsidian-vault-index.json");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getObsidianVaultOperationsPath() {
|
|
21
|
+
return getRuntimeSubsystemPath("context", "obsidian-vault-operations.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createDefaultObsidianBridge() {
|
|
25
|
+
return {
|
|
26
|
+
schemaVersion: obsidianIntegrationSchemaVersion,
|
|
27
|
+
product: "Meta-Architect",
|
|
28
|
+
purpose:
|
|
29
|
+
"Defines the default MA-owned Obsidian bridge for vault context, plugin requests, and note-selection metadata.",
|
|
30
|
+
compatibility_references: {
|
|
31
|
+
api_docs: "https://gitmcp.io/obsidianmd/obsidian-api",
|
|
32
|
+
sample_plugin_structure: "https://github.com/obsidianmd/obsidian-sample-plugin",
|
|
33
|
+
},
|
|
34
|
+
semantic_boundary: {
|
|
35
|
+
semantic_role: "brain_context",
|
|
36
|
+
records_as: "vault_context",
|
|
37
|
+
never_records_as: "build_evidence",
|
|
38
|
+
},
|
|
39
|
+
graph_link_policy: {
|
|
40
|
+
enabled_by_default: true,
|
|
41
|
+
semantic_symlink_type: "obsidian_wikilink",
|
|
42
|
+
map_note_path: obsidianGraphMapNotePath,
|
|
43
|
+
every_ma_note_links_to_map: true,
|
|
44
|
+
map_links_all_ma_notes: true,
|
|
45
|
+
records_as: "vault_context",
|
|
46
|
+
build_evidence: false,
|
|
47
|
+
},
|
|
48
|
+
plugin_contract: {
|
|
49
|
+
may_queue_requests: true,
|
|
50
|
+
may_expose_note_selection_metadata: true,
|
|
51
|
+
allowed_vault_operations: [
|
|
52
|
+
"create_note",
|
|
53
|
+
"read_note",
|
|
54
|
+
"update_note",
|
|
55
|
+
"delete_note",
|
|
56
|
+
"list_notes",
|
|
57
|
+
],
|
|
58
|
+
must_not_mutate: [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"],
|
|
59
|
+
authoritative_changes_return_through: "$maestro_or_owning_lane",
|
|
60
|
+
},
|
|
61
|
+
plugin_runtime: createDefaultObsidianPluginBridgeManifest(),
|
|
62
|
+
default_request_queue: [],
|
|
63
|
+
note_selection_metadata: [],
|
|
64
|
+
tag_graph_claims: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createDefaultObsidianVaultIndex() {
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: obsidianIntegrationSchemaVersion,
|
|
71
|
+
record_type: "obsidian_vault_index",
|
|
72
|
+
records_as: "vault_context",
|
|
73
|
+
build_evidence: false,
|
|
74
|
+
read_only_snapshot: true,
|
|
75
|
+
vault_path: null,
|
|
76
|
+
indexed_at: null,
|
|
77
|
+
note_count: 0,
|
|
78
|
+
total_words: 0,
|
|
79
|
+
total_chars: 0,
|
|
80
|
+
tags: [],
|
|
81
|
+
unresolved_links: [],
|
|
82
|
+
notes: [],
|
|
83
|
+
tag_graph_claims: [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createDefaultObsidianVaultOperations() {
|
|
88
|
+
return {
|
|
89
|
+
schemaVersion: obsidianIntegrationSchemaVersion,
|
|
90
|
+
record_type: "obsidian_vault_operations",
|
|
91
|
+
records_as: "vault_context",
|
|
92
|
+
build_evidence: false,
|
|
93
|
+
authority_boundary: "$maestro_or_owning_lane",
|
|
94
|
+
operations: [],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function createObsidianPluginRequest({
|
|
99
|
+
action,
|
|
100
|
+
notePaths = [],
|
|
101
|
+
tags = [],
|
|
102
|
+
reason = "operator_selected_context",
|
|
103
|
+
}) {
|
|
104
|
+
if (!action || typeof action !== "string") {
|
|
105
|
+
throw new Error("Obsidian plugin request requires an action");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
record_type: "obsidian_plugin_request",
|
|
110
|
+
action,
|
|
111
|
+
reason,
|
|
112
|
+
records_as: "vault_context",
|
|
113
|
+
authoritative_change: false,
|
|
114
|
+
note_paths: notePaths.filter((entry) => typeof entry === "string" && entry.trim()),
|
|
115
|
+
tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
|
|
116
|
+
queued_at: new Date().toISOString(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createObsidianVaultContext({ notePath, title, tags = [], claims = [] }) {
|
|
121
|
+
if (!notePath || typeof notePath !== "string") {
|
|
122
|
+
throw new Error("Obsidian vault context requires notePath");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
record_type: "vault_context",
|
|
127
|
+
note_path: notePath,
|
|
128
|
+
title: title || notePath,
|
|
129
|
+
tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
|
|
130
|
+
claims: claims.map((claim) => ({
|
|
131
|
+
...claim,
|
|
132
|
+
records_as: "vault_context",
|
|
133
|
+
build_evidence: false,
|
|
134
|
+
})),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function createObsidianVaultSnapshotExport({
|
|
139
|
+
artifactPath,
|
|
140
|
+
vaultPath,
|
|
141
|
+
notePath,
|
|
142
|
+
title,
|
|
143
|
+
tags = [],
|
|
144
|
+
provenance = [],
|
|
145
|
+
}) {
|
|
146
|
+
if (!artifactPath || !notePath) {
|
|
147
|
+
throw new Error("Obsidian vault snapshot export requires artifactPath and notePath");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
record_type: "obsidian_vault_snapshot_export",
|
|
152
|
+
records_as: "vault_context",
|
|
153
|
+
build_evidence: false,
|
|
154
|
+
read_only_snapshot: true,
|
|
155
|
+
artifact_path: artifactPath,
|
|
156
|
+
vault_path: vaultPath ?? null,
|
|
157
|
+
note_path: notePath,
|
|
158
|
+
title: title || notePath,
|
|
159
|
+
tags: tags.filter((entry) => typeof entry === "string" && entry.trim()),
|
|
160
|
+
provenance: provenance.filter((entry) => typeof entry === "string" && entry.trim()),
|
|
161
|
+
authoritative_change: false,
|
|
162
|
+
authority_boundary: "$maestro_or_owning_lane",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function createObsidianIntakeContext({ allowedNotes = [], tagGraph = [] }) {
|
|
167
|
+
const notes = allowedNotes.map((note) =>
|
|
168
|
+
createObsidianVaultContext({
|
|
169
|
+
notePath: note.notePath,
|
|
170
|
+
title: note.title,
|
|
171
|
+
tags: note.tags,
|
|
172
|
+
claims: note.claims,
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
record_type: "obsidian_intake_context",
|
|
177
|
+
records_as: "vault_context",
|
|
178
|
+
build_evidence: false,
|
|
179
|
+
allowed_notes: notes,
|
|
180
|
+
tag_graph_claims: tagGraph.map((claim) => ({
|
|
181
|
+
...claim,
|
|
182
|
+
records_as: "vault_context",
|
|
183
|
+
build_evidence: false,
|
|
184
|
+
})),
|
|
185
|
+
requires_operator_allowlist: true,
|
|
186
|
+
authoritative_change: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function indexObsidianVault({ vaultPath, maxExcerptChars = 360 } = {}) {
|
|
191
|
+
if (!vaultPath || typeof vaultPath !== "string") {
|
|
192
|
+
throw new Error("Obsidian vault indexing requires vaultPath");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const resolvedVaultPath = path.resolve(vaultPath);
|
|
196
|
+
const stat = await fs.stat(resolvedVaultPath);
|
|
197
|
+
if (!stat.isDirectory()) {
|
|
198
|
+
throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const notePaths = await listMarkdownNotes(resolvedVaultPath);
|
|
202
|
+
const notes = [];
|
|
203
|
+
const tagCounts = new Map();
|
|
204
|
+
const linkTargets = new Map();
|
|
205
|
+
let totalWords = 0;
|
|
206
|
+
let totalChars = 0;
|
|
207
|
+
|
|
208
|
+
for (const absolutePath of notePaths) {
|
|
209
|
+
const content = await fs.readFile(absolutePath, "utf8");
|
|
210
|
+
const relativePath = normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath));
|
|
211
|
+
const tags = extractObsidianTags(content);
|
|
212
|
+
const links = extractObsidianLinks(content);
|
|
213
|
+
const title = extractTitle(content) ?? path.basename(relativePath, ".md");
|
|
214
|
+
const words = countWords(content);
|
|
215
|
+
totalWords += words;
|
|
216
|
+
totalChars += content.length;
|
|
217
|
+
|
|
218
|
+
for (const tag of tags) {
|
|
219
|
+
tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
|
|
220
|
+
}
|
|
221
|
+
for (const link of links) {
|
|
222
|
+
const targetKey = normalizeLinkKey(link.target);
|
|
223
|
+
const refs = linkTargets.get(targetKey) ?? [];
|
|
224
|
+
refs.push(relativePath);
|
|
225
|
+
linkTargets.set(targetKey, refs);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
notes.push({
|
|
229
|
+
record_type: "vault_context_note",
|
|
230
|
+
records_as: "vault_context",
|
|
231
|
+
build_evidence: false,
|
|
232
|
+
relative_path: relativePath,
|
|
233
|
+
title,
|
|
234
|
+
tags,
|
|
235
|
+
links,
|
|
236
|
+
word_count: words,
|
|
237
|
+
char_count: content.length,
|
|
238
|
+
content_sha256: createHash("sha256").update(content).digest("hex"),
|
|
239
|
+
excerpt: createExcerpt(content, maxExcerptChars),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const existingNoteKeys = new Set([
|
|
244
|
+
...notes.map((note) => normalizeLinkKey(note.title)),
|
|
245
|
+
...notes.map((note) => normalizeLinkKey(note.relative_path)),
|
|
246
|
+
...notes.map((note) => normalizeLinkKey(note.relative_path.replace(/\.md$/i, ""))),
|
|
247
|
+
]);
|
|
248
|
+
const unresolvedLinks = [...linkTargets.entries()]
|
|
249
|
+
.filter(([target]) => !existingNoteKeys.has(target))
|
|
250
|
+
.map(([target, sources]) => ({
|
|
251
|
+
target,
|
|
252
|
+
sources: [...new Set(sources)].sort(),
|
|
253
|
+
records_as: "vault_context",
|
|
254
|
+
build_evidence: false,
|
|
255
|
+
}))
|
|
256
|
+
.sort((a, b) => a.target.localeCompare(b.target));
|
|
257
|
+
const tags = [...tagCounts.entries()]
|
|
258
|
+
.map(([tag, count]) => ({ tag, count }))
|
|
259
|
+
.sort((a, b) => b.count - a.count || a.tag.localeCompare(b.tag));
|
|
260
|
+
const tagGraphClaims = tags.map(({ tag, count }) => ({
|
|
261
|
+
tag,
|
|
262
|
+
claim: `Tag ${tag} appears in ${count} Obsidian note${count === 1 ? "" : "s"}.`,
|
|
263
|
+
records_as: "vault_context",
|
|
264
|
+
build_evidence: false,
|
|
265
|
+
}));
|
|
266
|
+
|
|
267
|
+
return validateObsidianVaultIndex({
|
|
268
|
+
schemaVersion: obsidianIntegrationSchemaVersion,
|
|
269
|
+
record_type: "obsidian_vault_index",
|
|
270
|
+
records_as: "vault_context",
|
|
271
|
+
build_evidence: false,
|
|
272
|
+
read_only_snapshot: true,
|
|
273
|
+
vault_path: resolvedVaultPath,
|
|
274
|
+
indexed_at: new Date().toISOString(),
|
|
275
|
+
note_count: notes.length,
|
|
276
|
+
total_words: totalWords,
|
|
277
|
+
total_chars: totalChars,
|
|
278
|
+
tags,
|
|
279
|
+
unresolved_links: unresolvedLinks,
|
|
280
|
+
notes,
|
|
281
|
+
tag_graph_claims: tagGraphClaims,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function listObsidianNotes({ vaultPath } = {}) {
|
|
286
|
+
const resolvedVaultPath = await resolveVaultRoot(vaultPath);
|
|
287
|
+
const notePaths = await listMarkdownNotes(resolvedVaultPath);
|
|
288
|
+
return {
|
|
289
|
+
record_type: "obsidian_note_list",
|
|
290
|
+
records_as: "vault_context",
|
|
291
|
+
build_evidence: false,
|
|
292
|
+
vault_path: resolvedVaultPath,
|
|
293
|
+
notes: notePaths.map((absolutePath) =>
|
|
294
|
+
normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)),
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function readObsidianNote({ vaultPath, notePath } = {}) {
|
|
300
|
+
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
|
|
301
|
+
const content = await fs.readFile(target.absolutePath, "utf8");
|
|
302
|
+
const stat = await fs.stat(target.absolutePath);
|
|
303
|
+
return {
|
|
304
|
+
record_type: "obsidian_note_read",
|
|
305
|
+
records_as: "vault_context",
|
|
306
|
+
build_evidence: false,
|
|
307
|
+
vault_path: target.vaultPath,
|
|
308
|
+
relative_path: target.relativePath,
|
|
309
|
+
title: extractTitle(content) ?? path.basename(target.relativePath, ".md"),
|
|
310
|
+
content,
|
|
311
|
+
content_sha256: createHash("sha256").update(content).digest("hex"),
|
|
312
|
+
char_count: content.length,
|
|
313
|
+
word_count: countWords(content),
|
|
314
|
+
updated_at: stat.mtime.toISOString(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export async function createObsidianNote({ vaultPath, notePath, content, overwrite = false } = {}) {
|
|
319
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
320
|
+
throw new Error("Obsidian create requires non-empty note content");
|
|
321
|
+
}
|
|
322
|
+
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
|
|
323
|
+
if (!overwrite && (await pathExists(target.absolutePath))) {
|
|
324
|
+
throw new Error(`Obsidian note already exists: ${target.relativePath}`);
|
|
325
|
+
}
|
|
326
|
+
await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
|
|
327
|
+
await fs.writeFile(target.absolutePath, content, "utf8");
|
|
328
|
+
return createObsidianOperationReceipt({
|
|
329
|
+
operation: overwrite ? "update_note" : "create_note",
|
|
330
|
+
target,
|
|
331
|
+
content,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function updateObsidianNote({ vaultPath, notePath, content } = {}) {
|
|
336
|
+
if (typeof content !== "string" || content.trim().length === 0) {
|
|
337
|
+
throw new Error("Obsidian update requires non-empty note content");
|
|
338
|
+
}
|
|
339
|
+
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
|
|
340
|
+
await fs.writeFile(target.absolutePath, content, "utf8");
|
|
341
|
+
return createObsidianOperationReceipt({ operation: "update_note", target, content });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export async function deleteObsidianNote({ vaultPath, notePath } = {}) {
|
|
345
|
+
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: true });
|
|
346
|
+
const content = await fs.readFile(target.absolutePath, "utf8");
|
|
347
|
+
await fs.unlink(target.absolutePath);
|
|
348
|
+
return createObsidianOperationReceipt({
|
|
349
|
+
operation: "delete_note",
|
|
350
|
+
target,
|
|
351
|
+
content,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function ensureObsidianGraphLinks({ vaultPath } = {}) {
|
|
356
|
+
const resolvedVaultPath = await resolveVaultRoot(vaultPath);
|
|
357
|
+
const notePaths = await listMarkdownNotes(resolvedVaultPath);
|
|
358
|
+
const maNotePaths = notePaths
|
|
359
|
+
.map((absolutePath) => normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)))
|
|
360
|
+
.filter((relativePath) => relativePath.startsWith("Meta-Architect/"));
|
|
361
|
+
const mapPath = obsidianGraphMapNotePath;
|
|
362
|
+
const nonMapMaNotes = maNotePaths.filter((relativePath) => relativePath !== mapPath).sort();
|
|
363
|
+
const receipts = [];
|
|
364
|
+
|
|
365
|
+
const mapContent = createObsidianGraphMapContent(nonMapMaNotes);
|
|
366
|
+
const mapReceipt = await upsertObsidianNoteContent({
|
|
367
|
+
vaultPath: resolvedVaultPath,
|
|
368
|
+
notePath: mapPath,
|
|
369
|
+
content: mapContent,
|
|
370
|
+
});
|
|
371
|
+
if (mapReceipt) receipts.push(mapReceipt);
|
|
372
|
+
|
|
373
|
+
const refreshedNotePaths = await listMarkdownNotes(resolvedVaultPath);
|
|
374
|
+
const refreshedMaNotes = refreshedNotePaths
|
|
375
|
+
.map((absolutePath) => normalizeVaultPath(path.relative(resolvedVaultPath, absolutePath)))
|
|
376
|
+
.filter((relativePath) => relativePath.startsWith("Meta-Architect/"))
|
|
377
|
+
.filter((relativePath) => relativePath !== mapPath)
|
|
378
|
+
.sort();
|
|
379
|
+
const hasCoreNote = refreshedMaNotes.includes("Meta-Architect/Core Brain Context.md");
|
|
380
|
+
|
|
381
|
+
for (const notePath of refreshedMaNotes) {
|
|
382
|
+
const target = await resolveVaultNotePath({
|
|
383
|
+
vaultPath: resolvedVaultPath,
|
|
384
|
+
notePath,
|
|
385
|
+
mustExist: true,
|
|
386
|
+
});
|
|
387
|
+
const current = await fs.readFile(target.absolutePath, "utf8");
|
|
388
|
+
const linked = withObsidianGraphSection(current, {
|
|
389
|
+
currentPath: notePath,
|
|
390
|
+
hasCoreNote,
|
|
391
|
+
});
|
|
392
|
+
if (linked === current) continue;
|
|
393
|
+
await fs.writeFile(target.absolutePath, linked, "utf8");
|
|
394
|
+
receipts.push(
|
|
395
|
+
createObsidianOperationReceipt({
|
|
396
|
+
operation: "update_note",
|
|
397
|
+
target,
|
|
398
|
+
content: linked,
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
record_type: "obsidian_graph_link_result",
|
|
405
|
+
records_as: "vault_context",
|
|
406
|
+
build_evidence: false,
|
|
407
|
+
semantic_symlink_type: "obsidian_wikilink",
|
|
408
|
+
map_note_path: mapPath,
|
|
409
|
+
linked_note_count: refreshedMaNotes.length + 1,
|
|
410
|
+
operations: receipts,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function appendObsidianOperationReceipt(receipt) {
|
|
415
|
+
validateObsidianOperationReceipt(receipt);
|
|
416
|
+
let log = createDefaultObsidianVaultOperations();
|
|
417
|
+
try {
|
|
418
|
+
log = validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
|
|
419
|
+
} catch {}
|
|
420
|
+
log.operations.push(receipt);
|
|
421
|
+
await writeJson(getObsidianVaultOperationsPath(), log);
|
|
422
|
+
return log;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function validateObsidianBridge(value) {
|
|
426
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
427
|
+
throw new Error("Obsidian bridge must be an object");
|
|
428
|
+
}
|
|
429
|
+
if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
|
|
430
|
+
throw new Error(`Unsupported Obsidian bridge schemaVersion: ${value.schemaVersion}`);
|
|
431
|
+
}
|
|
432
|
+
if (
|
|
433
|
+
value.semantic_boundary?.records_as !== "vault_context" ||
|
|
434
|
+
value.semantic_boundary?.never_records_as !== "build_evidence"
|
|
435
|
+
) {
|
|
436
|
+
throw new Error("Obsidian bridge must record as vault_context, not build_evidence");
|
|
437
|
+
}
|
|
438
|
+
if (value.plugin_contract?.authoritative_changes_return_through !== "$maestro_or_owning_lane") {
|
|
439
|
+
throw new Error("Obsidian bridge authoritative changes must return through MA lanes");
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
value.graph_link_policy?.enabled_by_default !== true ||
|
|
443
|
+
value.graph_link_policy?.semantic_symlink_type !== "obsidian_wikilink" ||
|
|
444
|
+
value.graph_link_policy?.map_note_path !== obsidianGraphMapNotePath ||
|
|
445
|
+
value.graph_link_policy?.records_as !== "vault_context" ||
|
|
446
|
+
value.graph_link_policy?.build_evidence !== false
|
|
447
|
+
) {
|
|
448
|
+
throw new Error("Obsidian bridge must enable default wikilink graph policy");
|
|
449
|
+
}
|
|
450
|
+
const forbidden = new Set(value.plugin_contract?.must_not_mutate ?? []);
|
|
451
|
+
for (const required of [".ma/release.json", ".ma/decisions.json", ".ma/plans/", ".ma/specs/"]) {
|
|
452
|
+
if (!forbidden.has(required)) {
|
|
453
|
+
throw new Error(`Obsidian bridge must forbid direct mutation of ${required}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (!Array.isArray(value.default_request_queue)) {
|
|
457
|
+
throw new Error("Obsidian bridge requires a request queue");
|
|
458
|
+
}
|
|
459
|
+
if (
|
|
460
|
+
value.plugin_runtime?.plugin_id !== "meta-architect" ||
|
|
461
|
+
value.plugin_runtime?.records_as !== "vault_context" ||
|
|
462
|
+
value.plugin_runtime?.build_evidence !== false ||
|
|
463
|
+
!Array.isArray(value.plugin_runtime?.capabilities)
|
|
464
|
+
) {
|
|
465
|
+
throw new Error("Obsidian bridge requires the default MA in-app plugin runtime");
|
|
466
|
+
}
|
|
467
|
+
for (const capability of [
|
|
468
|
+
"in_app_plugin_surface",
|
|
469
|
+
"active_note_selection_context",
|
|
470
|
+
"metadata_cache_deep_graph",
|
|
471
|
+
"frontmatter_authority_layer",
|
|
472
|
+
"link_safe_rename_move",
|
|
473
|
+
"protocol_handler",
|
|
474
|
+
"event_watchers",
|
|
475
|
+
"canvas_visual_workspace_context",
|
|
476
|
+
"attachments_binary_resources",
|
|
477
|
+
"plugin_request_queue",
|
|
478
|
+
]) {
|
|
479
|
+
if (!value.plugin_runtime.capabilities.some((entry) => entry.capability === capability)) {
|
|
480
|
+
throw new Error(`Obsidian plugin runtime missing capability: ${capability}`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(value.note_selection_metadata)) {
|
|
484
|
+
throw new Error("Obsidian bridge requires note selection metadata");
|
|
485
|
+
}
|
|
486
|
+
if (!Array.isArray(value.tag_graph_claims)) {
|
|
487
|
+
throw new Error("Obsidian bridge requires tag graph claims");
|
|
488
|
+
}
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function validateObsidianVaultOperations(value) {
|
|
493
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
494
|
+
throw new Error("Obsidian vault operations must be an object");
|
|
495
|
+
}
|
|
496
|
+
if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
|
|
497
|
+
throw new Error(`Unsupported Obsidian operations schemaVersion: ${value.schemaVersion}`);
|
|
498
|
+
}
|
|
499
|
+
if (
|
|
500
|
+
value.record_type !== "obsidian_vault_operations" ||
|
|
501
|
+
value.records_as !== "vault_context" ||
|
|
502
|
+
value.build_evidence !== false ||
|
|
503
|
+
value.authority_boundary !== "$maestro_or_owning_lane" ||
|
|
504
|
+
!Array.isArray(value.operations)
|
|
505
|
+
) {
|
|
506
|
+
throw new Error("Obsidian vault operations must remain lane-owned vault_context");
|
|
507
|
+
}
|
|
508
|
+
for (const operation of value.operations) {
|
|
509
|
+
validateObsidianOperationReceipt(operation);
|
|
510
|
+
}
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function validateObsidianOperationReceipt(value) {
|
|
515
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
516
|
+
throw new Error("Obsidian operation receipt must be an object");
|
|
517
|
+
}
|
|
518
|
+
if (
|
|
519
|
+
!["create_note", "read_note", "update_note", "delete_note", "plugin_install"].includes(
|
|
520
|
+
value.operation,
|
|
521
|
+
) ||
|
|
522
|
+
value.records_as !== "vault_context" ||
|
|
523
|
+
value.build_evidence !== false ||
|
|
524
|
+
value.authority_boundary !== "$maestro_or_owning_lane" ||
|
|
525
|
+
typeof value.relative_path !== "string" ||
|
|
526
|
+
typeof value.content_sha256 !== "string"
|
|
527
|
+
) {
|
|
528
|
+
throw new Error("Obsidian operation receipt must be lane-owned vault_context");
|
|
529
|
+
}
|
|
530
|
+
return value;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function validateObsidianVaultIndex(value) {
|
|
534
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
535
|
+
throw new Error("Obsidian vault index must be an object");
|
|
536
|
+
}
|
|
537
|
+
if (value.schemaVersion !== obsidianIntegrationSchemaVersion) {
|
|
538
|
+
throw new Error(`Unsupported Obsidian vault index schemaVersion: ${value.schemaVersion}`);
|
|
539
|
+
}
|
|
540
|
+
if (
|
|
541
|
+
value.record_type !== "obsidian_vault_index" ||
|
|
542
|
+
value.records_as !== "vault_context" ||
|
|
543
|
+
value.build_evidence !== false ||
|
|
544
|
+
value.read_only_snapshot !== true
|
|
545
|
+
) {
|
|
546
|
+
throw new Error("Obsidian vault index must be read-only vault_context");
|
|
547
|
+
}
|
|
548
|
+
if (!Number.isInteger(value.note_count) || value.note_count < 0) {
|
|
549
|
+
throw new Error("Obsidian vault index requires note_count");
|
|
550
|
+
}
|
|
551
|
+
if (!Array.isArray(value.notes) || value.notes.length !== value.note_count) {
|
|
552
|
+
throw new Error("Obsidian vault index notes must match note_count");
|
|
553
|
+
}
|
|
554
|
+
for (const note of value.notes) {
|
|
555
|
+
if (
|
|
556
|
+
note.record_type !== "vault_context_note" ||
|
|
557
|
+
note.records_as !== "vault_context" ||
|
|
558
|
+
note.build_evidence !== false ||
|
|
559
|
+
typeof note.relative_path !== "string" ||
|
|
560
|
+
typeof note.content_sha256 !== "string"
|
|
561
|
+
) {
|
|
562
|
+
throw new Error("Obsidian vault index note entries must be vault_context notes");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
for (const claim of value.tag_graph_claims ?? []) {
|
|
566
|
+
if (claim.records_as !== "vault_context" || claim.build_evidence !== false) {
|
|
567
|
+
throw new Error("Obsidian tag graph claims must remain vault_context");
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export async function seedObsidianBridgeArtifacts() {
|
|
574
|
+
await ensureDir(getRuntimeSubsystemPath("context"));
|
|
575
|
+
await writeFileIfMissing(
|
|
576
|
+
getObsidianBridgePath(),
|
|
577
|
+
`${JSON.stringify(createDefaultObsidianBridge(), null, 2)}\n`,
|
|
578
|
+
);
|
|
579
|
+
await writeFileIfMissing(
|
|
580
|
+
getObsidianVaultIndexPath(),
|
|
581
|
+
`${JSON.stringify(createDefaultObsidianVaultIndex(), null, 2)}\n`,
|
|
582
|
+
);
|
|
583
|
+
await writeFileIfMissing(
|
|
584
|
+
getObsidianVaultOperationsPath(),
|
|
585
|
+
`${JSON.stringify(createDefaultObsidianVaultOperations(), null, 2)}\n`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function loadObsidianBridge() {
|
|
590
|
+
return validateObsidianBridge(await readJson(getObsidianBridgePath()));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export async function loadObsidianVaultIndex() {
|
|
594
|
+
return validateObsidianVaultIndex(await readJson(getObsidianVaultIndexPath()));
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export async function loadObsidianVaultOperations() {
|
|
598
|
+
return validateObsidianVaultOperations(await readJson(getObsidianVaultOperationsPath()));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function writeObsidianVaultIndex({ vaultPath, ensureGraphLinks = true } = {}) {
|
|
602
|
+
if (ensureGraphLinks) {
|
|
603
|
+
const graph = await ensureObsidianGraphLinks({ vaultPath });
|
|
604
|
+
for (const receipt of graph.operations) {
|
|
605
|
+
await appendObsidianOperationReceipt(receipt);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
const index = await indexObsidianVault({ vaultPath });
|
|
609
|
+
await writeJson(getObsidianVaultIndexPath(), index);
|
|
610
|
+
return index;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
async function upsertObsidianNoteContent({ vaultPath, notePath, content }) {
|
|
614
|
+
const target = await resolveVaultNotePath({ vaultPath, notePath, mustExist: false });
|
|
615
|
+
let current = null;
|
|
616
|
+
if (await pathExists(target.absolutePath)) {
|
|
617
|
+
current = await fs.readFile(target.absolutePath, "utf8");
|
|
618
|
+
}
|
|
619
|
+
if (current === content) return null;
|
|
620
|
+
|
|
621
|
+
await fs.mkdir(path.dirname(target.absolutePath), { recursive: true });
|
|
622
|
+
await fs.writeFile(target.absolutePath, content, "utf8");
|
|
623
|
+
return createObsidianOperationReceipt({
|
|
624
|
+
operation: current === null ? "create_note" : "update_note",
|
|
625
|
+
target,
|
|
626
|
+
content,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function createObsidianGraphMapContent(notePaths) {
|
|
631
|
+
const noteLinks = [...new Set(notePaths)]
|
|
632
|
+
.filter((notePath) => notePath !== obsidianGraphMapNotePath)
|
|
633
|
+
.sort()
|
|
634
|
+
.map((notePath) => `- ${toObsidianWikiLink(notePath)} — MA vault context note.`);
|
|
635
|
+
const noteList =
|
|
636
|
+
noteLinks.length > 0 ? noteLinks.join("\n") : "- No MA context notes have been recorded yet.";
|
|
637
|
+
|
|
638
|
+
return `# Meta-Architect Map of Content
|
|
639
|
+
|
|
640
|
+
This is the canonical Obsidian graph hub for Meta-Architect vault context. MA creates and maintains this note automatically when a vault is indexed or when MA performs vault CRUD.
|
|
641
|
+
|
|
642
|
+
## Canonical MA Notes
|
|
643
|
+
|
|
644
|
+
${noteList}
|
|
645
|
+
|
|
646
|
+
## Core Capability Links
|
|
647
|
+
|
|
648
|
+
- Obsidian Integration Core uses wikilinks as semantic symlinks between MA vault notes.
|
|
649
|
+
- Context Economy Core preserves technical meaning while keeping note text compact when applicable.
|
|
650
|
+
- Ralph Execution Core and Active Autonomy Core use these notes as vault_context only.
|
|
651
|
+
|
|
652
|
+
## Evidence Boundary
|
|
653
|
+
|
|
654
|
+
These linked notes are vault_context. They are not build_evidence. Build evidence stays in MA lane artifacts and command outputs.
|
|
655
|
+
|
|
656
|
+
## Graph Rules
|
|
657
|
+
|
|
658
|
+
- Every MA-generated Obsidian note links back to ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}.
|
|
659
|
+
- The map links every note under Meta-Architect/.
|
|
660
|
+
- Obsidian links are semantic graph links, not release authority. They must not mutate .ma/release.json, .ma/decisions.json, or lane artifacts directly.
|
|
661
|
+
|
|
662
|
+
#ma/core #ma/map #ma/obsidian #ma/vault-context
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function withObsidianGraphSection(content, { currentPath, hasCoreNote }) {
|
|
667
|
+
const base = content
|
|
668
|
+
.replace(new RegExp(`\\n${escapeRegExp(obsidianGraphSectionHeading)}[\\s\\S]*$`, "m"), "")
|
|
669
|
+
.trimEnd();
|
|
670
|
+
const links = [`- Hub: ${toObsidianWikiLink(obsidianGraphMapNotePath, "MA Map of Content")}`];
|
|
671
|
+
if (hasCoreNote && currentPath !== "Meta-Architect/Core Brain Context.md") {
|
|
672
|
+
links.push(
|
|
673
|
+
`- Core context: ${toObsidianWikiLink("Meta-Architect/Core Brain Context.md", "Core Brain Context")}`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
links.push("- This note records as vault_context, not build_evidence.");
|
|
677
|
+
|
|
678
|
+
return `${base}
|
|
679
|
+
|
|
680
|
+
${obsidianGraphSectionHeading}
|
|
681
|
+
|
|
682
|
+
${links.join("\n")}
|
|
683
|
+
`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function toObsidianWikiLink(notePath, alias = null) {
|
|
687
|
+
const target = normalizeVaultPath(notePath).replace(/\.md$/i, "");
|
|
688
|
+
return alias ? `[[${target}|${alias}]]` : `[[${target}]]`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function createObsidianOperationReceipt({ operation, target, content }) {
|
|
692
|
+
return validateObsidianOperationReceipt({
|
|
693
|
+
record_type: "obsidian_vault_operation",
|
|
694
|
+
operation,
|
|
695
|
+
records_as: "vault_context",
|
|
696
|
+
build_evidence: false,
|
|
697
|
+
authority_boundary: "$maestro_or_owning_lane",
|
|
698
|
+
vault_path: target.vaultPath,
|
|
699
|
+
relative_path: target.relativePath,
|
|
700
|
+
content_sha256: createHash("sha256").update(content).digest("hex"),
|
|
701
|
+
char_count: content.length,
|
|
702
|
+
word_count: countWords(content),
|
|
703
|
+
operated_at: new Date().toISOString(),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function resolveVaultRoot(vaultPath) {
|
|
708
|
+
if (!vaultPath || typeof vaultPath !== "string") {
|
|
709
|
+
throw new Error("Obsidian vault operation requires vaultPath");
|
|
710
|
+
}
|
|
711
|
+
const resolvedVaultPath = path.resolve(vaultPath);
|
|
712
|
+
const stat = await fs.stat(resolvedVaultPath);
|
|
713
|
+
if (!stat.isDirectory()) {
|
|
714
|
+
throw new Error(`Obsidian vault path is not a directory: ${resolvedVaultPath}`);
|
|
715
|
+
}
|
|
716
|
+
return resolvedVaultPath;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
async function resolveVaultNotePath({ vaultPath, notePath, mustExist }) {
|
|
720
|
+
const resolvedVaultPath = await resolveVaultRoot(vaultPath);
|
|
721
|
+
if (!notePath || typeof notePath !== "string") {
|
|
722
|
+
throw new Error("Obsidian note operation requires notePath");
|
|
723
|
+
}
|
|
724
|
+
const normalizedInput = normalizeVaultPath(notePath.trim()).replace(/^\/+/, "");
|
|
725
|
+
if (
|
|
726
|
+
!normalizedInput ||
|
|
727
|
+
normalizedInput.startsWith("../") ||
|
|
728
|
+
normalizedInput.includes("/../") ||
|
|
729
|
+
normalizedInput === ".."
|
|
730
|
+
) {
|
|
731
|
+
throw new Error("Obsidian note path must stay inside the vault");
|
|
732
|
+
}
|
|
733
|
+
if (
|
|
734
|
+
normalizedInput.startsWith(".obsidian/") ||
|
|
735
|
+
normalizedInput.startsWith(".trash/") ||
|
|
736
|
+
normalizedInput.startsWith(".git/")
|
|
737
|
+
) {
|
|
738
|
+
throw new Error("Obsidian note operation cannot target vault control directories");
|
|
739
|
+
}
|
|
740
|
+
const relativePath = normalizedInput.toLowerCase().endsWith(".md")
|
|
741
|
+
? normalizedInput
|
|
742
|
+
: `${normalizedInput}.md`;
|
|
743
|
+
const absolutePath = path.resolve(resolvedVaultPath, relativePath);
|
|
744
|
+
if (!absolutePath.startsWith(`${resolvedVaultPath}${path.sep}`)) {
|
|
745
|
+
throw new Error("Obsidian note path resolved outside the vault");
|
|
746
|
+
}
|
|
747
|
+
if (mustExist && !(await pathExists(absolutePath))) {
|
|
748
|
+
throw new Error(`Obsidian note does not exist: ${relativePath}`);
|
|
749
|
+
}
|
|
750
|
+
return { vaultPath: resolvedVaultPath, relativePath, absolutePath };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function pathExists(filePath) {
|
|
754
|
+
try {
|
|
755
|
+
await fs.access(filePath);
|
|
756
|
+
return true;
|
|
757
|
+
} catch {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
async function listMarkdownNotes(root) {
|
|
763
|
+
const result = [];
|
|
764
|
+
async function walk(directory) {
|
|
765
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
766
|
+
for (const entry of entries) {
|
|
767
|
+
if (entry.name === ".obsidian" || entry.name === ".trash" || entry.name === ".git") {
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const absolutePath = path.join(directory, entry.name);
|
|
771
|
+
if (entry.isDirectory()) {
|
|
772
|
+
await walk(absolutePath);
|
|
773
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
774
|
+
result.push(absolutePath);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
await walk(root);
|
|
779
|
+
return result.sort();
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function extractTitle(content) {
|
|
783
|
+
const heading = content.match(/^#\s+(.+)$/m);
|
|
784
|
+
return heading?.[1]?.trim() || null;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function extractObsidianTags(content) {
|
|
788
|
+
const tags = new Set();
|
|
789
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
790
|
+
if (frontmatter) {
|
|
791
|
+
const tagBlock = frontmatter[1].match(/^tags:\s*([\s\S]*?)(?:\n\S|\n?$)/m);
|
|
792
|
+
if (tagBlock) {
|
|
793
|
+
for (const tag of tagBlock[1].split(/[\s,[\]]+/)) {
|
|
794
|
+
addTag(tags, tag);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
for (const match of content.matchAll(/(^|[\s([{])#([A-Za-z0-9_/-]+)/g)) {
|
|
799
|
+
addTag(tags, match[2]);
|
|
800
|
+
}
|
|
801
|
+
return [...tags].sort();
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
function extractObsidianLinks(content) {
|
|
805
|
+
const links = [];
|
|
806
|
+
for (const match of content.matchAll(/\[\[([^\]\n]+)\]\]/g)) {
|
|
807
|
+
const [target, alias] = match[1].split("|").map((part) => part.trim());
|
|
808
|
+
links.push({ type: "wikilink", target, alias: alias || null });
|
|
809
|
+
}
|
|
810
|
+
for (const match of content.matchAll(/\[[^\]\n]+\]\(([^)\n]+)\)/g)) {
|
|
811
|
+
const target = match[1].trim();
|
|
812
|
+
if (!target.startsWith("http://") && !target.startsWith("https://")) {
|
|
813
|
+
links.push({ type: "markdown", target, alias: null });
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return links.sort((a, b) => a.target.localeCompare(b.target));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function addTag(tags, value) {
|
|
820
|
+
const cleaned = value?.trim().replace(/^#/, "").replace(/,$/, "");
|
|
821
|
+
if (cleaned && /^[A-Za-z0-9_/-]+$/.test(cleaned)) {
|
|
822
|
+
tags.add(cleaned);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function normalizeVaultPath(value) {
|
|
827
|
+
return value.split(path.sep).join("/");
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function normalizeLinkKey(value) {
|
|
831
|
+
return normalizeVaultPath(String(value).trim())
|
|
832
|
+
.replace(/\.md$/i, "")
|
|
833
|
+
.replace(/^\/+/, "")
|
|
834
|
+
.toLowerCase();
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function escapeRegExp(value) {
|
|
838
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function countWords(content) {
|
|
842
|
+
return content.split(/\s+/).filter(Boolean).length;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function createExcerpt(content, maxChars) {
|
|
846
|
+
return content
|
|
847
|
+
.replace(/^---\n[\s\S]*?\n---/, "")
|
|
848
|
+
.replace(/\s+/g, " ")
|
|
849
|
+
.trim()
|
|
850
|
+
.slice(0, maxChars);
|
|
851
|
+
}
|