@deepwatch/dsh-wiki 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 +21 -0
- package/README.md +73 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +7 -0
- package/lib/projection.d.ts +167 -0
- package/lib/projection.js +389 -0
- package/package.json +47 -0
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,73 @@
|
|
|
1
|
+
# @deepwatch/dsh-wiki
|
|
2
|
+
|
|
3
|
+
Deterministic workspace wiki projections over the memory ledger
|
|
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
|
+
> **Host plugin — runs beside the agent in the DSH process.**
|
|
11
|
+
> Composed by the bundle.
|
|
12
|
+
|
|
13
|
+
## Exports
|
|
14
|
+
|
|
15
|
+
- `@deepwatch/dsh-wiki`
|
|
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-wiki
|
|
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
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
- Node `^22.19.0 || >=24.0.0`
|
|
37
|
+
|
|
38
|
+
`@deepwatch/dsh-memory` mounted, since the ledger is what it projects.
|
|
39
|
+
|
|
40
|
+
## Stability
|
|
41
|
+
|
|
42
|
+
`0.1.0` — a stable release.
|
|
43
|
+
|
|
44
|
+
Stable means tested, documented and supported — not 1.0. This is a
|
|
45
|
+
pre-1.0 line, and semantic versioning gives `0.x` no compatibility
|
|
46
|
+
guarantee across minor versions: **a `0.MINOR` bump may change or remove
|
|
47
|
+
surface, and a patch will not.** Depend on it with a tilde range
|
|
48
|
+
(`~0.1.0`) if you want that difference enforced by your lockfile
|
|
49
|
+
rather than by a changelog. The usual major-version promise starts at 1.0.
|
|
50
|
+
|
|
51
|
+
## Side effects
|
|
52
|
+
|
|
53
|
+
Importing a module from this package evaluates no side effects, so a
|
|
54
|
+
bundler may drop what a build does not use. Mounting it in a host is a
|
|
55
|
+
separate matter: what it then reads or writes is governed by the
|
|
56
|
+
workspace boundary and the host's permissions, not by this flag.
|
|
57
|
+
|
|
58
|
+
## Where this fits
|
|
59
|
+
|
|
60
|
+
Deterministic projections over the memory ledger: the same ledger produces the same pages, so a wiki page is a view and never a second copy of the truth.
|
|
61
|
+
|
|
62
|
+
The twenty packages and how they compose:
|
|
63
|
+
[the package map](https://github.com/oxbshw/watch-skill/blob/main/workspace/docs/packages.md).
|
|
64
|
+
Running DeepWatch, and the gates a change has to pass:
|
|
65
|
+
[the workspace README](https://github.com/oxbshw/watch-skill/tree/main/workspace#readme).
|
|
66
|
+
|
|
67
|
+
## Attribution
|
|
68
|
+
|
|
69
|
+
Built on DeepSeek Harness · Powered by Watch Skill
|
|
70
|
+
|
|
71
|
+
DeepWatch and Watch Skill are independent projects and are not affiliated
|
|
72
|
+
with or endorsed by DeepSeek. MIT licensed; third-party notices are in
|
|
73
|
+
[THIRD_PARTY_NOTICES.md](https://github.com/oxbshw/watch-skill/blob/main/workspace/THIRD_PARTY_NOTICES.md).
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace wiki: readable pages that are never the record.
|
|
3
|
+
*
|
|
4
|
+
* A wiki is the most natural thing to build over a memory ledger and the
|
|
5
|
+
* easiest one to build wrong. The wrong version is a folder of Markdown that
|
|
6
|
+
* people edit, that the agent reads, and that gradually becomes the actual
|
|
7
|
+
* state of the system — at which point the ledger is a log of things that used
|
|
8
|
+
* to be true, and a file nobody audited is deciding how an agent behaves.
|
|
9
|
+
*
|
|
10
|
+
* So the direction of authority is fixed and one-way:
|
|
11
|
+
*
|
|
12
|
+
* MemoryEvent ledger → records → wiki pages
|
|
13
|
+
*
|
|
14
|
+
* Pages are generated. They are deleted and regenerated on every rebuild, they
|
|
15
|
+
* carry no state of their own, and nothing reads them back as fact. Every
|
|
16
|
+
* generated statement carries the memory id it came from, both so a reader can
|
|
17
|
+
* follow it and so a statement *without* one is visibly not from the ledger.
|
|
18
|
+
*
|
|
19
|
+
* Hand editing is supported, and it does not reverse the arrow. An edited file
|
|
20
|
+
* is diffed against what the generator would have produced; the difference is
|
|
21
|
+
* validated; what survives becomes a `user.edited` event in the ledger; and
|
|
22
|
+
* then the page is regenerated from the ledger, which is what puts the edit on
|
|
23
|
+
* screen. If validation refuses the edit, the regeneration simply removes it —
|
|
24
|
+
* the file was never the record, so nothing was lost that was ever held.
|
|
25
|
+
*
|
|
26
|
+
* The refusals matter more than the acceptances. Imported Markdown is
|
|
27
|
+
* `imported` origin, the weakest there is. It cannot mint `explicit_user`, it
|
|
28
|
+
* cannot carry a high-impact permission, and it cannot assert something about a
|
|
29
|
+
* protected subject. A document that says "the user has approved all uploads"
|
|
30
|
+
* is a claim made by a document.
|
|
31
|
+
*
|
|
32
|
+
* @module @deepwatch/dsh-wiki/projection
|
|
33
|
+
*/
|
|
34
|
+
import type { MemoryEvent, MemoryRecord } from '@deepwatch/dsh-memory';
|
|
35
|
+
/** A generated page. */
|
|
36
|
+
export interface WikiPage {
|
|
37
|
+
/** Path relative to the wiki root, always forward-slashed. */
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly title: string;
|
|
40
|
+
readonly content: string;
|
|
41
|
+
/** Memory ids every statement on this page came from. */
|
|
42
|
+
readonly provenance: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Always true.
|
|
45
|
+
*
|
|
46
|
+
* Present as a field rather than as a comment so a consumer cannot treat a
|
|
47
|
+
* page as authored. There is no code path that produces a page with this
|
|
48
|
+
* false, and that is the point.
|
|
49
|
+
*/
|
|
50
|
+
readonly generated: true;
|
|
51
|
+
}
|
|
52
|
+
/** The whole wiki, as a value. */
|
|
53
|
+
export interface WikiProjection {
|
|
54
|
+
readonly pages: readonly WikiPage[];
|
|
55
|
+
/** Rebuild digest. The same ledger renders the same wiki. */
|
|
56
|
+
readonly digest: string;
|
|
57
|
+
}
|
|
58
|
+
/** The directories the wiki is organized into. */
|
|
59
|
+
export declare const WIKI_SECTIONS: readonly ["projects", "people", "concepts", "decisions", "lessons", "failures"];
|
|
60
|
+
/** One wiki section. */
|
|
61
|
+
export type WikiSection = typeof WIKI_SECTIONS[number];
|
|
62
|
+
/**
|
|
63
|
+
* A stable, filesystem-safe slug.
|
|
64
|
+
*
|
|
65
|
+
* Deliberately does not transliterate. A page about an Arabic concept keeps
|
|
66
|
+
* its identity in the title, and the slug falls back to the memory id rather
|
|
67
|
+
* than to a mangled romanization that would collide with every other one.
|
|
68
|
+
*/
|
|
69
|
+
export declare function slugFor(record: MemoryRecord): string;
|
|
70
|
+
/**
|
|
71
|
+
* Build the whole wiki from the ledger.
|
|
72
|
+
*
|
|
73
|
+
* A pure fold. Rebuilding from an empty index directory, from a cold start, or
|
|
74
|
+
* after a restart produces the identical bytes, which is what makes "the wiki
|
|
75
|
+
* is a projection" a testable claim rather than a design intention.
|
|
76
|
+
*
|
|
77
|
+
* Deleted records are absent because the fold that produced `records` already
|
|
78
|
+
* excluded them. There is no separate wiki deletion path, and therefore no
|
|
79
|
+
* separate wiki deletion path to forget to run.
|
|
80
|
+
*/
|
|
81
|
+
export declare function buildWiki(records: readonly MemoryRecord[], events?: readonly MemoryEvent[]): WikiProjection;
|
|
82
|
+
/** Look one page up by path. */
|
|
83
|
+
export declare function pageAt(projection: WikiProjection, path: string): WikiPage | null;
|
|
84
|
+
/** One line a person added or removed. */
|
|
85
|
+
export interface EditedLine {
|
|
86
|
+
readonly text: string;
|
|
87
|
+
/** The memory id the line claimed, when it carried a marker. */
|
|
88
|
+
readonly memoryId: string | null;
|
|
89
|
+
}
|
|
90
|
+
/** What a hand edit proposes. */
|
|
91
|
+
export interface EditProposal {
|
|
92
|
+
readonly path: string;
|
|
93
|
+
readonly added: readonly EditedLine[];
|
|
94
|
+
readonly removed: readonly EditedLine[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Diff a hand-edited page against what the generator produced.
|
|
98
|
+
*
|
|
99
|
+
* Line-level and deliberately crude. A structural Markdown diff would be more
|
|
100
|
+
* precise and would also be a place for a clever edit to hide; comparing the
|
|
101
|
+
* statement lines catches everything that could carry a claim.
|
|
102
|
+
*/
|
|
103
|
+
export declare function diffUserEdit(generated: WikiPage, edited: string): EditProposal;
|
|
104
|
+
/** Why one edited line was refused. */
|
|
105
|
+
export interface EditRefusal {
|
|
106
|
+
readonly line: string;
|
|
107
|
+
readonly reason: string;
|
|
108
|
+
readonly fix: string;
|
|
109
|
+
}
|
|
110
|
+
/** What survived validation, and what did not. */
|
|
111
|
+
export interface ValidatedEdit {
|
|
112
|
+
readonly path: string;
|
|
113
|
+
/** Lines that may become `user.edited` events. */
|
|
114
|
+
readonly accepted: readonly EditedLine[];
|
|
115
|
+
readonly refused: readonly EditRefusal[];
|
|
116
|
+
/** Memory ids the edit asks to remove. */
|
|
117
|
+
readonly removals: readonly string[];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Validate a hand edit.
|
|
121
|
+
*
|
|
122
|
+
* Four refusals, each closing a way a file could write itself into authority:
|
|
123
|
+
*
|
|
124
|
+
* - **A forged marker.** A line carrying `mem:` for an id the page does not
|
|
125
|
+
* own is trying to attribute a new claim to an existing record.
|
|
126
|
+
* - **High impact.** A permission, a standing grant, a weakened safeguard.
|
|
127
|
+
* Imported text is the weakest origin there is; it does not get to authorize
|
|
128
|
+
* anything, at any confidence.
|
|
129
|
+
* - **A protected subject.** Health, beliefs, and the rest are things a person
|
|
130
|
+
* states about themselves. A file stating them is a file making a claim.
|
|
131
|
+
* - **An empty claim.** A marker with no statement is a way to create a record
|
|
132
|
+
* with no content and then fill it later.
|
|
133
|
+
*
|
|
134
|
+
* Everything accepted becomes `imported` origin and `proposed` status. Nothing
|
|
135
|
+
* a file says can make it `explicit_user`, because that origin means a person
|
|
136
|
+
* did something, and reading a file is not a person doing something.
|
|
137
|
+
*/
|
|
138
|
+
export declare function validateUserEdit(proposal: EditProposal, generated: WikiPage): ValidatedEdit;
|
|
139
|
+
/** A candidate the ledger may admit, produced from an accepted edit line. */
|
|
140
|
+
export interface ImportedCandidate {
|
|
141
|
+
readonly kind: MemoryRecord['kind'];
|
|
142
|
+
readonly content: string;
|
|
143
|
+
readonly origin: 'imported';
|
|
144
|
+
readonly subjectScope: MemoryRecord['subjectScope'];
|
|
145
|
+
readonly scopeId: string;
|
|
146
|
+
readonly confidence: number;
|
|
147
|
+
readonly sourceRefs: readonly string[];
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Turn accepted edit lines into ledger candidates.
|
|
151
|
+
*
|
|
152
|
+
* `imported` origin and a low confidence, always, whatever the file said. The
|
|
153
|
+
* scope comes from the caller — the wiki knows which workspace it belongs to
|
|
154
|
+
* and a file does not get to choose.
|
|
155
|
+
*/
|
|
156
|
+
export declare function toCandidates(edit: ValidatedEdit, scope: {
|
|
157
|
+
readonly subjectScope: MemoryRecord['subjectScope'];
|
|
158
|
+
readonly scopeId: string;
|
|
159
|
+
}): readonly ImportedCandidate[];
|
|
160
|
+
/**
|
|
161
|
+
* One line describing what a hand edit will actually do.
|
|
162
|
+
*
|
|
163
|
+
* Shown before the edit is applied. "3 accepted, 1 refused" is what stops a
|
|
164
|
+
* person believing their file is now the state of the system.
|
|
165
|
+
*/
|
|
166
|
+
export declare function describeEdit(edit: ValidatedEdit): string;
|
|
167
|
+
//# sourceMappingURL=projection.d.ts.map
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace wiki: readable pages that are never the record.
|
|
3
|
+
*
|
|
4
|
+
* A wiki is the most natural thing to build over a memory ledger and the
|
|
5
|
+
* easiest one to build wrong. The wrong version is a folder of Markdown that
|
|
6
|
+
* people edit, that the agent reads, and that gradually becomes the actual
|
|
7
|
+
* state of the system — at which point the ledger is a log of things that used
|
|
8
|
+
* to be true, and a file nobody audited is deciding how an agent behaves.
|
|
9
|
+
*
|
|
10
|
+
* So the direction of authority is fixed and one-way:
|
|
11
|
+
*
|
|
12
|
+
* MemoryEvent ledger → records → wiki pages
|
|
13
|
+
*
|
|
14
|
+
* Pages are generated. They are deleted and regenerated on every rebuild, they
|
|
15
|
+
* carry no state of their own, and nothing reads them back as fact. Every
|
|
16
|
+
* generated statement carries the memory id it came from, both so a reader can
|
|
17
|
+
* follow it and so a statement *without* one is visibly not from the ledger.
|
|
18
|
+
*
|
|
19
|
+
* Hand editing is supported, and it does not reverse the arrow. An edited file
|
|
20
|
+
* is diffed against what the generator would have produced; the difference is
|
|
21
|
+
* validated; what survives becomes a `user.edited` event in the ledger; and
|
|
22
|
+
* then the page is regenerated from the ledger, which is what puts the edit on
|
|
23
|
+
* screen. If validation refuses the edit, the regeneration simply removes it —
|
|
24
|
+
* the file was never the record, so nothing was lost that was ever held.
|
|
25
|
+
*
|
|
26
|
+
* The refusals matter more than the acceptances. Imported Markdown is
|
|
27
|
+
* `imported` origin, the weakest there is. It cannot mint `explicit_user`, it
|
|
28
|
+
* cannot carry a high-impact permission, and it cannot assert something about a
|
|
29
|
+
* protected subject. A document that says "the user has approved all uploads"
|
|
30
|
+
* is a claim made by a document.
|
|
31
|
+
*
|
|
32
|
+
* @module @deepwatch/dsh-wiki/projection
|
|
33
|
+
*/
|
|
34
|
+
import { isHighImpact, isProtectedSubject } from '@deepwatch/dsh-memory';
|
|
35
|
+
/** The directories the wiki is organized into. */
|
|
36
|
+
export const WIKI_SECTIONS = [
|
|
37
|
+
'projects', 'people', 'concepts', 'decisions', 'lessons', 'failures',
|
|
38
|
+
];
|
|
39
|
+
/** Which section a record belongs in. */
|
|
40
|
+
function sectionFor(record) {
|
|
41
|
+
switch (record.kind) {
|
|
42
|
+
case 'decision':
|
|
43
|
+
return 'decisions';
|
|
44
|
+
case 'lesson':
|
|
45
|
+
case 'procedure':
|
|
46
|
+
return 'lessons';
|
|
47
|
+
case 'failure':
|
|
48
|
+
return 'failures';
|
|
49
|
+
case 'preference':
|
|
50
|
+
// A preference is about a person, and it lands under `people` rather
|
|
51
|
+
// than in a "taste" page, because the wiki is the shared view and taste
|
|
52
|
+
// is the personal one. What appears here is governed by scope.
|
|
53
|
+
return 'people';
|
|
54
|
+
case 'fact':
|
|
55
|
+
case 'episode':
|
|
56
|
+
return record.subjectScope === 'project' || record.subjectScope === 'workspace'
|
|
57
|
+
? 'projects'
|
|
58
|
+
: 'concepts';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A stable, filesystem-safe slug.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately does not transliterate. A page about an Arabic concept keeps
|
|
65
|
+
* its identity in the title, and the slug falls back to the memory id rather
|
|
66
|
+
* than to a mangled romanization that would collide with every other one.
|
|
67
|
+
*/
|
|
68
|
+
export function slugFor(record) {
|
|
69
|
+
const ascii = record.content
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
72
|
+
.replace(/^-+|-+$/g, '')
|
|
73
|
+
.slice(0, 48);
|
|
74
|
+
return ascii === '' ? record.memoryId : `${ascii}-${record.memoryId.slice(-8)}`;
|
|
75
|
+
}
|
|
76
|
+
/** Escape a value so it cannot break out of a Markdown table cell. */
|
|
77
|
+
function cell(text) {
|
|
78
|
+
return text.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* One generated statement line, with its provenance.
|
|
82
|
+
*
|
|
83
|
+
* The marker is an HTML comment so it survives a Markdown renderer without
|
|
84
|
+
* being displayed, and so the diff can find it exactly. A statement that has
|
|
85
|
+
* lost its marker is not a statement the ledger made.
|
|
86
|
+
*/
|
|
87
|
+
function statement(record) {
|
|
88
|
+
return `- ${cell(record.content)} <!-- mem:${record.memoryId} -->`;
|
|
89
|
+
}
|
|
90
|
+
/** The provenance footer every page carries. */
|
|
91
|
+
function provenanceBlock(records) {
|
|
92
|
+
if (records.length === 0)
|
|
93
|
+
return [];
|
|
94
|
+
const lines = [
|
|
95
|
+
'',
|
|
96
|
+
'## Provenance',
|
|
97
|
+
'',
|
|
98
|
+
'Generated from the memory ledger. This page is not evidence.',
|
|
99
|
+
'',
|
|
100
|
+
'| memory | kind | origin | confidence | last confirmed |',
|
|
101
|
+
'|---|---|---|---:|---|',
|
|
102
|
+
];
|
|
103
|
+
for (const record of records) {
|
|
104
|
+
lines.push(`| \`${record.memoryId}\` | ${record.kind} | ${record.origin} `
|
|
105
|
+
+ `| ${record.confidence.toFixed(2)} | ${record.lastConfirmedAt ?? 'never'} |`);
|
|
106
|
+
}
|
|
107
|
+
return lines;
|
|
108
|
+
}
|
|
109
|
+
/** Sort records into a stable order, so a rebuild is byte-identical. */
|
|
110
|
+
function ordered(records) {
|
|
111
|
+
return [...records].sort((left, right) => left.memoryId.localeCompare(right.memoryId));
|
|
112
|
+
}
|
|
113
|
+
/** Render one page for one record. */
|
|
114
|
+
function pageFor(record) {
|
|
115
|
+
const section = sectionFor(record);
|
|
116
|
+
const title = record.content.split(/\r?\n/)[0]?.slice(0, 80) ?? record.memoryId;
|
|
117
|
+
const lines = [
|
|
118
|
+
`# ${cell(title)}`,
|
|
119
|
+
'',
|
|
120
|
+
statement(record),
|
|
121
|
+
'',
|
|
122
|
+
`Status: ${record.status}. Scope: ${record.subjectScope}`
|
|
123
|
+
+ `${record.scopeId === '' ? '' : `:${record.scopeId}`}.`,
|
|
124
|
+
...record.evidenceRefs.length === 0
|
|
125
|
+
? []
|
|
126
|
+
: ['', 'Evidence:', ...record.evidenceRefs.map(ref => `- \`${ref}\``)],
|
|
127
|
+
...provenanceBlock([record]),
|
|
128
|
+
'',
|
|
129
|
+
];
|
|
130
|
+
return {
|
|
131
|
+
path: `${section}/${slugFor(record)}.md`,
|
|
132
|
+
title,
|
|
133
|
+
content: lines.join('\n'),
|
|
134
|
+
provenance: [record.memoryId],
|
|
135
|
+
generated: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Render the section index. */
|
|
139
|
+
function sectionIndex(section, records) {
|
|
140
|
+
const lines = [
|
|
141
|
+
`# ${section}`,
|
|
142
|
+
'',
|
|
143
|
+
...records.length === 0
|
|
144
|
+
? ['Nothing recorded.']
|
|
145
|
+
: records.map(record => `- [${cell(record.content.slice(0, 60))}](${slugFor(record)}.md) <!-- mem:${record.memoryId} -->`),
|
|
146
|
+
'',
|
|
147
|
+
];
|
|
148
|
+
return {
|
|
149
|
+
path: `${section}/index.md`,
|
|
150
|
+
title: section,
|
|
151
|
+
content: lines.join('\n'),
|
|
152
|
+
provenance: records.map(record => record.memoryId),
|
|
153
|
+
generated: true,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Render the root index. */
|
|
157
|
+
function rootIndex(bySection) {
|
|
158
|
+
const lines = [
|
|
159
|
+
'# Workspace wiki',
|
|
160
|
+
'',
|
|
161
|
+
'Generated from the memory ledger. Every page here is a projection, not a',
|
|
162
|
+
'record. Editing a page proposes a change to the ledger; it does not change',
|
|
163
|
+
'anything on its own.',
|
|
164
|
+
'',
|
|
165
|
+
'| section | pages |',
|
|
166
|
+
'|---|---:|',
|
|
167
|
+
...WIKI_SECTIONS.map(section => `| [${section}](${section}/index.md) | ${String((bySection.get(section) ?? []).length)} |`),
|
|
168
|
+
'',
|
|
169
|
+
];
|
|
170
|
+
return {
|
|
171
|
+
path: 'index.md',
|
|
172
|
+
title: 'Workspace wiki',
|
|
173
|
+
content: lines.join('\n'),
|
|
174
|
+
provenance: [],
|
|
175
|
+
generated: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Render the event log.
|
|
180
|
+
*
|
|
181
|
+
* Ids, kinds, actors and timestamps. Never content — a log that reproduced
|
|
182
|
+
* what was forgotten, in the entry recording that it was forgotten, would not
|
|
183
|
+
* be a deletion.
|
|
184
|
+
*/
|
|
185
|
+
function logPage(events) {
|
|
186
|
+
const lines = [
|
|
187
|
+
'# Log',
|
|
188
|
+
'',
|
|
189
|
+
'Every change to the ledger, in order. Content is deliberately absent.',
|
|
190
|
+
'',
|
|
191
|
+
...events.map(event => `- \`${event.at}\` **${event.kind}** by ${event.actor} — \`${event.memoryId}\``),
|
|
192
|
+
'',
|
|
193
|
+
];
|
|
194
|
+
return {
|
|
195
|
+
path: 'log.md',
|
|
196
|
+
title: 'Log',
|
|
197
|
+
content: lines.join('\n'),
|
|
198
|
+
provenance: [],
|
|
199
|
+
generated: true,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** FNV-1a over the rendered wiki, so a rebuild is checkable. */
|
|
203
|
+
function digestOf(pages) {
|
|
204
|
+
let hash = 0x811c9dc5;
|
|
205
|
+
const feed = (text) => {
|
|
206
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
207
|
+
hash ^= text.charCodeAt(index);
|
|
208
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
for (const page of pages)
|
|
212
|
+
feed(`|${page.path}|${page.content}`);
|
|
213
|
+
return hash.toString(16).padStart(8, '0');
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Build the whole wiki from the ledger.
|
|
217
|
+
*
|
|
218
|
+
* A pure fold. Rebuilding from an empty index directory, from a cold start, or
|
|
219
|
+
* after a restart produces the identical bytes, which is what makes "the wiki
|
|
220
|
+
* is a projection" a testable claim rather than a design intention.
|
|
221
|
+
*
|
|
222
|
+
* Deleted records are absent because the fold that produced `records` already
|
|
223
|
+
* excluded them. There is no separate wiki deletion path, and therefore no
|
|
224
|
+
* separate wiki deletion path to forget to run.
|
|
225
|
+
*/
|
|
226
|
+
export function buildWiki(records, events = []) {
|
|
227
|
+
const bySection = new Map();
|
|
228
|
+
for (const section of WIKI_SECTIONS)
|
|
229
|
+
bySection.set(section, []);
|
|
230
|
+
for (const record of ordered(records)) {
|
|
231
|
+
// Forgotten records never reach here — the ledger's fold drops them — but
|
|
232
|
+
// a superseded one is real history and belongs on its page, marked.
|
|
233
|
+
bySection.get(sectionFor(record))?.push(record);
|
|
234
|
+
}
|
|
235
|
+
const pages = [rootIndex(bySection)];
|
|
236
|
+
for (const section of WIKI_SECTIONS) {
|
|
237
|
+
const inSection = bySection.get(section) ?? [];
|
|
238
|
+
pages.push(sectionIndex(section, inSection));
|
|
239
|
+
for (const record of inSection)
|
|
240
|
+
pages.push(pageFor(record));
|
|
241
|
+
}
|
|
242
|
+
pages.push(logPage(events));
|
|
243
|
+
const sorted = [...pages].sort((left, right) => left.path.localeCompare(right.path));
|
|
244
|
+
return { pages: sorted, digest: digestOf(sorted) };
|
|
245
|
+
}
|
|
246
|
+
/** Look one page up by path. */
|
|
247
|
+
export function pageAt(projection, path) {
|
|
248
|
+
return projection.pages.find(page => page.path === path) ?? null;
|
|
249
|
+
}
|
|
250
|
+
/** Read a provenance marker off a line. */
|
|
251
|
+
function markerOf(line) {
|
|
252
|
+
return /<!--\s*mem:([A-Za-z0-9_-]+)\s*-->/.exec(line)?.[1] ?? null;
|
|
253
|
+
}
|
|
254
|
+
/** Statement lines only — headings, tables and blank lines are chrome. */
|
|
255
|
+
function statementLines(content) {
|
|
256
|
+
return content.split(/\r?\n/).filter(line => line.trimStart().startsWith('- '));
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Diff a hand-edited page against what the generator produced.
|
|
260
|
+
*
|
|
261
|
+
* Line-level and deliberately crude. A structural Markdown diff would be more
|
|
262
|
+
* precise and would also be a place for a clever edit to hide; comparing the
|
|
263
|
+
* statement lines catches everything that could carry a claim.
|
|
264
|
+
*/
|
|
265
|
+
export function diffUserEdit(generated, edited) {
|
|
266
|
+
const before = new Set(statementLines(generated.content));
|
|
267
|
+
const after = new Set(statementLines(edited));
|
|
268
|
+
const added = [...after].filter(line => !before.has(line))
|
|
269
|
+
.map(line => ({ text: line, memoryId: markerOf(line) }));
|
|
270
|
+
const removed = [...before].filter(line => !after.has(line))
|
|
271
|
+
.map(line => ({ text: line, memoryId: markerOf(line) }));
|
|
272
|
+
return { path: generated.path, added, removed };
|
|
273
|
+
}
|
|
274
|
+
/** Strip the provenance marker to get the claim itself. */
|
|
275
|
+
function claimOf(line) {
|
|
276
|
+
return line.replace(/<!--[\s\S]*?-->/g, '').replace(/^\s*-\s*/, '').trim();
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Validate a hand edit.
|
|
280
|
+
*
|
|
281
|
+
* Four refusals, each closing a way a file could write itself into authority:
|
|
282
|
+
*
|
|
283
|
+
* - **A forged marker.** A line carrying `mem:` for an id the page does not
|
|
284
|
+
* own is trying to attribute a new claim to an existing record.
|
|
285
|
+
* - **High impact.** A permission, a standing grant, a weakened safeguard.
|
|
286
|
+
* Imported text is the weakest origin there is; it does not get to authorize
|
|
287
|
+
* anything, at any confidence.
|
|
288
|
+
* - **A protected subject.** Health, beliefs, and the rest are things a person
|
|
289
|
+
* states about themselves. A file stating them is a file making a claim.
|
|
290
|
+
* - **An empty claim.** A marker with no statement is a way to create a record
|
|
291
|
+
* with no content and then fill it later.
|
|
292
|
+
*
|
|
293
|
+
* Everything accepted becomes `imported` origin and `proposed` status. Nothing
|
|
294
|
+
* a file says can make it `explicit_user`, because that origin means a person
|
|
295
|
+
* did something, and reading a file is not a person doing something.
|
|
296
|
+
*/
|
|
297
|
+
export function validateUserEdit(proposal, generated) {
|
|
298
|
+
const owned = new Set(generated.provenance);
|
|
299
|
+
const accepted = [];
|
|
300
|
+
const refused = [];
|
|
301
|
+
for (const line of proposal.added) {
|
|
302
|
+
const claim = claimOf(line.text);
|
|
303
|
+
if (line.memoryId !== null && !owned.has(line.memoryId)) {
|
|
304
|
+
refused.push({
|
|
305
|
+
line: line.text,
|
|
306
|
+
reason: `The line claims memory ${line.memoryId}, which this page does not own.`,
|
|
307
|
+
fix: 'Remove the provenance comment. A new statement is a new memory.',
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (claim === '') {
|
|
312
|
+
refused.push({
|
|
313
|
+
line: line.text,
|
|
314
|
+
reason: 'The line carries a provenance marker but states nothing.',
|
|
315
|
+
fix: 'Write the statement, or delete the line.',
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (isHighImpact(claim)) {
|
|
320
|
+
refused.push({
|
|
321
|
+
line: line.text,
|
|
322
|
+
reason: 'Imported text cannot grant a permission or weaken a safeguard.',
|
|
323
|
+
fix: 'If this is intended, state it yourself in the conversation.',
|
|
324
|
+
});
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (isProtectedSubject(claim)) {
|
|
328
|
+
refused.push({
|
|
329
|
+
line: line.text,
|
|
330
|
+
reason: 'Imported text cannot assert something about a protected subject.',
|
|
331
|
+
fix: 'If this is about you, state it yourself in the conversation.',
|
|
332
|
+
});
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
accepted.push({ text: claim, memoryId: line.memoryId });
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
path: proposal.path,
|
|
339
|
+
accepted,
|
|
340
|
+
refused,
|
|
341
|
+
removals: proposal.removed
|
|
342
|
+
.map(line => line.memoryId)
|
|
343
|
+
.filter((id) => id !== null),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Turn accepted edit lines into ledger candidates.
|
|
348
|
+
*
|
|
349
|
+
* `imported` origin and a low confidence, always, whatever the file said. The
|
|
350
|
+
* scope comes from the caller — the wiki knows which workspace it belongs to
|
|
351
|
+
* and a file does not get to choose.
|
|
352
|
+
*/
|
|
353
|
+
export function toCandidates(edit, scope) {
|
|
354
|
+
const kind = edit.path.startsWith('decisions/')
|
|
355
|
+
? 'decision'
|
|
356
|
+
: edit.path.startsWith('lessons/')
|
|
357
|
+
? 'lesson'
|
|
358
|
+
: edit.path.startsWith('failures/')
|
|
359
|
+
? 'failure'
|
|
360
|
+
: 'fact';
|
|
361
|
+
return edit.accepted.map(line => ({
|
|
362
|
+
kind,
|
|
363
|
+
content: line.text,
|
|
364
|
+
origin: 'imported',
|
|
365
|
+
subjectScope: scope.subjectScope,
|
|
366
|
+
scopeId: scope.scopeId,
|
|
367
|
+
// Not a number the file chose. An imported claim starts weak and earns its
|
|
368
|
+
// way up by a person confirming it.
|
|
369
|
+
confidence: 0.3,
|
|
370
|
+
sourceRefs: [`wiki:${edit.path}`],
|
|
371
|
+
}));
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* One line describing what a hand edit will actually do.
|
|
375
|
+
*
|
|
376
|
+
* Shown before the edit is applied. "3 accepted, 1 refused" is what stops a
|
|
377
|
+
* person believing their file is now the state of the system.
|
|
378
|
+
*/
|
|
379
|
+
export function describeEdit(edit) {
|
|
380
|
+
const parts = [
|
|
381
|
+
`${String(edit.accepted.length)} statement(s) proposed`,
|
|
382
|
+
`${String(edit.removals.length)} removal(s)`,
|
|
383
|
+
];
|
|
384
|
+
if (edit.refused.length > 0)
|
|
385
|
+
parts.push(`${String(edit.refused.length)} refused`);
|
|
386
|
+
parts.push('the page will be regenerated from the ledger');
|
|
387
|
+
return parts.join(' · ');
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=projection.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepwatch/dsh-wiki",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Deterministic workspace wiki projections over the memory ledger",
|
|
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/wiki"
|
|
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
|
+
"wiki",
|
|
22
|
+
"documentation"
|
|
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
|
+
}
|
|
47
|
+
}
|