kcode-pi 0.1.0 → 0.1.2

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.
@@ -0,0 +1,117 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import type { ActiveRun, KdPhase } from "./types.ts";
3
+ import { PHASE_ARTIFACTS, isKdPhase, nextPhase } from "./types.ts";
4
+ import { activeRunPath, kdDir, runsDir } from "./paths.ts";
5
+ import { defaultArtifactContent, ensureArtifact, ensureRunDirectories, writeArtifact } from "./artifacts.ts";
6
+ import { canEnterPhase, inspectGate } from "./gates.ts";
7
+ import { profileForProduct, resolveProductProfile } from "../product/profile.ts";
8
+
9
+ export function readActiveRun(cwd: string): ActiveRun | undefined {
10
+ const path = activeRunPath(cwd);
11
+ if (!existsSync(path)) return undefined;
12
+
13
+ try {
14
+ const parsed = JSON.parse(readFileSync(path, "utf8")) as ActiveRun;
15
+ if (!parsed.id || !isKdPhase(parsed.phase)) return undefined;
16
+ parsed.artifacts ??= {};
17
+ const legacyEdition = (parsed as ActiveRun & { edition?: string }).edition;
18
+ parsed.profile = parsed.profile ?? profileForProduct(parsed.product ?? resolveProductProfile(legacyEdition).product);
19
+ parsed.product = parsed.profile.product;
20
+ parsed.gate ??= { passed: false, checkedAt: new Date().toISOString() };
21
+ return parsed;
22
+ } catch {
23
+ return undefined;
24
+ }
25
+ }
26
+
27
+ export function writeActiveRun(cwd: string, run: ActiveRun): void {
28
+ mkdirSync(kdDir(cwd), { recursive: true });
29
+ mkdirSync(runsDir(cwd), { recursive: true });
30
+ writeFileSync(activeRunPath(cwd), `${JSON.stringify(run, null, 2)}\n`, "utf8");
31
+ }
32
+
33
+ export function createActiveRun(cwd: string, goal: string, productInput?: string, version?: string): ActiveRun {
34
+ const profile = resolveProductProfile(productInput ?? goal);
35
+ const run: ActiveRun = {
36
+ id: createRunId(goal),
37
+ phase: "discuss",
38
+ cwd,
39
+ product: profile.product,
40
+ version,
41
+ profile,
42
+ risk: "unknown",
43
+ artifacts: {},
44
+ gate: {
45
+ passed: false,
46
+ reason: "CONTEXT.md and product profile are required before moving to spec",
47
+ checkedAt: new Date().toISOString(),
48
+ },
49
+ };
50
+
51
+ ensureRunDirectories(cwd, run);
52
+ writeArtifact(cwd, run, "discuss", defaultArtifactContent("discuss", goal, profile));
53
+ run.gate = inspectGate(cwd, run);
54
+ writeActiveRun(cwd, run);
55
+ return run;
56
+ }
57
+
58
+ export function updateProductProfile(cwd: string, run: ActiveRun, productInput: string, version?: string): ActiveRun {
59
+ const profile = resolveProductProfile(productInput);
60
+ run.product = profile.product;
61
+ run.profile = profile;
62
+ if (version !== undefined) run.version = version;
63
+ run.gate = inspectGate(cwd, run);
64
+ writeActiveRun(cwd, run);
65
+ return run;
66
+ }
67
+
68
+ export function ensurePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase): string {
69
+ const path = ensureArtifact(cwd, run, phase, defaultArtifactContent(phase));
70
+ run.artifacts[phase] = PHASE_ARTIFACTS[phase];
71
+ run.gate = inspectGate(cwd, run);
72
+ writeActiveRun(cwd, run);
73
+ return path;
74
+ }
75
+
76
+ export function updatePhaseArtifact(cwd: string, run: ActiveRun, phase: KdPhase, content: string): string {
77
+ const path = writeArtifact(cwd, run, phase, content);
78
+ run.gate = inspectGate(cwd, run);
79
+ writeActiveRun(cwd, run);
80
+ return path;
81
+ }
82
+
83
+ export function advanceRun(cwd: string, run: ActiveRun, requestedPhase?: KdPhase): { run: ActiveRun; message: string } {
84
+ const target = requestedPhase ?? nextPhase(run.phase);
85
+ if (!target) {
86
+ return { run, message: `Run is already at final phase: ${run.phase}` };
87
+ }
88
+
89
+ const gate = canEnterPhase(cwd, run, target);
90
+ if (!gate.passed) {
91
+ run.gate = gate;
92
+ writeActiveRun(cwd, run);
93
+ return { run, message: gate.reason ?? `Cannot enter ${target}` };
94
+ }
95
+
96
+ run.phase = target;
97
+ ensurePhaseArtifact(cwd, run, target);
98
+ run.gate = inspectGate(cwd, run);
99
+ writeActiveRun(cwd, run);
100
+ return { run, message: `Advanced Kingdee run to phase: ${target}` };
101
+ }
102
+
103
+ export function refreshGate(cwd: string, run: ActiveRun): ActiveRun {
104
+ run.gate = inspectGate(cwd, run);
105
+ writeActiveRun(cwd, run);
106
+ return run;
107
+ }
108
+
109
+ function createRunId(goal: string): string {
110
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\..+/, "").replace("T", "-");
111
+ const slug = goal
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-")
114
+ .replace(/^-+|-+$/g, "")
115
+ .slice(0, 40);
116
+ return slug ? `${stamp}-${slug}` : stamp;
117
+ }
@@ -0,0 +1,42 @@
1
+ import type { KdProduct, ProductProfile } from "../product/profile.ts";
2
+
3
+ export type KdPhase = "discuss" | "spec" | "plan" | "execute" | "verify" | "ship";
4
+ export type KdRisk = "unknown" | "low" | "medium" | "high";
5
+
6
+ export interface GateResult {
7
+ passed: boolean;
8
+ reason?: string;
9
+ checkedAt: string;
10
+ }
11
+
12
+ export interface ActiveRun {
13
+ id: string;
14
+ phase: KdPhase;
15
+ cwd: string;
16
+ product?: KdProduct;
17
+ version?: string;
18
+ profile?: ProductProfile;
19
+ risk?: KdRisk;
20
+ artifacts: Record<string, string>;
21
+ gate: GateResult;
22
+ }
23
+
24
+ export const PHASE_ORDER: KdPhase[] = ["discuss", "spec", "plan", "execute", "verify", "ship"];
25
+
26
+ export const PHASE_ARTIFACTS: Record<KdPhase, string> = {
27
+ discuss: "CONTEXT.md",
28
+ spec: "SPEC.md",
29
+ plan: "PLAN.md",
30
+ execute: "EXECUTION.md",
31
+ verify: "VERIFY.md",
32
+ ship: "SHIP.md",
33
+ };
34
+
35
+ export function isKdPhase(value: string): value is KdPhase {
36
+ return (PHASE_ORDER as readonly string[]).includes(value);
37
+ }
38
+
39
+ export function nextPhase(phase: KdPhase): KdPhase | undefined {
40
+ const index = PHASE_ORDER.indexOf(phase);
41
+ return index >= 0 ? PHASE_ORDER[index + 1] : undefined;
42
+ }
@@ -0,0 +1,48 @@
1
+ import { relative } from "node:path";
2
+ import type { SearchResult, TableSchema } from "./types.ts";
3
+
4
+ export function formatSearchResults(query: string, results: SearchResult[], basePath: string): string {
5
+ if (results.length === 0) {
6
+ return `No Kingdee knowledge results found for "${query}".`;
7
+ }
8
+
9
+ const lines = [`Kingdee knowledge results for "${query}":`, ""];
10
+ for (let i = 0; i < results.length; i++) {
11
+ const result = results[i];
12
+ lines.push(`[${i + 1}] ${result.section.heading}`);
13
+ lines.push(`Source: ${relative(basePath, result.file.path)}:${result.section.lineStart + 1}`);
14
+ lines.push(`Score: ${result.score.toFixed(2)}`);
15
+ lines.push(result.highlights[0] || result.section.content.trim().split("\n").slice(0, 5).join("\n"));
16
+ lines.push("");
17
+ }
18
+ return lines.join("\n").trim();
19
+ }
20
+
21
+ export function formatTableSchema(tableName: string, schema: TableSchema | undefined): string {
22
+ if (!schema) {
23
+ return `No table schema found for "${tableName}".`;
24
+ }
25
+
26
+ const lines = [
27
+ `Table: ${schema.name} (${tableName.toUpperCase()})`,
28
+ `Module: ${schema.module || "unknown"}`,
29
+ `Description: ${schema.description || "none"}`,
30
+ "",
31
+ "Fields:",
32
+ ];
33
+
34
+ for (const field of schema.fields) {
35
+ const nullable = field.nullable ? "nullable" : "required";
36
+ lines.push(`- ${field.name} ${field.type} ${nullable} - ${field.description}`);
37
+ }
38
+
39
+ if (schema.relatedTables.length > 0) {
40
+ lines.push("", "Related tables:");
41
+ for (const related of schema.relatedTables) {
42
+ lines.push(`- ${related.table}: ${related.relation} - ${related.description}`);
43
+ }
44
+ }
45
+
46
+ return lines.join("\n");
47
+ }
48
+
@@ -0,0 +1,147 @@
1
+ import { basename, extname, join } from "node:path";
2
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
3
+ import type { KnowledgeBase, KnowledgeFile, KnowledgeScope, KnowledgeSection, TableSchema } from "./types.ts";
4
+
5
+ interface CacheEntry {
6
+ mtime: number;
7
+ knowledge: KnowledgeBase;
8
+ }
9
+
10
+ const cache = new Map<string, CacheEntry>();
11
+
12
+ export function loadKnowledge(scope: KnowledgeScope, basePath: string): KnowledgeBase {
13
+ const cacheKey = `${basePath}:${scope}`;
14
+ const cached = cache.get(cacheKey);
15
+ if (cached && !isCacheStale(basePath, cached.mtime)) {
16
+ return cached.knowledge;
17
+ }
18
+
19
+ const commonPath = join(basePath, "common");
20
+ const editionPath = join(basePath, scope);
21
+ const knowledge: KnowledgeBase = {
22
+ common: loadDirectory(commonPath),
23
+ edition: loadDirectory(editionPath),
24
+ tables: loadTables(editionPath),
25
+ };
26
+
27
+ cache.set(cacheKey, { mtime: Date.now(), knowledge });
28
+ return knowledge;
29
+ }
30
+
31
+ function loadDirectory(dirPath: string): KnowledgeFile[] {
32
+ if (!existsSync(dirPath)) return [];
33
+
34
+ const files: KnowledgeFile[] = [];
35
+ for (const entry of readdirSync(dirPath)) {
36
+ const filePath = join(dirPath, entry);
37
+ const stat = statSync(filePath);
38
+ if (!stat.isFile()) continue;
39
+
40
+ const ext = extname(entry).toLowerCase();
41
+ if (ext !== ".md") continue;
42
+
43
+ const file = parseMarkdown(filePath, readFileSync(filePath, "utf8"), stat.mtime.toISOString());
44
+ files.push(file);
45
+ }
46
+ return files;
47
+ }
48
+
49
+ function parseMarkdown(filePath: string, content: string, lastModified: string): KnowledgeFile {
50
+ const lines = content.split("\n");
51
+ const sections: KnowledgeSection[] = [];
52
+ const tags: string[] = [];
53
+ let currentSection: Partial<KnowledgeSection> | undefined;
54
+ let title = "";
55
+
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i].replace(/\r$/, "");
58
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
59
+
60
+ if (headingMatch) {
61
+ if (currentSection) {
62
+ currentSection.lineEnd = i - 1;
63
+ sections.push(currentSection as KnowledgeSection);
64
+ }
65
+
66
+ const level = headingMatch[1].length;
67
+ const heading = headingMatch[2].trim();
68
+ if (level === 1 && !title) title = heading;
69
+ if (level <= 2) tags.push(heading);
70
+
71
+ currentSection = {
72
+ heading,
73
+ level,
74
+ content: "",
75
+ lineStart: i,
76
+ lineEnd: i,
77
+ };
78
+ } else if (currentSection) {
79
+ currentSection.content = `${currentSection.content ?? ""}${line}\n`;
80
+ }
81
+ }
82
+
83
+ if (currentSection) {
84
+ currentSection.lineEnd = lines.length - 1;
85
+ sections.push(currentSection as KnowledgeSection);
86
+ }
87
+
88
+ const fileName = basename(filePath, ".md");
89
+ tags.push(fileName);
90
+
91
+ return {
92
+ path: filePath,
93
+ title: title || fileName,
94
+ type: "markdown",
95
+ sections,
96
+ tags: [...new Set(tags)],
97
+ lastModified,
98
+ };
99
+ }
100
+
101
+ function loadTables(editionPath: string): Map<string, TableSchema> {
102
+ const tables = new Map<string, TableSchema>();
103
+ const tablesPath = join(editionPath, "tables.json");
104
+ if (!existsSync(tablesPath)) return tables;
105
+
106
+ const data = JSON.parse(readFileSync(tablesPath, "utf8")) as Record<string, unknown>;
107
+ for (const [tableName, tableData] of Object.entries(data)) {
108
+ const table = tableData as {
109
+ name?: string;
110
+ module?: string;
111
+ description?: string;
112
+ fields?: Array<{ name?: string; type?: string; nullable?: boolean; description?: string }>;
113
+ relatedTables?: Array<{ table?: string; relation?: string; description?: string }>;
114
+ };
115
+
116
+ tables.set(tableName.toUpperCase(), {
117
+ name: table.name || tableName,
118
+ module: table.module || "",
119
+ description: table.description || "",
120
+ fields: (table.fields || []).map((field) => ({
121
+ name: field.name || "",
122
+ type: field.type || "",
123
+ nullable: field.nullable ?? true,
124
+ description: field.description || "",
125
+ })),
126
+ relatedTables: (table.relatedTables || []).map((related) => ({
127
+ table: related.table || "",
128
+ relation: related.relation || "",
129
+ description: related.description || "",
130
+ })),
131
+ });
132
+ }
133
+
134
+ return tables;
135
+ }
136
+
137
+ function isCacheStale(knowledgePath: string, cachedMtime: number): boolean {
138
+ try {
139
+ return statSync(knowledgePath).mtimeMs > cachedMtime;
140
+ } catch {
141
+ return true;
142
+ }
143
+ }
144
+
145
+ export function clearKnowledgeCache(): void {
146
+ cache.clear();
147
+ }
@@ -0,0 +1,118 @@
1
+ import type { Edition, KnowledgeFile, KnowledgeScope, KnowledgeSection, SearchOptions, SearchResult, TableSchema } from "./types.ts";
2
+ import { loadKnowledge } from "./loader.ts";
3
+
4
+ export function searchKnowledge(query: string, options: SearchOptions = {}, basePath: string): SearchResult[] {
5
+ if (!query.trim()) return [];
6
+
7
+ const { tags, edition, scope, scopes, topK = 10, minScore = 0 } = options;
8
+ const targetScopes = normalizeScopes(scopes ?? [scope ?? edition ?? "flagship"]);
9
+ const results: SearchResult[] = [];
10
+
11
+ const seenFiles = new Set<string>();
12
+ for (const targetScope of targetScopes) {
13
+ const knowledge = loadKnowledge(targetScope, basePath);
14
+ for (const file of [...knowledge.common, ...knowledge.edition]) {
15
+ if (seenFiles.has(file.path)) continue;
16
+ seenFiles.add(file.path);
17
+ results.push(...searchFile(file, query, tags));
18
+ }
19
+ }
20
+
21
+ return results
22
+ .filter((result) => result.score >= minScore)
23
+ .sort((a, b) => b.score - a.score)
24
+ .slice(0, topK);
25
+ }
26
+
27
+ export function findTableSchema(tableName: string, edition: Edition, basePath: string): TableSchema | undefined {
28
+ const knowledge = loadKnowledge(edition, basePath);
29
+ return knowledge.tables.get(tableName.toUpperCase());
30
+ }
31
+
32
+ function normalizeScopes(scopes: KnowledgeScope[]): KnowledgeScope[] {
33
+ return [...new Set(scopes)];
34
+ }
35
+
36
+ function searchFile(file: KnowledgeFile, query: string, tags?: string[]): SearchResult[] {
37
+ if (tags?.length) {
38
+ const hasTag = tags.some((tag) => file.tags.some((fileTag) => fileTag.toLowerCase().includes(tag.toLowerCase())));
39
+ if (!hasTag) return [];
40
+ }
41
+
42
+ const queryTokens = tokenize(query);
43
+ const results: SearchResult[] = [];
44
+
45
+ for (const section of file.sections) {
46
+ const score = calculateScore(queryTokens, section, file);
47
+ if (score <= 0) continue;
48
+
49
+ results.push({
50
+ file,
51
+ section,
52
+ score,
53
+ highlights: highlightMatches(section.content, query),
54
+ });
55
+ }
56
+
57
+ return results;
58
+ }
59
+
60
+ export function tokenize(text: string): string[] {
61
+ return text
62
+ .toLowerCase()
63
+ .replace(/[^\w\u4e00-\u9fff]/g, " ")
64
+ .split(/\s+/)
65
+ .filter(Boolean);
66
+ }
67
+
68
+ function calculateScore(queryTokens: string[], section: KnowledgeSection, file: KnowledgeFile): number {
69
+ let score = 0;
70
+ const headingLower = section.heading.toLowerCase();
71
+ const contentLower = section.content.toLowerCase();
72
+ const titleLower = file.title.toLowerCase();
73
+ const fullQuery = queryTokens.join(" ");
74
+
75
+ if (fullQuery && titleLower.includes(fullQuery)) score += 5;
76
+ if (fullQuery && headingLower.includes(fullQuery)) score += 5;
77
+ if (fullQuery && contentLower.includes(fullQuery)) score += 5;
78
+
79
+ for (let i = 0; i < queryTokens.length; i++) {
80
+ const token = queryTokens[i];
81
+ const positionWeight = 1 + 1 / (i + 1);
82
+ if (titleLower.includes(token)) score += 3 * positionWeight;
83
+ if (headingLower.includes(token)) score += 2 * positionWeight;
84
+ if (contentLower.includes(token)) {
85
+ const matchCount = contentLower.split(token).length - 1;
86
+ score += (1 + Math.min(matchCount * 0.2, 2)) * positionWeight;
87
+ }
88
+ }
89
+
90
+ return score;
91
+ }
92
+
93
+ function highlightMatches(content: string, query: string): string[] {
94
+ const highlights: string[] = [];
95
+ const queryTokens = tokenize(query);
96
+ const lines = content.split("\n");
97
+ const matchedLines: number[] = [];
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const lineLower = lines[i].toLowerCase();
101
+ if (queryTokens.some((token) => lineLower.includes(token))) {
102
+ matchedLines.push(i);
103
+ }
104
+ }
105
+
106
+ const visited = new Set<number>();
107
+ for (const lineIdx of matchedLines) {
108
+ if (visited.has(lineIdx)) continue;
109
+ const start = Math.max(0, lineIdx - 2);
110
+ const end = Math.min(lines.length - 1, lineIdx + 2);
111
+ for (let i = start; i <= end; i++) visited.add(i);
112
+
113
+ const context = lines.slice(start, end + 1).join("\n").trim();
114
+ if (context) highlights.push(context);
115
+ }
116
+
117
+ return highlights.slice(0, 3);
118
+ }
@@ -0,0 +1,64 @@
1
+ export type KnowledgeScope = "enterprise" | "flagship" | "cosmic" | "xinghan" | "cangqiong";
2
+ export type Edition = Extract<KnowledgeScope, "enterprise" | "flagship">;
3
+
4
+ export type KnowledgeFileType = "markdown" | "json";
5
+
6
+ export interface KnowledgeFile {
7
+ path: string;
8
+ title: string;
9
+ type: KnowledgeFileType;
10
+ sections: KnowledgeSection[];
11
+ tags: string[];
12
+ lastModified: string;
13
+ }
14
+
15
+ export interface KnowledgeSection {
16
+ heading: string;
17
+ level: number;
18
+ content: string;
19
+ lineStart: number;
20
+ lineEnd: number;
21
+ }
22
+
23
+ export interface TableSchema {
24
+ name: string;
25
+ module: string;
26
+ description: string;
27
+ fields: FieldDefinition[];
28
+ relatedTables: RelatedTable[];
29
+ }
30
+
31
+ export interface FieldDefinition {
32
+ name: string;
33
+ type: string;
34
+ nullable: boolean;
35
+ description: string;
36
+ }
37
+
38
+ export interface RelatedTable {
39
+ table: string;
40
+ relation: string;
41
+ description: string;
42
+ }
43
+
44
+ export interface KnowledgeBase {
45
+ common: KnowledgeFile[];
46
+ edition: KnowledgeFile[];
47
+ tables: Map<string, TableSchema>;
48
+ }
49
+
50
+ export interface SearchResult {
51
+ file: KnowledgeFile;
52
+ section: KnowledgeSection;
53
+ score: number;
54
+ highlights: string[];
55
+ }
56
+
57
+ export interface SearchOptions {
58
+ tags?: string[];
59
+ edition?: Edition;
60
+ scope?: KnowledgeScope;
61
+ scopes?: KnowledgeScope[];
62
+ topK?: number;
63
+ minScore?: number;
64
+ }