kibi-cli 0.4.3 → 0.5.1

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":"AAmDA,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;AAmGD,wBAAsB,YAAY,CAChC,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgR/B"}
@@ -15,21 +15,96 @@
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
+ import { existsSync } from "node:fs";
18
19
  import * as path from "node:path";
19
- import { extractFromManifest } from "../extractors/manifest.js";
20
+ import { extractFromManifest, extractFromManifestString } from "../extractors/manifest.js";
21
+ import { extractFromMarkdownString, } from "../extractors/markdown.js";
20
22
  import { PrologProcess } from "../prolog.js";
21
23
  import { escapeAtom, parseTriples, parseViolationRows, } from "../prolog/codec.js";
22
24
  import { getStagedFiles } from "../traceability/git-staged.js";
23
25
  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";
26
+ import { createManifestLookupSentinelKey, extractSymbolsFromStagedFile, } from "../traceability/symbol-extract.js";
27
+ import { cleanupTempKb, consultOverlay, createOverlayFacts, createTempKb, projectStagedEntities, } from "../traceability/temp-kb.js";
26
28
  import { formatViolations as formatStagedViolations, validateStagedSymbols, } from "../traceability/validate.js";
27
29
  import { loadConfig } from "../utils/config.js";
28
30
  import { safeCleanupProlog } from "../utils/prolog-cleanup.js";
29
31
  import { RULES, getEffectiveRules, } from "../utils/rule-registry.js";
30
32
  import { runAggregatedChecks } from "./aggregated-checks.js";
31
33
  import { getCurrentBranch } from "./init-helpers.js";
32
- import { discoverSourceFiles } from "./sync/discovery.js";
34
+ function buildManifestLookup(stagedFiles) {
35
+ const manifestLookup = new Map();
36
+ const manifestResults = [];
37
+ // Pre-populate lookup from working-tree manifests so that code-only changes
38
+ // (where symbols.yaml is not staged) still resolve to the correct symbol IDs
39
+ // and relationships already defined on disk.
40
+ const config = loadConfig(process.cwd());
41
+ const symbolsRelPath = config.paths.symbols;
42
+ if (symbolsRelPath) {
43
+ const absSymbolsPath = path.resolve(process.cwd(), symbolsRelPath);
44
+ if (existsSync(absSymbolsPath)) {
45
+ try {
46
+ const entries = extractFromManifest(absSymbolsPath);
47
+ for (const entry of entries) {
48
+ const sourceFile = entry.sourceFile || entry.entity.source || absSymbolsPath;
49
+ const key = `${sourceFile}:${entry.entity.title}`;
50
+ manifestLookup.set(key, {
51
+ id: entry.entity.id,
52
+ relationships: entry.relationships
53
+ .filter((relationship) => relationship.type === "implements" ||
54
+ relationship.type === "covered_by")
55
+ .map((relationship) => ({
56
+ type: relationship.type,
57
+ to: relationship.to,
58
+ })),
59
+ });
60
+ }
61
+ }
62
+ catch (e) {
63
+ // Ignore working-tree manifest parsing errors; staged-only fallback still applies
64
+ if (process.env.KIBI_TRACE || process.env.KIBI_DEBUG) {
65
+ const msg = e instanceof Error ? e.message : String(e);
66
+ console.debug(`[kibi] skipping working-tree manifest ${absSymbolsPath}: ${msg}`);
67
+ }
68
+ }
69
+ }
70
+ }
71
+ const stagedManifestFiles = stagedFiles.filter((file) => file.content !== undefined &&
72
+ (file.path.endsWith("/symbols.yaml") ||
73
+ file.path.endsWith("/symbols.yml") ||
74
+ file.path === "symbols.yaml" ||
75
+ file.path === "symbols.yml"));
76
+ for (const manifestFile of stagedManifestFiles) {
77
+ manifestLookup.set(createManifestLookupSentinelKey(manifestFile.path), {
78
+ id: manifestFile.path,
79
+ relationships: [],
80
+ });
81
+ try {
82
+ const entries = extractFromManifestString(manifestFile.content ?? "", manifestFile.path);
83
+ for (const entry of entries) {
84
+ manifestResults.push({
85
+ entity: entry.entity,
86
+ relationships: entry.relationships,
87
+ });
88
+ const sourceFile = entry.sourceFile || entry.entity.source || manifestFile.path;
89
+ const key = `${sourceFile}:${entry.entity.title}`;
90
+ manifestLookup.set(key, {
91
+ id: entry.entity.id,
92
+ relationships: entry.relationships
93
+ .filter((relationship) => relationship.type === "implements" ||
94
+ relationship.type === "covered_by")
95
+ .map((relationship) => ({
96
+ type: relationship.type,
97
+ to: relationship.to,
98
+ })),
99
+ });
100
+ }
101
+ }
102
+ catch {
103
+ // Ignore manifest parsing errors
104
+ }
105
+ }
106
+ return { manifestLookup, manifestResults };
107
+ }
33
108
  // implements REQ-006
