@hyperframes/share 0.9.28-alpha.1 → 0.9.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +1 -58
  3. package/dist/index.d.mts +0 -1
  4. package/dist/projectLock.d.mts +2 -2
  5. package/dist/projectSnapshot.d.mts +1 -3
  6. package/dist/reviewClient.d.mts +16 -13
  7. package/dist/reviewExport.d.mts +1 -11
  8. package/dist/reviewUpload.d.mts +0 -2
  9. package/package.json +1 -1
  10. package/scripts/copy-types.mjs +0 -3
  11. package/src/index.mjs +0 -6
  12. package/src/projectLock.mjs +2 -9
  13. package/src/projectSnapshot.mjs +6 -22
  14. package/src/reviewClient.mjs +83 -48
  15. package/src/reviewExport.mjs +41 -156
  16. package/src/reviewUpload.mjs +16 -18
  17. package/src/reviewVersionSentences.mjs +1 -2
  18. package/dist/shareSources.d.mts +0 -24
  19. package/dist/sourceIgnore.d.mts +0 -3
  20. package/dist/sources/adapterTypes.d.ts +0 -39
  21. package/dist/sources/api.d.mts +0 -29
  22. package/dist/sources/capture.d.mts +0 -24
  23. package/dist/sources/contract.d.mts +0 -2
  24. package/dist/sources/errors.d.mts +0 -24
  25. package/dist/sources/limits.d.mts +0 -12
  26. package/dist/sources/manifest.d.mts +0 -23
  27. package/dist/sources/pathPolicy.d.mts +0 -25
  28. package/dist/sources/projectState.d.mts +0 -9
  29. package/dist/sources/store.d.mts +0 -3
  30. package/dist/sources/transfer.d.mts +0 -45
  31. package/dist/sources/types.d.ts +0 -353
  32. package/dist/sources/unicode.d.mts +0 -15
  33. package/dist/sources/upload.d.mts +0 -9
  34. package/dist/sources/uploadFiles.d.mts +0 -31
  35. package/dist/sources/wire.d.mts +0 -21
  36. package/src/shareSources.mjs +0 -236
  37. package/src/sourceIgnore.mjs +0 -69
  38. package/src/sources/adapterTypes.d.ts +0 -39
  39. package/src/sources/api.mjs +0 -114
  40. package/src/sources/capture.mjs +0 -264
  41. package/src/sources/contract/LICENSE-Unicode.txt +0 -39
  42. package/src/sources/contract/path-policy.json +0 -311
  43. package/src/sources/contract/unicode-15.1.0.json +0 -6179
  44. package/src/sources/contract.mjs +0 -13
  45. package/src/sources/errors.mjs +0 -77
  46. package/src/sources/limits.mjs +0 -48
  47. package/src/sources/manifest.mjs +0 -154
  48. package/src/sources/pathPolicy.mjs +0 -197
  49. package/src/sources/projectState.mjs +0 -59
  50. package/src/sources/store.mjs +0 -82
  51. package/src/sources/transfer.mjs +0 -196
  52. package/src/sources/types.d.ts +0 -353
  53. package/src/sources/unicode.mjs +0 -163
  54. package/src/sources/upload.mjs +0 -201
  55. package/src/sources/uploadFiles.mjs +0 -263
  56. package/src/sources/wire.mjs +0 -205
