kibi-cli 0.10.0 → 0.11.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 (46) hide show
  1. package/dist/cli.d.ts.map +1 -1
  2. package/dist/cli.js +11 -0
  3. package/dist/commands/check.d.ts.map +1 -1
  4. package/dist/commands/check.js +204 -3
  5. package/dist/commands/init-helpers.d.ts.map +1 -1
  6. package/dist/commands/init-helpers.js +11 -14
  7. package/dist/commands/sync/manifest.d.ts +8 -2
  8. package/dist/commands/sync/manifest.d.ts.map +1 -1
  9. package/dist/commands/sync/manifest.js +56 -11
  10. package/dist/commands/sync.d.ts +1 -0
  11. package/dist/commands/sync.d.ts.map +1 -1
  12. package/dist/commands/sync.js +9 -7
  13. package/dist/commands/usage-metrics.d.ts +8 -0
  14. package/dist/commands/usage-metrics.d.ts.map +1 -0
  15. package/dist/commands/usage-metrics.js +323 -0
  16. package/dist/extractors/manifest.d.ts +30 -0
  17. package/dist/extractors/manifest.d.ts.map +1 -1
  18. package/dist/extractors/manifest.js +60 -7
  19. package/dist/extractors/symbol-coordinates.d.ts +15 -0
  20. package/dist/extractors/symbol-coordinates.d.ts.map +1 -0
  21. package/dist/extractors/symbol-coordinates.js +83 -0
  22. package/dist/public/extractors/manifest.d.ts +1 -1
  23. package/dist/public/extractors/manifest.d.ts.map +1 -1
  24. package/dist/public/extractors/manifest.js +1 -1
  25. package/dist/traceability/evidence-model.d.ts +142 -0
  26. package/dist/traceability/evidence-model.d.ts.map +1 -0
  27. package/dist/traceability/evidence-model.js +70 -0
  28. package/dist/traceability/git-staged.d.ts +1 -0
  29. package/dist/traceability/git-staged.d.ts.map +1 -1
  30. package/dist/traceability/git-staged.js +28 -3
  31. package/dist/traceability/staged-diagnostics.d.ts +25 -0
  32. package/dist/traceability/staged-diagnostics.d.ts.map +1 -0
  33. package/dist/traceability/staged-diagnostics.js +67 -0
  34. package/dist/traceability/staged-impact-contract.d.ts +57 -0
  35. package/dist/traceability/staged-impact-contract.d.ts.map +1 -0
  36. package/dist/traceability/staged-impact-contract.js +202 -0
  37. package/dist/traceability/staged-symbols-manifest.d.ts +23 -0
  38. package/dist/traceability/staged-symbols-manifest.d.ts.map +1 -0
  39. package/dist/traceability/staged-symbols-manifest.js +269 -0
  40. package/dist/traceability/symbol-extract.d.ts.map +1 -1
  41. package/dist/traceability/symbol-extract.js +33 -9
  42. package/dist/utils/manifest-paths.d.ts +8 -0
  43. package/dist/utils/manifest-paths.d.ts.map +1 -0
  44. package/dist/utils/manifest-paths.js +62 -0
  45. package/package.json +1 -1
  46. package/src/public/extractors/manifest.ts +2 -0
