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.
Files changed (78) hide show
  1. package/dist/cli.d.ts +4 -1
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +91 -14
  4. package/dist/commands/check.d.ts +5 -8
  5. package/dist/commands/check.d.ts.map +1 -1
  6. package/dist/commands/check.js +41 -62
  7. package/dist/commands/coverage.d.ts +12 -0
  8. package/dist/commands/coverage.d.ts.map +1 -0
  9. package/dist/commands/coverage.js +24 -0
  10. package/dist/commands/discovery-shared.d.ts +11 -0
  11. package/dist/commands/discovery-shared.d.ts.map +1 -0
  12. package/dist/commands/discovery-shared.js +281 -0
  13. package/dist/commands/doctor.d.ts +3 -1
  14. package/dist/commands/doctor.d.ts.map +1 -1
  15. package/dist/commands/doctor.js +12 -13
  16. package/dist/commands/gaps.d.ts +12 -0
  17. package/dist/commands/gaps.d.ts.map +1 -0
  18. package/dist/commands/gaps.js +28 -0
  19. package/dist/commands/graph.d.ts +13 -0
  20. package/dist/commands/graph.d.ts.map +1 -0
  21. package/dist/commands/graph.js +35 -0
  22. package/dist/commands/init.d.ts +3 -1
  23. package/dist/commands/init.d.ts.map +1 -1
  24. package/dist/commands/init.js +4 -3
  25. package/dist/commands/query.d.ts +3 -1
  26. package/dist/commands/query.d.ts.map +1 -1
  27. package/dist/commands/query.js +9 -20
  28. package/dist/commands/search.d.ts +9 -0
  29. package/dist/commands/search.d.ts.map +1 -0
  30. package/dist/commands/search.js +38 -0
  31. package/dist/commands/status.d.ts +6 -0
  32. package/dist/commands/status.d.ts.map +1 -0
  33. package/dist/commands/status.js +9 -0
  34. package/dist/commands/sync/persistence.d.ts.map +1 -1
  35. package/dist/commands/sync/persistence.js +79 -12
  36. package/dist/commands/sync.d.ts +4 -1
  37. package/dist/commands/sync.d.ts.map +1 -1
  38. package/dist/commands/sync.js +79 -31
  39. package/dist/extractors/markdown.d.ts +17 -0
  40. package/dist/extractors/markdown.d.ts.map +1 -1
  41. package/dist/extractors/markdown.js +104 -14
  42. package/dist/prolog/codec.d.ts +32 -5
  43. package/dist/prolog/codec.d.ts.map +1 -1
  44. package/dist/prolog/codec.js +95 -58
  45. package/dist/prolog.d.ts.map +1 -1
  46. package/dist/prolog.js +12 -2
  47. package/dist/public/check-types.d.ts +7 -0
  48. package/dist/public/check-types.d.ts.map +1 -0
  49. package/dist/public/check-types.js +18 -0
  50. package/dist/public/schemas/entity.d.ts +68 -0
  51. package/dist/public/schemas/entity.d.ts.map +1 -1
  52. package/dist/public/schemas/entity.js +52 -0
  53. package/dist/relationships/shards.d.ts.map +1 -1
  54. package/dist/relationships/shards.js +6 -3
  55. package/dist/schemas/entity.schema.json +120 -0
  56. package/dist/search-ranking.d.ts +9 -0
  57. package/dist/search-ranking.d.ts.map +1 -0
  58. package/dist/search-ranking.js +149 -0
  59. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  60. package/dist/traceability/symbol-extract.js +16 -8
  61. package/dist/types/entities.d.ts +19 -1
  62. package/dist/types/entities.d.ts.map +1 -1
  63. package/dist/utils/prolog-cleanup.d.ts +10 -0
  64. package/dist/utils/prolog-cleanup.d.ts.map +1 -0
  65. package/dist/utils/prolog-cleanup.js +39 -0
  66. package/dist/utils/rule-registry.d.ts +8 -0
  67. package/dist/utils/rule-registry.d.ts.map +1 -1
  68. package/dist/utils/rule-registry.js +14 -12
  69. package/package.json +10 -2
  70. package/schema/config.json +7 -1
  71. package/schema/entities.pl +18 -0
  72. package/schema/validation.pl +115 -4
  73. package/src/public/check-types.ts +37 -0
  74. package/src/public/schemas/entity.ts +58 -0
  75. package/src/schemas/entity.schema.json +56 -1
  76. package/dist/kb/target-resolver.d.ts +0 -80
  77. package/dist/kb/target-resolver.d.ts.map +0 -1
  78. 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,6 @@
1
+ interface StatusOptions {
2
+ format?: "json" | "table";
3
+ }
4
+ export declare function statusCommand(options: StatusOptions): Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=status.d.ts.map
@@ -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,EACV,qBAAqB,EACrB,gBAAgB,EACjB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGrD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,wBAAsB,eAAe,CACnC,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,CA0DvD;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"}
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
- export async function persistEntities(prolog, results, entityIds) {
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='${entity.id}'`,
43
- `title="${entity.title.replace(/"/g, '\\"')}"`,
103
+ `id=${toPrologAtom(entity.id)}`,
104
+ `title=${toPrologString(entity.title)}`,
44
105
  `status=${toPrologAtom(entity.status)}`,
45
- `created_at="${entity.created_at}"`,
46
- `updated_at="${entity.updated_at}"`,
47
- `source="${entity.source.replace(/"/g, '\\"')}"`,
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="${entity.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
- entityCount++;
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
- console.warn(`Warning: Failed to upsert entity ${entity.id}: ${message}`);
138
+ throw new Error(`Failed to upsert entity ${entity.id}: ${message}`);
72
139
  }
73
140
  }
74
141
  return { entityCount, kbModified };
@@ -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<SyncSummary>;
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,WAAW,CAAC,CA4VtB;AAED,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,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"}
@@ -68,23 +68,17 @@ export async function syncCommand(options = {}) {
68
68
  }
69
69
  currentBranch = branchResult.branch;
70
70
  if (process.env.KIBI_DEBUG) {
71
- try {
72
- // eslint-disable-next-line no-console
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
- try {
82
- // eslint-disable-next-line no-console
83
- console.log("[kibi-debug] markdownFiles:", markdownFiles.length);
84
- // eslint-disable-next-line no-console
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
- if (errors.length > 0) {
147
- for (const err of errors) {
148
- console.error(`${err.file}: ${err.message}`);
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
- for (const file of manifestFiles) {
159
- try {
160
- await refreshManifestCoordinates(file, process.cwd());
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
- process.exit(0);
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":"AAsBA,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;CACnB;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,CA4JtE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASjE"}
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
+ }
@@ -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;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDtE;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,CA2E5D;AAmED;;;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"}
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"}