pi-jscpd 0.1.0

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/CHANGELOG.md +99 -0
  2. package/CONTRIBUTING.md +144 -0
  3. package/LICENSE +21 -0
  4. package/README.md +231 -0
  5. package/SECURITY.md +93 -0
  6. package/docs/automatic-checkpoint.md +235 -0
  7. package/docs/compatibility.md +119 -0
  8. package/docs/effect-architecture.md +128 -0
  9. package/docs/fallow-coexistence.md +120 -0
  10. package/docs/overlay-interaction.md +347 -0
  11. package/docs/release.md +115 -0
  12. package/package.json +86 -0
  13. package/scripts/check-compatibility.mjs +103 -0
  14. package/skills/jscpd/SKILL.md +90 -0
  15. package/src/acknowledgements.ts +268 -0
  16. package/src/automatic.ts +396 -0
  17. package/src/baseline.ts +400 -0
  18. package/src/capability.ts +569 -0
  19. package/src/changed-files.ts +372 -0
  20. package/src/changed.ts +548 -0
  21. package/src/clone-identity.ts +373 -0
  22. package/src/config.ts +414 -0
  23. package/src/contract.ts +39 -0
  24. package/src/dispatch.ts +90 -0
  25. package/src/effect/clock.ts +10 -0
  26. package/src/effect/errors.ts +311 -0
  27. package/src/effect/filesystem.ts +240 -0
  28. package/src/effect/runtime-boundary.ts +25 -0
  29. package/src/effect/runtime-contract.ts +18 -0
  30. package/src/effect/services.ts +131 -0
  31. package/src/extension.ts +708 -0
  32. package/src/fallow.ts +479 -0
  33. package/src/finding-presentation.ts +73 -0
  34. package/src/index.ts +8 -0
  35. package/src/jscpd-report.ts +819 -0
  36. package/src/jscpd.ts +748 -0
  37. package/src/overlay.ts +1166 -0
  38. package/src/parser.ts +189 -0
  39. package/src/path-utils.ts +44 -0
  40. package/src/presentation.ts +232 -0
  41. package/src/process.ts +425 -0
  42. package/src/registry.ts +102 -0
  43. package/src/scan.ts +441 -0
  44. package/src/scheduler.ts +434 -0
  45. package/src/session-state.ts +229 -0
  46. package/src/status.ts +534 -0
  47. package/src/types.ts +334 -0
  48. package/src/value-utils.ts +14 -0
  49. package/src/verification.ts +220 -0
