@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.
- package/LICENSE +201 -0
- package/README.md +398 -2
- package/bin/knl-view.js +943 -0
- package/dist/assets/abnfDiagram-WEYOIA3C-B1gaBaSI.js +1 -0
- package/dist/assets/arc-NZ9uRaCl.js +1 -0
- package/dist/assets/architecture-WOLXFQ4H-8MalLwTd.js +1 -0
- package/dist/assets/architectureDiagram-ZHVYKT53-eZ8i5cl4.js +36 -0
- package/dist/assets/array-BifhSqXX.js +1 -0
- package/dist/assets/blockDiagram-RJAE7QA3-BavOE8BM.js +140 -0
- package/dist/assets/c4Diagram-XDPPPE54-DqMaTAzx.js +38 -0
- package/dist/assets/channel-BZ2_TTTJ.js +1 -0
- package/dist/assets/chunk-2E7WHVMD-B1Hj2BjJ.js +88 -0
- package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/assets/chunk-4EMNKR2T-HDscrCiq.js +1 -0
- package/dist/assets/chunk-55XCU76K-QR_ARj8I.js +1 -0
- package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/assets/chunk-7S6XSTFO-B0p3o547.js +2 -0
- package/dist/assets/chunk-AQDTSKZB-CCxwNyyA.js +72 -0
- package/dist/assets/chunk-DSNLSHJZ-BM5PBFmz.js +129 -0
- package/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/assets/chunk-HAGFOTED-UCDVFuN_.js +62 -0
- package/dist/assets/chunk-HNE4Q36P-C_1sakp2.js +1 -0
- package/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/dist/assets/chunk-NQOKRVIF-b2D1Z4oK.js +213 -0
- package/dist/assets/chunk-UHW7PFBP-CJs0VwBY.js +1 -0
- package/dist/assets/chunk-VQ5W62QI-DQxjgE4_.js +1 -0
- package/dist/assets/chunk-WQQL4CDK-DcjPeAfQ.js +10 -0
- package/dist/assets/chunk-XXDRQBXY-glA0kB5s.js +1 -0
- package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/assets/chunk-YZO67HFG-DJqACL5V.js +1 -0
- package/dist/assets/chunk-ZAUX2LTX-CWfaYtwb.js +1 -0
- package/dist/assets/chunk-ZCHMQSOR-BGX73Hho.js +2 -0
- package/dist/assets/classDiagram-v2-4MYWR3UA-BpYXPz-e.js +217 -0
- package/dist/assets/cose-bilkent-JH36ORCC-e1quKm0Q.js +1 -0
- package/dist/assets/cynefin-EF2NZ3EQ-hb3oUmFO.js +1 -0
- package/dist/assets/cynefinDiagram-WF24UDRP-CnMQvLqi.js +62 -0
- package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/dist/assets/dagre-NK4GIZVX-BH_Y8y3F.js +4 -0
- package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/assets/diagram-CBK3MWTC-Dh79VetZ.js +24 -0
- package/dist/assets/diagram-I5XIIV2D-D3u-o1RV.js +24 -0
- package/dist/assets/diagram-ODIL2UCW-Bfuo-OKS.js +30 -0
- package/dist/assets/diagram-PFBEJUXE-CwHasIbs.js +3 -0
- package/dist/assets/diagram-RJA3I4VV-D8ttmuE8.js +41 -0
- package/dist/assets/diagram-TYUNFSNR-CzC-8RZ0.js +200 -0
- package/dist/assets/dist-CUJYGnnI.js +1 -0
- package/dist/assets/ebnfDiagram-V4P3TTPT-9AAhU7In.js +1 -0
- package/dist/assets/elk-QONTBCTQ-BgvnD1v_.js +27 -0
- package/dist/assets/erDiagram-KYQIGQAA-RLtbiLlP.js +99 -0
- package/dist/assets/eventmodeling-K75KTNOO-DVY22vNZ.js +1 -0
- package/dist/assets/flowDiagram-IUGLLWBU-l7L5DTb5.js +1 -0
- package/dist/assets/ganttDiagram-R7GPNTJY-hHiBGa9W.js +292 -0
- package/dist/assets/gitGraph-VSP46ZUC-Cvxay1wh.js +1 -0
- package/dist/assets/gitGraphDiagram-PDYL7XHY-AOhlpSqg.js +106 -0
- package/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/assets/index-CBLNQvK3.js +76 -0
- package/dist/assets/index-DLgMsUs2.css +1 -0
- package/dist/assets/info-OHQRW6UA-CJ8EUO1m.js +1 -0
- package/dist/assets/infoDiagram-MVDRKAX5-DOQu7oYI.js +2 -0
- package/dist/assets/init-C-OQMol4.js +1 -0
- package/dist/assets/ishikawaDiagram-HPEHV2GE-DcVMvGmx.js +70 -0
- package/dist/assets/journeyDiagram-BOSHW7O7-luMeqEqz.js +139 -0
- package/dist/assets/kanban-definition-FNSA4HC5-KTZbmrW2.js +89 -0
- package/dist/assets/katex-B_JcIwqj.js +257 -0
- package/dist/assets/line-CmPHiOVm.js +1 -0
- package/dist/assets/linear-DHYlKXBA.js +1 -0
- package/dist/assets/mermaid-parser.core-BRDI-WhC.js +7 -0
- package/dist/assets/mermaid.core-DPTRaYLy.js +44 -0
- package/dist/assets/mindmap-definition-6OY57UG3-70EN5Kw_.js +98 -0
- package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/assets/packet-JDAUHWVQ-CXWz83oS.js +1 -0
- package/dist/assets/path-fybaL0A-.js +1 -0
- package/dist/assets/pegDiagram-ECIUJNBX-z5UXKmp4.js +1 -0
- package/dist/assets/pie-XZMESJXO-rWI1HIQ7.js +1 -0
- package/dist/assets/pieDiagram-GNMPY74T-Bh4Kv3NQ.js +39 -0
- package/dist/assets/quadrantDiagram-D57K4MC3-wGM4Ckig.js +7 -0
- package/dist/assets/radar-ABXABTNO-CIJ92p5N.js +1 -0
- package/dist/assets/railroad-I3PHUGI6-8bxua-Hf.js +1 -0
- package/dist/assets/railroad-abnf-LNEFI6M7-B9KVI10m.js +1 -0
- package/dist/assets/railroad-ebnf-DWYD2UWJ-Bt73rnyJ.js +1 -0
- package/dist/assets/railroad-peg-JR7BVNR7-Cie82UPT.js +1 -0
- package/dist/assets/railroadDiagram-Y72NRBL7-C1nmi_tl.js +1 -0
- package/dist/assets/requirementDiagram-EQYSW5RA-BSianW40.js +84 -0
- package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/assets/sankeyDiagram-SY24JHO5-DIa11-h7.js +40 -0
- package/dist/assets/sequenceDiagram-BLEHNHQU-Df5rZKz2.js +169 -0
- package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/assets/src-dClg85lX.js +1 -0
- package/dist/assets/stateDiagram-v2-A6IESKWZ-B15PTSvX.js +291 -0
- package/dist/assets/swimlanes-B3ENRKFQ-C9mgRCMi.js +1 -0
- package/dist/assets/swimlanesDiagram-AYQRV4AK-BGFq4t7O.js +8 -0
- package/dist/assets/timeline-definition-JXYKGLGB-BDVf_mIv.js +120 -0
- package/dist/assets/treeView-D4JQ5SDB-BbBK-25J.js +1 -0
- package/dist/assets/treemap-SAJKECNS-CEDX8apk.js +1 -0
- package/dist/assets/usecaseDiagram-WPOLBNNX-D1QkwFF8.js +328 -0
- package/dist/assets/vennDiagram-Q3Y3IDCW-5PeGZvEO.js +34 -0
- package/dist/assets/wardley-7MLQ67FV-BatN94pO.js +1 -0
- package/dist/assets/wardleyDiagram-IVAIDKWA-DyCcDS3Y.js +78 -0
- package/dist/assets/xychartDiagram-4URNHDU4-CbCXcRlg.js +7 -0
- package/dist/index.html +22 -0
- package/package.json +46 -4
- package/server/config.js +263 -0
- package/server/consensus.js +175 -0
- package/server/graph/anchordb_client.js +253 -0
- package/server/graph/model.js +157 -0
- package/server/graph/parser.js +326 -0
- package/server/graph/sources.js +660 -0
- package/server/graph/syntax.js +257 -0
- package/server/index.js +679 -0
- package/server/install.js +97 -0
- package/server/map.js +126 -0
- package/server/models_registry.js +84 -0
- 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
|
+
`
|