@@ -1,264 +0,0 @@
1
- // @ts-check
2
- // Captures a project folder as the canonical manifest the server accepts: every portable regular file with its
3
- // SHA-256 and length. A file that cannot be read, a link, a special file or a name no client can store fails the
4
- // capture with a list of them; it never becomes a deletion. Limits are checked from sizes alone, before any hashing.
5
- import { readSourceIgnore } from "../sourceIgnore.mjs";
6
- import { createHash } from "node:crypto";
7
- import { constants } from "node:fs";
8
- import { lstat, open, readdir, realpath } from "node:fs/promises";
9
- import { dirname, join } from "node:path";
10
-
11
- import { ENTRYPOINT, ManifestInvalid, canonicalBytes, parseManifest } from "./manifest.mjs";
12
- import { V1_LIMITS, overLimit } from "./limits.mjs";
13
- import {
14
- PathViolation,
15
- checkInventoryPaths,
16
- classifyPath,
17
- excludedFolder,
18
- prefiltered,
19
- skippedSymlink,
20
- validatePath,
21
- } from "./pathPolicy.mjs";
22
- import { readRenderOutputs, settledState } from "./projectState.mjs";
23
-
24
- /** @typedef {import("./types.js").CaptureProblem} CaptureProblem @typedef {import("./types.js").Found} Found
25
- * @typedef {import("./types.js").Walk} Walk @typedef {import("./types.js").Capture} Capture */
26
-
27
- /** Files up to this size use one bounded buffer; larger files stream from the verified handle. */
28
- const INLINE_HASH_BYTES = 4 * 1024 * 1024;
29
- const HASHING_AT_ONCE = 4;
30
-
31
- /** @param {string} root @param {string} rel @param {import("node:fs").Dirent} entry @param {Walk} walk
32
- * @param {string[]} dirs */
33
- async function visit(root, rel, entry, walk, dirs) {
34
- if (entry.isDirectory()) {
35
- if (!excludedFolder(rel)) dirs.push(rel);
36
- return;
37
- }
38
- const action = classifyPath(rel).client_action;
39
- if (action === "exclude") return;
40
- if (action === "wait_then_exclude") return void walk.waiting.push(rel);
41
- if (entry.isSymbolicLink()) {
42
- if (skippedSymlink(rel)) walk.skippedLinks.push(rel);
43
- else walk.problems.push({ path: rel, problem: "symlink" });
44
- return;
45
- }
46
- if (!entry.isFile()) return void walk.problems.push({ path: rel, problem: "special-file" });
47
- await visitFile(root, rel, walk);
48
- }
49
- /** @param {string} root @param {string} rel @param {Walk} walk */
50
- async function visitFile(root, rel, walk) {
51
- try {
52
- validatePath(rel);
53
- } catch (error) {
54
- if (!(error instanceof PathViolation)) throw error;
55
- return void walk.problems.push({ path: rel, problem: "invalid-name", code: error.code });
56
- }
57
- const full = join(root, rel);
58
- try {
59
- const { size, mtimeMs, ino, dev } = await lstat(full);
60
- walk.files.push({ path: rel, full, size, mtimeMs, ino, dev });
61
- } catch {
62
- walk.problems.push({ path: rel, problem: "unreadable" });
63
- }
64
- }
65
-
66
- /** A folder that was a link, or another folder, by the time it was read. */
67
- class FolderMoved extends Error {}
68
-
69
- /** A folder's entries, read only while the same real folder stands at its path before and after: one swapped for a
70
- * link to somewhere else is never listed. @param {string} root @param {string} dir */
71
- async function listFolder(root, dir) {
72
- const full = join(root, dir);
73
- const before = await lstat(full);
74
- if (!before.isDirectory()) throw new FolderMoved();
75
- const entries = await readdir(full, { withFileTypes: true });
76
- const after = await lstat(full);
77
- if (!after.isDirectory() || after.ino !== before.ino || after.dev !== before.dev)
78
- throw new FolderMoved();
79
- return entries;
80
- }
81
-
82
- /** Every file a capture would list, and what keeps one from being listed. @param {string} root
83
- * @param {(rel: string) => boolean} renderOutput @returns {Promise<Walk>} */
84
- async function walkProject(root, renderOutput) {
85
- /** @type {Walk} */
86
- const walk = { files: [], problems: [], waiting: [], skippedLinks: [] };
87
- const ignored = await readSourceIgnore(root);
88
- const dirs = [""];
89
- for (let dir = dirs.pop(); dir !== undefined; dir = dirs.pop()) {
90
- let entries;
91
- try {
92
- entries = await listFolder(root, dir);
93
- } catch (error) {
94
- const changed = error instanceof FolderMoved;
95
- walk.problems.push({ path: dir, problem: changed ? "changed" : "unreadable" });
96
- continue;
97
- }
98
- for (const entry of entries) {
99
- const rel = dir ? `${dir}/${entry.name}` : entry.name;
100
- if (prefiltered(entry.name) || renderOutput(rel) || ignored(rel)) continue;
101
- await visit(root, rel, entry, walk, dirs);
102
- }
103
- }
104
- return walk;
105
- }
106
-
107
- /** The manifest's canonical length, from paths and sizes alone: every SHA-256 is 64 characters.
108
- * @param {Found[]} files */
109
- const manifestBytes = (files) =>
110
- canonicalBytes({
111
- format_version: 1,
112
- entrypoint: ENTRYPOINT,
113
- files: files.map(({ path, size }) => ({ path, sha256: "0".repeat(64), size })),
114
- }).length;
115
-
116
- /** A file changed this recently can be written again within its time's granularity, keeping its size and time. */
117
- const SETTLED_MS = 2_000;
118
-
119
- /** A file's SHA-256 by path, size, time and inode, kept for the launch so a check after a focus reads no bytes.
120
- * Only a settled file's hash is kept. */
121
- export function hashMemo(now = Date.now) {
122
- /** @type {Map<string, { size: number, mtimeMs: number, ino: number, sha256: string }>} */
123
- const known = new Map();
124
- return {
125
- /** @param {Found} file */
126
- get: (file) => {
127
- const kept = known.get(file.full);
128
- const same =
129
- kept?.size === file.size && kept.mtimeMs === file.mtimeMs && kept.ino === file.ino;
130
- return same ? kept.sha256 : null;
131
- },
132
- /** @param {Found} file @param {string} sha256 */
133
- set: (file, sha256) => {
134
- if (now() - file.mtimeMs > SETTLED_MS) known.set(file.full, { ...file, sha256 });
135
- },
136
- };
137
- }
138
-
139
- /** The file the walk listed, as an open handle: never through a link at its own name (O_NOFOLLOW), and only when the
140
- * opened file is the same regular file (inode, device, size) found inside the project, so a file or folder swapped
141
- * for a link after the walk is refused rather than followed. Null when it is not. @param {string} root
142
- * @param {Found} file @param {typeof realpath} [resolve] */
143
- async function openListed(root, file, resolve = realpath) {
144
- const handle = await open(file.full, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)).catch(
145
- (/** @type {NodeJS.ErrnoException} */ error) => {
146
- if (error.code === "ELOOP" || error.code === "EMLINK") return null;
147
- throw error;
148
- },
149
- );
150
- if (!handle) return null;
151
- try {
152
- const opened = await handle.stat();
153
- const same =
154
- opened.isFile() &&
155
- opened.ino === file.ino &&
156
- opened.dev === file.dev &&
157
- opened.size === file.size &&
158
- (await resolve(dirname(file.full))) === join(root, dirname(file.path));
159
- if (same) return handle;
160
- } catch (error) {
161
- await handle.close();
162
- throw error;
163
- }
164
- await handle.close();
165
- return null;
166
- }
167
-
168
- /** The file's SHA-256, read through the handle `openListed` checked; null when the file is not the one listed, or its
169
- * bytes changed while being read. `root` is the project's real path; `resolve` finds a folder's real path.
170
- * @param {string} root @param {Found} file @param {typeof realpath} [resolve] */
171
- export async function hashListedFile(root, file, resolve = realpath) {
172
- const handle = await openListed(root, file, resolve);
173
- if (!handle) return null;
174
- try {
175
- const sha256 =
176
- file.size <= INLINE_HASH_BYTES
177
- ? await handle
178
- .readFile()
179
- .then((bytes) =>
180
- bytes.length === file.size ? createHash("sha256").update(bytes).digest("hex") : null,
181
- )
182
- : await hashHandle(handle);
183
- const after = await handle.stat();
184
- return after.size === file.size && after.mtimeMs === file.mtimeMs ? sha256 : null;
185
- } finally {
186
- await handle.close();
187
- }
188
- }
189
-
190
- /** @param {string} root @param {Found[]} files @param {ReturnType<typeof hashMemo>} memo
191
- * @returns {Promise<{ hashed: import("./types.js").ManifestFile[], problems: CaptureProblem[] }>} */
192
- async function hashAll(root, files, memo) {
193
- /** @type {import("./types.js").ManifestFile[]} */
194
- const hashed = [];
195
- /** @type {CaptureProblem[]} */
196
- const problems = [];
197
- const queue = [...files];
198
- const worker = async () => {
199
- for (let file = queue.shift(); file; file = queue.shift()) {
200
- const sha256 = memo.get(file) ?? (await hashListedFile(root, file).catch(() => undefined));
201
- if (sha256 === undefined) problems.push({ path: file.path, problem: "unreadable" });
202
- else if (sha256 === null) problems.push({ path: file.path, problem: "changed" });
203
- else {
204
- memo.set(file, sha256);
205
- hashed.push({ path: file.path, sha256, size: file.size });
206
- }
207
- }
208
- };
209
- await Promise.all(Array.from({ length: HASHING_AT_ONCE }, worker));
210
- return { hashed, problems };
211
- }
212
-
213
- /** Problems that keep the files found from being one manifest: two names one disk stores as one, no index.html.
214
- * @param {Found[]} files @returns {CaptureProblem[]} */
215
- function inventoryProblems(files) {
216
- if (!files.some((file) => file.path === ENTRYPOINT))
217
- return [{ path: ENTRYPOINT, problem: "no-entrypoint" }];
218
- try {
219
- checkInventoryPaths(files.map((file) => file.path));
220
- return [];
221
- } catch (error) {
222
- if (!(error instanceof PathViolation)) throw error;
223
- return [{ path: error.path, problem: "invalid-name", code: error.code }];
224
- }
225
- }
226
-
227
- /** Captures `root` (a project's real path). `memo` reuses hashes of unchanged files; a send should pass a fresh one.
228
- * @param {string} root @param {{ memo?: ReturnType<typeof hashMemo>, limits?: import("./types.js").Limits }} [options]
229
- * @returns {Promise<Capture>} */
230
- export async function captureProject(root, { memo = hashMemo(), limits = V1_LIMITS } = {}) {
231
- const renderOutput = await readRenderOutputs(root);
232
- const walk = await walkProject(root, renderOutput);
233
- const problems = walk.problems.length ? walk.problems : inventoryProblems(walk.files);
234
- if (problems.length) return { at: "problems", problems };
235
- const over = overLimit(walk.files, manifestBytes(walk.files), limits);
236
- if (over) return { at: "too-big", over };
237
- const { hashed, problems: unhashed } = await hashAll(await realpath(root), walk.files, memo);
238
- if (unhashed.length) return { at: "problems", problems: unhashed };
239
- try {
240
- const manifest = parseManifest({ format_version: 1, entrypoint: ENTRYPOINT, files: hashed });
241
- const settled = await settledState(root);
242
- return {
243
- at: "captured",
244
- manifest,
245
- settled,
246
- waiting: walk.waiting,
247
- skippedLinks: walk.skippedLinks,
248
- };
249
- } catch (error) {
250
- if (!(error instanceof ManifestInvalid)) throw error;
251
- const first = error.violations[0];
252
- return {
253
- at: "problems",
254
- problems: [{ path: first?.path ?? "", problem: "invalid-name", code: error.code }],
255
- };
256
- }
257
- }
258
-
259
- /** @param {import("node:fs/promises").FileHandle} handle */
260
- async function hashHandle(handle) {
261
- const hash = createHash("sha256");
262
- for await (const chunk of handle.createReadStream({ autoClose: false })) hash.update(chunk);
263
- return hash.digest("hex");
264
- }
@@ -1,39 +0,0 @@
1
- UNICODE LICENSE V3
2
-
3
- COPYRIGHT AND PERMISSION NOTICE
4
-
5
- Copyright © 1991-2026 Unicode, Inc.
6
-
7
- NOTICE TO USER: Carefully read the following legal agreement. BY
8
- DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR
9
- SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
10
- TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT
11
- DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
12
-
13
- Permission is hereby granted, free of charge, to any person obtaining a
14
- copy of data files and any associated documentation (the "Data Files") or
15
- software and any associated documentation (the "Software") to deal in the
16
- Data Files or Software without restriction, including without limitation
17
- the rights to use, copy, modify, merge, publish, distribute, and/or sell
18
- copies of the Data Files or Software, and to permit persons to whom the
19
- Data Files or Software are furnished to do so, provided that either (a)
20
- this copyright and permission notice appear with all copies of the Data
21
- Files or Software, or (b) this copyright and permission notice appear in
22
- associated Documentation.
23
-
24
- THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
25
- KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
27
- THIRD PARTY RIGHTS.
28
-
29
- IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE
30
- BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,
31
- OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
32
- WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
33
- ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA
34
- FILES OR SOFTWARE.
35
-
36
- Except as contained in this notice, the name of a copyright holder shall
37
- not be used in advertising or otherwise to promote the sale, use or other
38
- dealings in these Data Files or Software without prior written
39
- authorization of the copyright holder.
@@ -1,311 +0,0 @@
1
- {
2
- "policy_version": "portable-source-v4",
3
- "manifest": {
4
- "format_version": 1,
5
- "entrypoint": "index.html",
6
- "fields": ["format_version", "entrypoint", "files"],
7
- "file_fields": ["path", "sha256", "size"],
8
- "sha256": "64 lowercase hex characters: the SHA-256 of the file's exact bytes",
9
- "size": "the file's byte length, an integer from 0 to 9007199254740991 (2^53 - 1, the largest integer every JSON client reads exactly)",
10
- "same_digest_same_size": "two entries with the same sha256 must have the same size",
11
- "not_in_manifest": "upload state, latest pointer, authors, comments, timestamps, URLs, S3 keys and sync binding metadata"
12
- },
13
- "canonical_encoding": {
14
- "files_order": "ascending by the UTF-8 bytes of path (not UTF-16 code units: JavaScript's default sort differs above U+FFFF)",
15
- "object_keys": "sorted; every key is ASCII, so byte, code-point and UTF-16 order agree",
16
- "whitespace": "none outside strings: separators are ',' and ':'",
17
- "strings": "UTF-8; only '\"' and '\\' are escaped, because policy-valid paths contain no control characters or lone surrogates",
18
- "numbers": "integers in decimal with no sign, fraction or exponent",
19
- "manifest_sha256": "lowercase hex SHA-256 of the canonical bytes"
20
- },
21
- "unicode": {
22
- "version": "15.1.0",
23
- "table": "unicode-15.1.0.json",
24
- "assigned_only": "every code point must be assigned in Unicode 15.1; unassigned and future code points are refused",
25
- "normalization": "every path must already be NFC",
26
- "comparison_key": "NFC(full_casefold(NFC(segment))), with default full case folding (CaseFolding.txt status C and F)"
27
- },
28
- "path": {
29
- "max_path_bytes": 1024,
30
- "max_segment_bytes": 240,
31
- "separator": "/",
32
- "forbidden_characters": "<>:\"|?*\\",
33
- "reserved_segment_regex": "(?:con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])(?:\\..*)?",
34
- "reserved_segment_match": "full match against each segment's comparison key, so CON, Con.txt and nul.tar.gz are all reserved",
35
- "canonical_spelling_roots": [".media", ".hyperframes"],
36
- "checks": [
37
- {
38
- "code": "path_empty",
39
- "rule": "the path is not the empty string"
40
- },
41
- {
42
- "code": "path_invalid_character",
43
- "rule": "no C0 or C1 control character, DEL, lone surrogate or forbidden character"
44
- },
45
- {
46
- "code": "path_unassigned_code_point",
47
- "rule": "every code point is assigned in Unicode 15.1"
48
- },
49
- {
50
- "code": "path_not_nfc",
51
- "rule": "the path equals its NFC form"
52
- },
53
- {
54
- "code": "path_too_long",
55
- "rule": "at most max_path_bytes UTF-8 bytes"
56
- },
57
- {
58
- "code": "path_not_relative",
59
- "rule": "does not start with '/'"
60
- },
61
- {
62
- "code": "path_empty_segment",
63
- "rule": "no empty segment ('a//b', a trailing '/')"
64
- },
65
- {
66
- "code": "path_dot_segment",
67
- "rule": "no '.' or '..' segment"
68
- },
69
- {
70
- "code": "segment_too_long",
71
- "rule": "each segment is at most max_segment_bytes UTF-8 bytes"
72
- },
73
- {
74
- "code": "segment_trailing_dot_or_space",
75
- "rule": "no segment ends in '.' or ' ' (Windows strips them)"
76
- },
77
- {
78
- "code": "reserved_name",
79
- "rule": "no segment matches reserved_segment_regex"
80
- },
81
- {
82
- "code": "reserved_path_alias",
83
- "rule": "a first segment whose comparison key equals a canonical_spelling_roots entry is spelled exactly like it, and an allowed rule that matches the folded path also matches the path as spelled"
84
- },
85
- {
86
- "code": "excluded_path",
87
- "rule": "the first rule matching the folded path does not have server_action 'reject'"
88
- }
89
- ],
90
- "check_order": "checks run in the order listed; the first failing check names the error"
91
- },
92
- "matching": {
93
- "syntax": "each rule's regex must match the whole path (Python re.fullmatch, JavaScript ^(?:regex)$ with the 's' and 'u' flags)",
94
- "subject": "the folded path: every segment replaced by its comparison key, joined with '/'",
95
- "first_match_wins": true,
96
- "case": "rule regexes are written in lowercase, because they are matched against the case-folded path",
97
- "client_actions": {
98
- "include": "list the file",
99
- "exclude": "leave the file out",
100
- "wait_then_exclude": "wait for the writer to finish (the reserved sibling may be incomplete), then leave the marker out"
101
- },
102
- "rule_order": "every rule that rejects at any depth comes before the first allow rule, so a credential, environment file, package-manager config, VCS or dependency directory, agent state, build cache or OS metadata inside an allowed .hyperframes subtree stays out"
103
- },
104
- "rules": [
105
- {
106
- "id": "media-lock",
107
- "regex": "\\.media/\\.lock",
108
- "client_action": "exclude",
109
- "server_action": "reject",
110
- "basis": "Process lock held by the media writer, not project content"
111
- },
112
- {
113
- "id": "media-reservation",
114
- "regex": "\\.media/(?:audio/(?:bgm|sfx|voice)|images|video|luts|recipes)/[^/]+\\.hf[0-9a-f]{6}\\.tmp",
115
- "client_action": "wait_then_exclude",
116
- "server_action": "reject",
117
- "basis": "Media id reservation: the sibling asset may still be incomplete, so capture waits for the writer to finish"
118
- },
119
- {
120
- "id": "media-ledger",
121
- "regex": "\\.media/manifest\\.jsonl",
122
- "client_action": "exclude",
123
- "server_action": "reject",
124
- "basis": "Generated media metadata that the project does not use for preview or rendering"
125
- },
126
- {
127
- "id": "unfinished-writes",
128
- "regex": "(?:[^/]+/)*(?:[^/]+\\.hf[0-9a-f]{6}\\.tmp|[^/]*\\.(?:tmp|part|swp)|[^/]+~)",
129
- "client_action": "exclude",
130
- "server_action": "reject",
131
- "basis": "Unfinished or abandoned writes at any depth: core's atomic-write temp <file>.hfXXXXXX.tmp, *.tmp, *.part, editor *.swp and *~"
132
- },
133
- {
134
- "id": "render-temp-dirs",
135
- "regex": "(?:[^/]+/)*(?:(?:work-[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}|hf-render|\\.[^/]+\\.hf-transaction)-[A-Za-z0-9]{6}|[^/]+\\.assemble-work-[A-Za-z0-9]{6})(?:/.*)?",
136
- "client_action": "exclude",
137
- "server_action": "reject",
138
- "basis": "Render work directories at any depth (frames, often GBs): core RENDER_TEMP_DIR (packages/core/src/projectFolders.ts) plus distributed assembly's <output>.assemble-work-XXXXXX"
139
- },
140
- {
141
- "id": "agent-client-state",
142
- "regex": "(?:[^/]+/)*(?:\\.claude|\\.codex|\\.agents|\\.cursor|\\.vscode|\\.idea)(?:/.*)?",
143
- "client_action": "exclude",
144
- "server_action": "reject",
145
- "basis": "Agent and IDE state at any depth; Desktop vendors .claude/skills into every project and regenerates it"
146
- },
147
- {
148
- "id": "dependencies-and-git",
149
- "regex": "(?:[^/]+/)*(?:\\.git|\\.svn|\\.hg|node_modules)(?:/.*)?",
150
- "client_action": "exclude",
151
- "server_action": "reject",
152
- "basis": "Version control and installed dependencies at any depth"
153
- },
154
- {
155
- "id": "build-caches",
156
- "regex": "(?:[^/]+/)*(?:\\.cache|\\.vite|\\.next|\\.turbo|\\.parcel-cache)(?:/.*)?",
157
- "client_action": "exclude",
158
- "server_action": "reject",
159
- "basis": "Build tool caches at any depth"
160
- },
161
- {
162
- "id": "environment-secrets",
163
- "regex": "(?:[^/]+/)*\\.env(?:rc|\\.[^/]+)?",
164
- "client_action": "exclude",
165
- "server_action": "reject",
166
- "basis": "Credentials: .env, every .env.* (templates included: real keys land there and no reader needs them) and .envrc, at any depth"
167
- },
168
- {
169
- "id": "package-manager-config",
170
- "regex": "(?:[^/]+/)*(?:\\.npmrc|\\.yarnrc|\\.yarnrc\\.yml|pip\\.conf)",
171
- "client_action": "exclude",
172
- "server_action": "reject",
173
- "basis": "May hold registry tokens, and nothing in a HyperFrames project reads them; stripping credentials would change the bytes"
174
- },
175
- {
176
- "id": "credential-files",
177
- "regex": "(?:[^/]+/)*(?:\\.netrc|\\.pypirc|\\.git-credentials|[^/]*\\.(?:pem|p12|pfx)|id_rsa[^/]*|id_ed25519[^/]*|(?:\\.ssh|\\.aws)(?:/.*)?)",
178
- "client_action": "exclude",
179
- "server_action": "reject",
180
- "basis": "Credential files by name at any depth. *.key is deliberately absent: Keynote decks are plausible project sources"
181
- },
182
- {
183
- "id": "os-metadata",
184
- "regex": "(?:[^/]+/)*(?:\\.ds_store|thumbs\\.db|desktop\\.ini|\\._[^/]*|__macosx(?:/.*)?)",
185
- "client_action": "exclude",
186
- "server_action": "reject",
187
- "basis": "Operating-system metadata: it changes when a folder is browsed and would show as false edits and conflicts"
188
- },
189
- {
190
- "id": "portable-transcripts",
191
- "regex": "\\.hyperframes/transcripts/(?:[^/]+/)*[^/]+\\.json",
192
- "client_action": "include",
193
- "server_action": "allow",
194
- "basis": "Completed transcripts, keyed by media hash; costly to regenerate"
195
- },
196
- {
197
- "id": "portable-transcription-settings",
198
- "regex": "\\.hyperframes/transcription\\.json",
199
- "client_action": "include",
200
- "server_action": "allow",
201
- "basis": "Transcription (ASR) preferences, not run state; Desktop's Duplicate copies it with the transcripts"
202
- },
203
- {
204
- "id": "portable-brand-kit",
205
- "regex": "\\.hyperframes/brand-kit\\.json",
206
- "client_action": "include",
207
- "server_action": "allow",
208
- "basis": "The project's brand kit selection is authored, portable project content"
209
- },
210
- {
211
- "id": "portable-studio-edits",
212
- "regex": "\\.hyperframes/(?:studio-manual-edits|studio-motion)\\.json",
213
- "client_action": "include",
214
- "server_action": "allow",
215
- "basis": "Studio edits read at render and preview time; without them a pulled project renders differently"
216
- },
217
- {
218
- "id": "portable-caption-skin",
219
- "regex": "\\.hyperframes/caption-skin\\.html",
220
- "client_action": "include",
221
- "server_action": "allow",
222
- "basis": "Authored input to the caption build; rebuilding captions needs it"
223
- },
224
- {
225
- "id": "portable-podcast-media",
226
- "regex": "\\.hyperframes/prepared-assets/podcast/(?:[^/]+/)*[^./][^/]*",
227
- "client_action": "include",
228
- "server_action": "allow",
229
- "basis": "Desktop's podcast camera and recorder transcodes (media/, proxies/), which the page plays by this path and a receiving client cannot regenerate without the original files; a dotfile there stays out"
230
- },
231
- {
232
- "id": "sync-and-share-bindings",
233
- "regex": "\\.hyperframes/(?:share\\.json|review\\.json|review-export\\.json|review-[^/]+\\.json|project\\.json|lambda-stack-[^/]+\\.json|(?:sync|receipts)/.*)",
234
- "client_action": "exclude",
235
- "server_action": "reject",
236
- "basis": "External bindings and sync receipts: share and review links (review files carry the owner's email), the CLI publish id and deploy stacks. A copy must start unbound, so these never travel"
237
- },
238
- {
239
- "id": "known-hyperframes-state",
240
- "regex": "\\.hyperframes/(?:comments\\.jsonl|agent-runs\\.jsonl|app-history\\.jsonl|chat-starts\\.json|agent-handoff\\.json|agent-handoff-read\\.json|history-id|video-state\\.json|film-snapshot\\.json|moved-from\\.jsonl|hf-ids-stamped\\.json|settled-signature|render-outputs\\.json|render-outputs-lock|project\\.lock|history-turn\\.json|history-turns\\.json|voices\\.json|asset-creation\\.json|lesson\\.json|element-map\\.txt|\\.gitignore|(?:live-runs|run-traces|agent-sessions|agent-steps|video-state-lock|backup|preview|history|undo|artifacts|heygen-recipes|heygen-reperform|fixes|heygen-jobs|heygen-paid|podcast|prepared-assets|requests|recordings|frames|refs|references|review|heygen-previews|frame-packets|anim-map|contrast|bin|diagnostics)(?:/.*)?)",
241
- "client_action": "exclude",
242
- "server_action": "reject",
243
- "basis": "Known client state: comments, chat and run logs; machine pointers, locks and caches; personal data (photo and voice copies) and account-bound job state, podcast job state included; studio preview caches under prepared-assets/ other than the podcast media above"
244
- },
245
- {
246
- "id": "unknown-hyperframes-state",
247
- "regex": "\\.hyperframes(?:/.*)?",
248
- "client_action": "exclude",
249
- "server_action": "reject",
250
- "basis": "Reserved client state area: only the named portable files above travel"
251
- },
252
- {
253
- "id": "root-generated-output",
254
- "regex": "(?:\\.thumbnails|\\.transcode-cache|\\.waveform-cache|renders|snapshots)(?:/.*)?",
255
- "client_action": "exclude",
256
- "server_action": "reject",
257
- "basis": "Caches and render or snapshot output at the project root"
258
- },
259
- {
260
- "id": "root-build-output",
261
- "regex": "(?:dist|coverage)(?:/.*)?",
262
- "client_action": "exclude",
263
- "server_action": "reject",
264
- "basis": "Web build and test-coverage output at the project root, which the CLI publish leaves out and render never reads (build/ and outputs/ travel, because the publish ships them)"
265
- },
266
- {
267
- "id": "authoring-scratch",
268
- "regex": "(?:remotion-src|moments|references/moments)(?:/.*)?",
269
- "client_action": "exclude",
270
- "server_action": "reject",
271
- "basis": "Authoring scratch at the project root: a Remotion port's source and the moment working copies with their references, none of which preview or render reads"
272
- },
273
- {
274
- "id": "reference-video",
275
- "regex": "\\.viral-clone(?:/.*)?",
276
- "client_action": "exclude",
277
- "server_action": "reject",
278
- "basis": "Third-party reference video, large and re-downloadable"
279
- },
280
- {
281
- "id": "default-authored-file",
282
- "regex": ".+",
283
- "client_action": "include",
284
- "server_action": "allow",
285
- "basis": "Authored source and config (index.html, compositions/, scenes/, assets/, hyperframes.json, meta.json, project.json, package.json, lockfiles, .hyperframesignore, .gitignore), portable briefs, AGENTS.md and CLAUDE.md, completed .media assets with unused alternatives, .media/index.md, .media/preferences.json and .media/CREDITS.md, build/ and outputs/, and any unknown regular file outside a reserved area"
286
- }
287
- ],
288
- "client_notes": {
289
- "project_json": "Root project.json is portable project content and is included. It is not sync identity: the owner workspace, composition, base snapshot, revision and receipts live outside the inventory, never in project.json.",
290
- "capture_preconditions": "Do not capture while .hyperframes/history-turn.json exists (an agent turn is open) or while .hyperframes/project.lock is held.",
291
- "render_outputs": "Leave out every path listed in .hyperframes/render-outputs.json: render outputs written outside renders/ are recorded there, and the server cannot know them.",
292
- "complete_capture": "A file the client cannot read fails the capture; it never becomes a deletion. The exclusions above and the symlinks client_symlinks skips are the only files left out.",
293
- "symlinks": "Symlinks and special files fail the capture, listing them; they are neither followed nor skipped, except the symlinks client_symlinks skips.",
294
- "empty_directories": "Empty directories are not represented. Pull never deletes a local empty directory."
295
- },
296
- "client_prefilter": {
297
- "names": ["Icon\r"],
298
- "rule": "skip a file whose last segment is one of these before running any path check; macOS writes 'Icon\\r' for a folder with a custom icon, and the carriage return would otherwise fail the whole capture with path_invalid_character"
299
- },
300
- "client_symlinks": {
301
- "default": "fail_capture",
302
- "skip_prefixes": ["assets/podcast/"],
303
- "match": "a symlink whose folded path (each segment replaced by its comparison key, as for rules) starts with a listed prefix, at any depth below it",
304
- "rule": "skip such a symlink instead of failing the capture, and on Pull leave such a local symlink in place as local state; a regular file under the prefix is captured like any other, and every other symlink or special file fails the capture",
305
- "basis": "Desktop's podcast intake links the original camera files, which live outside the project, under assets/podcast/, and the page plays the transcodes under .hyperframes/prepared-assets/podcast/; real files there, such as the joins under assets/podcast/joins/, travel normally"
306
- },
307
- "stored_manifest": {
308
- "legacy_agent_state_regex": "(?:[^/]+/)*(?:\\.claude|\\.agents|\\.cursor|\\.vscode|\\.idea)(?:/.*)?",
309
- "rule": "Only stored snapshot reads use the v3 agent-state rule to retain immutable manifest digests. New captures and uploads use the current rules. Clients never install excluded paths; the server never signs their download URLs."
310
- }
311
- }