@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
package/package.json
CHANGED
|
@@ -1,62 +1,93 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prover-coder-ai/context-doc",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "",
|
|
5
|
-
"
|
|
6
|
-
"scripts": {
|
|
7
|
-
"start": "tsx src/main.ts",
|
|
8
|
-
"build": "tsc -p tsconfig.build.json",
|
|
9
|
-
"lint": "npx --no-install @ton-ai-core/vibecode-linter src/",
|
|
10
|
-
"test": "npx --no-install @ton-ai-core/vibecode-linter tests/ && vitest run --passWithNoTests",
|
|
11
|
-
"prepare": "git config core.hooksPath .githooks && chmod +x .githooks/pre-commit",
|
|
12
|
-
"sync:knowledge": "npm run build && PKG=prover-coder-ai-context-doc-$(node -p \"require('./package.json').version\").tgz && rm -f \"$PKG\" && npm pack >/dev/null && npx -y -p \"./$PKG\" context-doc",
|
|
13
|
-
"release": "npm run lint && npm run build && npm version patch && npm publish --access public"
|
|
14
|
-
},
|
|
3
|
+
"version": "1.0.13",
|
|
4
|
+
"description": "Minimal Vite-powered TypeScript console starter using Effect",
|
|
5
|
+
"main": "dist/main.js",
|
|
15
6
|
"bin": {
|
|
16
|
-
"context-doc": "
|
|
7
|
+
"context-doc": "bin/context-doc.js"
|
|
8
|
+
},
|
|
9
|
+
"directories": {
|
|
10
|
+
"doc": "doc"
|
|
17
11
|
},
|
|
18
12
|
"repository": {
|
|
19
13
|
"type": "git",
|
|
20
|
-
"url": "git+https://github.com/
|
|
14
|
+
"url": "git+https://github.com/ProverCoderAI/context-doc.git"
|
|
21
15
|
},
|
|
22
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"effect",
|
|
18
|
+
"typescript",
|
|
19
|
+
"vite",
|
|
20
|
+
"console"
|
|
21
|
+
],
|
|
23
22
|
"author": "",
|
|
24
23
|
"license": "ISC",
|
|
24
|
+
"type": "module",
|
|
25
25
|
"bugs": {
|
|
26
|
-
"url": "https://github.com/
|
|
26
|
+
"url": "https://github.com/ProverCoderAI/context-doc/issues"
|
|
27
27
|
},
|
|
28
|
-
"homepage": "https://github.com/
|
|
29
|
-
"files": [
|
|
30
|
-
"dist",
|
|
31
|
-
"src",
|
|
32
|
-
"doc",
|
|
33
|
-
"README.md",
|
|
34
|
-
"tsconfig.build.json",
|
|
35
|
-
"vitest.config.ts"
|
|
36
|
-
],
|
|
28
|
+
"homepage": "https://github.com/ProverCoderAI/context-doc#readme",
|
|
37
29
|
"publishConfig": {
|
|
38
30
|
"access": "public"
|
|
39
31
|
},
|
|
40
32
|
"dependencies": {
|
|
33
|
+
"@effect/cli": "^0.73.0",
|
|
34
|
+
"@effect/cluster": "^0.56.1",
|
|
35
|
+
"@effect/experimental": "^0.58.0",
|
|
36
|
+
"@effect/platform": "^0.94.1",
|
|
37
|
+
"@effect/platform-node": "^0.104.0",
|
|
38
|
+
"@effect/printer": "^0.47.0",
|
|
39
|
+
"@effect/printer-ansi": "^0.47.0",
|
|
40
|
+
"@effect/rpc": "^0.73.0",
|
|
41
41
|
"@effect/schema": "^0.75.5",
|
|
42
|
-
"effect": "^
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
42
|
+
"@effect/sql": "^0.49.0",
|
|
43
|
+
"@effect/typeclass": "^0.38.0",
|
|
44
|
+
"@effect/workflow": "^0.16.0",
|
|
45
|
+
"effect": "^3.19.14",
|
|
46
|
+
"ts-morph": "^27.0.2"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "^2.3.
|
|
49
|
-
"@
|
|
50
|
-
"@
|
|
51
|
-
"@
|
|
49
|
+
"@biomejs/biome": "^2.3.11",
|
|
50
|
+
"@effect/eslint-plugin": "^0.3.2",
|
|
51
|
+
"@effect/language-service": "latest",
|
|
52
|
+
"@effect/vitest": "^0.27.0",
|
|
53
|
+
"@eslint-community/eslint-plugin-eslint-comments": "^4.6.0",
|
|
54
|
+
"@eslint/compat": "2.0.1",
|
|
55
|
+
"@eslint/eslintrc": "3.3.3",
|
|
56
|
+
"@eslint/js": "9.39.2",
|
|
57
|
+
"@prover-coder-ai/eslint-plugin-suggest-members": "^0.0.13",
|
|
52
58
|
"@ton-ai-core/vibecode-linter": "^1.0.6",
|
|
53
|
-
"@types/node": "^24.10.
|
|
54
|
-
"eslint": "^
|
|
55
|
-
"eslint
|
|
56
|
-
"
|
|
57
|
-
"
|
|
59
|
+
"@types/node": "^24.10.9",
|
|
60
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
61
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
62
|
+
"@vitest/coverage-v8": "^4.0.17",
|
|
63
|
+
"@vitest/eslint-plugin": "^1.6.6",
|
|
64
|
+
"eslint": "^9.39.2",
|
|
65
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
66
|
+
"eslint-plugin-codegen": "0.34.1",
|
|
67
|
+
"eslint-plugin-import": "^2.32.0",
|
|
68
|
+
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
69
|
+
"eslint-plugin-sonarjs": "^3.0.5",
|
|
70
|
+
"eslint-plugin-sort-destructure-keys": "^2.0.0",
|
|
71
|
+
"eslint-plugin-unicorn": "^62.0.0",
|
|
72
|
+
"fast-check": "^4.5.3",
|
|
73
|
+
"globals": "^17.0.0",
|
|
74
|
+
"jscpd": "^4.0.7",
|
|
58
75
|
"typescript": "^5.9.3",
|
|
59
|
-
"typescript-eslint": "^8.
|
|
60
|
-
"
|
|
76
|
+
"typescript-eslint": "^8.53.0",
|
|
77
|
+
"vite": "^7.3.1",
|
|
78
|
+
"vite-tsconfig-paths": "^6.0.4",
|
|
79
|
+
"vitest": "^4.0.17"
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "vite build --ssr src/app/main.ts",
|
|
83
|
+
"dev": "vite build --watch --ssr src/app/main.ts",
|
|
84
|
+
"lint": "npx @ton-ai-core/vibecode-linter src/",
|
|
85
|
+
"lint:tests": "npx @ton-ai-core/vibecode-linter tests/",
|
|
86
|
+
"lint:effect": "npx eslint --config eslint.effect-ts-check.config.mjs .",
|
|
87
|
+
"check": "pnpm run typecheck",
|
|
88
|
+
"prestart": "pnpm run build",
|
|
89
|
+
"start": "node dist/main.js -- --project-root ../..",
|
|
90
|
+
"test": "pnpm run lint:tests && vitest run",
|
|
91
|
+
"typecheck": "tsc --noEmit"
|
|
61
92
|
}
|
|
62
|
-
}
|
|
93
|
+
}
|
package/src/app/main.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
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)
|
|
@@ -0,0 +1,32 @@
|
|
|
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
CHANGED
|
@@ -1,139 +1,115 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { Option, pipe } from "effect";
|
|
1
|
+
import { Option, pipe } from "effect"
|
|
3
2
|
|
|
4
|
-
type JsonPrimitive = string | number | boolean | null
|
|
5
|
-
type JsonValue = JsonPrimitive |
|
|
6
|
-
interface JsonRecord {
|
|
7
|
-
|
|
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
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export interface ProjectLocator {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
readonly normalizedCwd: string
|
|
11
|
+
readonly isWithinRoot: (candidate: string) => boolean
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
interface RecordMetadata {
|
|
16
|
-
|
|
17
|
-
readonly cwd: Option.Option<string>;
|
|
15
|
+
readonly cwd: Option.Option<string>
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
const isJsonRecord = (value: JsonValue): value is JsonRecord =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const normalizeRepositoryUrl = (value: string): string =>
|
|
24
|
-
pipe(
|
|
25
|
-
value.trim(),
|
|
26
|
-
(trimmed) => trimmed.replace(/^git\+/, ""),
|
|
27
|
-
(withoutPrefix) => withoutPrefix.replace(/\.git$/, ""),
|
|
28
|
-
(withoutGitSuffix) =>
|
|
29
|
-
withoutGitSuffix.replace(/^git@github\.com:/, "https://github.com/"),
|
|
30
|
-
(withoutSsh) =>
|
|
31
|
-
withoutSsh.replace(/^ssh:\/\/git@github\.com\//, "https://github.com/"),
|
|
32
|
-
(normalized) => normalized.toLowerCase(),
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
const normalizeCwd = (value: string): string => path.resolve(value);
|
|
19
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
36
20
|
|
|
37
21
|
const pickString = (record: JsonRecord, key: string): Option.Option<string> => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const pickRecord = (
|
|
43
|
-
record: JsonRecord,
|
|
44
|
-
key: string,
|
|
45
|
-
): Option.Option<JsonRecord> =>
|
|
46
|
-
pipe(record[key], Option.fromNullable, Option.filter(isJsonRecord));
|
|
47
|
-
|
|
48
|
-
const pickGitRepository = (record: JsonRecord): Option.Option<string> =>
|
|
49
|
-
pipe(
|
|
50
|
-
pickString(record, "repository_url"),
|
|
51
|
-
Option.orElse(() => pickString(record, "repositoryUrl")),
|
|
52
|
-
);
|
|
22
|
+
const candidate = record[key]
|
|
23
|
+
return Option.fromNullable(typeof candidate === "string" ? candidate : null)
|
|
24
|
+
}
|
|
53
25
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
pickRecord(record, "payload"),
|
|
61
|
-
Option.flatMap((payload) =>
|
|
62
|
-
pipe(pickRecord(payload, "git"), Option.flatMap(pickGitRepository)),
|
|
63
|
-
),
|
|
64
|
-
),
|
|
65
|
-
),
|
|
66
|
-
);
|
|
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
|
+
)
|
|
67
32
|
|
|
68
33
|
const extractCwd = (record: JsonRecord): Option.Option<string> =>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
)
|
|
78
43
|
|
|
79
44
|
const toMetadata = (value: JsonValue): RecordMetadata => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
repositoryUrl: extractRepository(value),
|
|
86
|
-
cwd: extractCwd(value),
|
|
87
|
-
};
|
|
88
|
-
};
|
|
45
|
+
if (!isJsonRecord(value)) {
|
|
46
|
+
return { cwd: Option.none() }
|
|
47
|
+
}
|
|
89
48
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
normalizedRepositoryUrl: normalizeRepositoryUrl(repositoryUrl),
|
|
95
|
-
normalizedCwd: normalizeCwd(cwd),
|
|
96
|
-
});
|
|
49
|
+
return {
|
|
50
|
+
cwd: extractCwd(value)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
97
53
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
return Option.some(JSON.parse(line));
|
|
101
|
-
} catch {
|
|
102
|
-
return Option.none();
|
|
103
|
-
}
|
|
104
|
-
};
|
|
54
|
+
const cwdMatches = (metadata: RecordMetadata, locator: ProjectLocator): boolean =>
|
|
55
|
+
Option.exists(metadata.cwd, (cwdValue) => locator.isWithinRoot(cwdValue))
|
|
105
56
|
|
|
106
57
|
const metadataMatches = (
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
): boolean =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
118
83
|
export const buildProjectLocator = (
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
): ProjectLocator =>
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
CHANGED
|
@@ -1,7 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { buildSyncProgram, parseArgs } from "./syncKnowledge.js";
|
|
1
|
+
import { Effect } from "effect"
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
import { RuntimeEnv } from "./services/runtime-env.js"
|
|
4
|
+
import type { SyncOptions } from "./sync/types.js"
|
|
6
5
|
|
|
7
|
-
|
|
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
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
})
|