@prover-coder-ai/context-doc 1.0.12 → 1.0.13
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.
- package/.jscpd.json +16 -0
- package/CHANGELOG.md +13 -0
- package/bin/context-doc.js +12 -0
- package/biome.json +37 -0
- package/eslint.config.mts +305 -0
- package/eslint.effect-ts-check.config.mjs +220 -0
- package/linter.config.json +33 -0
- package/package.json +72 -41
- package/src/app/main.ts +29 -0
- package/src/app/program.ts +32 -0
- package/src/core/knowledge.ts +93 -117
- package/src/shell/cli.ts +73 -5
- package/src/shell/services/crypto.ts +50 -0
- package/src/shell/services/file-system.ts +142 -0
- package/src/shell/services/runtime-env.ts +54 -0
- package/src/shell/sync/index.ts +35 -0
- package/src/shell/sync/shared.ts +229 -0
- package/src/shell/sync/sources/claude.ts +54 -0
- package/src/shell/sync/sources/codex.ts +261 -0
- package/src/shell/sync/sources/qwen.ts +97 -0
- package/src/shell/sync/types.ts +47 -0
- package/tests/core/knowledge.test.ts +95 -0
- package/tests/shell/cli-pack.test.ts +176 -0
- package/tests/shell/sync-knowledge.test.ts +203 -0
- package/tests/support/fs-helpers.ts +50 -0
- package/tsconfig.json +19 -0
- package/vite.config.ts +32 -0
- package/vitest.config.ts +70 -66
- package/README.md +0 -42
- package/dist/core/greeting.js +0 -25
- package/dist/core/knowledge.js +0 -49
- package/dist/main.js +0 -27
- package/dist/shell/claudeSync.js +0 -23
- package/dist/shell/cli.js +0 -5
- package/dist/shell/codexSync.js +0 -129
- package/dist/shell/qwenSync.js +0 -41
- package/dist/shell/syncKnowledge.js +0 -39
- package/dist/shell/syncShared.js +0 -49
- package/dist/shell/syncTypes.js +0 -1
- package/src/core/greeting.ts +0 -39
- package/src/main.ts +0 -49
- package/src/shell/claudeSync.ts +0 -55
- package/src/shell/codexSync.ts +0 -236
- package/src/shell/qwenSync.ts +0 -73
- package/src/shell/syncKnowledge.ts +0 -49
- package/src/shell/syncShared.ts +0 -94
- package/src/shell/syncTypes.ts +0 -30
- package/src/types/env.d.ts +0 -10
- package/tsconfig.build.json +0 -18
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { NodeContext } from "@effect/platform-node"
|
|
2
|
+
import type * as FileSystem from "@effect/platform/FileSystem"
|
|
3
|
+
import * as Path from "@effect/platform/Path"
|
|
4
|
+
import { describe, expect, it } from "@effect/vitest"
|
|
5
|
+
import { Effect, Layer, Option, pipe } from "effect"
|
|
6
|
+
import fc from "fast-check"
|
|
7
|
+
|
|
8
|
+
import { CryptoService, CryptoServiceLive } from "../../src/shell/services/crypto.js"
|
|
9
|
+
import { FileSystemLive } from "../../src/shell/services/file-system.js"
|
|
10
|
+
import { RuntimeEnv } from "../../src/shell/services/runtime-env.js"
|
|
11
|
+
import { buildSyncProgram } from "../../src/shell/sync/index.js"
|
|
12
|
+
import { buildTestPaths, makeTempDir, withFsPath, writeFile } from "../support/fs-helpers.js"
|
|
13
|
+
|
|
14
|
+
const forEach = Effect.forEach
|
|
15
|
+
const some = Option.some
|
|
16
|
+
const mapFileEntry = (
|
|
17
|
+
sessionDir: string,
|
|
18
|
+
fs: FileSystem.FileSystem,
|
|
19
|
+
path: Path.Path
|
|
20
|
+
) =>
|
|
21
|
+
(entry: string) =>
|
|
22
|
+
pipe(
|
|
23
|
+
fs.stat(path.join(sessionDir, entry)),
|
|
24
|
+
Effect.map((info) => info.type === "File" ? some(entry) : Option.none())
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
const testPaths = buildTestPaths(
|
|
28
|
+
new URL(import.meta.url),
|
|
29
|
+
"context-doc-tests"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
const withTempDir = Effect.gen(function*(_) {
|
|
33
|
+
const { tempBase } = yield* _(testPaths)
|
|
34
|
+
return yield* _(makeTempDir(tempBase, "context-doc-"))
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const makeRuntimeEnvLayer = (cwd: string): Layer.Layer<RuntimeEnv> =>
|
|
38
|
+
Layer.succeed(RuntimeEnv, {
|
|
39
|
+
argv: Effect.succeed(["node", "main"]),
|
|
40
|
+
cwd: Effect.succeed(cwd),
|
|
41
|
+
homedir: Effect.succeed(cwd),
|
|
42
|
+
envVar: () => Effect.succeed(Option.none())
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const assertSyncOutput = (
|
|
46
|
+
destDir: string,
|
|
47
|
+
qwenHash: string,
|
|
48
|
+
cwd: string,
|
|
49
|
+
expectedMessage: string,
|
|
50
|
+
skippedMessage: string
|
|
51
|
+
) =>
|
|
52
|
+
withFsPath((fs, path) =>
|
|
53
|
+
Effect.gen(function*(_) {
|
|
54
|
+
const sessionDir = path.join(destDir, "sessions/2025/11")
|
|
55
|
+
const entries = yield* _(fs.readDirectory(sessionDir))
|
|
56
|
+
const files = yield* _(
|
|
57
|
+
forEach(entries, mapFileEntry(sessionDir, fs, path))
|
|
58
|
+
)
|
|
59
|
+
const copiedFiles = files.flatMap((entry) => Option.isSome(entry) ? [entry.value] : [])
|
|
60
|
+
|
|
61
|
+
expect(copiedFiles).toEqual(["match.jsonl"])
|
|
62
|
+
|
|
63
|
+
const content = yield* _(
|
|
64
|
+
fs.readFileString(path.join(sessionDir, "match.jsonl"))
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(content).toContain(`"message":"${expectedMessage}"`)
|
|
68
|
+
expect(content).not.toContain(`"message":"${skippedMessage}"`)
|
|
69
|
+
|
|
70
|
+
const qwenCopied = path.join(
|
|
71
|
+
cwd,
|
|
72
|
+
".knowledge",
|
|
73
|
+
".qwen",
|
|
74
|
+
qwenHash,
|
|
75
|
+
"chats",
|
|
76
|
+
"session-1.json"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const exists = yield* _(fs.exists(qwenCopied))
|
|
80
|
+
expect(exists).toBe(true)
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const runSyncScenario = (
|
|
85
|
+
matchMessage: string,
|
|
86
|
+
skippedMessage: string
|
|
87
|
+
) =>
|
|
88
|
+
Effect.scoped(
|
|
89
|
+
Effect.gen(function*(_) {
|
|
90
|
+
const path = yield* _(Path.Path)
|
|
91
|
+
const crypto = yield* _(CryptoService)
|
|
92
|
+
const cwd = yield* _(withTempDir)
|
|
93
|
+
const codexDir = path.join(cwd, ".codex")
|
|
94
|
+
const destDir = path.join(cwd, ".knowledge", ".codex")
|
|
95
|
+
const qwenSource = path.join(cwd, ".qwen", "tmp")
|
|
96
|
+
const qwenHash = yield* _(crypto.sha256(cwd))
|
|
97
|
+
yield* _(
|
|
98
|
+
writeFile(
|
|
99
|
+
path.join(codexDir, "sessions/2025/11/match.jsonl"),
|
|
100
|
+
[
|
|
101
|
+
JSON.stringify({ cwd, message: matchMessage }),
|
|
102
|
+
JSON.stringify({
|
|
103
|
+
payload: { cwd: path.join(cwd, "sub") }
|
|
104
|
+
})
|
|
105
|
+
].join("\n")
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
yield* _(
|
|
110
|
+
writeFile(
|
|
111
|
+
path.join(codexDir, "sessions/2025/11/ignore.jsonl"),
|
|
112
|
+
[
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
cwd: "/home/user/other",
|
|
115
|
+
message: skippedMessage
|
|
116
|
+
})
|
|
117
|
+
].join("\n")
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
yield* _(
|
|
122
|
+
writeFile(
|
|
123
|
+
path.join(qwenSource, qwenHash, "chats", "session-1.json"),
|
|
124
|
+
JSON.stringify({ sessionId: "s1", projectHash: qwenHash })
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
yield* _(
|
|
129
|
+
Effect.provide(
|
|
130
|
+
buildSyncProgram({
|
|
131
|
+
cwd,
|
|
132
|
+
sourceDir: codexDir,
|
|
133
|
+
destinationDir: destDir,
|
|
134
|
+
qwenSourceDir: qwenSource
|
|
135
|
+
}),
|
|
136
|
+
Layer.mergeAll(FileSystemLive, makeRuntimeEnvLayer(cwd))
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
yield* _(assertSyncOutput(destDir, qwenHash, cwd, matchMessage, skippedMessage))
|
|
141
|
+
})
|
|
142
|
+
).pipe(
|
|
143
|
+
Effect.provide(Layer.mergeAll(NodeContext.layer, CryptoServiceLive))
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
describe("sync-knowledge end-to-end", () => {
|
|
147
|
+
const safeChar = fc.constantFrom(
|
|
148
|
+
"a",
|
|
149
|
+
"b",
|
|
150
|
+
"c",
|
|
151
|
+
"d",
|
|
152
|
+
"e",
|
|
153
|
+
"f",
|
|
154
|
+
"g",
|
|
155
|
+
"h",
|
|
156
|
+
"i",
|
|
157
|
+
"j",
|
|
158
|
+
"k",
|
|
159
|
+
"l",
|
|
160
|
+
"m",
|
|
161
|
+
"n",
|
|
162
|
+
"o",
|
|
163
|
+
"p",
|
|
164
|
+
"q",
|
|
165
|
+
"r",
|
|
166
|
+
"s",
|
|
167
|
+
"t",
|
|
168
|
+
"u",
|
|
169
|
+
"v",
|
|
170
|
+
"w",
|
|
171
|
+
"x",
|
|
172
|
+
"y",
|
|
173
|
+
"z",
|
|
174
|
+
"0",
|
|
175
|
+
"1",
|
|
176
|
+
"2",
|
|
177
|
+
"3",
|
|
178
|
+
"4",
|
|
179
|
+
"5"
|
|
180
|
+
)
|
|
181
|
+
const message: fc.Arbitrary<string> = fc.string({
|
|
182
|
+
minLength: 1,
|
|
183
|
+
maxLength: 12,
|
|
184
|
+
unit: safeChar
|
|
185
|
+
})
|
|
186
|
+
const messagePair: fc.Arbitrary<[string, string]> = fc
|
|
187
|
+
.tuple(message, message)
|
|
188
|
+
.filter((pair) => pair[0] !== pair[1])
|
|
189
|
+
|
|
190
|
+
it.effect("copies matching Codex files and Qwen json", () =>
|
|
191
|
+
Effect.tryPromise({
|
|
192
|
+
try: () =>
|
|
193
|
+
fc.assert(
|
|
194
|
+
fc.asyncProperty(
|
|
195
|
+
messagePair,
|
|
196
|
+
([matchMessage, skippedMessage]: [string, string]) =>
|
|
197
|
+
Effect.runPromise(runSyncScenario(matchMessage, skippedMessage))
|
|
198
|
+
),
|
|
199
|
+
{ numRuns: 5 }
|
|
200
|
+
),
|
|
201
|
+
catch: (error) => error instanceof Error ? error : new Error(String(error))
|
|
202
|
+
}))
|
|
203
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
+
import * as Path from "@effect/platform/Path"
|
|
3
|
+
import { Effect, pipe } from "effect"
|
|
4
|
+
|
|
5
|
+
const accessFsPath = Effect.gen(function*(_) {
|
|
6
|
+
const fs = yield* _(FileSystem.FileSystem)
|
|
7
|
+
const path = yield* _(Path.Path)
|
|
8
|
+
return { fs, path }
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
export const withFsPath = <A, E, R>(
|
|
12
|
+
fn: (fs: FileSystem.FileSystem, path: Path.Path) => Effect.Effect<A, E, R>
|
|
13
|
+
): Effect.Effect<A, E, FileSystem.FileSystem | Path.Path | R> =>
|
|
14
|
+
pipe(
|
|
15
|
+
accessFsPath,
|
|
16
|
+
Effect.flatMap(({ fs, path }) => fn(fs, path))
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
export const writeFile = (filePath: string, content: string) =>
|
|
20
|
+
withFsPath((fs, path) =>
|
|
21
|
+
Effect.gen(function*(_) {
|
|
22
|
+
yield* _(fs.makeDirectory(path.dirname(filePath), { recursive: true }))
|
|
23
|
+
yield* _(fs.writeFileString(filePath, content))
|
|
24
|
+
})
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export const makeTempDir = (base: string, prefix: string) =>
|
|
28
|
+
withFsPath((fs, _path) =>
|
|
29
|
+
Effect.gen(function*(_) {
|
|
30
|
+
yield* _(fs.makeDirectory(base, { recursive: true }))
|
|
31
|
+
return yield* _(fs.makeTempDirectoryScoped({ directory: base, prefix }))
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export const buildTestPaths = (
|
|
36
|
+
testUrl: URL,
|
|
37
|
+
tempName: string,
|
|
38
|
+
packName?: string
|
|
39
|
+
) =>
|
|
40
|
+
Effect.gen(function*(_) {
|
|
41
|
+
const path = yield* _(Path.Path)
|
|
42
|
+
const testFile = yield* _(path.fromFileUrl(testUrl))
|
|
43
|
+
const testRoot = path.dirname(testFile)
|
|
44
|
+
const projectRoot = path.resolve(testRoot, "../../../..")
|
|
45
|
+
const tempBase = path.join(projectRoot, ".tmp", tempName)
|
|
46
|
+
const packBase = packName === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: path.join(projectRoot, ".tmp", packName)
|
|
49
|
+
return { projectRoot, tempBase, packBase }
|
|
50
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"types": ["vitest"],
|
|
7
|
+
"baseUrl": ".",
|
|
8
|
+
"paths": {
|
|
9
|
+
"@/*": ["src/*"]
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"include": [
|
|
13
|
+
"src/**/*",
|
|
14
|
+
"tests/**/*",
|
|
15
|
+
"vite.config.ts",
|
|
16
|
+
"vitest.config.ts"
|
|
17
|
+
],
|
|
18
|
+
"exclude": ["dist", "node_modules"]
|
|
19
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import { fileURLToPath } from "node:url"
|
|
3
|
+
import { defineConfig } from "vite"
|
|
4
|
+
import tsconfigPaths from "vite-tsconfig-paths"
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
7
|
+
const __dirname = path.dirname(__filename)
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
plugins: [tsconfigPaths()],
|
|
11
|
+
publicDir: false,
|
|
12
|
+
resolve: {
|
|
13
|
+
alias: {
|
|
14
|
+
"@": path.resolve(__dirname, "src")
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
build: {
|
|
18
|
+
target: "node20",
|
|
19
|
+
outDir: "dist",
|
|
20
|
+
sourcemap: true,
|
|
21
|
+
ssr: "src/app/main.ts",
|
|
22
|
+
rollupOptions: {
|
|
23
|
+
output: {
|
|
24
|
+
format: "es",
|
|
25
|
+
entryFileNames: "main.js"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
ssr: {
|
|
30
|
+
target: "node"
|
|
31
|
+
}
|
|
32
|
+
})
|
package/vitest.config.ts
CHANGED
|
@@ -7,75 +7,79 @@
|
|
|
7
7
|
// EFFECT: Effect<TestReport, never, TestEnvironment>
|
|
8
8
|
// COMPLEXITY: O(n) test execution where n = |test_files|
|
|
9
9
|
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import path from "node:path"
|
|
11
|
+
import { fileURLToPath } from "node:url"
|
|
12
|
+
import tsconfigPaths from "vite-tsconfig-paths"
|
|
13
|
+
import { defineConfig } from "vitest/config"
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
test: {
|
|
16
|
-
// CHANGE: Native ESM support without experimental flags
|
|
17
|
-
// WHY: Vitest designed for ESM, no need for --experimental-vm-modules
|
|
18
|
-
// INVARIANT: Deterministic test execution without side effects
|
|
19
|
-
globals: false, // IMPORTANT: Use explicit imports for type safety
|
|
20
|
-
environment: "node",
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
16
|
+
const __dirname = path.dirname(__filename)
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
export default defineConfig({
|
|
19
|
+
plugins: [tsconfigPaths()], // Resolves @/* paths from tsconfig
|
|
20
|
+
test: {
|
|
21
|
+
// CHANGE: Native ESM support without experimental flags
|
|
22
|
+
// WHY: Vitest designed for ESM, no need for --experimental-vm-modules
|
|
23
|
+
// INVARIANT: Deterministic test execution without side effects
|
|
24
|
+
globals: false, // IMPORTANT: Use explicit imports for type safety
|
|
25
|
+
environment: "node",
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
provider: "v8", // Faster than babel (istanbul), native V8 coverage
|
|
32
|
-
reporter: ["text", "json", "html"],
|
|
33
|
-
include: ["src/**/*.ts"],
|
|
34
|
-
exclude: [
|
|
35
|
-
"src/**/*.test.ts",
|
|
36
|
-
"src/**/*.spec.ts",
|
|
37
|
-
"src/**/__tests__/**",
|
|
38
|
-
"scripts/**/*.ts",
|
|
39
|
-
],
|
|
40
|
-
// CHANGE: Maintain exact same thresholds as Jest
|
|
41
|
-
// WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
|
|
42
|
-
// INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
|
|
43
|
-
// NOTE: Vitest v8 provider collects coverage for all matched files by default
|
|
44
|
-
thresholds: {
|
|
45
|
-
"src/core/**/*.ts": {
|
|
46
|
-
branches: 100,
|
|
47
|
-
functions: 100,
|
|
48
|
-
lines: 100,
|
|
49
|
-
statements: 100,
|
|
50
|
-
},
|
|
51
|
-
global: {
|
|
52
|
-
branches: 10,
|
|
53
|
-
functions: 10,
|
|
54
|
-
lines: 10,
|
|
55
|
-
statements: 10,
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
},
|
|
27
|
+
// CHANGE: Match Jest's test file patterns
|
|
28
|
+
// INVARIANT: Same test discovery as Jest
|
|
29
|
+
include: ["tests/**/*.{test,spec}.ts"],
|
|
30
|
+
exclude: ["node_modules", "dist", "dist-test"],
|
|
59
31
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
32
|
+
// CHANGE: Coverage with 100% threshold for CORE (same as Jest)
|
|
33
|
+
// WHY: CORE must maintain mathematical guarantees via complete coverage
|
|
34
|
+
// INVARIANT: coverage_vitest ≥ coverage_jest ∧ ∀ f ∈ CORE: coverage(f) = 100%
|
|
35
|
+
coverage: {
|
|
36
|
+
provider: "v8", // Faster than babel (istanbul), native V8 coverage
|
|
37
|
+
reporter: ["text", "json", "html"],
|
|
38
|
+
include: ["src/**/*.ts"],
|
|
39
|
+
exclude: [
|
|
40
|
+
"src/**/*.test.ts",
|
|
41
|
+
"src/**/*.spec.ts",
|
|
42
|
+
"src/**/__tests__/**",
|
|
43
|
+
"scripts/**/*.ts"
|
|
44
|
+
],
|
|
45
|
+
// CHANGE: Maintain exact same thresholds as Jest
|
|
46
|
+
// WHY: Enforce 100% coverage for CORE, 10% minimum for SHELL
|
|
47
|
+
// INVARIANT: ∀ f ∈ src/core/**/*.ts: all_metrics(f) = 100%
|
|
48
|
+
// NOTE: Vitest v8 provider collects coverage for all matched files by default
|
|
49
|
+
thresholds: {
|
|
50
|
+
"src/core/**/*.ts": {
|
|
51
|
+
branches: 100,
|
|
52
|
+
functions: 100,
|
|
53
|
+
lines: 100,
|
|
54
|
+
statements: 100
|
|
55
|
+
},
|
|
56
|
+
global: {
|
|
57
|
+
branches: 10,
|
|
58
|
+
functions: 10,
|
|
59
|
+
lines: 10,
|
|
60
|
+
statements: 10
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
mockReset: true,
|
|
70
|
-
restoreMocks: true,
|
|
65
|
+
// CHANGE: Faster test execution via thread pooling
|
|
66
|
+
// WHY: Vitest uses worker threads by default (faster than Jest's processes)
|
|
67
|
+
// COMPLEXITY: O(n/k) where n = tests, k = worker_count
|
|
68
|
+
// NOTE: Vitest runs tests in parallel by default, no additional config needed
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
70
|
+
// CHANGE: Clear mocks between tests (Jest equivalence)
|
|
71
|
+
// WHY: Prevent test contamination, ensure test independence
|
|
72
|
+
// INVARIANT: ∀ test_i, test_j: independent(test_i, test_j) ⇒ no_shared_state
|
|
73
|
+
clearMocks: true,
|
|
74
|
+
mockReset: true,
|
|
75
|
+
restoreMocks: true
|
|
76
|
+
// CHANGE: Disable globals to enforce explicit imports
|
|
77
|
+
// WHY: Type safety, explicit dependencies, functional purity
|
|
78
|
+
// NOTE: Tests must import { describe, it, expect } from "vitest"
|
|
79
|
+
},
|
|
80
|
+
resolve: {
|
|
81
|
+
alias: {
|
|
82
|
+
"@": path.resolve(__dirname, "src")
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
})
|
package/README.md
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Knowledge Sync CLI
|
|
2
|
-
|
|
3
|
-
Command: `npx @prover-coder-ai/context-doc [flags]` (or `context-doc` when installed globally) — copies project dialogs into `.knowledge` for both Codex and Qwen.
|
|
4
|
-
|
|
5
|
-
## Installation (global CLI)
|
|
6
|
-
- Install: `npm install -g @prover-coder-ai/context-doc`
|
|
7
|
-
- Registry page: https://www.npmjs.com/package/@prover-coder-ai/context-doc
|
|
8
|
-
- Run globally: `context-doc [flags]`
|
|
9
|
-
- Ad-hoc (no install): `npx @prover-coder-ai/context-doc [flags]`
|
|
10
|
-
- Local (without global install): `npm run sync:knowledge` or `tsx src/shell/syncKnowledge.ts`
|
|
11
|
-
|
|
12
|
-
## Usage examples
|
|
13
|
-
- Default (auto-detect sources/destinations): `context-doc`
|
|
14
|
-
- Custom Codex source/destination: `context-doc --source /tmp/project/.codex --dest /tmp/project/.knowledge/.codex`
|
|
15
|
-
- Custom meta root (both Codex/Qwen resolution): `context-doc --meta-root /data/meta`
|
|
16
|
-
- Override project URL: `context-doc --project-url https://github.com/your/repo`
|
|
17
|
-
- Qwen source override: `context-doc --qwen-source /data/qwen/tmp/abcd1234`
|
|
18
|
-
|
|
19
|
-
## Flags
|
|
20
|
-
- `--source, -s <path>` — explicit path to `.codex` (Codex source).
|
|
21
|
-
- `--dest, -d <path>` — Codex destination root (defaults to `.knowledge/.codex`).
|
|
22
|
-
- `--project-url` / `--project-name <url>` — override repository URL (otherwise read from `package.json`).
|
|
23
|
-
- `--meta-root <path>` — meta folder root; Codex lookup tries `<meta-root>/.codex`, Qwen lookup tries `<meta-root>/.qwen/tmp/<hash>`.
|
|
24
|
-
- `--qwen-source <path>` — explicit path to Qwen source.
|
|
25
|
-
|
|
26
|
-
## Codex lookup order (`.jsonl` filtered by project)
|
|
27
|
-
1) `--source`
|
|
28
|
-
2) env `CODEX_SOURCE_DIR`
|
|
29
|
-
3) `--meta-root` (use if already `.codex` or append `/.codex`)
|
|
30
|
-
4) project-local `.codex`
|
|
31
|
-
5) home `~/.codex`
|
|
32
|
-
|
|
33
|
-
Files: copies only `.jsonl` whose `git.repository_url` or `cwd` match this project into `.knowledge/.codex`, preserving structure.
|
|
34
|
-
|
|
35
|
-
## Qwen lookup order (`.json` only, no project filter)
|
|
36
|
-
1) `--qwen-source`
|
|
37
|
-
2) env `QWEN_SOURCE_DIR`
|
|
38
|
-
3) `<meta-root>/.qwen/tmp/<hash>`
|
|
39
|
-
4) `<cwd>/.qwen/tmp/<hash>`
|
|
40
|
-
5) `~/.qwen/tmp/<hash>`
|
|
41
|
-
|
|
42
|
-
Files: copies only `.json` into `.knowledge/.qwen`, preserving structure (directories recreated).
|
package/dist/core/greeting.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { Option } from "effect";
|
|
2
|
-
const normalize = (value) => value.trim();
|
|
3
|
-
const isNonEmpty = (value) => value.length > 0;
|
|
4
|
-
/**
|
|
5
|
-
* CHANGE: Introduce pure greeting synthesis for console entrypoint
|
|
6
|
-
* WHY: Provide mathematically checkable construction of startup banner without side effects
|
|
7
|
-
* QUOTE(ТЗ): "Каждая функция — это теорема."
|
|
8
|
-
* REF: REQ-GREETING-CORE
|
|
9
|
-
* SOURCE: AGENTS.md – функциональное ядро без побочных эффектов
|
|
10
|
-
* FORMAT THEOREM: ∀p ∈ AppProfile: valid(p) → nonEmpty(buildGreeting(p).message)
|
|
11
|
-
* PURITY: CORE
|
|
12
|
-
* EFFECT: None (pure)
|
|
13
|
-
* INVARIANT: output.message.length > 0 when Option is Some
|
|
14
|
-
* COMPLEXITY: O(1)/O(1)
|
|
15
|
-
*/
|
|
16
|
-
export const buildGreeting = (profile) => {
|
|
17
|
-
const trimmedName = normalize(profile.name);
|
|
18
|
-
const trimmedMission = normalize(profile.mission);
|
|
19
|
-
if (!isNonEmpty(trimmedName) || !isNonEmpty(trimmedMission)) {
|
|
20
|
-
return Option.none();
|
|
21
|
-
}
|
|
22
|
-
return Option.some({
|
|
23
|
-
message: `${trimmedName}: ${trimmedMission}`,
|
|
24
|
-
});
|
|
25
|
-
};
|
package/dist/core/knowledge.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import { Option, pipe } from "effect";
|
|
3
|
-
const isJsonRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
-
const normalizeRepositoryUrl = (value) => pipe(value.trim(), (trimmed) => trimmed.replace(/^git\+/, ""), (withoutPrefix) => withoutPrefix.replace(/\.git$/, ""), (withoutGitSuffix) => withoutGitSuffix.replace(/^git@github\.com:/, "https://github.com/"), (withoutSsh) => withoutSsh.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/"), (normalized) => normalized.toLowerCase());
|
|
5
|
-
const normalizeCwd = (value) => path.resolve(value);
|
|
6
|
-
const pickString = (record, key) => {
|
|
7
|
-
const candidate = record[key];
|
|
8
|
-
return typeof candidate === "string" ? Option.some(candidate) : Option.none();
|
|
9
|
-
};
|
|
10
|
-
const pickRecord = (record, key) => pipe(record[key], Option.fromNullable, Option.filter(isJsonRecord));
|
|
11
|
-
const pickGitRepository = (record) => pipe(pickString(record, "repository_url"), Option.orElse(() => pickString(record, "repositoryUrl")));
|
|
12
|
-
const extractRepository = (record) => pipe(pickRecord(record, "git"), Option.flatMap(pickGitRepository), Option.orElse(() => pipe(pickRecord(record, "payload"), Option.flatMap((payload) => pipe(pickRecord(payload, "git"), Option.flatMap(pickGitRepository))))));
|
|
13
|
-
const extractCwd = (record) => pipe(pickString(record, "cwd"), Option.orElse(() => pipe(pickRecord(record, "payload"), Option.flatMap((payload) => pickString(payload, "cwd")))));
|
|
14
|
-
const toMetadata = (value) => {
|
|
15
|
-
if (!isJsonRecord(value)) {
|
|
16
|
-
return { repositoryUrl: Option.none(), cwd: Option.none() };
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
repositoryUrl: extractRepository(value),
|
|
20
|
-
cwd: extractCwd(value),
|
|
21
|
-
};
|
|
22
|
-
};
|
|
23
|
-
const normalizeLocator = (repositoryUrl, cwd) => ({
|
|
24
|
-
normalizedRepositoryUrl: normalizeRepositoryUrl(repositoryUrl),
|
|
25
|
-
normalizedCwd: normalizeCwd(cwd),
|
|
26
|
-
});
|
|
27
|
-
const safeParseJson = (line) => {
|
|
28
|
-
try {
|
|
29
|
-
return Option.some(JSON.parse(line));
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return Option.none();
|
|
33
|
-
}
|
|
34
|
-
};
|
|
35
|
-
const metadataMatches = (metadata, locator) => {
|
|
36
|
-
const cwdMatches = Option.exists(metadata.cwd, (cwdValue) => {
|
|
37
|
-
const normalized = normalizeCwd(cwdValue);
|
|
38
|
-
return normalized === locator.normalizedCwd;
|
|
39
|
-
});
|
|
40
|
-
return cwdMatches;
|
|
41
|
-
};
|
|
42
|
-
export const buildProjectLocator = (repositoryUrl, cwd) => normalizeLocator(repositoryUrl, cwd);
|
|
43
|
-
export const linesMatchProject = (lines, locator) => lines.some((line) => {
|
|
44
|
-
const trimmed = line.trim();
|
|
45
|
-
if (trimmed.length === 0) {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
return pipe(safeParseJson(trimmed), Option.map(toMetadata), Option.exists((metadata) => metadataMatches(metadata, locator)));
|
|
49
|
-
});
|
package/dist/main.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { Console, Effect, Option, pipe } from "effect";
|
|
2
|
-
import { match } from "ts-pattern";
|
|
3
|
-
import { buildGreeting } from "./core/greeting.js";
|
|
4
|
-
const appProfile = {
|
|
5
|
-
name: "Context Console",
|
|
6
|
-
mission: "Functional core ready for verifiable effects",
|
|
7
|
-
};
|
|
8
|
-
const toStartupError = (reason) => ({
|
|
9
|
-
_tag: "StartupError",
|
|
10
|
-
reason,
|
|
11
|
-
});
|
|
12
|
-
/**
|
|
13
|
-
* CHANGE: Compose shell-level startup program around pure greeting builder
|
|
14
|
-
* WHY: Isolate side effects (console IO) while delegating computation to functional core
|
|
15
|
-
* QUOTE(ТЗ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
|
|
16
|
-
* REF: REQ-SHELL-STARTUP
|
|
17
|
-
* SOURCE: AGENTS.md — эффекты только в SHELL
|
|
18
|
-
* FORMAT THEOREM: ∀profile: valid(profile) → logs(buildGreeting(profile))
|
|
19
|
-
* PURITY: SHELL
|
|
20
|
-
* EFFECT: Effect<void, StartupError, Console>
|
|
21
|
-
* INVARIANT: Console side-effects occur once and only after successful greeting synthesis
|
|
22
|
-
* COMPLEXITY: O(1)/O(1)
|
|
23
|
-
*/
|
|
24
|
-
const program = pipe(Effect.succeed(buildGreeting(appProfile)), Effect.filterOrFail(Option.isSome, () => toStartupError("Profile must not be empty")), Effect.map((option) => option.value), Effect.flatMap((greeting) => Console.log(greeting.message)), Effect.catchAll((error) => match(error)
|
|
25
|
-
.with({ _tag: "StartupError" }, (startupError) => Console.error(`StartupError: ${startupError.reason}`))
|
|
26
|
-
.exhaustive()));
|
|
27
|
-
Effect.runSync(program);
|
package/dist/shell/claudeSync.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as NodeFs from "node:fs";
|
|
2
|
-
import * as NodeOs from "node:os";
|
|
3
|
-
import * as NodePath from "node:path";
|
|
4
|
-
import { Effect, pipe } from "effect";
|
|
5
|
-
import { copyFilteredFiles, runSyncSource, syncError } from "./syncShared.js";
|
|
6
|
-
const slugFromCwd = (cwd) => `-${cwd.replace(/^\/+/, "").replace(/\//g, "-")}`;
|
|
7
|
-
const resolveClaudeProjectDir = (cwd, overrideProjectsRoot) => pipe(Effect.sync(() => {
|
|
8
|
-
const slug = slugFromCwd(cwd);
|
|
9
|
-
const base = overrideProjectsRoot ??
|
|
10
|
-
NodePath.join(NodeOs.homedir(), ".claude", "projects");
|
|
11
|
-
const candidate = NodePath.join(base, slug);
|
|
12
|
-
return NodeFs.existsSync(candidate) ? candidate : undefined;
|
|
13
|
-
}), Effect.flatMap((found) => found === undefined
|
|
14
|
-
? Effect.fail(syncError(".claude", "Claude project directory is missing"))
|
|
15
|
-
: Effect.succeed(found)));
|
|
16
|
-
const copyClaudeJsonl = (sourceDir, destinationDir) => copyFilteredFiles(sourceDir, destinationDir, (entry, fullPath) => entry.isFile() && fullPath.endsWith(".jsonl"), "Cannot traverse Claude project");
|
|
17
|
-
export const syncClaude = (options) => runSyncSource(claudeSource, options);
|
|
18
|
-
const claudeSource = {
|
|
19
|
-
name: "Claude",
|
|
20
|
-
destSubdir: ".claude",
|
|
21
|
-
resolveSource: (options) => resolveClaudeProjectDir(options.cwd, options.claudeProjectsRoot),
|
|
22
|
-
copy: (sourceDir, destinationDir) => copyClaudeJsonl(sourceDir, destinationDir),
|
|
23
|
-
};
|