@gmickel/gno 1.34.6 → 1.36.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.
- package/README.md +12 -1
- package/assets/skill/SKILL.md +26 -3
- package/assets/skill/cli-reference.md +9 -0
- package/assets/skill/mcp-reference.md +3 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.34.6.zip → gno-browser-clipper-v1.36.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.36.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +111 -1
- package/spec/mcp.md +60 -1
- package/spec/output-schemas/mcp-job-status.schema.json +6 -2
- package/spec/output-schemas/peek.schema.json +212 -0
- package/spec/output-schemas/search-results.schema.json +1 -1
- package/src/cli/commands/peek.ts +66 -0
- package/src/cli/options.ts +2 -0
- package/src/cli/program.ts +20 -0
- package/src/config/index.ts +4 -0
- package/src/config/types.ts +14 -0
- package/src/core/path-rules.ts +34 -0
- package/src/core/peek.ts +202 -0
- package/src/ingestion/index.ts +21 -0
- package/src/ingestion/record-container.ts +23 -1
- package/src/ingestion/source-availability/darwin-io.ts +295 -0
- package/src/ingestion/source-availability/darwin-path.ts +58 -0
- package/src/ingestion/source-availability/directory.ts +402 -0
- package/src/ingestion/source-availability/index.ts +74 -0
- package/src/ingestion/source-availability/readers.ts +360 -0
- package/src/ingestion/source-availability/resolve.ts +28 -0
- package/src/ingestion/source-availability/types.ts +170 -0
- package/src/ingestion/sync.ts +197 -24
- package/src/ingestion/types.ts +45 -3
- package/src/ingestion/walker.ts +263 -5
- package/src/mcp/http-egress.ts +1 -0
- package/src/mcp/tools/index.ts +14 -0
- package/src/mcp/tools/peek.ts +78 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/watch-reconciliation-fallback-disk.ts +239 -100
- package/src/serve/watch-reconciliation-fallback.ts +35 -5
- package/src/serve/watch-reconciliation-shared.ts +8 -3
- package/src/serve/watch-reconciliation.ts +7 -0
- package/src/serve/watch-service-flush.ts +10 -0
- package/src/serve/watch-service-lifecycle.ts +2 -0
- package/src/serve/watch-service-snapshot.ts +27 -3
- package/src/serve/watch-service.ts +1 -0
- package/src/serve/watch-snapshot-availability.ts +51 -0
- package/src/serve/watch-snapshot-handles.ts +117 -37
- package/src/serve/watch-snapshot-libc.ts +141 -22
- package/src/serve/watch-snapshot-ops.ts +151 -9
- package/src/serve/watch-snapshot-scan.ts +3 -0
- package/src/serve/watch-snapshot-types.ts +45 -3
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.6.zip.sha256 +0 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source content readers for `any` (legacy) and `local` (Darwin-guarded) modes.
|
|
3
|
+
*
|
|
4
|
+
* @module src/ingestion/source-availability/readers
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
classifyGuardedReadErrno,
|
|
9
|
+
type DarwinFileIoPort,
|
|
10
|
+
type DarwinIoPolicyPort,
|
|
11
|
+
guardedOpenFlags,
|
|
12
|
+
loadDarwinIo,
|
|
13
|
+
withNoMaterializePolicy,
|
|
14
|
+
} from "./darwin-io";
|
|
15
|
+
import {
|
|
16
|
+
classifyDarwinFileProviderPath,
|
|
17
|
+
type DarwinFileProviderPathSupport,
|
|
18
|
+
} from "./darwin-path";
|
|
19
|
+
import {
|
|
20
|
+
type SourceAvailabilityMode,
|
|
21
|
+
type SourceContentReaderPort,
|
|
22
|
+
type SourceReadResult,
|
|
23
|
+
sourceAvailabilityMessage,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
const READ_CHUNK_BYTES = 64 * 1024;
|
|
27
|
+
/** Injectable deps for unit tests; production uses loadDarwinIo(). */
|
|
28
|
+
export type LocalReaderDeps = {
|
|
29
|
+
platform?: string;
|
|
30
|
+
policy?: DarwinIoPolicyPort | null;
|
|
31
|
+
file?: DarwinFileIoPort | null;
|
|
32
|
+
pathSupport?: (absPath: string) => DarwinFileProviderPathSupport;
|
|
33
|
+
/**
|
|
34
|
+
* Optional pre-open flag probe. When provided and returns a non-null
|
|
35
|
+
* outcome, the reader fails closed without materializing content.
|
|
36
|
+
* Production leaves this unset (content-boundary policy is the guard).
|
|
37
|
+
*/
|
|
38
|
+
safetyProbe?: (absPath: string) => SourceReadResult | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Legacy path: Bun.file byte reads (current ingestion behavior). */
|
|
42
|
+
export class AnySourceContentReader implements SourceContentReaderPort {
|
|
43
|
+
readonly mode: SourceAvailabilityMode = "any";
|
|
44
|
+
|
|
45
|
+
async readAll(
|
|
46
|
+
absPath: string,
|
|
47
|
+
_expectedSize?: number
|
|
48
|
+
): Promise<SourceReadResult> {
|
|
49
|
+
try {
|
|
50
|
+
const bytes = await Bun.file(absPath).bytes();
|
|
51
|
+
return { ok: true, bytes };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
return mapNodeLikeReadError(error);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Local mode reader. On Darwin, establishes no-materialization I/O policy and
|
|
60
|
+
* reads via native open/read so EDEADLK maps to cloud-placeholder skip.
|
|
61
|
+
* Non-Darwin and policy/FFI failures fail closed with distinct codes.
|
|
62
|
+
*/
|
|
63
|
+
export class LocalSourceContentReader implements SourceContentReaderPort {
|
|
64
|
+
readonly mode: SourceAvailabilityMode = "local";
|
|
65
|
+
private readonly platform: string;
|
|
66
|
+
private readonly policy: DarwinIoPolicyPort | null;
|
|
67
|
+
private readonly file: DarwinFileIoPort | null;
|
|
68
|
+
private readonly safetyProbe?: (absPath: string) => SourceReadResult | null;
|
|
69
|
+
private readonly pathSupport: (
|
|
70
|
+
absPath: string
|
|
71
|
+
) => DarwinFileProviderPathSupport;
|
|
72
|
+
|
|
73
|
+
constructor(deps: LocalReaderDeps = {}) {
|
|
74
|
+
this.platform = deps.platform ?? process.platform;
|
|
75
|
+
if (deps.policy !== undefined || deps.file !== undefined) {
|
|
76
|
+
this.policy = deps.policy ?? null;
|
|
77
|
+
this.file = deps.file ?? null;
|
|
78
|
+
} else if (this.platform === "darwin") {
|
|
79
|
+
const loaded = loadDarwinIo();
|
|
80
|
+
this.policy = loaded?.policy ?? null;
|
|
81
|
+
this.file = loaded?.file ?? null;
|
|
82
|
+
} else {
|
|
83
|
+
this.policy = null;
|
|
84
|
+
this.file = null;
|
|
85
|
+
}
|
|
86
|
+
this.safetyProbe = deps.safetyProbe;
|
|
87
|
+
this.pathSupport = deps.pathSupport ?? classifyDarwinFileProviderPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async readAll(
|
|
91
|
+
absPath: string,
|
|
92
|
+
expectedSize?: number
|
|
93
|
+
): Promise<SourceReadResult> {
|
|
94
|
+
if (this.platform !== "darwin") {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
code: "SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
98
|
+
message: sourceAvailabilityMessage(
|
|
99
|
+
"SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
100
|
+
`platform=${this.platform}`
|
|
101
|
+
),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (!this.policy || !this.file) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
code: "SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
108
|
+
message: sourceAvailabilityMessage(
|
|
109
|
+
"SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
110
|
+
"darwin_io_unavailable"
|
|
111
|
+
),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const pathSupport = this.pathSupport(absPath);
|
|
116
|
+
if (pathSupport === "unsupported") {
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
code: "SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
120
|
+
message: sourceAvailabilityMessage(
|
|
121
|
+
"SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
122
|
+
"path is outside the physically evidenced macOS File Provider layouts"
|
|
123
|
+
),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (pathSupport === "unknown") {
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
130
|
+
message: sourceAvailabilityMessage(
|
|
131
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
132
|
+
"path support could not be established"
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.safetyProbe) {
|
|
138
|
+
const probed = this.safetyProbe(absPath);
|
|
139
|
+
if (probed !== null) {
|
|
140
|
+
return probed;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const policy = this.policy;
|
|
145
|
+
const file = this.file;
|
|
146
|
+
let wrapped: ReturnType<typeof withNoMaterializePolicy<SourceReadResult>>;
|
|
147
|
+
try {
|
|
148
|
+
wrapped = withNoMaterializePolicy(
|
|
149
|
+
() => readAllNative(absPath, file, expectedSize),
|
|
150
|
+
policy
|
|
151
|
+
);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
156
|
+
message: sourceAvailabilityMessage(
|
|
157
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
158
|
+
error instanceof Error ? error.message : "native_read_failed"
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
if (!wrapped.ok) {
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
code: "SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
166
|
+
message: sourceAvailabilityMessage(
|
|
167
|
+
"SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
168
|
+
wrapped.error
|
|
169
|
+
),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return wrapped.value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function readAllNative(
|
|
177
|
+
absPath: string,
|
|
178
|
+
file: DarwinFileIoPort,
|
|
179
|
+
expectedSize?: number
|
|
180
|
+
): SourceReadResult {
|
|
181
|
+
const fd = file.open(absPath, guardedOpenFlags());
|
|
182
|
+
if (fd < 0) {
|
|
183
|
+
return mapErrno(file.readErrno(), 0);
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const chunks: Uint8Array[] = [];
|
|
187
|
+
let total = 0;
|
|
188
|
+
const buf = new Uint8Array(READ_CHUNK_BYTES);
|
|
189
|
+
for (;;) {
|
|
190
|
+
const n = file.read(fd, buf);
|
|
191
|
+
if (n < 0) {
|
|
192
|
+
return mapErrno(file.readErrno(), total);
|
|
193
|
+
}
|
|
194
|
+
if (n === 0) {
|
|
195
|
+
if (expectedSize !== undefined && total < expectedSize) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
code: "IO_ERROR",
|
|
199
|
+
message: sourceAvailabilityMessage(
|
|
200
|
+
"IO_ERROR",
|
|
201
|
+
`short_read expected=${expectedSize} read=${total}`
|
|
202
|
+
),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
chunks.push(buf.slice(0, n));
|
|
208
|
+
total += n;
|
|
209
|
+
}
|
|
210
|
+
return { ok: true, bytes: concatChunks(chunks, total) };
|
|
211
|
+
} finally {
|
|
212
|
+
file.close(fd);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function mapErrno(errno: number, bytesAlreadyRead: number): SourceReadResult {
|
|
217
|
+
const kind = classifyGuardedReadErrno(errno);
|
|
218
|
+
if (kind === "EDEADLK") {
|
|
219
|
+
if (bytesAlreadyRead > 0) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
code: "CLOUD_PARTIAL",
|
|
223
|
+
message: sourceAvailabilityMessage("CLOUD_PARTIAL", `errno=${errno}`),
|
|
224
|
+
errno,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
code: "CLOUD_PLACEHOLDER",
|
|
230
|
+
message: sourceAvailabilityMessage("CLOUD_PLACEHOLDER", `errno=${errno}`),
|
|
231
|
+
errno,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
if (kind === "EACCES" || kind === "EPERM") {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
code: "PERMISSION",
|
|
238
|
+
message: sourceAvailabilityMessage("PERMISSION", `errno=${errno}`),
|
|
239
|
+
errno,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (kind === "ENOENT") {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
code: "NOT_FOUND",
|
|
246
|
+
message: sourceAvailabilityMessage("NOT_FOUND", `errno=${errno}`),
|
|
247
|
+
errno,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (kind === "EISDIR") {
|
|
251
|
+
return {
|
|
252
|
+
ok: false,
|
|
253
|
+
code: "NOT_FILE",
|
|
254
|
+
message: sourceAvailabilityMessage("NOT_FILE", `errno=${errno}`),
|
|
255
|
+
errno,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
if (kind === "ELOOP") {
|
|
259
|
+
return {
|
|
260
|
+
ok: false,
|
|
261
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
262
|
+
message: sourceAvailabilityMessage(
|
|
263
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
264
|
+
`symlink_loop errno=${errno}`
|
|
265
|
+
),
|
|
266
|
+
errno,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Unknown errno / flags / safety: fail closed, never claim safe to read.
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
code: "SOURCE_AVAILABILITY_UNKNOWN",
|
|
273
|
+
message: sourceAvailabilityMessage(
|
|
274
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
275
|
+
`errno=${errno}`
|
|
276
|
+
),
|
|
277
|
+
errno,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mapNodeLikeReadError(error: unknown): SourceReadResult {
|
|
282
|
+
const code =
|
|
283
|
+
typeof error === "object" &&
|
|
284
|
+
error !== null &&
|
|
285
|
+
"code" in error &&
|
|
286
|
+
typeof (error as { code?: unknown }).code === "string"
|
|
287
|
+
? (error as { code: string }).code
|
|
288
|
+
: undefined;
|
|
289
|
+
if (code === "ENOENT") {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
code: "NOT_FOUND",
|
|
293
|
+
message: sourceAvailabilityMessage("NOT_FOUND"),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (code === "EACCES" || code === "EPERM") {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
code: "PERMISSION",
|
|
300
|
+
message: sourceAvailabilityMessage("PERMISSION"),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (code === "EISDIR") {
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
code: "NOT_FILE",
|
|
307
|
+
message: sourceAvailabilityMessage("NOT_FILE"),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const detail = error instanceof Error ? error.message : "read_failed";
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
code: "IO_ERROR",
|
|
314
|
+
message: sourceAvailabilityMessage("IO_ERROR", detail),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function concatChunks(chunks: Uint8Array[], total: number): Uint8Array {
|
|
319
|
+
if (chunks.length === 0) {
|
|
320
|
+
return new Uint8Array(0);
|
|
321
|
+
}
|
|
322
|
+
if (chunks.length === 1) {
|
|
323
|
+
return chunks[0] ?? new Uint8Array(0);
|
|
324
|
+
}
|
|
325
|
+
const out = new Uint8Array(total);
|
|
326
|
+
let offset = 0;
|
|
327
|
+
for (const chunk of chunks) {
|
|
328
|
+
out.set(chunk, offset);
|
|
329
|
+
offset += chunk.byteLength;
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** Factory: mode → reader. Local uses Darwin guard; unsupported platforms fail closed. */
|
|
335
|
+
export function createSourceContentReader(
|
|
336
|
+
mode: SourceAvailabilityMode,
|
|
337
|
+
deps: LocalReaderDeps = {}
|
|
338
|
+
): SourceContentReaderPort {
|
|
339
|
+
if (mode === "any") {
|
|
340
|
+
return new AnySourceContentReader();
|
|
341
|
+
}
|
|
342
|
+
return new LocalSourceContentReader(deps);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Yield one pre-read buffer as an async iterable (record-import open seam). */
|
|
346
|
+
export function bytesAsAsyncIterable(
|
|
347
|
+
bytes: Uint8Array,
|
|
348
|
+
signal?: AbortSignal
|
|
349
|
+
): AsyncIterable<Uint8Array> {
|
|
350
|
+
return {
|
|
351
|
+
async *[Symbol.asyncIterator]() {
|
|
352
|
+
if (signal?.aborted) {
|
|
353
|
+
throw new Error("source read aborted");
|
|
354
|
+
}
|
|
355
|
+
if (bytes.byteLength > 0) {
|
|
356
|
+
yield bytes;
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve effective source-availability mode for a collection/index run.
|
|
3
|
+
*
|
|
4
|
+
* @module src/ingestion/source-availability/resolve
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Collection } from "../../config/types";
|
|
8
|
+
import type { SyncOptions } from "../types";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_SOURCE_AVAILABILITY,
|
|
12
|
+
type SourceAvailabilityMode,
|
|
13
|
+
} from "./types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Run-level SyncOptions override wins over collection config.
|
|
17
|
+
* Missing values resolve to `any` (behaviorally unchanged default).
|
|
18
|
+
*/
|
|
19
|
+
export function resolveSourceAvailability(
|
|
20
|
+
collection: Pick<Collection, "sourceAvailability">,
|
|
21
|
+
options?: Pick<SyncOptions, "sourceAvailability">
|
|
22
|
+
): SourceAvailabilityMode {
|
|
23
|
+
return (
|
|
24
|
+
options?.sourceAvailability ??
|
|
25
|
+
collection.sourceAvailability ??
|
|
26
|
+
DEFAULT_SOURCE_AVAILABILITY
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-neutral source-availability policy contract.
|
|
3
|
+
* Controls whether source content materialization is allowed during ingestion.
|
|
4
|
+
* Distinct from collection egress/privacy policy.
|
|
5
|
+
*
|
|
6
|
+
* @module src/ingestion/source-availability/types
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_SOURCE_AVAILABILITY,
|
|
11
|
+
SOURCE_AVAILABILITY_MODES,
|
|
12
|
+
type SourceAvailabilityMode,
|
|
13
|
+
} from "../../config/types";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
DEFAULT_SOURCE_AVAILABILITY,
|
|
17
|
+
SOURCE_AVAILABILITY_MODES,
|
|
18
|
+
type SourceAvailabilityMode,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Distinct fail-closed / skip outcomes for guarded source content access.
|
|
23
|
+
* CLOUD_* and DATALESS_DIRECTORY are skips (not conversion errors).
|
|
24
|
+
* Others fail closed as errors. Directory-boundary unknowns preserve indexed
|
|
25
|
+
* descendants (unproven absence) rather than proving deletion.
|
|
26
|
+
*/
|
|
27
|
+
export const SOURCE_AVAILABILITY_CODES = [
|
|
28
|
+
"CLOUD_PLACEHOLDER",
|
|
29
|
+
"CLOUD_PARTIAL",
|
|
30
|
+
"DATALESS_DIRECTORY",
|
|
31
|
+
"SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
32
|
+
"SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
33
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
34
|
+
"PERMISSION",
|
|
35
|
+
"NOT_FOUND",
|
|
36
|
+
"NOT_FILE",
|
|
37
|
+
"IO_ERROR",
|
|
38
|
+
] as const;
|
|
39
|
+
export type SourceAvailabilityCode = (typeof SOURCE_AVAILABILITY_CODES)[number];
|
|
40
|
+
|
|
41
|
+
/** Codes that must surface as skipped (not conversion/store errors). */
|
|
42
|
+
export const SOURCE_AVAILABILITY_SKIP_CODES = new Set<SourceAvailabilityCode>([
|
|
43
|
+
"CLOUD_PLACEHOLDER",
|
|
44
|
+
"CLOUD_PARTIAL",
|
|
45
|
+
"DATALESS_DIRECTORY",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Directory / prefix outcomes that must not prove descendant absence.
|
|
50
|
+
* Includes fail-closed errors where enumeration safety is unknown.
|
|
51
|
+
*/
|
|
52
|
+
export const SOURCE_AVAILABILITY_UNPROVEN_PREFIX_CODES =
|
|
53
|
+
new Set<SourceAvailabilityCode>([
|
|
54
|
+
"DATALESS_DIRECTORY",
|
|
55
|
+
"SOURCE_AVAILABILITY_UNSUPPORTED",
|
|
56
|
+
"SOURCE_AVAILABILITY_POLICY_FAILED",
|
|
57
|
+
"SOURCE_AVAILABILITY_UNKNOWN",
|
|
58
|
+
"PERMISSION",
|
|
59
|
+
"IO_ERROR",
|
|
60
|
+
"NOT_FOUND",
|
|
61
|
+
"NOT_FILE",
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
export type SourceReadSuccess = {
|
|
65
|
+
ok: true;
|
|
66
|
+
bytes: Uint8Array;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type SourceReadFailure = {
|
|
70
|
+
ok: false;
|
|
71
|
+
code: SourceAvailabilityCode;
|
|
72
|
+
message: string;
|
|
73
|
+
/** Optional low-level errno for diagnostics (never required by callers). */
|
|
74
|
+
errno?: number | null;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type SourceReadResult = SourceReadSuccess | SourceReadFailure;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Single content-boundary port: one guarded read supplies bytes for
|
|
81
|
+
* sniff/hash/conversion and record-import open paths.
|
|
82
|
+
*/
|
|
83
|
+
export interface SourceContentReaderPort {
|
|
84
|
+
readonly mode: SourceAvailabilityMode;
|
|
85
|
+
/**
|
|
86
|
+
* Read all source bytes under the active availability policy.
|
|
87
|
+
* Local mode rechecks only at this byte-consumption boundary.
|
|
88
|
+
*/
|
|
89
|
+
readAll(absPath: string, expectedSize?: number): Promise<SourceReadResult>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Amortized directory-boundary classification (not per discovered file). */
|
|
93
|
+
export type DirectoryAvailabilityResult =
|
|
94
|
+
| { kind: "available" }
|
|
95
|
+
| {
|
|
96
|
+
kind: "dataless";
|
|
97
|
+
code: "DATALESS_DIRECTORY";
|
|
98
|
+
message: string;
|
|
99
|
+
}
|
|
100
|
+
| {
|
|
101
|
+
kind: "error";
|
|
102
|
+
code: SourceAvailabilityCode;
|
|
103
|
+
message: string;
|
|
104
|
+
errno?: number | null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type DirectoryReadResult<T> =
|
|
108
|
+
| { kind: "available"; value: T }
|
|
109
|
+
| Exclude<DirectoryAvailabilityResult, { kind: "available" }>;
|
|
110
|
+
|
|
111
|
+
export type SynchronousDirectoryRead<T> = () => T &
|
|
112
|
+
(T extends PromiseLike<unknown> ? never : unknown);
|
|
113
|
+
|
|
114
|
+
export interface DirectoryAvailabilityPort {
|
|
115
|
+
readonly mode: SourceAvailabilityMode;
|
|
116
|
+
/** Classify one directory before descent or ancestor-prefix checks. */
|
|
117
|
+
classify(absPath: string): Promise<DirectoryAvailabilityResult>;
|
|
118
|
+
/** Reclassify and synchronously enumerate while the policy remains active. */
|
|
119
|
+
readDirectory<T>(
|
|
120
|
+
absPath: string,
|
|
121
|
+
read: SynchronousDirectoryRead<T>
|
|
122
|
+
): DirectoryReadResult<T>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function isSourceAvailabilitySkip(
|
|
126
|
+
code: string | undefined
|
|
127
|
+
): code is "CLOUD_PLACEHOLDER" | "CLOUD_PARTIAL" | "DATALESS_DIRECTORY" {
|
|
128
|
+
return (
|
|
129
|
+
code === "CLOUD_PLACEHOLDER" ||
|
|
130
|
+
code === "CLOUD_PARTIAL" ||
|
|
131
|
+
code === "DATALESS_DIRECTORY"
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function isUnprovenAbsenceCode(code: string | undefined): boolean {
|
|
136
|
+
return (
|
|
137
|
+
typeof code === "string" &&
|
|
138
|
+
SOURCE_AVAILABILITY_UNPROVEN_PREFIX_CODES.has(
|
|
139
|
+
code as SourceAvailabilityCode
|
|
140
|
+
)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function sourceAvailabilityMessage(
|
|
145
|
+
code: SourceAvailabilityCode,
|
|
146
|
+
detail?: string
|
|
147
|
+
): string {
|
|
148
|
+
const base: Record<SourceAvailabilityCode, string> = {
|
|
149
|
+
CLOUD_PLACEHOLDER:
|
|
150
|
+
"Source is a cloud placeholder; local mode refused materialization",
|
|
151
|
+
CLOUD_PARTIAL:
|
|
152
|
+
"Source has partial cloud content; local mode refused incomplete materialization",
|
|
153
|
+
DATALESS_DIRECTORY:
|
|
154
|
+
"Directory is dataless or availability-unknown; local mode refused descent",
|
|
155
|
+
SOURCE_AVAILABILITY_UNSUPPORTED:
|
|
156
|
+
"sourceAvailability local is not supported on this platform or filesystem",
|
|
157
|
+
SOURCE_AVAILABILITY_POLICY_FAILED:
|
|
158
|
+
"Failed to establish no-materialization I/O policy for sourceAvailability local",
|
|
159
|
+
SOURCE_AVAILABILITY_UNKNOWN:
|
|
160
|
+
"Source availability could not be determined safely; local mode fails closed",
|
|
161
|
+
PERMISSION: "Permission denied reading source file",
|
|
162
|
+
NOT_FOUND: "Source file not found",
|
|
163
|
+
NOT_FILE: "Path is not a regular file",
|
|
164
|
+
IO_ERROR: "I/O error reading source file",
|
|
165
|
+
};
|
|
166
|
+
if (detail && detail.length > 0) {
|
|
167
|
+
return `${base[code]}: ${detail}`;
|
|
168
|
+
}
|
|
169
|
+
return base[code];
|
|
170
|
+
}
|