@prover-coder-ai/context-doc 1.0.18 → 1.0.19
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/README.md +18 -0
- package/dist/main.js +553 -0
- package/dist/main.js.map +1 -0
- package/package.json +6 -1
- package/.jscpd.json +0 -16
- package/CHANGELOG.md +0 -43
- package/biome.json +0 -37
- package/eslint.config.mts +0 -305
- package/eslint.effect-ts-check.config.mjs +0 -220
- package/linter.config.json +0 -33
- package/src/app/main.ts +0 -29
- package/src/app/program.ts +0 -32
- package/src/core/knowledge.ts +0 -115
- package/src/shell/cli.ts +0 -75
- package/src/shell/services/crypto.ts +0 -50
- package/src/shell/services/file-system.ts +0 -142
- package/src/shell/services/runtime-env.ts +0 -54
- package/src/shell/sync/index.ts +0 -35
- package/src/shell/sync/shared.ts +0 -229
- package/src/shell/sync/sources/claude.ts +0 -54
- package/src/shell/sync/sources/codex.ts +0 -261
- package/src/shell/sync/sources/qwen.ts +0 -97
- package/src/shell/sync/types.ts +0 -47
- package/tests/core/knowledge.test.ts +0 -95
- package/tests/shell/cli-pack.test.ts +0 -176
- package/tests/shell/sync-knowledge.test.ts +0 -203
- package/tests/support/fs-helpers.ts +0 -50
- package/tsconfig.json +0 -19
- package/vite.config.ts +0 -32
- package/vitest.config.ts +0 -85
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
// CHANGE: add Effect-TS compliance lint profile
|
|
2
|
-
// WHY: detect current deviations from strict Effect-TS guidance
|
|
3
|
-
// QUOTE(TZ): n/a
|
|
4
|
-
// REF: AGENTS.md Effect-TS compliance checks
|
|
5
|
-
// SOURCE: n/a
|
|
6
|
-
// PURITY: SHELL
|
|
7
|
-
// EFFECT: eslint config
|
|
8
|
-
// INVARIANT: config only flags explicit policy deviations
|
|
9
|
-
// COMPLEXITY: O(1)/O(1)
|
|
10
|
-
import eslintComments from "@eslint-community/eslint-plugin-eslint-comments"
|
|
11
|
-
import globals from "globals"
|
|
12
|
-
import tseslint from "typescript-eslint"
|
|
13
|
-
|
|
14
|
-
const restrictedImports = [
|
|
15
|
-
{
|
|
16
|
-
name: "node:fs",
|
|
17
|
-
message: "Use @effect/platform FileSystem instead of node:fs."
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
name: "fs",
|
|
21
|
-
message: "Use @effect/platform FileSystem instead of fs."
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: "node:fs/promises",
|
|
25
|
-
message: "Use @effect/platform FileSystem instead of node:fs/promises."
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
name: "node:path/posix",
|
|
29
|
-
message: "Use @effect/platform Path instead of node:path/posix."
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "node:path",
|
|
33
|
-
message: "Use @effect/platform Path instead of node:path."
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: "path",
|
|
37
|
-
message: "Use @effect/platform Path instead of path."
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: "node:child_process",
|
|
41
|
-
message: "Use @effect/platform Command instead of node:child_process."
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
name: "child_process",
|
|
45
|
-
message: "Use @effect/platform Command instead of child_process."
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: "node:process",
|
|
49
|
-
message: "Use @effect/platform Runtime instead of node:process."
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
name: "process",
|
|
53
|
-
message: "Use @effect/platform Runtime instead of process."
|
|
54
|
-
}
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
const restrictedSyntaxBase = [
|
|
58
|
-
{
|
|
59
|
-
selector: "SwitchStatement",
|
|
60
|
-
message: "Switch is forbidden. Use Match.exhaustive."
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
selector: "TryStatement",
|
|
64
|
-
message: "Avoid try/catch in product code. Use Effect.try / Effect.catch*."
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
selector: "AwaitExpression",
|
|
68
|
-
message: "Avoid await. Use Effect.gen / Effect.flatMap."
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
selector: "FunctionDeclaration[async=true], FunctionExpression[async=true], ArrowFunctionExpression[async=true]",
|
|
72
|
-
message: "Avoid async/await. Use Effect.gen / Effect.tryPromise."
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
selector: "NewExpression[callee.name='Promise']",
|
|
76
|
-
message: "Avoid new Promise. Use Effect.async / Effect.tryPromise."
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
selector: "CallExpression[callee.object.name='Promise']",
|
|
80
|
-
message: "Avoid Promise.*. Use Effect combinators."
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
selector: "CallExpression[callee.name='require']",
|
|
84
|
-
message: "Avoid require(). Use ES module imports."
|
|
85
|
-
},
|
|
86
|
-
{
|
|
87
|
-
selector: "TSAsExpression",
|
|
88
|
-
message: "Casting is only allowed in src/core/axioms.ts."
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
selector: "TSTypeAssertion",
|
|
92
|
-
message: "Casting is only allowed in src/core/axioms.ts."
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
selector: "CallExpression[callee.name='makeFilesystemService']",
|
|
96
|
-
message: "Do not instantiate FilesystemService directly. Provide Layer and access via Tag."
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
selector: "CallExpression[callee.property.name='catchAll']",
|
|
100
|
-
message: "Avoid catchAll that discards typed errors; map or propagate explicitly."
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
const restrictedSyntaxCore = [
|
|
105
|
-
...restrictedSyntaxBase,
|
|
106
|
-
{
|
|
107
|
-
selector: "TSUnknownKeyword",
|
|
108
|
-
message: "unknown is allowed only at shell boundaries with decoding."
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
selector: "CallExpression[callee.property.name='runSyncExit']",
|
|
112
|
-
message: "Effect.runSyncExit is shell-only. Move to a runner."
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
selector: "CallExpression[callee.property.name='runSync']",
|
|
116
|
-
message: "Effect.runSync is shell-only. Move to a runner."
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
selector: "CallExpression[callee.property.name='runPromise']",
|
|
120
|
-
message: "Effect.runPromise is shell-only. Move to a runner."
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
|
|
124
|
-
const restrictedSyntaxCoreNoAs = [
|
|
125
|
-
...restrictedSyntaxCore.filter((rule) =>
|
|
126
|
-
rule.selector !== "TSAsExpression" && rule.selector !== "TSTypeAssertion"
|
|
127
|
-
)
|
|
128
|
-
]
|
|
129
|
-
|
|
130
|
-
const restrictedSyntaxBaseNoServiceFactory = [
|
|
131
|
-
...restrictedSyntaxBase.filter((rule) =>
|
|
132
|
-
rule.selector !== "CallExpression[callee.name='makeFilesystemService']"
|
|
133
|
-
)
|
|
134
|
-
]
|
|
135
|
-
|
|
136
|
-
export default tseslint.config(
|
|
137
|
-
{
|
|
138
|
-
name: "effect-ts-compliance-check",
|
|
139
|
-
files: ["src/**/*.ts", "scripts/**/*.ts"],
|
|
140
|
-
languageOptions: {
|
|
141
|
-
parser: tseslint.parser,
|
|
142
|
-
globals: { ...globals.node }
|
|
143
|
-
},
|
|
144
|
-
plugins: {
|
|
145
|
-
"@typescript-eslint": tseslint.plugin,
|
|
146
|
-
"eslint-comments": eslintComments
|
|
147
|
-
},
|
|
148
|
-
rules: {
|
|
149
|
-
"no-console": "error",
|
|
150
|
-
"no-restricted-imports": ["error", {
|
|
151
|
-
paths: restrictedImports,
|
|
152
|
-
patterns: [
|
|
153
|
-
{
|
|
154
|
-
group: ["node:*"],
|
|
155
|
-
message: "Do not import from node:* directly. Use @effect/platform-node or @effect/platform services."
|
|
156
|
-
}
|
|
157
|
-
]
|
|
158
|
-
}],
|
|
159
|
-
"no-restricted-syntax": ["error", ...restrictedSyntaxBase],
|
|
160
|
-
"@typescript-eslint/no-explicit-any": "error",
|
|
161
|
-
"@typescript-eslint/ban-ts-comment": ["error", {
|
|
162
|
-
"ts-ignore": true,
|
|
163
|
-
"ts-nocheck": true,
|
|
164
|
-
"ts-check": false,
|
|
165
|
-
"ts-expect-error": true
|
|
166
|
-
}],
|
|
167
|
-
"@typescript-eslint/no-restricted-types": ["error", {
|
|
168
|
-
types: {
|
|
169
|
-
Promise: {
|
|
170
|
-
message: "Avoid Promise in types. Use Effect.Effect<A, E, R>."
|
|
171
|
-
},
|
|
172
|
-
"Promise<*>": {
|
|
173
|
-
message: "Avoid Promise<T>. Use Effect.Effect<T, E, R>."
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}],
|
|
177
|
-
"eslint-comments/no-use": "error",
|
|
178
|
-
"eslint-comments/no-unlimited-disable": "error",
|
|
179
|
-
"eslint-comments/disable-enable-pair": "error",
|
|
180
|
-
"eslint-comments/no-unused-disable": "error"
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: "effect-ts-compliance-core",
|
|
185
|
-
files: ["src/core/**/*.ts"],
|
|
186
|
-
rules: {
|
|
187
|
-
"no-restricted-syntax": ["error", ...restrictedSyntaxCore],
|
|
188
|
-
"no-restricted-imports": ["error", {
|
|
189
|
-
paths: restrictedImports,
|
|
190
|
-
patterns: [
|
|
191
|
-
{
|
|
192
|
-
group: [
|
|
193
|
-
"../shell/**",
|
|
194
|
-
"../../shell/**",
|
|
195
|
-
"../../../shell/**",
|
|
196
|
-
"./shell/**",
|
|
197
|
-
"src/shell/**",
|
|
198
|
-
"shell/**"
|
|
199
|
-
],
|
|
200
|
-
message: "CORE must not import from SHELL."
|
|
201
|
-
}
|
|
202
|
-
]
|
|
203
|
-
}]
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
{
|
|
207
|
-
name: "effect-ts-compliance-axioms",
|
|
208
|
-
files: ["src/core/axioms.ts"],
|
|
209
|
-
rules: {
|
|
210
|
-
"no-restricted-syntax": ["error", ...restrictedSyntaxCoreNoAs]
|
|
211
|
-
}
|
|
212
|
-
},
|
|
213
|
-
{
|
|
214
|
-
name: "effect-ts-compliance-filesystem-service",
|
|
215
|
-
files: ["src/shell/services/filesystem.ts"],
|
|
216
|
-
rules: {
|
|
217
|
-
"no-restricted-syntax": ["error", ...restrictedSyntaxBaseNoServiceFactory]
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
)
|
package/linter.config.json
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"priorityLevels": [
|
|
3
|
-
{
|
|
4
|
-
"level": 1,
|
|
5
|
-
"name": "Critical Compiler Errors",
|
|
6
|
-
"rules": [
|
|
7
|
-
"ts(2835)",
|
|
8
|
-
"ts(2307)",
|
|
9
|
-
"@prover-coder-ai/suggest-members/suggest-members",
|
|
10
|
-
"@prover-coder-ai/suggest-members/suggest-imports",
|
|
11
|
-
"@prover-coder-ai/suggest-members/suggest-module-paths",
|
|
12
|
-
"@prover-coder-ai/suggest-members/suggest-exports",
|
|
13
|
-
"@prover-coder-ai/suggest-members/suggest-missing-names",
|
|
14
|
-
"@typescript-eslint/no-explicit-any"
|
|
15
|
-
]
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
"level": 2,
|
|
19
|
-
"name": "Critical Compiler Errors",
|
|
20
|
-
"rules": ["all"]
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
"level": 3,
|
|
24
|
-
"name": "Critical Compiler Errors (Code must follow Clean Code and best practices)",
|
|
25
|
-
"rules": ["max-lines-per-function", "max-lines"]
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
"level": 4,
|
|
29
|
-
"name": "Critical Compiler Errors (Code must follow Clean Code and best practices)",
|
|
30
|
-
"rules": ["complexity", "max-params", "max-depth"]
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
}
|
package/src/app/main.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { NodeContext, NodeRuntime } from "@effect/platform-node"
|
|
2
|
-
import { Effect, Layer, pipe } from "effect"
|
|
3
|
-
|
|
4
|
-
import { CryptoServiceLive } from "../shell/services/crypto.js"
|
|
5
|
-
import { FileSystemLive } from "../shell/services/file-system.js"
|
|
6
|
-
import { RuntimeEnvLive } from "../shell/services/runtime-env.js"
|
|
7
|
-
import { program } from "./program.js"
|
|
8
|
-
|
|
9
|
-
// CHANGE: run the sync program through the Node runtime with all live layers
|
|
10
|
-
// WHY: provide platform services and shell dependencies in one place
|
|
11
|
-
// QUOTE(TZ): "SHELL: Все эффекты изолированы"
|
|
12
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
13
|
-
// SOURCE: n/a
|
|
14
|
-
// FORMAT THEOREM: forall env: provide(env) -> runMain(program)
|
|
15
|
-
// PURITY: SHELL
|
|
16
|
-
// EFFECT: Effect<void, never, RuntimeEnv | FileSystemService | CryptoService | Path>
|
|
17
|
-
// INVARIANT: program executed with NodeContext + live services
|
|
18
|
-
// COMPLEXITY: O(1)/O(1)
|
|
19
|
-
const main = pipe(
|
|
20
|
-
program,
|
|
21
|
-
Effect.provide(
|
|
22
|
-
Layer.provideMerge(
|
|
23
|
-
Layer.mergeAll(RuntimeEnvLive, FileSystemLive, CryptoServiceLive),
|
|
24
|
-
NodeContext.layer
|
|
25
|
-
)
|
|
26
|
-
)
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
NodeRuntime.runMain(main)
|
package/src/app/program.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Effect, pipe } from "effect"
|
|
2
|
-
|
|
3
|
-
import { readSyncOptions } from "../shell/cli.js"
|
|
4
|
-
import { buildSyncProgram } from "../shell/sync/index.js"
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Compose the knowledge sync CLI as a single effect.
|
|
8
|
-
*
|
|
9
|
-
* @returns Effect that runs multi-source sync in sequence.
|
|
10
|
-
*
|
|
11
|
-
* @pure false - reads argv and performs filesystem IO
|
|
12
|
-
* @effect RuntimeEnv, FileSystemService, CryptoService, Path
|
|
13
|
-
* @invariant forall opts: buildSyncProgram(opts) runs each source exactly once
|
|
14
|
-
* @precondition true
|
|
15
|
-
* @postcondition sources are synced or skipped with logs
|
|
16
|
-
* @complexity O(n) where n = number of files scanned
|
|
17
|
-
* @throws Never - all errors are typed in the Effect error channel
|
|
18
|
-
*/
|
|
19
|
-
// CHANGE: rewire program to knowledge-sync orchestration
|
|
20
|
-
// WHY: replace greeting demo with the fully effectful sync pipeline
|
|
21
|
-
// QUOTE(TZ): "Возьми прошлый ... код и перепиши его полностью"
|
|
22
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
23
|
-
// SOURCE: n/a
|
|
24
|
-
// FORMAT THEOREM: forall a: parse(a) -> run(sync(a))
|
|
25
|
-
// PURITY: SHELL
|
|
26
|
-
// EFFECT: Effect<void, never, RuntimeEnv | FileSystemService | CryptoService | Path>
|
|
27
|
-
// INVARIANT: sync sources run in deterministic order
|
|
28
|
-
// COMPLEXITY: O(n)/O(n)
|
|
29
|
-
export const program = pipe(
|
|
30
|
-
readSyncOptions,
|
|
31
|
-
Effect.flatMap((options) => buildSyncProgram(options))
|
|
32
|
-
)
|
package/src/core/knowledge.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { Option, pipe } from "effect"
|
|
2
|
-
|
|
3
|
-
export type JsonPrimitive = string | number | boolean | null
|
|
4
|
-
export type JsonValue = JsonPrimitive | ReadonlyArray<JsonValue> | JsonRecord
|
|
5
|
-
export interface JsonRecord {
|
|
6
|
-
readonly [key: string]: JsonValue
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ProjectLocator {
|
|
10
|
-
readonly normalizedCwd: string
|
|
11
|
-
readonly isWithinRoot: (candidate: string) => boolean
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface RecordMetadata {
|
|
15
|
-
readonly cwd: Option.Option<string>
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const isJsonRecord = (value: JsonValue): value is JsonRecord =>
|
|
19
|
-
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
20
|
-
|
|
21
|
-
const pickString = (record: JsonRecord, key: string): Option.Option<string> => {
|
|
22
|
-
const candidate = record[key]
|
|
23
|
-
return Option.fromNullable(typeof candidate === "string" ? candidate : null)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const pickRecord = (record: JsonRecord, key: string): Option.Option<JsonRecord> =>
|
|
27
|
-
pipe(
|
|
28
|
-
record[key],
|
|
29
|
-
Option.fromNullable,
|
|
30
|
-
Option.filter((value) => isJsonRecord(value))
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
const extractCwd = (record: JsonRecord): Option.Option<string> =>
|
|
34
|
-
pipe(
|
|
35
|
-
pickString(record, "cwd"),
|
|
36
|
-
Option.orElse(() =>
|
|
37
|
-
pipe(
|
|
38
|
-
pickRecord(record, "payload"),
|
|
39
|
-
Option.flatMap((payload) => pickString(payload, "cwd"))
|
|
40
|
-
)
|
|
41
|
-
)
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
const toMetadata = (value: JsonValue): RecordMetadata => {
|
|
45
|
-
if (!isJsonRecord(value)) {
|
|
46
|
-
return { cwd: Option.none() }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
cwd: extractCwd(value)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const cwdMatches = (metadata: RecordMetadata, locator: ProjectLocator): boolean =>
|
|
55
|
-
Option.exists(metadata.cwd, (cwdValue) => locator.isWithinRoot(cwdValue))
|
|
56
|
-
|
|
57
|
-
const metadataMatches = (
|
|
58
|
-
metadata: RecordMetadata,
|
|
59
|
-
locator: ProjectLocator
|
|
60
|
-
): boolean => cwdMatches(metadata, locator)
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Builds a locator for matching project-scoped metadata.
|
|
64
|
-
*
|
|
65
|
-
* @param normalizedCwd - Pre-normalized cwd of the project root.
|
|
66
|
-
* @param isWithinRoot - Pure predicate for checking candidate cwd membership.
|
|
67
|
-
* @returns Locator values for project comparisons.
|
|
68
|
-
*
|
|
69
|
-
* @pure true
|
|
70
|
-
* @invariant normalizedCwd is absolute and stable for equal inputs
|
|
71
|
-
* @complexity O(1) time / O(1) space
|
|
72
|
-
*/
|
|
73
|
-
// CHANGE: bundle normalized root and pure membership predicate in a locator
|
|
74
|
-
// WHY: keep matching invariants in CORE and leave path normalization in SHELL
|
|
75
|
-
// QUOTE(TZ): "FUNCTIONAL CORE, IMPERATIVE SHELL"
|
|
76
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
77
|
-
// SOURCE: n/a
|
|
78
|
-
// FORMAT THEOREM: forall c: locator(c) -> membership(c)
|
|
79
|
-
// PURITY: CORE
|
|
80
|
-
// EFFECT: n/a
|
|
81
|
-
// INVARIANT: normalizedCwd is absolute and stable for equal inputs
|
|
82
|
-
// COMPLEXITY: O(1)/O(1)
|
|
83
|
-
export const buildProjectLocator = (
|
|
84
|
-
normalizedCwd: string,
|
|
85
|
-
isWithinRoot: (candidate: string) => boolean
|
|
86
|
-
): ProjectLocator => ({
|
|
87
|
-
normalizedCwd,
|
|
88
|
-
isWithinRoot
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Checks whether a parsed JSON value contains project metadata.
|
|
93
|
-
*
|
|
94
|
-
* @param value - Parsed JSON value from a .jsonl line.
|
|
95
|
-
* @param locator - Normalized project locator.
|
|
96
|
-
* @returns True when cwd matches the project root.
|
|
97
|
-
*
|
|
98
|
-
* @pure true
|
|
99
|
-
* @invariant valueMatchesProject implies metadataMatches for extracted metadata
|
|
100
|
-
* @complexity O(k) where k = number of metadata fields read
|
|
101
|
-
*/
|
|
102
|
-
// CHANGE: restrict project matching to cwd-only semantics
|
|
103
|
-
// WHY: align with project-local path matching requirement
|
|
104
|
-
// QUOTE(TZ): "каждая функция — теорема"
|
|
105
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
106
|
-
// SOURCE: n/a
|
|
107
|
-
// FORMAT THEOREM: forall v: JsonValue -> matches(v, locator) <-> metadataMatches(v)
|
|
108
|
-
// PURITY: CORE
|
|
109
|
-
// EFFECT: n/a
|
|
110
|
-
// INVARIANT: metadataMatches uses cwd evidence only
|
|
111
|
-
// COMPLEXITY: O(k)/O(1)
|
|
112
|
-
export const valueMatchesProject = (
|
|
113
|
-
value: JsonValue,
|
|
114
|
-
locator: ProjectLocator
|
|
115
|
-
): boolean => metadataMatches(toMetadata(value), locator)
|
package/src/shell/cli.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { Effect } from "effect"
|
|
2
|
-
|
|
3
|
-
import { RuntimeEnv } from "./services/runtime-env.js"
|
|
4
|
-
import type { SyncOptions } from "./sync/types.js"
|
|
5
|
-
|
|
6
|
-
type CliKey = Exclude<keyof SyncOptions, "cwd">
|
|
7
|
-
|
|
8
|
-
const flagMap = new Map<string, CliKey>([
|
|
9
|
-
["--project-root", "projectRoot"],
|
|
10
|
-
["-r", "projectRoot"],
|
|
11
|
-
["--source", "sourceDir"],
|
|
12
|
-
["-s", "sourceDir"],
|
|
13
|
-
["--dest", "destinationDir"],
|
|
14
|
-
["-d", "destinationDir"],
|
|
15
|
-
["--project-url", "repositoryUrlOverride"],
|
|
16
|
-
["--project-name", "repositoryUrlOverride"],
|
|
17
|
-
["--meta-root", "metaRoot"],
|
|
18
|
-
["--qwen-source", "qwenSourceDir"],
|
|
19
|
-
["--claude-projects", "claudeProjectsRoot"]
|
|
20
|
-
])
|
|
21
|
-
|
|
22
|
-
const parseArgs = (args: ReadonlyArray<string>, cwd: string): SyncOptions => {
|
|
23
|
-
let result: SyncOptions = { cwd }
|
|
24
|
-
|
|
25
|
-
let index = 0
|
|
26
|
-
while (index < args.length) {
|
|
27
|
-
const arg = args[index]
|
|
28
|
-
if (arg === undefined) {
|
|
29
|
-
index += 1
|
|
30
|
-
continue
|
|
31
|
-
}
|
|
32
|
-
const key = flagMap.get(arg)
|
|
33
|
-
if (key === undefined) {
|
|
34
|
-
index += 1
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const value = args[index + 1]
|
|
39
|
-
if (value !== undefined) {
|
|
40
|
-
result = { ...result, [key]: value }
|
|
41
|
-
index += 2
|
|
42
|
-
continue
|
|
43
|
-
}
|
|
44
|
-
index += 1
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return result
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Reads CLI arguments and builds SyncOptions.
|
|
52
|
-
*
|
|
53
|
-
* @returns Effect with resolved SyncOptions.
|
|
54
|
-
*
|
|
55
|
-
* @pure false - reads process argv/cwd via RuntimeEnv
|
|
56
|
-
* @effect RuntimeEnv
|
|
57
|
-
* @invariant options.cwd is always defined; projectRoot overrides cwd for matching
|
|
58
|
-
* @complexity O(n) where n = |args|
|
|
59
|
-
*/
|
|
60
|
-
// CHANGE: parse CLI flags with optional project root override
|
|
61
|
-
// WHY: allow matching against repo root while running from subpackages
|
|
62
|
-
// QUOTE(TZ): "передай root-path на основную папку"
|
|
63
|
-
// REF: user-2026-01-19-project-root
|
|
64
|
-
// SOURCE: n/a
|
|
65
|
-
// FORMAT THEOREM: forall a: parse(a) -> SyncOptions
|
|
66
|
-
// PURITY: SHELL
|
|
67
|
-
// EFFECT: Effect<SyncOptions, never, RuntimeEnv>
|
|
68
|
-
// INVARIANT: unknown flags are ignored
|
|
69
|
-
// COMPLEXITY: O(n)/O(1)
|
|
70
|
-
export const readSyncOptions = Effect.gen(function*(_) {
|
|
71
|
-
const env = yield* _(RuntimeEnv)
|
|
72
|
-
const argv = yield* _(env.argv)
|
|
73
|
-
const cwd = yield* _(env.cwd)
|
|
74
|
-
return parseArgs(argv.slice(2), cwd)
|
|
75
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { Context, Effect, Layer, pipe } from "effect"
|
|
2
|
-
|
|
3
|
-
export class CryptoService extends Context.Tag("CryptoService")<
|
|
4
|
-
CryptoService,
|
|
5
|
-
{
|
|
6
|
-
readonly sha256: (value: string) => Effect.Effect<string, CryptoError>
|
|
7
|
-
}
|
|
8
|
-
>() {}
|
|
9
|
-
|
|
10
|
-
export interface CryptoError {
|
|
11
|
-
readonly _tag: "CryptoError"
|
|
12
|
-
readonly reason: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const cryptoError = (reason: string): CryptoError => ({
|
|
16
|
-
_tag: "CryptoError",
|
|
17
|
-
reason
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const toHex = (buffer: ArrayBuffer): string =>
|
|
21
|
-
[...new Uint8Array(buffer)]
|
|
22
|
-
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
23
|
-
.join("")
|
|
24
|
-
|
|
25
|
-
const digestSha256 = (value: string): Effect.Effect<string, CryptoError> =>
|
|
26
|
-
pipe(
|
|
27
|
-
Effect.tryPromise({
|
|
28
|
-
try: () => {
|
|
29
|
-
const crypto = globalThis.crypto
|
|
30
|
-
const bytes = new TextEncoder().encode(value)
|
|
31
|
-
return crypto.subtle.digest("SHA-256", bytes)
|
|
32
|
-
},
|
|
33
|
-
catch: (error) => cryptoError(error instanceof Error ? error.message : "Crypto digest failed")
|
|
34
|
-
}),
|
|
35
|
-
Effect.map((buffer) => toHex(buffer))
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
// CHANGE: isolate hashing behind a service for deterministic testing
|
|
39
|
-
// WHY: avoid direct crypto usage in shell logic
|
|
40
|
-
// QUOTE(TZ): "Внешние зависимости: только через типизированные интерфейсы"
|
|
41
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
42
|
-
// SOURCE: n/a
|
|
43
|
-
// FORMAT THEOREM: forall s: sha256(s) -> hex(s)
|
|
44
|
-
// PURITY: SHELL
|
|
45
|
-
// EFFECT: Effect<CryptoService, never, never>
|
|
46
|
-
// INVARIANT: sha256 output length = 64
|
|
47
|
-
// COMPLEXITY: O(n)/O(1)
|
|
48
|
-
export const CryptoServiceLive = Layer.succeed(CryptoService, {
|
|
49
|
-
sha256: (value) => digestSha256(value)
|
|
50
|
-
})
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import * as FileSystem from "@effect/platform/FileSystem"
|
|
2
|
-
import * as Path from "@effect/platform/Path"
|
|
3
|
-
import { Context, Effect, Layer, pipe } from "effect"
|
|
4
|
-
|
|
5
|
-
import { type SyncError, syncError } from "../sync/types.js"
|
|
6
|
-
|
|
7
|
-
export type DirectoryEntryKind = "file" | "directory" | "other"
|
|
8
|
-
|
|
9
|
-
export interface DirectoryEntry {
|
|
10
|
-
readonly name: string
|
|
11
|
-
readonly path: string
|
|
12
|
-
readonly kind: DirectoryEntryKind
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export class FileSystemService extends Context.Tag("FileSystemService")<
|
|
16
|
-
FileSystemService,
|
|
17
|
-
{
|
|
18
|
-
readonly readFileString: (pathValue: string) => Effect.Effect<string, SyncError>
|
|
19
|
-
readonly readDirectory: (
|
|
20
|
-
pathValue: string
|
|
21
|
-
) => Effect.Effect<ReadonlyArray<DirectoryEntry>, SyncError>
|
|
22
|
-
readonly makeDirectory: (pathValue: string) => Effect.Effect<void, SyncError>
|
|
23
|
-
readonly copyFile: (
|
|
24
|
-
sourcePath: string,
|
|
25
|
-
destinationPath: string
|
|
26
|
-
) => Effect.Effect<void, SyncError>
|
|
27
|
-
readonly exists: (pathValue: string) => Effect.Effect<boolean, SyncError>
|
|
28
|
-
}
|
|
29
|
-
>() {}
|
|
30
|
-
|
|
31
|
-
const forEach = Effect.forEach
|
|
32
|
-
|
|
33
|
-
const resolveEntryPath = (
|
|
34
|
-
path: Path.Path,
|
|
35
|
-
root: string,
|
|
36
|
-
entry: string
|
|
37
|
-
): string => (path.isAbsolute(entry) ? entry : path.join(root, entry))
|
|
38
|
-
|
|
39
|
-
const entryKindFromInfo = (
|
|
40
|
-
info: FileSystem.File.Info
|
|
41
|
-
): DirectoryEntryKind => {
|
|
42
|
-
if (info.type === "Directory") {
|
|
43
|
-
return "directory"
|
|
44
|
-
}
|
|
45
|
-
if (info.type === "File") {
|
|
46
|
-
return "file"
|
|
47
|
-
}
|
|
48
|
-
return "other"
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const toDirectoryEntry = (
|
|
52
|
-
path: Path.Path,
|
|
53
|
-
entryPath: string,
|
|
54
|
-
info: FileSystem.File.Info
|
|
55
|
-
): DirectoryEntry => ({
|
|
56
|
-
name: path.basename(entryPath),
|
|
57
|
-
path: entryPath,
|
|
58
|
-
kind: entryKindFromInfo(info)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const readEntry = (
|
|
62
|
-
fs: FileSystem.FileSystem,
|
|
63
|
-
path: Path.Path,
|
|
64
|
-
entryPath: string
|
|
65
|
-
): Effect.Effect<DirectoryEntry, SyncError> =>
|
|
66
|
-
pipe(
|
|
67
|
-
fs.stat(entryPath),
|
|
68
|
-
Effect.map((info) => toDirectoryEntry(path, entryPath, info)),
|
|
69
|
-
Effect.mapError(() => syncError(entryPath, "Cannot read directory entry"))
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
const resolveEntry = (
|
|
73
|
-
fs: FileSystem.FileSystem,
|
|
74
|
-
path: Path.Path,
|
|
75
|
-
root: string,
|
|
76
|
-
entry: string
|
|
77
|
-
): Effect.Effect<DirectoryEntry, SyncError> => {
|
|
78
|
-
const entryPath = resolveEntryPath(path, root, entry)
|
|
79
|
-
return readEntry(fs, path, entryPath)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// CHANGE: wrap filesystem access behind a service for typed errors and testing
|
|
83
|
-
// WHY: enforce shell boundary and avoid raw fs usage in logic
|
|
84
|
-
// QUOTE(TZ): "Внешние зависимости: только через типизированные интерфейсы"
|
|
85
|
-
// REF: user-2026-01-19-sync-rewrite
|
|
86
|
-
// SOURCE: n/a
|
|
87
|
-
// FORMAT THEOREM: forall p: exists(p) -> readable(p)
|
|
88
|
-
// PURITY: SHELL
|
|
89
|
-
// EFFECT: Effect<FileSystemService, SyncError, never>
|
|
90
|
-
// INVARIANT: readDirectory returns absolute entry paths
|
|
91
|
-
// COMPLEXITY: O(n)/O(n)
|
|
92
|
-
export const FileSystemLive = Layer.effect(
|
|
93
|
-
FileSystemService,
|
|
94
|
-
Effect.gen(function*(_) {
|
|
95
|
-
const fs = yield* _(FileSystem.FileSystem)
|
|
96
|
-
const path = yield* _(Path.Path)
|
|
97
|
-
|
|
98
|
-
const readFileString = (pathValue: string): Effect.Effect<string, SyncError> =>
|
|
99
|
-
pipe(
|
|
100
|
-
fs.readFileString(pathValue, "utf8"),
|
|
101
|
-
Effect.mapError(() => syncError(pathValue, "Cannot read file"))
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const readDirectory = (
|
|
105
|
-
pathValue: string
|
|
106
|
-
): Effect.Effect<ReadonlyArray<DirectoryEntry>, SyncError> =>
|
|
107
|
-
pipe(
|
|
108
|
-
fs.readDirectory(pathValue),
|
|
109
|
-
Effect.mapError(() => syncError(pathValue, "Cannot read directory")),
|
|
110
|
-
Effect.flatMap((entries) => forEach(entries, (entry) => resolveEntry(fs, path, pathValue, entry)))
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
const makeDirectory = (pathValue: string): Effect.Effect<void, SyncError> =>
|
|
114
|
-
pipe(
|
|
115
|
-
fs.makeDirectory(pathValue, { recursive: true }),
|
|
116
|
-
Effect.mapError(() => syncError(pathValue, "Cannot create destination directory structure"))
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
const copyFile = (
|
|
120
|
-
sourcePath: string,
|
|
121
|
-
destinationPath: string
|
|
122
|
-
): Effect.Effect<void, SyncError> =>
|
|
123
|
-
pipe(
|
|
124
|
-
fs.copyFile(sourcePath, destinationPath),
|
|
125
|
-
Effect.mapError(() => syncError(sourcePath, "Cannot copy file into destination"))
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
const exists = (pathValue: string): Effect.Effect<boolean, SyncError> =>
|
|
129
|
-
pipe(
|
|
130
|
-
fs.exists(pathValue),
|
|
131
|
-
Effect.mapError(() => syncError(pathValue, "Cannot check path existence"))
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
readFileString,
|
|
136
|
-
readDirectory,
|
|
137
|
-
makeDirectory,
|
|
138
|
-
copyFile,
|
|
139
|
-
exists
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
)
|