kibi-cli 0.2.3 → 0.2.5
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/dist/cli.js +3 -28
- package/dist/commands/aggregated-checks.d.ts +4 -1
- package/dist/commands/aggregated-checks.d.ts.map +1 -1
- package/dist/commands/aggregated-checks.js +13 -3
- package/dist/commands/branch.d.ts.map +1 -1
- package/dist/commands/branch.js +3 -41
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +54 -44
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +0 -27
- package/dist/commands/gc.d.ts.map +1 -1
- package/dist/commands/gc.js +2 -51
- package/dist/commands/init-helpers.d.ts.map +1 -1
- package/dist/commands/init-helpers.js +23 -36
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +0 -27
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +7 -288
- package/dist/commands/sync/cache.d.ts +13 -0
- package/dist/commands/sync/cache.d.ts.map +1 -0
- package/dist/commands/sync/cache.js +76 -0
- package/dist/commands/sync/discovery.d.ts +8 -0
- package/dist/commands/sync/discovery.d.ts.map +1 -0
- package/dist/commands/sync/discovery.js +50 -0
- package/dist/commands/sync/extraction.d.ts +11 -0
- package/dist/commands/sync/extraction.d.ts.map +1 -0
- package/dist/commands/sync/extraction.js +69 -0
- package/dist/commands/sync/manifest.d.ts +5 -0
- package/dist/commands/sync/manifest.d.ts.map +1 -0
- package/dist/commands/sync/manifest.js +118 -0
- package/dist/commands/sync/persistence.d.ts +16 -0
- package/dist/commands/sync/persistence.d.ts.map +1 -0
- package/dist/commands/sync/persistence.js +188 -0
- package/dist/commands/sync/staging.d.ts +4 -0
- package/dist/commands/sync/staging.d.ts.map +1 -0
- package/dist/commands/sync/staging.js +86 -0
- package/dist/commands/sync.d.ts +2 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +69 -501
- package/dist/extractors/manifest.d.ts +0 -1
- package/dist/extractors/manifest.d.ts.map +1 -1
- package/dist/extractors/manifest.js +39 -48
- package/dist/extractors/markdown.d.ts +0 -1
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +16 -49
- package/dist/extractors/relationships.d.ts +39 -0
- package/dist/extractors/relationships.d.ts.map +1 -0
- package/dist/extractors/relationships.js +137 -0
- package/dist/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/extractors/symbols-coordinator.js +0 -27
- package/dist/extractors/symbols-ts.d.ts.map +1 -1
- package/dist/extractors/symbols-ts.js +0 -27
- package/dist/kb/target-resolver.d.ts +80 -0
- package/dist/kb/target-resolver.d.ts.map +1 -0
- package/dist/kb/target-resolver.js +313 -0
- package/dist/prolog/codec.d.ts +63 -0
- package/dist/prolog/codec.d.ts.map +1 -0
- package/dist/prolog/codec.js +434 -0
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +0 -27
- package/dist/public/extractors/symbols-coordinator.d.ts.map +1 -1
- package/dist/public/extractors/symbols-coordinator.js +0 -27
- package/dist/public/prolog/index.d.ts.map +1 -1
- package/dist/public/prolog/index.js +0 -27
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +0 -27
- package/dist/public/schemas/relationship.d.ts.map +1 -1
- package/dist/public/schemas/relationship.js +0 -27
- package/dist/query/service.d.ts +35 -0
- package/dist/query/service.d.ts.map +1 -0
- package/dist/query/service.js +149 -0
- package/dist/relationships/shards.d.ts +68 -0
- package/dist/relationships/shards.d.ts.map +1 -0
- package/dist/relationships/shards.js +263 -0
- package/dist/traceability/git-staged.d.ts +4 -1
- package/dist/traceability/git-staged.d.ts.map +1 -1
- package/dist/traceability/git-staged.js +24 -11
- package/dist/types/changeset.d.ts.map +1 -1
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/types/relationships.d.ts.map +1 -1
- package/dist/utils/branch-resolver.d.ts +4 -0
- package/dist/utils/branch-resolver.d.ts.map +1 -1
- package/dist/utils/branch-resolver.js +4 -0
- package/dist/utils/config.d.ts +10 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +27 -1
- package/dist/utils/rule-registry.d.ts +47 -0
- package/dist/utils/rule-registry.d.ts.map +1 -0
- package/dist/utils/rule-registry.js +139 -0
- package/package.json +5 -1
- package/schema/config.json +156 -0
- package/src/public/extractors/symbols-coordinator.ts +0 -27
- package/src/public/prolog/index.ts +0 -27
- package/src/public/schemas/entity.ts +0 -27
- package/src/public/schemas/relationship.ts +0 -27
package/dist/commands/sync.js
CHANGED
|
@@ -14,213 +14,33 @@
|
|
|
14
14
|
|
|
15
15
|
You should have received a copy of the GNU Affero General Public License
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
18
|
-
/*
|
|
19
|
-
How to apply this header to source files (examples)
|
|
20
|
-
|
|
21
|
-
1) Prepend header to a single file (POSIX shells):
|
|
22
|
-
|
|
23
|
-
cat LICENSE_HEADER.txt "$FILE" > "$FILE".with-header && mv "$FILE".with-header "$FILE"
|
|
24
|
-
|
|
25
|
-
2) Apply to multiple files (example: the project's main entry files):
|
|
26
|
-
|
|
27
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp packages/cli/src/*.ts packages/mcp/src/*.ts; do
|
|
28
|
-
if [ -f "$f" ]; then
|
|
29
|
-
cp "$f" "$f".bak
|
|
30
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
31
|
-
fi
|
|
32
|
-
done
|
|
33
|
-
|
|
34
|
-
3) Avoid duplicating the header: run a quick guard to only add if missing
|
|
35
|
-
|
|
36
|
-
for f in packages/cli/bin/kibi packages/mcp/bin/kibi-mcp; do
|
|
37
|
-
if [ -f "$f" ]; then
|
|
38
|
-
if ! head -n 5 "$f" | grep -q "Copyright (C) 2026 Piotr Franczyk"; then
|
|
39
|
-
cp "$f" "$f".bak
|
|
40
|
-
(cat LICENSE_HEADER.txt; echo; cat "$f" ) > "$f".new && mv "$f".new "$f"
|
|
41
|
-
fi
|
|
42
|
-
fi
|
|
43
|
-
done
|
|
44
|
-
*/
|
|
17
|
+
*/
|
|
45
18
|
import { execSync } from "node:child_process";
|
|
46
|
-
import {
|
|
47
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
48
20
|
import * as path from "node:path";
|
|
49
|
-
import fg from "fast-glob";
|
|
50
|
-
import { dump as dumpYAML, load as parseYAML } from "js-yaml";
|
|
51
|
-
import { extractFromManifest } from "../extractors/manifest.js";
|
|
52
|
-
import { FrontmatterError, extractFromMarkdown, } from "../extractors/markdown.js";
|
|
53
|
-
import { copyFileSync } from "node:fs";
|
|
54
21
|
import { branchErrorToDiagnostic, createDocsNotIndexedDiagnostic, createInvalidAuthoringDiagnostic, createKbMissingDiagnostic, formatSyncSummary, } from "../diagnostics.js";
|
|
55
|
-
import {
|
|
22
|
+
import { extractFromRelationshipShards, flattenRelationships, validateRelationships, } from "../extractors/relationships.js";
|
|
56
23
|
import { PrologProcess } from "../prolog.js";
|
|
57
|
-
import {
|
|
24
|
+
import { resolveActiveBranch, } from "../utils/branch-resolver.js";
|
|
58
25
|
import { loadSyncConfig } from "../utils/config.js";
|
|
26
|
+
import { SYNC_CACHE_TTL_MS, SYNC_CACHE_VERSION, hashFile, readSyncCache, toCacheKey, writeSyncCache, } from "./sync/cache.js";
|
|
27
|
+
import { discoverSourceFiles, } from "./sync/discovery.js";
|
|
28
|
+
import { processExtractions } from "./sync/extraction.js";
|
|
29
|
+
import { refreshManifestCoordinates } from "./sync/manifest.js";
|
|
30
|
+
import { persistEntities, persistRelationships } from "./sync/persistence.js";
|
|
31
|
+
import { atomicPublish, cleanupStaging, prepareStagingEnvironment, } from "./sync/staging.js";
|
|
59
32
|
export class SyncError extends Error {
|
|
60
33
|
constructor(message) {
|
|
61
34
|
super(message);
|
|
62
35
|
this.name = "SyncError";
|
|
63
36
|
}
|
|
64
37
|
}
|
|
65
|
-
const SYNC_CACHE_VERSION = 1;
|
|
66
|
-
const SYNC_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
67
|
-
const SYMBOLS_MANIFEST_COMMENT_BLOCK = `# symbols.yaml
|
|
68
|
-
# AUTHORED fields (edit freely):
|
|
69
|
-
# id, title, sourceFile, links, status, tags, owner, priority
|
|
70
|
-
# GENERATED fields (never edit manually — overwritten by kibi sync and kb.symbols.refresh):
|
|
71
|
-
# sourceLine, sourceColumn, sourceEndLine, sourceEndColumn, coordinatesGeneratedAt
|
|
72
|
-
# Run \`kibi sync\` or call the \`kb.symbols.refresh\` MCP tool to refresh coordinates.
|
|
73
|
-
`;
|
|
74
|
-
const SYMBOL_COORD_EXTENSIONS = new Set([
|
|
75
|
-
".ts",
|
|
76
|
-
".tsx",
|
|
77
|
-
".js",
|
|
78
|
-
".jsx",
|
|
79
|
-
".mts",
|
|
80
|
-
".cts",
|
|
81
|
-
".mjs",
|
|
82
|
-
".cjs",
|
|
83
|
-
]);
|
|
84
|
-
const GENERATED_COORD_FIELDS = [
|
|
85
|
-
"sourceLine",
|
|
86
|
-
"sourceColumn",
|
|
87
|
-
"sourceEndLine",
|
|
88
|
-
"sourceEndColumn",
|
|
89
|
-
"coordinatesGeneratedAt",
|
|
90
|
-
];
|
|
91
|
-
function toCacheKey(filePath) {
|
|
92
|
-
return path.relative(process.cwd(), filePath).split(path.sep).join("/");
|
|
93
|
-
}
|
|
94
|
-
function hashFile(filePath) {
|
|
95
|
-
const content = readFileSync(filePath);
|
|
96
|
-
return createHash("sha256").update(content).digest("hex");
|
|
97
|
-
}
|
|
98
|
-
function readSyncCache(cachePath) {
|
|
99
|
-
if (!existsSync(cachePath)) {
|
|
100
|
-
return {
|
|
101
|
-
version: SYNC_CACHE_VERSION,
|
|
102
|
-
hashes: {},
|
|
103
|
-
seenAt: {},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
try {
|
|
107
|
-
const parsed = JSON.parse(readFileSync(cachePath, "utf8"));
|
|
108
|
-
if (parsed.version !== SYNC_CACHE_VERSION) {
|
|
109
|
-
return {
|
|
110
|
-
version: SYNC_CACHE_VERSION,
|
|
111
|
-
hashes: {},
|
|
112
|
-
seenAt: {},
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
version: SYNC_CACHE_VERSION,
|
|
117
|
-
hashes: parsed.hashes ?? {},
|
|
118
|
-
seenAt: parsed.seenAt ?? {},
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return {
|
|
123
|
-
version: SYNC_CACHE_VERSION,
|
|
124
|
-
hashes: {},
|
|
125
|
-
seenAt: {},
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
function writeSyncCache(cachePath, cache) {
|
|
130
|
-
const cacheDir = path.dirname(cachePath);
|
|
131
|
-
if (!existsSync(cacheDir)) {
|
|
132
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
133
|
-
}
|
|
134
|
-
writeFileSync(cachePath, `${JSON.stringify(cache, null, 2)}
|
|
135
|
-
`, "utf8");
|
|
136
|
-
}
|
|
137
|
-
function copySyncCache(livePath, stagingPath) {
|
|
138
|
-
const liveCachePath = path.join(livePath, "sync-cache.json");
|
|
139
|
-
const stagingCachePath = path.join(stagingPath, "sync-cache.json");
|
|
140
|
-
if (existsSync(liveCachePath)) {
|
|
141
|
-
const cacheContent = readFileSync(liveCachePath, "utf8");
|
|
142
|
-
writeFileSync(stagingCachePath, cacheContent, "utf8");
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function copySchemaToStaging(stagingPath) {
|
|
146
|
-
const possibleSchemaPaths = [
|
|
147
|
-
path.resolve(process.cwd(), "node_modules", "kibi-cli", "schema"),
|
|
148
|
-
path.resolve(process.cwd(), "..", "..", "schema"),
|
|
149
|
-
path.resolve(import.meta.dirname || __dirname, "..", "..", "schema"),
|
|
150
|
-
path.resolve(process.cwd(), "packages", "cli", "schema"),
|
|
151
|
-
];
|
|
152
|
-
let schemaSourceDir = null;
|
|
153
|
-
for (const p of possibleSchemaPaths) {
|
|
154
|
-
if (existsSync(p)) {
|
|
155
|
-
schemaSourceDir = p;
|
|
156
|
-
break;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
if (!schemaSourceDir) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const schemaFiles = await fg("*.pl", {
|
|
163
|
-
cwd: schemaSourceDir,
|
|
164
|
-
absolute: false,
|
|
165
|
-
});
|
|
166
|
-
const schemaDestDir = path.join(stagingPath, "schema");
|
|
167
|
-
if (!existsSync(schemaDestDir)) {
|
|
168
|
-
mkdirSync(schemaDestDir, { recursive: true });
|
|
169
|
-
}
|
|
170
|
-
for (const file of schemaFiles) {
|
|
171
|
-
const sourcePath = path.join(schemaSourceDir, file);
|
|
172
|
-
const destPath = path.join(schemaDestDir, file);
|
|
173
|
-
copyFileSync(sourcePath, destPath);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
async function validateStagingKB(stagingPath) {
|
|
177
|
-
const prolog = new PrologProcess({ timeout: 60000 });
|
|
178
|
-
await prolog.start();
|
|
179
|
-
try {
|
|
180
|
-
const attachResult = await prolog.query(`kb_attach('${stagingPath}')`);
|
|
181
|
-
if (!attachResult.success) {
|
|
182
|
-
console.error(`Failed to attach to staging KB: ${attachResult.error}`);
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
await prolog.query("kb_detach");
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
-
console.error(`Validation error: ${message}`);
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
|
-
finally {
|
|
194
|
-
await prolog.terminate();
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
function atomicPublish(stagingPath, livePath) {
|
|
198
|
-
const liveParent = path.dirname(livePath);
|
|
199
|
-
if (!existsSync(liveParent)) {
|
|
200
|
-
mkdirSync(liveParent, { recursive: true });
|
|
201
|
-
}
|
|
202
|
-
if (existsSync(livePath)) {
|
|
203
|
-
const tempPath = `${livePath}.old.${Date.now()}`;
|
|
204
|
-
renameSync(livePath, tempPath);
|
|
205
|
-
renameSync(stagingPath, livePath);
|
|
206
|
-
rmSync(tempPath, { recursive: true, force: true });
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
renameSync(stagingPath, livePath);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
function cleanupStaging(stagingPath) {
|
|
213
|
-
if (existsSync(stagingPath)) {
|
|
214
|
-
rmSync(stagingPath, { recursive: true, force: true });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
38
|
export async function syncCommand(options = {}) {
|
|
218
39
|
const validateOnly = options.validateOnly ?? false;
|
|
219
40
|
const rebuild = options.rebuild ?? false;
|
|
220
41
|
const startTime = Date.now();
|
|
221
42
|
const diagnostics = [];
|
|
222
43
|
const entityCounts = {};
|
|
223
|
-
const relationshipCount = 0;
|
|
224
44
|
let published = false;
|
|
225
45
|
let currentBranch;
|
|
226
46
|
const getCurrentCommit = () => {
|
|
@@ -237,11 +57,12 @@ export async function syncCommand(options = {}) {
|
|
|
237
57
|
}
|
|
238
58
|
};
|
|
239
59
|
try {
|
|
60
|
+
// Branch resolution
|
|
240
61
|
const branchResult = resolveActiveBranch(process.cwd());
|
|
241
62
|
if ("error" in branchResult) {
|
|
242
63
|
const diagnostic = branchErrorToDiagnostic(branchResult.code, branchResult.error);
|
|
243
64
|
diagnostics.push(diagnostic);
|
|
244
|
-
console.error(
|
|
65
|
+
console.error(`Failed to resolve active branch: ${branchResult.error}`);
|
|
245
66
|
throw new SyncError(`Failed to resolve active branch: ${branchResult.error}`);
|
|
246
67
|
}
|
|
247
68
|
currentBranch = branchResult.branch;
|
|
@@ -252,61 +73,33 @@ export async function syncCommand(options = {}) {
|
|
|
252
73
|
}
|
|
253
74
|
catch { }
|
|
254
75
|
}
|
|
255
|
-
// Load config
|
|
76
|
+
// Load config
|
|
256
77
|
const config = loadSyncConfig(process.cwd());
|
|
257
78
|
const paths = config.paths;
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
if (!pattern)
|
|
261
|
-
return null;
|
|
262
|
-
if (pattern.includes("*"))
|
|
263
|
-
return pattern;
|
|
264
|
-
return `${pattern}/**/*.md`;
|
|
265
|
-
};
|
|
266
|
-
const markdownPatterns = [
|
|
267
|
-
normalizeMarkdownPath(paths.requirements),
|
|
268
|
-
normalizeMarkdownPath(paths.scenarios),
|
|
269
|
-
normalizeMarkdownPath(paths.tests),
|
|
270
|
-
normalizeMarkdownPath(paths.adr),
|
|
271
|
-
normalizeMarkdownPath(paths.flags),
|
|
272
|
-
normalizeMarkdownPath(paths.events),
|
|
273
|
-
normalizeMarkdownPath(paths.facts),
|
|
274
|
-
].filter((p) => Boolean(p));
|
|
275
|
-
const markdownFiles = await fg(markdownPatterns, {
|
|
276
|
-
cwd: process.cwd(),
|
|
277
|
-
absolute: true,
|
|
278
|
-
});
|
|
79
|
+
// File discovery
|
|
80
|
+
const { markdownFiles, manifestFiles, relationshipsDir } = await discoverSourceFiles(process.cwd(), paths);
|
|
279
81
|
if (process.env.KIBI_DEBUG) {
|
|
280
82
|
try {
|
|
281
83
|
// eslint-disable-next-line no-console
|
|
282
|
-
console.log("[kibi-debug]
|
|
84
|
+
console.log("[kibi-debug] markdownFiles:", markdownFiles.length);
|
|
283
85
|
// eslint-disable-next-line no-console
|
|
284
|
-
console.log("[kibi-debug]
|
|
86
|
+
console.log("[kibi-debug] manifestFiles:", manifestFiles.length);
|
|
285
87
|
}
|
|
286
88
|
catch { }
|
|
287
89
|
}
|
|
288
|
-
const manifestFiles = paths.symbols
|
|
289
|
-
? await fg(paths.symbols, {
|
|
290
|
-
cwd: process.cwd(),
|
|
291
|
-
absolute: true,
|
|
292
|
-
})
|
|
293
|
-
: [];
|
|
294
90
|
const sourceFiles = [...markdownFiles, ...manifestFiles].sort();
|
|
295
|
-
// Use branch-specific cache to handle branch isolation correctly
|
|
296
91
|
const cachePath = path.join(process.cwd(), `.kb/branches/${currentBranch}/sync-cache.json`);
|
|
297
92
|
const syncCache = readSyncCache(cachePath);
|
|
298
93
|
const nowIso = new Date().toISOString();
|
|
299
94
|
const nowMs = Date.now();
|
|
300
95
|
const nextHashes = {};
|
|
301
96
|
const nextSeenAt = {};
|
|
97
|
+
// Extract relationships from shard files
|
|
98
|
+
const shardResults = extractFromRelationshipShards(relationshipsDir);
|
|
99
|
+
const allRelationships = flattenRelationships(shardResults);
|
|
302
100
|
const changedMarkdownFiles = [];
|
|
303
101
|
const changedManifestFiles = [];
|
|
304
|
-
|
|
305
|
-
// eslint-disable-next-line no-console
|
|
306
|
-
console.log("[kibi-debug] sourceFiles:", sourceFiles);
|
|
307
|
-
// eslint-disable-next-line no-console
|
|
308
|
-
console.log("[kibi-debug] syncCache.hashes:", syncCache.hashes);
|
|
309
|
-
}
|
|
102
|
+
// Detect changed files
|
|
310
103
|
for (const file of sourceFiles) {
|
|
311
104
|
try {
|
|
312
105
|
const key = toCacheKey(file);
|
|
@@ -318,11 +111,7 @@ export async function syncCommand(options = {}) {
|
|
|
318
111
|
: nowMs - lastSeenMs > SYNC_CACHE_TTL_MS;
|
|
319
112
|
nextHashes[key] = hash;
|
|
320
113
|
nextSeenAt[key] = nowIso;
|
|
321
|
-
const isChanged = expired || syncCache.hashes[key] !== hash || validateOnly;
|
|
322
|
-
if (process.env.KIBI_DEBUG) {
|
|
323
|
-
// eslint-disable-next-line no-console
|
|
324
|
-
console.log(`[kibi-debug] ${key}: cached=${syncCache.hashes[key]}, current=${hash}, changed=${isChanged}`);
|
|
325
|
-
}
|
|
114
|
+
const isChanged = expired || syncCache.hashes[key] !== hash || validateOnly || rebuild;
|
|
326
115
|
if (isChanged) {
|
|
327
116
|
if (markdownFiles.includes(file)) {
|
|
328
117
|
changedMarkdownFiles.push(file);
|
|
@@ -337,54 +126,22 @@ export async function syncCommand(options = {}) {
|
|
|
337
126
|
console.warn(`Warning: Failed to hash ${file}: ${message}`);
|
|
338
127
|
}
|
|
339
128
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const embeddedTypes = message.includes("scenario") && message.includes("test")
|
|
357
|
-
? ["scenario", "test"]
|
|
358
|
-
: message.includes("scenario")
|
|
359
|
-
? ["scenario"]
|
|
360
|
-
: message.includes("test")
|
|
361
|
-
? ["test"]
|
|
362
|
-
: ["entity"];
|
|
363
|
-
diagnostics.push(createInvalidAuthoringDiagnostic(file, embeddedTypes));
|
|
364
|
-
}
|
|
365
|
-
if (validateOnly) {
|
|
366
|
-
errors.push({ file, message });
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
console.warn(`Warning: Failed to extract from ${file}: ${message}`);
|
|
370
|
-
}
|
|
371
|
-
failedCacheKeys.add(toCacheKey(file));
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
for (const file of changedManifestFiles) {
|
|
375
|
-
try {
|
|
376
|
-
const manifestResults = extractFromManifest(file);
|
|
377
|
-
results.push(...manifestResults);
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
381
|
-
if (validateOnly) {
|
|
382
|
-
errors.push({ file, message });
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
console.warn(`Warning: Failed to extract from ${file}: ${message}`);
|
|
386
|
-
}
|
|
387
|
-
failedCacheKeys.add(toCacheKey(file));
|
|
129
|
+
// Content extraction
|
|
130
|
+
const { results, failedCacheKeys, errors } = await processExtractions(changedMarkdownFiles, changedManifestFiles, validateOnly);
|
|
131
|
+
// Collect INVALID_AUTHORING diagnostics
|
|
132
|
+
for (const err of errors) {
|
|
133
|
+
const error = new Error(err.message);
|
|
134
|
+
if (err.message.includes("Embedded Entity Violation") ||
|
|
135
|
+
err.message.includes("scenario") ||
|
|
136
|
+
err.message.includes("test")) {
|
|
137
|
+
const embeddedTypes = err.message.includes("scenario") && err.message.includes("test")
|
|
138
|
+
? ["scenario", "test"]
|
|
139
|
+
: err.message.includes("scenario")
|
|
140
|
+
? ["scenario"]
|
|
141
|
+
: err.message.includes("test")
|
|
142
|
+
? ["test"]
|
|
143
|
+
: ["entity"];
|
|
144
|
+
diagnostics.push(createInvalidAuthoringDiagnostic(err.file, embeddedTypes));
|
|
388
145
|
}
|
|
389
146
|
}
|
|
390
147
|
if (validateOnly) {
|
|
@@ -400,6 +157,7 @@ export async function syncCommand(options = {}) {
|
|
|
400
157
|
process.exit(0);
|
|
401
158
|
}
|
|
402
159
|
}
|
|
160
|
+
// Refresh symbol manifest coordinates
|
|
403
161
|
for (const file of manifestFiles) {
|
|
404
162
|
try {
|
|
405
163
|
await refreshManifestCoordinates(file, process.cwd());
|
|
@@ -409,7 +167,8 @@ export async function syncCommand(options = {}) {
|
|
|
409
167
|
console.warn(`Warning: Failed to refresh symbol coordinates in ${file}: ${message}`);
|
|
410
168
|
}
|
|
411
169
|
}
|
|
412
|
-
|
|
170
|
+
// Early exit if no changes
|
|
171
|
+
if (results.length === 0 && allRelationships.length === 0 && !rebuild) {
|
|
413
172
|
const evictedHashes = {};
|
|
414
173
|
const evictedSeenAt = {};
|
|
415
174
|
for (const [key, hash] of Object.entries(nextHashes)) {
|
|
@@ -417,7 +176,7 @@ export async function syncCommand(options = {}) {
|
|
|
417
176
|
continue;
|
|
418
177
|
}
|
|
419
178
|
evictedHashes[key] = hash;
|
|
420
|
-
evictedSeenAt[key] =
|
|
179
|
+
evictedSeenAt[key] = nextHashes[key] ?? nowIso;
|
|
421
180
|
}
|
|
422
181
|
writeSyncCache(cachePath, {
|
|
423
182
|
version: SYNC_CACHE_VERSION,
|
|
@@ -427,37 +186,16 @@ export async function syncCommand(options = {}) {
|
|
|
427
186
|
console.log("✓ Imported 0 entities, 0 relationships (no changes)");
|
|
428
187
|
process.exit(0);
|
|
429
188
|
}
|
|
189
|
+
// Staging setup
|
|
430
190
|
const livePath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
|
|
431
|
-
// Check if KB exists (for diagnostic purposes)
|
|
432
191
|
const kbExists = existsSync(livePath);
|
|
433
192
|
if (!kbExists && !rebuild) {
|
|
434
193
|
diagnostics.push(createKbMissingDiagnostic(currentBranch, livePath));
|
|
435
194
|
}
|
|
436
195
|
const stagingPath = path.join(process.cwd(), `.kb/branches/${currentBranch}.staging`);
|
|
437
|
-
|
|
438
|
-
|
|
196
|
+
await prepareStagingEnvironment(stagingPath, livePath, rebuild);
|
|
197
|
+
// Persistence to KB
|
|
439
198
|
try {
|
|
440
|
-
if (!rebuild) {
|
|
441
|
-
const config = loadSyncConfig(process.cwd());
|
|
442
|
-
const defaultBranchResult = resolveDefaultBranch(process.cwd(), config);
|
|
443
|
-
const defaultBranch = "branch" in defaultBranchResult ? defaultBranchResult.branch : "main";
|
|
444
|
-
const defaultBranchPath = path.join(process.cwd(), `.kb/branches/${defaultBranch}`);
|
|
445
|
-
const sourcePath = existsSync(livePath)
|
|
446
|
-
? livePath
|
|
447
|
-
: existsSync(defaultBranchPath) && currentBranch !== defaultBranch
|
|
448
|
-
? defaultBranchPath
|
|
449
|
-
: null;
|
|
450
|
-
if (sourcePath) {
|
|
451
|
-
copyCleanSnapshot(sourcePath, stagingPath);
|
|
452
|
-
copySyncCache(sourcePath, stagingPath);
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
await copySchemaToStaging(stagingPath);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
else {
|
|
459
|
-
await copySchemaToStaging(stagingPath);
|
|
460
|
-
}
|
|
461
199
|
const prolog = new PrologProcess({ timeout: 120000 });
|
|
462
200
|
await prolog.start();
|
|
463
201
|
const attachResult = await prolog.query(`kb_attach('${stagingPath}')`);
|
|
@@ -465,133 +203,35 @@ export async function syncCommand(options = {}) {
|
|
|
465
203
|
await prolog.terminate();
|
|
466
204
|
throw new SyncError(`Failed to attach to staging KB: ${attachResult.error || "Unknown error"}`);
|
|
467
205
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const prologAtom = (value) => simplePrologAtom.test(value) ? value : `'${value.replace(/'/g, "''")}'`;
|
|
476
|
-
for (const { entity } of results) {
|
|
477
|
-
try {
|
|
478
|
-
const props = [
|
|
479
|
-
`id='${entity.id}'`,
|
|
480
|
-
`title="${entity.title.replace(/"/g, '\\"')}"`,
|
|
481
|
-
`status=${prologAtom(entity.status)}`,
|
|
482
|
-
`created_at="${entity.created_at}"`,
|
|
483
|
-
`updated_at="${entity.updated_at}"`,
|
|
484
|
-
`source="${entity.source.replace(/"/g, '\\"')}"`,
|
|
485
|
-
];
|
|
486
|
-
if (entity.tags && entity.tags.length > 0) {
|
|
487
|
-
const tagsList = entity.tags.map(prologAtom).join(",");
|
|
488
|
-
props.push(`tags=[${tagsList}]`);
|
|
489
|
-
}
|
|
490
|
-
if (entity.owner)
|
|
491
|
-
props.push(`owner=${prologAtom(entity.owner)}`);
|
|
492
|
-
if (entity.priority)
|
|
493
|
-
props.push(`priority=${prologAtom(entity.priority)}`);
|
|
494
|
-
if (entity.severity)
|
|
495
|
-
props.push(`severity=${prologAtom(entity.severity)}`);
|
|
496
|
-
if (entity.text_ref)
|
|
497
|
-
props.push(`text_ref="${entity.text_ref}"`);
|
|
498
|
-
const propsList = `[${props.join(", ")}]`;
|
|
499
|
-
const goal = `kb_assert_entity(${entity.type}, ${propsList})`;
|
|
500
|
-
const result = await prolog.query(goal);
|
|
501
|
-
if (result.success) {
|
|
502
|
-
entityCount++;
|
|
503
|
-
kbModified = true;
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
catch (error) {
|
|
507
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
508
|
-
console.warn(`Warning: Failed to upsert entity ${entity.id}: ${message}`);
|
|
206
|
+
const entityIds = new Set();
|
|
207
|
+
// Validate and filter dangling relationships
|
|
208
|
+
const validationErrors = validateRelationships(allRelationships, entityIds);
|
|
209
|
+
if (validationErrors.length > 0) {
|
|
210
|
+
console.warn(`Warning: ${validationErrors.length} dangling relationship(s) found`);
|
|
211
|
+
for (const { relationship, error } of validationErrors) {
|
|
212
|
+
console.warn(` - ${error}: ${relationship.type} ${relationship.from} -> ${relationship.to}`);
|
|
509
213
|
}
|
|
510
214
|
}
|
|
511
|
-
const
|
|
215
|
+
const danglingKeys = new Set(validationErrors.map(({ relationship: r }) => `${r.type}|${r.from}|${r.to}`));
|
|
216
|
+
const validRelationships = allRelationships.filter((r) => !danglingKeys.has(`${r.type}|${r.from}|${r.to}`));
|
|
217
|
+
// Track entity counts by type
|
|
512
218
|
for (const { entity } of results) {
|
|
513
|
-
|
|
514
|
-
idLookup.set(filename, entity.id);
|
|
515
|
-
idLookup.set(entity.id, entity.id);
|
|
516
|
-
}
|
|
517
|
-
let relCount = 0;
|
|
518
|
-
const failedRelationships = [];
|
|
519
|
-
for (const { relationships } of results) {
|
|
520
|
-
for (const rel of relationships) {
|
|
521
|
-
try {
|
|
522
|
-
const fromId = idLookup.get(rel.from) || rel.from;
|
|
523
|
-
const toId = idLookup.get(rel.to) || rel.to;
|
|
524
|
-
const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
|
|
525
|
-
const result = await prolog.query(goal);
|
|
526
|
-
if (result.success) {
|
|
527
|
-
relCount++;
|
|
528
|
-
kbModified = true;
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
failedRelationships.push({
|
|
532
|
-
rel,
|
|
533
|
-
fromId,
|
|
534
|
-
toId,
|
|
535
|
-
error: result.error || "Unknown error",
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
catch (error) {
|
|
540
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
541
|
-
const fromId = idLookup.get(rel.from) || rel.from;
|
|
542
|
-
const toId = idLookup.get(rel.to) || rel.to;
|
|
543
|
-
failedRelationships.push({ rel, fromId, toId, error: message });
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const retryCount = 3;
|
|
548
|
-
for (let pass = 0; pass < retryCount && failedRelationships.length > 0; pass++) {
|
|
549
|
-
const remainingFailed = [];
|
|
550
|
-
for (const { rel, fromId, toId } of failedRelationships) {
|
|
551
|
-
try {
|
|
552
|
-
const goal = `kb_assert_relationship(${rel.type}, '${fromId}', '${toId}', [])`;
|
|
553
|
-
const result = await prolog.query(goal);
|
|
554
|
-
if (result.success) {
|
|
555
|
-
relCount++;
|
|
556
|
-
kbModified = true;
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
remainingFailed.push({
|
|
560
|
-
rel,
|
|
561
|
-
fromId,
|
|
562
|
-
toId,
|
|
563
|
-
error: result.error || "Unknown error",
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
catch (err) {
|
|
568
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
569
|
-
remainingFailed.push({ rel, fromId, toId, error: message });
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
failedRelationships.length = 0;
|
|
573
|
-
failedRelationships.push(...remainingFailed);
|
|
574
|
-
}
|
|
575
|
-
if (failedRelationships.length > 0) {
|
|
576
|
-
console.warn(`\nWarning: ${failedRelationships.length} relationship(s) failed to sync:`);
|
|
577
|
-
const seen = new Set();
|
|
578
|
-
for (const { rel, fromId, toId, error } of failedRelationships) {
|
|
579
|
-
const key = `${rel.type}:${fromId}->${toId}`;
|
|
580
|
-
if (!seen.has(key)) {
|
|
581
|
-
seen.add(key);
|
|
582
|
-
console.warn(` - ${rel.type}: ${fromId} -> ${toId}`);
|
|
583
|
-
console.warn(` Error: ${error}`);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
console.warn("\nTip: Ensure target entities exist before creating relationships.");
|
|
219
|
+
entityCounts[entity.type] = (entityCounts[entity.type] || 0) + 1;
|
|
587
220
|
}
|
|
221
|
+
// Persist entities
|
|
222
|
+
const { entityCount, kbModified: entitiesModified } = await persistEntities(prolog, results, entityIds);
|
|
223
|
+
// Persist relationships
|
|
224
|
+
const { relationshipCount, kbModified: relationshipsModified } = await persistRelationships(prolog, results, validRelationships);
|
|
225
|
+
const kbModified = entitiesModified || relationshipsModified;
|
|
588
226
|
if (kbModified) {
|
|
589
227
|
prolog.invalidateCache();
|
|
590
228
|
}
|
|
591
229
|
await prolog.query("kb_save");
|
|
592
230
|
await prolog.query("kb_detach");
|
|
593
231
|
await prolog.terminate();
|
|
232
|
+
// Publish staging to live
|
|
594
233
|
atomicPublish(stagingPath, livePath);
|
|
234
|
+
// Update cache
|
|
595
235
|
const evictedHashes = {};
|
|
596
236
|
const evictedSeenAt = {};
|
|
597
237
|
for (const [key, hash] of Object.entries(nextHashes)) {
|
|
@@ -599,7 +239,7 @@ export async function syncCommand(options = {}) {
|
|
|
599
239
|
continue;
|
|
600
240
|
}
|
|
601
241
|
evictedHashes[key] = hash;
|
|
602
|
-
evictedSeenAt[key] =
|
|
242
|
+
evictedSeenAt[key] = nextHashes[key] ?? nowIso;
|
|
603
243
|
}
|
|
604
244
|
const liveCachePath = path.join(livePath, "sync-cache.json");
|
|
605
245
|
writeSyncCache(liveCachePath, {
|
|
@@ -611,14 +251,14 @@ export async function syncCommand(options = {}) {
|
|
|
611
251
|
if (markdownFiles.length > 0 && entityCount < markdownFiles.length) {
|
|
612
252
|
diagnostics.push(createDocsNotIndexedDiagnostic(markdownFiles.length, entityCount));
|
|
613
253
|
}
|
|
614
|
-
console.log(`✓ Imported ${entityCount} entities, ${
|
|
254
|
+
console.log(`✓ Imported ${entityCount} entities, ${relationshipCount} relationships`);
|
|
615
255
|
const commit = getCurrentCommit();
|
|
616
256
|
const summary = {
|
|
617
257
|
branch: currentBranch,
|
|
618
258
|
commit,
|
|
619
259
|
timestamp: new Date().toISOString(),
|
|
620
260
|
entityCounts,
|
|
621
|
-
relationshipCount
|
|
261
|
+
relationshipCount,
|
|
622
262
|
success: true,
|
|
623
263
|
published,
|
|
624
264
|
failures: diagnostics,
|
|
@@ -635,14 +275,13 @@ export async function syncCommand(options = {}) {
|
|
|
635
275
|
catch (error) {
|
|
636
276
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
637
277
|
console.error(`Error: ${errorMessage}`);
|
|
638
|
-
// Return failure summary
|
|
639
278
|
const commit = getCurrentCommit();
|
|
640
279
|
const summary = {
|
|
641
280
|
branch: currentBranch || "unknown",
|
|
642
281
|
commit,
|
|
643
282
|
timestamp: new Date().toISOString(),
|
|
644
283
|
entityCounts,
|
|
645
|
-
relationshipCount,
|
|
284
|
+
relationshipCount: 0,
|
|
646
285
|
success: false,
|
|
647
286
|
published: false,
|
|
648
287
|
failures: diagnostics,
|
|
@@ -657,76 +296,5 @@ export async function syncCommand(options = {}) {
|
|
|
657
296
|
throw error;
|
|
658
297
|
}
|
|
659
298
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const parsed = parseYAML(rawContent);
|
|
663
|
-
if (!isRecord(parsed)) {
|
|
664
|
-
console.warn(`Warning: symbols manifest ${manifestPath} is not a YAML object; skipping coordinate refresh`);
|
|
665
|
-
return;
|
|
666
|
-
}
|
|
667
|
-
const rawSymbols = parsed.symbols;
|
|
668
|
-
if (!Array.isArray(rawSymbols)) {
|
|
669
|
-
console.warn(`Warning: symbols manifest ${manifestPath} has no symbols array; skipping coordinate refresh`);
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const before = rawSymbols.map((entry) => isRecord(entry)
|
|
673
|
-
? { ...entry }
|
|
674
|
-
: {});
|
|
675
|
-
const enriched = await enrichSymbolCoordinates(before, workspaceRoot);
|
|
676
|
-
parsed.symbols = enriched;
|
|
677
|
-
let refreshed = 0;
|
|
678
|
-
let failed = 0;
|
|
679
|
-
let unchanged = 0;
|
|
680
|
-
for (let i = 0; i < before.length; i++) {
|
|
681
|
-
const previous = before[i] ?? {};
|
|
682
|
-
const current = enriched[i] ?? previous;
|
|
683
|
-
const changed = GENERATED_COORD_FIELDS.some((field) => previous[field] !== current[field]);
|
|
684
|
-
if (changed) {
|
|
685
|
-
refreshed++;
|
|
686
|
-
continue;
|
|
687
|
-
}
|
|
688
|
-
const eligible = isEligibleForCoordinateRefresh(typeof current.sourceFile === "string"
|
|
689
|
-
? current.sourceFile
|
|
690
|
-
: typeof previous.sourceFile === "string"
|
|
691
|
-
? previous.sourceFile
|
|
692
|
-
: undefined, workspaceRoot);
|
|
693
|
-
if (eligible && !hasAllGeneratedCoordinates(current)) {
|
|
694
|
-
failed++;
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
unchanged++;
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
const dumped = dumpYAML(parsed, {
|
|
701
|
-
lineWidth: -1,
|
|
702
|
-
noRefs: true,
|
|
703
|
-
sortKeys: false,
|
|
704
|
-
});
|
|
705
|
-
const nextContent = `${SYMBOLS_MANIFEST_COMMENT_BLOCK}${dumped}`;
|
|
706
|
-
if (rawContent !== nextContent) {
|
|
707
|
-
writeFileSync(manifestPath, nextContent, "utf8");
|
|
708
|
-
}
|
|
709
|
-
console.log(`✓ Refreshed symbol coordinates in ${path.relative(workspaceRoot, manifestPath)} (refreshed=${refreshed}, unchanged=${unchanged}, failed=${failed})`);
|
|
710
|
-
}
|
|
711
|
-
function hasAllGeneratedCoordinates(entry) {
|
|
712
|
-
return (typeof entry.sourceLine === "number" &&
|
|
713
|
-
typeof entry.sourceColumn === "number" &&
|
|
714
|
-
typeof entry.sourceEndLine === "number" &&
|
|
715
|
-
typeof entry.sourceEndColumn === "number" &&
|
|
716
|
-
typeof entry.coordinatesGeneratedAt === "string" &&
|
|
717
|
-
entry.coordinatesGeneratedAt.length > 0);
|
|
718
|
-
}
|
|
719
|
-
function isEligibleForCoordinateRefresh(sourceFile, workspaceRoot) {
|
|
720
|
-
if (!sourceFile)
|
|
721
|
-
return false;
|
|
722
|
-
const absolute = path.isAbsolute(sourceFile)
|
|
723
|
-
? sourceFile
|
|
724
|
-
: path.resolve(workspaceRoot, sourceFile);
|
|
725
|
-
if (!existsSync(absolute))
|
|
726
|
-
return false;
|
|
727
|
-
const ext = path.extname(absolute).toLowerCase();
|
|
728
|
-
return SYMBOL_COORD_EXTENSIONS.has(ext);
|
|
729
|
-
}
|
|
730
|
-
function isRecord(value) {
|
|
731
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
732
|
-
}
|
|
299
|
+
// Export for use by modules that need these functions
|
|
300
|
+
export { normalizeMarkdownPath } from "./sync/discovery.js";
|