@hraness/sponge 0.1.0 → 0.2.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 +163 -17
- package/bin/sponge.js +9 -2
- package/dist/contracts/index.d.ts +164 -0
- package/dist/contracts/index.js +6 -0
- package/dist/contracts/validate.d.ts +14 -0
- package/dist/contracts/validate.js +187 -0
- package/dist/migrations/001.d.ts +3 -0
- package/dist/migrations/001.js +33 -0
- package/dist/migrations/002.d.ts +3 -0
- package/dist/migrations/002.js +6 -0
- package/dist/migrations/003.d.ts +3 -0
- package/dist/migrations/003.js +10 -0
- package/dist/migrations/004.d.ts +3 -0
- package/dist/migrations/004.js +9 -0
- package/dist/src/acquisition/capture.d.ts +29 -0
- package/dist/src/acquisition/capture.js +160 -0
- package/dist/src/acquisition/cleanup.d.ts +6 -0
- package/dist/src/acquisition/cleanup.js +20 -0
- package/dist/src/acquisition/errors.d.ts +3 -0
- package/dist/src/acquisition/errors.js +20 -0
- package/dist/src/acquisition/files.d.ts +4 -0
- package/dist/src/acquisition/files.js +49 -0
- package/dist/src/acquisition/index.d.ts +6 -0
- package/dist/src/acquisition/index.js +4 -0
- package/dist/src/acquisition/lock.d.ts +1 -0
- package/dist/src/acquisition/lock.js +2 -0
- package/dist/src/acquisition/normalize.d.ts +11 -0
- package/dist/src/acquisition/normalize.js +62 -0
- package/dist/src/acquisition/pins.d.ts +3 -0
- package/dist/src/acquisition/pins.js +9 -0
- package/dist/src/acquisition/process.d.ts +16 -0
- package/dist/src/acquisition/process.js +63 -0
- package/dist/src/acquisition/runtime.d.ts +53 -0
- package/dist/src/acquisition/runtime.js +216 -0
- package/dist/src/acquisition/verify-helper.d.ts +1 -0
- package/dist/src/acquisition/verify-helper.js +19 -0
- package/dist/src/app/article.d.ts +23 -0
- package/dist/src/app/article.js +82 -0
- package/dist/src/app/bundle-files.d.ts +8 -0
- package/dist/src/app/bundle-files.js +66 -0
- package/dist/src/app/markdown.d.ts +6 -0
- package/dist/src/app/markdown.js +90 -0
- package/dist/src/app/source.d.ts +15 -0
- package/dist/src/app/source.js +40 -0
- package/dist/src/cli/arguments.d.ts +9 -0
- package/dist/src/cli/arguments.js +106 -0
- package/dist/src/cli/config.d.ts +8 -0
- package/dist/src/cli/config.js +45 -0
- package/dist/src/cli/import.d.ts +4 -0
- package/dist/src/cli/import.js +3 -0
- package/dist/src/cli/indexing.d.ts +7 -0
- package/dist/src/cli/indexing.js +21 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +333 -0
- package/dist/src/cli/portability.d.ts +9 -0
- package/dist/src/cli/portability.js +39 -0
- package/dist/src/cli/queue.d.ts +2 -0
- package/dist/src/cli/queue.js +23 -0
- package/dist/src/cli/terminal.d.ts +2 -0
- package/dist/src/cli/terminal.js +4 -0
- package/dist/src/cli/worker.d.ts +16 -0
- package/dist/src/cli/worker.js +62 -0
- package/dist/src/importers/apply.d.ts +29 -0
- package/dist/src/importers/apply.js +85 -0
- package/dist/src/importers/custody.d.ts +6 -0
- package/dist/src/importers/custody.js +35 -0
- package/dist/src/importers/inspect.d.ts +21 -0
- package/dist/src/importers/inspect.js +97 -0
- package/dist/src/importers/manifest.d.ts +22 -0
- package/dist/src/importers/manifest.js +43 -0
- package/dist/src/importers/verify-helper.d.ts +1 -0
- package/dist/src/importers/verify-helper.js +27 -0
- package/dist/src/indexing/image.d.ts +10 -0
- package/dist/src/indexing/image.js +34 -0
- package/dist/src/indexing/index.d.ts +53 -0
- package/dist/src/indexing/index.js +123 -0
- package/dist/src/indexing/jobs.d.ts +14 -0
- package/dist/src/indexing/jobs.js +49 -0
- package/dist/src/indexing/runner.d.ts +23 -0
- package/dist/src/indexing/runner.js +114 -0
- package/dist/src/inference/asr/bridge.d.ts +1 -0
- package/dist/src/inference/asr/bridge.js +16 -0
- package/dist/src/inference/asr/protocol.d.ts +23 -0
- package/dist/src/inference/asr/protocol.js +60 -0
- package/dist/src/inference/asr/runtime.d.ts +29 -0
- package/dist/src/inference/asr/runtime.js +103 -0
- package/dist/src/inference/asr/windows.d.ts +9 -0
- package/dist/src/inference/asr/windows.js +41 -0
- package/dist/src/inference/embedding/index.d.ts +47 -0
- package/dist/src/inference/embedding/index.js +122 -0
- package/dist/src/media/capture-helper.d.ts +1 -0
- package/dist/src/media/capture-helper.js +16 -0
- package/dist/src/media/capture.d.ts +32 -0
- package/dist/src/media/capture.js +193 -0
- package/dist/src/media/handler.d.ts +22 -0
- package/dist/src/media/handler.js +82 -0
- package/dist/src/media/transcribe.d.ts +29 -0
- package/dist/src/media/transcribe.js +93 -0
- package/dist/src/metadata/article.d.ts +5 -0
- package/dist/src/metadata/article.js +51 -0
- package/dist/src/models/artifacts.d.ts +36 -0
- package/dist/src/models/artifacts.js +274 -0
- package/dist/src/models/catalog.d.ts +35 -0
- package/dist/src/models/catalog.js +71 -0
- package/dist/src/models/download.d.ts +9 -0
- package/dist/src/models/download.js +147 -0
- package/dist/src/models/runtime.d.ts +24 -0
- package/dist/src/models/runtime.js +37 -0
- package/dist/src/pdf/capture-helper.d.ts +1 -0
- package/dist/src/pdf/capture-helper.js +12 -0
- package/dist/src/pdf/capture.d.ts +29 -0
- package/dist/src/pdf/capture.js +156 -0
- package/dist/src/pdf/errors.d.ts +2 -0
- package/dist/src/pdf/errors.js +13 -0
- package/dist/src/pdf/handler.d.ts +24 -0
- package/dist/src/pdf/handler.js +84 -0
- package/dist/src/pdf/index.d.ts +4 -0
- package/dist/src/pdf/index.js +4 -0
- package/dist/src/pdf/input.d.ts +2 -0
- package/dist/src/pdf/input.js +46 -0
- package/dist/src/pdf/normalize.d.ts +10 -0
- package/dist/src/pdf/normalize.js +64 -0
- package/dist/src/pdf/source-identity.d.ts +4 -0
- package/dist/src/pdf/source-identity.js +12 -0
- package/dist/src/pdf/source.d.ts +6 -0
- package/dist/src/pdf/source.js +22 -0
- package/dist/src/pdf/verify-helper.d.ts +1 -0
- package/dist/src/pdf/verify-helper.js +17 -0
- package/dist/src/portability/backup.d.ts +23 -0
- package/dist/src/portability/backup.js +138 -0
- package/dist/src/portability/database.d.ts +14 -0
- package/dist/src/portability/database.js +78 -0
- package/dist/src/portability/export.d.ts +8 -0
- package/dist/src/portability/export.js +36 -0
- package/dist/src/portability/files.d.ts +33 -0
- package/dist/src/portability/files.js +78 -0
- package/dist/src/portability/gc.d.ts +21 -0
- package/dist/src/portability/gc.js +66 -0
- package/dist/src/search/extension.d.ts +3 -0
- package/dist/src/search/extension.js +17 -0
- package/dist/src/search/hybrid.d.ts +39 -0
- package/dist/src/search/hybrid.js +118 -0
- package/dist/src/search/query.d.ts +12 -0
- package/dist/src/search/query.js +47 -0
- package/dist/src/search/vector.d.ts +21 -0
- package/dist/src/search/vector.js +21 -0
- package/dist/src/service/launch-agent.d.ts +47 -0
- package/dist/src/service/launch-agent.js +179 -0
- package/dist/src/service/lifetime.d.ts +7 -0
- package/dist/src/service/lifetime.js +73 -0
- package/dist/src/service/loop.d.ts +21 -0
- package/dist/src/service/loop.js +94 -0
- package/dist/src/store/files.d.ts +12 -0
- package/dist/src/store/files.js +101 -0
- package/dist/src/store/index.d.ts +118 -0
- package/dist/src/store/index.js +727 -0
- package/dist/src/store/types.d.ts +90 -0
- package/dist/src/store/types.js +8 -0
- package/dist/src/worker/basic.d.ts +10 -0
- package/dist/src/worker/basic.js +27 -0
- package/dist/src/worker/custody.d.ts +13 -0
- package/dist/src/worker/custody.js +185 -0
- package/dist/src/worker/engine.d.ts +28 -0
- package/dist/src/worker/engine.js +78 -0
- package/dist/src/worker/identity.d.ts +7 -0
- package/dist/src/worker/identity.js +27 -0
- package/dist/src/worker/journal.d.ts +5 -0
- package/dist/src/worker/journal.js +30 -0
- package/dist/src/worker/supervisor-child.d.ts +1 -0
- package/dist/src/worker/supervisor-child.js +33 -0
- package/dist/src/worker/supervisor.d.ts +3 -0
- package/dist/src/worker/supervisor.js +47 -0
- package/native/runtime-manifest.json +40 -0
- package/package.json +36 -6
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { MetadataClaim, MetadataWarning } from '../../contracts/index.js';
|
|
2
|
+
export type ItemKind = 'article' | 'pdf' | 'media';
|
|
3
|
+
export type FaultPoint = 'staged' | 'published' | 'transaction' | 'committed';
|
|
4
|
+
export interface StoreOptions {
|
|
5
|
+
fault?: (point: FaultPoint) => void;
|
|
6
|
+
appVersion?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface OwnerToken {
|
|
9
|
+
ownerIncarnation: string;
|
|
10
|
+
fence: number;
|
|
11
|
+
}
|
|
12
|
+
export interface AttemptToken extends OwnerToken {
|
|
13
|
+
attemptId: string;
|
|
14
|
+
jobId: string;
|
|
15
|
+
}
|
|
16
|
+
export interface OwnedAttempt extends AttemptToken {
|
|
17
|
+
kind?: 'capture' | 'index';
|
|
18
|
+
stage: string;
|
|
19
|
+
cleanup: 'pending' | 'settled';
|
|
20
|
+
finished: boolean;
|
|
21
|
+
jobState: string;
|
|
22
|
+
cancelRequested: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface SourceSubmission {
|
|
25
|
+
sourceKey: string;
|
|
26
|
+
locator: string;
|
|
27
|
+
realm?: string;
|
|
28
|
+
kind: ItemKind;
|
|
29
|
+
idempotencyKey: string;
|
|
30
|
+
intent?: 'save' | 'refresh';
|
|
31
|
+
}
|
|
32
|
+
export interface Submission {
|
|
33
|
+
itemId: string;
|
|
34
|
+
jobId: string;
|
|
35
|
+
submissionId: string;
|
|
36
|
+
duplicate: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface BlobReference {
|
|
39
|
+
sha256: string;
|
|
40
|
+
bytes: number;
|
|
41
|
+
relativePath: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RevisionAsset extends BlobReference {
|
|
44
|
+
mediaType: string;
|
|
45
|
+
relationship?: string;
|
|
46
|
+
alt?: string;
|
|
47
|
+
caption?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface CommitRevision extends AttemptToken {
|
|
50
|
+
importedState?: {
|
|
51
|
+
namespace: string;
|
|
52
|
+
sourceKey: string;
|
|
53
|
+
sourceHash: string;
|
|
54
|
+
importerVersion: string;
|
|
55
|
+
notes: {
|
|
56
|
+
id: string;
|
|
57
|
+
body: string;
|
|
58
|
+
}[];
|
|
59
|
+
readingState?: Record<string, unknown>;
|
|
60
|
+
privacy: 'private' | 'published';
|
|
61
|
+
};
|
|
62
|
+
itemId: string;
|
|
63
|
+
bundleHash: string;
|
|
64
|
+
markdown: string;
|
|
65
|
+
title?: string;
|
|
66
|
+
extractor: {
|
|
67
|
+
name: string;
|
|
68
|
+
version: string;
|
|
69
|
+
};
|
|
70
|
+
status: 'complete' | 'partial';
|
|
71
|
+
metadata?: MetadataClaim[];
|
|
72
|
+
metadataWarnings?: MetadataWarning[];
|
|
73
|
+
assets: RevisionAsset[];
|
|
74
|
+
warnings?: string[];
|
|
75
|
+
}
|
|
76
|
+
export interface ReadableItem {
|
|
77
|
+
itemId: string;
|
|
78
|
+
kind: ItemKind;
|
|
79
|
+
title: string;
|
|
80
|
+
revisionId: string;
|
|
81
|
+
markdown: string;
|
|
82
|
+
status: 'complete' | 'partial';
|
|
83
|
+
assets: RevisionAsset[];
|
|
84
|
+
metadata: MetadataClaim[];
|
|
85
|
+
metadataWarnings: MetadataWarning[];
|
|
86
|
+
}
|
|
87
|
+
export declare class StoreError extends Error {
|
|
88
|
+
readonly code: 'invalid-input' | 'integrity' | 'limit-exceeded' | 'schema-newer' | 'owner-busy' | 'cancelled' | 'unavailable';
|
|
89
|
+
constructor(code: 'invalid-input' | 'integrity' | 'limit-exceeded' | 'schema-newer' | 'owner-busy' | 'cancelled' | 'unavailable', message?: "unavailable" | "cancelled" | "invalid-input" | "integrity" | "limit-exceeded" | "schema-newer" | "owner-busy");
|
|
90
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LibraryStore } from '../store/index.js';
|
|
2
|
+
import { type ArticleOptions, type ArticleOutcome } from '../app/article.js';
|
|
3
|
+
import { type MediaOptions } from '../media/handler.js';
|
|
4
|
+
/** Content handlers over the durable sequential worker and its process supervisor. */
|
|
5
|
+
export declare function drainLibrary(store: LibraryStore, options: ArticleOptions & Pick<MediaOptions, 'asrRuntime' | 'language' | 'forceLocal'> & {
|
|
6
|
+
jobId?: string;
|
|
7
|
+
resolveConnection?: (realm: string) => ArticleOptions['connection'];
|
|
8
|
+
onResult?: (result: ArticleOutcome) => void;
|
|
9
|
+
}): Promise<ArticleOutcome[]>;
|
|
10
|
+
export declare const drainArticles: typeof drainLibrary;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { failure } from '../acquisition/errors.js';
|
|
2
|
+
import { ingestArticle } from '../app/article.js';
|
|
3
|
+
import { ingestPdf } from '../pdf/index.js';
|
|
4
|
+
import { ingestMedia } from '../media/handler.js';
|
|
5
|
+
import { drainQueue } from './engine.js';
|
|
6
|
+
/** Content handlers over the durable sequential worker and its process supervisor. */
|
|
7
|
+
export async function drainLibrary(store, options) {
|
|
8
|
+
const handler = async (context) => {
|
|
9
|
+
const source = store.sourceForJob(context.attempt.jobId);
|
|
10
|
+
let connection;
|
|
11
|
+
try {
|
|
12
|
+
connection = options.resolveConnection?.(source?.realm ?? 'public') ?? options.connection;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
store.failAttempt(context.attempt, 'repair-auth');
|
|
16
|
+
store.settleAttempt(context.attempt);
|
|
17
|
+
return { ...failure(context.attempt.attemptId, 'repair-auth', 'acquisition'), cleanup: 'settled' };
|
|
18
|
+
}
|
|
19
|
+
if (source?.kind === 'media') {
|
|
20
|
+
const { capture: _articleCapture, ...mediaOptions } = options;
|
|
21
|
+
return ingestMedia(store, context.attempt, { ...mediaOptions, connection, signal: context.signal, runProcess: context.runProcess, checkpoint: context.checkpoint });
|
|
22
|
+
}
|
|
23
|
+
return (source?.kind === 'pdf' ? ingestPdf : ingestArticle)(store, context.attempt, { ...options, connection, signal: context.signal, runProcess: context.runProcess, checkpoint: context.checkpoint });
|
|
24
|
+
};
|
|
25
|
+
return drainQueue(store, { signal: options.signal, jobId: options.jobId, onResult: options.onResult, handlers: { article: handler, pdf: handler, media: handler } });
|
|
26
|
+
}
|
|
27
|
+
export const drainArticles = drainLibrary;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type LibraryStore } from '../store/index.js';
|
|
2
|
+
export interface WorkerCustody {
|
|
3
|
+
incarnation: string;
|
|
4
|
+
directory: string;
|
|
5
|
+
release: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function acquireWorkerCustody(root: string): Promise<WorkerCustody>;
|
|
8
|
+
/** Receipts are specific to every pre-launch ticket. Missing receipts never mean absent children. */
|
|
9
|
+
export declare function proveAttemptCleanup(ownerDirectory: string, incarnation: string, attemptId: string): Promise<boolean>;
|
|
10
|
+
export declare function recoverWorker(store: LibraryStore): Promise<{
|
|
11
|
+
recovered: boolean;
|
|
12
|
+
attempts: number;
|
|
13
|
+
}>;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { lstat, link, unlink, readdir, rm, realpath } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { StoreError } from '../store/index.js';
|
|
5
|
+
import { directory, identifier, readJournal, writeJournal, syncDir } from './journal.js';
|
|
6
|
+
import { processIdentity, identityAlive } from './identity.js';
|
|
7
|
+
const pending = () => Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
|
|
8
|
+
async function ownerRecord(path) { const record = await readJournal(path); if (record.schemaVersion !== 1 || typeof record.incarnation !== 'string' || !record.identity || !Number.isSafeInteger(record.identity.pid) || record.identity.uid !== process.getuid?.() || typeof record.identity.started !== 'string')
|
|
9
|
+
throw pending(); identifier(record.incarnation); return record; }
|
|
10
|
+
async function sameFile(a, b) { const [x, y] = await Promise.all([lstat(a), lstat(b)]); if (!x.isFile() || !y.isFile() || x.ino !== y.ino || x.dev !== y.dev)
|
|
11
|
+
throw pending(); }
|
|
12
|
+
export async function acquireWorkerCustody(root) {
|
|
13
|
+
const workers = await directory(join(root, 'runtime', 'workers')), incarnation = randomUUID(), ownerDirectory = await directory(join(workers, incarnation));
|
|
14
|
+
const identity = await processIdentity(process.pid);
|
|
15
|
+
if (!identity)
|
|
16
|
+
throw pending();
|
|
17
|
+
const file = join(ownerDirectory, 'owner.json'), lock = join(root, 'runtime', 'worker.lock');
|
|
18
|
+
await writeJournal(file, { schemaVersion: 1, incarnation, identity });
|
|
19
|
+
try {
|
|
20
|
+
await link(file, lock);
|
|
21
|
+
await syncDir(join(root, 'runtime'));
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === 'EEXIST')
|
|
25
|
+
throw new StoreError('owner-busy');
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return { incarnation, directory: ownerDirectory, release: async () => { await sameFile(file, lock); await unlink(lock); await syncDir(join(root, 'runtime')); } };
|
|
29
|
+
}
|
|
30
|
+
/** Receipts are specific to every pre-launch ticket. Missing receipts never mean absent children. */
|
|
31
|
+
export async function proveAttemptCleanup(ownerDirectory, incarnation, attemptId) {
|
|
32
|
+
identifier(attemptId);
|
|
33
|
+
let entries;
|
|
34
|
+
const root = join(ownerDirectory, 'attempts', attemptId);
|
|
35
|
+
try {
|
|
36
|
+
const stat = await lstat(root);
|
|
37
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(root) !== root || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
|
|
38
|
+
return false;
|
|
39
|
+
entries = await readdir(root);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (const entry of entries.filter(name => /\.(settled|supervisor)\.json$/.test(name)))
|
|
45
|
+
if (!entries.includes(entry.replace(/\.(settled|supervisor)\.json$/, '.pending.json')))
|
|
46
|
+
return false;
|
|
47
|
+
for (const entry of entries.filter(name => name.endsWith('.pending.json'))) {
|
|
48
|
+
try {
|
|
49
|
+
const ticket = await readJournal(join(root, entry)), receipt = await readJournal(join(root, entry.replace('.pending.', '.settled.')));
|
|
50
|
+
if (typeof ticket.ticket !== 'string' || entry !== ticket.ticket + '.pending.json')
|
|
51
|
+
return false;
|
|
52
|
+
identifier(ticket.ticket);
|
|
53
|
+
if (ticket.schemaVersion !== 1 || ticket.attemptId !== attemptId || ticket.ownerIncarnation !== incarnation || receipt.schemaVersion !== 1 || receipt.ticket !== ticket.ticket || receipt.attemptId !== attemptId || receipt.ownerIncarnation !== incarnation || receipt.cleanup !== 'settled')
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
export async function recoverWorker(store) {
|
|
63
|
+
const lock = join(store.root, 'runtime', 'worker.lock');
|
|
64
|
+
let record;
|
|
65
|
+
try {
|
|
66
|
+
record = await ownerRecord(lock);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code === 'ENOENT') {
|
|
70
|
+
if (store.ownerSnapshot())
|
|
71
|
+
throw pending();
|
|
72
|
+
return { recovered: false, attempts: 0 };
|
|
73
|
+
}
|
|
74
|
+
throw pending();
|
|
75
|
+
}
|
|
76
|
+
if (await identityAlive(record.identity))
|
|
77
|
+
throw new StoreError('owner-busy');
|
|
78
|
+
const ownerDirectory = join(store.root, 'runtime', 'workers', record.incarnation), file = join(ownerDirectory, 'owner.json');
|
|
79
|
+
await sameFile(file, lock);
|
|
80
|
+
// Serialize recovery. No timeout, PID disappearance or age can steal this guard.
|
|
81
|
+
const recovery = join(store.root, 'runtime', 'worker-recovery.lock');
|
|
82
|
+
try {
|
|
83
|
+
await link(file, recovery);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code === 'EEXIST')
|
|
87
|
+
throw pending();
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
await sameFile(file, lock);
|
|
92
|
+
if (await identityAlive(record.identity))
|
|
93
|
+
throw new StoreError('owner-busy');
|
|
94
|
+
const owner = store.ownerSnapshot();
|
|
95
|
+
if (owner && owner.ownerIncarnation !== record.incarnation)
|
|
96
|
+
throw pending();
|
|
97
|
+
const attempts = owner ? store.ownerAttempts(owner) : [];
|
|
98
|
+
for (const attempt of attempts)
|
|
99
|
+
if (attempt.cleanup === 'pending' && !await proveAttemptCleanup(ownerDirectory, record.incarnation, attempt.attemptId))
|
|
100
|
+
throw pending();
|
|
101
|
+
const attemptIds = new Set(attempts.map(a => a.attemptId)), transientIds = new Map();
|
|
102
|
+
let journals = [];
|
|
103
|
+
try {
|
|
104
|
+
journals = await readdir(join(ownerDirectory, 'attempts'));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code !== 'ENOENT')
|
|
108
|
+
throw pending();
|
|
109
|
+
}
|
|
110
|
+
for (const id of journals) {
|
|
111
|
+
identifier(id);
|
|
112
|
+
if (!attemptIds.has(id)) {
|
|
113
|
+
let marker;
|
|
114
|
+
for (const kind of ['query', 'import-verification']) {
|
|
115
|
+
try {
|
|
116
|
+
const candidate = await readJournal(join(ownerDirectory, 'attempts', id, kind + '.json'));
|
|
117
|
+
if (marker || candidate.kind !== kind)
|
|
118
|
+
throw pending();
|
|
119
|
+
marker = candidate;
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (error.code !== 'ENOENT')
|
|
123
|
+
throw pending();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!marker || marker.schemaVersion !== 1 || !['query', 'import-verification'].includes(marker.kind) || marker.attemptId !== id || marker.ownerIncarnation !== record.incarnation)
|
|
127
|
+
throw pending();
|
|
128
|
+
transientIds.set(id, marker.kind);
|
|
129
|
+
}
|
|
130
|
+
if (!await proveAttemptCleanup(ownerDirectory, record.incarnation, id))
|
|
131
|
+
throw pending();
|
|
132
|
+
}
|
|
133
|
+
const staging = join(store.root, 'staging');
|
|
134
|
+
for (const entry of await readdir(staging)) {
|
|
135
|
+
if (entry.startsWith('query-') || entry.startsWith('import-verification-')) {
|
|
136
|
+
const path = join(staging, entry);
|
|
137
|
+
await directory(path);
|
|
138
|
+
let marker;
|
|
139
|
+
try {
|
|
140
|
+
marker = await readJournal(join(path, 'ownership.json'));
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
throw pending();
|
|
144
|
+
}
|
|
145
|
+
if (marker.schemaVersion !== 1 || !['query', 'import-verification'].includes(marker.kind) || typeof marker.requestId !== 'string' || entry !== marker.kind + '-' + marker.requestId || typeof marker.ownerIncarnation !== 'string')
|
|
146
|
+
throw pending();
|
|
147
|
+
if (marker.ownerIncarnation === record.incarnation) {
|
|
148
|
+
if (transientIds.get(marker.requestId) !== marker.kind)
|
|
149
|
+
throw pending();
|
|
150
|
+
await rm(path, { recursive: true });
|
|
151
|
+
await syncDir(staging);
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!entry.startsWith('capture-'))
|
|
156
|
+
continue;
|
|
157
|
+
const path = join(staging, entry);
|
|
158
|
+
await directory(path);
|
|
159
|
+
let marker;
|
|
160
|
+
try {
|
|
161
|
+
marker = await readJournal(join(path, 'ownership.json'));
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
throw pending();
|
|
165
|
+
}
|
|
166
|
+
if (marker.schemaVersion !== 1 || typeof marker.requestId !== 'string')
|
|
167
|
+
throw pending();
|
|
168
|
+
if (attemptIds.has(marker.requestId)) {
|
|
169
|
+
await rm(path, { recursive: true });
|
|
170
|
+
await syncDir(staging);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (owner)
|
|
174
|
+
store.recoverOwner(owner, attempts.filter(a => a.cleanup === 'pending').map(a => a.attemptId));
|
|
175
|
+
await sameFile(file, lock);
|
|
176
|
+
await unlink(lock);
|
|
177
|
+
await syncDir(join(store.root, 'runtime'));
|
|
178
|
+
return { recovered: true, attempts: attempts.length };
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await sameFile(file, recovery);
|
|
182
|
+
await unlink(recovery);
|
|
183
|
+
await syncDir(join(store.root, 'runtime'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ErrorEnvelope, JobCheckpoint } from '../../contracts/index.js';
|
|
2
|
+
import type { LibraryStore, AttemptToken } from '../store/index.js';
|
|
3
|
+
import { type ProcessRunner } from './supervisor.js';
|
|
4
|
+
export type WorkerFailure = ErrorEnvelope & {
|
|
5
|
+
cleanup: 'settled' | 'pending';
|
|
6
|
+
};
|
|
7
|
+
export type WorkerOutcome = ({
|
|
8
|
+
ok: true;
|
|
9
|
+
} | ErrorEnvelope) & {
|
|
10
|
+
cleanup: 'settled' | 'pending';
|
|
11
|
+
};
|
|
12
|
+
export interface HandlerContext {
|
|
13
|
+
store: LibraryStore;
|
|
14
|
+
attempt: AttemptToken;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
runProcess: ProcessRunner;
|
|
17
|
+
checkpoint: (stage: JobCheckpoint['stage']) => Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/** Handlers finish and settle storage only after all process and artifact cleanup is established. */
|
|
20
|
+
export type JobHandler<T extends WorkerOutcome = WorkerOutcome> = (context: HandlerContext) => Promise<T>;
|
|
21
|
+
export interface QueueOptions<T extends WorkerOutcome> {
|
|
22
|
+
handlers: Readonly<Record<string, JobHandler<T>>>;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
jobId?: string;
|
|
25
|
+
onResult?: (result: T | WorkerFailure) => void;
|
|
26
|
+
onCheckpoint?: (attempt: AttemptToken, stage: JobCheckpoint['stage']) => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export declare function drainQueue<T extends WorkerOutcome>(store: LibraryStore, options: QueueOptions<T>): Promise<Array<T | WorkerFailure>>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { failure } from '../acquisition/errors.js';
|
|
3
|
+
import { acquireWorkerCustody, proveAttemptCleanup } from './custody.js';
|
|
4
|
+
import { supervisedRunner } from './supervisor.js';
|
|
5
|
+
import { writeJournal, directory } from './journal.js';
|
|
6
|
+
export async function drainQueue(store, options) {
|
|
7
|
+
const custody = await acquireWorkerCustody(store.root);
|
|
8
|
+
let owner, settled = true;
|
|
9
|
+
const results = [];
|
|
10
|
+
try {
|
|
11
|
+
owner = store.acquireOwner(custody.incarnation);
|
|
12
|
+
while (!options.signal?.aborted) {
|
|
13
|
+
const attempt = options.jobId ? store.claimJob(owner, options.jobId) : store.claimNext(owner);
|
|
14
|
+
if (!attempt)
|
|
15
|
+
break;
|
|
16
|
+
settled = false;
|
|
17
|
+
const attemptDirectory = await directory(join(custody.directory, 'attempts', attempt.attemptId));
|
|
18
|
+
const checkpoint = async (stage) => { store.checkpointAttempt(attempt, stage); await writeJournal(join(attemptDirectory, 'checkpoint.json'), { schemaVersion: 1, ...attempt, stage }); await options.onCheckpoint?.(attempt, stage); };
|
|
19
|
+
await checkpoint('acquiring');
|
|
20
|
+
const controller = new AbortController(), cancel = () => { store.requestCancellation(attempt.jobId); controller.abort(); };
|
|
21
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
22
|
+
const poll = setInterval(() => { try {
|
|
23
|
+
if (store.jobCancellationRequested(attempt.jobId))
|
|
24
|
+
controller.abort();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
controller.abort();
|
|
28
|
+
} }, 100);
|
|
29
|
+
try {
|
|
30
|
+
if (options.signal?.aborted)
|
|
31
|
+
cancel();
|
|
32
|
+
const source = store.sourceForJob(attempt.jobId), handler = source && options.handlers[source.kind];
|
|
33
|
+
if (!handler) {
|
|
34
|
+
store.failAttempt(attempt, 'unsupported');
|
|
35
|
+
store.settleAttempt(attempt);
|
|
36
|
+
settled = true;
|
|
37
|
+
const result = { ...failure(attempt.attemptId, 'unsupported'), cleanup: 'settled' };
|
|
38
|
+
results.push(result);
|
|
39
|
+
options.onResult?.(result);
|
|
40
|
+
if (options.jobId)
|
|
41
|
+
break;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const result = await handler({ store, attempt, signal: controller.signal, runProcess: supervisedRunner(custody.directory, custody.incarnation, attempt.attemptId), checkpoint });
|
|
45
|
+
results.push(result);
|
|
46
|
+
options.onResult?.(result);
|
|
47
|
+
const state = store.ownerAttempts(owner).find(a => a.attemptId === attempt.attemptId);
|
|
48
|
+
settled = result.cleanup === 'settled' && state?.cleanup === 'settled' && await proveAttemptCleanup(custody.directory, custody.incarnation, attempt.attemptId);
|
|
49
|
+
if (!settled || options.jobId)
|
|
50
|
+
break;
|
|
51
|
+
if (!result.ok && result.retryable && ['unavailable', 'rate-limited'].includes(result.code)) {
|
|
52
|
+
const job = store.listJobs().find(j => j.id === attempt.jobId), count = Number(job?.attempt_count ?? 3);
|
|
53
|
+
if (count < 3)
|
|
54
|
+
store.retryJob(attempt.jobId, { automatic: true, notBefore: new Date(Date.now() + Math.min(60000, 1000 * 2 ** count)).toISOString() });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
clearInterval(poll);
|
|
59
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!settled)
|
|
63
|
+
throw Object.assign(Error('cleanup-required'), { code: 'cleanup-required' });
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
if (owner && settled) {
|
|
68
|
+
try {
|
|
69
|
+
store.releaseOwner(owner);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
settled = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (settled)
|
|
76
|
+
await custody.release();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
export async function processIdentity(pid) {
|
|
5
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
6
|
+
throw Error('invalid-input');
|
|
7
|
+
try {
|
|
8
|
+
const { stdout } = await exec('/bin/ps', ['-p', String(pid), '-o', 'uid=', '-o', 'lstart='], { timeout: 3000, maxBuffer: 4096, env: { PATH: '/usr/bin:/bin', LC_ALL: 'C' } });
|
|
9
|
+
const match = /^\s*(\d+)\s+(.+?)\s*$/.exec(stdout);
|
|
10
|
+
if (!match)
|
|
11
|
+
throw Error('unavailable');
|
|
12
|
+
return { pid, uid: Number(match[1]), started: match[2] };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error.code === 1) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
}
|
|
19
|
+
catch (e) {
|
|
20
|
+
if (e.code === 'ESRCH')
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
throw Object.assign(Error('unavailable'), { code: 'unavailable' });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function identityAlive(identity) { const current = await processIdentity(identity.pid); return current !== null && current.uid === identity.uid && current.started === identity.started; }
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const identifier: (value: string) => string;
|
|
2
|
+
export declare function syncDir(path: string): Promise<void>;
|
|
3
|
+
export declare function directory(path: string): Promise<string>;
|
|
4
|
+
export declare function writeJournal(path: string, value: unknown): Promise<void>;
|
|
5
|
+
export declare function readJournal(path: string): Promise<any>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { open, mkdir, rename, lstat, realpath } from 'node:fs/promises';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
export const identifier = (value) => { if (!/^[a-zA-Z0-9_-]{1,128}$/.test(value))
|
|
6
|
+
throw Object.assign(Error('invalid-input'), { code: 'invalid-input' }); return value; };
|
|
7
|
+
export async function syncDir(path) { const f = await open(path, constants.O_RDONLY); try {
|
|
8
|
+
await f.sync();
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
await f.close();
|
|
12
|
+
} }
|
|
13
|
+
export async function directory(path) { await mkdir(path, { recursive: true, mode: 0o700 }); const stat = await lstat(path); if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(path) !== path || stat.uid !== process.getuid?.() || (stat.mode & 0o077) !== 0)
|
|
14
|
+
throw Object.assign(Error('integrity'), { code: 'integrity' }); return path; }
|
|
15
|
+
export async function writeJournal(path, value) { const temp = join(dirname(path), '.' + randomUUID() + '.tmp'); const file = await open(temp, 'wx', 0o600); try {
|
|
16
|
+
await file.writeFile(JSON.stringify(value));
|
|
17
|
+
await file.sync();
|
|
18
|
+
}
|
|
19
|
+
finally {
|
|
20
|
+
await file.close();
|
|
21
|
+
} await rename(temp, path); await syncDir(dirname(path)); }
|
|
22
|
+
export async function readJournal(path) { const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW); try {
|
|
23
|
+
const s = await file.stat();
|
|
24
|
+
if (!s.isFile() || s.uid !== process.getuid?.() || (s.mode & 0o077) !== 0 || s.size > 65536)
|
|
25
|
+
throw Error('integrity');
|
|
26
|
+
return JSON.parse(await file.readFile('utf8'));
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await file.close();
|
|
30
|
+
} }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { runOwned } from '../acquisition/process.js';
|
|
3
|
+
import { readJournal, writeJournal } from './journal.js';
|
|
4
|
+
import { processIdentity } from './identity.js';
|
|
5
|
+
// This process owns collection even when its requesting worker is killed.
|
|
6
|
+
const [root, ticketId] = process.argv.slice(2);
|
|
7
|
+
if (!root || !ticketId || !process.send)
|
|
8
|
+
process.exit(2);
|
|
9
|
+
const pending = await readJournal(join(root, ticketId + '.pending.json'));
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let started = false;
|
|
12
|
+
const abort = () => controller.abort();
|
|
13
|
+
process.on('disconnect', abort);
|
|
14
|
+
process.on('SIGTERM', abort);
|
|
15
|
+
process.on('SIGINT', abort);
|
|
16
|
+
await writeJournal(join(root, ticketId + '.supervisor.json'), { ...pending, identity: await processIdentity(process.pid) });
|
|
17
|
+
const deadline = setTimeout(() => { if (!started)
|
|
18
|
+
process.exit(2); }, 10000);
|
|
19
|
+
process.once('message', async (message) => {
|
|
20
|
+
if (message?.type !== 'run' || message.ticket !== pending.ticket || message.attemptId !== pending.attemptId)
|
|
21
|
+
process.exit(2);
|
|
22
|
+
started = true;
|
|
23
|
+
clearTimeout(deadline);
|
|
24
|
+
const result = await runOwned(message.executable, message.args, { ...message.options, signal: controller.signal });
|
|
25
|
+
if (result.cleanup === 'settled')
|
|
26
|
+
await writeJournal(join(root, ticketId + '.settled.json'), { schemaVersion: 1, ticket: pending.ticket, attemptId: pending.attemptId, ownerIncarnation: pending.ownerIncarnation, cleanup: 'settled' });
|
|
27
|
+
if (process.connected)
|
|
28
|
+
process.send?.({ type: 'result', result }, undefined, undefined, () => process.disconnect());
|
|
29
|
+
});
|
|
30
|
+
process.on('message', (message) => { if (message?.type === 'cancel')
|
|
31
|
+
abort(); });
|
|
32
|
+
if (process.connected)
|
|
33
|
+
process.send({ type: 'ready', ticket: pending.ticket });
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { fork } from 'node:child_process';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { directory, identifier, readJournal, writeJournal } from './journal.js';
|
|
6
|
+
export function supervisedRunner(ownerDirectory, ownerIncarnation, attemptId) {
|
|
7
|
+
identifier(ownerIncarnation);
|
|
8
|
+
identifier(attemptId);
|
|
9
|
+
return async (executable, args, options) => {
|
|
10
|
+
if (options.signal?.aborted)
|
|
11
|
+
return { code: null, stdout: '', stopped: 'cancelled', cleanup: 'settled' };
|
|
12
|
+
const root = await directory(join(ownerDirectory, 'attempts', attemptId)), ticket = randomUUID();
|
|
13
|
+
await writeJournal(join(root, ticket + '.pending.json'), { schemaVersion: 1, ticket, attemptId, ownerIncarnation });
|
|
14
|
+
const source = import.meta.url.endsWith('.ts'), entry = fileURLToPath(new URL(source ? './supervisor-child.ts' : './supervisor-child.js', import.meta.url));
|
|
15
|
+
return new Promise(resolve => {
|
|
16
|
+
const child = fork(entry, [root, ticket], { detached: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], execArgv: source ? ['--import', import.meta.resolve('tsx')] : [], env: { PATH: '/usr/bin:/bin', LANG: 'en_US.UTF-8' } });
|
|
17
|
+
let result, finished = false, dispatched = false;
|
|
18
|
+
const cancel = () => { if (dispatched && child.connected)
|
|
19
|
+
child.send({ type: 'cancel' }, () => { }); };
|
|
20
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
21
|
+
child.on('message', (message) => {
|
|
22
|
+
if (message?.type === 'result' && dispatched)
|
|
23
|
+
result = message.result;
|
|
24
|
+
if (message?.type === 'ready' && message.ticket === ticket && !dispatched) {
|
|
25
|
+
dispatched = true;
|
|
26
|
+
child.send({ type: 'run', ticket, attemptId, executable, args, options: { cwd: options.cwd, env: options.env, deadlineMs: options.deadlineMs, maxOutputBytes: options.maxOutputBytes, graceMs: options.graceMs } }, () => { if (options.signal?.aborted)
|
|
27
|
+
cancel(); });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
const finish = async () => {
|
|
31
|
+
if (finished)
|
|
32
|
+
return;
|
|
33
|
+
finished = true;
|
|
34
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
35
|
+
let settled = false;
|
|
36
|
+
try {
|
|
37
|
+
const receipt = await readJournal(join(root, ticket + '.settled.json'));
|
|
38
|
+
settled = receipt.ticket === ticket && receipt.attemptId === attemptId && receipt.ownerIncarnation === ownerIncarnation && receipt.cleanup === 'settled';
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
resolve(result && settled ? result : { code: null, stdout: '', stopped: options.signal?.aborted ? 'cancelled' : 'unavailable', cleanup: 'pending' });
|
|
42
|
+
};
|
|
43
|
+
child.once('error', () => void finish());
|
|
44
|
+
child.once('exit', () => void finish());
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"architecture": "arm64",
|
|
3
|
+
"artifacts": [
|
|
4
|
+
{
|
|
5
|
+
"bytes": 1421832,
|
|
6
|
+
"name": "sponge-speech-macos26-arm64",
|
|
7
|
+
"role": "asr-helper",
|
|
8
|
+
"sha256": "aee98a6f8f3273237e1ec3c899021a2e3d826a14af843e96a770db25f7b1d7ce"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"bytes": 35920,
|
|
12
|
+
"name": "sponge-embedding-macos26-arm64",
|
|
13
|
+
"role": "embedding-helper",
|
|
14
|
+
"sha256": "aa5a1bffb45ff2a8823a7c7dd013400ce447d6f080c716eae3f1aba9e9ab7489"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"bytes": 137802416,
|
|
18
|
+
"name": "libCLiteRTLM_mac.dylib",
|
|
19
|
+
"role": "embedding-library",
|
|
20
|
+
"sha256": "9526db9ee35e7eb4a5a58ac1e53330032f22a13890bd4ee01818fb380a57a362"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"bytes": 23486,
|
|
24
|
+
"name": "native-NOTICES.txt",
|
|
25
|
+
"role": "notices",
|
|
26
|
+
"sha256": "6e62f14bd094001b8da42af6fc42877e51b0d5616ae960917ef214fc45767088"
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"build": {
|
|
30
|
+
"asrSourceSha256": "945667d78c598c30293f666d6605c9af26818b8d313c2219ac95985807c00ffc",
|
|
31
|
+
"clangVersion": "Apple clang version 21.0.0 (clang-2100.1.1.101)",
|
|
32
|
+
"embeddingSourceSha256": "e31b9a70865c3c7195d201233a2a240fc7ef2aa40b4ae63291d1277262456c69",
|
|
33
|
+
"swiftVersion": "Apple Swift version 6.3.3 (swiftlang-6.3.3.1.3 clang-2100.1.1.101)"
|
|
34
|
+
},
|
|
35
|
+
"minimumMacOS": "26.0",
|
|
36
|
+
"packageVersion": "0.2.0",
|
|
37
|
+
"platform": "darwin",
|
|
38
|
+
"schemaVersion": 1,
|
|
39
|
+
"sourceCommit": "439d0bd72f834a26a248ce2e9fee1a6a6bffaf57"
|
|
40
|
+
}
|