@rahmantech/knl-view 0.0.0-stage → 0.2.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 +483 -2
  3. package/bin/knl-view.js +1320 -0
  4. package/dist/assets/abnfDiagram-WEYOIA3C-hoSbsMOz.js +1 -0
  5. package/dist/assets/arc-g87Np6E5.js +1 -0
  6. package/dist/assets/architecture-WOLXFQ4H-DOIq-0IA.js +1 -0
  7. package/dist/assets/architectureDiagram-ZHVYKT53-Dxwe8n7m.js +36 -0
  8. package/dist/assets/array-BifhSqXX.js +1 -0
  9. package/dist/assets/blockDiagram-RJAE7QA3-BnGng59Z.js +140 -0
  10. package/dist/assets/c4Diagram-XDPPPE54-C5LjqIIo.js +38 -0
  11. package/dist/assets/channel-BVWkRFBw.js +1 -0
  12. package/dist/assets/chunk-2E7WHVMD-BJ1dyGLq.js +88 -0
  13. package/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  14. package/dist/assets/chunk-4EMNKR2T-DXF02Sgw.js +1 -0
  15. package/dist/assets/chunk-55XCU76K-CM1akAbu.js +1 -0
  16. package/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  17. package/dist/assets/chunk-7S6XSTFO-BOUjqldL.js +2 -0
  18. package/dist/assets/chunk-AQDTSKZB-C4isdNLA.js +72 -0
  19. package/dist/assets/chunk-DSNLSHJZ-CbOTkSVM.js +129 -0
  20. package/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  21. package/dist/assets/chunk-HAGFOTED-CRfnketR.js +62 -0
  22. package/dist/assets/chunk-HNE4Q36P-rxlpGUU8.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-BOYCgTmV.js +213 -0
  27. package/dist/assets/chunk-UHW7PFBP-Cnogp1Fb.js +1 -0
  28. package/dist/assets/chunk-VQ5W62QI-Cu1WY5Uo.js +1 -0
  29. package/dist/assets/chunk-WQQL4CDK-BJc3FPE2.js +10 -0
  30. package/dist/assets/chunk-XXDRQBXY-BlPj4H7s.js +1 -0
  31. package/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  32. package/dist/assets/chunk-YZO67HFG-V85H31x4.js +1 -0
  33. package/dist/assets/chunk-ZAUX2LTX-C3XKXW09.js +1 -0
  34. package/dist/assets/chunk-ZCHMQSOR-B0mmpEgW.js +2 -0
  35. package/dist/assets/classDiagram-v2-4MYWR3UA-oMd7YaHP.js +217 -0
  36. package/dist/assets/cose-bilkent-JH36ORCC-J0kSVsDf.js +1 -0
  37. package/dist/assets/cynefin-EF2NZ3EQ-DK2TNhel.js +1 -0
  38. package/dist/assets/cynefinDiagram-WF24UDRP-DDvKgieA.js +62 -0
  39. package/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  40. package/dist/assets/dagre-NK4GIZVX-Ca2Fe-i-.js +4 -0
  41. package/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  42. package/dist/assets/diagram-CBK3MWTC-L6dmeQ2V.js +24 -0
  43. package/dist/assets/diagram-I5XIIV2D-DhG8R-v5.js +24 -0
  44. package/dist/assets/diagram-ODIL2UCW-BpYYbYBT.js +30 -0
  45. package/dist/assets/diagram-PFBEJUXE-DKQpHSsC.js +3 -0
  46. package/dist/assets/diagram-RJA3I4VV-DP7Dsnya.js +41 -0
  47. package/dist/assets/diagram-TYUNFSNR-CfSFewUS.js +200 -0
  48. package/dist/assets/dist-CQNjnrld.js +1 -0
  49. package/dist/assets/ebnfDiagram-V4P3TTPT-DE3MZoJO.js +1 -0
  50. package/dist/assets/elk-QONTBCTQ-p_aAQPHU.js +27 -0
  51. package/dist/assets/erDiagram-KYQIGQAA-DZO3vQrk.js +99 -0
  52. package/dist/assets/eventmodeling-K75KTNOO-BII9EPl5.js +1 -0
  53. package/dist/assets/flowDiagram-IUGLLWBU-D9uPno39.js +1 -0
  54. package/dist/assets/ganttDiagram-R7GPNTJY-BKiH3v2r.js +292 -0
  55. package/dist/assets/gitGraph-VSP46ZUC-oHL6cr73.js +1 -0
  56. package/dist/assets/gitGraphDiagram-PDYL7XHY-CqPVIEeo.js +106 -0
  57. package/dist/assets/graphlib-DS17s2tU.js +1 -0
  58. package/dist/assets/index-CSpirV16.js +76 -0
  59. package/dist/assets/index-DLgMsUs2.css +1 -0
  60. package/dist/assets/info-OHQRW6UA-Drrbnhpd.js +1 -0
  61. package/dist/assets/infoDiagram-MVDRKAX5-DnQQFMAd.js +2 -0
  62. package/dist/assets/init-C-OQMol4.js +1 -0
  63. package/dist/assets/ishikawaDiagram-HPEHV2GE-DIpQlxRh.js +70 -0
  64. package/dist/assets/journeyDiagram-BOSHW7O7-EBR9I5h1.js +139 -0
  65. package/dist/assets/kanban-definition-FNSA4HC5-CdvNsjik.js +89 -0
  66. package/dist/assets/katex-B_JcIwqj.js +257 -0
  67. package/dist/assets/line-CDLzgxDc.js +1 -0
  68. package/dist/assets/linear-DfXKql9u.js +1 -0
  69. package/dist/assets/mermaid-parser.core-DnXGphT2.js +7 -0
  70. package/dist/assets/mermaid.core-h6fiCaSZ.js +44 -0
  71. package/dist/assets/mindmap-definition-6OY57UG3-BiNfio-j.js +98 -0
  72. package/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  73. package/dist/assets/packet-JDAUHWVQ-D76byv1S.js +1 -0
  74. package/dist/assets/path-fybaL0A-.js +1 -0
  75. package/dist/assets/pegDiagram-ECIUJNBX-Drs8Pxdt.js +1 -0
  76. package/dist/assets/pie-XZMESJXO-D_83z1lM.js +1 -0
  77. package/dist/assets/pieDiagram-GNMPY74T-wvzIphSf.js +39 -0
  78. package/dist/assets/quadrantDiagram-D57K4MC3-B7EfMRWb.js +7 -0
  79. package/dist/assets/radar-ABXABTNO-Bs6dVdAR.js +1 -0
  80. package/dist/assets/railroad-I3PHUGI6-lKaN4miw.js +1 -0
  81. package/dist/assets/railroad-abnf-LNEFI6M7-Bhh16b7Z.js +1 -0
  82. package/dist/assets/railroad-ebnf-DWYD2UWJ-m6ybW-Y_.js +1 -0
  83. package/dist/assets/railroad-peg-JR7BVNR7-DNn72e7r.js +1 -0
  84. package/dist/assets/railroadDiagram-Y72NRBL7-DNeop1iZ.js +1 -0
  85. package/dist/assets/requirementDiagram-EQYSW5RA-BVYZ2XYJ.js +84 -0
  86. package/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  87. package/dist/assets/sankeyDiagram-SY24JHO5-B2MP51on.js +40 -0
  88. package/dist/assets/sequenceDiagram-BLEHNHQU-BUvxstXG.js +169 -0
  89. package/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  90. package/dist/assets/src-BrR0lqlG.js +1 -0
  91. package/dist/assets/stateDiagram-v2-A6IESKWZ-Ba84OGKE.js +291 -0
  92. package/dist/assets/swimlanes-B3ENRKFQ-BMSl2PNA.js +1 -0
  93. package/dist/assets/swimlanesDiagram-AYQRV4AK-M1dmvq7m.js +8 -0
  94. package/dist/assets/timeline-definition-JXYKGLGB-DOdW-dxq.js +120 -0
  95. package/dist/assets/treeView-D4JQ5SDB-DZ9KQyn5.js +1 -0
  96. package/dist/assets/treemap-SAJKECNS-WEC5wtDV.js +1 -0
  97. package/dist/assets/usecaseDiagram-WPOLBNNX-cldTztFL.js +328 -0
  98. package/dist/assets/vennDiagram-Q3Y3IDCW-c41KySoY.js +34 -0
  99. package/dist/assets/wardley-7MLQ67FV-Bf9_aSSZ.js +1 -0
  100. package/dist/assets/wardleyDiagram-IVAIDKWA-BeKEvEBG.js +78 -0
  101. package/dist/assets/xychartDiagram-4URNHDU4-tNkAxTxG.js +7 -0
  102. package/dist/index.html +22 -0
  103. package/package.json +50 -4
  104. package/server/config.js +389 -0
  105. package/server/consensus.js +180 -0
  106. package/server/graph/anchordb_client.js +253 -0
  107. package/server/graph/model.js +157 -0
  108. package/server/graph/parser.js +327 -0
  109. package/server/graph/sources.js +801 -0
  110. package/server/graph/syntax.js +437 -0
  111. package/server/index.js +742 -0
  112. package/server/install.js +437 -0
  113. package/server/map.js +126 -0
  114. package/server/models_registry.js +109 -0
  115. package/server/scaffold.js +466 -0
