@deepwatch/dsh-adapters 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 oxbshw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @deepwatch/dsh-adapters
2
+
3
+ Optional adapters: Obsidian vaults and LLMWiki bundles. Watch works fully without either.
4
+
5
+ Part of **DeepWatch** — the agent workspace built on the official
6
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
7
+ and powered by [Watch Skill](https://github.com/oxbshw/watch-skill) for perception, evidence, memory and
8
+ independent verification.
9
+
10
+ > **Optional extra — nothing requires it.**
11
+ > Anyone exporting Watch's memory into another tool. Nothing depends on it.
12
+
13
+ ## Exports
14
+
15
+ - `@deepwatch/dsh-adapters`
16
+
17
+ ## Install
18
+
19
+ > **Not on npm yet.** Nothing exists under the `@deepwatch` scope. This
20
+ > package is published for the first time by the `deepwatch-v0.1.0`
21
+ > release; until then the command below resolves nothing, and
22
+ > [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme) has the path
23
+ > that works from a checkout.
24
+
25
+ ```sh
26
+ npm install @deepwatch/dsh-adapters
27
+ ```
28
+
29
+ Rarely on its own. [`@deepwatch/dsh-bundle`](https://github.com/oxbshw/watch-skill/tree/main/workspace/packages/watch/bundle#readme)
30
+ composes this package with the rest of DeepWatch and is what a profile
31
+ normally depends on; installing this one directly is for embedding a
32
+ single piece in a composition you control.
33
+
34
+ ## Example
35
+
36
+ Export the wiki as an Obsidian vault:
37
+
38
+ > Pending the `deepwatch-v0.1.0` release — see Install above.
39
+
40
+ ```sh
41
+ import { buildWiki, toVault } from '@deepwatch/dsh-adapters'
42
+
43
+ const vault = toVault(buildWiki(records), { name: 'Watch' })
44
+ ```
45
+
46
+ ## Requirements
47
+
48
+ - Node `^22.19.0 || >=24.0.0`
49
+
50
+ ## Stability
51
+
52
+ `0.1.0` — a stable release.
53
+
54
+ Stable means tested, documented and supported — not 1.0. This is a
55
+ pre-1.0 line, and semantic versioning gives `0.x` no compatibility
56
+ guarantee across minor versions: **a `0.MINOR` bump may change or remove
57
+ surface, and a patch will not.** Depend on it with a tilde range
58
+ (`~0.1.0`) if you want that difference enforced by your lockfile
59
+ rather than by a changelog. The usual major-version promise starts at 1.0.
60
+
61
+ ## Side effects
62
+
63
+ Importing a module from this package evaluates no side effects, so a
64
+ bundler may drop what a build does not use. Mounting it in a host is a
65
+ separate matter: what it then reads or writes is governed by the
66
+ workspace boundary and the host's permissions, not by this flag.
67
+
68
+ ## Where this fits
69
+
70
+ Obsidian vaults and LLMWiki bundles, generated from the ledger. An edit in an exported vault is a proposal, never a change: Watch is the record and the folder is a copy.
71
+
72
+ The twenty packages and how they compose:
73
+ [the package map](https://github.com/oxbshw/watch-skill/blob/main/workspace/docs/packages.md).
74
+ Running DeepWatch, and the gates a change has to pass:
75
+ [the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme).
76
+
77
+ ## Attribution
78
+
79
+ Built on DeepSeek Harness · Powered by Watch Skill
80
+
81
+ DeepWatch and Watch Skill are independent projects and are not affiliated
82
+ with or endorsed by DeepSeek. MIT licensed; third-party notices are in
83
+ [THIRD_PARTY_NOTICES.md](https://github.com/oxbshw/watch-skill/blob/main/workspace/THIRD_PARTY_NOTICES.md).
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Optional adapters.
3
+ *
4
+ * Both are optional in the strong sense: Watch is complete without either, and
5
+ * neither can become an authority for anything. Content that arrives through
6
+ * an adapter is imported at the weakest origin there is.
7
+ *
8
+ * @module @deepwatch/dsh-adapters
9
+ */
10
+ export * from './obsidian.js';
11
+ export * from './llmwiki.js';
12
+ //# sourceMappingURL=index.d.ts.map
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Optional adapters.
3
+ *
4
+ * Both are optional in the strong sense: Watch is complete without either, and
5
+ * neither can become an authority for anything. Content that arrives through
6
+ * an adapter is imported at the weakest origin there is.
7
+ *
8
+ * @module @deepwatch/dsh-adapters
9
+ */
10
+ export * from './obsidian.js';
11
+ export * from './llmwiki.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,115 @@
1
+ /**
2
+ * LLMWiki compatibility: five files in, five files out, no authority moved.
3
+ *
4
+ * LLMWiki's shape — raw, wiki, citations, index, log — is a good one, and
5
+ * being able to read and write it means a workspace is not trapped here. That
6
+ * is the whole reason this adapter exists, and it is optional in both
7
+ * directions: nothing in Watch requires it, and nothing about importing a
8
+ * bundle makes LLMWiki the authority for anything.
9
+ *
10
+ * The asymmetry is deliberate and is the only interesting thing in the module.
11
+ *
12
+ * **Export is lossless about provenance and lossy about nothing else.** Every
13
+ * exported statement carries its memory id, and `citations` carries the
14
+ * evidence refs, so a bundle that leaves here can be read by something else
15
+ * without the reader having to trust a sentence with no source.
16
+ *
17
+ * **Import is lossy on purpose.** Everything imported arrives as `imported`
18
+ * origin at low confidence, whatever the bundle claims. A bundle that says a
19
+ * statement is `explicit_user`, or high confidence, or already verified, is a
20
+ * bundle making claims about a person it has never met. Those fields are read
21
+ * and discarded rather than trusted, and the discarding is tested with a
22
+ * bundle that asserts all of them at once.
23
+ *
24
+ * @module @deepwatch/dsh-adapters/llmwiki
25
+ */
26
+ import type { MemoryEvent, MemoryRecord } from '@deepwatch/dsh-memory';
27
+ /** The five files an LLMWiki bundle carries. */
28
+ export interface LlmWikiBundle {
29
+ /** Statements as they were captured, one per line, with provenance markers. */
30
+ readonly raw: string;
31
+ /** The readable pages. */
32
+ readonly wiki: string;
33
+ /** Evidence references, one per line. */
34
+ readonly citations: string;
35
+ /** What is in the bundle, by kind. */
36
+ readonly index: string;
37
+ /** What happened, by id and kind. Never content. */
38
+ readonly log: string;
39
+ }
40
+ /** One statement, as the bundle format carries it. */
41
+ export interface LlmWikiStatement {
42
+ readonly id: string;
43
+ readonly kind: string;
44
+ readonly text: string;
45
+ /** What the bundle claims. Read, reported, and never trusted on import. */
46
+ readonly claimedOrigin: string | null;
47
+ readonly claimedConfidence: number | null;
48
+ readonly evidenceRefs: readonly string[];
49
+ }
50
+ /** Parse one statement line back. */
51
+ export declare function parseStatement(line: string): LlmWikiStatement | null;
52
+ /**
53
+ * Export a bundle.
54
+ *
55
+ * Deterministic: the same records and events produce the same five files, so
56
+ * two exports of an unchanged workspace are identical and a diff between them
57
+ * is a real change rather than a reordering.
58
+ */
59
+ export declare function toLlmWiki(records: readonly MemoryRecord[], events?: readonly MemoryEvent[]): LlmWikiBundle;
60
+ /** Why an imported statement was refused. */
61
+ export interface ImportRefusal {
62
+ readonly statement: LlmWikiStatement;
63
+ readonly reason: string;
64
+ }
65
+ /** A candidate the ledger may consider, produced from an imported statement. */
66
+ export interface ImportedStatement {
67
+ readonly kind: MemoryRecord['kind'];
68
+ readonly content: string;
69
+ readonly origin: 'imported';
70
+ readonly confidence: number;
71
+ readonly sourceRefs: readonly string[];
72
+ readonly evidenceRefs: readonly string[];
73
+ /** What the bundle claimed, kept for the review UI. Never acted on. */
74
+ readonly claimed: {
75
+ readonly origin: string | null;
76
+ readonly confidence: number | null;
77
+ };
78
+ }
79
+ /** The outcome of reading a bundle. */
80
+ export interface ImportResult {
81
+ readonly accepted: readonly ImportedStatement[];
82
+ readonly refused: readonly ImportRefusal[];
83
+ /** One line for the import dialog. */
84
+ readonly summary: string;
85
+ }
86
+ /**
87
+ * Read a bundle.
88
+ *
89
+ * Every accepted statement comes back as `imported` at a fixed low confidence.
90
+ * The claimed origin and confidence travel alongside, so a review surface can
91
+ * show "this bundle says you stated it" without the ledger acting on it —
92
+ * which is the difference between reporting a claim and believing one.
93
+ *
94
+ * The refusals are the same two the wiki import uses, for the same reason: a
95
+ * bundle is a file, and a file that could grant a permission or assert
96
+ * something about a protected subject is a file that writes itself into
97
+ * authority.
98
+ */
99
+ export declare function fromLlmWiki(bundle: LlmWikiBundle): ImportResult;
100
+ /**
101
+ * Whether a round trip preserved what it should.
102
+ *
103
+ * Checks content and evidence refs, and deliberately does *not* check origin or
104
+ * confidence — those are supposed to be lost on import, and a round-trip test
105
+ * that required them to survive would be a test demanding the vulnerability.
106
+ */
107
+ export declare function roundTripPreservesContent(original: readonly MemoryRecord[], imported: readonly ImportedStatement[]): boolean;
108
+ /** What this adapter can claim. Optional, and never an authority. */
109
+ export declare function llmWikiAvailability(): {
110
+ readonly adapterId: string;
111
+ readonly optional: true;
112
+ readonly proven: readonly string[];
113
+ readonly notMachineTested: readonly string[];
114
+ };
115
+ //# sourceMappingURL=llmwiki.d.ts.map
package/lib/llmwiki.js ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * LLMWiki compatibility: five files in, five files out, no authority moved.
3
+ *
4
+ * LLMWiki's shape — raw, wiki, citations, index, log — is a good one, and
5
+ * being able to read and write it means a workspace is not trapped here. That
6
+ * is the whole reason this adapter exists, and it is optional in both
7
+ * directions: nothing in Watch requires it, and nothing about importing a
8
+ * bundle makes LLMWiki the authority for anything.
9
+ *
10
+ * The asymmetry is deliberate and is the only interesting thing in the module.
11
+ *
12
+ * **Export is lossless about provenance and lossy about nothing else.** Every
13
+ * exported statement carries its memory id, and `citations` carries the
14
+ * evidence refs, so a bundle that leaves here can be read by something else
15
+ * without the reader having to trust a sentence with no source.
16
+ *
17
+ * **Import is lossy on purpose.** Everything imported arrives as `imported`
18
+ * origin at low confidence, whatever the bundle claims. A bundle that says a
19
+ * statement is `explicit_user`, or high confidence, or already verified, is a
20
+ * bundle making claims about a person it has never met. Those fields are read
21
+ * and discarded rather than trusted, and the discarding is tested with a
22
+ * bundle that asserts all of them at once.
23
+ *
24
+ * @module @deepwatch/dsh-adapters/llmwiki
25
+ */
26
+ import { isHighImpact, isProtectedSubject } from '@deepwatch/dsh-memory';
27
+ /** Serialize one statement line. */
28
+ function statementLine(record) {
29
+ const refs = record.evidenceRefs.length === 0 ? '' : ` refs=${record.evidenceRefs.join(',')}`;
30
+ return `[${record.memoryId}] (${record.kind}) origin=${record.origin} `
31
+ + `confidence=${record.confidence.toFixed(2)}${refs} :: ${record.content.replace(/\r?\n/g, ' ')}`;
32
+ }
33
+ /** Parse one statement line back. */
34
+ export function parseStatement(line) {
35
+ const match = /^\[([^\]]+)\]\s*\(([^)]*)\)\s*(.*?)::\s*(.*)$/.exec(line.trim());
36
+ if (match === null)
37
+ return null;
38
+ const [, id, kind, attributes, text] = match;
39
+ const origin = /origin=([A-Za-z_]+)/.exec(attributes ?? '')?.[1] ?? null;
40
+ const confidence = /confidence=([0-9.]+)/.exec(attributes ?? '')?.[1];
41
+ const refs = /refs=([^\s]+)/.exec(attributes ?? '')?.[1];
42
+ return {
43
+ id: id ?? '',
44
+ kind: kind ?? '',
45
+ text: (text ?? '').trim(),
46
+ claimedOrigin: origin,
47
+ claimedConfidence: confidence === undefined ? null : Number(confidence),
48
+ evidenceRefs: refs === undefined ? [] : refs.split(',').filter(ref => ref !== ''),
49
+ };
50
+ }
51
+ /** Records in a stable order, so a bundle is byte-reproducible. */
52
+ function ordered(records) {
53
+ return [...records].sort((left, right) => left.memoryId.localeCompare(right.memoryId));
54
+ }
55
+ /**
56
+ * Export a bundle.
57
+ *
58
+ * Deterministic: the same records and events produce the same five files, so
59
+ * two exports of an unchanged workspace are identical and a diff between them
60
+ * is a real change rather than a reordering.
61
+ */
62
+ export function toLlmWiki(records, events = []) {
63
+ const sorted = ordered(records);
64
+ const raw = sorted.map(statementLine).join('\n');
65
+ const byKind = new Map();
66
+ for (const record of sorted) {
67
+ const bucket = byKind.get(record.kind) ?? [];
68
+ bucket.push(record);
69
+ byKind.set(record.kind, bucket);
70
+ }
71
+ const wiki = [...byKind.entries()]
72
+ .sort(([left], [right]) => left.localeCompare(right))
73
+ .flatMap(([kind, bucket]) => [
74
+ `## ${kind}`,
75
+ '',
76
+ ...bucket.map(record => `- ${record.content.replace(/\r?\n/g, ' ')} ^${record.memoryId}`),
77
+ '',
78
+ ])
79
+ .join('\n');
80
+ const citations = sorted
81
+ .filter(record => record.evidenceRefs.length > 0 || record.sourceRefs.length > 0)
82
+ .map(record => `${record.memoryId} :: ${[...record.evidenceRefs, ...record.sourceRefs].join(',')}`)
83
+ .join('\n');
84
+ const index = [
85
+ '| kind | count |',
86
+ '|---|---:|',
87
+ ...[...byKind.entries()]
88
+ .sort(([left], [right]) => left.localeCompare(right))
89
+ .map(([kind, bucket]) => `| ${kind} | ${String(bucket.length)} |`),
90
+ ].join('\n');
91
+ // Ids, kinds, actors, timestamps. Never content — a log that reproduced what
92
+ // was forgotten, in the entry recording that it was forgotten, is not a
93
+ // deletion, and an export is exactly where that mistake escapes the product.
94
+ const log = events
95
+ .map(event => `${event.at} ${event.kind} ${event.actor} ${event.memoryId}`)
96
+ .join('\n');
97
+ return { raw, wiki, citations, index, log };
98
+ }
99
+ /** Kinds this adapter recognizes; anything else becomes a plain fact. */
100
+ const KNOWN_KINDS = new Set([
101
+ 'preference', 'fact', 'episode', 'decision', 'lesson', 'procedure', 'failure',
102
+ ]);
103
+ /** Coerce a claimed kind into one the ledger knows. */
104
+ function kindOf(claimed) {
105
+ return KNOWN_KINDS.has(claimed)
106
+ ? claimed
107
+ : 'fact';
108
+ }
109
+ /**
110
+ * Read a bundle.
111
+ *
112
+ * Every accepted statement comes back as `imported` at a fixed low confidence.
113
+ * The claimed origin and confidence travel alongside, so a review surface can
114
+ * show "this bundle says you stated it" without the ledger acting on it —
115
+ * which is the difference between reporting a claim and believing one.
116
+ *
117
+ * The refusals are the same two the wiki import uses, for the same reason: a
118
+ * bundle is a file, and a file that could grant a permission or assert
119
+ * something about a protected subject is a file that writes itself into
120
+ * authority.
121
+ */
122
+ export function fromLlmWiki(bundle) {
123
+ const accepted = [];
124
+ const refused = [];
125
+ for (const line of bundle.raw.split(/\r?\n/)) {
126
+ if (line.trim() === '')
127
+ continue;
128
+ const statement = parseStatement(line);
129
+ if (statement === null) {
130
+ refused.push({
131
+ statement: { id: '', kind: '', text: line, claimedOrigin: null, claimedConfidence: null, evidenceRefs: [] },
132
+ reason: 'The line is not in the bundle statement format.',
133
+ });
134
+ continue;
135
+ }
136
+ if (statement.text === '') {
137
+ refused.push({ statement, reason: 'The statement is empty.' });
138
+ continue;
139
+ }
140
+ if (isHighImpact(statement.text)) {
141
+ refused.push({
142
+ statement,
143
+ reason: 'An imported statement cannot grant a permission or weaken a safeguard.',
144
+ });
145
+ continue;
146
+ }
147
+ if (isProtectedSubject(statement.text)) {
148
+ refused.push({
149
+ statement,
150
+ reason: 'An imported statement cannot assert something about a protected subject.',
151
+ });
152
+ continue;
153
+ }
154
+ accepted.push({
155
+ kind: kindOf(statement.kind),
156
+ content: statement.text,
157
+ // Fixed, whatever the bundle said. A file claiming a person stated
158
+ // something is a file making a claim about a person it never met.
159
+ origin: 'imported',
160
+ confidence: 0.3,
161
+ sourceRefs: ['llmwiki:import'],
162
+ evidenceRefs: statement.evidenceRefs,
163
+ claimed: { origin: statement.claimedOrigin, confidence: statement.claimedConfidence },
164
+ });
165
+ }
166
+ const summary = `${String(accepted.length)} statement(s) proposed, `
167
+ + `${String(refused.length)} refused. `
168
+ + 'Everything imported arrives unconfirmed, at the weakest origin.';
169
+ return { accepted, refused, summary };
170
+ }
171
+ /**
172
+ * Whether a round trip preserved what it should.
173
+ *
174
+ * Checks content and evidence refs, and deliberately does *not* check origin or
175
+ * confidence — those are supposed to be lost on import, and a round-trip test
176
+ * that required them to survive would be a test demanding the vulnerability.
177
+ */
178
+ export function roundTripPreservesContent(original, imported) {
179
+ const wanted = new Map(original.map(record => [record.content, record.evidenceRefs.join(',')]));
180
+ for (const statement of imported) {
181
+ const refs = wanted.get(statement.content);
182
+ if (refs === undefined)
183
+ return false;
184
+ if (refs !== statement.evidenceRefs.join(','))
185
+ return false;
186
+ }
187
+ return wanted.size === new Set(imported.map(statement => statement.content)).size;
188
+ }
189
+ /** What this adapter can claim. Optional, and never an authority. */
190
+ export function llmWikiAvailability() {
191
+ return {
192
+ adapterId: 'adapter.llmwiki',
193
+ optional: true,
194
+ proven: [
195
+ 'Export of all five files, deterministically.',
196
+ 'Import of a bundle as proposals, at imported origin.',
197
+ 'Round trip preserving content and evidence refs.',
198
+ 'Refusal of a hostile bundle: permissions, safeguards and protected subjects.',
199
+ ],
200
+ notMachineTested: [
201
+ 'Interoperability with a specific LLMWiki release. The format here is written '
202
+ + 'against its documented file set, and no LLMWiki installation was available '
203
+ + 'to exchange a bundle with.',
204
+ ],
205
+ };
206
+ }
207
+ //# sourceMappingURL=llmwiki.js.map
@@ -0,0 +1,134 @@
1
+ /**
2
+ * The Obsidian adapter: optional, one-directional about authority, and inert.
3
+ *
4
+ * Obsidian is a good place to read a workspace wiki and a terrible place to
5
+ * keep the record of what an agent believes. It is a folder of Markdown that
6
+ * anything can write to — a sync client, a plugin, a shared drive, another
7
+ * person — and the thing that makes it pleasant is exactly the thing that
8
+ * makes it unsuitable as an authority.
9
+ *
10
+ * So this adapter does two things and refuses a third.
11
+ *
12
+ * It **exports** a vault: the generated wiki, plus the frontmatter and
13
+ * wikilinks that make Obsidian's graph and backlinks work. It **imports** an
14
+ * edit as a *proposal*, through the same validation a hand-edited wiki page
15
+ * goes through. It never lets vault content become authority — not evidence,
16
+ * not a verdict, not `explicit_user`, not a permission, not a scope change.
17
+ *
18
+ * Nothing in Watch Core depends on this module, and nothing here imports
19
+ * Obsidian. There is no Obsidian API to import; the integration surface is a
20
+ * folder and a URI scheme, which is why the adapter can be tested completely
21
+ * against a filesystem-shaped fixture. What cannot be tested here is whether a
22
+ * real Obsidian installation opens the URI, and that is reported as
23
+ * NOT MACHINE TESTED rather than assumed.
24
+ *
25
+ * @module @deepwatch/dsh-adapters/obsidian
26
+ */
27
+ import type { MemoryRecord } from '@deepwatch/dsh-memory';
28
+ import type { WikiPage, WikiProjection, ValidatedEdit } from '@deepwatch/dsh-wiki';
29
+ /** One file as it would sit in a vault. */
30
+ export interface VaultFile {
31
+ /** Path relative to the vault root. */
32
+ readonly path: string;
33
+ readonly content: string;
34
+ /** Tags in the frontmatter, without the leading hash. */
35
+ readonly tags: readonly string[];
36
+ /** Pages this one links to, by vault path. */
37
+ readonly links: readonly string[];
38
+ }
39
+ /** A whole exported vault. */
40
+ export interface Vault {
41
+ readonly name: string;
42
+ readonly files: readonly VaultFile[];
43
+ /**
44
+ * The note that travels with the export.
45
+ *
46
+ * Present as data rather than as a README somebody might not open: the
47
+ * export writes it into the vault, so a person who arrives at these files
48
+ * from a sync client rather than from Watch still learns what they are.
49
+ */
50
+ readonly readme: string;
51
+ }
52
+ /** How the export should be shaped. */
53
+ export interface VaultOptions {
54
+ readonly name: string;
55
+ /** Prefix inside the vault, so a Watch export can live beside other notes. */
56
+ readonly folder?: string;
57
+ /** Include the log page. Off by default: it is long and rarely read here. */
58
+ readonly includeLog?: boolean;
59
+ }
60
+ /**
61
+ * Export the wiki as an Obsidian vault.
62
+ *
63
+ * Frontmatter carries tags and the provenance ids, so a person browsing in
64
+ * Obsidian can still see where a statement came from — the graph view is
65
+ * pleasant and it is also a place where provenance is very easy to lose.
66
+ */
67
+ export declare function toVault(projection: WikiProjection, options: VaultOptions): Vault;
68
+ /**
69
+ * Backlinks, computed from the export rather than read from Obsidian.
70
+ *
71
+ * Computing them here means the export can be checked for a dangling link
72
+ * before anybody opens it, and means the adapter does not need Obsidian to
73
+ * answer a question about its own output.
74
+ */
75
+ export declare function backlinks(vault: Vault): ReadonlyMap<string, readonly string[]>;
76
+ /** Links that point at a file the export does not contain. */
77
+ export declare function danglingLinks(vault: Vault): readonly string[];
78
+ /**
79
+ * The URI that opens one page in Obsidian.
80
+ *
81
+ * Constructed, never executed. Handing a URI to the shell is the desktop
82
+ * layer's job and is gated there; an adapter that could launch things would be
83
+ * an adapter that a generated page could aim.
84
+ */
85
+ export declare function pageUri(vaultName: string, path: string): string;
86
+ /** The URI that opens the vault itself. */
87
+ export declare function vaultUri(vaultName: string): string;
88
+ /**
89
+ * Strip the frontmatter an export added, so a diff compares like with like.
90
+ *
91
+ * Without this, every imported file would appear to have removed the entire
92
+ * generated page and added an entire new one.
93
+ */
94
+ export declare function stripFrontmatter(content: string): string;
95
+ /**
96
+ * Import an edited vault file as a proposal.
97
+ *
98
+ * Runs through the wiki's own diff and validation, which is the point: there
99
+ * is one place that decides what an edited file may assert, and an adapter
100
+ * that had its own copy of those rules would be a second place for them to
101
+ * drift.
102
+ */
103
+ export declare function importVaultFile(file: {
104
+ readonly path: string;
105
+ readonly content: string;
106
+ }, generated: WikiPage): ValidatedEdit;
107
+ /**
108
+ * What this adapter can and cannot claim on this machine.
109
+ *
110
+ * Stated as a value so the Settings surface renders the truth rather than a
111
+ * checkmark. Export, backlinks, URI construction and import are all pure and
112
+ * are gated by tests. Whether a real Obsidian installation opens the URI is
113
+ * not something this machine can answer, and it says so.
114
+ */
115
+ export interface AdapterAvailability {
116
+ readonly adapterId: string;
117
+ readonly optional: true;
118
+ readonly proven: readonly string[];
119
+ readonly notMachineTested: readonly string[];
120
+ }
121
+ /** The Obsidian adapter's honest self-description. */
122
+ export declare function obsidianAvailability(): AdapterAvailability;
123
+ /**
124
+ * Whether a memory record may be written into a shared vault.
125
+ *
126
+ * An export is a copy that leaves the product, and once it is in a synced
127
+ * folder it is wherever that folder goes. Personal taste and anything
128
+ * sensitive stay out by default, and the caller has to say otherwise per
129
+ * export rather than once in a setting.
130
+ */
131
+ export declare function mayExport(record: MemoryRecord, options?: {
132
+ readonly includePersonal?: boolean;
133
+ }): boolean;
134
+ //# sourceMappingURL=obsidian.d.ts.map
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The Obsidian adapter: optional, one-directional about authority, and inert.
3
+ *
4
+ * Obsidian is a good place to read a workspace wiki and a terrible place to
5
+ * keep the record of what an agent believes. It is a folder of Markdown that
6
+ * anything can write to — a sync client, a plugin, a shared drive, another
7
+ * person — and the thing that makes it pleasant is exactly the thing that
8
+ * makes it unsuitable as an authority.
9
+ *
10
+ * So this adapter does two things and refuses a third.
11
+ *
12
+ * It **exports** a vault: the generated wiki, plus the frontmatter and
13
+ * wikilinks that make Obsidian's graph and backlinks work. It **imports** an
14
+ * edit as a *proposal*, through the same validation a hand-edited wiki page
15
+ * goes through. It never lets vault content become authority — not evidence,
16
+ * not a verdict, not `explicit_user`, not a permission, not a scope change.
17
+ *
18
+ * Nothing in Watch Core depends on this module, and nothing here imports
19
+ * Obsidian. There is no Obsidian API to import; the integration surface is a
20
+ * folder and a URI scheme, which is why the adapter can be tested completely
21
+ * against a filesystem-shaped fixture. What cannot be tested here is whether a
22
+ * real Obsidian installation opens the URI, and that is reported as
23
+ * NOT MACHINE TESTED rather than assumed.
24
+ *
25
+ * @module @deepwatch/dsh-adapters/obsidian
26
+ */
27
+ import { diffUserEdit, validateUserEdit } from '@deepwatch/dsh-wiki';
28
+ /** Escape a YAML scalar the crude, safe way. */
29
+ function yamlString(value) {
30
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
31
+ }
32
+ /** Derive tags from a page's path and provenance. */
33
+ function tagsFor(page) {
34
+ const section = page.path.includes('/') ? page.path.split('/')[0] ?? '' : 'index';
35
+ const tags = ['watch', `watch/${section}`];
36
+ if (page.provenance.length === 0)
37
+ tags.push('watch/generated-index');
38
+ return tags;
39
+ }
40
+ /**
41
+ * Rewrite a generated page's Markdown links as Obsidian wikilinks.
42
+ *
43
+ * Deliberately conservative: only links that resolve to a page in the same
44
+ * projection are rewritten. A rewritten link that goes nowhere is worse than a
45
+ * plain one, because Obsidian will offer to create the missing note — and a
46
+ * note created that way is a file with no provenance sitting in a folder full
47
+ * of files that have some.
48
+ */
49
+ function toWikilinks(page, known) {
50
+ const links = [];
51
+ const folder = page.path.includes('/') ? `${page.path.split('/')[0] ?? ''}/` : '';
52
+ const content = page.content.replace(/\[([^\]]+)\]\(([^)]+\.md)\)/g, (whole, label, target) => {
53
+ const resolved = target.startsWith('.') || target.includes('/')
54
+ ? target.replace(/^\.\//, '')
55
+ : `${folder}${target}`;
56
+ if (!known.has(resolved))
57
+ return whole;
58
+ links.push(resolved);
59
+ const stem = resolved.replace(/\.md$/, '');
60
+ return `[[${stem}|${label}]]`;
61
+ });
62
+ return { content, links };
63
+ }
64
+ /** The note that ships inside every exported vault. */
65
+ const VAULT_README = `# Watch export
66
+
67
+ These notes are **generated** from the Watch memory ledger. They are a
68
+ projection, not the record.
69
+
70
+ - Editing a note here does not change what Watch believes. An edit is imported
71
+ as a *proposal*, validated, and then the note is regenerated from the ledger.
72
+ - Nothing in this vault is evidence, and nothing here can make something
73
+ verified.
74
+ - A note added by hand — or by a sync client, a plugin, or another person — is
75
+ imported at the weakest origin there is. It cannot grant a permission, change
76
+ a memory's scope, or claim that you said something.
77
+
78
+ If this folder and Watch disagree, Watch is right and this folder is stale.
79
+ Re-export to fix it.
80
+ `;
81
+ /**
82
+ * Export the wiki as an Obsidian vault.
83
+ *
84
+ * Frontmatter carries tags and the provenance ids, so a person browsing in
85
+ * Obsidian can still see where a statement came from — the graph view is
86
+ * pleasant and it is also a place where provenance is very easy to lose.
87
+ */
88
+ export function toVault(projection, options) {
89
+ const folder = options.folder === undefined || options.folder === ''
90
+ ? ''
91
+ : `${options.folder.replace(/\/+$/, '')}/`;
92
+ const included = projection.pages.filter(page => options.includeLog === true || page.path !== 'log.md');
93
+ const known = new Set(included.map(page => page.path));
94
+ const files = included.map(page => {
95
+ const { content, links } = toWikilinks(page, known);
96
+ const tags = tagsFor(page);
97
+ const frontmatter = [
98
+ '---',
99
+ `title: ${yamlString(page.title)}`,
100
+ `tags: [${tags.map(tag => yamlString(tag)).join(', ')}]`,
101
+ 'watch_generated: true',
102
+ `watch_provenance: [${page.provenance.map(id => yamlString(id)).join(', ')}]`,
103
+ '---',
104
+ '',
105
+ ].join('\n');
106
+ return {
107
+ path: `${folder}${page.path}`,
108
+ content: `${frontmatter}${content}`,
109
+ tags,
110
+ links: links.map(link => `${folder}${link}`),
111
+ };
112
+ });
113
+ return {
114
+ name: options.name,
115
+ files: [
116
+ { path: `${folder}README.md`, content: VAULT_README, tags: ['watch'], links: [] },
117
+ ...files,
118
+ ],
119
+ readme: VAULT_README,
120
+ };
121
+ }
122
+ /**
123
+ * Backlinks, computed from the export rather than read from Obsidian.
124
+ *
125
+ * Computing them here means the export can be checked for a dangling link
126
+ * before anybody opens it, and means the adapter does not need Obsidian to
127
+ * answer a question about its own output.
128
+ */
129
+ export function backlinks(vault) {
130
+ const incoming = new Map();
131
+ for (const file of vault.files) {
132
+ for (const link of file.links) {
133
+ const list = incoming.get(link) ?? [];
134
+ list.push(file.path);
135
+ incoming.set(link, list);
136
+ }
137
+ }
138
+ return new Map([...incoming].map(([path, sources]) => [path, [...sources].sort()]));
139
+ }
140
+ /** Links that point at a file the export does not contain. */
141
+ export function danglingLinks(vault) {
142
+ const known = new Set(vault.files.map(file => file.path));
143
+ const dangling = new Set();
144
+ for (const file of vault.files) {
145
+ for (const link of file.links)
146
+ if (!known.has(link))
147
+ dangling.add(link);
148
+ }
149
+ return [...dangling].sort();
150
+ }
151
+ /**
152
+ * The URI that opens one page in Obsidian.
153
+ *
154
+ * Constructed, never executed. Handing a URI to the shell is the desktop
155
+ * layer's job and is gated there; an adapter that could launch things would be
156
+ * an adapter that a generated page could aim.
157
+ */
158
+ export function pageUri(vaultName, path) {
159
+ const file = path.replace(/\.md$/, '');
160
+ return `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodeURIComponent(file)}`;
161
+ }
162
+ /** The URI that opens the vault itself. */
163
+ export function vaultUri(vaultName) {
164
+ return `obsidian://open?vault=${encodeURIComponent(vaultName)}`;
165
+ }
166
+ /**
167
+ * Strip the frontmatter an export added, so a diff compares like with like.
168
+ *
169
+ * Without this, every imported file would appear to have removed the entire
170
+ * generated page and added an entire new one.
171
+ */
172
+ export function stripFrontmatter(content) {
173
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(content);
174
+ return match === null ? content : content.slice(match[0].length);
175
+ }
176
+ /**
177
+ * Import an edited vault file as a proposal.
178
+ *
179
+ * Runs through the wiki's own diff and validation, which is the point: there
180
+ * is one place that decides what an edited file may assert, and an adapter
181
+ * that had its own copy of those rules would be a second place for them to
182
+ * drift.
183
+ */
184
+ export function importVaultFile(file, generated) {
185
+ const body = stripFrontmatter(file.content);
186
+ return validateUserEdit(diffUserEdit(generated, body), generated);
187
+ }
188
+ /** The Obsidian adapter's honest self-description. */
189
+ export function obsidianAvailability() {
190
+ return {
191
+ adapterId: 'adapter.obsidian',
192
+ optional: true,
193
+ proven: [
194
+ 'Vault export, including frontmatter, tags and wikilinks.',
195
+ 'Backlink and dangling-link computation over the export.',
196
+ 'URI construction for a vault and for one page.',
197
+ 'Import of an edited file as a proposal, through the wiki’s own validation.',
198
+ ],
199
+ notMachineTested: [
200
+ 'Opening an obsidian:// URI. No Obsidian installation is present here, so '
201
+ + 'whether it launches and resolves the page has not been observed.',
202
+ 'Round-tripping through Obsidian’s own editor, which may normalise Markdown '
203
+ + 'in ways this adapter has not seen.',
204
+ ],
205
+ };
206
+ }
207
+ /**
208
+ * Whether a memory record may be written into a shared vault.
209
+ *
210
+ * An export is a copy that leaves the product, and once it is in a synced
211
+ * folder it is wherever that folder goes. Personal taste and anything
212
+ * sensitive stay out by default, and the caller has to say otherwise per
213
+ * export rather than once in a setting.
214
+ */
215
+ export function mayExport(record, options = {}) {
216
+ if (record.sensitivity === 'sensitive' || record.sensitivity === 'restricted')
217
+ return false;
218
+ if (record.kind === 'preference' && options.includePersonal !== true)
219
+ return false;
220
+ if (record.subjectScope === 'user' && options.includePersonal !== true)
221
+ return false;
222
+ return true;
223
+ }
224
+ //# sourceMappingURL=obsidian.js.map
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@deepwatch/dsh-adapters",
3
+ "version": "0.1.0",
4
+ "description": "Optional adapters: Obsidian vaults and LLMWiki bundles. Watch works fully without either.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Sayed Allam",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/oxbshw/watch-skill.git",
11
+ "directory": "workspace/packages/watch/adapters"
12
+ },
13
+ "homepage": "https://github.com/oxbshw/watch-skill/tree/main/workspace#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/oxbshw/watch-skill/issues"
16
+ },
17
+ "keywords": [
18
+ "deepwatch",
19
+ "deepseek-harness",
20
+ "watch-skill",
21
+ "adapters",
22
+ "perception"
23
+ ],
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "types": "./lib/index.d.ts",
29
+ "default": "./lib/index.js"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "lib/**/*.js",
35
+ "lib/**/*.d.ts"
36
+ ],
37
+ "sideEffects": false,
38
+ "engines": {
39
+ "node": "^22.19.0 || >=24.0.0"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "dependencies": {
45
+ "@deepwatch/dsh-memory": "^0.1.0",
46
+ "@deepwatch/dsh-wiki": "^0.1.0"
47
+ }
48
+ }