@hraness/sponge 0.1.2 → 0.2.1
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 +164 -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 +54 -0
- package/dist/src/acquisition/runtime.js +218 -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 +31 -5
package/README.md
CHANGED
|
@@ -1,30 +1,177 @@
|
|
|
1
1
|
# Sponge
|
|
2
2
|
|
|
3
|
-
Sponge is
|
|
3
|
+
Sponge is a local library for articles, images, PDFs and media transcripts. Capturing runs on your machine through Ghostget; saved
|
|
4
|
+
content stays in a private SQLite database and adjacent hashed asset files.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
Version 0.2.0 introduces the local-library CLI; earlier 0.1.x commands are inactive.
|
|
7
|
+
The native desktop app and cloud sync are not available yet.
|
|
8
|
+
The website is [sponge.computer](https://sponge.computer). Previous research lives
|
|
9
|
+
at [`hraness/sponge-research`](https://github.com/hraness/sponge-research).
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
[`hraness/sponge-research`](https://github.com/hraness/sponge-research).
|
|
11
|
+
## CLI
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
Node.js 24.20 or newer is required. Capture currently targets Apple Silicon
|
|
14
|
+
macOS; read and queue interfaces remain portable.
|
|
15
|
+
|
|
16
|
+
Install the released package with `npm install -g @hraness/sponge`.
|
|
17
|
+
For development from this checkout:
|
|
13
18
|
|
|
14
19
|
```sh
|
|
15
|
-
cd site
|
|
16
20
|
npm ci
|
|
17
|
-
npm run
|
|
21
|
+
npm run build
|
|
22
|
+
node bin/sponge.js init
|
|
23
|
+
node bin/sponge.js setup --capture
|
|
24
|
+
node bin/sponge.js add https://example.com/article --wait
|
|
25
|
+
node bin/sponge.js list
|
|
26
|
+
node bin/sponge.js show ITEM
|
|
27
|
+
node bin/sponge.js search 'your query'
|
|
18
28
|
```
|
|
19
29
|
|
|
20
|
-
The
|
|
21
|
-
|
|
30
|
+
The default library is `~/.local/share/sponge`. Use `--library /absolute/path`
|
|
31
|
+
for another private library. `add` without `--wait` queues a link; `run --drain`
|
|
32
|
+
processes queued links sequentially. `refresh ITEM --wait` attempts a new
|
|
33
|
+
revision and preserves the old one if capture fails. `--json` provides structured
|
|
34
|
+
output. Errors are categorical JSON on stderr; exit codes are 0 success,
|
|
35
|
+
1 failure, 2 invalid input, 3 partial content, 4 action required, and 130 cancelled.
|
|
36
|
+
|
|
37
|
+
Capture setup explicitly downloads pinned Ghostget, Wordcell and Bun artifacts
|
|
38
|
+
plus frozen dependencies. Read/search never download runtimes or contact source
|
|
39
|
+
websites. Setup reports disk requirements, a monitored 2 GiB temporary installation
|
|
40
|
+
limit, a 1 GiB retained runtime limit and a 30-minute dependency installation
|
|
41
|
+
deadline. Temporary dependency caches are removed
|
|
42
|
+
before the runtime is promoted.
|
|
43
|
+
`doctor` reports runtime integrity and pending cleanup. Private-network capture
|
|
44
|
+
requires `--allow-private-network` with `--wait` or `run --drain`.
|
|
45
|
+
|
|
46
|
+
Images use `sponge-asset:SHA256` references. `asset ITEM SHA256 > image.png`
|
|
47
|
+
retrieves verified bytes offline; `asset ITEM SHA256 --json` returns its metadata.
|
|
48
|
+
Unresolved remote images become unavailable markers, and captures retain partial
|
|
49
|
+
status. `delete ITEM` hides content and removes it from search. `queue status`,
|
|
50
|
+
`queue cancel JOB`, and `queue retry JOB` manage work. After an interruption,
|
|
51
|
+
`queue recover` verifies that the old worker and its subprocesses have stopped
|
|
52
|
+
before making work available again. Uncertain cleanup remains blocked.
|
|
53
|
+
|
|
54
|
+
### PDFs and media
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
node bin/sponge.js add /absolute/path/paper.pdf --kind pdf --wait
|
|
58
|
+
node bin/sponge.js add https://example.com/episode --kind media --wait
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
PDF capture requires Poppler (`pdfinfo` and `pdftohtml`). Tesseract enables
|
|
62
|
+
optional local OCR. Provider media acquisition requires yt-dlp; audio validation
|
|
63
|
+
requires ffprobe from FFmpeg. Install these separately, for example with
|
|
64
|
+
`brew install poppler tesseract yt-dlp ffmpeg` on macOS. Capture setup does not
|
|
65
|
+
install system tools.
|
|
66
|
+
|
|
67
|
+
PDF capture retains the original and readable Markdown with page references.
|
|
68
|
+
Wordcell owns extraction and reports uncertain layout. Media capture prefers
|
|
69
|
+
available captions and retains timestamped transcripts, metadata and thumbnails.
|
|
70
|
+
Temporary audio is deleted after transcription and verified cleanup.
|
|
71
|
+
|
|
72
|
+
Local inference requires Apple Silicon and macOS 26 or newer. Release packages
|
|
73
|
+
include a manifest for optional native downloads; a source checkout without that
|
|
74
|
+
manifest reports `runtime-missing`. Installation never downloads models.
|
|
75
|
+
|
|
76
|
+
Native helpers are distributed from the private `hraness/sponge` repository.
|
|
77
|
+
For explicit native setup, provide `GH_TOKEN` (or `GITHUB_TOKEN`) with read access
|
|
78
|
+
to that repository's contents. Sponge never saves the token and sends it only to
|
|
79
|
+
GitHub's API; public model downloads do not receive it. With an authorized GitHub
|
|
80
|
+
CLI session, for example: `GH_TOKEN="$(gh auth token)" sponge setup --asr`.
|
|
81
|
+
Repeat with `--embedding` for search. Already verified caches work without a token.
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
sponge setup --asr
|
|
85
|
+
sponge add https://example.com/episode --kind media --wait --language en
|
|
86
|
+
sponge refresh ITEM --wait --language en --force-local
|
|
87
|
+
sponge setup --embedding
|
|
88
|
+
sponge index create
|
|
89
|
+
sponge index run
|
|
90
|
+
sponge index status GENERATION
|
|
91
|
+
sponge search 'your query'
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Local ASR currently supports English. `--force-local` bypasses provider captions.
|
|
95
|
+
Language choices apply to the current `--wait`, `run --drain`, or `worker serve`
|
|
96
|
+
invocation; queued-only submissions reject these flags. `worker install --language en`
|
|
97
|
+
saves the English ASR choice for background jobs. Search combines local
|
|
98
|
+
full-text and semantic results when a compatible index and model are available,
|
|
99
|
+
and reports when semantic search is unavailable. `--mode lexical` uses only
|
|
100
|
+
full-text search. Use `index create --profile NAME` and `search QUERY --profile NAME`
|
|
101
|
+
to keep separate search profiles. Model changes create separate index generations; a new one
|
|
102
|
+
becomes active only after it covers the current library. `index rollback
|
|
103
|
+
GENERATION` requires a complete generation compatible with the current revisions.
|
|
104
|
+
|
|
105
|
+
### Export, recovery and background work
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
sponge export ITEM --output /absolute/new/export-directory
|
|
109
|
+
sponge backup --output /absolute/new/library.sponge
|
|
110
|
+
sponge restore /absolute/library.sponge --into /absolute/new/library
|
|
111
|
+
sponge gc --dry-run
|
|
112
|
+
sponge gc --apply /absolute/path/to/report.json
|
|
113
|
+
sponge worker install --language en
|
|
114
|
+
sponge worker status
|
|
115
|
+
sponge worker uninstall
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Exports contain Markdown, images and saved notes. Backups preserve revisions,
|
|
119
|
+
assets, reading state and notes. Restore requires a new directory, verifies every
|
|
120
|
+
file and leaves connections unresolved; runtimes and credentials are excluded.
|
|
121
|
+
Garbage collection removes only unreferenced blobs after rechecking the report.
|
|
122
|
+
|
|
123
|
+
The background worker is opt-in and processes work sequentially. Its LaunchAgent
|
|
124
|
+
pins the installed command; after an upgrade, reinstall the worker to update that
|
|
125
|
+
binding. Uninstall stops the worker and preserves the library. In-session startup
|
|
126
|
+
and cleanup are checked; fresh-login startup remains unqualified. See
|
|
127
|
+
[importing existing captures](docs/importing.md) for a preview-first import flow.
|
|
128
|
+
|
|
129
|
+
### Selected authenticated connections
|
|
130
|
+
|
|
131
|
+
The initial development interface accepts private `connections.json` in the
|
|
132
|
+
library (mode `0600`). It contains references to existing Ghostget accounts,
|
|
133
|
+
never cookies or tokens. A future setup phase adds connection-management commands.
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{"schemaVersion":1,"connections":[{"id":"personal","kind":"account","stateHome":"/absolute/ghostget-auth","accountId":"personal"}]}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Use `add URL --connection personal --wait`. A public capture never silently
|
|
140
|
+
selects your browser or account. For articles, attached browser connections use
|
|
141
|
+
`kind: "cdp"` and an explicitly selected local `endpoint`; attached browsers remain
|
|
142
|
+
open. PDF and media capture support selected Ghostget accounts, not CDP. Remote
|
|
143
|
+
PDF acquisition requires public network addresses; `--allow-private-network` is
|
|
144
|
+
not supported for PDF capture. Live authenticated remote-PDF qualification remains
|
|
145
|
+
pending; failures never fall back to a different account or a public request.
|
|
146
|
+
Changing connection identity creates a distinct private source realm.
|
|
147
|
+
|
|
148
|
+
## Design and validation
|
|
149
|
+
|
|
150
|
+
See the [design](docs/local-library-design.md),
|
|
151
|
+
[phase plan](plans/local-library-cli.md), and
|
|
152
|
+
[evidence inventory](docs/local-library-evidence.md). PDF improvements belong
|
|
153
|
+
upstream in Wordcell; Sponge owns library ingestion and offline reading.
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
npm run typecheck
|
|
157
|
+
npm test
|
|
158
|
+
npm run build
|
|
159
|
+
npm run verify:package
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Use the repository's host scheduler for required local resource lanes. The
|
|
163
|
+
installed capture qualification also requires a separately started controlled
|
|
164
|
+
fixture server, its allocated `PORT`, and an installed `SPONGE_INSTALLED_CLI`:
|
|
165
|
+
`npm run verify:steel-thread -- --installed`. It tests public/authenticated
|
|
166
|
+
capture, restart, offline read/search/assets and failed refresh preservation.
|
|
167
|
+
|
|
168
|
+
The website lives in `site/`: `npm ci`, `npm run dev`, and `npm run build` there.
|
|
22
169
|
|
|
23
170
|
## Releases
|
|
24
171
|
|
|
25
|
-
After a version bump
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
172
|
+
After a version bump reaches protected `main` with green CI, the release tagger
|
|
173
|
+
creates an annotated version tag. Release CI builds, packs and installs the
|
|
174
|
+
exact artifact, then publishes immutable GitHub Release assets. The same verified
|
|
175
|
+
tarball is mirrored to npm through trusted publishing. No runtime installation
|
|
176
|
+
runs as an npm lifecycle hook. The repository is private, so npm provenance is
|
|
177
|
+
disabled. The website stays out of the package.
|
package/bin/sponge.js
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
const [major,minor]=process.versions.node.split('.').map(Number);
|
|
4
|
+
if(major<24||(major===24&&minor<20)){
|
|
5
|
+
process.stderr.write('Sponge requires Node.js 24.20 or newer.\n');process.exitCode=2;
|
|
6
|
+
}else{
|
|
7
|
+
const {version}=createRequire(import.meta.url)('../package.json');
|
|
8
|
+
const {main}=await import('../dist/src/cli/main.js');
|
|
9
|
+
process.exitCode=await main(process.argv.slice(2),version);
|
|
10
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/** Version 1 public boundaries, frozen at G00. No browser credentials cross these interfaces. */
|
|
2
|
+
export type Sha256 = string;
|
|
3
|
+
export interface Envelope {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
requestId: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const exitCodes: {
|
|
8
|
+
readonly success: 0;
|
|
9
|
+
readonly failure: 1;
|
|
10
|
+
readonly 'invalid-input': 2;
|
|
11
|
+
readonly partial: 3;
|
|
12
|
+
readonly 'action-required': 4;
|
|
13
|
+
readonly cancelled: 130;
|
|
14
|
+
};
|
|
15
|
+
export type ErrorCode = 'grant-permission' | 'repair-auth' | 'unsupported' | 'unavailable' | 'rate-limited' | 'partial' | 'cleanup-required' | 'cancelled' | 'invalid-input' | 'unclassified' | 'integrity' | 'limit-exceeded' | 'runtime-missing' | 'schema-newer' | 'owner-busy';
|
|
16
|
+
export interface ErrorEnvelope extends Envelope {
|
|
17
|
+
ok: false;
|
|
18
|
+
code: ErrorCode;
|
|
19
|
+
retryable: boolean;
|
|
20
|
+
retryAfterMs?: number;
|
|
21
|
+
/** Categorical, never upstream prose, source content, or credentials. */
|
|
22
|
+
phase?: 'setup' | 'acquisition' | 'verification' | 'storage' | 'inference' | 'cleanup';
|
|
23
|
+
}
|
|
24
|
+
export type MetadataWarning = 'input-limit' | 'claim-limit' | 'value-limit' | 'json-limit' | 'json-invalid';
|
|
25
|
+
export interface MetadataClaim {
|
|
26
|
+
field: string;
|
|
27
|
+
value: string | string[];
|
|
28
|
+
source: 'html' | 'opengraph' | 'twitter' | 'jsonld' | 'pdf' | 'media';
|
|
29
|
+
observedAt: string;
|
|
30
|
+
rule?: string;
|
|
31
|
+
timezoneKnown?: boolean;
|
|
32
|
+
observed?: string | string[];
|
|
33
|
+
locator?: string;
|
|
34
|
+
selected?: boolean;
|
|
35
|
+
extractionVersion?: string;
|
|
36
|
+
datePrecision?: 'date' | 'datetime' | 'invalid';
|
|
37
|
+
}
|
|
38
|
+
export interface Asset {
|
|
39
|
+
sha256: Sha256;
|
|
40
|
+
relativePath: string;
|
|
41
|
+
mediaType: string;
|
|
42
|
+
bytes: number;
|
|
43
|
+
}
|
|
44
|
+
export interface CaptureResult extends Envelope {
|
|
45
|
+
kind: 'article' | 'pdf';
|
|
46
|
+
status: 'complete' | 'partial';
|
|
47
|
+
sourceUrl: string;
|
|
48
|
+
markdown: Asset;
|
|
49
|
+
assets: Asset[];
|
|
50
|
+
sourcePdf?: Asset;
|
|
51
|
+
metadata: MetadataClaim[];
|
|
52
|
+
metadataWarnings?: MetadataWarning[];
|
|
53
|
+
warnings: ErrorCode[];
|
|
54
|
+
extractor: {
|
|
55
|
+
name: string;
|
|
56
|
+
version: string;
|
|
57
|
+
};
|
|
58
|
+
manifestSha256: Sha256;
|
|
59
|
+
completeness: {
|
|
60
|
+
text: 'complete' | 'partial';
|
|
61
|
+
images: 'complete' | 'partial' | 'not-requested';
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export interface Transcript extends Envelope {
|
|
65
|
+
source: 'human-captions' | 'automatic-captions' | 'local-asr';
|
|
66
|
+
language: string;
|
|
67
|
+
modelFingerprint?: Sha256;
|
|
68
|
+
segments: {
|
|
69
|
+
startMs: number;
|
|
70
|
+
endMs: number;
|
|
71
|
+
text: string;
|
|
72
|
+
}[];
|
|
73
|
+
cleanup: 'settled' | 'pending';
|
|
74
|
+
}
|
|
75
|
+
export interface ModelManifest extends Envelope {
|
|
76
|
+
id: string;
|
|
77
|
+
version: string;
|
|
78
|
+
license: string;
|
|
79
|
+
runtime: {
|
|
80
|
+
name: string;
|
|
81
|
+
version: string;
|
|
82
|
+
sha256: Sha256;
|
|
83
|
+
};
|
|
84
|
+
artifacts: {
|
|
85
|
+
url: string;
|
|
86
|
+
sha256: Sha256;
|
|
87
|
+
bytes: number;
|
|
88
|
+
}[];
|
|
89
|
+
platforms: {
|
|
90
|
+
os: string;
|
|
91
|
+
arch: string;
|
|
92
|
+
minimumOs: string;
|
|
93
|
+
}[];
|
|
94
|
+
}
|
|
95
|
+
export interface EmbeddingIdentity {
|
|
96
|
+
modelSha256: Sha256;
|
|
97
|
+
runtimeSha256: Sha256;
|
|
98
|
+
tokenizerSha256: Sha256;
|
|
99
|
+
dimensions: number;
|
|
100
|
+
modalities: ('text' | 'image')[];
|
|
101
|
+
normalized: boolean;
|
|
102
|
+
metric: 'cosine';
|
|
103
|
+
pooling: string;
|
|
104
|
+
quantization: string;
|
|
105
|
+
backend: string;
|
|
106
|
+
queryTemplateSha256: Sha256;
|
|
107
|
+
documentTemplateSha256: Sha256;
|
|
108
|
+
preprocessingSha256: Sha256;
|
|
109
|
+
}
|
|
110
|
+
export interface EmbeddingSpace extends Envelope, EmbeddingIdentity {
|
|
111
|
+
fingerprint: Sha256;
|
|
112
|
+
}
|
|
113
|
+
export interface JobCheckpoint extends Envelope {
|
|
114
|
+
jobId: string;
|
|
115
|
+
attemptId: string;
|
|
116
|
+
ownerIncarnation: string;
|
|
117
|
+
fence: number;
|
|
118
|
+
stage: 'queued' | 'acquiring' | 'extracting' | 'indexing' | 'promoting' | 'cleanup' | 'settled';
|
|
119
|
+
status: 'pending' | 'running' | 'action-required' | 'cancelled' | 'failed' | 'complete';
|
|
120
|
+
artifacts: Asset[];
|
|
121
|
+
cleanup: 'settled' | 'pending';
|
|
122
|
+
}
|
|
123
|
+
export interface HelperBounds extends Envelope {
|
|
124
|
+
deadlineMs: number;
|
|
125
|
+
maxInputBytes: number;
|
|
126
|
+
maxOutputBytes: number;
|
|
127
|
+
}
|
|
128
|
+
/** Data-only file reference. The parent separately proves ownership and byte identity. */
|
|
129
|
+
export interface HelperFile {
|
|
130
|
+
ownedPath: string;
|
|
131
|
+
sha256: Sha256;
|
|
132
|
+
bytes: number;
|
|
133
|
+
}
|
|
134
|
+
export type HelperRequest = HelperBounds & ({
|
|
135
|
+
operation: 'transcribe';
|
|
136
|
+
input: HelperFile;
|
|
137
|
+
} | {
|
|
138
|
+
operation: 'embed-image';
|
|
139
|
+
input: HelperFile;
|
|
140
|
+
} | {
|
|
141
|
+
operation: 'embed-text';
|
|
142
|
+
input: {
|
|
143
|
+
text: string;
|
|
144
|
+
};
|
|
145
|
+
} | {
|
|
146
|
+
operation: 'cancel';
|
|
147
|
+
targetRequestId: string;
|
|
148
|
+
});
|
|
149
|
+
export declare const limits: Readonly<{
|
|
150
|
+
pdfBytes: number;
|
|
151
|
+
imageBytes: number;
|
|
152
|
+
imagePixels: 40000000;
|
|
153
|
+
imagesPerItem: 200;
|
|
154
|
+
mediaDurationMs: 3600000;
|
|
155
|
+
temporaryMediaBytes: number;
|
|
156
|
+
stagingBytes: number;
|
|
157
|
+
freeReserveBytes: number;
|
|
158
|
+
metadataBytes: number;
|
|
159
|
+
metadataClaims: 512;
|
|
160
|
+
markdownBytes: number;
|
|
161
|
+
captureDeadlineMs: 120000;
|
|
162
|
+
helperFrameBytes: number;
|
|
163
|
+
helperOutputBytes: number;
|
|
164
|
+
}>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const exitCodes = { success: 0, failure: 1, 'invalid-input': 2, partial: 3, 'action-required': 4, cancelled: 130 };
|
|
2
|
+
export const limits = Object.freeze({ pdfBytes: 100 * 1024 ** 2, imageBytes: 20 * 1024 ** 2,
|
|
3
|
+
imagePixels: 40_000_000, imagesPerItem: 200, mediaDurationMs: 3_600_000,
|
|
4
|
+
temporaryMediaBytes: 1024 ** 3, stagingBytes: 2 * 1024 ** 3, freeReserveBytes: 2 * 1024 ** 3,
|
|
5
|
+
metadataBytes: 1024 ** 2, metadataClaims: 512, markdownBytes: 16 * 1024 ** 2,
|
|
6
|
+
captureDeadlineMs: 120_000, helperFrameBytes: 64 * 1024, helperOutputBytes: 16 * 1024 ** 2 });
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Asset, MetadataClaim, MetadataWarning, CaptureResult, EmbeddingIdentity, ErrorEnvelope, ModelManifest, JobCheckpoint, HelperRequest } from './index.js';
|
|
2
|
+
/** Lexical confinement only. Consumers MUST additionally no-follow open/hash the bytes. */
|
|
3
|
+
export declare function validateAsset(value: unknown): asserts value is Asset;
|
|
4
|
+
export declare function validateCapture(value: unknown): asserts value is CaptureResult;
|
|
5
|
+
export declare function validateMetadataWarnings(value: unknown): asserts value is MetadataWarning[];
|
|
6
|
+
export declare function validateMetadataClaim(claim: unknown): asserts claim is MetadataClaim;
|
|
7
|
+
export declare function validateError(value: unknown): asserts value is ErrorEnvelope;
|
|
8
|
+
/** Fixed ordered tuple avoids key-order or irrelevant envelope fields changing a space. */
|
|
9
|
+
export declare function embeddingFingerprint(identity: EmbeddingIdentity): string;
|
|
10
|
+
/** Manifest validation is admission metadata, not proof of downloaded bytes. */
|
|
11
|
+
export declare function validateModelManifest(value: unknown): asserts value is ModelManifest;
|
|
12
|
+
/** Structural validation; storage separately enforces current-owner/fence transitions. */
|
|
13
|
+
export declare function validateJobCheckpoint(value: unknown): asserts value is JobCheckpoint;
|
|
14
|
+
export declare function validateHelperRequest(value: unknown): asserts value is HelperRequest;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { limits } from './index.js';
|
|
4
|
+
const digest = /^[a-f0-9]{64}$/;
|
|
5
|
+
function requireValue(ok) { if (!ok)
|
|
6
|
+
throw new Error('invalid-contract'); }
|
|
7
|
+
function record(value) { requireValue(value !== null && typeof value === 'object' && !Array.isArray(value)); }
|
|
8
|
+
function boundedString(value, max = 4096) { requireValue(typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\0')); }
|
|
9
|
+
function onlyKeys(value, keys) { requireValue(Object.keys(value).every(key => keys.includes(key))); }
|
|
10
|
+
function envelope(value) { requireValue(value.schemaVersion === 1); boundedString(value.requestId, 128); }
|
|
11
|
+
/** Lexical confinement only. Consumers MUST additionally no-follow open/hash the bytes. */
|
|
12
|
+
export function validateAsset(value) {
|
|
13
|
+
record(value);
|
|
14
|
+
requireValue(typeof value.sha256 === 'string' && digest.test(value.sha256));
|
|
15
|
+
boundedString(value.relativePath);
|
|
16
|
+
requireValue(!value.relativePath.startsWith('/') && !value.relativePath.includes('\\') && !value.relativePath.includes(':') && value.relativePath.split('/').every(p => p !== '' && p !== '.' && p !== '..'));
|
|
17
|
+
boundedString(value.mediaType, 128);
|
|
18
|
+
requireValue(Number.isSafeInteger(value.bytes) && Number(value.bytes) >= 0);
|
|
19
|
+
}
|
|
20
|
+
export function validateCapture(value) {
|
|
21
|
+
record(value);
|
|
22
|
+
envelope(value);
|
|
23
|
+
requireValue(value.kind === 'article' || value.kind === 'pdf');
|
|
24
|
+
requireValue(value.status === 'complete' || value.status === 'partial');
|
|
25
|
+
boundedString(value.sourceUrl, 16384);
|
|
26
|
+
requireValue(typeof value.manifestSha256 === 'string' && digest.test(value.manifestSha256));
|
|
27
|
+
validateAsset(value.markdown);
|
|
28
|
+
requireValue(value.markdown.bytes <= limits.markdownBytes && value.markdown.mediaType === 'text/markdown');
|
|
29
|
+
requireValue(Array.isArray(value.assets) && value.assets.length <= limits.imagesPerItem);
|
|
30
|
+
value.assets.forEach(asset => { validateAsset(asset); requireValue(asset.mediaType.startsWith('image/') && asset.bytes <= limits.imageBytes); });
|
|
31
|
+
if (value.sourcePdf !== undefined) {
|
|
32
|
+
validateAsset(value.sourcePdf);
|
|
33
|
+
requireValue(value.sourcePdf.mediaType === 'application/pdf' && value.sourcePdf.bytes <= limits.pdfBytes);
|
|
34
|
+
}
|
|
35
|
+
requireValue(value.kind !== 'pdf' || value.sourcePdf !== undefined);
|
|
36
|
+
record(value.completeness);
|
|
37
|
+
requireValue(typeof value.completeness.text === 'string' && ['complete', 'partial'].includes(String(value.completeness.text)));
|
|
38
|
+
requireValue(typeof value.completeness.images === 'string' && ['complete', 'partial', 'not-requested'].includes(String(value.completeness.images)));
|
|
39
|
+
requireValue(value.status !== 'complete' || (value.completeness.text === 'complete' && value.completeness.images !== 'partial'));
|
|
40
|
+
record(value.extractor);
|
|
41
|
+
boundedString(value.extractor.name, 128);
|
|
42
|
+
boundedString(value.extractor.version, 128);
|
|
43
|
+
requireValue(Array.isArray(value.warnings) && value.warnings.length <= 128);
|
|
44
|
+
value.warnings.forEach(v => requireValue(errorCodes.has(v)));
|
|
45
|
+
requireValue(Array.isArray(value.metadata) && value.metadata.length <= limits.metadataClaims && Buffer.byteLength(JSON.stringify(value.metadata)) <= limits.metadataBytes);
|
|
46
|
+
value.metadata.forEach(validateMetadataClaim);
|
|
47
|
+
if (value.metadataWarnings !== undefined)
|
|
48
|
+
validateMetadataWarnings(value.metadataWarnings);
|
|
49
|
+
}
|
|
50
|
+
export function validateMetadataWarnings(value) {
|
|
51
|
+
requireValue(Array.isArray(value) && value.length <= 5 && new Set(value).size === value.length && value.every(v => typeof v === 'string' && ['input-limit', 'claim-limit', 'value-limit', 'json-limit', 'json-invalid'].includes(v)));
|
|
52
|
+
}
|
|
53
|
+
export function validateMetadataClaim(claim) {
|
|
54
|
+
record(claim);
|
|
55
|
+
boundedString(claim.field, 128);
|
|
56
|
+
boundedString(claim.observedAt, 64);
|
|
57
|
+
requireValue(Number.isFinite(Date.parse(claim.observedAt)));
|
|
58
|
+
requireValue(typeof claim.source === 'string' && ['html', 'opengraph', 'twitter', 'jsonld', 'pdf', 'media'].includes(String(claim.source)));
|
|
59
|
+
if (Array.isArray(claim.value)) {
|
|
60
|
+
requireValue(claim.value.length <= 128);
|
|
61
|
+
claim.value.forEach(v => boundedString(v, 16384));
|
|
62
|
+
}
|
|
63
|
+
else
|
|
64
|
+
boundedString(claim.value, 16384);
|
|
65
|
+
if (claim.rule !== undefined)
|
|
66
|
+
boundedString(claim.rule, 256);
|
|
67
|
+
if (claim.timezoneKnown !== undefined)
|
|
68
|
+
requireValue(typeof claim.timezoneKnown === 'boolean');
|
|
69
|
+
if (claim.observed !== undefined) {
|
|
70
|
+
if (Array.isArray(claim.observed)) {
|
|
71
|
+
requireValue(claim.observed.length <= 128);
|
|
72
|
+
claim.observed.forEach(v => boundedString(v, 16384));
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
boundedString(claim.observed, 16384);
|
|
76
|
+
}
|
|
77
|
+
if (claim.locator !== undefined)
|
|
78
|
+
boundedString(claim.locator, 512);
|
|
79
|
+
if (claim.extractionVersion !== undefined)
|
|
80
|
+
boundedString(claim.extractionVersion, 128);
|
|
81
|
+
if (claim.selected !== undefined)
|
|
82
|
+
requireValue(typeof claim.selected === 'boolean');
|
|
83
|
+
if (claim.datePrecision !== undefined)
|
|
84
|
+
requireValue(typeof claim.datePrecision === 'string' && ['date', 'datetime', 'invalid'].includes(claim.datePrecision));
|
|
85
|
+
}
|
|
86
|
+
const errorCodes = new Set(['grant-permission', 'repair-auth', 'unsupported', 'unavailable', 'rate-limited', 'partial', 'cleanup-required', 'cancelled', 'invalid-input', 'unclassified', 'integrity', 'limit-exceeded', 'runtime-missing', 'schema-newer', 'owner-busy']);
|
|
87
|
+
export function validateError(value) {
|
|
88
|
+
record(value);
|
|
89
|
+
envelope(value);
|
|
90
|
+
onlyKeys(value, ['schemaVersion', 'requestId', 'ok', 'code', 'retryable', 'retryAfterMs', 'phase']);
|
|
91
|
+
requireValue(value.ok === false && typeof value.code === 'string' && errorCodes.has(value.code) && typeof value.retryable === 'boolean');
|
|
92
|
+
requireValue(!value.retryable || value.code === 'unavailable' || value.code === 'rate-limited');
|
|
93
|
+
if (value.phase !== undefined)
|
|
94
|
+
requireValue(typeof value.phase === 'string' && ['setup', 'acquisition', 'verification', 'storage', 'inference', 'cleanup'].includes(String(value.phase)));
|
|
95
|
+
if (value.retryAfterMs !== undefined)
|
|
96
|
+
requireValue(Number.isSafeInteger(value.retryAfterMs) && Number(value.retryAfterMs) >= 0 && Number(value.retryAfterMs) <= 86_400_000);
|
|
97
|
+
}
|
|
98
|
+
/** Fixed ordered tuple avoids key-order or irrelevant envelope fields changing a space. */
|
|
99
|
+
export function embeddingFingerprint(identity) {
|
|
100
|
+
const hashes = [identity.modelSha256, identity.runtimeSha256, identity.tokenizerSha256, identity.queryTemplateSha256, identity.documentTemplateSha256, identity.preprocessingSha256];
|
|
101
|
+
requireValue(hashes.every(h => typeof h === 'string' && digest.test(h)));
|
|
102
|
+
requireValue(Number.isSafeInteger(identity.dimensions) && identity.dimensions > 0 && identity.dimensions <= 16384);
|
|
103
|
+
requireValue(identity.metric === 'cosine' && typeof identity.normalized === 'boolean');
|
|
104
|
+
requireValue(Array.isArray(identity.modalities) && identity.modalities.length > 0 && identity.modalities.every(m => m === 'text' || m === 'image') && new Set(identity.modalities).size === identity.modalities.length);
|
|
105
|
+
[identity.pooling, identity.quantization, identity.backend].forEach(v => boundedString(v, 256));
|
|
106
|
+
return createHash('sha256').update(JSON.stringify(['sponge-space-v1', ...hashes, identity.dimensions, [...identity.modalities].sort(), identity.normalized, identity.metric, identity.pooling, identity.quantization, identity.backend])).digest('hex');
|
|
107
|
+
}
|
|
108
|
+
/** Manifest validation is admission metadata, not proof of downloaded bytes. */
|
|
109
|
+
export function validateModelManifest(value) {
|
|
110
|
+
record(value);
|
|
111
|
+
envelope(value);
|
|
112
|
+
boundedString(value.id, 128);
|
|
113
|
+
boundedString(value.version, 128);
|
|
114
|
+
boundedString(value.license, 256);
|
|
115
|
+
record(value.runtime);
|
|
116
|
+
boundedString(value.runtime.name, 128);
|
|
117
|
+
boundedString(value.runtime.version, 128);
|
|
118
|
+
requireValue(typeof value.runtime.sha256 === 'string' && digest.test(value.runtime.sha256));
|
|
119
|
+
requireValue(Array.isArray(value.artifacts) && value.artifacts.length > 0 && value.artifacts.length <= 128);
|
|
120
|
+
const urls = new Set();
|
|
121
|
+
let bytes = 0;
|
|
122
|
+
for (const artifact of value.artifacts) {
|
|
123
|
+
record(artifact);
|
|
124
|
+
boundedString(artifact.url, 16384);
|
|
125
|
+
const url = new URL(artifact.url);
|
|
126
|
+
requireValue(url.protocol === 'https:' && !url.username && !url.password && !url.hash && !urls.has(url.href));
|
|
127
|
+
urls.add(url.href);
|
|
128
|
+
requireValue(typeof artifact.sha256 === 'string' && digest.test(artifact.sha256));
|
|
129
|
+
requireValue(Number.isSafeInteger(artifact.bytes) && Number(artifact.bytes) > 0);
|
|
130
|
+
bytes += Number(artifact.bytes);
|
|
131
|
+
requireValue(Number.isSafeInteger(bytes));
|
|
132
|
+
}
|
|
133
|
+
requireValue(Array.isArray(value.platforms) && value.platforms.length > 0 && value.platforms.length <= 32);
|
|
134
|
+
for (const platform of value.platforms) {
|
|
135
|
+
record(platform);
|
|
136
|
+
boundedString(platform.os, 64);
|
|
137
|
+
boundedString(platform.arch, 64);
|
|
138
|
+
boundedString(platform.minimumOs, 64);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Structural validation; storage separately enforces current-owner/fence transitions. */
|
|
142
|
+
export function validateJobCheckpoint(value) {
|
|
143
|
+
record(value);
|
|
144
|
+
envelope(value);
|
|
145
|
+
for (const key of ['jobId', 'attemptId', 'ownerIncarnation'])
|
|
146
|
+
boundedString(value[key], 128);
|
|
147
|
+
requireValue(Number.isSafeInteger(value.fence) && Number(value.fence) > 0);
|
|
148
|
+
requireValue(typeof value.stage === 'string' && ['queued', 'acquiring', 'extracting', 'indexing', 'promoting', 'cleanup', 'settled'].includes(String(value.stage)));
|
|
149
|
+
requireValue(typeof value.status === 'string' && ['pending', 'running', 'action-required', 'cancelled', 'failed', 'complete'].includes(String(value.status)));
|
|
150
|
+
requireValue(value.cleanup === 'settled' || value.cleanup === 'pending');
|
|
151
|
+
requireValue(value.stage !== 'settled' || value.cleanup === 'settled');
|
|
152
|
+
requireValue(value.status !== 'complete' || value.stage === 'settled');
|
|
153
|
+
requireValue(value.stage !== 'queued' || value.status === 'pending');
|
|
154
|
+
requireValue(Array.isArray(value.artifacts) && value.artifacts.length <= 4096);
|
|
155
|
+
value.artifacts.forEach(validateAsset);
|
|
156
|
+
}
|
|
157
|
+
export function validateHelperRequest(value) {
|
|
158
|
+
record(value);
|
|
159
|
+
envelope(value);
|
|
160
|
+
requireValue(Buffer.byteLength(JSON.stringify(value)) <= limits.helperFrameBytes);
|
|
161
|
+
requireValue(Number.isSafeInteger(value.deadlineMs) && Number(value.deadlineMs) > 0 && Number(value.deadlineMs) <= 86_400_000);
|
|
162
|
+
requireValue(Number.isSafeInteger(value.maxInputBytes) && Number(value.maxInputBytes) > 0 && Number(value.maxInputBytes) <= limits.temporaryMediaBytes);
|
|
163
|
+
requireValue(Number.isSafeInteger(value.maxOutputBytes) && Number(value.maxOutputBytes) > 0 && Number(value.maxOutputBytes) <= limits.helperOutputBytes);
|
|
164
|
+
const keys = ['schemaVersion', 'requestId', 'operation', 'deadlineMs', 'maxInputBytes', 'maxOutputBytes'];
|
|
165
|
+
if (value.operation === 'cancel') {
|
|
166
|
+
onlyKeys(value, [...keys, 'targetRequestId']);
|
|
167
|
+
boundedString(value.targetRequestId, 128);
|
|
168
|
+
requireValue(value.targetRequestId !== value.requestId);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
onlyKeys(value, [...keys, 'input']);
|
|
172
|
+
record(value.input);
|
|
173
|
+
if (value.operation === 'embed-text') {
|
|
174
|
+
onlyKeys(value.input, ['text']);
|
|
175
|
+
boundedString(value.input.text, limits.markdownBytes);
|
|
176
|
+
requireValue(Buffer.byteLength(value.input.text) <= Number(value.maxInputBytes));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
requireValue(value.operation === 'transcribe' || value.operation === 'embed-image');
|
|
180
|
+
onlyKeys(value.input, ['ownedPath', 'sha256', 'bytes']);
|
|
181
|
+
boundedString(value.input.ownedPath);
|
|
182
|
+
requireValue(isAbsolute(value.input.ownedPath));
|
|
183
|
+
requireValue(typeof value.input.sha256 === 'string' && digest.test(value.input.sha256));
|
|
184
|
+
requireValue(Number.isSafeInteger(value.input.bytes) && Number(value.input.bytes) > 0 && Number(value.input.bytes) <= Number(value.maxInputBytes));
|
|
185
|
+
if (value.operation === 'embed-image')
|
|
186
|
+
requireValue(Number(value.input.bytes) <= limits.imageBytes);
|
|
187
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Immutable schema migration. Add a new numbered migration after this ships. */
|
|
2
|
+
export declare const version = 1;
|
|
3
|
+
export declare const sql = "\nCREATE TABLE schema_migrations(version INTEGER PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL) STRICT;\nCREATE TABLE library_info(id INTEGER PRIMARY KEY CHECK(id=1), uuid TEXT NOT NULL, schema_version INTEGER NOT NULL, created_by TEXT NOT NULL, revision_epoch INTEGER NOT NULL DEFAULT 0 CHECK(revision_epoch>=0)) STRICT;\nCREATE TABLE items(id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('article','pdf','media')), title TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, current_revision TEXT REFERENCES revisions(id), deleted_at TEXT) STRICT;\nCREATE TABLE source_refs(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), source_key TEXT NOT NULL, realm_key TEXT NOT NULL DEFAULT '', locator TEXT NOT NULL, final_url TEXT, canonical_claim TEXT, provider_id TEXT, UNIQUE(source_key,realm_key)) STRICT;\nCREATE TABLE jobs(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), operation TEXT NOT NULL DEFAULT 'capture', state TEXT NOT NULL CHECK(state IN ('queued','running','delayed','needs-permission','needs-auth','unsupported','failed','succeeded','cancelled','cleanup-pending')), priority INTEGER NOT NULL DEFAULT 0, not_before TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK(cancel_requested IN (0,1)), reason TEXT, created_at TEXT NOT NULL) STRICT;\nCREATE INDEX jobs_ready ON jobs(state,not_before,priority);\nCREATE TABLE submissions(id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE, source_id TEXT NOT NULL REFERENCES source_refs(id), item_id TEXT NOT NULL REFERENCES items(id), job_id TEXT NOT NULL REFERENCES jobs(id), created_at TEXT NOT NULL, intent TEXT NOT NULL) STRICT;\nCREATE TABLE worker_owner(id INTEGER PRIMARY KEY CHECK(id=1), incarnation TEXT, fence INTEGER NOT NULL DEFAULT 0 CHECK(fence>=0)) STRICT;\nINSERT INTO worker_owner(id) VALUES(1);\nCREATE TABLE attempts(id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES jobs(id), owner_incarnation TEXT NOT NULL, fence INTEGER NOT NULL, stage TEXT NOT NULL, cleanup TEXT NOT NULL CHECK(cleanup IN ('pending','settled')), created_at TEXT NOT NULL, finished_at TEXT) STRICT;\nCREATE INDEX attempts_job ON attempts(job_id);\nCREATE TABLE staging_pins(attempt_id TEXT NOT NULL REFERENCES attempts(id), sha256 TEXT NOT NULL CHECK(length(sha256)=64), bytes INTEGER NOT NULL CHECK(bytes>=0), state TEXT NOT NULL CHECK(state IN ('staging','published')), PRIMARY KEY(attempt_id,sha256)) STRICT;\nCREATE TABLE revisions(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), parent_id TEXT REFERENCES revisions(id), bundle_hash TEXT NOT NULL CHECK(length(bundle_hash)=64), extractor_name TEXT NOT NULL, extractor_version TEXT NOT NULL, captured_at TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('complete','partial')), warnings_json TEXT NOT NULL, payload_hash TEXT NOT NULL, UNIQUE(item_id,bundle_hash)) STRICT;\nCREATE TRIGGER immutable_revision BEFORE UPDATE ON revisions BEGIN SELECT RAISE(ABORT,'immutable-revision'); END;\nCREATE TABLE assets(sha256 TEXT PRIMARY KEY CHECK(length(sha256)=64), bytes INTEGER NOT NULL CHECK(bytes>=0), media_type TEXT NOT NULL, relative_path TEXT NOT NULL UNIQUE) STRICT;\nCREATE TABLE revision_assets(revision_id TEXT NOT NULL REFERENCES revisions(id), sha256 TEXT NOT NULL REFERENCES assets(sha256), ordinal INTEGER NOT NULL CHECK(ordinal>=0), relationship TEXT NOT NULL, original_locator TEXT, alt TEXT, caption TEXT, PRIMARY KEY(revision_id,ordinal)) STRICT;\nCREATE TABLE representations(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), kind TEXT NOT NULL, mime_type TEXT NOT NULL, sha256 TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes>=0), markdown TEXT, blob_sha256 TEXT REFERENCES assets(sha256), extractor_version TEXT NOT NULL, ancestry_json TEXT NOT NULL DEFAULT '[]', CHECK((markdown IS NOT NULL AND blob_sha256 IS NULL) OR (markdown IS NULL AND blob_sha256 IS NOT NULL))) STRICT;\nCREATE TABLE metadata_claims(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), field TEXT NOT NULL, value_json TEXT NOT NULL, source TEXT NOT NULL, observed_at TEXT NOT NULL, rule TEXT, timezone_known INTEGER CHECK(timezone_known IN (0,1))) STRICT;\nCREATE TABLE blocks(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), ordinal INTEGER NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL, heading_path_json TEXT NOT NULL DEFAULT '[]', page INTEGER, bounds_json TEXT, start_ms INTEGER, end_ms INTEGER, asset_sha256 TEXT REFERENCES assets(sha256), UNIQUE(revision_id,ordinal)) STRICT;\nCREATE VIRTUAL TABLE readable_fts USING fts5(item_id UNINDEXED,revision_id UNINDEXED,title,markdown,tokenize='unicode61');\nCREATE TABLE model_artifacts(id TEXT PRIMARY KEY, manifest_json TEXT NOT NULL, sha256 TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes>=0), license TEXT NOT NULL, available INTEGER NOT NULL DEFAULT 0 CHECK(available IN (0,1))) STRICT;\nCREATE TABLE embedding_spaces(fingerprint TEXT PRIMARY KEY, identity_json TEXT NOT NULL, dimensions INTEGER NOT NULL CHECK(dimensions>0), normalization TEXT NOT NULL, metric TEXT NOT NULL) STRICT;\nCREATE TABLE index_generations(id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL REFERENCES embedding_spaces(fingerprint), profile_json TEXT NOT NULL, state TEXT NOT NULL, revision_epoch INTEGER NOT NULL, coverage_json TEXT NOT NULL DEFAULT '{}') STRICT;\nCREATE TABLE vectors(generation_id TEXT NOT NULL REFERENCES index_generations(id), source_id TEXT NOT NULL, revision_id TEXT NOT NULL REFERENCES revisions(id), asset_sha256 TEXT REFERENCES assets(sha256), derivation_hash TEXT NOT NULL, vector BLOB NOT NULL, PRIMARY KEY(generation_id,source_id)) STRICT;\nCREATE TABLE active_indexes(profile_id TEXT PRIMARY KEY, generation_id TEXT NOT NULL REFERENCES index_generations(id)) STRICT;\nCREATE TABLE reading_state(item_id TEXT PRIMARY KEY REFERENCES items(id), state_json TEXT NOT NULL, updated_at TEXT NOT NULL) STRICT;\nCREATE TABLE annotations(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), revision_id TEXT REFERENCES revisions(id), anchor_json TEXT NOT NULL, body TEXT NOT NULL, created_at TEXT NOT NULL) STRICT;\nCREATE TABLE import_receipts(namespace TEXT NOT NULL, source_key TEXT NOT NULL, source_hash TEXT NOT NULL, importer_version TEXT NOT NULL, item_id TEXT REFERENCES items(id), outcome TEXT NOT NULL, PRIMARY KEY(namespace,source_key,source_hash)) STRICT;\nCREATE TABLE backup_fences(id TEXT PRIMARY KEY, revision_epoch INTEGER NOT NULL, state TEXT NOT NULL CHECK(state IN ('active','complete')), created_at TEXT NOT NULL) STRICT;\n";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Immutable schema migration. Add a new numbered migration after this ships. */
|
|
2
|
+
export const version = 1;
|
|
3
|
+
export const sql = `
|
|
4
|
+
CREATE TABLE schema_migrations(version INTEGER PRIMARY KEY, checksum TEXT NOT NULL, applied_at TEXT NOT NULL) STRICT;
|
|
5
|
+
CREATE TABLE library_info(id INTEGER PRIMARY KEY CHECK(id=1), uuid TEXT NOT NULL, schema_version INTEGER NOT NULL, created_by TEXT NOT NULL, revision_epoch INTEGER NOT NULL DEFAULT 0 CHECK(revision_epoch>=0)) STRICT;
|
|
6
|
+
CREATE TABLE items(id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('article','pdf','media')), title TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL, current_revision TEXT REFERENCES revisions(id), deleted_at TEXT) STRICT;
|
|
7
|
+
CREATE TABLE source_refs(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), source_key TEXT NOT NULL, realm_key TEXT NOT NULL DEFAULT '', locator TEXT NOT NULL, final_url TEXT, canonical_claim TEXT, provider_id TEXT, UNIQUE(source_key,realm_key)) STRICT;
|
|
8
|
+
CREATE TABLE jobs(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), operation TEXT NOT NULL DEFAULT 'capture', state TEXT NOT NULL CHECK(state IN ('queued','running','delayed','needs-permission','needs-auth','unsupported','failed','succeeded','cancelled','cleanup-pending')), priority INTEGER NOT NULL DEFAULT 0, not_before TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK(cancel_requested IN (0,1)), reason TEXT, created_at TEXT NOT NULL) STRICT;
|
|
9
|
+
CREATE INDEX jobs_ready ON jobs(state,not_before,priority);
|
|
10
|
+
CREATE TABLE submissions(id TEXT PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE, source_id TEXT NOT NULL REFERENCES source_refs(id), item_id TEXT NOT NULL REFERENCES items(id), job_id TEXT NOT NULL REFERENCES jobs(id), created_at TEXT NOT NULL, intent TEXT NOT NULL) STRICT;
|
|
11
|
+
CREATE TABLE worker_owner(id INTEGER PRIMARY KEY CHECK(id=1), incarnation TEXT, fence INTEGER NOT NULL DEFAULT 0 CHECK(fence>=0)) STRICT;
|
|
12
|
+
INSERT INTO worker_owner(id) VALUES(1);
|
|
13
|
+
CREATE TABLE attempts(id TEXT PRIMARY KEY, job_id TEXT NOT NULL REFERENCES jobs(id), owner_incarnation TEXT NOT NULL, fence INTEGER NOT NULL, stage TEXT NOT NULL, cleanup TEXT NOT NULL CHECK(cleanup IN ('pending','settled')), created_at TEXT NOT NULL, finished_at TEXT) STRICT;
|
|
14
|
+
CREATE INDEX attempts_job ON attempts(job_id);
|
|
15
|
+
CREATE TABLE staging_pins(attempt_id TEXT NOT NULL REFERENCES attempts(id), sha256 TEXT NOT NULL CHECK(length(sha256)=64), bytes INTEGER NOT NULL CHECK(bytes>=0), state TEXT NOT NULL CHECK(state IN ('staging','published')), PRIMARY KEY(attempt_id,sha256)) STRICT;
|
|
16
|
+
CREATE TABLE revisions(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), parent_id TEXT REFERENCES revisions(id), bundle_hash TEXT NOT NULL CHECK(length(bundle_hash)=64), extractor_name TEXT NOT NULL, extractor_version TEXT NOT NULL, captured_at TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN ('complete','partial')), warnings_json TEXT NOT NULL, payload_hash TEXT NOT NULL, UNIQUE(item_id,bundle_hash)) STRICT;
|
|
17
|
+
CREATE TRIGGER immutable_revision BEFORE UPDATE ON revisions BEGIN SELECT RAISE(ABORT,'immutable-revision'); END;
|
|
18
|
+
CREATE TABLE assets(sha256 TEXT PRIMARY KEY CHECK(length(sha256)=64), bytes INTEGER NOT NULL CHECK(bytes>=0), media_type TEXT NOT NULL, relative_path TEXT NOT NULL UNIQUE) STRICT;
|
|
19
|
+
CREATE TABLE revision_assets(revision_id TEXT NOT NULL REFERENCES revisions(id), sha256 TEXT NOT NULL REFERENCES assets(sha256), ordinal INTEGER NOT NULL CHECK(ordinal>=0), relationship TEXT NOT NULL, original_locator TEXT, alt TEXT, caption TEXT, PRIMARY KEY(revision_id,ordinal)) STRICT;
|
|
20
|
+
CREATE TABLE representations(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), kind TEXT NOT NULL, mime_type TEXT NOT NULL, sha256 TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes>=0), markdown TEXT, blob_sha256 TEXT REFERENCES assets(sha256), extractor_version TEXT NOT NULL, ancestry_json TEXT NOT NULL DEFAULT '[]', CHECK((markdown IS NOT NULL AND blob_sha256 IS NULL) OR (markdown IS NULL AND blob_sha256 IS NOT NULL))) STRICT;
|
|
21
|
+
CREATE TABLE metadata_claims(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), field TEXT NOT NULL, value_json TEXT NOT NULL, source TEXT NOT NULL, observed_at TEXT NOT NULL, rule TEXT, timezone_known INTEGER CHECK(timezone_known IN (0,1))) STRICT;
|
|
22
|
+
CREATE TABLE blocks(id TEXT PRIMARY KEY, revision_id TEXT NOT NULL REFERENCES revisions(id), ordinal INTEGER NOT NULL, kind TEXT NOT NULL, text TEXT NOT NULL, heading_path_json TEXT NOT NULL DEFAULT '[]', page INTEGER, bounds_json TEXT, start_ms INTEGER, end_ms INTEGER, asset_sha256 TEXT REFERENCES assets(sha256), UNIQUE(revision_id,ordinal)) STRICT;
|
|
23
|
+
CREATE VIRTUAL TABLE readable_fts USING fts5(item_id UNINDEXED,revision_id UNINDEXED,title,markdown,tokenize='unicode61');
|
|
24
|
+
CREATE TABLE model_artifacts(id TEXT PRIMARY KEY, manifest_json TEXT NOT NULL, sha256 TEXT NOT NULL, bytes INTEGER NOT NULL CHECK(bytes>=0), license TEXT NOT NULL, available INTEGER NOT NULL DEFAULT 0 CHECK(available IN (0,1))) STRICT;
|
|
25
|
+
CREATE TABLE embedding_spaces(fingerprint TEXT PRIMARY KEY, identity_json TEXT NOT NULL, dimensions INTEGER NOT NULL CHECK(dimensions>0), normalization TEXT NOT NULL, metric TEXT NOT NULL) STRICT;
|
|
26
|
+
CREATE TABLE index_generations(id TEXT PRIMARY KEY, fingerprint TEXT NOT NULL REFERENCES embedding_spaces(fingerprint), profile_json TEXT NOT NULL, state TEXT NOT NULL, revision_epoch INTEGER NOT NULL, coverage_json TEXT NOT NULL DEFAULT '{}') STRICT;
|
|
27
|
+
CREATE TABLE vectors(generation_id TEXT NOT NULL REFERENCES index_generations(id), source_id TEXT NOT NULL, revision_id TEXT NOT NULL REFERENCES revisions(id), asset_sha256 TEXT REFERENCES assets(sha256), derivation_hash TEXT NOT NULL, vector BLOB NOT NULL, PRIMARY KEY(generation_id,source_id)) STRICT;
|
|
28
|
+
CREATE TABLE active_indexes(profile_id TEXT PRIMARY KEY, generation_id TEXT NOT NULL REFERENCES index_generations(id)) STRICT;
|
|
29
|
+
CREATE TABLE reading_state(item_id TEXT PRIMARY KEY REFERENCES items(id), state_json TEXT NOT NULL, updated_at TEXT NOT NULL) STRICT;
|
|
30
|
+
CREATE TABLE annotations(id TEXT PRIMARY KEY, item_id TEXT NOT NULL REFERENCES items(id), revision_id TEXT REFERENCES revisions(id), anchor_json TEXT NOT NULL, body TEXT NOT NULL, created_at TEXT NOT NULL) STRICT;
|
|
31
|
+
CREATE TABLE import_receipts(namespace TEXT NOT NULL, source_key TEXT NOT NULL, source_hash TEXT NOT NULL, importer_version TEXT NOT NULL, item_id TEXT REFERENCES items(id), outcome TEXT NOT NULL, PRIMARY KEY(namespace,source_key,source_hash)) STRICT;
|
|
32
|
+
CREATE TABLE backup_fences(id TEXT PRIMARY KEY, revision_epoch INTEGER NOT NULL, state TEXT NOT NULL CHECK(state IN ('active','complete')), created_at TEXT NOT NULL) STRICT;
|
|
33
|
+
`;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Additive retry evidence; schema 1 and its checksum remain immutable. */
|
|
2
|
+
export declare const version = 2;
|
|
3
|
+
export declare const sql = "\nALTER TABLE jobs ADD COLUMN retryable INTEGER NOT NULL DEFAULT 0 CHECK(retryable IN (0,1));\nALTER TABLE jobs ADD COLUMN retry_after_ms INTEGER CHECK(retry_after_ms>=0 AND retry_after_ms<=86400000);\n";
|