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.
Files changed (42) hide show
  1. package/README.md +87 -0
  2. package/README.zh.md +82 -0
  3. package/SKILL.md +76 -0
  4. package/cli/bin.js +29 -0
  5. package/cli/citations.js +45 -0
  6. package/cli/config.js +52 -0
  7. package/cli/engine.js +99 -0
  8. package/cli/frontmatter.js +33 -0
  9. package/cli/links.js +12 -0
  10. package/cli/report.js +24 -0
  11. package/cli/resolve.js +25 -0
  12. package/cli/rules/citation-malformed.js +12 -0
  13. package/cli/rules/citation-target-missing.js +21 -0
  14. package/cli/rules/frontmatter-fields.js +39 -0
  15. package/cli/rules/frontmatter-required.js +12 -0
  16. package/cli/rules/index.js +19 -0
  17. package/cli/rules/link-broken.js +22 -0
  18. package/cli/rules/link-index-missing.js +30 -0
  19. package/cli/rules/link-type-mismatch.js +16 -0
  20. package/cli/rules/page-orphan.js +14 -0
  21. package/cli/rules/placeholder-present.js +17 -0
  22. package/cli/rules/provenance-contradicted.js +31 -0
  23. package/cli/rules/provenance-excess-inferred.js +21 -0
  24. package/cli/rules/provenance-low-confidence.js +15 -0
  25. package/cli/walk.js +19 -0
  26. package/docs/method.md +69 -0
  27. package/docs/method.zh.md +61 -0
  28. package/package.json +31 -0
  29. package/proof/STATS.md +18 -0
  30. package/schema/spec.md +110 -0
  31. package/schema/spec.zh.md +103 -0
  32. package/templates/demo-vault/.trustwiki.json +6 -0
  33. package/templates/demo-vault/index.md +7 -0
  34. package/templates/demo-vault/notes/conflict-a.md +13 -0
  35. package/templates/demo-vault/notes/conflict-b.md +12 -0
  36. package/templates/demo-vault/notes/honest-page.md +13 -0
  37. package/templates/demo-vault/notes/sloppy-page.md +20 -0
  38. package/templates/demo-vault/sources/tea.md +12 -0
  39. package/templates/starter-vault/.trustwiki.json +5 -0
  40. package/templates/starter-vault/index.md +4 -0
  41. package/templates/starter-vault/notes/first-note.md +11 -0
  42. 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` 配置或用法错误。
@@ -0,0 +1,6 @@
1
+ {
2
+ "roots": ["notes", "sources"],
3
+ "index": "index.md",
4
+ "sourceDir": "sources",
5
+ "rules": { "page.orphan": "off" }
6
+ }
@@ -0,0 +1,7 @@
1
+ # Demo vault
2
+
3
+ - [[notes/honest-page]] — cited example
4
+ - [[notes/sloppy-page]] — the patient
5
+ - [[notes/conflict-a]] — contradiction side A
6
+ - [[notes/conflict-b]] — contradiction side B
7
+ - [[sources/tea]] — raw source
@@ -0,0 +1,13 @@
1
+ ---
2
+ title: Conflict A
3
+ created: 2026-09-05
4
+ updated: 2026-09-05
5
+ type: note
6
+ tags: [demo]
7
+ ---
8
+
9
+ # Conflict A
10
+
11
+ Tea should be brewed at 100°C.^[sources/tea.md]
12
+
13
+ > [!contradiction] 参见 [[notes/conflict-b]] 持相反观点
@@ -0,0 +1,12 @@
1
+ ---
2
+ title: Conflict B
3
+ created: 2026-09-05
4
+ updated: 2026-09-05
5
+ type: note
6
+ tags: [demo]
7
+ contradicted_by: [notes/conflict-a]
8
+ ---
9
+
10
+ # Conflict B
11
+
12
+ Tea should be brewed at 80°C.^[sources/tea.md]