@@ -0,0 +1,327 @@
1
+ import { promises as fs } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { GraphNode, GraphEdge, DirectedGraph } from './model.js'
4
+
5
+ const MD_EXTS = new Set(['.md', '.markdown', '.prd'])
6
+
7
+ /**
8
+ * Simple (YAML-like) frontmatter extraction and the first heading.
9
+ */
10
+ export function extractFrontmatterAndTitle(content) {
11
+ let metadata = {}
12
+ let body = content
13
+ let title = null
14
+
15
+ if (content.startsWith('---')) {
16
+ const endIdx = content.indexOf('\n---', 3)
17
+ if (endIdx !== -1) {
18
+ const rawFm = content.slice(3, endIdx).trim()
19
+ body = content.slice(endIdx + 4)
20
+ metadata = parseSimpleYaml(rawFm)
21
+ }
22
+ }
23
+
24
+ if (metadata.title) {
25
+ title = String(metadata.title).trim()
26
+ } else {
27
+ // Take the first H1 as the title
28
+ const match = body.match(/^#\s+(.+)$/m)
29
+ if (match) {
30
+ title = match[1].trim()
31
+ }
32
+ }
33
+
34
+ return { metadata, body, title }
35
+ }
36
+
37
+ function parseSimpleYaml(text) {
38
+ const result = {}
39
+ const lines = text.replace(/\r/g, '').split('\n')
40
+ for (const line of lines) {
41
+ const match = line.match(/^([a-zA-Z0-9_-]+)\s*:\s*(.*)$/)
42
+ if (!match) continue
43
+ const key = match[1].trim()
44
+ let val = match[2].trim()
45
+
46
+ // Handle array format [a, b, c]
47
+ if (val.startsWith('[') && val.endsWith(']')) {
48
+ const items = val.slice(1, -1).split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean)
49
+ result[key] = items
50
+ } else {
51
+ // Strip quotes when present
52
+ val = val.replace(/^['"]|['"]$/g, '')
53
+ result[key] = val
54
+ }
55
+ }
56
+ return result
57
+ }
58
+
59
+ /**
60
+ * Parse a knowledge Markdown file (e.g. docs/xxx.md or features/xxx.md).
61
+ */
62
+ export function parseKnowledgeContent(relPath, content) {
63
+ const { metadata, body, title } = extractFrontmatterAndTitle(content)
64
+ const id = relPath.replace(/\\/g, '/')
65
+ const node = new GraphNode({
66
+ id,
67
+ title: title || path.basename(relPath),
68
+ type: 'knowledge',
69
+ path: id,
70
+ metadata
71
+ })
72
+
73
+ const edges = []
74
+
75
+ // Parse dependencies from the frontmatter
76
+ const deps = Array.isArray(metadata.depends_on) ? metadata.depends_on : (metadata.depends_on ? [metadata.depends_on] : [])
77
+ for (const dep of deps) {
78
+ edges.push(new GraphEdge({
79
+ from: id,
80
+ to: String(dep).trim(),
81
+ type: 'depends_on'
82
+ }))
83
+ }
84
+
85
+ // Parse references from the frontmatter
86
+ const refs = Array.isArray(metadata.references) ? metadata.references : (metadata.references ? [metadata.references] : [])
87
+ for (const ref of refs) {
88
+ edges.push(new GraphEdge({
89
+ from: id,
90
+ to: String(ref).trim(),
91
+ type: 'references'
92
+ }))
93
+ }
94
+
95
+ // Parse plain markdown links [text](target.md)
96
+ const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g
97
+ let linkMatch
98
+ while ((linkMatch = linkRegex.exec(body)) !== null) {
99
+ const targetUrl = linkMatch[2].trim()
100
+ // Ignore external web links (http/https/mailto)
101
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://') && !targetUrl.startsWith('mailto:') && !targetUrl.startsWith('#')) {
102
+ const cleanTarget = targetUrl.split('#')[0].split('?')[0].replace(/^\.\//, '')
103
+ if (cleanTarget) {
104
+ const docDir = path.dirname(relPath).replace(/\\/g, '/')
105
+ const resolvedTarget = (docDir && docDir !== '.' && !cleanTarget.startsWith('/'))
106
+ ? path.posix.normalize(`${docDir}/${cleanTarget}`)
107
+ : cleanTarget.replace(/^\//, '')
108
+
109
+ if (resolvedTarget && resolvedTarget !== id) {
110
+ edges.push(new GraphEdge({
111
+ from: id,
112
+ to: resolvedTarget,
113
+ type: 'references',
114
+ metadata: { text: linkMatch[1] }
115
+ }))
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ return { node, edges }
122
+ }
123
+
124
+ /**
125
+ * Parse decision blocks from a decision file (decisionskills format: <!-- decision:start -->).
126
+ */
127
+ export function parseDecisionBlocks(relPath, content) {
128
+ const nodes = []
129
+ const edges = []
130
+
131
+ const blockRegex = /<!--\s*decision:start\s*-->([\s\S]*?)<!--\s*decision:end\s*-->/g
132
+ let match
133
+
134
+ while ((match = blockRegex.exec(content)) !== null) {
135
+ const rawBlock = match[1]
136
+ const parsed = parseSingleDecisionBlock(relPath, rawBlock)
137
+ if (parsed) {
138
+ nodes.push(parsed.node)
139
+ edges.push(...parsed.edges)
140
+ }
141
+ }
142
+
143
+ return { nodes, edges }
144
+ }
145
+
146
+ // Helper for list fields that may repeat: the lines "- references: a" then
147
+ // "- references: b" produce [a, b], not just [b].
148
+ function appendList(existing, val) {
149
+ const items = val.split(',').map(s => s.trim().replace(/^`|`$/g, '')).filter(Boolean)
150
+ return existing.concat(items.filter((item) => !existing.includes(item)))
151
+ }
152
+
153
+ function parseSingleDecisionBlock(relPath, rawBlock) {
154
+ const lines = rawBlock.replace(/\r/g, '').split('\n')
155
+ let title = 'Untitled Decision'
156
+ let id = null
157
+ let decidedAt = null
158
+ let scope = null
159
+ let tags = []
160
+ let supersedes = []
161
+ let related = []
162
+ let references = []
163
+ let decisionText = ''
164
+
165
+ let currentSection = null
166
+
167
+ for (const line of lines) {
168
+ const trimmed = line.trim()
169
+
170
+ // Title heading (## Title)
171
+ if (trimmed.startsWith('## ')) {
172
+ title = trimmed.slice(3).trim()
173
+ continue
174
+ }
175
+
176
+ // Section header (### Section)
177
+ if (trimmed.startsWith('### ')) {
178
+ currentSection = trimmed.slice(4).trim().toLowerCase()
179
+ continue
180
+ }
181
+
182
+ // Metadata bullet items (- Field: Value)
183
+ const metaMatch = trimmed.match(/^-\s+([^:]+):\s*(.*)$/)
184
+ if (metaMatch && !currentSection) {
185
+ const field = metaMatch[1].trim().toLowerCase()
186
+ const val = metaMatch[2].trim().replace(/^`|`$/g, '')
187
+
188
+ if (val === 'none' || !val) continue
189
+
190
+ if (field === 'id') id = val
191
+ else if (field === 'decided at') decidedAt = val
192
+ else if (field === 'scope') scope = val
193
+ else if (field === 'tags') tags = val.split(',').map(s => s.trim().replace(/^`|`$/g, '')).filter(Boolean)
194
+ // List fields may repeat across lines; the second line appends,
195
+ // it does not overwrite the first.
196
+ else if (field === 'supersedes') supersedes = appendList(supersedes, val)
197
+ else if (field === 'related') related = appendList(related, val)
198
+ else if (field === 'references') references = appendList(references, val)
199
+ continue
200
+ }
201
+
202
+ // Body content per section
203
+ if (currentSection === 'decision' && trimmed) {
204
+ decisionText += (decisionText ? ' ' : '') + trimmed
205
+ }
206
+ }
207
+
208
+ if (!id) return null
209
+
210
+ const node = new GraphNode({
211
+ id,
212
+ title,
213
+ type: 'decision',
214
+ path: relPath.replace(/\\/g, '/'),
215
+ metadata: {
216
+ decided_at: decidedAt,
217
+ scope,
218
+ tags,
219
+ decision: decisionText
220
+ }
221
+ })
222
+
223
+ const edges = []
224
+
225
+ for (const sup of supersedes) {
226
+ edges.push(new GraphEdge({
227
+ from: id,
228
+ to: sup,
229
+ type: 'supersedes'
230
+ }))
231
+ }
232
+
233
+ for (const rel of related) {
234
+ edges.push(new GraphEdge({
235
+ from: id,
236
+ to: rel,
237
+ type: 'related_to'
238
+ }))
239
+ }
240
+
241
+ // References are resolved relative to the decision file's own folder,
242
+ // the same rule markdown links follow: a decision in decisions/2026-10.md
243
+ // writing `ref/DEC-x.md` points at decisions/ref/DEC-x.md. Absolute and
244
+ // URL-shaped values are kept as-is.
245
+ const docDir = path.posix.dirname(relPath.replace(/\\/g, '/'))
246
+ for (const ref of references) {
247
+ const isUrl = /^[a-z][a-z0-9+.-]*:\/\//i.test(ref)
248
+ const isDocRef = !isUrl && (ref.endsWith('.md') || ref.includes('/'))
249
+ let target = ref
250
+ if (isDocRef && !ref.startsWith('/')) {
251
+ target = (docDir && docDir !== '.')
252
+ ? path.posix.normalize(`${docDir}/${ref}`)
253
+ : ref.replace(/^\//, '')
254
+ }
255
+ edges.push(new GraphEdge({
256
+ from: id,
257
+ to: target,
258
+ type: isDocRef ? 'affects_knowledge' : 'references'
259
+ }))
260
+ }
261
+
262
+ return { node, edges }
263
+ }
264
+
265
+ /**
266
+ * Build the full graph recursively from the knowledge base root folder (.knowledges).
267
+ */
268
+ export async function buildGraphFromRoot(rootAbs) {
269
+ const graph = new DirectedGraph()
270
+ await walkDir(rootAbs, '', async (absPath, relPath) => {
271
+ const ext = path.extname(absPath).toLowerCase()
272
+ if (!MD_EXTS.has(ext)) return
273
+
274
+ let content
275
+ try {
276
+ content = await fs.readFile(absPath, 'utf8')
277
+ } catch {
278
+ return
279
+ }
280
+
281
+ const normRel = relPath.replace(/\\/g, '/')
282
+
283
+ // Check whether this is a decision file (under the decisions/ folder)
284
+ if (normRel.startsWith('decisions/')) {
285
+ const { nodes, edges: decisionEdges } = parseDecisionBlocks(normRel, content)
286
+ for (const n of nodes) graph.addNode(n)
287
+ for (const e of decisionEdges) graph.addEdge(e)
288
+ // A decision file with no blocks is a plain document (for example
289
+ // reference material under decisions/ref/): keep it in the graph as
290
+ // a knowledge node so decisions can point at it by path.
291
+ if (nodes.length === 0) {
292
+ const { node } = parseKnowledgeContent(normRel, content)
293
+ graph.addNode(node)
294
+ }
295
+ } else {
296
+ const { node, edges } = parseKnowledgeContent(normRel, content)
297
+ graph.addNode(node)
298
+ for (const e of edges) graph.addEdge(e)
299
+ }
300
+ })
301
+
302
+ return graph
303
+ }
304
+
305
+ async function walkDir(baseDir, currentRel, visitor) {
306
+ let entries
307
+ try {
308
+ entries = await fs.readdir(path.join(baseDir, currentRel), { withFileTypes: true })
309
+ } catch {
310
+ return
311
+ }
312
+
313
+ for (const ent of entries) {
314
+ if (ent.name.startsWith('.')) {
315
+ continue
316
+ }
317
+
318
+ const nextRel = currentRel ? path.join(currentRel, ent.name) : ent.name
319
+ const fullPath = path.join(baseDir, nextRel)
320
+
321
+ if (ent.isDirectory()) {
322
+ await walkDir(baseDir, nextRel, visitor)
323
+ } else if (ent.isFile()) {
324
+ await visitor(fullPath, nextRel)
325
+ }
326
+ }
327
+ }