kibi-cli 0.4.1 → 0.5.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.
@@ -1 +1 @@
1
- {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AA4CA,OAAO,EAGL,KAAK,SAAS,EAEf,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EAAE,SAAS,EAAE,CAAC;AAK1B,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAGD,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CA0R/B"}
1
+ {"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../src/commands/check.ts"],"names":[],"mappings":"AAkDA,OAAO,EAGL,KAAK,SAAS,EAEf,MAAM,2BAA2B,CAAC;AAEnC,YAAY,EAAE,SAAS,EAAE,CAAC;AAI1B,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AA4DD,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgR/B"}
@@ -16,20 +16,60 @@
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 { extractFromManifest } from "../extractors/manifest.js";
19
+ import { extractFromManifestString } from "../extractors/manifest.js";
20
+ import { extractFromMarkdownString, } from "../extractors/markdown.js";
20
21
  import { PrologProcess } from "../prolog.js";
21
22
  import { escapeAtom, parseTriples, parseViolationRows, } from "../prolog/codec.js";
22
23
  import { getStagedFiles } from "../traceability/git-staged.js";
23
24
  import { validateStagedMarkdown } from "../traceability/markdown-validate.js";
24
- import { extractSymbolsFromStagedFile, } from "../traceability/symbol-extract.js";
25
- import { cleanupTempKb, consultOverlay, createOverlayFacts, createTempKb, } from "../traceability/temp-kb.js";
25
+ import { createManifestLookupSentinelKey, extractSymbolsFromStagedFile, } from "../traceability/symbol-extract.js";
26
+ import { cleanupTempKb, consultOverlay, createOverlayFacts, createTempKb, projectStagedEntities, } from "../traceability/temp-kb.js";
26
27
  import { formatViolations as formatStagedViolations, validateStagedSymbols, } from "../traceability/validate.js";
27
28
  import { loadConfig } from "../utils/config.js";
28
29
  import { safeCleanupProlog } from "../utils/prolog-cleanup.js";
29
30
  import { RULES, getEffectiveRules, } from "../utils/rule-registry.js";
30
31
  import { runAggregatedChecks } from "./aggregated-checks.js";
31
32
  import { getCurrentBranch } from "./init-helpers.js";
32
- import { discoverSourceFiles } from "./sync/discovery.js";
33
+ function buildManifestLookup(stagedFiles) {
34
+ const manifestLookup = new Map();
35
+ const manifestResults = [];
36
+ const stagedManifestFiles = stagedFiles.filter((file) => file.content !== undefined &&
37
+ (file.path.endsWith("/symbols.yaml") ||
38
+ file.path.endsWith("/symbols.yml") ||
39
+ file.path === "symbols.yaml" ||
40
+ file.path === "symbols.yml"));
41
+ for (const manifestFile of stagedManifestFiles) {
42
+ manifestLookup.set(createManifestLookupSentinelKey(manifestFile.path), {
43
+ id: manifestFile.path,
44
+ relationships: [],
45
+ });
46
+ try {
47
+ const entries = extractFromManifestString(manifestFile.content ?? "", manifestFile.path);
48
+ for (const entry of entries) {
49
+ manifestResults.push({
50
+ entity: entry.entity,
51
+ relationships: entry.relationships,
52
+ });
53
+ const sourceFile = entry.sourceFile || entry.entity.source || manifestFile.path;
54
+ const key = `${sourceFile}:${entry.entity.title}`;
55
+ manifestLookup.set(key, {
56
+ id: entry.entity.id,
57
+ relationships: entry.relationships
58
+ .filter((relationship) => relationship.type === "implements" ||
59
+ relationship.type === "covered_by")
60
+ .map((relationship) => ({
61
+ type: relationship.type,
62
+ to: relationship.to,
63
+ })),
64
+ });
65
+ }
66
+ }
67
+ catch {
68
+ // Ignore manifest parsing errors
69
+ }
70
+ }
71
+ return { manifestLookup, manifestResults };
72
+ }
33
73
  // implements REQ-006
