@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.
Files changed (49) hide show
  1. package/.jscpd.json +16 -0
  2. package/CHANGELOG.md +13 -0
  3. package/bin/context-doc.js +12 -0
  4. package/biome.json +37 -0
  5. package/eslint.config.mts +305 -0
  6. package/eslint.effect-ts-check.config.mjs +220 -0
  7. package/linter.config.json +33 -0
  8. package/package.json +72 -41
  9. package/src/app/main.ts +29 -0
  10. package/src/app/program.ts +32 -0
  11. package/src/core/knowledge.ts +93 -117
  12. package/src/shell/cli.ts +73 -5
  13. package/src/shell/services/crypto.ts +50 -0
  14. package/src/shell/services/file-system.ts +142 -0
  15. package/src/shell/services/runtime-env.ts +54 -0
  16. package/src/shell/sync/index.ts +35 -0
  17. package/src/shell/sync/shared.ts +229 -0
  18. package/src/shell/sync/sources/claude.ts +54 -0
  19. package/src/shell/sync/sources/codex.ts +261 -0
  20. package/src/shell/sync/sources/qwen.ts +97 -0
  21. package/src/shell/sync/types.ts +47 -0
  22. package/tests/core/knowledge.test.ts +95 -0
  23. package/tests/shell/cli-pack.test.ts +176 -0
  24. package/tests/shell/sync-knowledge.test.ts +203 -0
  25. package/tests/support/fs-helpers.ts +50 -0
  26. package/tsconfig.json +19 -0
  27. package/vite.config.ts +32 -0
  28. package/vitest.config.ts +70 -66
  29. package/README.md +0 -42
  30. package/dist/core/greeting.js +0 -25
  31. package/dist/core/knowledge.js +0 -49
  32. package/dist/main.js +0 -27
  33. package/dist/shell/claudeSync.js +0 -23
  34. package/dist/shell/cli.js +0 -5
  35. package/dist/shell/codexSync.js +0 -129
  36. package/dist/shell/qwenSync.js +0 -41
  37. package/dist/shell/syncKnowledge.js +0 -39
  38. package/dist/shell/syncShared.js +0 -49
  39. package/dist/shell/syncTypes.js +0 -1
  40. package/src/core/greeting.ts +0 -39
  41. package/src/main.ts +0 -49
  42. package/src/shell/claudeSync.ts +0 -55
  43. package/src/shell/codexSync.ts +0 -236
  44. package/src/shell/qwenSync.ts +0 -73
  45. package/src/shell/syncKnowledge.ts +0 -49
  46. package/src/shell/syncShared.ts +0 -94
  47. package/src/shell/syncTypes.ts +0 -30
  48. package/src/types/env.d.ts +0 -10
  49. 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.12",
4
- "description": "",
5
- "type": "module",
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": "dist/shell/cli.js"
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/prover-coder-ai/context-doc.git"
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/prover-coder-ai/context-doc/issues"
26
+ "url": "https://github.com/ProverCoderAI/context-doc/issues"
27
27
  },
28
- "homepage": "https://github.com/prover-coder-ai/context-doc#readme",
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": "^3.19.6",
43
- "ts-morph": "^27.0.2",
44
- "ts-pattern": "^5.9.0",
45
- "vite-tsconfig-paths": "^5.1.4"
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.7",
49
- "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
50
- "@eslint/js": "^9.39.1",
51
- "@ton-ai-core/eslint-plugin-suggest-members": "^1.6.17",
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.1",
54
- "eslint": "^9.39.1",
55
- "eslint-plugin-vitest": "^0.5.4",
56
- "globals": "^16.5.0",
57
- "tsx": "^4.20.6",
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.48.0",
60
- "vitest": "^4.0.14"
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
+ }
@@ -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
+ )
@@ -1,139 +1,115 @@
1
- import path from "node:path";
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 | readonly JsonValue[] | JsonRecord;
6
- interface JsonRecord {
7
- readonly [key: string]: JsonValue;
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
- readonly normalizedRepositoryUrl: string;
12
- readonly normalizedCwd: string;
10
+ readonly normalizedCwd: string
11
+ readonly isWithinRoot: (candidate: string) => boolean
13
12
  }
14
13
 
15
14
  interface RecordMetadata {
16
- readonly repositoryUrl: Option.Option<string>;
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
- typeof value === "object" && value !== null && !Array.isArray(value);
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
- const candidate = record[key];
39
- return typeof candidate === "string" ? Option.some(candidate) : Option.none();
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 extractRepository = (record: JsonRecord): Option.Option<string> =>
55
- pipe(
56
- pickRecord(record, "git"),
57
- Option.flatMap(pickGitRepository),
58
- Option.orElse(() =>
59
- pipe(
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
- pipe(
70
- pickString(record, "cwd"),
71
- Option.orElse(() =>
72
- pipe(
73
- pickRecord(record, "payload"),
74
- Option.flatMap((payload) => pickString(payload, "cwd")),
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
- if (!isJsonRecord(value)) {
81
- return { repositoryUrl: Option.none(), cwd: Option.none() };
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
- const normalizeLocator = (
91
- repositoryUrl: string,
92
- cwd: string,
93
- ): ProjectLocator => ({
94
- normalizedRepositoryUrl: normalizeRepositoryUrl(repositoryUrl),
95
- normalizedCwd: normalizeCwd(cwd),
96
- });
49
+ return {
50
+ cwd: extractCwd(value)
51
+ }
52
+ }
97
53
 
98
- const safeParseJson = (line: string): Option.Option<JsonValue> => {
99
- try {
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
- metadata: RecordMetadata,
108
- locator: ProjectLocator,
109
- ): boolean => {
110
- const cwdMatches = Option.exists(metadata.cwd, (cwdValue) => {
111
- const normalized = normalizeCwd(cwdValue);
112
- return normalized === locator.normalizedCwd;
113
- });
114
-
115
- return cwdMatches;
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
- repositoryUrl: string,
120
- cwd: string,
121
- ): ProjectLocator => normalizeLocator(repositoryUrl, cwd);
122
-
123
- export const linesMatchProject = (
124
- lines: readonly string[],
125
- locator: ProjectLocator,
126
- ): boolean =>
127
- lines.some((line) => {
128
- const trimmed = line.trim();
129
-
130
- if (trimmed.length === 0) {
131
- return false;
132
- }
133
-
134
- return pipe(
135
- safeParseJson(trimmed),
136
- Option.map(toMetadata),
137
- Option.exists((metadata) => metadataMatches(metadata, locator)),
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
- #!/usr/bin/env node
2
- import { Effect } from "effect";
3
- import { buildSyncProgram, parseArgs } from "./syncKnowledge.js";
1
+ import { Effect } from "effect"
4
2
 
5
- const program = buildSyncProgram(parseArgs());
3
+ import { RuntimeEnv } from "./services/runtime-env.js"
4
+ import type { SyncOptions } from "./sync/types.js"
6
5
 
7
- Effect.runSync(program);
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
+ })