@rahmantech/knl-view 0.0.0-stage → 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.
Files changed (115) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +398 -2
  3. package/bin/knl-view.js +943 -0
  4. package/dist/assets/abnfDiagram-WEYOIA3C-B1gaBaSI.js +1 -0
  5. package/dist/assets/arc-NZ9uRaCl.js +1 -0
  6. package/dist/assets/architecture-WOLXFQ4H-8MalLwTd.js +1 -0
  7. package/dist/assets/architectureDiagram-ZHVYKT53-eZ8i5cl4.js +36 -0
  8. package/dist/assets/array-BifhSqXX.js +1 -0
  9. package/dist/assets/blockDiagram-RJAE7QA3-BavOE8BM.js +140 -0
  10. package/dist/assets/c4Diagram-XDPPPE54-DqMaTAzx.js +38 -0
  11. package/dist/assets/channel-BZ2_TTTJ.js +1 -0
  12. package/dist/assets/chunk-2E7WHVMD-B1Hj2BjJ.js +88 -0
  13. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  14. package/dist/assets/chunk-4EMNKR2T-HDscrCiq.js +1 -0
  15. package/dist/assets/chunk-55XCU76K-QR_ARj8I.js +1 -0
  16. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  17. package/dist/assets/chunk-7S6XSTFO-B0p3o547.js +2 -0
  18. package/dist/assets/chunk-AQDTSKZB-CCxwNyyA.js +72 -0
  19. package/dist/assets/chunk-DSNLSHJZ-BM5PBFmz.js +129 -0
  20. package/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  21. package/dist/assets/chunk-HAGFOTED-UCDVFuN_.js +62 -0
  22. package/dist/assets/chunk-HNE4Q36P-C_1sakp2.js +1 -0
  23. package/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  24. package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  25. package/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  26. package/dist/assets/chunk-NQOKRVIF-b2D1Z4oK.js +213 -0
  27. package/dist/assets/chunk-UHW7PFBP-CJs0VwBY.js +1 -0
  28. package/dist/assets/chunk-VQ5W62QI-DQxjgE4_.js +1 -0
  29. package/dist/assets/chunk-WQQL4CDK-DcjPeAfQ.js +10 -0
  30. package/dist/assets/chunk-XXDRQBXY-glA0kB5s.js +1 -0
  31. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  32. package/dist/assets/chunk-YZO67HFG-DJqACL5V.js +1 -0
  33. package/dist/assets/chunk-ZAUX2LTX-CWfaYtwb.js +1 -0
  34. package/dist/assets/chunk-ZCHMQSOR-BGX73Hho.js +2 -0
  35. package/dist/assets/classDiagram-v2-4MYWR3UA-BpYXPz-e.js +217 -0
  36. package/dist/assets/cose-bilkent-JH36ORCC-e1quKm0Q.js +1 -0
  37. package/dist/assets/cynefin-EF2NZ3EQ-hb3oUmFO.js +1 -0
  38. package/dist/assets/cynefinDiagram-WF24UDRP-CnMQvLqi.js +62 -0
  39. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  40. package/dist/assets/dagre-NK4GIZVX-BH_Y8y3F.js +4 -0
  41. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  42. package/dist/assets/diagram-CBK3MWTC-Dh79VetZ.js +24 -0
  43. package/dist/assets/diagram-I5XIIV2D-D3u-o1RV.js +24 -0
  44. package/dist/assets/diagram-ODIL2UCW-Bfuo-OKS.js +30 -0
  45. package/dist/assets/diagram-PFBEJUXE-CwHasIbs.js +3 -0
  46. package/dist/assets/diagram-RJA3I4VV-D8ttmuE8.js +41 -0
  47. package/dist/assets/diagram-TYUNFSNR-CzC-8RZ0.js +200 -0
  48. package/dist/assets/dist-CUJYGnnI.js +1 -0
  49. package/dist/assets/ebnfDiagram-V4P3TTPT-9AAhU7In.js +1 -0
  50. package/dist/assets/elk-QONTBCTQ-BgvnD1v_.js +27 -0
  51. package/dist/assets/erDiagram-KYQIGQAA-RLtbiLlP.js +99 -0
  52. package/dist/assets/eventmodeling-K75KTNOO-DVY22vNZ.js +1 -0
  53. package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +1 -0
  54. package/dist/assets/ganttDiagram-R7GPNTJY-hHiBGa9W.js +292 -0
  55. package/dist/assets/gitGraph-VSP46ZUC-Cvxay1wh.js +1 -0
  56. package/dist/assets/gitGraphDiagram-PDYL7XHY-AOhlpSqg.js +106 -0
  57. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  58. package/dist/assets/index-CBLNQvK3.js +76 -0
  59. package/dist/assets/index-DLgMsUs2.css +1 -0
  60. package/dist/assets/info-OHQRW6UA-CJ8EUO1m.js +1 -0
  61. package/dist/assets/infoDiagram-MVDRKAX5-DOQu7oYI.js +2 -0
  62. package/dist/assets/init-C-OQMol4.js +1 -0
  63. package/dist/assets/ishikawaDiagram-HPEHV2GE-DcVMvGmx.js +70 -0
  64. package/dist/assets/journeyDiagram-BOSHW7O7-luMeqEqz.js +139 -0
  65. package/dist/assets/kanban-definition-FNSA4HC5-KTZbmrW2.js +89 -0
  66. package/dist/assets/katex-B_JcIwqj.js +257 -0
  67. package/dist/assets/line-CmPHiOVm.js +1 -0
  68. package/dist/assets/linear-DHYlKXBA.js +1 -0
  69. package/dist/assets/mermaid-parser.core-BRDI-WhC.js +7 -0
  70. package/dist/assets/mermaid.core-DPTRaYLy.js +44 -0
  71. package/dist/assets/mindmap-definition-6OY57UG3-70EN5Kw_.js +98 -0
  72. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  73. package/dist/assets/packet-JDAUHWVQ-CXWz83oS.js +1 -0
  74. package/dist/assets/path-fybaL0A-.js +1 -0
  75. package/dist/assets/pegDiagram-ECIUJNBX-z5UXKmp4.js +1 -0
  76. package/dist/assets/pie-XZMESJXO-rWI1HIQ7.js +1 -0
  77. package/dist/assets/pieDiagram-GNMPY74T-Bh4Kv3NQ.js +39 -0
  78. package/dist/assets/quadrantDiagram-D57K4MC3-wGM4Ckig.js +7 -0
  79. package/dist/assets/radar-ABXABTNO-CIJ92p5N.js +1 -0
  80. package/dist/assets/railroad-I3PHUGI6-8bxua-Hf.js +1 -0
  81. package/dist/assets/railroad-abnf-LNEFI6M7-B9KVI10m.js +1 -0
  82. package/dist/assets/railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js +1 -0
  83. package/dist/assets/railroad-peg-JR7BVNR7-Cie82UPT.js +1 -0
  84. package/dist/assets/railroadDiagram-Y72NRBL7-C1nmi_tl.js +1 -0
  85. package/dist/assets/requirementDiagram-EQYSW5RA-BSianW40.js +84 -0
  86. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  87. package/dist/assets/sankeyDiagram-SY24JHO5-DIa11-h7.js +40 -0
  88. package/dist/assets/sequenceDiagram-BLEHNHQU-Df5rZKz2.js +169 -0
  89. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  90. package/dist/assets/src-dClg85lX.js +1 -0
  91. package/dist/assets/stateDiagram-v2-A6IESKWZ-B15PTSvX.js +291 -0
  92. package/dist/assets/swimlanes-B3ENRKFQ-C9mgRCMi.js +1 -0
  93. package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +8 -0
  94. package/dist/assets/timeline-definition-JXYKGLGB-BDVf_mIv.js +120 -0
  95. package/dist/assets/treeView-D4JQ5SDB-BbBK-25J.js +1 -0
  96. package/dist/assets/treemap-SAJKECNS-CEDX8apk.js +1 -0
  97. package/dist/assets/usecaseDiagram-WPOLBNNX-D1QkwFF8.js +328 -0
  98. package/dist/assets/vennDiagram-Q3Y3IDCW-5PeGZvEO.js +34 -0
  99. package/dist/assets/wardley-7MLQ67FV-BatN94pO.js +1 -0
  100. package/dist/assets/wardleyDiagram-IVAIDKWA-DyCcDS3Y.js +78 -0
  101. package/dist/assets/xychartDiagram-4URNHDU4-CbCXcRlg.js +7 -0
  102. package/dist/index.html +22 -0
  103. package/package.json +46 -4
  104. package/server/config.js +263 -0
  105. package/server/consensus.js +175 -0
  106. package/server/graph/anchordb_client.js +253 -0
  107. package/server/graph/model.js +157 -0
  108. package/server/graph/parser.js +326 -0
  109. package/server/graph/sources.js +660 -0
  110. package/server/graph/syntax.js +257 -0
  111. package/server/index.js +679 -0
  112. package/server/install.js +97 -0
  113. package/server/map.js +126 -0
  114. package/server/models_registry.js +84 -0
  115. package/server/scaffold.js +376 -0