34
109
  export async function checkCommand(options) {
35
110
  let prolog = null;
@@ -59,33 +134,12 @@ export async function checkCommand(options) {
59
134
  const minLinks = options.minLinks ? Number(options.minLinks) : 1;
60
135
  let tempCtx = null;
61
136
  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
137
  const stagedFiles = getStagedFiles();
85
138
  if (!stagedFiles || stagedFiles.length === 0) {
86
139
  console.log("No staged files found.");
87
140
  return { exitCode: 0 };
88
141
  }
142
+ const { manifestLookup, manifestResults } = buildManifestLookup(stagedFiles);
89
143
  const codeFiles = stagedFiles.filter((f) => !f.path.endsWith(".md"));
90
144
  const markdownFiles = stagedFiles.filter((f) => f.path.endsWith(".md"));
91
145
  const markdownErrors = [];
@@ -118,8 +172,13 @@ export async function checkCommand(options) {
118
172
  console.error(`Error extracting symbols from staged file ${f.path}: ${e instanceof Error ? e.message : String(e)}`);
119
173
  }
120
174
  }
121
- if (allSymbols.length === 0 && markdownFiles.length === 0) {
122
- console.log("No exported symbols or markdown entities found in staged files.");
175
+ const markdownResults = markdownFiles.map((file) => extractFromMarkdownString(file.content ?? "", file.path));
176
+ const stagedEntityResults = [
177
+ ...manifestResults,
178
+ ...markdownResults,
179
+ ];
180
+ if (allSymbols.length === 0 && stagedEntityResults.length === 0) {
181
+ console.log("No exported symbols or staged entities found in staged files.");
123
182
  return { exitCode: 0 };
124
183
  }
125
184
  if (allSymbols.length === 0) {
@@ -128,9 +187,13 @@ export async function checkCommand(options) {
128
187
  }
129
188
  // Create temp KB
130
189
  tempCtx = await createTempKb(resolvedKbPath);
190
+ if (stagedEntityResults.length > 0) {
191
+ await projectStagedEntities(tempCtx.prolog, stagedEntityResults);
192
+ }
131
193
  const overlayFacts = createOverlayFacts(allSymbols);
132
194
  const fs = await import("node:fs/promises");
133
195
  await fs.writeFile(tempCtx.overlayPath, overlayFacts, "utf8");
196
+ await fs.cp(tempCtx.overlayPath, path.join(tempCtx.kbPath, "changed_symbols.pl"));
134
197
  await consultOverlay(tempCtx);
135
198
  const violationsRaw = await validateStagedSymbols({
136
199
  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":"prolog.d.ts","sourceRoot":"","sources":["../src/prolog.ts"],"names":[],"mappings":"AA4BA,wBAAgB,eAAe,IAAI,MAAM,CAyCxC;AACD,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,oBAAoB,CAAoC;IAChE,OAAO,CAAC,cAAc,CACyC;IAC/D,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,aAAa,CAA6B;gBAEtC,OAAO,GAAE,aAAkB;IAKjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAoCd,YAAY;IAyCpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAuJ1D,eAAe,IAAI,IAAI;IAIvB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,kBAAkB;YAIZ,YAAY;IAkC1B,OAAO,CAAC,WAAW;IAsGnB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,cAAc;IA8BtB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAIV,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBjC"}
1
+ {"version":3,"file":"prolog.d.ts","sourceRoot":"","sources":["../src/prolog.ts"],"names":[],"mappings":"AA4BA,wBAAgB,eAAe,IAAI,MAAM,CAyCxC;AACD,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,KAAK,CAAuC;IACpD,OAAO,CAAC,oBAAoB,CAAoC;IAChE,OAAO,CAAC,cAAc,CACyC;IAC/D,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,aAAa,CAA6B;gBAEtC,OAAO,GAAE,aAAkB;IAKjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAsCd,YAAY;IA0CpB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC;IAuJ1D,eAAe,IAAI,IAAI;IAIvB,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,kBAAkB;YAIZ,YAAY;IAkC1B,OAAO,CAAC,WAAW;IAsGnB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,eAAe;IAevB,OAAO,CAAC,cAAc;IA8BtB,SAAS,IAAI,OAAO;IAIpB,MAAM,IAAI,MAAM;IAIV,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC;CAyBjC"}
package/dist/prolog.js CHANGED
@@ -88,6 +88,7 @@ export class PrologProcess {
88
88
  this.process.stderr.on("data", (chunk) => {
89
89
  this.errorBuffer += chunk.toString();
90
90
  });
91
+ this.process.stdin.write("true.\n");
91
92
  if (!this.onProcessExit) {
92
93
  this.onProcessExit = () => {
93
94
  void this.terminate();
@@ -109,13 +110,14 @@ export class PrologProcess {
109
110
  if (this.errorBuffer.includes("ERROR")) {
110
111
  throw new Error(`Failed to load kb module: ${this.translateError(this.errorBuffer)}`);
111
112
  }
112
- // If stdout or stderr shows any output, assume ready.
113
- if (this.outputBuffer.length > 0 || this.errorBuffer.length > 0) {
114
- break;
113
+ if (this.outputBuffer.includes("true.")) {
114
+ this.outputBuffer = "";
115
+ this.errorBuffer = "";
116
+ return;
115
117
  }
116
118
  // brief pause
117
119
  // eslint-disable-next-line no-await-in-loop
118
- await new Promise((resolve) => setTimeout(resolve, 50));
120
+ await new Promise((resolve) => setTimeout(resolve, 10));
119
121
  }
120
122
  // Final sanity check
121
123
  if (this.errorBuffer.includes("ERROR")) {
@@ -46,6 +46,9 @@ declare const entitySchema: {
46
46
  text_ref: {
47
47
  type: string;
48
48
  };
49
+ sourceFile: {
50
+ type: string;
51
+ };
49
52
  type: {
50
53
  type: string;
51
54
  enum: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../../src/public/schemas/entity.ts"],"names":[],"mappings":"AAwBA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoHjB,CAAC;AAEF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"entity.d.ts","sourceRoot":"","sources":["../../../src/public/schemas/entity.ts"],"names":[],"mappings":"AAwBA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqHjB,CAAC;AAEF,eAAe,YAAY,CAAC"}
@@ -55,6 +55,7 @@ const entitySchema = {
55
55
  severity: { type: "string" },
56
56
  links: { type: "array", items: { type: "string" } },
57
57
  text_ref: { type: "string" },
58
+ sourceFile: { type: "string" },
58
59
  type: {
59
60
  type: "string",
60
61
  enum: [
@@ -38,6 +38,7 @@
38
38
  "severity": { "type": "string" },
39
39
  "links": { "type": "array", "items": { "type": "string" } },
40
40
  "text_ref": { "type": "string" },
41
+ "sourceFile": { "type": "string" },
41
42
  "type": {
42
43
  "type": "string",
43
44
  "enum": [
@@ -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;AA4ID,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, toPrologString } 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,66 @@ function trace(message) {
18
37
  function escapePrologAtom(value) {
19
38
  return `'${value.replace(/'/g, "''")}'`;
20
39
  }
40
+ function serializeTypedFactFields(entity) {
41
+ const fields = [];
42
+ const entityRecord = entity;
43
+ for (const field of FACT_STRING_FIELDS) {
44
+ const value = entityRecord[field];
45
+ if (value !== undefined && value !== null) {
46
+ fields.push(`${field}=${toPrologString(String(value))}`);
47
+ }
48
+ }
49
+ for (const field of FACT_ATOM_FIELDS) {
50
+ const value = entityRecord[field];
51
+ if (value !== undefined && value !== null) {
52
+ fields.push(`${field}=${toPrologAtom(String(value))}`);
53
+ }
54
+ }
55
+ for (const field of FACT_NUMBER_FIELDS) {
56
+ const value = entityRecord[field];
57
+ if (value !== undefined && value !== null && typeof value === "number") {
58
+ if (field === "value_int" && !Number.isInteger(value)) {
59
+ continue;
60
+ }
61
+ fields.push(`${field}=${value}`);
62
+ }
63
+ }
64
+ for (const field of FACT_BOOLEAN_FIELDS) {
65
+ const value = entityRecord[field];
66
+ if (value !== undefined && value !== null && typeof value === "boolean") {
67
+ fields.push(`${field}=${value}`);
68
+ }
69
+ }
70
+ return fields;
71
+ }
72
+ function buildEntityAssertionGoal(entity) {
73
+ const props = [
74
+ `id=${toPrologAtom(entity.id)}`,
75
+ `title=${toPrologString(entity.title)}`,
76
+ `status=${toPrologAtom(entity.status)}`,
77
+ `created_at=${toPrologString(entity.created_at)}`,
78
+ `updated_at=${toPrologString(entity.updated_at)}`,
79
+ `source=${toPrologString(entity.source)}`,
80
+ ];
81
+ if (entity.tags && entity.tags.length > 0) {
82
+ props.push(`tags=[${entity.tags.map(toPrologAtom).join(",")}]`);
83
+ }
84
+ if (entity.owner)
85
+ props.push(`owner=${toPrologAtom(entity.owner)}`);
86
+ if (entity.priority)
87
+ props.push(`priority=${toPrologAtom(entity.priority)}`);
88
+ if (entity.severity)
89
+ props.push(`severity=${toPrologAtom(entity.severity)}`);
90
+ if (entity.text_ref)
91
+ props.push(`text_ref=${toPrologString(entity.text_ref)}`);
92
+ if (entity.type === "fact") {
93
+ props.push(...serializeTypedFactFields(entity));
94
+ }
95
+ return `kb_assert_entity(${entity.type}, [${props.join(", ")}])`;
96
+ }
97
+ function buildRelationshipAssertionGoal(relationship) {
98
+ return `kb_assert_relationship(${toPrologAtom(relationship.type)}, ${toPrologAtom(relationship.from)}, ${toPrologAtom(relationship.to)}, [])`;
99
+ }
21
100
  function createCleanupHandler(tempDir) {
22
101
  let inProgress = false;
23
102
  return () => {
@@ -49,12 +128,34 @@ async function consultOverlay(ctx) {
49
128
  }
50
129
  const consultResult = await prolog.query([
51
130
  `consult(${escapePrologAtom(ctx.overlayPath)})`,
131
+ "kb_save",
52
132
  ]);
53
133
  if (!consultResult.success) {
54
134
  throw new Error(`Failed to consult overlay facts ${ctx.overlayPath}: ${consultResult.error || "unknown error"}`);
55
135
  }
56
136
  }
57
137
  export { consultOverlay };
138
+ // implements REQ-014
139
+ export async function projectStagedEntities(prolog, results) {
140
+ for (const { entity } of results) {
141
+ const retractResult = await prolog.query(`kb_retract_entity(${toPrologAtom(entity.id)})`);
142
+ if (!retractResult.success) {
143
+ throw new Error(`Failed to retract staged entity ${entity.id}: ${retractResult.error || "unknown error"}`);
144
+ }
145
+ const assertEntityResult = await prolog.query(buildEntityAssertionGoal(entity));
146
+ if (!assertEntityResult.success) {
147
+ throw new Error(`Failed to assert staged entity ${entity.id}: ${assertEntityResult.error || "unknown error"}`);
148
+ }
149
+ }
150
+ for (const { relationships } of results) {
151
+ for (const relationship of relationships) {
152
+ const assertRelationshipResult = await prolog.query(buildRelationshipAssertionGoal(relationship));
153
+ if (!assertRelationshipResult.success) {
154
+ throw new Error(`Failed to assert staged relationship ${relationship.type} ${relationship.from} -> ${relationship.to}: ${assertRelationshipResult.error || "unknown error"}`);
155
+ }
156
+ }
157
+ }
158
+ }
58
159
  export async function createTempKb(baseKbPath) {
59
160
  if (!existsSync(baseKbPath)) {
60
161
  throw new Error(`Base KB path does not exist: ${baseKbPath}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kibi-cli",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
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.1",
80
80
  "ts-morph": "^23.0.0"
81
81
  },
82
82
  "devDependencies": {
@@ -62,6 +62,7 @@ const entitySchema = {
62
62
  severity: { type: "string" },
63
63
  links: { type: "array", items: { type: "string" } },
64
64
  text_ref: { type: "string" },
65
+ sourceFile: { type: "string" },
65
66
  type: {
66
67
  type: "string",
67
68
  enum: [
@@ -38,6 +38,7 @@
38
38
  "severity": { "type": "string" },
39
39
  "links": { "type": "array", "items": { "type": "string" } },
40
40
  "text_ref": { "type": "string" },
41
+ "sourceFile": { "type": "string" },
41
42
  "type": {
42
43
  "type": "string",
43
44
  "enum": [