34
74
  export async function checkCommand(options) {
35
75
  let prolog = null;
@@ -59,33 +99,12 @@ export async function checkCommand(options) {
59
99
  const minLinks = options.minLinks ? Number(options.minLinks) : 1;
60
100
  let tempCtx = null;
61
101
  try {
62
- const config = loadConfig(process.cwd());
63
- const manifestLookup = new Map();
64
- const { manifestFiles } = await discoverSourceFiles(process.cwd(), config.paths);
65
- for (const manifestPath of manifestFiles) {
66
- try {
67
- const entries = extractFromManifest(manifestPath);
68
- for (const entry of entries) {
69
- // Prefer the per-symbol sourceFile; fall back to entity.source or manifest path
70
- const sourceFile = entry.sourceFile || entry.entity.source || manifestPath;
71
- const key = `${sourceFile}:${entry.entity.title}`;
72
- // Extract requirement links (implements relationships to REQ-*)
73
- const links = entry.relationships
74
- .filter((r) => r.type === "implements" &&
75
- r.to.match(/^[A-Z][A-Z0-9\-_]*$/))
76
- .map((r) => r.to);
77
- manifestLookup.set(key, { id: entry.entity.id, links });
78
- }
79
- }
80
- catch {
81
- // Ignore manifest parsing errors
82
- }
83
- }
84
102
  const stagedFiles = getStagedFiles();
85
103
  if (!stagedFiles || stagedFiles.length === 0) {
86
104
  console.log("No staged files found.");
87
105
  return { exitCode: 0 };
88
106
  }
107
+ const { manifestLookup, manifestResults } = buildManifestLookup(stagedFiles);
89
108
  const codeFiles = stagedFiles.filter((f) => !f.path.endsWith(".md"));
90
109
  const markdownFiles = stagedFiles.filter((f) => f.path.endsWith(".md"));
91
110
  const markdownErrors = [];
@@ -118,8 +137,13 @@ export async function checkCommand(options) {
118
137
  console.error(`Error extracting symbols from staged file ${f.path}: ${e instanceof Error ? e.message : String(e)}`);
119
138
  }
120
139
  }
121
- if (allSymbols.length === 0 && markdownFiles.length === 0) {
122
- console.log("No exported symbols or markdown entities found in staged files.");
140
+ const markdownResults = markdownFiles.map((file) => extractFromMarkdownString(file.content ?? "", file.path));
141
+ const stagedEntityResults = [
142
+ ...manifestResults,
143
+ ...markdownResults,
144
+ ];
145
+ if (allSymbols.length === 0 && stagedEntityResults.length === 0) {
146
+ console.log("No exported symbols or staged entities found in staged files.");
123
147
  return { exitCode: 0 };
124
148
  }
125
149
  if (allSymbols.length === 0) {
@@ -128,9 +152,13 @@ export async function checkCommand(options) {
128
152
  }
129
153
  // Create temp KB
130
154
  tempCtx = await createTempKb(resolvedKbPath);
155
+ if (stagedEntityResults.length > 0) {
156
+ await projectStagedEntities(tempCtx.prolog, stagedEntityResults);
157
+ }
131
158
  const overlayFacts = createOverlayFacts(allSymbols);
132
159
  const fs = await import("node:fs/promises");
133
160
  await fs.writeFile(tempCtx.overlayPath, overlayFacts, "utf8");
161
+ await fs.cp(tempCtx.overlayPath, path.join(tempCtx.kbPath, "changed_symbols.pl"));
134
162
  await consultOverlay(tempCtx);
135
163
  const violationsRaw = await validateStagedSymbols({
136
164
  minLinks,
@@ -1 +1 @@
1
- {"version":3,"file":"init-helpers.d.ts","sourceRoot":"","sources":["../../src/commands/init-helpers.ts"],"names":[],"mappings":"AAmFA,wBAAsB,gBAAgB,CACpC,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAajD;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAYf;AASD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAmCnE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAiBpD"}
1
+ {"version":3,"file":"init-helpers.d.ts","sourceRoot":"","sources":["../../src/commands/init-helpers.ts"],"names":[],"mappings":"AAuFA,wBAAsB,gBAAgB,CACpC,GAAG,GAAE,MAAsB,GAC1B,OAAO,CAAC,MAAM,CAAC,CASjB;AAED,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,GACpB,IAAI,CAQN;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAMpD;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAajD;AAED,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,MAAM,GACtB,OAAO,CAAC,IAAI,CAAC,CAYf;AASD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAmCnE;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAiBpD"}
@@ -24,6 +24,8 @@ const POST_CHECKOUT_HOOK = `#!/bin/sh
24
24
  # post-checkout hook for kibi
25
25
  # Parameters: old_ref new_ref branch_flag
26
26
  # branch_flag is 1 for branch checkout, 0 for file checkout
27
+ # Refresh branch/worktree assumptions after checkout so advisory plugin state
28
+ # starts from synced KB data instead of stale in-memory cache assumptions.
27
29
 
28
30
  old_ref=$1
29
31
  new_ref=$2
@@ -44,6 +46,7 @@ fi
44
46
  const POST_MERGE_HOOK = `#!/bin/sh
45
47
  # post-merge hook for kibi
46
48
  # Parameter: squash_flag (not used)
49
+ # Refresh KB state after merge so branch-level assumptions remain current.
47
50
 
48
51
  kibi sync
49
52
  `;
@@ -60,7 +63,8 @@ fi
60
63
  `;
61
64
  const PRE_COMMIT_HOOK = `#!/bin/sh
62
65
  # pre-commit hook for kibi
63
- # Blocks commits if kibi check finds violations
66
+ # Hard enforcement boundary: commits are blocked only here via kibi check.
67
+ # The OpenCode plugin remains advisory and must not replace this gate.
64
68
 
65
69
  set -e
66
70
  kibi check --staged
@@ -27,5 +27,6 @@ export declare class ManifestError extends Error {
27
27
  filePath: string;
28
28
  constructor(message: string, filePath: string);
29
29
  }
30
+ export declare function extractFromManifestString(content: string, filePath: string): ExtractionResult[];
30
31
  export declare function extractFromManifest(filePath: string): ExtractionResult[];
31
32
  //# 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;AAwBD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE,CA6FxE"}
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;AA6GD,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"}
@@ -26,77 +26,79 @@ export class ManifestError extends Error {
26
26
  this.name = "ManifestError";
27
27
  }
28
28
  }
29
- // implements REQ-007
30
- export function extractFromManifest(filePath) {
31
- try {
32
- const content = readFileSync(filePath, "utf8");
33
- const manifest = parseYAML(content);
34
- if (!manifest.symbols || !Array.isArray(manifest.symbols)) {
35
- throw new ManifestError("No symbols array found in manifest", filePath);
36
- }
37
- return manifest.symbols.map((symbol) => {
38
- if (!symbol.title) {
39
- throw new ManifestError("Missing required field: title", filePath);
29
+ function extractRelationships(id, symbol) {
30
+ const relationships = [];
31
+ if (Array.isArray(symbol.links)) {
32
+ for (const link of symbol.links) {
33
+ if (typeof link === "string") {
34
+ relationships.push({
35
+ type: "implements",
36
+ from: id,
37
+ to: link,
38
+ });
40
39
  }
41
- const id = symbol.id || generateId(filePath, symbol.title);
42
- const relationships = [];
43
- // Extract relationships from links field
44
- // Supports both simple strings (treated as implements) and typed objects
45
- if (Array.isArray(symbol.links)) {
46
- for (const link of symbol.links) {
47
- if (typeof link === "string") {
48
- relationships.push({
49
- type: "implements",
50
- from: id,
51
- to: link,
52
- });
53
- }
54
- else if (link !== null && typeof link === "object") {
55
- const typedLink = link;
56
- if (typeof typedLink.type === "string" &&
57
- typeof typedLink.target === "string") {
58
- relationships.push({
59
- type: typedLink.type,
60
- from: id,
61
- to: typedLink.target,
62
- });
63
- }
64
- }
40
+ else if (link !== null && typeof link === "object") {
41
+ const typedLink = link;
42
+ if (typeof typedLink.type === "string" &&
43
+ typeof typedLink.target === "string") {
44
+ relationships.push({
45
+ type: typedLink.type,
46
+ from: id,
47
+ to: typedLink.target,
48
+ });
65
49
  }
66
50
  }
67
- // Extract relationships from relationships field
68
- if (Array.isArray(symbol.relationships)) {
69
- for (const rel of symbol.relationships) {
70
- if (rel &&
71
- typeof rel.type === "string" &&
72
- typeof rel.target === "string") {
73
- relationships.push({
74
- type: rel.type,
75
- from: id,
76
- to: rel.target,
77
- });
78
- }
79
- }
51
+ }
52
+ }
53
+ if (Array.isArray(symbol.relationships)) {
54
+ for (const rel of symbol.relationships) {
55
+ if (rel &&
56
+ typeof rel.type === "string" &&
57
+ typeof rel.target === "string") {
58
+ relationships.push({
59
+ type: rel.type,
60
+ from: id,
61
+ to: rel.target,
62
+ });
80
63
  }
81
- return {
82
- entity: {
83
- id,
84
- type: "symbol",
85
- title: symbol.title,
86
- status: symbol.status || "active",
87
- created_at: symbol.created_at || new Date().toISOString(),
88
- updated_at: symbol.updated_at || new Date().toISOString(),
89
- source: filePath,
90
- tags: symbol.tags,
91
- owner: symbol.owner,
92
- priority: symbol.priority,
93
- severity: symbol.severity,
94
- text_ref: symbol.text_ref,
95
- },
96
- relationships,
97
- sourceFile: symbol.sourceFile ?? symbol.source,
98
- };
99
- });
64
+ }
65
+ }
66
+ return relationships;
67
+ }
68
+ 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) => {
73
+ if (!symbol.title) {
74
+ throw new ManifestError("Missing required field: title", filePath);
75
+ }
76
+ const id = symbol.id || generateId(filePath, symbol.title);
77
+ return {
78
+ entity: {
79
+ id,
80
+ type: "symbol",
81
+ title: symbol.title,
82
+ status: symbol.status || "active",
83
+ created_at: symbol.created_at || new Date().toISOString(),
84
+ updated_at: symbol.updated_at || new Date().toISOString(),
85
+ source: filePath,
86
+ tags: symbol.tags,
87
+ owner: symbol.owner,
88
+ priority: symbol.priority,
89
+ severity: symbol.severity,
90
+ text_ref: symbol.text_ref,
91
+ },
92
+ relationships: extractRelationships(id, symbol),
93
+ sourceFile: symbol.sourceFile ?? symbol.source,
94
+ };
95
+ });
96
+ }
97
+ // implements REQ-007
98
+ export function extractFromManifestString(content, filePath) {
99
+ try {
100
+ const manifest = parseYAML(content);
101
+ return extractFromParsedManifest(manifest, filePath);
100
102
  }
101
103
  catch (error) {
102
104
  if (error instanceof ManifestError) {
@@ -108,6 +110,10 @@ export function extractFromManifest(filePath) {
108
110
  throw error;
109
111
  }
110
112
  }
113
+ export function extractFromManifest(filePath) {
114
+ const content = readFileSync(filePath, "utf8");
115
+ return extractFromManifestString(content, filePath);
116
+ }
111
117
  function generateId(filePath, title) {
112
118
  const hash = createHash("sha256");
113
119
  hash.update(`${filePath}:${title}`);
@@ -50,6 +50,7 @@ export declare class FrontmatterError extends Error {
50
50
  toString(): string;
51
51
  }
52
52
  export declare function detectEmbeddedEntities(data: Record<string, unknown>, entityType: string): string[];
53
+ export declare function extractFromMarkdownString(content: string, filePath: string): ExtractionResult;
53
54
  export declare function extractFromMarkdown(filePath: string): ExtractionResult;
54
55
  export declare function inferTypeFromPath(filePath: string): string | null;
55
56
  export declare function normalizeDateLike(value: unknown): string | undefined;
@@ -1 +1 @@
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"}
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;AA6ND,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,gBAAgB,CAElB;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,CAatE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASjE;AASD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQpE"}
@@ -118,14 +118,7 @@ export function detectEmbeddedEntities(data, entityType) {
118
118
  return detected;
119
119
  }
120
120
  // implements REQ-007, REQ-004
121
- export function extractFromMarkdown(filePath) {
122
- let content;
123
- try {
124
- content = readFileSync(filePath, "utf8");
125
- }
126
- catch (error) {
127
- throw new FrontmatterError(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`, filePath, { classification: "File Read Error" });
128
- }
121
+ function extractFromMarkdownContent(content, filePath) {
129
122
  try {
130
123
  const { data } = matter(content);
131
124
  if (content.trim().startsWith("---")) {
@@ -292,6 +285,21 @@ export function extractFromMarkdown(filePath) {
292
285
  throw error;
293
286
  }
294
287
  }
288
+ // implements REQ-007, REQ-004
289
+ export function extractFromMarkdownString(content, filePath) {
290
+ return extractFromMarkdownContent(content, filePath);
291
+ }
292
+ // implements REQ-007, REQ-004
293
+ export function extractFromMarkdown(filePath) {
294
+ let content;
295
+ try {
296
+ content = readFileSync(filePath, "utf8");
297
+ }
298
+ catch (error) {
299
+ throw new FrontmatterError(`Failed to read file: ${error instanceof Error ? error.message : String(error)}`, filePath, { classification: "File Read Error" });
300
+ }
301
+ return extractFromMarkdownContent(content, filePath);
302
+ }
295
303
  export function inferTypeFromPath(filePath) {
296
304
  if (filePath.includes("/requirements/"))
297
305
  return "req";
@@ -1 +1 @@
1
- {"version":3,"file":"symbols-coordinator.d.ts","sourceRoot":"","sources":["../../src/extractors/symbols-coordinator.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAazB,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,mBAAmB,EAAE,EAC9B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAmChC"}
1
+ {"version":3,"file":"symbols-coordinator.d.ts","sourceRoot":"","sources":["../../src/extractors/symbols-coordinator.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,KAAK,mBAAmB,EAEzB,MAAM,iBAAiB,CAAC;AAazB,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,mBAAmB,EAAE,EAC9B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAoChC"}
@@ -29,6 +29,7 @@ const TS_JS_EXTENSIONS = new Set([
29
29
  ".cjs",
30
30
  ]);
31
31
  export async function enrichSymbolCoordinates(entries, workspaceRoot) {
32
+ // implements REQ-vscode-traceability
32
33
  const output = entries.map((entry) => ({ ...entry }));
33
34
  const tsIndices = [];
34
35
  const tsEntries = [];
@@ -29,6 +29,7 @@ const SUPPORTED_SOURCE_EXTENSIONS = new Set([
29
29
  ".cjs",
30
30
  ]);
31
31
  export async function enrichSymbolCoordinatesWithTsMorph(entries, workspaceRoot) {
32
+ // implements REQ-vscode-traceability
32
33
  const project = new Project({
33
34
  skipAddingFilesFromTsConfig: true,
34
35
  });
@@ -163,8 +164,41 @@ function findNamedDeclaration(sourceFile, title) {
163
164
  candidates.push({ node: declaration, getNameNode: () => nameNode });
164
165
  }
165
166
  }
166
- if (candidates.length === 0)
167
+ if (candidates.length === 0) {
168
+ // Second pass: unique non-exported top-level functions only
169
+ const internalCandidates = [];
170
+ for (const decl of sourceFile.getFunctions()) {
171
+ if (decl.isExported())
172
+ continue; // Already scanned in first pass
173
+ if (decl.getName() !== title)
174
+ continue;
175
+ const nameNode = decl.getNameNode();
176
+ if (!nameNode)
177
+ continue;
178
+ internalCandidates.push({ node: decl, getNameNode: () => nameNode });
179
+ }
180
+ // Fail closed: only return if exactly one unique match
181
+ if (internalCandidates.length === 1) {
182
+ return internalCandidates[0];
183
+ }
184
+ // Third pass: unique class methods
185
+ const methodCandidates = [];
186
+ for (const cls of sourceFile.getClasses()) {
187
+ for (const method of cls.getMethods()) {
188
+ if (method.getName() !== title)
189
+ continue;
190
+ const nameNode = method.getNameNode();
191
+ if (!nameNode)
192
+ continue;
193
+ methodCandidates.push({ node: method, getNameNode: () => nameNode });
194
+ }
195
+ }
196
+ // Fail closed: only return if exactly one unique match
197
+ if (methodCandidates.length === 1) {
198
+ return methodCandidates[0];
199
+ }
167
200
  return null;
201
+ }
168
202
  candidates.sort((a, b) => a.getNameNode().getStart() - b.getNameNode().getStart());
169
203
  return candidates[0] ?? null;
170
204
  }
@@ -1,4 +1,8 @@
1
1
  import type { HunkRange, StagedFile } from "./git-staged.js";
2
+ type TraceabilityRelationship = {
3
+ type: string;
4
+ to: string;
5
+ };
2
6
  export interface ExtractedSymbol {
3
7
  id: string;
4
8
  name: string;
@@ -10,11 +14,14 @@ export interface ExtractedSymbol {
10
14
  };
11
15
  hunkRanges: HunkRange[];
12
16
  reqLinks: string[];
17
+ relationships?: TraceabilityRelationship[];
13
18
  }
14
19
  export interface ManifestLookupEntry {
15
20
  id: string;
16
- links?: string[];
21
+ relationships?: TraceabilityRelationship[];
17
22
  }
18
23
  export type ManifestLookup = Map<string, ManifestLookupEntry>;
24
+ export declare function createManifestLookupSentinelKey(manifestPath: string): string;
19
25
  export declare function extractSymbolsFromStagedFile(stagedFile: StagedFile, manifestLookup?: ManifestLookup): ExtractedSymbol[];
26
+ export {};
20
27
  //# sourceMappingURL=symbol-extract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AA4D9D,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CAuNnB"}
1
+ {"version":3,"file":"symbol-extract.d.ts","sourceRoot":"","sources":["../../src/traceability/symbol-extract.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7D,KAAK,wBAAwB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAC;AAM7D,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,SAAS,CAAC;IAC7D,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,wBAAwB,EAAE,CAAC;CAC5C;AAED,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAE9D,wBAAgB,+BAA+B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAG5E;AAyLD,wBAAgB,4BAA4B,CAE1C,UAAU,EAAE,UAAU,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,eAAe,EAAE,CA+JnB"}
@@ -1,6 +1,93 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Project, ScriptKind } from "ts-morph";
3
3
  import { extractFromManifest } from "../extractors/manifest.js";
4
+ const TRACEABILITY_RELATIONSHIP_TYPES = new Set(["implements", "covered_by"]);
5
+ const REQUIREMENT_ID_PATTERN = /^[A-Z][A-Z0-9\-_]*$/;
6
+ const LOCAL_MANIFEST_NAMES = ["symbols.yaml", "symbols.yml"];
7
+ const MANIFEST_SENTINEL_PREFIX = "__manifest__:";
8
+ export function createManifestLookupSentinelKey(manifestPath) {
9
+ // implements REQ-008
10
+ return `${MANIFEST_SENTINEL_PREFIX}${manifestPath}`;
11
+ }
12
+ function getCandidateManifestPaths(filePath) {
13
+ const dir = filePath.substring(0, filePath.lastIndexOf("/"));
14
+ if (!dir) {
15
+ return [];
16
+ }
17
+ return LOCAL_MANIFEST_NAMES.map((manifestName) => `${dir}/${manifestName}`);
18
+ }
19
+ function createHashFallbackId(filePath, name) {
20
+ const h = createHash("sha256");
21
+ h.update(`${filePath}:${name}`);
22
+ return h.digest("hex").slice(0, 16);
23
+ }
24
+ function filterTraceabilityRelationships(relationships) {
25
+ if (!relationships?.length) {
26
+ return [];
27
+ }
28
+ return relationships.filter((relationship) => TRACEABILITY_RELATIONSHIP_TYPES.has(relationship.type));
29
+ }
30
+ function getRequirementLinks(relationships) {
31
+ return filterTraceabilityRelationships(relationships)
32
+ .filter((relationship) => relationship.type === "implements" &&
33
+ REQUIREMENT_ID_PATTERN.test(relationship.to))
34
+ .map((relationship) => relationship.to);
35
+ }
36
+ function resolveSymbolTraceability(filePath, name, manifestLookup) {
37
+ if (manifestLookup) {
38
+ const lookupKey = `${filePath}:${name}`;
39
+ const entry = manifestLookup.get(lookupKey);
40
+ if (entry) {
41
+ return {
42
+ id: entry.id,
43
+ relationships: filterTraceabilityRelationships(entry.relationships),
44
+ };
45
+ }
46
+ }
47
+ const candidateManifestPaths = getCandidateManifestPaths(filePath);
48
+ if (manifestLookup &&
49
+ candidateManifestPaths.some((manifestPath) => manifestLookup.has(createManifestLookupSentinelKey(manifestPath)))) {
50
+ return { id: createHashFallbackId(filePath, name) };
51
+ }
52
+ for (const manifestPath of candidateManifestPaths) {
53
+ try {
54
+ const ents = extractFromManifest(manifestPath);
55
+ for (const e of ents) {
56
+ if (e.entity.title === name) {
57
+ return {
58
+ id: e.entity.id,
59
+ relationships: filterTraceabilityRelationships(e.relationships.map((relationship) => ({
60
+ type: relationship.type,
61
+ to: relationship.to,
62
+ }))),
63
+ };
64
+ }
65
+ }
66
+ }
67
+ catch {
68
+ // ignore - no local manifest or parse error
69
+ }
70
+ }
71
+ return { id: createHashFallbackId(filePath, name) };
72
+ }
73
+ function buildSymbolResult(stagedFile, name, kind, span, inlineReqLinks, manifestLookup) {
74
+ const { id, relationships } = resolveSymbolTraceability(stagedFile.path, name, manifestLookup);
75
+ const manifestReqLinks = getRequirementLinks(relationships);
76
+ const mergedReqLinks = inlineReqLinks.length > 0 ? inlineReqLinks : manifestReqLinks;
77
+ return {
78
+ id,
79
+ name,
80
+ kind,
81
+ location: {
82
+ file: stagedFile.path,
83
+ startLine: span.startLine,
84
+ endLine: span.endLine,
85
+ },
86
+ hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
87
+ reqLinks: mergedReqLinks,
88
+ relationships,
89
+ };
90
+ }
4
91
  // Simple in-memory cache keyed by blob sha with 30s TTL
5
92
  const sourceFileCache = new Map();
6
93
  const CACHE_TTL_MS = 30 * 1000;
@@ -94,21 +181,7 @@ stagedFile, manifestLookup) {
94
181
  .getJsDocs()
95
182
  .map((d) => d.getFullText())
96
183
  .join("\n")}`);
97
- const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
98
- // Merge manifest links with inline directive links (manifest links as fallback)
99
- const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
100
- results.push({
101
- id,
102
- name,
103
- kind: "function",
104
- location: {
105
- file: stagedFile.path,
106
- startLine: span.startLine,
107
- endLine: span.endLine,
108
- },
109
- hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
110
- reqLinks: mergedReqLinks,
111
- });
184
+ results.push(buildSymbolResult(stagedFile, name, "function", span, reqLinks, manifestLookup));
112
185
  }
113
186
  catch {
114
187
  // skip: individual declaration extraction may fail on malformed AST nodes
@@ -127,21 +200,7 @@ stagedFile, manifestLookup) {
127
200
  .getJsDocs()
128
201
  .map((d) => d.getFullText())
129
202
  .join("\n")}`);
130
- const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
131
- // Merge manifest links with inline directive links (manifest links as fallback)
132
- const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
133
- results.push({
134
- id,
135
- name,
136
- kind: "class",
137
- location: {
138
- file: stagedFile.path,
139
- startLine: span.startLine,
140
- endLine: span.endLine,
141
- },
142
- hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
143
- reqLinks: mergedReqLinks,
144
- });
203
+ results.push(buildSymbolResult(stagedFile, name, "class", span, reqLinks, manifestLookup));
145
204
  }
146
205
  catch {
147
206
  // skip: individual declaration extraction may fail on malformed AST nodes
@@ -157,21 +216,7 @@ stagedFile, manifestLookup) {
157
216
  const end = en.getEnd();
158
217
  const span = getSpan(start, end);
159
218
  const reqLinks = parseReqDirectives(en.getText());
160
- const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
161
- // Merge manifest links with inline directive links (manifest links as fallback)
162
- const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
163
- results.push({
164
- id,
165
- name,
166
- kind: "enum",
167
- location: {
168
- file: stagedFile.path,
169
- startLine: span.startLine,
170
- endLine: span.endLine,
171
- },
172
- hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
173
- reqLinks: mergedReqLinks,
174
- });
219
+ results.push(buildSymbolResult(stagedFile, name, "enum", span, reqLinks, manifestLookup));
175
220
  }
176
221
  catch {
177
222
  // skip: individual declaration extraction may fail on malformed AST nodes
@@ -188,21 +233,7 @@ stagedFile, manifestLookup) {
188
233
  const end = decl.getEnd();
189
234
  const span = getSpan(start, end);
190
235
  const reqLinks = parseReqDirectives(decl.getText());
191
- const { id, reqLinks: manifestLinks } = resolveSymbolId(stagedFile.path, name, manifestLookup);
192
- // Merge manifest links with inline directive links (manifest links as fallback)
193
- const mergedReqLinks = reqLinks.length > 0 ? reqLinks : (manifestLinks ?? []);
194
- results.push({
195
- id,
196
- name,
197
- kind: "variable",
198
- location: {
199
- file: stagedFile.path,
200
- startLine: span.startLine,
201
- endLine: span.endLine,
202
- },
203
- hunkRanges: intersectingHunks(span.startLine, span.endLine, stagedFile.hunkRanges),
204
- reqLinks: mergedReqLinks,
205
- });
236
+ results.push(buildSymbolResult(stagedFile, name, "variable", span, reqLinks, manifestLookup));
206
237
  }
207
238
  catch {
208
239
  // skip: individual declaration extraction may fail on malformed AST nodes
@@ -225,40 +256,3 @@ function intersectingHunks(startLine, endLine, hunks) {
225
256
  }
226
257
  return out;
227
258
  }
228
- function resolveSymbolId(filePath, name, manifestLookup) {
229
- // First, check the provided manifest lookup if available
230
- if (manifestLookup) {
231
- // Normalize the source file path for consistent lookup
232
- const normalizedSource = filePath;
233
- const lookupKey = `${normalizedSource}:${name}`;
234
- const entry = manifestLookup.get(lookupKey);
235
- if (entry) {
236
- return { id: entry.id, reqLinks: entry.links };
237
- }
238
- }
239
- // Fallback: attempt to read manifest entries from a local symbols.yaml (best-effort)
240
- try {
241
- // Try to find a symbols.yaml in the same directory as the file
242
- const dir = filePath.substring(0, filePath.lastIndexOf("/"));
243
- if (dir) {
244
- const manifestPath = `${dir}/symbols.yaml`;
245
- const ents = extractFromManifest(manifestPath);
246
- for (const e of ents) {
247
- if (e.entity.title === name) {
248
- // Extract requirement links from relationships
249
- const reqLinks = e.relationships
250
- .filter((r) => r.type === "implements" && r.to.match(/^[A-Z][A-Z0-9\-_]*$/))
251
- .map((r) => r.to);
252
- return { id: e.entity.id, reqLinks };
253
- }
254
- }
255
- }
256
- }
257
- catch {
258
- // ignore - no local manifest or parse error
259
- }
260
- // Final fallback: deterministic id: sha(file:path:name)
261
- const h = createHash("sha256");
262
- h.update(`${filePath}:${name}`);
263
- return { id: h.digest("hex").slice(0, 16) };
264
- }
@@ -1,3 +1,4 @@
1
+ import type { ExtractionResult } from "../extractors/markdown.js";
1
2
  import { PrologProcess } from "../prolog.js";
2
3
  import type { ExtractedSymbol } from "./symbol-extract";
3
4
  export interface TempKbContext {
@@ -8,6 +9,7 @@ export interface TempKbContext {
8
9
  }
9
10
  declare function consultOverlay(ctx: TempKbContext): Promise<void>;
10
11
  export { consultOverlay };
12
+ export declare function projectStagedEntities(prolog: PrologProcess, results: ExtractionResult[]): Promise<void>;
11
13
  export declare function createTempKb(baseKbPath: string): Promise<TempKbContext>;
12
14
  export declare function createOverlayFacts(symbols: ExtractedSymbol[]): string;
13
15
  export declare function cleanupTempKb(tempDir: string): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAmDD,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2C7E;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
1
+ {"version":3,"file":"temp-kb.d.ts","sourceRoot":"","sources":["../../src/traceability/temp-kb.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAGV,gBAAgB,EACjB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAE7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAuJD,iBAAe,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAgB/D;AAED,OAAO,EAAE,cAAc,EAAE,CAAC;AAG1B,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,gBAAgB,EAAE,GAC1B,OAAO,CAAC,IAAI,CAAC,CAiCf;AAED,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2C7E;AAGD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,MAAM,CAkBrE;AAED,wBAAsB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BlE"}
@@ -3,9 +3,28 @@ import { cp, mkdir, rm, writeFile } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { PrologProcess } from "../prolog.js";
6
+ import { toPrologAtom } from "../prolog/codec.js";
6
7
  const prologByTempDir = new Map();
7
8
  const cleanupByTempDir = new Map();
8
9
  const cleanedTempDirs = new Set();
10
+ const FACT_ATOM_FIELDS = new Set([
11
+ "fact_kind",
12
+ "operator",
13
+ "value_type",
14
+ "polarity",
15
+ ]);
16
+ const FACT_STRING_FIELDS = new Set([
17
+ "subject_key",
18
+ "property_key",
19
+ "value_string",
20
+ "unit",
21
+ "scope",
22
+ "valid_from",
23
+ "valid_to",
24
+ "canonical_key",
25
+ ]);
26
+ const FACT_NUMBER_FIELDS = new Set(["value_int", "value_number"]);
27
+ const FACT_BOOLEAN_FIELDS = new Set(["value_bool", "closed_world"]);
9
28
  function isTraceEnabled() {
10
29
  return Boolean(process.env.KIBI_TRACE || process.env.KIBI_DEBUG);
11
30
  }
@@ -18,6 +37,75 @@ function trace(message) {
18
37
  function escapePrologAtom(value) {
19
38
  return `'${value.replace(/'/g, "''")}'`;
20
39
  }
40
+ function toPrologString(value) {
41
+ const escaped = value
42
+ .replace(/\\/g, "\\\\")
43
+ .replace(/"/g, '\\"')
44
+ .replace(/\n/g, "\\n")
45
+ .replace(/\r/g, "\\r")
46
+ .replace(/\t/g, "\\t");
47
+ return `"${escaped}"`;
48
+ }
49
+ function serializeTypedFactFields(entity) {
50
+ const fields = [];
51
+ const entityRecord = entity;
52
+ for (const field of FACT_STRING_FIELDS) {
53
+ const value = entityRecord[field];
54
+ if (value !== undefined && value !== null) {
55
+ fields.push(`${field}=${toPrologString(String(value))}`);
56
+ }
57
+ }
58
+ for (const field of FACT_ATOM_FIELDS) {
59
+ const value = entityRecord[field];
60
+ if (value !== undefined && value !== null) {
61
+ fields.push(`${field}=${toPrologAtom(String(value))}`);
62
+ }
63
+ }
64
+ for (const field of FACT_NUMBER_FIELDS) {
65
+ const value = entityRecord[field];
66
+ if (value !== undefined && value !== null && typeof value === "number") {
67
+ if (field === "value_int" && !Number.isInteger(value)) {
68
+ continue;
69
+ }
70
+ fields.push(`${field}=${value}`);
71
+ }
72
+ }
73
+ for (const field of FACT_BOOLEAN_FIELDS) {
74
+ const value = entityRecord[field];
75
+ if (value !== undefined && value !== null && typeof value === "boolean") {
76
+ fields.push(`${field}=${value}`);
77
+ }
78
+ }
79
+ return fields;
80
+ }
81
+ function buildEntityAssertionGoal(entity) {
82
+ const props = [
83
+ `id=${toPrologAtom(entity.id)}`,
84
+ `title=${toPrologString(entity.title)}`,
85
+ `status=${toPrologAtom(entity.status)}`,
86
+ `created_at=${toPrologString(entity.created_at)}`,
87
+ `updated_at=${toPrologString(entity.updated_at)}`,
88
+ `source=${toPrologString(entity.source)}`,
89
+ ];
90
+ if (entity.tags && entity.tags.length > 0) {
91
+ props.push(`tags=[${entity.tags.map(toPrologAtom).join(",")}]`);
92
+ }
93
+ if (entity.owner)
94
+ props.push(`owner=${toPrologAtom(entity.owner)}`);
95
+ if (entity.priority)
96
+ props.push(`priority=${toPrologAtom(entity.priority)}`);
97
+ if (entity.severity)
98
+ props.push(`severity=${toPrologAtom(entity.severity)}`);
99
+ if (entity.text_ref)
100
+ props.push(`text_ref=${toPrologString(entity.text_ref)}`);
101
+ if (entity.type === "fact") {
102
+ props.push(...serializeTypedFactFields(entity));
103
+ }
104
+ return `kb_assert_entity(${entity.type}, [${props.join(", ")}])`;
105
+ }
106
+ function buildRelationshipAssertionGoal(relationship) {
107
+ return `kb_assert_relationship(${toPrologAtom(relationship.type)}, ${toPrologAtom(relationship.from)}, ${toPrologAtom(relationship.to)}, [])`;
108
+ }
21
109
  function createCleanupHandler(tempDir) {
22
110
  let inProgress = false;
23
111
  return () => {
@@ -49,12 +137,34 @@ async function consultOverlay(ctx) {
49
137
  }
50
138
  const consultResult = await prolog.query([
51
139
  `consult(${escapePrologAtom(ctx.overlayPath)})`,
140
+ "kb_save",
52
141
  ]);
53
142
  if (!consultResult.success) {
54
143
  throw new Error(`Failed to consult overlay facts ${ctx.overlayPath}: ${consultResult.error || "unknown error"}`);
55
144
  }
56
145
  }
57
146
  export { consultOverlay };
147
+ // implements REQ-014
148
+ export async function projectStagedEntities(prolog, results) {
149
+ for (const { entity } of results) {
150
+ const retractResult = await prolog.query(`kb_retract_entity(${toPrologAtom(entity.id)})`);
151
+ if (!retractResult.success) {
152
+ throw new Error(`Failed to retract staged entity ${entity.id}: ${retractResult.error || "unknown error"}`);
153
+ }
154
+ const assertEntityResult = await prolog.query(buildEntityAssertionGoal(entity));
155
+ if (!assertEntityResult.success) {
156
+ throw new Error(`Failed to assert staged entity ${entity.id}: ${assertEntityResult.error || "unknown error"}`);
157
+ }
158
+ }
159
+ for (const { relationships } of results) {
160
+ for (const relationship of relationships) {
161
+ const assertRelationshipResult = await prolog.query(buildRelationshipAssertionGoal(relationship));
162
+ if (!assertRelationshipResult.success) {
163
+ throw new Error(`Failed to assert staged relationship ${relationship.type} ${relationship.from} -> ${relationship.to}: ${assertRelationshipResult.error || "unknown error"}`);
164
+ }
165
+ }
166
+ }
167
+ }
58
168
  export async function createTempKb(baseKbPath) {
59
169
  if (!existsSync(baseKbPath)) {
60
170
  throw new Error(`Base KB path does not exist: ${baseKbPath}`);
@@ -12,6 +12,17 @@ export interface Violation {
12
12
  currentLinks: number;
13
13
  requiredLinks: number;
14
14
  }
15
+ declare function unquoteAtom(value: string): string;
16
+ declare function splitTopLevelComma(s: string): string[];
17
+ declare function splitTopLevelLists(value: string): string[];
18
+ declare function parsePrologListOfLists(value: string): string[][];
15
19
  export declare function validateStagedSymbols(options: ValidationOptions): Promise<Violation[]>;
16
20
  export declare function formatViolations(violations: Violation[]): string;
21
+ export declare const __test__: {
22
+ unquoteAtom: typeof unquoteAtom;
23
+ splitTopLevelComma: typeof splitTopLevelComma;
24
+ splitTopLevelLists: typeof splitTopLevelLists;
25
+ parsePrologListOfLists: typeof parsePrologListOfLists;
26
+ };
27
+ export {};
17
28
  //# sourceMappingURL=validate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAiHD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqCtB;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAiBhE"}
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/traceability/validate.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,iBAAS,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAO1C;AAED,iBAAS,kBAAkB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CA0B/C;AAED,iBAAS,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CA8CnD;AAED,iBAAS,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CAuBzD;AAGD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,SAAS,EAAE,CAAC,CAqCtB;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,MAAM,CAiBhE;AAGD,eAAO,MAAM,QAAQ;;;;;CAKpB,CAAC"}
@@ -149,3 +149,10 @@ export function formatViolations(violations) {
149
149
  }
150
150
  return lines.join("\n");
151
151
  }
152
+ // Test helpers for edge-case coverage
153
+ export const __test__ = {
154
+ unquoteAtom,
155
+ splitTopLevelComma,
156
+ splitTopLevelLists,
157
+ parsePrologListOfLists,
158
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Kibi CLI for knowledge base management",
6
6
  "engines": {
@@ -76,7 +76,7 @@
76
76
  "fast-glob": "^3.2.12",
77
77
  "gray-matter": "^4.0.3",
78
78
  "js-yaml": "^4.1.0",
79
- "kibi-core": "^0.3.0",
79
+ "kibi-core": "^0.4.0",
80
80
  "ts-morph": "^23.0.0"
81
81
  },
82
82
  "devDependencies": {