@@ -0,0 +1,97 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+
5
+ // AnchorDB installer for knl-view. Two paths:
6
+ // - Linux: binary from GitHub Releases (tar.gz per architecture, verified
7
+ // against the release's official SHA256SUMS.txt).
8
+ // - Non-Linux: AnchorDB needs Linux mmap, so the path is the
9
+ // ghcr.io/rachmanzz/anchordb:<version> container via docker/podman.
10
+ // Everything here is as pure/deterministic as possible; interactive
11
+ // orchestration (prompts, spawn) stays in bin/knl-view.js.
12
+
13
+ export const GHCR_IMAGE = 'ghcr.io/rachmanzz/anchordb'
14
+ export const HTTP_PORT_DEFAULT = 7447
15
+ export const WIRE_PORT_DEFAULT = 7448
16
+ export const CONTAINER_NAME = 'anchordb-knlview'
17
+
18
+ /** Release asset name for a platform/architecture, per the AnchorDB release pattern. */
19
+ export function releaseAssetName(version, platform = process.platform, arch = process.arch) {
20
+ const archName = arch === 'arm64' ? 'arm64' : 'amd64'
21
+ const platformName = platform === 'darwin' ? 'darwin' : 'linux'
22
+ return `anchordb_${version}_${platformName}_${archName}.tar.gz`
23
+ }
24
+
25
+ /** Random token for ANCHORDB_AUTH_TOKEN: 32 bytes of hex. */
26
+ export function generateToken() {
27
+ return crypto.randomBytes(32).toString('hex')
28
+ }
29
+
30
+ /** Fetch the latest release from the GitHub API. */
31
+ export async function latestRelease(repo = 'rachmanzz/anchordb') {
32
+ const res = await fetch(`https://api.github.com/repos/${repo}/releases/latest`)
33
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: failed to read the latest release`)
34
+ const body = await res.json()
35
+ if (!body.tag_name) throw new Error('Latest release has no tag_name')
36
+ const version = String(body.tag_name).replace(/^v/, '')
37
+ return { tag: body.tag_name, version }
38
+ }
39
+
40
+ /** Download one file to disk. */
41
+ export async function downloadFile(url, dest) {
42
+ const res = await fetch(url, { redirect: 'follow' })
43
+ if (!res.ok) throw new Error(`Download failed (${res.status}): ${url}`)
44
+ const buf = Buffer.from(await res.arrayBuffer())
45
+ await fs.writeFile(dest, buf)
46
+ return buf.length
47
+ }
48
+
49
+ /**
50
+ * Verify a file against SHA256SUMS.txt contents in the standard format:
51
+ * "<hash> <name>" per line. Throws an Error when the name is missing or
52
+ * the hash does not match.
53
+ */
54
+ export async function verifySha256(filePath, sumsText) {
55
+ const fileName = path.basename(filePath)
56
+ const line = String(sumsText)
57
+ .split('\n')
58
+ .find((l) => l.trim().endsWith(fileName))
59
+ if (!line) throw new Error(`${fileName} is not in SHA256SUMS.txt`)
60
+ const expected = line.trim().split(/\s+/)[0].toLowerCase()
61
+ const actual = crypto.createHash('sha256').update(await fs.readFile(filePath)).digest('hex')
62
+ if (actual !== expected) {
63
+ throw new Error(`SHA256 mismatch for ${fileName}\n wanted ${expected}\n got ${actual}`)
64
+ }
65
+ return expected
66
+ }
67
+
68
+ /**
69
+ * Merge the anchordb block into the global config (~/.knlview/knl-config.json)
70
+ * without dropping other fields that may live there. Returns the config
71
+ * contents before the overwrite (as a backup, in case the caller shows it).
72
+ */
73
+ export async function mergeGlobalAnchordb(home, anchordb) {
74
+ const file = path.join(home, '.knlview', 'knl-config.json')
75
+ let before = null
76
+ let raw = {}
77
+ try {
78
+ const text = await fs.readFile(file, 'utf8')
79
+ before = text
80
+ const parsed = JSON.parse(text)
81
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) raw = parsed
82
+ } catch {
83
+ // Missing/broken file: start from an empty object.
84
+ }
85
+ raw.anchordb = anchordb
86
+ await fs.mkdir(path.dirname(file), { recursive: true })
87
+ await fs.writeFile(file, JSON.stringify(raw, null, 2) + '\n', 'utf8')
88
+ return before
89
+ }
90
+
91
+ /** Pick an available container runtime: podman first, then docker. */
92
+ export async function detectContainerRuntime(which) {
93
+ for (const runtime of ['podman', 'docker']) {
94
+ if (await which(runtime)) return runtime
95
+ }
96
+ return null
97
+ }
package/server/map.js ADDED
@@ -0,0 +1,126 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // The optional map (map.json): pure annotation on top of the disk structure.
5
+ // Principle: the disk is the truth, the map is an overlay. The map NEVER
6
+ // defines the existence of files/folders. A file missing from disk stays
7
+ // missing from the viewer even when the map still lists it (orphan entries
8
+ // are silently ignored). The map only decorates: title, order, metadata.
9
+
10
+ const MAP_FILE = 'map.json'
11
+ const MAX_MAP_BYTES = 1024 * 1024
12
+
13
+ export async function readMap(rootAbs) {
14
+ const abs = path.join(rootAbs, MAP_FILE)
15
+ const stat = await statOrNull(abs)
16
+ if (!stat || !stat.isFile() || stat.size > MAX_MAP_BYTES) return null
17
+
18
+ let parsed
19
+ try {
20
+ parsed = JSON.parse(await fs.readFile(abs, 'utf8'))
21
+ } catch {
22
+ // A broken map must never bring the viewer down: ignore it quietly.
23
+ return null
24
+ }
25
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
26
+ return parsed
27
+ }
28
+
29
+ // Normalize shapes: the map may write the title in `title` (root/files),
30
+ // `sections`/`dirs` (folders), and `docs`/`files` (files). Only one clean
31
+ // representation matters for the overlay. Each entry may carry
32
+ // `ignore: true` to hide it (the disk still owns the file).
33
+ export function normalizeMap(map) {
34
+ if (!map) return { title: null, entries: new Map() }
35
+ const entries = new Map()
36
+
37
+ const dirs = { ...asObject(map.sections), ...asObject(map.dirs) }
38
+ for (const [key, value] of Object.entries(dirs)) {
39
+ const entry = asObject(value)
40
+ if (!entry) continue
41
+ mergeEntry(entries, cleanKey(key), entry)
42
+ }
43
+
44
+ // `docs`/`files` can be placed per folder:
45
+ // { "features/x": { "docs": { "a.md": {...} } } } or globally.
46
+ // Both are written into the same file entries.
47
+ const globalDocs = { ...asObject(map.docs), ...asObject(map.files) }
48
+ applyDocs(entries, '', globalDocs)
49
+ for (const [key, value] of Object.entries(dirs)) {
50
+ const docs = asObject(asObject(value)?.docs)
51
+ if (docs) applyDocs(entries, cleanKey(key), docs)
52
+ }
53
+
54
+ const ignoreList = Array.isArray(map.ignore) ? map.ignore : []
55
+ for (const path of ignoreList) {
56
+ if (typeof path !== 'string' || !path.trim()) continue
57
+ mergeEntry(entries, cleanKey(path), { ignore: true })
58
+ }
59
+
60
+ return {
61
+ title: typeof map.title === 'string' ? map.title.trim() : null,
62
+ entries
63
+ }
64
+ }
65
+
66
+ function applyDocs(entries, dirKey, docs) {
67
+ for (const [name, value] of Object.entries(docs)) {
68
+ const entry = asObject(value) || {}
69
+ const key = dirKey ? `${dirKey}/${name}` : name
70
+ mergeEntry(entries, cleanKey(key), entry)
71
+ }
72
+ }
73
+
74
+ function mergeEntry(entries, key, entry) {
75
+ const prev = entries.get(key) || {}
76
+ entries.set(key, {
77
+ title: typeof entry.title === 'string' ? entry.title.trim() : prev.title ?? null,
78
+ order: toOrder(entry.order) ?? prev.order ?? null,
79
+ meta: typeof entry.meta === 'string' ? entry.meta.trim() : prev.meta ?? null,
80
+ ignore: entry.ignore === true || entry.hidden === true ? true : prev.ignore === true
81
+ })
82
+ }
83
+
84
+ // Apply the overlay to one node. Returns a new node (without corrupting the
85
+ // original object from disk), or the same node when the map does not touch it.
86
+ export function decorateNode(node, norm) {
87
+ const entry = norm?.entries?.get(node.path)
88
+ let next = node
89
+
90
+ const title = entry?.title
91
+ if (title) {
92
+ // A map title beats the heading title read from the file contents.
93
+ // `titleFromMap` marks that this label came from the map, so the view
94
+ // may use it directly as the label (not just as a subtitle).
95
+ next = { ...next, title, titleFromMap: true }
96
+ }
97
+ const order = entry?.order
98
+ if (order != null) next = { ...next, order }
99
+ const meta = entry?.meta
100
+ if (meta != null) next = { ...next, meta }
101
+
102
+ // Children are already decorated by buildTree while walking the disk,
103
+ // so here it is enough to touch this node only.
104
+ return next
105
+ }
106
+
107
+ function asObject(value) {
108
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null
109
+ }
110
+
111
+ function cleanKey(key) {
112
+ return String(key).replace(/^\.?\//, '').replace(/\/+$/, '')
113
+ }
114
+
115
+ function toOrder(value) {
116
+ const n = Number(value)
117
+ return Number.isFinite(n) ? n : null
118
+ }
119
+
120
+ async function statOrNull(abs) {
121
+ try {
122
+ return await fs.stat(abs)
123
+ } catch {
124
+ return null
125
+ }
126
+ }
@@ -0,0 +1,84 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import crypto from 'node:crypto'
4
+
5
+ // Global registry of AnchorDB model names: ~/.knlview/models.json.
6
+ //
7
+ // Model names may be random; what makes a random name meaningful is this
8
+ // registry: the mapping of name → project, role, and project location.
9
+ // knl-view code never guesses model names: it only knows roles (knowledges,
10
+ // consensus, etc.) and reads each role from the project map.
11
+
12
+ const REGISTRY_FILE = 'models.json'
13
+
14
+ export function modelRegistryPath(home = process.env.HOME) {
15
+ return path.join(home || '', '.knlview', REGISTRY_FILE)
16
+ }
17
+
18
+ /**
19
+ * Random name for a model: 8 Crockford base32 characters (no I, L, O, U
20
+ * so humans never misread/mistype them).
21
+ */
22
+ export function generateModelName() {
23
+ const alphabet = '0123456789abcdefghjkmnpqrstvwxyz'
24
+ const bytes = crypto.randomBytes(8)
25
+ let name = ''
26
+ for (const byte of bytes) name += alphabet[byte % alphabet.length]
27
+ return name
28
+ }
29
+
30
+ /**
31
+ * Read the registry. Missing/broken file = empty registry, not an error.
32
+ * @returns {Promise<{models: object, error: string|null}>}
33
+ */
34
+ export async function readModelRegistry(home = process.env.HOME) {
35
+ try {
36
+ const parsed = JSON.parse(await fs.readFile(modelRegistryPath(home), 'utf8'))
37
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
38
+ return { models: {}, error: 'registry contents are not an object' }
39
+ }
40
+ const models = {}
41
+ for (const [name, entry] of Object.entries(parsed)) {
42
+ if (entry && typeof entry === 'object' && typeof entry.role === 'string') {
43
+ models[name] = entry
44
+ }
45
+ }
46
+ return { models, error: null }
47
+ } catch (err) {
48
+ if (err?.code === 'ENOENT') return { models: {}, error: null }
49
+ return { models: {}, error: err.message }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Register (or update) one model in the registry. Returns the previous owner
55
+ * when the name is already claimed by another project; the caller decides
56
+ * whether to warn.
57
+ * @returns {Promise<{previous: object|null, written: boolean, error: string|null}>}
58
+ */
59
+ export async function registerModel(name, { role, project, cwd }, home = process.env.HOME) {
60
+ const { models, error } = await readModelRegistry(home)
61
+ if (error) return { previous: null, written: false, error }
62
+
63
+ const previous = models[name] || null
64
+ const sameOwner = previous && path.resolve(previous.cwd || '') === path.resolve(cwd)
65
+ if (previous && !sameOwner) {
66
+ return { previous, written: false, error: null }
67
+ }
68
+
69
+ models[name] = {
70
+ role,
71
+ project,
72
+ cwd: path.resolve(cwd),
73
+ registeredAt: new Date().toISOString()
74
+ }
75
+
76
+ try {
77
+ const file = modelRegistryPath(home)
78
+ await fs.mkdir(path.dirname(file), { recursive: true })
79
+ await fs.writeFile(file, JSON.stringify(models, null, 2) + '\n', 'utf8')
80
+ return { previous: null, written: true, error: null }
81
+ } catch (err) {
82
+ return { previous: null, written: false, error: err.message }
83
+ }
84
+ }
@@ -0,0 +1,376 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+
4
+ // Knowledge base builder for --init and --seed-skill.
5
+ // Same principle as the map: NEVER destroy anything that already exists.
6
+ // Existing files are skipped, and the .gitignore line is only added when
7
+ // not already present.
8
+
9
+ export const STANDARD_DIRS = ['roadmap', 'features', 'docs']
10
+ export const SKILL_ID = 'knl-view'
11
+
12
+ const ROOT_README = `# Knowledge Base
13
+
14
+ Lede: write a one-paragraph summary of this base here.
15
+
16
+ ## How to read
17
+
18
+ - \`roadmap/\`: direction and plans.
19
+ - \`features/\`: feature notes.
20
+ - \`docs/\`: general documents.
21
+
22
+ Add \`map.json\` to this folder if you want to control menu labels and
23
+ display order without moving files. See the \`knl-view\` skill for the
24
+ full format.
25
+ `
26
+
27
+ const SECTION_README = (name) => `# ${name}
28
+
29
+ Nothing here yet. Put markdown files in this folder.
30
+ `
31
+
32
+ export async function scaffoldBase({ root, structure, folders = [] }) {
33
+ const actions = []
34
+ await fs.mkdir(root, { recursive: true })
35
+ actions.push({ kind: 'dir', path: root, status: 'created' })
36
+
37
+ if (structure === 'empty') return actions
38
+
39
+ const dirs = structure === 'custom' ? folders.map(cleanRel).filter(Boolean) : STANDARD_DIRS
40
+
41
+ await writeFileSafe(path.join(root, 'README.md'), ROOT_README, actions)
42
+ for (const dir of dirs) {
43
+ const abs = path.join(root, dir)
44
+ await fs.mkdir(abs, { recursive: true })
45
+ actions.push({ kind: 'dir', path: abs, status: 'created' })
46
+ await writeFileSafe(path.join(abs, 'README.md'), SECTION_README(dir), actions)
47
+ }
48
+ return actions
49
+ }
50
+
51
+ export async function seedSkill({ scope, cwd, home = process.env.HOME, xdgConfig = process.env.XDG_CONFIG_HOME }) {
52
+ const base =
53
+ scope === 'global'
54
+ ? path.join(xdgConfig || path.join(home, '.config'), 'opencode', 'skills')
55
+ : path.join(cwd, '.agents', 'skills')
56
+ const target = path.join(base, SKILL_ID, 'SKILL.md')
57
+
58
+ const exists = await statOrNull(target)
59
+ if (exists) return { path: target, status: 'skipped' }
60
+
61
+ await fs.mkdir(path.dirname(target), { recursive: true })
62
+ await fs.writeFile(target, SKILL_MD, 'utf8')
63
+ return { path: target, status: 'created' }
64
+ }
65
+
66
+ export async function ensureGitignore({ cwd }) {
67
+ const abs = path.join(cwd, '.gitignore')
68
+ const line = '.knowledges/'
69
+ let content = ''
70
+ const stat = await statOrNull(abs)
71
+ if (stat && stat.isFile()) content = await fs.readFile(abs, 'utf8')
72
+
73
+ const present = content.split(/\r?\n/).some((row) => row.trim() === line || row.trim() === '.knowledges')
74
+ if (present) return { path: abs, status: 'skipped' }
75
+
76
+ const next = content.length && !content.endsWith('\n') ? `${content}\n${line}\n` : `${content}${line}\n`
77
+ await fs.writeFile(abs, next, 'utf8')
78
+ return { path: abs, status: 'created' }
79
+ }
80
+
81
+ async function writeFileSafe(abs, content, actions) {
82
+ const exists = await statOrNull(abs)
83
+ if (exists) {
84
+ actions.push({ kind: 'file', path: abs, status: 'skipped' })
85
+ return
86
+ }
87
+ await fs.writeFile(abs, content, 'utf8')
88
+ actions.push({ kind: 'file', path: abs, status: 'created' })
89
+ }
90
+
91
+ function cleanRel(value) {
92
+ return String(value).trim().replace(/^\/+/, '').replace(/\/+$/, '')
93
+ }
94
+
95
+ async function statOrNull(abs) {
96
+ try {
97
+ return await fs.stat(abs)
98
+ } catch {
99
+ return null
100
+ }
101
+ }
102
+
103
+ // The skill is in English, following skill ecosystem conventions. It teaches
104
+ // the agent how to read the .knowledges base: README first, then map.json as
105
+ // the source of labels and menu order, then the anchor-connector-like
106
+ // structure.
107
+ const SKILL_MD = `---
108
+ name: knl-view
109
+ description: Read and maintain .knowledges knowledge bases. Use whenever a task involves a .knowledges folder, its map.json, or the knl-view viewer.
110
+ ---
111
+
112
+ # Knowledges base
113
+
114
+ A \`.knowledges\` folder is a plain-directory knowledge base, viewed with
115
+ knl-view. The disk is the source of truth: files and folders exist because
116
+ they exist on disk, never because a map says so.
117
+
118
+ ## Read in this order
119
+
120
+ 1. \`README.md\` at the base root: the lede and the overview.
121
+ 2. \`map.json\` at the base root, when present: the authoritative menu
122
+ labels, grouping, and order for everything below.
123
+ 3. Then the structure the map (or the disk) describes. A typical base
124
+ mirrors anchor-connector: \`roadmap/\`, \`features/\`, \`futures/\`,
125
+ \`audit/\`, \`roasted/\`, plus root files \`coding-rules.md\` and
126
+ \`documentation-rules.md\`. Anything not on disk does not exist; do not
127
+ invent it from map entries.
128
+
129
+ ## map.json
130
+
131
+ Optional. Every entry is decoration only. It sets the label shown in the
132
+ menu, the order in menus, and a short note. It never creates, moves, or
133
+ deletes files. Entries pointing at paths that are not on disk are ignored
134
+ silently. Without \`map.json\`, the viewer falls back to the first
135
+ \`# heading\` of each markdown file and plain name order: files first,
136
+ then folders.
137
+
138
+ \`\`\`json
139
+ {
140
+ "title": "Base title shown in the masthead",
141
+ "sections": {
142
+ "roadmap": { "title": "Roadmap", "order": 1, "meta": "Where this is going" },
143
+ "features": {
144
+ "title": "Features",
145
+ "order": 2,
146
+ "docs": { "auth.md": { "title": "Auth", "order": 1 } }
147
+ }
148
+ },
149
+ "docs": {
150
+ "coding-rules.md": { "title": "Coding rules", "order": 1 },
151
+ "documentation-rules.md": { "title": "Documentation rules", "order": 2 }
152
+ },
153
+ "files": {
154
+ "personal-assistant.md": { "title": "Personal Assistant" },
155
+ "rahasia.md": { "ignore": true }
156
+ },
157
+ "dirs": {
158
+ "draft-lama": { "ignore": true }
159
+ }
160
+ }
161
+ \`\`\`
162
+
163
+ | Field | Applies to | Effect |
164
+ | --- | --- | --- |
165
+ | \`title\` | the whole file | Masthead title; wins over the README heading |
166
+ | \`sections.<dir>\` / \`dirs.<path>\` | a directory | \`title\` is the menu label, \`meta\` is a short note |
167
+ | \`sections.<dir>.docs.<file>\` | a file inside that directory | same shape as \`docs\` entries |
168
+ | \`docs.<file>\` / \`files.<path>\` | a file | \`title\` is the menu label |
169
+ | \`order\` | any entry | Ascending menu order. Entries without \`order\` follow, still files before folders |
170
+ | \`ignore\` (or \`hidden\`) | any entry | \`true\` hides the file or folder from the menu, lists, search, home, and contents. Hiding a folder hides everything inside it |
171
+
172
+ \`ignore\` is view-only: the file still exists on disk. Remove the flag to
173
+ bring it back:
174
+
175
+ \`\`\`json
176
+ { "files": { "rahasia.md": { "ignore": true } }, "dirs": { "draft-lama": { "ignore": true } } }
177
+ \`\`\`
178
+
179
+ Paths are relative to the \`.knowledges\` root, use \`/\` separators, and
180
+ must match the on-disk path exactly as the viewer shows it, for example
181
+ \`features/auth.md\`. Nested folders use the full relative path as the key.
182
+
183
+ The sidebar always opens with two fixed entries above the tree: \`index\`
184
+ (the root \`README.md\`) and \`contents\` (\`#/a/contents\`, a system route with
185
+ no file on disk that lists the whole base). The root README is therefore
186
+ not repeated inside the tree.
187
+
188
+ ## Config (two layers)
189
+
190
+ knl-view reads two optional config files. A missing file is never an error;
191
+ a broken file is ignored with one warning.
192
+
193
+ **Global (\`~/.knlview/knl-config.json\`)**: shared by every project on this
194
+ machine. Holds the \`anchordb\` block (connection). Without it, AnchorDB
195
+ stays completely off:
196
+
197
+ \`\`\`json
198
+ {
199
+ "anchordb": {
200
+ "url": "wire://127.0.0.1:7448",
201
+ "token": "your-token",
202
+ "sync": true
203
+ }
204
+ }
205
+ \`\`\`
206
+
207
+ **Per project: \`knl-config.json\` at the project root**, machine-local,
208
+ gitignored. Holds only \`path\`, \`port\`, \`host\`, and the \`models\` map:
209
+
210
+ \`\`\`json
211
+ {
212
+ "path": ".knowledges",
213
+ "port": 8085,
214
+ "models": { "knowledges": "vez0vqrp" }
215
+ }
216
+ \`\`\`
217
+
218
+ The map owns model naming: role → AnchorDB model name, names may be
219
+ random; the map records what each name is for, and names stay stable
220
+ across project renames. Create names with \`knl-view model new <role>\`
221
+ and inspect ownership with \`knl-view model check\`. Without a map entry
222
+ for a role, that role runs fully local. CLI flags always win over
223
+ config values.
224
+
225
+ Installing AnchorDB (optional; the viewer runs fully local without it):
226
+ \`knl-view install anchordb\`: native binary from GitHub Releases on
227
+ Linux and macOS (SHA256-verified), or a container from
228
+ ghcr.io/rachmanzz/anchordb via docker/podman on other platforms. Flags:
229
+ \`--yes\`, \`--method=binary|container\`, \`--token=...\`.
230
+
231
+ Two-way lineage: graph traces take \`--direction\` (CLI) or
232
+ \`?direction=\` (HTTP): \`out\` follows edges leaving the target, \`in\`
233
+ follows edges entering it, \`both\` is the local default. Direction is
234
+ mechanical (edge direction), never cause-and-effect interpretation.
235
+ Each node in the result carries \`via\` (\`center\`, \`in\`, \`out\`,
236
+ \`in+out\`). On /api/anchordb/trace the default is \`out\`; \`in\` is
237
+ walked backwards from the local sync mirror and reported with
238
+ \`mirrored: true\`.
239
+
240
+ Consensus (claims): a second AnchorDB model (the \`consensus\` role in
241
+ the models map) holds agreement and conflict between claims only (never
242
+ decisions or files). Claims are content-addressed:
243
+ \`knl-view consensus claim "<statement>"\` prints the id (\`c-\` + 16 hex
244
+ chars of the statement hash); the same text is the same claim everywhere.
245
+ Relations: \`agree\`, \`contradict\`, \`retract\` take two claim
246
+ ids. \`status <id>\` derives: any incoming \`contradicts\` → \`disputed\`,
247
+ else incoming \`agrees_with\` → \`consensus\`, else \`uncontested\`.
248
+ \`list\` shows every locally known claim. Mutations are CLI-only;
249
+ \`GET /api/consensus?target=<claimId>\` is read-only. Requires AnchorDB
250
+ plus a mapped \`consensus\` role.
251
+
252
+ Sources graph (whole project): a third AnchorDB model, the \`sources\`
253
+ role, holds the dependency graph of the whole project: files and their
254
+ named symbols. Every file is a node (id = repo-relative path); import
255
+ edges are extracted with tree-sitter syntax parsing (JS family and Go
256
+ today) and stored in both directions (\`imports\`, \`imported_by\`).
257
+ Each file also defines symbol nodes: ids are \`<path>#<name>\` (class
258
+ methods \`Class.method\`; a name defined more than once in one file gets
259
+ a line suffix \`<path>#<name>@<line>\`), kinds are \`function\`,
260
+ \`class\`, \`method\`, \`type\`. Edges: \`defines\`/\`defined_in\`
261
+ (file → symbol) and \`calls\`/\`called_by\` (symbol → symbol), both
262
+ directions stored. Call resolution is conservative: the call belongs to
263
+ its innermost enclosing symbol and the callee must match exactly one
264
+ definition project-wide; ambiguous names are skipped, never guessed.
265
+ Label text (symbol names and kinds; AnchorDB attributes are
266
+ numeric-only) lives in \`.knowledges/.knl-data/<path>.md\`, one file per
267
+ source file that has symbols; override with \`labelDir\` in the project
268
+ config. Hidden folders are skipped except .github; node_modules, dist,
269
+ and .gitignore patterns are ignored. \`knl-view sources sync\` is
270
+ idempotent and server-authoritative (no local mirror);
271
+ \`knl-view sources trace --target=<id>\` and
272
+ \`GET /api/sources/trace?target=<id>\` answer lineage: a target
273
+ containing \`#\` walks symbol lineage (\`calls\`/\`called_by\`), a
274
+ plain path walks file lineage (\`imports\`/\`imported_by\`). A removed
275
+ file or symbol is tombstoned (attribute \`tombstone = 2\`) after its
276
+ edges are unlinked; if it comes back, the tombstone lifts and the edges
277
+ are relinked.
278
+
279
+ Decision history skill: recording architectural decisions is delegated
280
+ to the separate decision-history skill (its decision block format is its
281
+ own domain; this skill never writes decision blocks). If it is not
282
+ installed in the project yet, install it first:
283
+ \`npx skills add rachmanzz/decisionskills --skill decision-history\`
284
+ or, with agy: \`agy plugin install https://github.com/rachmanzz/decisionskills\`.
285
+
286
+ ## Rules
287
+
288
+ - Never move or delete files to satisfy the map. Either add the file or
289
+ remove the stale map entry.
290
+ - Give every markdown file a top-level \`# heading\` so it still reads well
291
+ when no map entry covers it.
292
+ - Keep \`order\` values small and dense per menu level (1, 2, 3).
293
+ - When the map and disk disagree about existence, the disk wins.
294
+
295
+ ## Features writing rules
296
+
297
+ Under \`features/{feature_name}/\`, prefer to keep at least:
298
+ - \`prd.md\`: Product Requirements Document for this feature
299
+ - \`detail-plan.md\`: concrete implementation plan, decisions, trade-offs
300
+
301
+ Names are kebab-case for the folder. Keep other artifacts minimal and cover
302
+ them via \`map.json\` if they need custom menu labels/order.
303
+
304
+ ## Futures writing rules
305
+
306
+ Under \`futures/\`, the base typically has:
307
+ - \`README.md\`: vision/overview
308
+ - \`prd.md\`: global PRD covering proposed future work
309
+
310
+ Use kebab-case for any additional future documents. Defer exotic filenames
311
+ to \`map.json\`.
312
+
313
+ ## Roadmap writing rules
314
+
315
+ Under \`roadmap/\`, maintain milestone-oriented notes. Keep \`README.md\` as
316
+ the index and split by milestones only when they grow large. Use
317
+ \`map.json\` to reorder milestones/files without renaming.
318
+
319
+ ## Roasted writing rules
320
+
321
+ All artifacts from roasting/audits go under \`roasted/\` (relative to the
322
+ knowledge base root in use; i.e. the directory passed via \`--path\`, which
323
+ may be \`.knowledges\` or a custom one). Do **not** place them in the root
324
+ of that directory.
325
+
326
+ ### Naming
327
+
328
+ \`roasted/{roasted_name}_v{number}.md\`: kebab-case name, integer
329
+ \`number\` starting at \`1\`.
330
+
331
+ ### Stack order
332
+
333
+ - \`_v1\` is the first roast → placed at the bottom of the stack
334
+ - each new roast increments number and goes at the top (highest number =
335
+ newest)
336
+ - files are immutable by number; updates belong to the same file, not a
337
+ new one
338
+
339
+ ### Classification
340
+
341
+ Every roast must state:
342
+ - \`Severity\`: \`P0|P1|P2|P3\`
343
+ - \`Class\`: \`BUG|PERF|VULN|DEBT|CEILING|PROSES\`
344
+
345
+ ## When the agent triggers the graph
346
+
347
+ The graph stays true only if the agent feeds it. Every trigger goes
348
+ through the single \`knl-view\` entrypoint with the existing subcommands
349
+ below; never invent another binary, script, or wrapper to fire any of
350
+ this.
351
+
352
+ - After creating, editing, renaming, or deleting any project file:
353
+ run \`knl-view sources sync\`. It is server-authoritative and
354
+ idempotent: run it again and only the difference is applied.
355
+ - Before changing a file that other files may depend on:
356
+ \`knl-view sources trace --target=<path> --direction=in\` shows who
357
+ depends on it first (the blast radius).
358
+ - Before adding an import to, or moving logic out of, another file:
359
+ \`knl-view sources trace --target=<path> --direction=out\` shows what
360
+ it already touches.
361
+ - Before changing or removing a function, method, or type:
362
+ \`knl-view sources trace --target=<path>#<name> --direction=in\` shows
363
+ every symbol that calls it first.
364
+ - Before renaming a symbol or moving it to another file: remember its
365
+ record id contains the old name/path. Sync tombstones the stale record
366
+ and creates the new one; trace by the new id afterwards.
367
+ - A new architectural decision: trigger the decision-history skill
368
+ (install it first if missing; see "Decision history skill" above).
369
+ The decision block format is that skill's domain; this skill never
370
+ writes decision blocks.
371
+ - A technical disagreement worth recording as claims:
372
+ \`knl-view consensus claim "<statement>"\`, then
373
+ \`knl-view consensus agree <id> <id>\` or
374
+ \`knl-view consensus contradict <id> <id>\`. A claim that lost its
375
+ ground: \`knl-view consensus retract <id> <id>\`.
376
+ `