kibi-cli 0.2.8 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +4 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -14
- package/dist/commands/check.d.ts +5 -8
- package/dist/commands/check.d.ts.map +1 -1
- package/dist/commands/check.js +41 -62
- package/dist/commands/coverage.d.ts +12 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +24 -0
- package/dist/commands/discovery-shared.d.ts +11 -0
- package/dist/commands/discovery-shared.d.ts.map +1 -0
- package/dist/commands/discovery-shared.js +281 -0
- package/dist/commands/doctor.d.ts +3 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +12 -13
- package/dist/commands/gaps.d.ts +12 -0
- package/dist/commands/gaps.d.ts.map +1 -0
- package/dist/commands/gaps.js +28 -0
- package/dist/commands/graph.d.ts +13 -0
- package/dist/commands/graph.d.ts.map +1 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/init.d.ts +3 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -3
- package/dist/commands/query.d.ts +3 -1
- package/dist/commands/query.d.ts.map +1 -1
- package/dist/commands/query.js +9 -20
- package/dist/commands/search.d.ts +9 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +38 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +9 -0
- package/dist/commands/sync/persistence.d.ts.map +1 -1
- package/dist/commands/sync/persistence.js +79 -12
- package/dist/commands/sync.d.ts +4 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +79 -31
- package/dist/extractors/markdown.d.ts +17 -0
- package/dist/extractors/markdown.d.ts.map +1 -1
- package/dist/extractors/markdown.js +104 -14
- package/dist/prolog/codec.d.ts +32 -5
- package/dist/prolog/codec.d.ts.map +1 -1
- package/dist/prolog/codec.js +95 -58
- package/dist/prolog.d.ts.map +1 -1
- package/dist/prolog.js +12 -2
- package/dist/public/check-types.d.ts +7 -0
- package/dist/public/check-types.d.ts.map +1 -0
- package/dist/public/check-types.js +18 -0
- package/dist/public/schemas/entity.d.ts +68 -0
- package/dist/public/schemas/entity.d.ts.map +1 -1
- package/dist/public/schemas/entity.js +52 -0
- package/dist/relationships/shards.d.ts.map +1 -1
- package/dist/relationships/shards.js +6 -3
- package/dist/schemas/entity.schema.json +120 -0
- package/dist/search-ranking.d.ts +9 -0
- package/dist/search-ranking.d.ts.map +1 -0
- package/dist/search-ranking.js +149 -0
- package/dist/traceability/symbol-extract.d.ts.map +1 -1
- package/dist/traceability/symbol-extract.js +16 -8
- package/dist/types/entities.d.ts +19 -1
- package/dist/types/entities.d.ts.map +1 -1
- package/dist/utils/prolog-cleanup.d.ts +10 -0
- package/dist/utils/prolog-cleanup.d.ts.map +1 -0
- package/dist/utils/prolog-cleanup.js +39 -0
- package/dist/utils/rule-registry.d.ts +8 -0
- package/dist/utils/rule-registry.d.ts.map +1 -1
- package/dist/utils/rule-registry.js +14 -12
- package/package.json +10 -2
- package/schema/config.json +7 -1
- package/schema/entities.pl +18 -0
- package/schema/validation.pl +115 -4
- package/src/public/check-types.ts +37 -0
- package/src/public/schemas/entity.ts +58 -0
- package/src/schemas/entity.schema.json +56 -1
- package/dist/kb/target-resolver.d.ts +0 -80
- package/dist/kb/target-resolver.d.ts.map +0 -1
- package/dist/kb/target-resolver.js +0 -313
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { VALID_ENTITY_TYPES, queryEntities } from "../query/service.js";
|
|
2
|
+
import { rankEntities } from "../search-ranking.js";
|
|
3
|
+
import { printDiscoveryResult, withAttachedBranchProlog, } from "./discovery-shared.js";
|
|
4
|
+
// implements REQ-mcp-search-discovery, REQ-003
|
|
5
|
+
export async function searchCommand(query, options) {
|
|
6
|
+
if (!query?.trim()) {
|
|
7
|
+
console.error("Error: search query is required");
|
|
8
|
+
process.exitCode = 1;
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
if (options.type && !VALID_ENTITY_TYPES.includes(options.type)) {
|
|
12
|
+
console.error(`Error: invalid type '${options.type}'. Valid types: ${VALID_ENTITY_TYPES.join(", ")}`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
await withAttachedBranchProlog(async (prolog) => {
|
|
17
|
+
const limit = Number.parseInt(options.limit || "20", 10);
|
|
18
|
+
const offset = Number.parseInt(options.offset || "0", 10);
|
|
19
|
+
const result = await executeSearch(prolog, query, options.type, limit, offset);
|
|
20
|
+
printDiscoveryResult(options.format, result, buildSearchText(query, result));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async function executeSearch(prolog, query, type, limit, offset) {
|
|
24
|
+
const entitiesResult = await queryEntities(prolog, {
|
|
25
|
+
type,
|
|
26
|
+
limit: 100000,
|
|
27
|
+
offset: 0,
|
|
28
|
+
});
|
|
29
|
+
const matches = await rankEntities(entitiesResult.entities, query, process.cwd());
|
|
30
|
+
const paginated = matches.slice(offset, offset + limit);
|
|
31
|
+
return { results: paginated, count: matches.length };
|
|
32
|
+
}
|
|
33
|
+
function buildSearchText(query, result) {
|
|
34
|
+
if (result.count === 0) {
|
|
35
|
+
return `No search results for '${query}'.`;
|
|
36
|
+
}
|
|
37
|
+
return `Found ${result.count} search results for '${query}'. Showing ${result.results.length}: ${result.results.map((match) => match.entity.id).join(", ")}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAOA,UAAU,aAAa;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAGD,wBAAsB,aAAa,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBzE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { printDiscoveryResult, resolveCurrentKbPath, runJsonModuleQuery, withPrologProcess, } from "./discovery-shared.js";
|
|
2
|
+
// implements REQ-002, REQ-003
|
|
3
|
+
export async function statusCommand(options) {
|
|
4
|
+
await withPrologProcess(async (prolog) => {
|
|
5
|
+
const kbPath = await resolveCurrentKbPath();
|
|
6
|
+
const result = await runJsonModuleQuery(prolog, "status.pl", "status:kb_status_json(JsonString)", "status query failed", kbPath);
|
|
7
|
+
printDiscoveryResult(options.format, result, `Branch ${result.branch} is ${result.syncState} (dirty=${result.dirty})`);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/persistence.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../src/commands/sync/persistence.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAqErD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,eAAe,CAEnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,GACrB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CAoEvD;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,kBAAkB,EAAE,qBAAqB,EAAE,GAC1C,OAAO,CAAC;IAAE,iBAAiB,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,OAAO,CAAA;CAAE,CAAC,CAiI7D"}
|
|
@@ -16,8 +16,69 @@
|
|
|
16
16
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
17
|
*/
|
|
18
18
|
import * as path from "node:path";
|
|
19
|
-
import { toPrologAtom } from "../../prolog/codec.js";
|
|
20
|
-
|
|
19
|
+
import { toPrologAtom, toPrologString } from "../../prolog/codec.js";
|
|
20
|
+
// Field categorization for typed fact serialization
|
|
21
|
+
// NOTE: base entity fields (status, owner, priority, severity) are NOT listed here —
|
|
22
|
+
// they are emitted in the base props block above. Only fact-specific atom fields go here.
|
|
23
|
+
const ATOM_FIELDS = new Set([
|
|
24
|
+
"fact_kind",
|
|
25
|
+
"operator",
|
|
26
|
+
"value_type",
|
|
27
|
+
"polarity",
|
|
28
|
+
]);
|
|
29
|
+
// Typed fact fields only (exclude base entity fields like id, title, etc.)
|
|
30
|
+
const STRING_FIELDS = new Set([
|
|
31
|
+
"subject_key",
|
|
32
|
+
"property_key",
|
|
33
|
+
"value_string",
|
|
34
|
+
"unit",
|
|
35
|
+
"scope",
|
|
36
|
+
"valid_from",
|
|
37
|
+
"valid_to",
|
|
38
|
+
"canonical_key",
|
|
39
|
+
]);
|
|
40
|
+
const NUMBER_FIELDS = new Set(["value_int", "value_number"]);
|
|
41
|
+
const BOOLEAN_FIELDS = new Set(["value_bool", "closed_world"]);
|
|
42
|
+
// Serialize typed fact fields from entity
|
|
43
|
+
function serializeTypedFactFields(entity) {
|
|
44
|
+
const fields = [];
|
|
45
|
+
const entityRecord = entity;
|
|
46
|
+
// String fields (safely escaped double-quoted Prolog strings)
|
|
47
|
+
for (const field of STRING_FIELDS) {
|
|
48
|
+
const value = entityRecord[field];
|
|
49
|
+
if (value !== undefined && value !== null) {
|
|
50
|
+
fields.push(`${field}=${toPrologString(String(value))}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Atom fields (possibly unquoted if simple)
|
|
54
|
+
for (const field of ATOM_FIELDS) {
|
|
55
|
+
const value = entityRecord[field];
|
|
56
|
+
if (value !== undefined && value !== null) {
|
|
57
|
+
fields.push(`${field}=${toPrologAtom(String(value))}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Number fields (unquoted); value_int must be a true integer
|
|
61
|
+
for (const field of NUMBER_FIELDS) {
|
|
62
|
+
const value = entityRecord[field];
|
|
63
|
+
if (value !== undefined && value !== null && typeof value === "number") {
|
|
64
|
+
if (field === "value_int" && !Number.isInteger(value)) {
|
|
65
|
+
continue; // silently drop non-integer value_int
|
|
66
|
+
}
|
|
67
|
+
fields.push(`${field}=${value}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Boolean fields (true/false atoms)
|
|
71
|
+
for (const field of BOOLEAN_FIELDS) {
|
|
72
|
+
const value = entityRecord[field];
|
|
73
|
+
if (value !== undefined && value !== null && typeof value === "boolean") {
|
|
74
|
+
fields.push(`${field}=${value}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return fields;
|
|
78
|
+
}
|
|
79
|
+
export async function persistEntities(
|
|
80
|
+
// implements REQ-009
|
|
81
|
+
prolog, results, entityIds) {
|
|
21
82
|
let entityCount = 0;
|
|
22
83
|
let kbModified = false;
|
|
23
84
|
// Query existing entity IDs to include unchanged entities
|
|
@@ -39,12 +100,12 @@ export async function persistEntities(prolog, results, entityIds) {
|
|
|
39
100
|
for (const { entity } of results) {
|
|
40
101
|
try {
|
|
41
102
|
const props = [
|
|
42
|
-
`id
|
|
43
|
-
`title
|
|
103
|
+
`id=${toPrologAtom(entity.id)}`,
|
|
104
|
+
`title=${toPrologString(entity.title)}`,
|
|
44
105
|
`status=${toPrologAtom(entity.status)}`,
|
|
45
|
-
`created_at
|
|
46
|
-
`updated_at
|
|
47
|
-
`source
|
|
106
|
+
`created_at=${toPrologString(entity.created_at)}`,
|
|
107
|
+
`updated_at=${toPrologString(entity.updated_at)}`,
|
|
108
|
+
`source=${toPrologString(entity.source)}`,
|
|
48
109
|
];
|
|
49
110
|
if (entity.tags && entity.tags.length > 0) {
|
|
50
111
|
const tagsList = entity.tags.map(toPrologAtom).join(",");
|
|
@@ -57,18 +118,24 @@ export async function persistEntities(prolog, results, entityIds) {
|
|
|
57
118
|
if (entity.severity)
|
|
58
119
|
props.push(`severity=${toPrologAtom(entity.severity)}`);
|
|
59
120
|
if (entity.text_ref)
|
|
60
|
-
props.push(`text_ref
|
|
121
|
+
props.push(`text_ref=${toPrologString(entity.text_ref)}`);
|
|
122
|
+
// Add typed fact fields for fact entities
|
|
123
|
+
if (entity.type === "fact") {
|
|
124
|
+
const factFields = serializeTypedFactFields(entity);
|
|
125
|
+
props.push(...factFields);
|
|
126
|
+
}
|
|
61
127
|
const propsList = `[${props.join(", ")}]`;
|
|
62
128
|
const goal = `kb_assert_entity(${entity.type}, ${propsList})`;
|
|
63
129
|
const result = await prolog.query(goal);
|
|
64
|
-
if (result.success) {
|
|
65
|
-
|
|
66
|
-
kbModified = true;
|
|
130
|
+
if (!result.success) {
|
|
131
|
+
throw new Error(result.error || `kb_assert_entity failed for ${entity.id}`);
|
|
67
132
|
}
|
|
133
|
+
entityCount++;
|
|
134
|
+
kbModified = true;
|
|
68
135
|
}
|
|
69
136
|
catch (error) {
|
|
70
137
|
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
-
|
|
138
|
+
throw new Error(`Failed to upsert entity ${entity.id}: ${message}`);
|
|
72
139
|
}
|
|
73
140
|
}
|
|
74
141
|
return { entityCount, kbModified };
|
package/dist/commands/sync.d.ts
CHANGED
|
@@ -2,9 +2,12 @@ import type { SyncSummary } from "../diagnostics.js";
|
|
|
2
2
|
export declare class SyncError extends Error {
|
|
3
3
|
constructor(message: string);
|
|
4
4
|
}
|
|
5
|
+
export interface SyncResult extends SyncSummary {
|
|
6
|
+
exitCode?: number;
|
|
7
|
+
}
|
|
5
8
|
export declare function syncCommand(options?: {
|
|
6
9
|
validateOnly?: boolean;
|
|
7
10
|
rebuild?: boolean;
|
|
8
|
-
}): Promise<
|
|
11
|
+
}): Promise<SyncResult>;
|
|
9
12
|
export { normalizeMarkdownPath } from "./sync/discovery.js";
|
|
10
13
|
//# sourceMappingURL=sync.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyCjE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAGD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,GACL,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/commands/sync.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAyCjE,qBAAa,SAAU,SAAQ,KAAK;gBACtB,OAAO,EAAE,MAAM;CAI5B;AAGD,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAGD,wBAAsB,WAAW,CAC/B,OAAO,GAAE;IACP,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,GACL,OAAO,CAAC,UAAU,CAAC,CAmZrB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/commands/sync.js
CHANGED
|
@@ -68,23 +68,17 @@ export async function syncCommand(options = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
currentBranch = branchResult.branch;
|
|
70
70
|
if (process.env.KIBI_DEBUG) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
console.log("[kibi-debug] currentBranch:", currentBranch);
|
|
74
|
-
}
|
|
75
|
-
catch { }
|
|
71
|
+
// eslint-disable-next-line no-console
|
|
72
|
+
console.log("[kibi-debug] currentBranch:", currentBranch);
|
|
76
73
|
}
|
|
77
74
|
const config = loadSyncConfig(process.cwd());
|
|
78
75
|
const paths = config.paths;
|
|
79
76
|
const { markdownFiles, manifestFiles, relationshipsDir } = await discoverSourceFiles(process.cwd(), paths);
|
|
80
77
|
if (process.env.KIBI_DEBUG) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
console.log("[kibi-debug] manifestFiles:", manifestFiles.length);
|
|
86
|
-
}
|
|
87
|
-
catch { }
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.log("[kibi-debug] markdownFiles:", markdownFiles.length);
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.log("[kibi-debug] manifestFiles:", manifestFiles.length);
|
|
88
82
|
}
|
|
89
83
|
const sourceFiles = [...markdownFiles, ...manifestFiles].sort();
|
|
90
84
|
const cachePath = path.join(process.cwd(), `.kb/branches/${currentBranch}/sync-cache.json`);
|
|
@@ -142,27 +136,34 @@ export async function syncCommand(options = {}) {
|
|
|
142
136
|
diagnostics.push(createInvalidAuthoringDiagnostic(err.file, embeddedTypes));
|
|
143
137
|
}
|
|
144
138
|
}
|
|
145
|
-
if (validateOnly) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
if (!validateOnly) {
|
|
140
|
+
for (const file of manifestFiles) {
|
|
141
|
+
try {
|
|
142
|
+
await refreshManifestCoordinates(file, process.cwd());
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
146
|
+
console.warn(`Warning: Failed to refresh symbol coordinates in ${file}: ${message}`);
|
|
149
147
|
}
|
|
150
|
-
console.error(`FAILED: ${errors.length} errors found`);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
console.log(`OK: Validation passed (${results.length} entities)`);
|
|
155
|
-
process.exit(0);
|
|
156
148
|
}
|
|
157
149
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
catch (error) {
|
|
163
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
164
|
-
console.warn(`Warning: Failed to refresh symbol coordinates in ${file}: ${message}`);
|
|
150
|
+
if (validateOnly && errors.length > 0 && results.length === 0) {
|
|
151
|
+
for (const err of errors) {
|
|
152
|
+
console.error(`${err.file}: ${err.message}`);
|
|
165
153
|
}
|
|
154
|
+
console.error(`FAILED: ${errors.length} errors found`);
|
|
155
|
+
return {
|
|
156
|
+
branch: currentBranch,
|
|
157
|
+
commit: getCurrentCommit(),
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
entityCounts,
|
|
160
|
+
relationshipCount: 0,
|
|
161
|
+
success: false,
|
|
162
|
+
published: false,
|
|
163
|
+
failures: diagnostics,
|
|
164
|
+
durationMs: Date.now() - startTime,
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
};
|
|
166
167
|
}
|
|
167
168
|
if (results.length === 0 && allRelationships.length === 0 && !rebuild) {
|
|
168
169
|
const evictedHashes = {};
|
|
@@ -180,7 +181,18 @@ export async function syncCommand(options = {}) {
|
|
|
180
181
|
seenAt: evictedSeenAt,
|
|
181
182
|
});
|
|
182
183
|
console.log("✓ Imported 0 entities, 0 relationships (no changes)");
|
|
183
|
-
|
|
184
|
+
return {
|
|
185
|
+
branch: currentBranch,
|
|
186
|
+
commit: getCurrentCommit(),
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
entityCounts,
|
|
189
|
+
relationshipCount: 0,
|
|
190
|
+
success: true,
|
|
191
|
+
published: false,
|
|
192
|
+
failures: diagnostics,
|
|
193
|
+
durationMs: Date.now() - startTime,
|
|
194
|
+
exitCode: 0,
|
|
195
|
+
};
|
|
184
196
|
}
|
|
185
197
|
const livePath = path.join(process.cwd(), `.kb/branches/${currentBranch}`);
|
|
186
198
|
const kbExists = existsSync(livePath);
|
|
@@ -214,6 +226,42 @@ export async function syncCommand(options = {}) {
|
|
|
214
226
|
// Persist relationships
|
|
215
227
|
const { relationshipCount, kbModified: relationshipsModified } = await persistRelationships(prolog, results, validRelationships);
|
|
216
228
|
const kbModified = entitiesModified || relationshipsModified;
|
|
229
|
+
if (validateOnly) {
|
|
230
|
+
await prolog.query("kb_detach");
|
|
231
|
+
await prolog.terminate();
|
|
232
|
+
cleanupStaging(stagingPath);
|
|
233
|
+
if (errors.length > 0) {
|
|
234
|
+
for (const err of errors) {
|
|
235
|
+
console.error(`${err.file}: ${err.message}`);
|
|
236
|
+
}
|
|
237
|
+
console.error(`FAILED: ${errors.length} errors found`);
|
|
238
|
+
return {
|
|
239
|
+
branch: currentBranch,
|
|
240
|
+
commit: getCurrentCommit(),
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
entityCounts,
|
|
243
|
+
relationshipCount: 0,
|
|
244
|
+
success: false,
|
|
245
|
+
published: false,
|
|
246
|
+
failures: diagnostics,
|
|
247
|
+
durationMs: Date.now() - startTime,
|
|
248
|
+
exitCode: 1,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
console.log(`OK: Validation passed (${entityCount} entities)`);
|
|
252
|
+
return {
|
|
253
|
+
branch: currentBranch,
|
|
254
|
+
commit: getCurrentCommit(),
|
|
255
|
+
timestamp: new Date().toISOString(),
|
|
256
|
+
entityCounts,
|
|
257
|
+
relationshipCount: 0,
|
|
258
|
+
success: true,
|
|
259
|
+
published: false,
|
|
260
|
+
failures: diagnostics,
|
|
261
|
+
durationMs: Date.now() - startTime,
|
|
262
|
+
exitCode: 0,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
217
265
|
if (kbModified) {
|
|
218
266
|
prolog.invalidateCache();
|
|
219
267
|
}
|
|
@@ -259,7 +307,7 @@ export async function syncCommand(options = {}) {
|
|
|
259
307
|
durationMs: Date.now() - startTime,
|
|
260
308
|
};
|
|
261
309
|
console.log(formatSyncSummary(summary));
|
|
262
|
-
return summary;
|
|
310
|
+
return { ...summary, exitCode: 0 };
|
|
263
311
|
}
|
|
264
312
|
catch (error) {
|
|
265
313
|
cleanupStaging(stagingPath);
|
|
@@ -11,6 +11,22 @@ export interface ExtractedEntity {
|
|
|
11
11
|
priority?: string;
|
|
12
12
|
severity?: string;
|
|
13
13
|
text_ref?: string;
|
|
14
|
+
fact_kind?: "subject" | "property_value" | "observation" | "meta";
|
|
15
|
+
subject_key?: string;
|
|
16
|
+
property_key?: string;
|
|
17
|
+
operator?: "eq" | "neq" | "lt" | "lte" | "gt" | "gte";
|
|
18
|
+
value_type?: "string" | "int" | "number" | "bool";
|
|
19
|
+
value_string?: string;
|
|
20
|
+
value_int?: number;
|
|
21
|
+
value_number?: number;
|
|
22
|
+
value_bool?: boolean;
|
|
23
|
+
unit?: string;
|
|
24
|
+
scope?: string;
|
|
25
|
+
polarity?: "require" | "forbid";
|
|
26
|
+
closed_world?: boolean;
|
|
27
|
+
valid_from?: string;
|
|
28
|
+
valid_to?: string;
|
|
29
|
+
canonical_key?: string;
|
|
14
30
|
}
|
|
15
31
|
export interface ExtractedRelationship {
|
|
16
32
|
type: string;
|
|
@@ -36,4 +52,5 @@ export declare class FrontmatterError extends Error {
|
|
|
36
52
|
export declare function detectEmbeddedEntities(data: Record<string, unknown>, entityType: string): string[];
|
|
37
53
|
export declare function extractFromMarkdown(filePath: string): ExtractionResult;
|
|
38
54
|
export declare function inferTypeFromPath(filePath: string): string | null;
|
|
55
|
+
export declare function normalizeDateLike(value: unknown): string | undefined;
|
|
39
56
|
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/extractors/markdown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/extractors/markdown.ts"],"names":[],"mappings":"AAmDA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,SAAS,CAAC,EAAE,SAAS,GAAG,gBAAgB,GAAG,aAAa,GAAG,MAAM,CAAC;IAClE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC;IACtD,UAAU,CAAC,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAClD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IAChC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,CAAC;IACxB,aAAa,EAAE,qBAAqB,EAAE,CAAC;CACxC;AAaD,qBAAa,gBAAiB,SAAQ,KAAK;IAOhC,QAAQ,EAAE,MAAM;IANlB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;gBAG5B,OAAO,EAAE,MAAM,EACR,QAAQ,EAAE,MAAM,EACvB,OAAO,CAAC,EAAE;QACR,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB;IASM,QAAQ;CAUlB;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,MAAM,GACjB,MAAM,EAAE,CA8CV;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CA+NtE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASjE;AASD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQpE"}
|
|
@@ -17,7 +17,33 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
|
+
import Ajv from "ajv";
|
|
20
21
|
import matter from "gray-matter";
|
|
22
|
+
import entitySchema from "../schemas/entity.schema.json" with { type: "json" };
|
|
23
|
+
// Typed fact field constants for extraction
|
|
24
|
+
const FACT_STRING_FIELDS = [
|
|
25
|
+
"fact_kind",
|
|
26
|
+
"subject_key",
|
|
27
|
+
"property_key",
|
|
28
|
+
"operator",
|
|
29
|
+
"value_type",
|
|
30
|
+
"value_string",
|
|
31
|
+
"unit",
|
|
32
|
+
"scope",
|
|
33
|
+
"polarity",
|
|
34
|
+
"valid_from",
|
|
35
|
+
"valid_to",
|
|
36
|
+
"canonical_key",
|
|
37
|
+
];
|
|
38
|
+
const FACT_NUMBER_FIELDS = ["value_int", "value_number"];
|
|
39
|
+
const FACT_BOOLEAN_FIELDS = ["value_bool", "closed_world"];
|
|
40
|
+
const FACT_ONLY_FIELDS = [
|
|
41
|
+
...FACT_STRING_FIELDS,
|
|
42
|
+
...FACT_NUMBER_FIELDS,
|
|
43
|
+
...FACT_BOOLEAN_FIELDS,
|
|
44
|
+
];
|
|
45
|
+
const ajv = new Ajv({ strict: false, allErrors: true });
|
|
46
|
+
const validateExtractedEntity = ajv.compile(entitySchema);
|
|
21
47
|
const DEFAULT_STATUS_BY_TYPE = {
|
|
22
48
|
req: "open",
|
|
23
49
|
scenario: "draft",
|
|
@@ -156,21 +182,75 @@ export function extractFromMarkdown(filePath) {
|
|
|
156
182
|
}
|
|
157
183
|
}
|
|
158
184
|
}
|
|
185
|
+
// Build base entity with explicit field list
|
|
186
|
+
const entity = {
|
|
187
|
+
id,
|
|
188
|
+
type,
|
|
189
|
+
title: data.title,
|
|
190
|
+
status: data.status || DEFAULT_STATUS_BY_TYPE[String(type)] || "active",
|
|
191
|
+
created_at: normalizeDateLike(data.created_at) || new Date().toISOString(),
|
|
192
|
+
updated_at: normalizeDateLike(data.updated_at) || new Date().toISOString(),
|
|
193
|
+
source: filePath,
|
|
194
|
+
};
|
|
195
|
+
// Add optional base fields only if present
|
|
196
|
+
if (data.tags !== undefined)
|
|
197
|
+
entity.tags = data.tags;
|
|
198
|
+
if (data.owner !== undefined)
|
|
199
|
+
entity.owner = data.owner;
|
|
200
|
+
if (data.priority !== undefined)
|
|
201
|
+
entity.priority = data.priority;
|
|
202
|
+
if (data.severity !== undefined)
|
|
203
|
+
entity.severity = data.severity;
|
|
204
|
+
if (data.text_ref !== undefined)
|
|
205
|
+
entity.text_ref = data.text_ref;
|
|
206
|
+
if (type !== "fact") {
|
|
207
|
+
const invalidFactField = FACT_ONLY_FIELDS.find((field) => data[field] !== undefined);
|
|
208
|
+
if (invalidFactField) {
|
|
209
|
+
throw new FrontmatterError(`Fact-only fields are only allowed on type: fact (found ${invalidFactField})`, filePath, {
|
|
210
|
+
classification: "Fact Field on Non-Fact Entity",
|
|
211
|
+
hint: "Remove fact-only fields or change the entity type to fact.",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Add typed fact fields only for fact entities
|
|
216
|
+
if (type === "fact") {
|
|
217
|
+
// String fields
|
|
218
|
+
for (const field of FACT_STRING_FIELDS) {
|
|
219
|
+
if (data[field] !== undefined) {
|
|
220
|
+
// Normalize date fields
|
|
221
|
+
if (field === "valid_from" || field === "valid_to") {
|
|
222
|
+
entity[field] =
|
|
223
|
+
normalizeDateLike(data[field]);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
entity[field] = data[field];
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Number fields
|
|
231
|
+
for (const field of FACT_NUMBER_FIELDS) {
|
|
232
|
+
if (data[field] !== undefined) {
|
|
233
|
+
entity[field] = data[field];
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Boolean fields
|
|
237
|
+
for (const field of FACT_BOOLEAN_FIELDS) {
|
|
238
|
+
if (data[field] !== undefined) {
|
|
239
|
+
entity[field] = data[field];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (!validateExtractedEntity(entity)) {
|
|
244
|
+
const messages = (validateExtractedEntity.errors || [])
|
|
245
|
+
.map((e) => `${e.instancePath || "root"}: ${e.message}`)
|
|
246
|
+
.join("; ");
|
|
247
|
+
throw new FrontmatterError(`Entity validation failed: ${messages}`, filePath, {
|
|
248
|
+
classification: "Entity Validation Error",
|
|
249
|
+
hint: "Fix the entity fields so they match the public schema.",
|
|
250
|
+
});
|
|
251
|
+
}
|
|
159
252
|
return {
|
|
160
|
-
entity
|
|
161
|
-
id,
|
|
162
|
-
type,
|
|
163
|
-
title: data.title,
|
|
164
|
-
status: data.status || DEFAULT_STATUS_BY_TYPE[String(type)] || "active",
|
|
165
|
-
created_at: data.created_at || new Date().toISOString(),
|
|
166
|
-
updated_at: data.updated_at || new Date().toISOString(),
|
|
167
|
-
source: filePath,
|
|
168
|
-
tags: data.tags,
|
|
169
|
-
owner: data.owner,
|
|
170
|
-
priority: data.priority,
|
|
171
|
-
severity: data.severity,
|
|
172
|
-
text_ref: data.text_ref,
|
|
173
|
-
},
|
|
253
|
+
entity,
|
|
174
254
|
relationships,
|
|
175
255
|
};
|
|
176
256
|
}
|
|
@@ -234,3 +314,13 @@ function generateId(filePath, title) {
|
|
|
234
314
|
hash.update(`${filePath}:${title}`);
|
|
235
315
|
return hash.digest("hex").substring(0, 16);
|
|
236
316
|
}
|
|
317
|
+
// implements REQ-007
|
|
318
|
+
export function normalizeDateLike(value) {
|
|
319
|
+
if (value instanceof Date) {
|
|
320
|
+
return value.toISOString();
|
|
321
|
+
}
|
|
322
|
+
if (typeof value === "string") {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
package/dist/prolog/codec.d.ts
CHANGED
|
@@ -12,16 +12,17 @@ export declare function escapeAtom(value: string): string;
|
|
|
12
12
|
* Simple atoms (lowercase start, alphanumeric + underscore) pass through.
|
|
13
13
|
*/
|
|
14
14
|
export declare function toPrologAtom(value: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Escape a string value for use inside a Prolog double-quoted string literal.
|
|
17
|
+
* Escapes: backslash, double-quote, newline, carriage-return, tab.
|
|
18
|
+
* Returns the full quoted literal including surrounding double-quotes.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toPrologString(value: string): string;
|
|
15
21
|
/**
|
|
16
22
|
* Escape a string for embedding inside a single-quoted Prolog atom.
|
|
17
23
|
* Alias for escapeAtom for semantic clarity.
|
|
18
24
|
*/
|
|
19
25
|
export declare function escapeAtomContent(value: string): string;
|
|
20
|
-
/**
|
|
21
|
-
* Split a string by delimiter at the top level (not inside brackets or quotes).
|
|
22
|
-
* This is the general-purpose version that splits at depth 0.
|
|
23
|
-
*/
|
|
24
|
-
export declare function splitTopLevel(str: string, delimiter: string): string[];
|
|
25
26
|
/**
|
|
26
27
|
* Parse a Prolog list of lists into a JavaScript array.
|
|
27
28
|
* Input: "[[a,b,c],[d,e,f]]"
|
|
@@ -47,6 +48,16 @@ export declare function parsePropertyList(propsStr: string): Record<string, unkn
|
|
|
47
48
|
* Parse a single Prolog value, handling typed literals and URIs.
|
|
48
49
|
*/
|
|
49
50
|
export declare function parsePrologValue(valueInput: string): unknown;
|
|
51
|
+
/**
|
|
52
|
+
* General-purpose split at top level (not inside brackets or quotes).
|
|
53
|
+
* More robust version used by property parsing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function splitTopLevelGeneral(str: string, delimiter: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Split a string by delimiter at the top level (not inside brackets or quotes).
|
|
58
|
+
* @see splitTopLevelGeneral
|
|
59
|
+
*/
|
|
60
|
+
export declare function splitTopLevel(str: string, delimiter: string): string[];
|
|
50
61
|
/**
|
|
51
62
|
* Parse an atom list from Prolog response.
|
|
52
63
|
* Input: "[a, b, c]" or atom string
|
|
@@ -60,4 +71,20 @@ export declare function parsePairList(raw: string): Array<[string, string]>;
|
|
|
60
71
|
* Parse a list of triples from Prolog response.
|
|
61
72
|
*/
|
|
62
73
|
export declare function parseTriples(raw: string): Array<[string, string, string]>;
|
|
74
|
+
/**
|
|
75
|
+
* Parsed violation from Prolog check output.
|
|
76
|
+
*/
|
|
77
|
+
export interface ParsedViolation {
|
|
78
|
+
rule: string;
|
|
79
|
+
entityId: string;
|
|
80
|
+
description: string;
|
|
81
|
+
suggestion: string;
|
|
82
|
+
source?: string;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse a Prolog list of violation/5 terms into JavaScript objects.
|
|
86
|
+
* Handles descriptions and suggestions that contain commas or nested parens.
|
|
87
|
+
* Input: "[violation(rule,'EntityId',\"Desc\",\"Sugg\",'src')]"
|
|
88
|
+
*/
|
|
89
|
+
export declare function parseViolationRows(raw: string): ParsedViolation[];
|
|
63
90
|
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/prolog/codec.ts"],"names":[],"mappings":"AAUA;;;GAGG;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/prolog/codec.ts"],"names":[],"mappings":"AAUA;;;GAGG;AAEH;;;GAGG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CASpD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA6C5D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,GACjB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAczB;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAW3E;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA6B3E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAwF5D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAoD7E;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAGtE;AA2BD;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAcnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAclE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAczE;AA6DD;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,EAAE,CA0CjE"}
|