@grafema/util 0.3.0-beta
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 +190 -0
- package/dist/api/GraphAPI.d.ts +87 -0
- package/dist/api/GraphAPI.d.ts.map +1 -0
- package/dist/api/GraphAPI.js +212 -0
- package/dist/api/GraphAPI.js.map +1 -0
- package/dist/api/GuaranteeAPI.d.ts +147 -0
- package/dist/api/GuaranteeAPI.d.ts.map +1 -0
- package/dist/api/GuaranteeAPI.js +290 -0
- package/dist/api/GuaranteeAPI.js.map +1 -0
- package/dist/config/ConfigLoader.d.ts +214 -0
- package/dist/config/ConfigLoader.d.ts.map +1 -0
- package/dist/config/ConfigLoader.js +441 -0
- package/dist/config/ConfigLoader.js.map +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +5 -0
- package/dist/config/index.js.map +1 -0
- package/dist/core/CoverageAnalyzer.d.ts +65 -0
- package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
- package/dist/core/CoverageAnalyzer.js +199 -0
- package/dist/core/CoverageAnalyzer.js.map +1 -0
- package/dist/core/FileExplainer.d.ts +101 -0
- package/dist/core/FileExplainer.d.ts.map +1 -0
- package/dist/core/FileExplainer.js +140 -0
- package/dist/core/FileExplainer.js.map +1 -0
- package/dist/core/FileOverview.d.ts +124 -0
- package/dist/core/FileOverview.d.ts.map +1 -0
- package/dist/core/FileOverview.js +279 -0
- package/dist/core/FileOverview.js.map +1 -0
- package/dist/core/GrafemaUri.d.ts +66 -0
- package/dist/core/GrafemaUri.d.ts.map +1 -0
- package/dist/core/GrafemaUri.js +191 -0
- package/dist/core/GrafemaUri.js.map +1 -0
- package/dist/core/GraphBackend.d.ts +158 -0
- package/dist/core/GraphBackend.d.ts.map +1 -0
- package/dist/core/GraphBackend.js +85 -0
- package/dist/core/GraphBackend.js.map +1 -0
- package/dist/core/GraphFreshnessChecker.d.ts +33 -0
- package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
- package/dist/core/GraphFreshnessChecker.js +104 -0
- package/dist/core/GraphFreshnessChecker.js.map +1 -0
- package/dist/core/GuaranteeManager.d.ts +254 -0
- package/dist/core/GuaranteeManager.d.ts.map +1 -0
- package/dist/core/GuaranteeManager.js +447 -0
- package/dist/core/GuaranteeManager.js.map +1 -0
- package/dist/core/HashUtils.d.ts +24 -0
- package/dist/core/HashUtils.d.ts.map +1 -0
- package/dist/core/HashUtils.js +46 -0
- package/dist/core/HashUtils.js.map +1 -0
- package/dist/core/IncrementalReanalyzer.d.ts +33 -0
- package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
- package/dist/core/IncrementalReanalyzer.js +67 -0
- package/dist/core/IncrementalReanalyzer.js.map +1 -0
- package/dist/core/ResourceRegistry.d.ts +17 -0
- package/dist/core/ResourceRegistry.d.ts.map +1 -0
- package/dist/core/ResourceRegistry.js +32 -0
- package/dist/core/ResourceRegistry.js.map +1 -0
- package/dist/core/SemanticId.d.ts +159 -0
- package/dist/core/SemanticId.d.ts.map +1 -0
- package/dist/core/SemanticId.js +291 -0
- package/dist/core/SemanticId.js.map +1 -0
- package/dist/core/VersionManager.d.ts +166 -0
- package/dist/core/VersionManager.d.ts.map +1 -0
- package/dist/core/VersionManager.js +239 -0
- package/dist/core/VersionManager.js.map +1 -0
- package/dist/core/brandNodeInternal.d.ts +14 -0
- package/dist/core/brandNodeInternal.d.ts.map +1 -0
- package/dist/core/brandNodeInternal.js +4 -0
- package/dist/core/brandNodeInternal.js.map +1 -0
- package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
- package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
- package/dist/core/nodes/GuaranteeNode.js +118 -0
- package/dist/core/nodes/GuaranteeNode.js.map +1 -0
- package/dist/core/nodes/IssueNode.d.ts +73 -0
- package/dist/core/nodes/IssueNode.d.ts.map +1 -0
- package/dist/core/nodes/IssueNode.js +130 -0
- package/dist/core/nodes/IssueNode.js.map +1 -0
- package/dist/core/nodes/NodeKind.d.ts +104 -0
- package/dist/core/nodes/NodeKind.d.ts.map +1 -0
- package/dist/core/nodes/NodeKind.js +166 -0
- package/dist/core/nodes/NodeKind.js.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts +103 -0
- package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticCollector.js +133 -0
- package/dist/diagnostics/DiagnosticCollector.js.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts +122 -0
- package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticReporter.js +300 -0
- package/dist/diagnostics/DiagnosticReporter.js.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
- package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
- package/dist/diagnostics/DiagnosticWriter.js +44 -0
- package/dist/diagnostics/DiagnosticWriter.js.map +1 -0
- package/dist/diagnostics/categories.d.ts +57 -0
- package/dist/diagnostics/categories.d.ts.map +1 -0
- package/dist/diagnostics/categories.js +71 -0
- package/dist/diagnostics/categories.js.map +1 -0
- package/dist/diagnostics/index.d.ts +17 -0
- package/dist/diagnostics/index.d.ts.map +1 -0
- package/dist/diagnostics/index.js +15 -0
- package/dist/diagnostics/index.js.map +1 -0
- package/dist/errors/GrafemaError.d.ts +200 -0
- package/dist/errors/GrafemaError.d.ts.map +1 -0
- package/dist/errors/GrafemaError.js +209 -0
- package/dist/errors/GrafemaError.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +76 -0
- package/dist/index.js.map +1 -0
- package/dist/instructions/index.d.ts +8 -0
- package/dist/instructions/index.d.ts.map +1 -0
- package/dist/instructions/index.js +20 -0
- package/dist/instructions/index.js.map +1 -0
- package/dist/instructions/onboarding.md +133 -0
- package/dist/knowledge/KnowledgeBase.d.ts +113 -0
- package/dist/knowledge/KnowledgeBase.d.ts.map +1 -0
- package/dist/knowledge/KnowledgeBase.js +420 -0
- package/dist/knowledge/KnowledgeBase.js.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts +59 -0
- package/dist/knowledge/SemanticAddressResolver.d.ts.map +1 -0
- package/dist/knowledge/SemanticAddressResolver.js +160 -0
- package/dist/knowledge/SemanticAddressResolver.js.map +1 -0
- package/dist/knowledge/git-ingest.d.ts +58 -0
- package/dist/knowledge/git-ingest.d.ts.map +1 -0
- package/dist/knowledge/git-ingest.js +301 -0
- package/dist/knowledge/git-ingest.js.map +1 -0
- package/dist/knowledge/git-queries.d.ts +86 -0
- package/dist/knowledge/git-queries.d.ts.map +1 -0
- package/dist/knowledge/git-queries.js +177 -0
- package/dist/knowledge/git-queries.js.map +1 -0
- package/dist/knowledge/index.d.ts +14 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge/index.js +10 -0
- package/dist/knowledge/index.js.map +1 -0
- package/dist/knowledge/parser.d.ts +39 -0
- package/dist/knowledge/parser.d.ts.map +1 -0
- package/dist/knowledge/parser.js +292 -0
- package/dist/knowledge/parser.js.map +1 -0
- package/dist/knowledge/types.d.ts +133 -0
- package/dist/knowledge/types.d.ts.map +1 -0
- package/dist/knowledge/types.js +8 -0
- package/dist/knowledge/types.js.map +1 -0
- package/dist/logging/Logger.d.ts +98 -0
- package/dist/logging/Logger.d.ts.map +1 -0
- package/dist/logging/Logger.js +274 -0
- package/dist/logging/Logger.js.map +1 -0
- package/dist/notation/archetypes.d.ts +36 -0
- package/dist/notation/archetypes.d.ts.map +1 -0
- package/dist/notation/archetypes.js +173 -0
- package/dist/notation/archetypes.js.map +1 -0
- package/dist/notation/fold.d.ts +25 -0
- package/dist/notation/fold.d.ts.map +1 -0
- package/dist/notation/fold.js +598 -0
- package/dist/notation/fold.js.map +1 -0
- package/dist/notation/index.d.ts +18 -0
- package/dist/notation/index.d.ts.map +1 -0
- package/dist/notation/index.js +16 -0
- package/dist/notation/index.js.map +1 -0
- package/dist/notation/lodExtractor.d.ts +32 -0
- package/dist/notation/lodExtractor.d.ts.map +1 -0
- package/dist/notation/lodExtractor.js +149 -0
- package/dist/notation/lodExtractor.js.map +1 -0
- package/dist/notation/nameShortener.d.ts +22 -0
- package/dist/notation/nameShortener.d.ts.map +1 -0
- package/dist/notation/nameShortener.js +24 -0
- package/dist/notation/nameShortener.js.map +1 -0
- package/dist/notation/perspectives.d.ts +11 -0
- package/dist/notation/perspectives.d.ts.map +1 -0
- package/dist/notation/perspectives.js +16 -0
- package/dist/notation/perspectives.js.map +1 -0
- package/dist/notation/renderer.d.ts +31 -0
- package/dist/notation/renderer.d.ts.map +1 -0
- package/dist/notation/renderer.js +315 -0
- package/dist/notation/renderer.js.map +1 -0
- package/dist/notation/traceRenderer.d.ts +39 -0
- package/dist/notation/traceRenderer.d.ts.map +1 -0
- package/dist/notation/traceRenderer.js +358 -0
- package/dist/notation/traceRenderer.js.map +1 -0
- package/dist/notation/types.d.ts +66 -0
- package/dist/notation/types.d.ts.map +1 -0
- package/dist/notation/types.js +10 -0
- package/dist/notation/types.js.map +1 -0
- package/dist/queries/NodeContext.d.ts +81 -0
- package/dist/queries/NodeContext.d.ts.map +1 -0
- package/dist/queries/NodeContext.js +196 -0
- package/dist/queries/NodeContext.js.map +1 -0
- package/dist/queries/findCallsInFunction.d.ts +62 -0
- package/dist/queries/findCallsInFunction.d.ts.map +1 -0
- package/dist/queries/findCallsInFunction.js +169 -0
- package/dist/queries/findCallsInFunction.js.map +1 -0
- package/dist/queries/findContainingFunction.d.ts +57 -0
- package/dist/queries/findContainingFunction.d.ts.map +1 -0
- package/dist/queries/findContainingFunction.js +91 -0
- package/dist/queries/findContainingFunction.js.map +1 -0
- package/dist/queries/index.d.ts +18 -0
- package/dist/queries/index.d.ts.map +1 -0
- package/dist/queries/index.js +14 -0
- package/dist/queries/index.js.map +1 -0
- package/dist/queries/traceDataflow.d.ts +65 -0
- package/dist/queries/traceDataflow.d.ts.map +1 -0
- package/dist/queries/traceDataflow.js +754 -0
- package/dist/queries/traceDataflow.js.map +1 -0
- package/dist/queries/traceValues.d.ts +70 -0
- package/dist/queries/traceValues.d.ts.map +1 -0
- package/dist/queries/traceValues.js +373 -0
- package/dist/queries/traceValues.js.map +1 -0
- package/dist/queries/types.d.ts +166 -0
- package/dist/queries/types.d.ts.map +1 -0
- package/dist/queries/types.js +10 -0
- package/dist/queries/types.js.map +1 -0
- package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
- package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/GraphSchemaExtractor.js +125 -0
- package/dist/schema/GraphSchemaExtractor.js.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
- package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
- package/dist/schema/InterfaceSchemaExtractor.js +113 -0
- package/dist/schema/InterfaceSchemaExtractor.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +3 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts +356 -0
- package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
- package/dist/storage/backends/RFDBServerBackend.js +748 -0
- package/dist/storage/backends/RFDBServerBackend.js.map +1 -0
- package/dist/storage/backends/typeValidation.d.ts +47 -0
- package/dist/storage/backends/typeValidation.d.ts.map +1 -0
- package/dist/storage/backends/typeValidation.js +141 -0
- package/dist/storage/backends/typeValidation.js.map +1 -0
- package/dist/utils/findRfdbBinary.d.ts +67 -0
- package/dist/utils/findRfdbBinary.d.ts.map +1 -0
- package/dist/utils/findRfdbBinary.js +261 -0
- package/dist/utils/findRfdbBinary.js.map +1 -0
- package/dist/utils/lazyDownload.d.ts +43 -0
- package/dist/utils/lazyDownload.d.ts.map +1 -0
- package/dist/utils/lazyDownload.js +175 -0
- package/dist/utils/lazyDownload.js.map +1 -0
- package/dist/utils/moduleResolution.d.ts +134 -0
- package/dist/utils/moduleResolution.d.ts.map +1 -0
- package/dist/utils/moduleResolution.js +189 -0
- package/dist/utils/moduleResolution.js.map +1 -0
- package/dist/utils/resolveNodeFile.d.ts +13 -0
- package/dist/utils/resolveNodeFile.d.ts.map +1 -0
- package/dist/utils/resolveNodeFile.js +18 -0
- package/dist/utils/resolveNodeFile.js.map +1 -0
- package/dist/utils/startRfdbServer.d.ts +63 -0
- package/dist/utils/startRfdbServer.d.ts.map +1 -0
- package/dist/utils/startRfdbServer.js +142 -0
- package/dist/utils/startRfdbServer.js.map +1 -0
- package/dist/validation/PathValidator.d.ts +80 -0
- package/dist/validation/PathValidator.d.ts.map +1 -0
- package/dist/validation/PathValidator.js +252 -0
- package/dist/validation/PathValidator.js.map +1 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +26 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -0
- package/src/api/GraphAPI.ts +307 -0
- package/src/api/GuaranteeAPI.ts +402 -0
- package/src/config/ConfigLoader.ts +653 -0
- package/src/config/index.ts +13 -0
- package/src/core/CoverageAnalyzer.ts +243 -0
- package/src/core/FileExplainer.ts +179 -0
- package/src/core/FileOverview.ts +397 -0
- package/src/core/GrafemaUri.ts +216 -0
- package/src/core/GraphBackend.ts +266 -0
- package/src/core/GraphFreshnessChecker.ts +145 -0
- package/src/core/GuaranteeManager.ts +684 -0
- package/src/core/HashUtils.ts +48 -0
- package/src/core/IncrementalReanalyzer.ts +106 -0
- package/src/core/ResourceRegistry.ts +39 -0
- package/src/core/SemanticId.ts +423 -0
- package/src/core/VersionManager.ts +405 -0
- package/src/core/brandNodeInternal.ts +16 -0
- package/src/core/nodes/GuaranteeNode.ts +162 -0
- package/src/core/nodes/IssueNode.ts +177 -0
- package/src/core/nodes/NodeKind.ts +192 -0
- package/src/diagnostics/DiagnosticCollector.ts +170 -0
- package/src/diagnostics/DiagnosticReporter.ts +395 -0
- package/src/diagnostics/DiagnosticWriter.ts +50 -0
- package/src/diagnostics/categories.ts +104 -0
- package/src/diagnostics/index.ts +30 -0
- package/src/errors/GrafemaError.ts +297 -0
- package/src/index.ts +261 -0
- package/src/instructions/index.ts +21 -0
- package/src/instructions/onboarding.md +133 -0
- package/src/knowledge/KnowledgeBase.ts +486 -0
- package/src/knowledge/SemanticAddressResolver.ts +191 -0
- package/src/knowledge/git-ingest.ts +402 -0
- package/src/knowledge/git-queries.ts +269 -0
- package/src/knowledge/index.ts +29 -0
- package/src/knowledge/parser.ts +294 -0
- package/src/knowledge/types.ts +146 -0
- package/src/logging/Logger.ts +303 -0
- package/src/notation/archetypes.ts +189 -0
- package/src/notation/fold.ts +696 -0
- package/src/notation/index.ts +27 -0
- package/src/notation/lodExtractor.ts +177 -0
- package/src/notation/nameShortener.ts +24 -0
- package/src/notation/perspectives.ts +18 -0
- package/src/notation/renderer.ts +394 -0
- package/src/notation/traceRenderer.ts +458 -0
- package/src/notation/types.ts +79 -0
- package/src/queries/NodeContext.ts +280 -0
- package/src/queries/findCallsInFunction.ts +249 -0
- package/src/queries/findContainingFunction.ts +124 -0
- package/src/queries/index.ts +44 -0
- package/src/queries/traceDataflow.ts +838 -0
- package/src/queries/traceValues.ts +531 -0
- package/src/queries/types.ts +191 -0
- package/src/schema/GraphSchemaExtractor.ts +177 -0
- package/src/schema/InterfaceSchemaExtractor.ts +173 -0
- package/src/schema/index.ts +5 -0
- package/src/storage/backends/RFDBServerBackend.ts +895 -0
- package/src/storage/backends/typeValidation.ts +154 -0
- package/src/utils/findRfdbBinary.ts +288 -0
- package/src/utils/lazyDownload.ts +206 -0
- package/src/utils/moduleResolution.ts +271 -0
- package/src/utils/resolveNodeFile.ts +18 -0
- package/src/utils/startRfdbServer.ts +197 -0
- package/src/validation/PathValidator.ts +334 -0
- package/src/version.ts +28 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Query Functions for KnowledgeBase
|
|
3
|
+
*
|
|
4
|
+
* Provides analytical queries over git data (COMMIT, AUTHOR nodes)
|
|
5
|
+
* stored in the KnowledgeBase. Operates on KBNode arrays loaded from
|
|
6
|
+
* YAML files into the KB index.
|
|
7
|
+
*
|
|
8
|
+
* COMMIT nodes carry raw YAML fields: hash, message, author_ref, date, files.
|
|
9
|
+
* AUTHOR nodes carry: name, emails, aliases.
|
|
10
|
+
* These are accessed via type assertion since they're extra properties
|
|
11
|
+
* on KBNode objects loaded from YAML frontmatter.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { KBNode } from './types.js';
|
|
15
|
+
import type { KnowledgeBase } from './KnowledgeBase.js';
|
|
16
|
+
|
|
17
|
+
// -- Public result types --
|
|
18
|
+
|
|
19
|
+
export interface ChurnEntry {
|
|
20
|
+
path: string;
|
|
21
|
+
changeCount: number;
|
|
22
|
+
totalAdded: number;
|
|
23
|
+
totalRemoved: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface CoChangeEntry {
|
|
27
|
+
path: string;
|
|
28
|
+
coChangeCount: number;
|
|
29
|
+
support: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface OwnershipEntry {
|
|
33
|
+
authorId: string;
|
|
34
|
+
authorName: string;
|
|
35
|
+
commits: number;
|
|
36
|
+
linesAdded: number;
|
|
37
|
+
linesRemoved: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ArchaeologyEntry {
|
|
41
|
+
path: string;
|
|
42
|
+
lastCommitHash: string;
|
|
43
|
+
lastCommitDate: string;
|
|
44
|
+
lastAuthor: string;
|
|
45
|
+
firstCommitDate: string;
|
|
46
|
+
firstAuthor: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// -- Internal raw-data accessors --
|
|
50
|
+
|
|
51
|
+
interface RawCommitFile {
|
|
52
|
+
path: string;
|
|
53
|
+
added: number;
|
|
54
|
+
removed: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getCommitHash(node: KBNode): string {
|
|
58
|
+
return (node as any).hash ?? '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getCommitDate(node: KBNode): string {
|
|
62
|
+
return (node as any).date ?? '';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getCommitFiles(node: KBNode): RawCommitFile[] {
|
|
66
|
+
return (node as any).files ?? [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getCommitAuthorRef(node: KBNode): string {
|
|
70
|
+
return (node as any).author_ref ?? '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getAuthorName(node: KBNode): string {
|
|
74
|
+
return (node as any).name ?? '';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// -- Query functions --
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Compute file churn: how many commits touch each file and total lines added/removed.
|
|
81
|
+
*
|
|
82
|
+
* @param kb - KnowledgeBase instance (must be loaded)
|
|
83
|
+
* @param opts.limit - Return only the top N entries
|
|
84
|
+
* @param opts.since - ISO date string; only consider commits on or after this date
|
|
85
|
+
* @returns Churn entries sorted by changeCount descending
|
|
86
|
+
*/
|
|
87
|
+
export async function getChurn(
|
|
88
|
+
kb: KnowledgeBase,
|
|
89
|
+
opts?: { limit?: number; since?: string },
|
|
90
|
+
): Promise<ChurnEntry[]> {
|
|
91
|
+
const commits = await kb.queryNodes({ type: 'COMMIT' });
|
|
92
|
+
|
|
93
|
+
const filtered = opts?.since
|
|
94
|
+
? commits.filter(c => getCommitDate(c) >= opts.since!)
|
|
95
|
+
: commits;
|
|
96
|
+
|
|
97
|
+
const map = new Map<string, ChurnEntry>();
|
|
98
|
+
|
|
99
|
+
for (const commit of filtered) {
|
|
100
|
+
for (const file of getCommitFiles(commit)) {
|
|
101
|
+
let entry = map.get(file.path);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
entry = { path: file.path, changeCount: 0, totalAdded: 0, totalRemoved: 0 };
|
|
104
|
+
map.set(file.path, entry);
|
|
105
|
+
}
|
|
106
|
+
entry.changeCount += 1;
|
|
107
|
+
entry.totalAdded += file.added;
|
|
108
|
+
entry.totalRemoved += file.removed;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sorted = Array.from(map.values()).sort((a, b) => b.changeCount - a.changeCount);
|
|
113
|
+
|
|
114
|
+
return opts?.limit ? sorted.slice(0, opts.limit) : sorted;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Find files that frequently change together with the given file.
|
|
119
|
+
*
|
|
120
|
+
* For every commit that touches `filePath`, counts how many of those commits
|
|
121
|
+
* also touch each other file. `support` is the ratio of co-change count to
|
|
122
|
+
* the total number of commits touching `filePath`.
|
|
123
|
+
*
|
|
124
|
+
* @param kb - KnowledgeBase instance (must be loaded)
|
|
125
|
+
* @param filePath - The file to find co-changes for
|
|
126
|
+
* @param opts.minSupport - Minimum support threshold (default 0.1 = 10%)
|
|
127
|
+
* @returns Co-change entries sorted by coChangeCount descending
|
|
128
|
+
*/
|
|
129
|
+
export async function getCoChanges(
|
|
130
|
+
kb: KnowledgeBase,
|
|
131
|
+
filePath: string,
|
|
132
|
+
opts?: { minSupport?: number },
|
|
133
|
+
): Promise<CoChangeEntry[]> {
|
|
134
|
+
const commits = await kb.queryNodes({ type: 'COMMIT' });
|
|
135
|
+
const minSupport = opts?.minSupport ?? 0.1;
|
|
136
|
+
|
|
137
|
+
// Find commits that touch the target file
|
|
138
|
+
const touchingCommits = commits.filter(c =>
|
|
139
|
+
getCommitFiles(c).some(f => f.path === filePath),
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const totalTouching = touchingCommits.length;
|
|
143
|
+
if (totalTouching === 0) return [];
|
|
144
|
+
|
|
145
|
+
// Count co-occurrences of other files
|
|
146
|
+
const coMap = new Map<string, number>();
|
|
147
|
+
|
|
148
|
+
for (const commit of touchingCommits) {
|
|
149
|
+
for (const file of getCommitFiles(commit)) {
|
|
150
|
+
if (file.path === filePath) continue;
|
|
151
|
+
coMap.set(file.path, (coMap.get(file.path) ?? 0) + 1);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const entries: CoChangeEntry[] = [];
|
|
156
|
+
|
|
157
|
+
for (const [path, count] of coMap) {
|
|
158
|
+
const support = count / totalTouching;
|
|
159
|
+
if (support >= minSupport) {
|
|
160
|
+
entries.push({ path, coChangeCount: count, support });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return entries.sort((a, b) => b.coChangeCount - a.coChangeCount);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Compute ownership of a file: who committed changes and how much.
|
|
169
|
+
*
|
|
170
|
+
* Groups commits touching `filePath` by author_ref, sums commit count and
|
|
171
|
+
* lines added/removed. Looks up author names from AUTHOR nodes in the KB.
|
|
172
|
+
*
|
|
173
|
+
* @param kb - KnowledgeBase instance (must be loaded)
|
|
174
|
+
* @param filePath - The file to compute ownership for
|
|
175
|
+
* @returns Ownership entries sorted by commits descending
|
|
176
|
+
*/
|
|
177
|
+
export async function getOwnership(
|
|
178
|
+
kb: KnowledgeBase,
|
|
179
|
+
filePath: string,
|
|
180
|
+
): Promise<OwnershipEntry[]> {
|
|
181
|
+
const commits = await kb.queryNodes({ type: 'COMMIT' });
|
|
182
|
+
const authors = await kb.queryNodes({ type: 'AUTHOR' });
|
|
183
|
+
|
|
184
|
+
// Build author name lookup: id -> name
|
|
185
|
+
const authorNameById = new Map<string, string>();
|
|
186
|
+
for (const author of authors) {
|
|
187
|
+
authorNameById.set(author.id, getAuthorName(author));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Accumulate per-author stats
|
|
191
|
+
const map = new Map<string, { commits: number; linesAdded: number; linesRemoved: number }>();
|
|
192
|
+
|
|
193
|
+
for (const commit of commits) {
|
|
194
|
+
const files = getCommitFiles(commit);
|
|
195
|
+
const fileEntry = files.find(f => f.path === filePath);
|
|
196
|
+
if (!fileEntry) continue;
|
|
197
|
+
|
|
198
|
+
const authorRef = getCommitAuthorRef(commit);
|
|
199
|
+
let stats = map.get(authorRef);
|
|
200
|
+
if (!stats) {
|
|
201
|
+
stats = { commits: 0, linesAdded: 0, linesRemoved: 0 };
|
|
202
|
+
map.set(authorRef, stats);
|
|
203
|
+
}
|
|
204
|
+
stats.commits += 1;
|
|
205
|
+
stats.linesAdded += fileEntry.added;
|
|
206
|
+
stats.linesRemoved += fileEntry.removed;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const entries: OwnershipEntry[] = [];
|
|
210
|
+
|
|
211
|
+
for (const [authorId, stats] of map) {
|
|
212
|
+
entries.push({
|
|
213
|
+
authorId,
|
|
214
|
+
authorName: authorNameById.get(authorId) ?? authorId,
|
|
215
|
+
commits: stats.commits,
|
|
216
|
+
linesAdded: stats.linesAdded,
|
|
217
|
+
linesRemoved: stats.linesRemoved,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return entries.sort((a, b) => b.commits - a.commits);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Dig into the history of a file: when it was first created, last modified, and by whom.
|
|
226
|
+
*
|
|
227
|
+
* @param kb - KnowledgeBase instance (must be loaded)
|
|
228
|
+
* @param filePath - The file to investigate
|
|
229
|
+
* @returns Archaeology entry with first/last commit info, or empty strings if no commits found
|
|
230
|
+
*/
|
|
231
|
+
export async function getArchaeology(
|
|
232
|
+
kb: KnowledgeBase,
|
|
233
|
+
filePath: string,
|
|
234
|
+
): Promise<ArchaeologyEntry> {
|
|
235
|
+
const commits = await kb.queryNodes({ type: 'COMMIT' });
|
|
236
|
+
|
|
237
|
+
// Filter to commits touching this file
|
|
238
|
+
const touching = commits.filter(c =>
|
|
239
|
+
getCommitFiles(c).some(f => f.path === filePath),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (touching.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
path: filePath,
|
|
245
|
+
lastCommitHash: '',
|
|
246
|
+
lastCommitDate: '',
|
|
247
|
+
lastAuthor: '',
|
|
248
|
+
firstCommitDate: '',
|
|
249
|
+
firstAuthor: '',
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Sort by date ascending
|
|
254
|
+
const sorted = touching.sort((a, b) =>
|
|
255
|
+
getCommitDate(a).localeCompare(getCommitDate(b)),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const first = sorted[0];
|
|
259
|
+
const last = sorted[sorted.length - 1];
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
path: filePath,
|
|
263
|
+
lastCommitHash: getCommitHash(last),
|
|
264
|
+
lastCommitDate: getCommitDate(last),
|
|
265
|
+
lastAuthor: getCommitAuthorRef(last),
|
|
266
|
+
firstCommitDate: getCommitDate(first),
|
|
267
|
+
firstAuthor: getCommitAuthorRef(first),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Base — persistent knowledge layer for Grafema
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { KnowledgeBase } from './KnowledgeBase.js';
|
|
6
|
+
export { SemanticAddressResolver, parseSemanticAddress } from './SemanticAddressResolver.js';
|
|
7
|
+
export type { ResolverBackend } from './SemanticAddressResolver.js';
|
|
8
|
+
export { parseFrontmatter, parseKBNode, serializeKBNode, parseEdgesFile, appendEdge } from './parser.js';
|
|
9
|
+
export type {
|
|
10
|
+
KBNodeType,
|
|
11
|
+
KBLifecycle,
|
|
12
|
+
KBScope,
|
|
13
|
+
KBNodeBase,
|
|
14
|
+
KBDecision,
|
|
15
|
+
KBFact,
|
|
16
|
+
KBSession,
|
|
17
|
+
KBNode,
|
|
18
|
+
KBEdge,
|
|
19
|
+
KBStats,
|
|
20
|
+
KBQueryFilter,
|
|
21
|
+
ParsedSemanticAddress,
|
|
22
|
+
ResolvedAddress,
|
|
23
|
+
DanglingCodeRef,
|
|
24
|
+
} from './types.js';
|
|
25
|
+
export { parseYamlArrayFile } from './parser.js';
|
|
26
|
+
export { GitIngest, parseGitLog, normalizeAuthors } from './git-ingest.js';
|
|
27
|
+
export type { RawCommit, FileChange, AuthorEntry, CommitEntry, IngestResult, Meta } from './git-ingest.js';
|
|
28
|
+
export { getChurn, getCoChanges, getOwnership, getArchaeology } from './git-queries.js';
|
|
29
|
+
export type { ChurnEntry, CoChangeEntry, OwnershipEntry, ArchaeologyEntry } from './git-queries.js';
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge Base Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses markdown files with YAML frontmatter into KBNode objects.
|
|
5
|
+
* Serializes KBNode objects back to markdown format.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
9
|
+
import { readFileSync, appendFileSync, mkdirSync, existsSync } from 'fs';
|
|
10
|
+
import { dirname } from 'path';
|
|
11
|
+
import type { KBNode, KBNodeType, KBEdge, KBLifecycle, KBDecision, KBFact, KBSession, KBScope } from './types.js';
|
|
12
|
+
|
|
13
|
+
const VALID_TYPES: KBNodeType[] = ['DECISION', 'FACT', 'SESSION', 'COMMIT', 'FILE_CHANGE', 'AUTHOR', 'TICKET', 'INCIDENT'];
|
|
14
|
+
const ID_PATTERN = /^kb:[a-z_]+:[a-z0-9_-]+$/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Split markdown content into YAML frontmatter and body.
|
|
18
|
+
*/
|
|
19
|
+
export function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
20
|
+
const trimmed = content.trimStart();
|
|
21
|
+
if (!trimmed.startsWith('---\n') && !trimmed.startsWith('---\r\n')) {
|
|
22
|
+
throw new Error('Missing frontmatter: file must start with ---');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Find closing --- delimiter
|
|
26
|
+
const firstNewline = trimmed.indexOf('\n');
|
|
27
|
+
const rest = trimmed.slice(firstNewline + 1);
|
|
28
|
+
const closingIndex = rest.indexOf('\n---');
|
|
29
|
+
|
|
30
|
+
if (closingIndex === -1) {
|
|
31
|
+
throw new Error('Missing frontmatter: no closing --- delimiter found');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const yamlContent = rest.slice(0, closingIndex);
|
|
35
|
+
const body = rest.slice(closingIndex + 4).replace(/^\r?\n/, ''); // skip \n--- and leading newline
|
|
36
|
+
|
|
37
|
+
const frontmatter = parseYaml(yamlContent) as Record<string, unknown>;
|
|
38
|
+
if (!frontmatter || typeof frontmatter !== 'object') {
|
|
39
|
+
throw new Error('Invalid frontmatter: YAML must be an object');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { frontmatter, body };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive lifecycle from file path.
|
|
47
|
+
* declared/ → declared, derived/ → derived, synced/ → synced
|
|
48
|
+
*/
|
|
49
|
+
function deriveLifecycle(filePath: string): KBLifecycle {
|
|
50
|
+
if (filePath.includes('/derived/') || filePath.includes('\\derived\\')) return 'derived';
|
|
51
|
+
if (filePath.includes('/synced/') || filePath.includes('\\synced\\')) return 'synced';
|
|
52
|
+
return 'declared'; // default
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate and construct a typed KBNode from parsed frontmatter and body.
|
|
57
|
+
*/
|
|
58
|
+
export function parseKBNode(frontmatter: Record<string, unknown>, body: string, filePath: string): KBNode {
|
|
59
|
+
// Required: id
|
|
60
|
+
const id = frontmatter.id;
|
|
61
|
+
if (typeof id !== 'string' || !id) {
|
|
62
|
+
throw new Error(`Missing required field "id" in ${filePath}`);
|
|
63
|
+
}
|
|
64
|
+
if (!ID_PATTERN.test(id)) {
|
|
65
|
+
throw new Error(`Invalid ID format "${id}" in ${filePath}. Must match kb:<type>:<slug> (lowercase, hyphens, underscores, digits)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Required: type
|
|
69
|
+
const type = frontmatter.type;
|
|
70
|
+
if (typeof type !== 'string' || !VALID_TYPES.includes(type as KBNodeType)) {
|
|
71
|
+
throw new Error(`Invalid or missing type "${type}" in ${filePath}. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const lifecycle = deriveLifecycle(filePath);
|
|
75
|
+
|
|
76
|
+
// Base node
|
|
77
|
+
const base: KBNode = {
|
|
78
|
+
id: id as string,
|
|
79
|
+
type: type as KBNodeType,
|
|
80
|
+
projections: Array.isArray(frontmatter.projections) ? frontmatter.projections as string[] : [],
|
|
81
|
+
created: String(frontmatter.created ?? ''),
|
|
82
|
+
content: body.trim(),
|
|
83
|
+
filePath,
|
|
84
|
+
lifecycle,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// Optional base fields
|
|
88
|
+
if (frontmatter.subtype) base.subtype = String(frontmatter.subtype);
|
|
89
|
+
if (frontmatter.scope) base.scope = frontmatter.scope as KBScope;
|
|
90
|
+
if (frontmatter.source) base.source = String(frontmatter.source);
|
|
91
|
+
if (Array.isArray(frontmatter.relates_to)) base.relates_to = frontmatter.relates_to as string[];
|
|
92
|
+
|
|
93
|
+
// Type-specific fields
|
|
94
|
+
switch (type) {
|
|
95
|
+
case 'DECISION': {
|
|
96
|
+
const decision = base as KBDecision;
|
|
97
|
+
decision.status = (frontmatter.status as KBDecision['status']) ?? 'proposed';
|
|
98
|
+
if (frontmatter.effective_from) decision.effective_from = String(frontmatter.effective_from);
|
|
99
|
+
if (frontmatter.effective_until) decision.effective_until = String(frontmatter.effective_until);
|
|
100
|
+
if (Array.isArray(frontmatter.applies_to)) decision.applies_to = frontmatter.applies_to as string[];
|
|
101
|
+
if (frontmatter.superseded_by) decision.superseded_by = String(frontmatter.superseded_by);
|
|
102
|
+
return decision;
|
|
103
|
+
}
|
|
104
|
+
case 'FACT': {
|
|
105
|
+
const fact = base as KBFact;
|
|
106
|
+
if (frontmatter.confidence) fact.confidence = frontmatter.confidence as KBFact['confidence'];
|
|
107
|
+
if (frontmatter.superseded_by) fact.superseded_by = String(frontmatter.superseded_by);
|
|
108
|
+
return fact;
|
|
109
|
+
}
|
|
110
|
+
case 'SESSION': {
|
|
111
|
+
const session = base as KBSession;
|
|
112
|
+
if (frontmatter.task_id) session.task_id = String(frontmatter.task_id);
|
|
113
|
+
if (frontmatter.session_path) session.session_path = String(frontmatter.session_path);
|
|
114
|
+
if (Array.isArray(frontmatter.produced)) session.produced = frontmatter.produced as string[];
|
|
115
|
+
return session;
|
|
116
|
+
}
|
|
117
|
+
default:
|
|
118
|
+
return base;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Serialize a KBNode back to markdown with YAML frontmatter.
|
|
124
|
+
*/
|
|
125
|
+
export function serializeKBNode(node: KBNode): string {
|
|
126
|
+
// Build frontmatter object (exclude computed/internal fields)
|
|
127
|
+
const fm: Record<string, unknown> = {
|
|
128
|
+
id: node.id,
|
|
129
|
+
type: node.type,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Decision-specific
|
|
133
|
+
if (node.type === 'DECISION') {
|
|
134
|
+
const d = node as KBDecision;
|
|
135
|
+
fm.status = d.status;
|
|
136
|
+
if (d.applies_to?.length) fm.applies_to = d.applies_to;
|
|
137
|
+
if (d.effective_from) fm.effective_from = d.effective_from;
|
|
138
|
+
if (d.effective_until) fm.effective_until = d.effective_until;
|
|
139
|
+
if (d.superseded_by) fm.superseded_by = d.superseded_by;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Fact-specific
|
|
143
|
+
if (node.type === 'FACT') {
|
|
144
|
+
const f = node as KBFact;
|
|
145
|
+
if (f.confidence) fm.confidence = f.confidence;
|
|
146
|
+
if (f.superseded_by) fm.superseded_by = f.superseded_by;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Session-specific
|
|
150
|
+
if (node.type === 'SESSION') {
|
|
151
|
+
const s = node as KBSession;
|
|
152
|
+
if (s.task_id) fm.task_id = s.task_id;
|
|
153
|
+
if (s.session_path) fm.session_path = s.session_path;
|
|
154
|
+
if (s.produced?.length) fm.produced = s.produced;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Common optional fields
|
|
158
|
+
if (node.subtype) fm.subtype = node.subtype;
|
|
159
|
+
if (node.scope) fm.scope = node.scope;
|
|
160
|
+
if (node.projections.length > 0) fm.projections = node.projections;
|
|
161
|
+
if (node.source) fm.source = node.source;
|
|
162
|
+
if (node.relates_to?.length) fm.relates_to = node.relates_to;
|
|
163
|
+
fm.created = node.created;
|
|
164
|
+
|
|
165
|
+
const yamlStr = stringifyYaml(fm, { lineWidth: 0 }).trimEnd();
|
|
166
|
+
const body = node.content ? `\n${node.content}\n` : '\n';
|
|
167
|
+
|
|
168
|
+
return `---\n${yamlStr}\n---\n${body}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse edges.yaml file into KBEdge array.
|
|
173
|
+
*/
|
|
174
|
+
export function parseEdgesFile(filePath: string): KBEdge[] {
|
|
175
|
+
if (!existsSync(filePath)) return [];
|
|
176
|
+
|
|
177
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
178
|
+
const parsed = parseYaml(content);
|
|
179
|
+
|
|
180
|
+
if (!parsed) return [];
|
|
181
|
+
if (!Array.isArray(parsed)) {
|
|
182
|
+
throw new Error(`edges.yaml must be a YAML array, got ${typeof parsed}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return parsed.map((entry: unknown, i: number) => {
|
|
186
|
+
if (!entry || typeof entry !== 'object') {
|
|
187
|
+
throw new Error(`Edge at index ${i} is not an object`);
|
|
188
|
+
}
|
|
189
|
+
const e = entry as Record<string, unknown>;
|
|
190
|
+
if (!e.type || !e.from || !e.to) {
|
|
191
|
+
throw new Error(`Edge at index ${i} missing required fields (type, from, to)`);
|
|
192
|
+
}
|
|
193
|
+
const edge: KBEdge = {
|
|
194
|
+
type: String(e.type),
|
|
195
|
+
from: String(e.from),
|
|
196
|
+
to: String(e.to),
|
|
197
|
+
};
|
|
198
|
+
if (e.evidence) edge.evidence = String(e.evidence);
|
|
199
|
+
return edge;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Parse a YAML file containing an array of node objects (e.g., commits/2026-03.yaml, authors.yaml).
|
|
205
|
+
* Each entry must have at least a `type` field. ID is generated based on type:
|
|
206
|
+
* - COMMIT → kb:commit:<short-hash>
|
|
207
|
+
* - AUTHOR → kb:author:<slug>
|
|
208
|
+
* - Others → kb:<type>:<index>
|
|
209
|
+
*/
|
|
210
|
+
export function parseYamlArrayFile(filePath: string): KBNode[] {
|
|
211
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
212
|
+
const parsed = parseYaml(content);
|
|
213
|
+
|
|
214
|
+
if (!Array.isArray(parsed)) {
|
|
215
|
+
throw new Error(`YAML array file must contain an array, got ${typeof parsed} in ${filePath}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return parsed.map((entry: unknown, index: number) => {
|
|
219
|
+
if (!entry || typeof entry !== 'object') {
|
|
220
|
+
throw new Error(`Entry at index ${index} is not an object in ${filePath}`);
|
|
221
|
+
}
|
|
222
|
+
const e = entry as Record<string, unknown>;
|
|
223
|
+
|
|
224
|
+
const type = e.type;
|
|
225
|
+
if (typeof type !== 'string' || !VALID_TYPES.includes(type as KBNodeType)) {
|
|
226
|
+
throw new Error(`Invalid or missing type "${type}" at index ${index} in ${filePath}. Must be one of: ${VALID_TYPES.join(', ')}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Generate ID based on type
|
|
230
|
+
let id: string;
|
|
231
|
+
switch (type) {
|
|
232
|
+
case 'COMMIT':
|
|
233
|
+
id = `kb:commit:${String(e.hash ?? '').slice(0, 8)}`;
|
|
234
|
+
break;
|
|
235
|
+
case 'AUTHOR': {
|
|
236
|
+
if (typeof e.id === 'string' && e.id.startsWith('kb:author:')) {
|
|
237
|
+
id = e.id;
|
|
238
|
+
} else if (typeof e.id === 'string') {
|
|
239
|
+
id = `kb:author:${e.id}`;
|
|
240
|
+
} else {
|
|
241
|
+
const name = String(e.name ?? '');
|
|
242
|
+
id = `kb:author:${name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')}`;
|
|
243
|
+
}
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
id = `kb:${type.toLowerCase()}:${index}`;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const base: KBNode = {
|
|
252
|
+
id,
|
|
253
|
+
type: type as KBNodeType,
|
|
254
|
+
projections: Array.isArray(e.projections) ? e.projections as string[] : [],
|
|
255
|
+
created: type === 'COMMIT' ? (String(e.date ?? '').split('T')[0]) : '',
|
|
256
|
+
content: '',
|
|
257
|
+
filePath,
|
|
258
|
+
lifecycle: 'derived' as KBLifecycle,
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// Spread all entry fields onto the node, preserving base fields
|
|
262
|
+
const node: KBNode = { ...e, ...base } as KBNode;
|
|
263
|
+
|
|
264
|
+
return node;
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Append an edge to the edges.yaml file.
|
|
270
|
+
*/
|
|
271
|
+
export function appendEdge(filePath: string, edge: KBEdge): void {
|
|
272
|
+
const dir = dirname(filePath);
|
|
273
|
+
if (!existsSync(dir)) {
|
|
274
|
+
mkdirSync(dir, { recursive: true });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const entry: Record<string, string> = {
|
|
278
|
+
type: edge.type,
|
|
279
|
+
from: edge.from,
|
|
280
|
+
to: edge.to,
|
|
281
|
+
};
|
|
282
|
+
if (edge.evidence) entry.evidence = edge.evidence;
|
|
283
|
+
|
|
284
|
+
const yamlEntry = stringifyYaml([entry], { lineWidth: 0 }).trimEnd();
|
|
285
|
+
|
|
286
|
+
if (!existsSync(filePath)) {
|
|
287
|
+
// New file — write with comment header
|
|
288
|
+
const header = '# Knowledge Graph edges\n# Format: {type, from, to, evidence?}\n\n';
|
|
289
|
+
appendFileSync(filePath, header + yamlEntry + '\n', 'utf-8');
|
|
290
|
+
} else {
|
|
291
|
+
// Append to existing file
|
|
292
|
+
appendFileSync(filePath, '\n' + yamlEntry + '\n', 'utf-8');
|
|
293
|
+
}
|
|
294
|
+
}
|