trustwiki 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/README.zh.md +82 -0
- package/SKILL.md +76 -0
- package/cli/bin.js +29 -0
- package/cli/citations.js +45 -0
- package/cli/config.js +52 -0
- package/cli/engine.js +99 -0
- package/cli/frontmatter.js +33 -0
- package/cli/links.js +12 -0
- package/cli/report.js +24 -0
- package/cli/resolve.js +25 -0
- package/cli/rules/citation-malformed.js +12 -0
- package/cli/rules/citation-target-missing.js +21 -0
- package/cli/rules/frontmatter-fields.js +39 -0
- package/cli/rules/frontmatter-required.js +12 -0
- package/cli/rules/index.js +19 -0
- package/cli/rules/link-broken.js +22 -0
- package/cli/rules/link-index-missing.js +30 -0
- package/cli/rules/link-type-mismatch.js +16 -0
- package/cli/rules/page-orphan.js +14 -0
- package/cli/rules/placeholder-present.js +17 -0
- package/cli/rules/provenance-contradicted.js +31 -0
- package/cli/rules/provenance-excess-inferred.js +21 -0
- package/cli/rules/provenance-low-confidence.js +15 -0
- package/cli/walk.js +19 -0
- package/docs/method.md +69 -0
- package/docs/method.zh.md +61 -0
- package/package.json +31 -0
- package/proof/STATS.md +18 -0
- package/schema/spec.md +110 -0
- package/schema/spec.zh.md +103 -0
- package/templates/demo-vault/.trustwiki.json +6 -0
- package/templates/demo-vault/index.md +7 -0
- package/templates/demo-vault/notes/conflict-a.md +13 -0
- package/templates/demo-vault/notes/conflict-b.md +12 -0
- package/templates/demo-vault/notes/honest-page.md +13 -0
- package/templates/demo-vault/notes/sloppy-page.md +20 -0
- package/templates/demo-vault/sources/tea.md +12 -0
- package/templates/starter-vault/.trustwiki.json +5 -0
- package/templates/starter-vault/index.md +4 -0
- package/templates/starter-vault/notes/first-note.md +11 -0
- package/templates/starter-vault/sources/example-source.md +12 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { rule as frontmatterRequired } from './frontmatter-required.js';
|
|
2
|
+
import { rule as frontmatterFields } from './frontmatter-fields.js';
|
|
3
|
+
import { rule as placeholderPresent } from './placeholder-present.js';
|
|
4
|
+
import { rule as linkBroken } from './link-broken.js';
|
|
5
|
+
import { rule as indexMissing } from './link-index-missing.js';
|
|
6
|
+
import { rule as typeMismatch } from './link-type-mismatch.js';
|
|
7
|
+
import { rule as pageOrphan } from './page-orphan.js';
|
|
8
|
+
import { rule as citationMalformed } from './citation-malformed.js';
|
|
9
|
+
import { rule as citationTarget } from './citation-target-missing.js';
|
|
10
|
+
import { rule as excessInferred } from './provenance-excess-inferred.js';
|
|
11
|
+
import { rule as lowConfidence } from './provenance-low-confidence.js';
|
|
12
|
+
import { rule as contradicted } from './provenance-contradicted.js';
|
|
13
|
+
|
|
14
|
+
export const RULES = [
|
|
15
|
+
frontmatterRequired, frontmatterFields, placeholderPresent,
|
|
16
|
+
linkBroken, indexMissing, typeMismatch, pageOrphan,
|
|
17
|
+
citationMalformed, citationTarget,
|
|
18
|
+
excessInferred, lowConfidence, contradicted,
|
|
19
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function resolvesTo(model, pageTarget) {
|
|
2
|
+
const norm = pageTarget.replace(/\.md$/, '');
|
|
3
|
+
if (model.filePaths.has(`${norm}.md`)) return true;
|
|
4
|
+
const base = `${norm.split('/').pop()}.md`;
|
|
5
|
+
const hits = [...model.filePaths].filter(p => p.endsWith(`/${base}`) || p === base);
|
|
6
|
+
return hits.length === 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const rule = {
|
|
10
|
+
id: 'link.broken',
|
|
11
|
+
run(model) {
|
|
12
|
+
const out = [];
|
|
13
|
+
for (const f of model.files) for (const l of f.links) {
|
|
14
|
+
const pageTarget = l.target.split('#')[0]; // [[page#Section]] → page
|
|
15
|
+
if (!pageTarget) continue; // pure-anchor link: nothing to resolve here
|
|
16
|
+
if (!resolvesTo(model, pageTarget)) out.push({ file: f.relPath, line: l.line,
|
|
17
|
+
message: `broken wikilink [[${l.target}]]`,
|
|
18
|
+
hint: 'fix the path or create the target page' });
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function listedEntryResolves(model, norm) {
|
|
2
|
+
if (model.filePaths.has(`${norm}.md`)) return true;
|
|
3
|
+
const base = `${norm.split('/').pop()}.md`;
|
|
4
|
+
const hits = [...model.filePaths].filter(p => p.endsWith(`/${base}`) || p === base);
|
|
5
|
+
return hits.length === 1;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const rule = {
|
|
9
|
+
id: 'link.index-missing', needs: 'index',
|
|
10
|
+
run(model) {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const f of model.files) {
|
|
13
|
+
const norm = f.relPath.replace(/\.md$/, '');
|
|
14
|
+
let listed = model.indexEntries.has(norm);
|
|
15
|
+
if (!listed) {
|
|
16
|
+
const base = norm.split('/').pop();
|
|
17
|
+
listed = [...model.indexEntries].some(e => e.endsWith(`/${base}`) || e === base);
|
|
18
|
+
}
|
|
19
|
+
if (!listed) out.push({ file: f.relPath, line: 1,
|
|
20
|
+
message: 'missing from index',
|
|
21
|
+
hint: `add an entry to ${model.config.index}` });
|
|
22
|
+
}
|
|
23
|
+
for (const [norm, line] of model.indexLines) {
|
|
24
|
+
if (!listedEntryResolves(model, norm)) out.push({ file: model.config.index, line,
|
|
25
|
+
message: `index entry does not resolve: [[${norm}]]`,
|
|
26
|
+
hint: 'fix the path or remove the entry' });
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const rule = {
|
|
2
|
+
id: 'link.type-mismatch',
|
|
3
|
+
run(model) {
|
|
4
|
+
const out = [];
|
|
5
|
+
if (!Object.keys(model.config.typeByDir).length) return out; // no-op by design
|
|
6
|
+
for (const f of model.files) {
|
|
7
|
+
const topDir = f.relPath.split('/')[0];
|
|
8
|
+
const want = model.config.typeByDir[topDir];
|
|
9
|
+
const got = f.fm.ok ? f.fm.fields.type : undefined;
|
|
10
|
+
if (want && got && got !== want) out.push({ file: f.relPath, line: 1,
|
|
11
|
+
message: `type "${got}" does not match directory type "${want}"`,
|
|
12
|
+
hint: `set type: ${want} or move the page` });
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const rule = {
|
|
2
|
+
id: 'page.orphan',
|
|
3
|
+
run(model) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const f of model.files) {
|
|
6
|
+
if (!f.fm.ok) continue;
|
|
7
|
+
const n = f.links.length;
|
|
8
|
+
if (n < model.config.minOutboundLinks) out.push({ file: f.relPath, line: 1,
|
|
9
|
+
message: `only ${n} outbound link(s) — orphaned page`,
|
|
10
|
+
hint: `weave in at least ${model.config.minOutboundLinks} related pages` });
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const RE = /\b(TODO|TBD|FIXME|lorem ipsum)\b/i;
|
|
2
|
+
|
|
3
|
+
export const rule = {
|
|
4
|
+
id: 'placeholder.present',
|
|
5
|
+
run(model) {
|
|
6
|
+
const out = [];
|
|
7
|
+
for (const f of model.files) {
|
|
8
|
+
const head = f.body.split('\n').slice(0, 20).join('\n');
|
|
9
|
+
const m = head.match(RE);
|
|
10
|
+
if (m) out.push({ file: f.relPath,
|
|
11
|
+
line: f.bodyStartLine + head.slice(0, m.index).split('\n').length - 1,
|
|
12
|
+
message: `placeholder text: ${m[0]}`,
|
|
13
|
+
hint: 'unfinished content erodes trust — finish or remove' });
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
},
|
|
17
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const CALLOUT = /\[!contradiction\]([^\n]*)/;
|
|
2
|
+
|
|
3
|
+
export const rule = {
|
|
4
|
+
id: 'provenance.contradicted',
|
|
5
|
+
run(model) {
|
|
6
|
+
const out = [];
|
|
7
|
+
const targetsIn = b => [...b.matchAll(/\[\[([^\]|\n]+)/g)].map(x => x[1].trim().replace(/\.md$/, ''));
|
|
8
|
+
for (const f of model.files) {
|
|
9
|
+
const m = f.body.match(CALLOUT);
|
|
10
|
+
const calloutTargets = m ? targetsIn(m[1]) : [];
|
|
11
|
+
const fmList = (f.fm?.fields?.contradicted_by || '').replace(/[\[\]]/g, '');
|
|
12
|
+
const fmTargets = fmList ? fmList.split(',').map(s => s.trim()).filter(Boolean).map(t => t.replace(/\.md$/, '')) : [];
|
|
13
|
+
if (m && !fmTargets.length) out.push({ file: f.relPath,
|
|
14
|
+
line: f.bodyStartLine + f.body.slice(0, m.index).split('\n').length - 1,
|
|
15
|
+
message: 'contradiction callout without contradicted_by in frontmatter',
|
|
16
|
+
hint: 'mirror the contradiction in frontmatter so lint can check both sides' });
|
|
17
|
+
if (!m && fmTargets.length) out.push({ file: f.relPath, line: 1,
|
|
18
|
+
message: 'contradicted_by lists targets but no [!contradiction] callout in body',
|
|
19
|
+
hint: 'surface the conflict in the body so readers see it' });
|
|
20
|
+
if (m && fmTargets.length) {
|
|
21
|
+
const a = new Set(calloutTargets), b = new Set(fmTargets);
|
|
22
|
+
const onlyCallout = [...a].filter(t => !b.has(t));
|
|
23
|
+
const onlyFm = [...b].filter(t => !a.has(t));
|
|
24
|
+
if (onlyCallout.length || onlyFm.length) out.push({ file: f.relPath, line: 1,
|
|
25
|
+
message: `contradiction target sets differ — callout-only: [${onlyCallout.join(', ') || 'none'}], frontmatter-only: [${onlyFm.join(', ') || 'none'}]`,
|
|
26
|
+
hint: 'make the body callout and contradicted_by list agree' });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const rule = {
|
|
2
|
+
id: 'provenance.excess-inferred',
|
|
3
|
+
run(model) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const f of model.files) {
|
|
6
|
+
if (!f.fm.ok) continue;
|
|
7
|
+
if (model.config.inferredSkipTypes.includes(f.fm.fields.type)) continue;
|
|
8
|
+
const prose = f.paragraphs.filter(p => p.isProse && p.text);
|
|
9
|
+
if (!prose.length) continue;
|
|
10
|
+
// a paragraph counts as cited only when a citation sits on its final line
|
|
11
|
+
const citedLines = new Set(f.citations.map(c => c.line));
|
|
12
|
+
const uncited = prose.filter(p => !citedLines.has(p.startLine + p.text.split('\n').length - 1));
|
|
13
|
+
if (uncited.length / prose.length > model.config.inferredThreshold) {
|
|
14
|
+
out.push({ file: f.relPath, line: uncited[0].startLine,
|
|
15
|
+
message: `${uncited.length}/${prose.length} prose paragraphs uncited (>${model.config.inferredThreshold})`,
|
|
16
|
+
hint: 'cite sources at paragraph end, or mark the page as inference — unattributed claims erode trust' });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const rule = {
|
|
2
|
+
id: 'provenance.low-confidence',
|
|
3
|
+
run(model) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const f of model.files) {
|
|
6
|
+
const c = Number(f.fm?.ok ? f.fm.fields.confidence : NaN);
|
|
7
|
+
if (!Number.isNaN(c) && c < model.config.confidenceFloor) {
|
|
8
|
+
out.push({ file: f.relPath, line: 1,
|
|
9
|
+
message: `confidence ${c} below floor ${model.config.confidenceFloor}`,
|
|
10
|
+
hint: 'add sources to raise confidence, or archive the page' });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
},
|
|
15
|
+
};
|
package/cli/walk.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const SKIP = new Set(['.git', 'node_modules']);
|
|
5
|
+
|
|
6
|
+
export async function walkVault(root, roots, indexFile = null) {
|
|
7
|
+
const out = [];
|
|
8
|
+
async function rec(rel) {
|
|
9
|
+
const abs = join(root, rel);
|
|
10
|
+
for (const entry of await readdir(abs, { withFileTypes: true })) {
|
|
11
|
+
if (entry.name.startsWith('.') || SKIP.has(entry.name)) continue;
|
|
12
|
+
const relChild = rel ? `${rel}/${entry.name}` : entry.name;
|
|
13
|
+
if (entry.isDirectory()) await rec(relChild);
|
|
14
|
+
else if (entry.name.endsWith('.md') && relChild !== indexFile) out.push(relChild);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
for (const r of roots.length ? roots : ['.']) await rec(r === '.' ? '' : r);
|
|
18
|
+
return out.sort();
|
|
19
|
+
}
|
package/docs/method.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# The trustwiki method — one rule, twelve checks
|
|
2
|
+
|
|
3
|
+
The one rule: **never write a claim your vault cannot trace.** Agent-written
|
|
4
|
+
knowledge bases fail in predictable ways; each rule below exists because its
|
|
5
|
+
failure mode was observed, repeatedly, in production. Grouped by what they
|
|
6
|
+
protect.
|
|
7
|
+
|
|
8
|
+
## Truth
|
|
9
|
+
|
|
10
|
+
### citation.malformed
|
|
11
|
+
A citation that doesn't parse is decoration, not provenance. The check
|
|
12
|
+
validates every `^[…]` against the grammar in `schema/spec.md` (path, optional
|
|
13
|
+
`:42-58` or `#L42-L58` range, comma-separated multi-source).
|
|
14
|
+
*Incident class:* hand-written citations that "look right" but were never
|
|
15
|
+
resolvable — the visual signature of provenance without the substance.
|
|
16
|
+
|
|
17
|
+
### citation.target-missing
|
|
18
|
+
Every cited path must exist in the vault. A citation pointing at a file that
|
|
19
|
+
isn't there is a lie with a hair cut.
|
|
20
|
+
*Incident class:* pages renamed or archived without sweeping their backlinks.
|
|
21
|
+
|
|
22
|
+
### provenance.excess-inferred
|
|
23
|
+
Inference is legitimate; invisible inference is not. When more than the
|
|
24
|
+
threshold (default 30%) of a page's prose paragraphs carry no citation, the
|
|
25
|
+
page is flagged. Source pages (`type: source`) are exempt — they quote, they
|
|
26
|
+
don't cite themselves.
|
|
27
|
+
*Incident class:* the slop-vault — fluent, confident prose that no source
|
|
28
|
+
can confirm. This is the failure mode people mean when they say "AI slop".
|
|
29
|
+
|
|
30
|
+
### provenance.low-confidence
|
|
31
|
+
Pages may declare `confidence: 0–1`. Below the floor (default 0.5) the page
|
|
32
|
+
is flagged so weak claims are visible at a glance.
|
|
33
|
+
|
|
34
|
+
## Conflict
|
|
35
|
+
|
|
36
|
+
### provenance.contradicted
|
|
37
|
+
When two pages disagree, the disagreement is data. The rule enforces the
|
|
38
|
+
two halves: a `> [!contradiction]` callout in the body **and** a
|
|
39
|
+
`contradicted_by` list in frontmatter — either half alone is flagged,
|
|
40
|
+
because machines read frontmatter and humans read prose.
|
|
41
|
+
*Incident class:* a contradiction quietly rewritten away, then rediscovered
|
|
42
|
+
six weeks later by someone re-deriving the same wrong conclusion.
|
|
43
|
+
|
|
44
|
+
## Structure
|
|
45
|
+
|
|
46
|
+
### link.broken
|
|
47
|
+
Every `[[wikilink]]` must resolve — by exact path or unique basename.
|
|
48
|
+
### link.index-missing
|
|
49
|
+
Every page must appear in the index; every index entry must resolve.
|
|
50
|
+
*Incident class:* index corruption — entries glued onto one line by a bad
|
|
51
|
+
edit, silently dropping every second page from navigation.
|
|
52
|
+
### link.type-mismatch
|
|
53
|
+
If you declare expected `type` per directory, pages must match it.
|
|
54
|
+
### page.orphan
|
|
55
|
+
Fewer than `minOutboundLinks` (default 2) outbound links → flagged. Islands
|
|
56
|
+
rot first.
|
|
57
|
+
### frontmatter.required / frontmatter.fields
|
|
58
|
+
No frontmatter, or missing required fields (`title, created, updated, type,
|
|
59
|
+
tags`; sources add `source_url, ingested, sha256`) — flagged. The sha256 is
|
|
60
|
+
how a silently edited source is caught months later.
|
|
61
|
+
### placeholder.present
|
|
62
|
+
`TODO`/`TBD`/`FIXME` in the first twenty body lines. Unfinished content that
|
|
63
|
+
looks finished erodes trust in everything around it.
|
|
64
|
+
|
|
65
|
+
## The operating loop
|
|
66
|
+
|
|
67
|
+
Ingest → Synthesize → Evolve → Gate. See `SKILL.md` for the agent-facing
|
|
68
|
+
method; see `schema/spec.md` for the frozen grammar. Chinese version:
|
|
69
|
+
`docs/method.zh.md`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# trustwiki 方法——一条铁律,十二道检查
|
|
2
|
+
|
|
3
|
+
铁律:**永远不要写下知识库无法溯源的论断。** agent 维护的知识库以一种
|
|
4
|
+
可预测的方式腐烂;下面每条规则都来自生产环境中反复出现的真实事故。
|
|
5
|
+
按保护对象分组。
|
|
6
|
+
|
|
7
|
+
## 真相
|
|
8
|
+
|
|
9
|
+
### citation.malformed
|
|
10
|
+
解析不了的引用只是装饰,不是溯源。检查器按 `schema/spec.zh.md` 的文法
|
|
11
|
+
校验每个 `^[…]`(路径、可选 `:42-58` / `#L42-L58` 区间、逗号分隔多来源)。
|
|
12
|
+
*事故类型*:看起来像引用、实际永远无法解析的手写引用——有溯源的外形,
|
|
13
|
+
没有溯源的实体。
|
|
14
|
+
|
|
15
|
+
### citation.target-missing
|
|
16
|
+
被引用的路径必须存在于库内。指向不存在文件的引用是剪了头发的谎言。
|
|
17
|
+
*事故类型*:页面改名或归档时没有清扫反向引用。
|
|
18
|
+
|
|
19
|
+
### provenance.excess-inferred
|
|
20
|
+
推断是合法的;不可见的推断不是。当一页未引用散文段落占比超过阈值
|
|
21
|
+
(默认 30%)时标记。来源页(`type: source`)豁免——它们引用来源,
|
|
22
|
+
不需要引用自己。
|
|
23
|
+
*事故类型*:slop 库——流畅、自信、没有任何来源能佐证的散文。
|
|
24
|
+
这就是人们说"AI slop"时指的那种失败。
|
|
25
|
+
|
|
26
|
+
### provenance.low-confidence
|
|
27
|
+
页面可声明 `confidence: 0–1`。低于下限(默认 0.5)即标记,
|
|
28
|
+
让弱论断一眼可见。
|
|
29
|
+
|
|
30
|
+
## 冲突
|
|
31
|
+
|
|
32
|
+
### provenance.contradicted
|
|
33
|
+
两页相左时,分歧本身就是数据。规则强制两半齐备:正文
|
|
34
|
+
`> [!contradiction]` callout **加** frontmatter `contradicted_by` 列表——
|
|
35
|
+
只有一半就标记,因为机器读 frontmatter 而人读正文。
|
|
36
|
+
*事故类型*:矛盾被悄悄改写抹平,六周后有人重新推导出同一个错误结论。
|
|
37
|
+
|
|
38
|
+
## 结构
|
|
39
|
+
|
|
40
|
+
### link.broken
|
|
41
|
+
每个 `[[wikilink]]` 必须可解析——精确路径或全局唯一 basename。
|
|
42
|
+
### link.index-missing
|
|
43
|
+
每页必须进索引;每个索引项必须可解析。
|
|
44
|
+
*事故类型*:索引腐化——一次坏编辑把两行条目粘成一行,导航里静默消失
|
|
45
|
+
一半的页面。
|
|
46
|
+
### link.type-mismatch
|
|
47
|
+
声明了目录级期望 `type` 后,页面必须匹配。
|
|
48
|
+
### page.orphan
|
|
49
|
+
出链少于 `minOutboundLinks`(默认 2)即标记。孤岛先腐烂。
|
|
50
|
+
### frontmatter.required / frontmatter.fields
|
|
51
|
+
缺 frontmatter 或缺必填字段(`title, created, updated, type, tags`;
|
|
52
|
+
来源页另需 `source_url, ingested, sha256`)即标记。sha256 是数月后
|
|
53
|
+
发现来源被静默篡改的唯一手段。
|
|
54
|
+
### placeholder.present
|
|
55
|
+
正文前二十行里的 `TODO`/`TBD`/`FIXME`。看起来完成的未完成内容,
|
|
56
|
+
会侵蚀周围一切的信任。
|
|
57
|
+
|
|
58
|
+
## 运行循环
|
|
59
|
+
|
|
60
|
+
Ingest → Synthesize → Evolve → Gate。面向 agent 的方法见 `SKILL.md`;
|
|
61
|
+
冻结文法见 `schema/spec.md`。English version: `docs/method.md`.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trustwiki",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Provenance linter and method for agent-maintained knowledge bases — every claim cited, contradictions surfaced, rot detected.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"trustwiki": "cli/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test test/*.test.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"cli",
|
|
17
|
+
"schema",
|
|
18
|
+
"templates",
|
|
19
|
+
"SKILL.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"README.zh.md",
|
|
22
|
+
"docs/method.md",
|
|
23
|
+
"docs/method.zh.md",
|
|
24
|
+
"proof"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/QianJinGuo/trustwiki.git"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/proof/STATS.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Living stats — the proof vault
|
|
2
|
+
|
|
3
|
+
trustwiki is not a thought experiment: the method runs in production on an
|
|
4
|
+
agent-maintained personal knowledge base. Every number below carries its
|
|
5
|
+
source and verification date — the same discipline this tool enforces.
|
|
6
|
+
|
|
7
|
+
| metric | value | source | verified |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| operating since | 2026-05 | oldest rotation archive `log-2026-05.md` | 2026-09-05 |
|
|
10
|
+
| tracked pages | 8,658 | `scripts/wiki-lint.mjs` (the authoritative counter this linter was generalized from) | 2026-09-05 |
|
|
11
|
+
| raw source pages | 4,162 | file count under `raw/articles/` | 2026-09-05 |
|
|
12
|
+
| lint errors | 0 | same wiki-lint run (92 warnings, exit 0) | 2026-09-05 |
|
|
13
|
+
| provenance warnings surfaced by trustwiki | 15,052 | stricter defaults (inference ratio, orphans) on the same vault — see `scripts/parity-check.mjs` | 2026-09-05 |
|
|
14
|
+
| automated jobs | ingest + quality pipelines on cron; daily public projection | CRON.md | 2026-09-05 |
|
|
15
|
+
|
|
16
|
+
The gap between "0 errors" and "15,052 warnings" is the point: the production
|
|
17
|
+
vault is clean by its own rules, and trustwiki's stricter defaults still find
|
|
18
|
+
rot worth looking at. Diagnosis is a dial, not a verdict.
|
package/schema/spec.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# trustwiki-schema v0.1
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
`trustwiki-schema v0.1` — frozen 2026-09-05. Additions only, never rewrites,
|
|
6
|
+
until v0.2. This schema is implemented by the `trustwiki` linter and the
|
|
7
|
+
`trustwiki` agent skill; other tools MAY implement it.
|
|
8
|
+
|
|
9
|
+
## Citation grammar
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
citation := "^[" source-list "]"
|
|
13
|
+
source-list := source ( ", " source )*
|
|
14
|
+
source := path ( line-ref | anchor-ref )?
|
|
15
|
+
path := vault-relative path; the .md suffix is optional
|
|
16
|
+
line-ref := ":" start "-" end e.g. ^[sources/abc.md:42-58]
|
|
17
|
+
anchor-ref := "#L" start "-L" end e.g. ^[sources/abc.md#L42-L58]
|
|
18
|
+
start, end := positive integers; start ≤ end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Worked examples:
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
Tea brews differently by style.^[sources/tea.md]
|
|
25
|
+
|
|
26
|
+
Steeping times dominate the bitterness outcome.^[sources/tea.md:42-58]
|
|
27
|
+
|
|
28
|
+
Both studies agree on the ratio.^[sources/study-a.md, sources/study-b.md#L3-L4]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Placement rules:
|
|
32
|
+
|
|
33
|
+
- Citations attach to the end of prose paragraphs. Never to headings, list
|
|
34
|
+
items, code blocks, or callouts.
|
|
35
|
+
- Uncited inference is allowed — knowledge synthesis requires it — but the
|
|
36
|
+
share of uncited prose paragraphs on a page must stay under the configured
|
|
37
|
+
threshold (default 0.3), and honest pages mark inference as inference via
|
|
38
|
+
frontmatter (`provenance_state: inferred`).
|
|
39
|
+
- Raw source pages (`type: source`) are exempt from the inference ratio:
|
|
40
|
+
they quote the source, they do not cite themselves.
|
|
41
|
+
|
|
42
|
+
## Frontmatter
|
|
43
|
+
|
|
44
|
+
Required on every page:
|
|
45
|
+
|
|
46
|
+
| field | type | notes |
|
|
47
|
+
|----------|--------|--------------------------------|
|
|
48
|
+
| title | string | human-readable page title |
|
|
49
|
+
| created | date | ISO date, set once |
|
|
50
|
+
| updated | date | ISO date, bumped on every edit |
|
|
51
|
+
| type | string | page type (e.g. note, source, moc) |
|
|
52
|
+
| tags | list | `[a, b]` |
|
|
53
|
+
|
|
54
|
+
Additional required on source pages (`type: source` or pages under the
|
|
55
|
+
configured `sourceDir`):
|
|
56
|
+
|
|
57
|
+
| field | type | notes |
|
|
58
|
+
|------------|--------|-----------------------------------------|
|
|
59
|
+
| source_url | string | original URL |
|
|
60
|
+
| ingested | date | when the source was captured |
|
|
61
|
+
| sha256 | string | hash of the raw body at capture time — this is how silent source edits are detected later |
|
|
62
|
+
|
|
63
|
+
Optional provenance fields:
|
|
64
|
+
|
|
65
|
+
| field | type | notes |
|
|
66
|
+
|------------------|--------|--------------------------------------------------|
|
|
67
|
+
| confidence | float | 0–1; below the floor (default 0.5) is flagged |
|
|
68
|
+
| provenance_state | enum | `extracted \| merged \| inferred \| ambiguous` |
|
|
69
|
+
| contradicted_by | list | slugs of pages that disagree with this one |
|
|
70
|
+
|
|
71
|
+
## Contradiction marking
|
|
72
|
+
|
|
73
|
+
When two pages disagree, the disagreement is surfaced, not resolved away:
|
|
74
|
+
|
|
75
|
+
1. Body callout at the end of the disagreeing page:
|
|
76
|
+
`> [!contradiction] see [[other-page]] which holds the opposite view`
|
|
77
|
+
2. Frontmatter mirror: `contradicted_by: [other-page]`
|
|
78
|
+
|
|
79
|
+
The linter's `provenance.contradicted` rule requires both halves — a callout
|
|
80
|
+
without the frontmatter mirror (or vice versa) is flagged, because
|
|
81
|
+
machines read one and humans read the other.
|
|
82
|
+
|
|
83
|
+
## Checking your vault
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npx trustwiki lint ./your-vault
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Configuration lives in `.trustwiki.json` at the vault root (all keys optional):
|
|
90
|
+
|
|
91
|
+
| key | default | meaning |
|
|
92
|
+
|---------------------|-----------|---------------------------------------------|
|
|
93
|
+
| roots | `["."]` | directories that participate in scanning |
|
|
94
|
+
| index | `null` | index file; enables index-drift rules |
|
|
95
|
+
| sourceDir | `null` | root for citation path resolution |
|
|
96
|
+
| typeByDir | `{}` | expected `type` per top-level directory |
|
|
97
|
+
| minOutboundLinks | `2` | orphan threshold |
|
|
98
|
+
| inferredThreshold | `0.3` | max share of uncited prose paragraphs |
|
|
99
|
+
| confidenceFloor | `0.5` | minimum confidence |
|
|
100
|
+
| inferredSkipTypes | `["source"]` | page types exempt from inference ratio |
|
|
101
|
+
| rules | all on | per-rule `error \| warn \| off` |
|
|
102
|
+
|
|
103
|
+
Rule ids: `frontmatter.required`, `frontmatter.fields`, `placeholder.present`,
|
|
104
|
+
`link.broken`, `link.index-missing`, `link.type-mismatch`, `page.orphan`,
|
|
105
|
+
`citation.malformed`, `citation.target-missing`,
|
|
106
|
+
`provenance.excess-inferred`, `provenance.low-confidence`,
|
|
107
|
+
`provenance.contradicted`.
|
|
108
|
+
|
|
109
|
+
Exit codes: `0` clean (warnings allowed), `1` at least one error,
|
|
110
|
+
`2` configuration or usage error.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# trustwiki-schema v0.1(中文版)
|
|
2
|
+
|
|
3
|
+
## 状态
|
|
4
|
+
|
|
5
|
+
`trustwiki-schema v0.1`——2026-09-05 冻结。v0.2 之前只增不改。本规范由
|
|
6
|
+
`trustwiki` linter 与同名 agent skill 实现,其他工具亦可实现。
|
|
7
|
+
|
|
8
|
+
## 引用语法
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
citation := "^[" source-list "]"
|
|
12
|
+
source-list := source ( ", " source )*
|
|
13
|
+
source := path ( line-ref | anchor-ref )?
|
|
14
|
+
path := vault 内相对路径;.md 后缀可省略
|
|
15
|
+
line-ref := ":" start "-" end 例:^[sources/abc.md:42-58]
|
|
16
|
+
anchor-ref := "#L" start "-L" end 例:^[sources/abc.md#L42-L58]
|
|
17
|
+
start, end := 正整数;start ≤ end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
示例:
|
|
21
|
+
|
|
22
|
+
```markdown
|
|
23
|
+
不同茶类冲泡方式不同。^[sources/tea.md]
|
|
24
|
+
|
|
25
|
+
浸泡时间是苦涩的主导因素。^[sources/tea.md:42-58]
|
|
26
|
+
|
|
27
|
+
两项研究对比率结论一致。^[sources/study-a.md, sources/study-b.md#L3-L4]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
放置规则:
|
|
31
|
+
|
|
32
|
+
- 引用挂在散文段落末尾;不得挂在标题、列表项、代码块或 callout 上。
|
|
33
|
+
- 允许无引用的推断——知识合成离不开推断——但单页未引用散文段落占比
|
|
34
|
+
不得超过阈值(默认 0.3),且诚实页面用 frontmatter 标注推断
|
|
35
|
+
(`provenance_state: inferred`)。
|
|
36
|
+
- 原始来源页(`type: source`)豁免推断占比:它们引用来源本身,
|
|
37
|
+
不需要引用自己。
|
|
38
|
+
|
|
39
|
+
## Frontmatter
|
|
40
|
+
|
|
41
|
+
每页必填:
|
|
42
|
+
|
|
43
|
+
| 字段 | 类型 | 说明 |
|
|
44
|
+
|----------|--------|---------------------------------|
|
|
45
|
+
| title | string | 页面标题 |
|
|
46
|
+
| created | date | ISO 日期,创建时定死 |
|
|
47
|
+
| updated | date | ISO 日期,每次编辑必须 bump |
|
|
48
|
+
| type | string | 页面类型(note、source、moc 等)|
|
|
49
|
+
| tags | list | `[a, b]` |
|
|
50
|
+
|
|
51
|
+
来源页(`type: source` 或位于 `sourceDir` 下)额外必填:
|
|
52
|
+
|
|
53
|
+
| 字段 | 类型 | 说明 |
|
|
54
|
+
|------------|--------|-----------------------------------------|
|
|
55
|
+
| source_url | string | 原始 URL |
|
|
56
|
+
| ingested | date | 抓取时间 |
|
|
57
|
+
| sha256 | string | 抓取时正文哈希——日后借此发现来源被静默篡改 |
|
|
58
|
+
|
|
59
|
+
可选 provenance 字段:
|
|
60
|
+
|
|
61
|
+
| 字段 | 类型 | 说明 |
|
|
62
|
+
|------------------|--------|-----------------------------------------------|
|
|
63
|
+
| confidence | float | 0–1;低于下限(默认 0.5)会被标记 |
|
|
64
|
+
| provenance_state | enum | `extracted \| merged \| inferred \| ambiguous`|
|
|
65
|
+
| contradicted_by | list | 与本页结论相左的页面 slug 列表 |
|
|
66
|
+
|
|
67
|
+
## 矛盾标记
|
|
68
|
+
|
|
69
|
+
两页观点冲突时,冲突必须显性化,不允许悄悄改写:
|
|
70
|
+
|
|
71
|
+
1. 正文 callout:`> [!contradiction] 参见 [[other-page]] 持相反观点`
|
|
72
|
+
2. Frontmatter 镜像:`contradicted_by: [other-page]`
|
|
73
|
+
|
|
74
|
+
linter 的 `provenance.contradicted` 规则要求两处齐备——只有 callout 或只有
|
|
75
|
+
frontmatter 都会被标记,因为机器读后者而人读前者。
|
|
76
|
+
|
|
77
|
+
## 检查你的知识库
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx trustwiki lint ./your-vault
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
配置位于 vault 根目录 `.trustwiki.json`(所有键可选):
|
|
84
|
+
|
|
85
|
+
| 键 | 默认值 | 含义 |
|
|
86
|
+
|---------------------|----------|----------------------------------------|
|
|
87
|
+
| roots | `["."]` | 参与扫描的目录 |
|
|
88
|
+
| index | `null` | 索引文件;声明后启用索引漂移规则 |
|
|
89
|
+
| sourceDir | `null` | 引用路径解析根 |
|
|
90
|
+
| typeByDir | `{}` | 顶层目录对应的期望 type |
|
|
91
|
+
| minOutboundLinks | `2` | 孤页阈值 |
|
|
92
|
+
| inferredThreshold | `0.3` | 未引用散文段落占比上限 |
|
|
93
|
+
| confidenceFloor | `0.5` | 最低置信度 |
|
|
94
|
+
| inferredSkipTypes | `["source"]` | 豁免推断占比的页面类型 |
|
|
95
|
+
| rules | 全开 | 逐规则 `error \| warn \| off` |
|
|
96
|
+
|
|
97
|
+
规则 id:`frontmatter.required`、`frontmatter.fields`、`placeholder.present`、
|
|
98
|
+
`link.broken`、`link.index-missing`、`link.type-mismatch`、`page.orphan`、
|
|
99
|
+
`citation.malformed`、`citation.target-missing`、
|
|
100
|
+
`provenance.excess-inferred`、`provenance.low-confidence`、
|
|
101
|
+
`provenance.contradicted`。
|
|
102
|
+
|
|
103
|
+
退出码:`0` 干净(允许警告)、`1` 存在 error、`2` 配置或用法错误。
|