@@ -0,0 +1,311 @@
1
+ import { Data } from "effect";
2
+ import type {
3
+ JscpdChangedUnavailableReason,
4
+ JscpdScanFailureReason,
5
+ JscpdUnavailableReason,
6
+ } from "../types.js";
7
+
8
+ /** Stable tags for expected operational failures across the Effect migration. */
9
+ export const JSCPD_EXPECTED_ERROR_TAGS = Object.freeze([
10
+ "JscpdAnalyzerUnavailable",
11
+ "JscpdProcessFailure",
12
+ "JscpdFileSystemFailure",
13
+ "JscpdOperationCancelled",
14
+ "JscpdOperationTimedOut",
15
+ "JscpdLimitExceeded",
16
+ "JscpdInvalidInput",
17
+ "JscpdStaleOperation",
18
+ "JscpdWorkspaceFailure",
19
+ "JscpdPersistenceFailure",
20
+ "JscpdDeliveryFailure",
21
+ ] as const);
22
+
23
+ export type JscpdOperationStage = "probe" | "scan" | "baseline" | "lifecycle";
24
+
25
+ export class JscpdAnalyzerUnavailable extends Data.TaggedError("JscpdAnalyzerUnavailable")<{
26
+ readonly reason: "missing" | "incompatible";
27
+ }> {}
28
+
29
+ export class JscpdProcessFailure extends Data.TaggedError("JscpdProcessFailure")<{
30
+ readonly stage: "probe" | "scan";
31
+ readonly reason: "not-found" | "spawn" | "exit" | "termination";
32
+ }> {}
33
+
34
+ export class JscpdFileSystemFailure extends Data.TaggedError("JscpdFileSystemFailure")<{
35
+ readonly operation: "canonicalize" | "metadata" | "read" | "write" | "remove";
36
+ readonly reason: "missing" | "permission" | "not-regular" | "symlink" | "io";
37
+ }> {}
38
+
39
+ export class JscpdOperationCancelled extends Data.TaggedError("JscpdOperationCancelled")<{
40
+ readonly stage: JscpdOperationStage;
41
+ }> {}
42
+
43
+ export class JscpdOperationTimedOut extends Data.TaggedError("JscpdOperationTimedOut")<{
44
+ readonly stage: Exclude<JscpdOperationStage, "lifecycle">;
45
+ }> {}
46
+
47
+ export class JscpdLimitExceeded extends Data.TaggedError("JscpdLimitExceeded")<{
48
+ readonly subject: "process-output" | "report" | "configuration" | "path" | "message" | "state";
49
+ }> {}
50
+
51
+ export class JscpdInvalidInput extends Data.TaggedError("JscpdInvalidInput")<{
52
+ readonly subject: "configuration" | "path" | "process-request" | "report";
53
+ readonly reason: "unsafe" | "unsupported" | "malformed" | "incompatible" | "invalid";
54
+ }> {}
55
+
56
+ export class JscpdStaleOperation extends Data.TaggedError("JscpdStaleOperation")<{
57
+ readonly operation: "baseline" | "scan" | "automatic" | "verification";
58
+ }> {}
59
+
60
+ export class JscpdWorkspaceFailure extends Data.TaggedError("JscpdWorkspaceFailure")<{
61
+ readonly operation: "create" | "read-report" | "cleanup";
62
+ }> {}
63
+
64
+ export class JscpdPersistenceFailure extends Data.TaggedError("JscpdPersistenceFailure")<{
65
+ readonly operation: "restore" | "append";
66
+ }> {}
67
+
68
+ export class JscpdDeliveryFailure extends Data.TaggedError("JscpdDeliveryFailure")<{
69
+ readonly channel: "message" | "notification" | "status";
70
+ }> {}
71
+
72
+ export type JscpdExpectedError =
73
+ | JscpdAnalyzerUnavailable
74
+ | JscpdProcessFailure
75
+ | JscpdFileSystemFailure
76
+ | JscpdOperationCancelled
77
+ | JscpdOperationTimedOut
78
+ | JscpdLimitExceeded
79
+ | JscpdInvalidInput
80
+ | JscpdStaleOperation
81
+ | JscpdWorkspaceFailure
82
+ | JscpdPersistenceFailure
83
+ | JscpdDeliveryFailure;
84
+
85
+ export const JSCPD_EXPECTED_ERROR_MESSAGE_MAX_LENGTH = 240;
86
+
87
+ type PublicResultMapping =
88
+ | {
89
+ readonly disposition: "result";
90
+ readonly status: "unavailable";
91
+ readonly reason: JscpdUnavailableReason;
92
+ readonly message: string;
93
+ }
94
+ | {
95
+ readonly disposition: "result";
96
+ readonly status: "failed";
97
+ readonly reason: JscpdScanFailureReason;
98
+ readonly message: string;
99
+ }
100
+ | {
101
+ readonly disposition: "result";
102
+ readonly status: "changed-unavailable";
103
+ readonly reason: JscpdChangedUnavailableReason;
104
+ readonly message: string;
105
+ }
106
+ | {
107
+ readonly disposition: "diagnostic" | "defer" | "ignore";
108
+ readonly reason:
109
+ | "invalid-configuration"
110
+ | "stale-operation"
111
+ | "bounded-state"
112
+ | "delivery-failed"
113
+ | "persistence-failed";
114
+ readonly message: string;
115
+ };
116
+
117
+ /**
118
+ * Foundation mapping only. Production adapters continue using their current mappings until the
119
+ * owning migration slices replace them; this function fixes bounded public intent meanwhile.
120
+ */
121
+ export function mapJscpdExpectedError(error: JscpdExpectedError): PublicResultMapping {
122
+ const mapper = expectedErrorMappers[error._tag] as (
123
+ candidate: JscpdExpectedError,
124
+ ) => PublicResultMapping;
125
+ return mapper(error);
126
+ }
127
+
128
+ type ExpectedErrorMappers = {
129
+ readonly [Error in JscpdExpectedError as Error["_tag"]]: (error: Error) => PublicResultMapping;
130
+ };
131
+
132
+ const expectedErrorMappers = {
133
+ JscpdAnalyzerUnavailable: mapAnalyzerUnavailable,
134
+ JscpdProcessFailure: mapProcessFailure,
135
+ JscpdFileSystemFailure: mapFileSystemFailure,
136
+ JscpdOperationCancelled: mapOperationCancelled,
137
+ JscpdOperationTimedOut: mapOperationTimedOut,
138
+ JscpdLimitExceeded: mapLimitExceeded,
139
+ JscpdInvalidInput: mapInvalidInput,
140
+ JscpdStaleOperation: mapStaleOperation,
141
+ JscpdWorkspaceFailure: mapWorkspaceFailure,
142
+ JscpdPersistenceFailure: mapPersistenceFailure,
143
+ JscpdDeliveryFailure: mapDeliveryFailure,
144
+ } satisfies ExpectedErrorMappers;
145
+
146
+ function mapAnalyzerUnavailable(error: JscpdAnalyzerUnavailable): PublicResultMapping {
147
+ return publicResult(
148
+ "unavailable",
149
+ error.reason === "missing" ? "missing-binary" : "incompatible-version",
150
+ "jscpd is unavailable; reinstall pi-jscpd or use a compatible jscpd v5 installation.",
151
+ );
152
+ }
153
+
154
+ function mapProcessFailure(error: JscpdProcessFailure): PublicResultMapping {
155
+ return error.stage === "probe"
156
+ ? publicResult("unavailable", "probe-failed", "The jscpd probe failed safely.")
157
+ : publicResult("failed", "process-failed", "The jscpd process did not complete safely.");
158
+ }
159
+
160
+ function mapFileSystemFailure(error: JscpdFileSystemFailure): PublicResultMapping {
161
+ return error.operation === "read"
162
+ ? publicResult("failed", "missing-report", "The bounded jscpd report could not be read.")
163
+ : publicResult("failed", "process-failed", "A required bounded filesystem operation failed.");
164
+ }
165
+
166
+ function mapOperationCancelled(error: JscpdOperationCancelled): PublicResultMapping {
167
+ const mappings = {
168
+ probe: publicResult("unavailable", "probe-cancelled", "The jscpd probe was cancelled."),
169
+ scan: publicResult("failed", "scan-cancelled", "The jscpd scan was cancelled."),
170
+ baseline: publicResult(
171
+ "changed-unavailable",
172
+ "baseline-cancelled",
173
+ "The session baseline was cancelled; a later changed check can retry.",
174
+ ),
175
+ lifecycle: publicResult(
176
+ "changed-unavailable",
177
+ "baseline-cancelled",
178
+ "The session baseline was cancelled; a later changed check can retry.",
179
+ ),
180
+ } satisfies Record<JscpdOperationStage, PublicResultMapping>;
181
+ return mappings[error.stage];
182
+ }
183
+
184
+ function mapOperationTimedOut(error: JscpdOperationTimedOut): PublicResultMapping {
185
+ const mappings = {
186
+ probe: publicResult("unavailable", "probe-timed-out", "The jscpd probe timed out."),
187
+ scan: publicResult("failed", "scan-timed-out", "The jscpd scan timed out."),
188
+ baseline: publicResult(
189
+ "changed-unavailable",
190
+ "baseline-timed-out",
191
+ "The session baseline timed out; explicit project scans remain available.",
192
+ ),
193
+ } satisfies Record<JscpdOperationTimedOut["stage"], PublicResultMapping>;
194
+ return mappings[error.stage];
195
+ }
196
+
197
+ function mapLimitExceeded(error: JscpdLimitExceeded): PublicResultMapping {
198
+ if (error.subject === "configuration") {
199
+ return boundedDisposition(
200
+ "diagnostic",
201
+ "invalid-configuration",
202
+ "Oversized jscpd guardrail configuration was ignored.",
203
+ );
204
+ }
205
+ if (error.subject === "message" || error.subject === "state") {
206
+ return boundedDisposition("defer", "bounded-state", "Oversized advisory data was omitted.");
207
+ }
208
+ return publicResult(
209
+ "failed",
210
+ error.subject === "report" ? "invalid-report" : "process-failed",
211
+ "The jscpd operation exceeded a configured safety limit.",
212
+ );
213
+ }
214
+
215
+ function mapInvalidInput(error: JscpdInvalidInput): PublicResultMapping {
216
+ if (error.subject === "configuration") {
217
+ return boundedDisposition(
218
+ "diagnostic",
219
+ "invalid-configuration",
220
+ "Invalid jscpd guardrail configuration was ignored.",
221
+ );
222
+ }
223
+ if (error.subject === "path") return mapInvalidPath(error);
224
+ return error.subject === "process-request"
225
+ ? publicResult("failed", "process-failed", "The bounded process request was invalid.")
226
+ : mapInvalidReport(error);
227
+ }
228
+
229
+ function mapInvalidPath(error: JscpdInvalidInput): PublicResultMapping {
230
+ const unsupported = error.reason === "unsupported";
231
+ return publicResult(
232
+ "failed",
233
+ unsupported ? "unsupported-path" : "unsafe-path",
234
+ unsupported
235
+ ? "The requested scan path is unsupported."
236
+ : "The requested scan path is outside the project or unsafe.",
237
+ );
238
+ }
239
+
240
+ function mapInvalidReport(error: JscpdInvalidInput): PublicResultMapping {
241
+ const reasons = {
242
+ unsafe: "invalid-report",
243
+ unsupported: "invalid-report",
244
+ malformed: "malformed-report",
245
+ incompatible: "incompatible-report",
246
+ invalid: "invalid-report",
247
+ } as const satisfies Record<JscpdInvalidInput["reason"], JscpdScanFailureReason>;
248
+ return publicResult(
249
+ "failed",
250
+ reasons[error.reason],
251
+ "jscpd returned an invalid or incompatible report.",
252
+ );
253
+ }
254
+
255
+ function mapStaleOperation(_error: JscpdStaleOperation): PublicResultMapping {
256
+ return boundedDisposition(
257
+ "defer",
258
+ "stale-operation",
259
+ "A superseded jscpd result was discarded safely.",
260
+ );
261
+ }
262
+
263
+ function mapWorkspaceFailure(error: JscpdWorkspaceFailure): PublicResultMapping {
264
+ const reasons = {
265
+ create: "process-failed",
266
+ "read-report": "missing-report",
267
+ cleanup: "cleanup-failed",
268
+ } as const satisfies Record<JscpdWorkspaceFailure["operation"], JscpdScanFailureReason>;
269
+ const messages = {
270
+ create: "The temporary jscpd report workspace was unavailable.",
271
+ "read-report": "The temporary jscpd report workspace was unavailable.",
272
+ cleanup: "The temporary jscpd report workspace could not be confirmed clean.",
273
+ } as const satisfies Record<JscpdWorkspaceFailure["operation"], string>;
274
+ return publicResult("failed", reasons[error.operation], messages[error.operation]);
275
+ }
276
+
277
+ function mapPersistenceFailure(_error: JscpdPersistenceFailure): PublicResultMapping {
278
+ return boundedDisposition(
279
+ "ignore",
280
+ "persistence-failed",
281
+ "Advisory jscpd session state could not be persisted.",
282
+ );
283
+ }
284
+
285
+ function mapDeliveryFailure(_error: JscpdDeliveryFailure): PublicResultMapping {
286
+ return boundedDisposition(
287
+ "defer",
288
+ "delivery-failed",
289
+ "The advisory jscpd update could not be delivered and may be retried.",
290
+ );
291
+ }
292
+
293
+ function publicResult<
294
+ Status extends "unavailable" | "failed" | "changed-unavailable",
295
+ Reason extends JscpdUnavailableReason | JscpdScanFailureReason | JscpdChangedUnavailableReason,
296
+ >(status: Status, reason: Reason, message: string) {
297
+ return Object.freeze({ disposition: "result" as const, status, reason, message });
298
+ }
299
+
300
+ function boundedDisposition(
301
+ disposition: "diagnostic" | "defer" | "ignore",
302
+ reason:
303
+ | "invalid-configuration"
304
+ | "stale-operation"
305
+ | "bounded-state"
306
+ | "delivery-failed"
307
+ | "persistence-failed",
308
+ message: string,
309
+ ): PublicResultMapping {
310
+ return Object.freeze({ disposition, reason, message });
311
+ }
@@ -0,0 +1,240 @@
1
+ import { constants as fsConstants, type Stats } from "node:fs";
2
+ import type { FileHandle } from "node:fs/promises";
3
+ import { chmod, mkdtemp, open, realpath, rm, stat, writeFile } from "node:fs/promises";
4
+ import { Effect, Layer } from "effect";
5
+ import { JscpdFileSystemFailure, JscpdLimitExceeded, JscpdWorkspaceFailure } from "./errors.js";
6
+ import {
7
+ type JscpdBoundedReadRequest,
8
+ type JscpdFileMetadata,
9
+ JscpdFileSystem,
10
+ type JscpdFileSystemError,
11
+ } from "./services.js";
12
+
13
+ const MAX_FILESYSTEM_BYTES = 64 * 1_024 * 1_024;
14
+
15
+ /** Live bounded filesystem implementation shared by configuration and untrusted-data boundaries. */
16
+ export const jscpdFileSystemLive: JscpdFileSystem = {
17
+ canonicalize: (path) =>
18
+ Effect.tryPromise({
19
+ try: () => realpath(path),
20
+ catch: (error) => fileSystemFailure("canonicalize", error),
21
+ }),
22
+ metadata: (path) =>
23
+ Effect.tryPromise({
24
+ try: async () => metadataFrom(await stat(path)),
25
+ catch: (error) => fileSystemFailure("metadata", error),
26
+ }),
27
+ read: readBoundedFile,
28
+ write: (request) => {
29
+ if (!isValidByteBound(request.maxBytes) || request.bytes.byteLength > request.maxBytes) {
30
+ return Effect.fail(new JscpdLimitExceeded({ subject: "state" }));
31
+ }
32
+ return Effect.tryPromise({
33
+ try: () => writeFile(request.path, request.bytes, { mode: request.mode }),
34
+ catch: (error) => fileSystemFailure("write", error),
35
+ });
36
+ },
37
+ makeTempDirectory: makeSecureTempDirectory,
38
+ remove: (path, recursive) =>
39
+ Effect.tryPromise({
40
+ try: () =>
41
+ rm(path, {
42
+ recursive,
43
+ force: true,
44
+ maxRetries: recursive ? 2 : 0,
45
+ retryDelay: recursive ? 10 : 100,
46
+ }),
47
+ catch: (error) => fileSystemFailure("remove", error),
48
+ }),
49
+ };
50
+
51
+ export const JscpdFileSystemLive = Layer.succeed(JscpdFileSystem, jscpdFileSystemLive);
52
+
53
+ function makeSecureTempDirectory(
54
+ prefix: string,
55
+ ): Effect.Effect<string, JscpdWorkspaceFailure | JscpdFileSystemFailure> {
56
+ return Effect.tryPromise({
57
+ try: () => mkdtemp(prefix),
58
+ catch: () => new JscpdWorkspaceFailure({ operation: "create" }),
59
+ }).pipe(
60
+ Effect.flatMap((directory) =>
61
+ Effect.tryPromise({
62
+ try: () => chmod(directory, 0o700),
63
+ catch: () => new JscpdWorkspaceFailure({ operation: "create" }),
64
+ }).pipe(
65
+ Effect.as(directory),
66
+ Effect.catchAll((error) =>
67
+ Effect.tryPromise({
68
+ try: () => rm(directory, { recursive: true, force: true }),
69
+ catch: () => fileSystemFailure("remove", undefined),
70
+ }).pipe(
71
+ Effect.catchAll(() => Effect.void),
72
+ Effect.zipRight(Effect.fail(error)),
73
+ ),
74
+ ),
75
+ ),
76
+ ),
77
+ );
78
+ }
79
+
80
+ function readBoundedFile(
81
+ request: JscpdBoundedReadRequest,
82
+ ): Effect.Effect<Uint8Array, JscpdFileSystemError> {
83
+ if (!isValidReadRequest(request)) {
84
+ return Effect.fail(new JscpdFileSystemFailure({ operation: "read", reason: "io" }));
85
+ }
86
+ const flags = fsConstants.O_RDONLY | (request.noFollow ? fsConstants.O_NOFOLLOW : 0);
87
+ return readBoundedFileWith(
88
+ request,
89
+ Effect.tryPromise({
90
+ try: () => open(request.path, flags),
91
+ catch: (error) => fileSystemFailure("read", error),
92
+ }),
93
+ );
94
+ }
95
+
96
+ /** Bracketed handle seam used to prove acquisition/read settlement under interruption. */
97
+ export function readBoundedFileWith(
98
+ request: JscpdBoundedReadRequest,
99
+ acquire: Effect.Effect<FileHandle, JscpdFileSystemFailure>,
100
+ ): Effect.Effect<Uint8Array, JscpdFileSystemError> {
101
+ return Effect.acquireUseRelease(
102
+ acquire,
103
+ (file) => readFromHandle(file, request).pipe(Effect.uninterruptible),
104
+ closeFile,
105
+ );
106
+ }
107
+
108
+ function readFromHandle(
109
+ file: FileHandle,
110
+ request: JscpdBoundedReadRequest,
111
+ ): Effect.Effect<Uint8Array, JscpdFileSystemError> {
112
+ return Effect.tryPromise({
113
+ try: () => readValidatedFile(file, request),
114
+ catch: (error) => readFailure(error),
115
+ });
116
+ }
117
+
118
+ async function readValidatedFile(
119
+ file: FileHandle,
120
+ request: JscpdBoundedReadRequest,
121
+ ): Promise<Uint8Array> {
122
+ const metadata = await file.stat();
123
+ assertRegularFile(metadata, request.regularFileOnly);
124
+ const offset = request.offset ?? 0;
125
+ const capacity = readCapacity(metadata.size, offset, request);
126
+ const bytes = Buffer.alloc(capacity);
127
+ const total = await fillBuffer(file, bytes, offset);
128
+ assertValidReadLength(total, request);
129
+ return bytes.subarray(0, total);
130
+ }
131
+
132
+ function assertRegularFile(metadata: Stats, regularFileOnly: boolean): void {
133
+ if (regularFileOnly && !metadata.isFile()) {
134
+ throw new JscpdFileSystemFailure({ operation: "read", reason: "not-regular" });
135
+ }
136
+ }
137
+
138
+ function readCapacity(fileSize: number, offset: number, request: JscpdBoundedReadRequest): number {
139
+ const requestedLength = request.length;
140
+ if (offset > fileSize || (requestedLength !== undefined && offset + requestedLength > fileSize)) {
141
+ throw new JscpdFileSystemFailure({ operation: "read", reason: "io" });
142
+ }
143
+ if (requestedLength === undefined && fileSize - offset > request.maxBytes) {
144
+ throw new JscpdLimitExceeded({ subject: request.limitSubject });
145
+ }
146
+ return requestedLength ?? request.maxBytes + 1;
147
+ }
148
+
149
+ function assertValidReadLength(total: number, request: JscpdBoundedReadRequest): void {
150
+ if (request.length !== undefined && total !== request.length) {
151
+ throw new JscpdFileSystemFailure({ operation: "read", reason: "io" });
152
+ }
153
+ if (total > request.maxBytes) {
154
+ throw new JscpdLimitExceeded({ subject: request.limitSubject });
155
+ }
156
+ }
157
+
158
+ async function fillBuffer(file: FileHandle, bytes: Buffer, offset: number): Promise<number> {
159
+ let total = 0;
160
+ while (total < bytes.byteLength) {
161
+ const read = await file.read(bytes, total, bytes.byteLength - total, offset + total);
162
+ if (read.bytesRead === 0) break;
163
+ total += read.bytesRead;
164
+ }
165
+ return total;
166
+ }
167
+
168
+ function closeFile(file: FileHandle): Effect.Effect<void> {
169
+ return Effect.tryPromise({
170
+ try: () => file.close(),
171
+ catch: (error) => fileSystemFailure("read", error),
172
+ }).pipe(Effect.orDie);
173
+ }
174
+
175
+ function metadataFrom(metadata: Stats): JscpdFileMetadata {
176
+ const kind = metadata.isFile()
177
+ ? "file"
178
+ : metadata.isDirectory()
179
+ ? "directory"
180
+ : metadata.isSymbolicLink()
181
+ ? "symlink"
182
+ : "other";
183
+ return { kind, size: metadata.size };
184
+ }
185
+
186
+ function isValidReadRequest(request: JscpdBoundedReadRequest): boolean {
187
+ const offset = request.offset ?? 0;
188
+ return (
189
+ isValidByteBound(request.maxBytes) &&
190
+ Number.isSafeInteger(offset) &&
191
+ offset >= 0 &&
192
+ (request.length === undefined ||
193
+ (Number.isSafeInteger(request.length) &&
194
+ request.length >= 0 &&
195
+ request.length <= request.maxBytes &&
196
+ Number.isSafeInteger(offset + request.length)))
197
+ );
198
+ }
199
+
200
+ function isValidByteBound(value: number): boolean {
201
+ return Number.isSafeInteger(value) && value > 0 && value <= MAX_FILESYSTEM_BYTES;
202
+ }
203
+
204
+ function readFailure(error: unknown): JscpdFileSystemError {
205
+ return error instanceof JscpdFileSystemFailure || error instanceof JscpdLimitExceeded
206
+ ? error
207
+ : fileSystemFailure("read", error);
208
+ }
209
+
210
+ function fileSystemFailure(
211
+ operation: JscpdFileSystemFailure["operation"],
212
+ error: unknown,
213
+ ): JscpdFileSystemFailure {
214
+ const code = errorCode(error);
215
+ const reason = isMissingCode(operation, code)
216
+ ? "missing"
217
+ : code === "EACCES" || code === "EPERM"
218
+ ? "permission"
219
+ : code === "ELOOP"
220
+ ? "symlink"
221
+ : "io";
222
+ return new JscpdFileSystemFailure({ operation, reason });
223
+ }
224
+
225
+ function isMissingCode(
226
+ operation: JscpdFileSystemFailure["operation"],
227
+ code: string | undefined,
228
+ ): boolean {
229
+ return (
230
+ code === "ENOENT" ||
231
+ code === "ENOTDIR" ||
232
+ (operation === "canonicalize" && process.platform === "win32" && code === "EINVAL")
233
+ );
234
+ }
235
+
236
+ function errorCode(error: unknown): string | undefined {
237
+ return typeof error === "object" && error !== null && "code" in error
238
+ ? (error as NodeJS.ErrnoException).code
239
+ : undefined;
240
+ }
@@ -0,0 +1,25 @@
1
+ import { type Scope as EffectScope, Layer, ManagedRuntime, Scope } from "effect";
2
+ import { JscpdProcessLive } from "../process.js";
3
+ import { JscpdClockLive } from "./clock.js";
4
+ import { JscpdFileSystemLive } from "./filesystem.js";
5
+ import type { JscpdEffectRuntime } from "./runtime-contract.js";
6
+
7
+ export type { JscpdEffectRuntime, JscpdRuntimeRequirements } from "./runtime-contract.js";
8
+
9
+ const JscpdRuntimeLive = Layer.mergeAll(JscpdClockLive, JscpdFileSystemLive, JscpdProcessLive);
10
+
11
+ /** Create the sole managed production runtime for one extension instance. */
12
+ export function createJscpdManagedRuntime(): JscpdEffectRuntime {
13
+ const runtime = ManagedRuntime.make(JscpdRuntimeLive);
14
+ return {
15
+ runPromise: (effect, signal) => runtime.runPromise(effect, signal ? { signal } : undefined),
16
+ runPromiseExit: (effect, signal) =>
17
+ runtime.runPromiseExit(effect, signal ? { signal } : undefined),
18
+ runSync: (effect) => runtime.runSync(effect),
19
+ dispose: () => runtime.dispose(),
20
+ } as JscpdEffectRuntime;
21
+ }
22
+
23
+ export function makeEffectScope(runtime: JscpdEffectRuntime): EffectScope.CloseableScope {
24
+ return runtime.runSync(Scope.make());
25
+ }
@@ -0,0 +1,18 @@
1
+ import type { Effect, Exit as EffectExit } from "effect";
2
+ import type { JscpdWorkflowRequirements } from "./services.js";
3
+
4
+ export type JscpdRuntimeRequirements = JscpdWorkflowRequirements;
5
+
6
+ /** Host-owned execution boundary shared by every compatibility facade in one extension. */
7
+ export interface JscpdEffectRuntime {
8
+ runPromise<A, E, R extends JscpdRuntimeRequirements>(
9
+ effect: Effect.Effect<A, E, R>,
10
+ signal?: AbortSignal,
11
+ ): Promise<A>;
12
+ runPromiseExit<A, E, R extends JscpdRuntimeRequirements>(
13
+ effect: Effect.Effect<A, E, R>,
14
+ signal?: AbortSignal,
15
+ ): Promise<EffectExit.Exit<A, E>>;
16
+ runSync<A, E, R extends JscpdRuntimeRequirements>(effect: Effect.Effect<A, E, R>): A;
17
+ dispose(): Promise<void>;
18
+ }