@@ -0,0 +1,323 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import Table from "cli-table3";
4
+ // implements REQ-003
5
+ export async function usageMetricsCommand(options) {
6
+ const limit = Number.parseInt(options.limit || "10", 10);
7
+ if (!Number.isFinite(limit) || limit < 1) {
8
+ console.error("Error: --limit must be a positive integer");
9
+ return { exitCode: 1 };
10
+ }
11
+ const usageLogPath = path.join(process.cwd(), ".kb", "usage.log");
12
+ if (!existsSync(usageLogPath)) {
13
+ console.error(`Error: usage log not found at ${usageLogPath}`);
14
+ return { exitCode: 1 };
15
+ }
16
+ const rows = parseUsageLog(readFileSync(usageLogPath, "utf8"));
17
+ const report = buildUsageMetricsReport(rows, limit);
18
+ if (options.format === "json") {
19
+ console.log(JSON.stringify(report, null, 2));
20
+ return;
21
+ }
22
+ console.log(renderUsageMetricsReport(report));
23
+ return;
24
+ }
25
+ function parseUsageLog(contents) {
26
+ const rows = [];
27
+ for (const [index, line] of contents.split(/\r?\n/).entries()) {
28
+ const trimmed = line.trim();
29
+ if (!trimmed) {
30
+ continue;
31
+ }
32
+ let parsed;
33
+ try {
34
+ parsed = JSON.parse(trimmed);
35
+ }
36
+ catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: ${message}`);
39
+ }
40
+ if (!parsed || typeof parsed !== "object") {
41
+ throw new Error(`Failed to parse .kb/usage.log line ${index + 1}: expected object`);
42
+ }
43
+ rows.push(parsed);
44
+ }
45
+ return rows;
46
+ }
47
+ function buildUsageMetricsReport(rows, limit) {
48
+ const timestamps = rows
49
+ .map((row) => row.timestamp)
50
+ .filter((value) => typeof value === "string")
51
+ .sort((left, right) => left.localeCompare(right));
52
+ const toolCounts = new Map();
53
+ const branchCounts = new Map();
54
+ const zeroResultToolCounts = new Map();
55
+ const zeroResultSourceFileCounts = new Map();
56
+ const upsertErrorCategories = new Map();
57
+ const violationTrend = [];
58
+ let successCount = 0;
59
+ let errorCount = 0;
60
+ let telemetryCompleteCount = 0;
61
+ let telemetryMissingCount = 0;
62
+ let zeroResultCount = 0;
63
+ for (const row of rows) {
64
+ increment(toolCounts, normalizeKey(row.tool, "unknown"));
65
+ increment(branchCounts, normalizeKey(row.active_branch || row.branch, "unknown"));
66
+ const outcome = getOutcome(row);
67
+ if (outcome === "success") {
68
+ successCount += 1;
69
+ }
70
+ if (outcome === "error") {
71
+ errorCount += 1;
72
+ }
73
+ if (hasCompleteTelemetry(row)) {
74
+ telemetryCompleteCount += 1;
75
+ }
76
+ else {
77
+ telemetryMissingCount += 1;
78
+ }
79
+ if (isZeroResult(row)) {
80
+ zeroResultCount += 1;
81
+ increment(zeroResultToolCounts, normalizeKey(row.tool, "unknown"));
82
+ const sourceFile = getSourceFile(row);
83
+ if (sourceFile) {
84
+ increment(zeroResultSourceFileCounts, sourceFile);
85
+ }
86
+ }
87
+ if (row.tool === "kb_check" &&
88
+ typeof row.violation_count === "number" &&
89
+ typeof row.timestamp === "string") {
90
+ violationTrend.push({
91
+ timestamp: row.timestamp,
92
+ violationCount: row.violation_count,
93
+ });
94
+ }
95
+ if (row.tool === "kb_upsert" && outcome === "error") {
96
+ increment(upsertErrorCategories, categorizeUpsertError(row.error_message ?? row.error));
97
+ }
98
+ }
99
+ violationTrend.sort((left, right) => left.timestamp.localeCompare(right.timestamp));
100
+ return {
101
+ rowCount: rows.length,
102
+ dateRange: {
103
+ first: timestamps[0] ?? null,
104
+ last: timestamps.at(-1) ?? null,
105
+ },
106
+ toolCounts: mapToSortedObject(toolCounts),
107
+ branchCounts: mapToSortedObject(branchCounts),
108
+ outcomeCounts: {
109
+ success: successCount,
110
+ error: errorCount,
111
+ },
112
+ telemetry: {
113
+ completeCount: telemetryCompleteCount,
114
+ missingCount: telemetryMissingCount,
115
+ completenessRate: rows.length === 0 ? 0 : telemetryCompleteCount / rows.length,
116
+ },
117
+ zeroResults: {
118
+ count: zeroResultCount,
119
+ rate: rows.length === 0 ? 0 : zeroResultCount / rows.length,
120
+ byTool: mapToSortedObject(zeroResultToolCounts),
121
+ topSourceFiles: sortCountEntries(zeroResultSourceFileCounts)
122
+ .slice(0, limit)
123
+ .map(([sourceFile, count]) => ({ sourceFile, count })),
124
+ },
125
+ kbCheck: {
126
+ violationTrend,
127
+ },
128
+ upsertErrors: {
129
+ categories: mapToSortedObject(upsertErrorCategories),
130
+ },
131
+ };
132
+ }
133
+ function hasCompleteTelemetry(row) {
134
+ if (row.telemetry_status === "provided") {
135
+ return true;
136
+ }
137
+ if (row.telemetry_status === "missing") {
138
+ return false;
139
+ }
140
+ return row.telemetry !== null && row.telemetry !== undefined;
141
+ }
142
+ function getOutcome(row) {
143
+ if (row.status === "success" || row.status === "error") {
144
+ return row.status;
145
+ }
146
+ if (row.success === true) {
147
+ return "success";
148
+ }
149
+ if (row.success === false) {
150
+ return "error";
151
+ }
152
+ return null;
153
+ }
154
+ function isZeroResult(row) {
155
+ if (row.zero_results === true) {
156
+ return true;
157
+ }
158
+ if (row.zero_results === false) {
159
+ return false;
160
+ }
161
+ if (row.result_count === 0) {
162
+ return true;
163
+ }
164
+ return row.result_summary === "0 results";
165
+ }
166
+ function getSourceFile(row) {
167
+ if (typeof row.sourceFile === "string" && row.sourceFile.trim()) {
168
+ return row.sourceFile;
169
+ }
170
+ if (row.args &&
171
+ typeof row.args.sourceFile === "string" &&
172
+ row.args.sourceFile.trim()) {
173
+ return row.args.sourceFile;
174
+ }
175
+ if (row.business_args &&
176
+ typeof row.business_args.sourceFile === "string" &&
177
+ row.business_args.sourceFile.trim()) {
178
+ return row.business_args.sourceFile;
179
+ }
180
+ return null;
181
+ }
182
+ function categorizeUpsertError(errorMessage) {
183
+ if (!errorMessage) {
184
+ return "Unknown error";
185
+ }
186
+ if (errorMessage.startsWith("Entity validation failed:")) {
187
+ return "Entity validation failed";
188
+ }
189
+ if (errorMessage.startsWith("Relationship source must match the upserted entity")) {
190
+ return "Relationship source must match the upserted entity";
191
+ }
192
+ const semicolonIndex = errorMessage.indexOf(";");
193
+ if (semicolonIndex > 0) {
194
+ return errorMessage.slice(0, semicolonIndex).trim();
195
+ }
196
+ const colonIndex = errorMessage.indexOf(":");
197
+ if (colonIndex > 0) {
198
+ return errorMessage.slice(0, colonIndex).trim();
199
+ }
200
+ return errorMessage.trim() || "Unknown error";
201
+ }
202
+ function increment(counts, key) {
203
+ counts.set(key, (counts.get(key) ?? 0) + 1);
204
+ }
205
+ function normalizeKey(value, fallback) {
206
+ return typeof value === "string" && value.trim() ? value : fallback;
207
+ }
208
+ function sortCountEntries(counts) {
209
+ return [...counts.entries()].sort(([leftKey, leftCount], [rightKey, rightCount]) => rightCount - leftCount || leftKey.localeCompare(rightKey));
210
+ }
211
+ function mapToSortedObject(counts) {
212
+ return Object.fromEntries(sortCountEntries(counts));
213
+ }
214
+ function renderUsageMetricsReport(report) {
215
+ const sections = [
216
+ renderSummaryTable(report),
217
+ renderCountsTable("Tool Counts", "Tool", report.toolCounts),
218
+ renderCountsTable("Branch Counts", "Branch", report.branchCounts),
219
+ renderCountsTable("Outcome Counts", "Outcome", report.outcomeCounts),
220
+ renderTelemetryTable(report),
221
+ renderZeroResultsTable(report),
222
+ renderViolationTrendTable(report),
223
+ renderCountsTable("Upsert Error Categories", "Category", report.upsertErrors.categories),
224
+ ].filter(Boolean);
225
+ return sections.join("\n\n");
226
+ }
227
+ function renderSummaryTable(report) {
228
+ const table = new Table({
229
+ head: ["Field", "Value"],
230
+ colWidths: [24, 56],
231
+ wordWrap: true,
232
+ });
233
+ table.push(["Row Count", String(report.rowCount)], ["First Timestamp", report.dateRange.first ?? "-"], ["Last Timestamp", report.dateRange.last ?? "-"]);
234
+ return table.toString();
235
+ }
236
+ function renderCountsTable(title, label, counts) {
237
+ const entries = Object.entries(counts);
238
+ const table = new Table({
239
+ head: [label, "Count"],
240
+ colWidths: [48, 12],
241
+ wordWrap: true,
242
+ });
243
+ if (entries.length === 0) {
244
+ table.push(["-", "0"]);
245
+ }
246
+ else {
247
+ for (const [key, count] of entries) {
248
+ table.push([key, String(count)]);
249
+ }
250
+ }
251
+ return `${title}\n${table.toString()}`;
252
+ }
253
+ function renderTelemetryTable(report) {
254
+ const table = new Table({
255
+ head: ["Metric", "Value"],
256
+ colWidths: [32, 28],
257
+ wordWrap: true,
258
+ });
259
+ table.push(["Complete", String(report.telemetry.completeCount)], ["Missing", String(report.telemetry.missingCount)], ["Completeness Rate", formatRate(report.telemetry.completenessRate)]);
260
+ return `Telemetry\n${table.toString()}`;
261
+ }
262
+ function renderZeroResultsTable(report) {
263
+ const summary = new Table({
264
+ head: ["Metric", "Value"],
265
+ colWidths: [32, 28],
266
+ wordWrap: true,
267
+ });
268
+ summary.push(["Count", String(report.zeroResults.count)], ["Rate", formatRate(report.zeroResults.rate)]);
269
+ const byTool = new Table({
270
+ head: ["Tool", "Count"],
271
+ colWidths: [48, 12],
272
+ wordWrap: true,
273
+ });
274
+ const byToolEntries = Object.entries(report.zeroResults.byTool);
275
+ if (byToolEntries.length === 0) {
276
+ byTool.push(["-", "0"]);
277
+ }
278
+ else {
279
+ for (const [tool, count] of byToolEntries) {
280
+ byTool.push([tool, String(count)]);
281
+ }
282
+ }
283
+ const sourceFiles = new Table({
284
+ head: ["Source File", "Zero Results"],
285
+ colWidths: [48, 14],
286
+ wordWrap: true,
287
+ });
288
+ if (report.zeroResults.topSourceFiles.length === 0) {
289
+ sourceFiles.push(["-", "0"]);
290
+ }
291
+ else {
292
+ for (const entry of report.zeroResults.topSourceFiles) {
293
+ sourceFiles.push([entry.sourceFile, String(entry.count)]);
294
+ }
295
+ }
296
+ return [
297
+ "Zero Results",
298
+ summary.toString(),
299
+ "Zero-Result Counts By Tool",
300
+ byTool.toString(),
301
+ "Zero-Result Source Files",
302
+ sourceFiles.toString(),
303
+ ].join("\n");
304
+ }
305
+ function renderViolationTrendTable(report) {
306
+ const table = new Table({
307
+ head: ["Timestamp", "Violations"],
308
+ colWidths: [32, 12],
309
+ wordWrap: true,
310
+ });
311
+ if (report.kbCheck.violationTrend.length === 0) {
312
+ table.push(["-", "0"]);
313
+ }
314
+ else {
315
+ for (const entry of report.kbCheck.violationTrend) {
316
+ table.push([entry.timestamp, String(entry.violationCount)]);
317
+ }
318
+ }
319
+ return `KB Check Violation Trend\n${table.toString()}`;
320
+ }
321
+ function formatRate(value) {
322
+ return `${(value * 100).toFixed(1)}%`;
323
+ }
@@ -27,6 +27,36 @@ export declare class ManifestError extends Error {
27
27
  filePath: string;
28
28
  constructor(message: string, filePath: string);
29
29
  }
30
+ export interface ManifestSymbolRecord {
31
+ id?: string;
32
+ title?: string;
33
+ source?: string;
34
+ sourceFile?: string;
35
+ status?: string;
36
+ tags?: string[];
37
+ owner?: string;
38
+ priority?: string;
39
+ severity?: string;
40
+ text_ref?: string;
41
+ created_at?: string;
42
+ updated_at?: string;
43
+ links?: Array<string | {
44
+ type: string;
45
+ target: string;
46
+ }>;
47
+ relationships?: Array<{
48
+ type: string;
49
+ target: string;
50
+ }>;
51
+ sourceLine?: number;
52
+ sourceColumn?: number;
53
+ sourceEndLine?: number;
54
+ sourceEndColumn?: number;
55
+ coordinatesGeneratedAt?: string;
56
+ [key: string]: unknown;
57
+ }
30
58
  export declare function extractFromManifestString(content: string, filePath: string): ExtractionResult[];
59
+ export declare function extractManifestSymbolRecordsString(content: string, filePath: string): ManifestSymbolRecord[];
31
60
  export declare function extractFromManifest(filePath: string): ExtractionResult[];
61
+ export declare function readManifestWithCoordinateOverlay(manifestPath: string, coordinatesPath?: string): ManifestSymbolRecord[];
32
62
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/extractors/manifest.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;IACvC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,aAAc,SAAQ,KAAK;IAG7B,QAAQ,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACR,QAAQ,EAAE,MAAM;CAK1B;AA+GD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,gBAAgB,EAAE,CAmBpB;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAGxE"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/extractors/manifest.ts"],"names":[],"mappings":"AA6BA,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;IACvC,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,aAAc,SAAQ,KAAK;IAG7B,QAAQ,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACR,QAAQ,EAAE,MAAM;CAK1B;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,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;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACzD,aAAa,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAmHD,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,gBAAgB,EAAE,CAmBpB;AAED,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,oBAAoB,EAAE,CAmBxB;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CAKxE;AAsCD,wBAAgB,iCAAiC,CAC/C,YAAY,EAAE,MAAM,EACpB,eAAe,CAAC,EAAE,MAAM,GACvB,oBAAoB,EAAE,CAWxB"}
@@ -16,8 +16,11 @@
16
16
  along with this program. If not, see <https://www.gnu.org/licenses/>.
17
17
  */
18
18
  import { createHash } from "node:crypto";
19
- import { readFileSync } from "node:fs";
19
+ import { existsSync, readFileSync } from "node:fs";
20
+ import * as path from "node:path";
20
21
  import { load as parseYAML } from "js-yaml";
22
+ import { DEFAULT_COORDINATES_PATH } from "../utils/manifest-paths.js";
23
+ import { mergeCoordinatesWithManifest, readCoordinateArtifact, } from "./symbol-coordinates.js";
21
24
  export class ManifestError extends Error {
22
25
  filePath;
23
26
  constructor(message, filePath) {
@@ -26,6 +29,12 @@ export class ManifestError extends Error {
26
29
  this.name = "ManifestError";
27
30
  }
28
31
  }
32
+ function getManifestSymbols(manifest, filePath) {
33
+ if (!manifest.symbols || !Array.isArray(manifest.symbols)) {
34
+ throw new ManifestError("No symbols array found in manifest", filePath);
35
+ }
36
+ return manifest.symbols;
37
+ }
29
38
  function extractRelationships(id, symbol) {
30
39
  const relationships = [];
31
40
  if (Array.isArray(symbol.links)) {
@@ -66,10 +75,10 @@ function extractRelationships(id, symbol) {
66
75
  return relationships;
67
76
  }
68
77
  function extractFromParsedManifest(manifest, filePath) {
69
- if (!manifest.symbols || !Array.isArray(manifest.symbols)) {
70
- throw new ManifestError("No symbols array found in manifest", filePath);
71
- }
72
- return manifest.symbols.map((symbol) => {
78
+ return extractFromManifestSymbolRecords(getManifestSymbols(manifest, filePath), filePath);
79
+ }
80
+ function extractFromManifestSymbolRecords(manifestSymbols, filePath) {
81
+ return manifestSymbols.map((symbol) => {
73
82
  if (!symbol.title) {
74
83
  throw new ManifestError("Missing required field: title", filePath);
75
84
  }
@@ -96,6 +105,9 @@ function extractFromParsedManifest(manifest, filePath) {
96
105
  };
97
106
  });
98
107
  }
108
+ function cloneManifestSymbols(manifest, filePath) {
109
+ return getManifestSymbols(manifest, filePath).map((symbol) => ({ ...symbol }));
110
+ }
99
111
  // implements REQ-007
100
112
  export function extractFromManifestString(content, filePath) {
101
113
  try {
@@ -112,9 +124,50 @@ export function extractFromManifestString(content, filePath) {
112
124
  throw error;
113
125
  }
114
126
  }
127
+ export function extractManifestSymbolRecordsString(content, filePath) {
128
+ try {
129
+ const manifest = parseYAML(content);
130
+ return cloneManifestSymbols(manifest, filePath);
131
+ }
132
+ catch (error) {
133
+ if (error instanceof ManifestError) {
134
+ throw error;
135
+ }
136
+ if (error instanceof Error) {
137
+ throw new ManifestError(`Failed to parse manifest: ${error.message}`, filePath);
138
+ }
139
+ throw error;
140
+ }
141
+ }
115
142
  export function extractFromManifest(filePath) {
116
- const content = readFileSync(filePath, "utf8");
117
- return extractFromManifestString(content, filePath);
143
+ return extractFromManifestSymbolRecords(readManifestWithCoordinateOverlay(filePath), filePath);
144
+ }
145
+ function resolveCoordinatesPath(manifestPath, coordinatesPath) {
146
+ if (coordinatesPath) {
147
+ return coordinatesPath;
148
+ }
149
+ return path.join(path.dirname(manifestPath), path.basename(DEFAULT_COORDINATES_PATH));
150
+ }
151
+ function readCoordinateArtifactFromFile(coordinatesPath) {
152
+ if (!existsSync(coordinatesPath)) {
153
+ return null;
154
+ }
155
+ try {
156
+ return readCoordinateArtifact(readFileSync(coordinatesPath, "utf8"));
157
+ }
158
+ catch (error) {
159
+ if (error instanceof Error) {
160
+ throw new ManifestError(`Failed to parse coordinate artifact: ${error.message}`, coordinatesPath);
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+ // implements REQ-core-extractors
166
+ export function readManifestWithCoordinateOverlay(manifestPath, coordinatesPath) {
167
+ const manifestContent = readFileSync(manifestPath, "utf8");
168
+ const manifestRecords = extractManifestSymbolRecordsString(manifestContent, manifestPath);
169
+ const coordinateArtifact = readCoordinateArtifactFromFile(resolveCoordinatesPath(manifestPath, coordinatesPath));
170
+ return mergeCoordinatesWithManifest(manifestRecords, coordinateArtifact);
118
171
  }
119
172
  function generateId(filePath, title) {
120
173
  const hash = createHash("sha256");
@@ -0,0 +1,15 @@
1
+ import type { ManifestSymbolRecord } from "./manifest.js";
2
+ export interface SymbolCoordinatesRecord {
3
+ sourceFile: string;
4
+ sourceLine: number;
5
+ sourceColumn: number;
6
+ sourceEndLine: number;
7
+ sourceEndColumn: number;
8
+ }
9
+ export interface SymbolCoordinatesArtifact {
10
+ coordinates: Record<string, SymbolCoordinatesRecord>;
11
+ }
12
+ export declare function readCoordinateArtifact(content: string): SymbolCoordinatesArtifact;
13
+ export declare function writeCoordinateArtifact(coordinates: Record<string, SymbolCoordinatesRecord>): string;
14
+ export declare function mergeCoordinatesWithManifest(symbolRecords: ManifestSymbolRecord[], coordinateArtifact: SymbolCoordinatesArtifact | null): ManifestSymbolRecord[];
15
+ //# sourceMappingURL=symbol-coordinates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbol-coordinates.d.ts","sourceRoot":"","sources":["../../src/extractors/symbol-coordinates.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAE1D,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,yBAAyB;IACxC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;CACtD;AA0DD,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,MAAM,GACd,yBAAyB,CAkB3B;AAGD,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,GACnD,MAAM,CAUR;AAGD,wBAAgB,4BAA4B,CAC1C,aAAa,EAAE,oBAAoB,EAAE,EACrC,kBAAkB,EAAE,yBAAyB,GAAG,IAAI,GACnD,oBAAoB,EAAE,CAoBxB"}
@@ -0,0 +1,83 @@
1
+ import { dump as dumpYAML, load as parseYAML } from "js-yaml";
2
+ const SYMBOL_COORDINATES_COMMENT_BLOCK = `# symbol-coordinates.yaml
3
+ # GENERATED coordinate artifact — do not edit manually.
4
+ # Run \`kibi sync --refresh-symbol-coordinates\` to refresh.
5
+ `;
6
+ function isRecord(value) {
7
+ return typeof value === "object" && value !== null && !Array.isArray(value);
8
+ }
9
+ function normalizeCoordinateRecord(value) {
10
+ if (!isRecord(value)) {
11
+ return null;
12
+ }
13
+ const { sourceColumn, sourceEndColumn, sourceEndLine, sourceFile, sourceLine } = value;
14
+ if (typeof sourceFile !== "string" ||
15
+ typeof sourceLine !== "number" ||
16
+ typeof sourceColumn !== "number" ||
17
+ typeof sourceEndLine !== "number" ||
18
+ typeof sourceEndColumn !== "number") {
19
+ return null;
20
+ }
21
+ return {
22
+ sourceFile,
23
+ sourceLine,
24
+ sourceColumn,
25
+ sourceEndLine,
26
+ sourceEndColumn,
27
+ };
28
+ }
29
+ function sortCoordinates(coordinates) {
30
+ const sortedCoordinates = {};
31
+ for (const symbolId of Object.keys(coordinates).sort((left, right) => left.localeCompare(right))) {
32
+ const record = normalizeCoordinateRecord(coordinates[symbolId]);
33
+ if (!record) {
34
+ continue;
35
+ }
36
+ sortedCoordinates[symbolId] = record;
37
+ }
38
+ return sortedCoordinates;
39
+ }
40
+ // implements REQ-core-extractors
41
+ export function readCoordinateArtifact(content) {
42
+ const parsed = parseYAML(content);
43
+ if (!isRecord(parsed) || !isRecord(parsed.coordinates)) {
44
+ return { coordinates: {} };
45
+ }
46
+ const coordinates = {};
47
+ for (const [symbolId, record] of Object.entries(parsed.coordinates)) {
48
+ const normalizedRecord = normalizeCoordinateRecord(record);
49
+ if (!normalizedRecord) {
50
+ continue;
51
+ }
52
+ coordinates[symbolId] = normalizedRecord;
53
+ }
54
+ return { coordinates };
55
+ }
56
+ // implements REQ-core-extractors
57
+ export function writeCoordinateArtifact(coordinates) {
58
+ const artifact = {
59
+ coordinates: sortCoordinates(coordinates),
60
+ };
61
+ return `${SYMBOL_COORDINATES_COMMENT_BLOCK}${dumpYAML(artifact, {
62
+ lineWidth: -1,
63
+ noRefs: true,
64
+ sortKeys: true,
65
+ })}`;
66
+ }
67
+ // implements REQ-core-extractors
68
+ export function mergeCoordinatesWithManifest(symbolRecords, coordinateArtifact) {
69
+ const coordinates = coordinateArtifact?.coordinates ?? {};
70
+ return symbolRecords.map((symbolRecord) => {
71
+ const legacyRecord = { ...symbolRecord };
72
+ const symbolId = typeof symbolRecord.id === "string" ? symbolRecord.id : undefined;
73
+ const coordinateRecord = symbolId ? coordinates[symbolId] : undefined;
74
+ if (!coordinateRecord) {
75
+ return legacyRecord;
76
+ }
77
+ const { coordinatesGeneratedAt: _coordinatesGeneratedAt, ...mergedRecord } = legacyRecord;
78
+ return {
79
+ ...mergedRecord,
80
+ ...coordinateRecord,
81
+ };
82
+ });
83
+ }
@@ -1,2 +1,2 @@
1
- export { extractFromManifest, extractFromManifestString, type ExtractionResult, type ManifestError, } from "../../extractors/manifest.js";
1
+ export { extractFromManifest, extractFromManifestString, readManifestWithCoordinateOverlay, type ExtractionResult, type ManifestError, type ManifestSymbolRecord, } from "../../extractors/manifest.js";
2
2
  //# sourceMappingURL=manifest.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/public/extractors/manifest.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,KAAK,gBAAgB,EACrB,KAAK,aAAa,GACnB,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/public/extractors/manifest.ts"],"names":[],"mappings":"AAkBA,OAAO,EACL,mBAAmB,EACnB,yBAAyB,EACzB,iCAAiC,EACjC,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,oBAAoB,GAC1B,MAAM,8BAA8B,CAAC"}
@@ -15,4 +15,4 @@
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
17
  */
18
- export { extractFromManifest, extractFromManifestString, } from "../../extractors/manifest.js";
18
+ export { extractFromManifest, extractFromManifestString, readManifestWithCoordinateOverlay, } from "../../extractors/manifest.js";