cursedbelt-core 2.13.2 → 2.13.3
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/dist/core/analytics/range.d.ts +2 -2
- package/dist/core/analytics/range.js +2 -2
- package/dist/core/analytics/schedule.d.ts +1 -1
- package/dist/core/analytics/schedule.js +3 -3
- package/dist/core/duplicates/index.d.ts +2 -2
- package/dist/core/duplicates/index.js +1 -1
- package/dist/core/file-tree/fileTreeModel.d.ts +3 -3
- package/dist/core/file-tree/fileTreeModel.js +2 -2
- package/dist/core/folder-tree/actions.d.ts +1 -1
- package/dist/core/folder-tree/actions.js +1 -1
- package/dist/core/master-lock/kdf.js +1 -1
- package/dist/core/master-lock/presence.d.ts +1 -1
- package/dist/core/master-lock/presence.js +1 -1
- package/dist/core/master-lock/wire.d.ts +1 -1
- package/dist/core/media/mediaItem.js +1 -1
- package/dist/core/media/types.d.ts +4 -4
- package/dist/core/media/videoCuts.d.ts +1 -1
- package/dist/core/media/videoCuts.js +2 -2
- package/dist/core/navigation/index.js +1 -1
- package/dist/core/navigation/navigationModel.d.ts +1 -1
- package/dist/core/navigation/navigationModel.js +1 -1
- package/dist/core/navigation/navigationSelectors.d.ts +2 -2
- package/dist/core/navigation/navigationSelectors.js +2 -2
- package/dist/core/navigation/searchCatalogue.d.ts +2 -2
- package/dist/core/navigation/searchCatalogue.js +4 -4
- package/dist/core/palette/fuzzyRank.d.ts +1 -1
- package/dist/core/palette/fuzzyRank.js +3 -3
- package/dist/core/tokens/registry.d.ts +1 -1
- package/dist/core/tokens/registry.js +1 -1
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +2 -2
- package/dist/core/wire/index.d.ts +1 -1
- package/dist/core/wire/index.js +1 -1
- package/dist/core/wire/ingress.d.ts +4 -4
- package/dist/core/wire/ingress.js +12 -12
- package/dist/core/wire/marshalWorker.d.ts +1 -1
- package/dist/core/wire/marshalWorker.js +15 -15
- package/dist/core/wire/marshalling.d.ts +16 -16
- package/dist/core/wire/marshalling.js +13 -13
- package/dist/ctgrClient/convertRequest.js +2 -2
- package/dist/testing/bytes.d.ts +1 -1
- package/dist/testing/bytes.js +1 -1
- package/package.json +1 -1
- package/src/core/analytics/range.spec.ts +1 -1
- package/src/core/analytics/range.ts +2 -2
- package/src/core/analytics/schedule.spec.ts +3 -3
- package/src/core/analytics/schedule.ts +3 -3
- package/src/core/duplicates/index.ts +2 -2
- package/src/core/file-tree/fileTreeModel.ts +4 -4
- package/src/core/folder-tree/actions.ts +1 -1
- package/src/core/master-lock/kdf.ts +1 -1
- package/src/core/master-lock/presence.ts +1 -1
- package/src/core/master-lock/wire.ts +1 -1
- package/src/core/media/mediaItem.ts +1 -1
- package/src/core/media/types.ts +4 -4
- package/src/core/media/videoCuts.ts +2 -2
- package/src/core/navigation/index.ts +1 -1
- package/src/core/navigation/navigation.spec.ts +3 -3
- package/src/core/navigation/navigationModel.ts +2 -2
- package/src/core/navigation/navigationSelectors.ts +3 -3
- package/src/core/navigation/searchCatalogue.ts +4 -4
- package/src/core/palette/fuzzyRank.ts +3 -3
- package/src/core/tokens/registry.ts +2 -2
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +2 -2
- package/src/core/wire/index.ts +1 -1
- package/src/core/wire/ingress.spec.ts +12 -12
- package/src/core/wire/ingress.ts +15 -15
- package/src/core/wire/marshalWorker.spec.ts +7 -7
- package/src/core/wire/marshalWorker.ts +15 -15
- package/src/core/wire/marshalling.spec.ts +3 -3
- package/src/core/wire/marshalling.ts +25 -25
- package/src/ctgrClient/convertRequest.ts +2 -2
- package/src/namedSubpathsResolve.spec.ts +1 -1
- package/src/publishShape.spec.ts +1 -1
- package/src/shippedFilesAreTracked.spec.ts +2 -2
- package/src/testing/bytes.ts +1 -1
|
@@ -94,8 +94,8 @@ export declare const startOfBucket: (t: number, unit: BucketUnit) => number;
|
|
|
94
94
|
* duplicated or missing bar exactly once every spring and autumn — the kind of
|
|
95
95
|
* bug nobody reproduces.
|
|
96
96
|
*
|
|
97
|
-
* The time-of-day is carried through rather than
|
|
98
|
-
* caller here passes an already-aligned value, so
|
|
97
|
+
* The time-of-day is carried through rather than normalized to midnight: every
|
|
98
|
+
* caller here passes an already-aligned value, so normalizing would be
|
|
99
99
|
* invisible in this file and a trap for the first caller that does not.
|
|
100
100
|
*/
|
|
101
101
|
export declare const addDays: (t: number, n: number) => number;
|
|
@@ -102,8 +102,8 @@ export const startOfBucket = (t, unit) => unit === 'hour' ? startOfHour(t) : sta
|
|
|
102
102
|
* duplicated or missing bar exactly once every spring and autumn — the kind of
|
|
103
103
|
* bug nobody reproduces.
|
|
104
104
|
*
|
|
105
|
-
* The time-of-day is carried through rather than
|
|
106
|
-
* caller here passes an already-aligned value, so
|
|
105
|
+
* The time-of-day is carried through rather than normalized to midnight: every
|
|
106
|
+
* caller here passes an already-aligned value, so normalizing would be
|
|
107
107
|
* invisible in this file and a trap for the first caller that does not.
|
|
108
108
|
*/
|
|
109
109
|
export const addDays = (t, n) => {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Three separate claims hide in that, and a `remaining / rate` division answers
|
|
12
12
|
* none of them:
|
|
13
13
|
*
|
|
14
|
-
* 1. **Work does not
|
|
14
|
+
* 1. **Work does not parallelize freely.** Two tasks that write the same files
|
|
15
15
|
* may not run together whatever the fleet size — that is the orchestrator's
|
|
16
16
|
* own scope rule, and it is why a queue of ten tasks all scoped to one app
|
|
17
17
|
* drains at exactly the same speed with eight workers as with one.
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Three separate claims hide in that, and a `remaining / rate` division answers
|
|
12
12
|
* none of them:
|
|
13
13
|
*
|
|
14
|
-
* 1. **Work does not
|
|
14
|
+
* 1. **Work does not parallelize freely.** Two tasks that write the same files
|
|
15
15
|
* may not run together whatever the fleet size — that is the orchestrator's
|
|
16
16
|
* own scope rule, and it is why a queue of ten tasks all scoped to one app
|
|
17
17
|
* drains at exactly the same speed with eight workers as with one.
|
|
@@ -179,10 +179,10 @@ function blocks(candidate, other) {
|
|
|
179
179
|
/**
|
|
180
180
|
* A best-effort reason an item started late — for the table behind the chart.
|
|
181
181
|
*
|
|
182
|
-
* Deliberately approximate and
|
|
182
|
+
* Deliberately approximate and labeled as such: the precise answer would need
|
|
183
183
|
* the admission loop to record every refusal at every event, which is a lot of
|
|
184
184
|
* bookkeeping for a column. "Was it waiting on a dependency, on a conflicting
|
|
185
|
-
*
|
|
185
|
+
* neighbor, or simply for a free worker" is the distinction a reader acts on,
|
|
186
186
|
* and that is decidable from the item alone.
|
|
187
187
|
*/
|
|
188
188
|
function whyWaited(item, _timeline, known) {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ── What is NOT here, deliberately ──────────────────────────────────────────────────────
|
|
19
19
|
* Anything that needs a database, a filesystem, a fetch or a picture decoder. This module is
|
|
20
|
-
* pure so both consumers can prove their
|
|
20
|
+
* pure so both consumers can prove their behavior with `bun test` instead of against a live
|
|
21
21
|
* store, and so a browser bundle may import it (music renders the pair side by side).
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
@@ -93,7 +93,7 @@ export declare function durationsClose(a: number | null | undefined, b: number |
|
|
|
93
93
|
/** One file, reduced to what this module can reason about. Units are the caller's — pass
|
|
94
94
|
* both durations in the same unit or not at all. */
|
|
95
95
|
export interface NamedFileFacts {
|
|
96
|
-
/** The name a person would
|
|
96
|
+
/** The name a person would recognize: a display title, else the last path segment. */
|
|
97
97
|
name: string | null | undefined;
|
|
98
98
|
sizeBytes: number;
|
|
99
99
|
/** Playing time, in whatever unit the caller uses consistently. Null = never measured. */
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* ── What is NOT here, deliberately ──────────────────────────────────────────────────────
|
|
19
19
|
* Anything that needs a database, a filesystem, a fetch or a picture decoder. This module is
|
|
20
|
-
* pure so both consumers can prove their
|
|
20
|
+
* pure so both consumers can prove their behavior with `bun test` instead of against a live
|
|
21
21
|
* store, and so a browser bundle may import it (music renders the pair side by side).
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
/** What a row IS. `folder` may hold children and accept drops; `file` is a leaf. */
|
|
23
23
|
export type FileTreeNodeKind = 'folder' | 'file';
|
|
24
24
|
/**
|
|
25
|
-
* How visible a node is,
|
|
25
|
+
* How visible a node is, generalized from collections' private/locked folders.
|
|
26
26
|
*
|
|
27
27
|
* 🔴 It is a LABEL, never an enforcement. The server decides what a session may see, and a
|
|
28
28
|
* tree that hides a row the server sent (or shows one it did not) is a second, weaker
|
|
@@ -114,7 +114,7 @@ export interface FileTreeNode {
|
|
|
114
114
|
*
|
|
115
115
|
* For a consumer that narrows the tree itself (by content, not only by name) and keeps
|
|
116
116
|
* the folders on the way to each hit: the hits are what the eye is scanning for, and the
|
|
117
|
-
* folders kept around them are context. Omit it and nothing is
|
|
117
|
+
* folders kept around them are context. Omit it and nothing is emphasized.
|
|
118
118
|
*/
|
|
119
119
|
matched?: boolean;
|
|
120
120
|
/**
|
|
@@ -280,7 +280,7 @@ export interface KeyboardMoveOptions {
|
|
|
280
280
|
* without asking which of several folders was meant — and **← promotes out to the folder its
|
|
281
281
|
* own folder is in**, which is `root` at depth 1.
|
|
282
282
|
*
|
|
283
|
-
* 🔴 Every refusal the DRAG
|
|
283
|
+
* 🔴 Every refusal the DRAG honors is honored here, from the same fields: `acceptsDrop`,
|
|
284
284
|
* the consumer's `canDrop`, and the descendant rule. A keyboard route that can reach a
|
|
285
285
|
* destination the pointer refuses is a second, weaker opinion about what is legal — and it
|
|
286
286
|
* is the one nobody looks at, because the pointer is what gets tested by hand.
|
|
@@ -68,7 +68,7 @@ export function flattenFileTree(nodes, collapsed) {
|
|
|
68
68
|
* "A row that claims to open onto nothing is its own small lie" was the old reasoning
|
|
69
69
|
* and it is answered by what VS Code does: the chevron says *folder*, not *occupied*,
|
|
70
70
|
* and opening an empty one onto nothing is the honest answer, not a broken promise.
|
|
71
|
-
* `node.expandable === false` is still
|
|
71
|
+
* `node.expandable === false` is still honored for a row that can never hold anything.
|
|
72
72
|
*/
|
|
73
73
|
const expandable = node.kind === 'folder' && (node.expandable ?? true);
|
|
74
74
|
const expanded = expandable && !shut.has(node.id);
|
|
@@ -210,7 +210,7 @@ export function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder = fa
|
|
|
210
210
|
* without asking which of several folders was meant — and **← promotes out to the folder its
|
|
211
211
|
* own folder is in**, which is `root` at depth 1.
|
|
212
212
|
*
|
|
213
|
-
* 🔴 Every refusal the DRAG
|
|
213
|
+
* 🔴 Every refusal the DRAG honors is honored here, from the same fields: `acceptsDrop`,
|
|
214
214
|
* the consumer's `canDrop`, and the descendant rule. A keyboard route that can reach a
|
|
215
215
|
* destination the pointer refuses is a second, weaker opinion about what is legal — and it
|
|
216
216
|
* is the one nobody looks at, because the pointer is what gets tested by hand.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* applies, with nothing destroyed. Deleting the CONTENTS destroys items, and this module
|
|
26
26
|
* has never seen an item; it only ever sees the paths they carry. So `purge` is a mode the
|
|
27
27
|
* menu offers and the CONSUMER implements against its own store, which is also the only
|
|
28
|
-
* place that can count what is about to be lost or refuse the operation.
|
|
28
|
+
* place that can count what is about to be lost or refuse the operation. Modeling it here
|
|
29
29
|
* would mean inventing an item, and an item this model cannot see is one it cannot protect.
|
|
30
30
|
*/
|
|
31
31
|
import type { FolderNode, FolderRemap } from "./tree.js";
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* applies, with nothing destroyed. Deleting the CONTENTS destroys items, and this module
|
|
26
26
|
* has never seen an item; it only ever sees the paths they carry. So `purge` is a mode the
|
|
27
27
|
* menu offers and the CONSUMER implements against its own store, which is also the only
|
|
28
|
-
* place that can count what is about to be lost or refuse the operation.
|
|
28
|
+
* place that can count what is about to be lost or refuse the operation. Modeling it here
|
|
29
29
|
* would mean inventing an item, and an item this model cannot see is one it cannot protect.
|
|
30
30
|
*/
|
|
31
31
|
import { folderPathKey, isFolderPathUnder, parseFolderPath, serializeFolderPath, } from "./path.js";
|
|
@@ -70,7 +70,7 @@ export async function deriveMasterLockVerifier(masterPassword, params) {
|
|
|
70
70
|
}
|
|
71
71
|
const salt = unb64u(params.salt);
|
|
72
72
|
const kek = await pbkdf2(utf8(masterPassword), salt, params.iter);
|
|
73
|
-
// Same salt as the KEK, by design: it
|
|
73
|
+
// Same salt as the KEK, by design: it individualizes the verifier without a second
|
|
74
74
|
// stored value. One round, over KEK ‖ password.
|
|
75
75
|
const verifier = await pbkdf2(concat(kek, utf8(masterPassword)), salt, 1);
|
|
76
76
|
return b64u(verifier);
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ── 🔴 Why a decorative loop is NOT presence ────────────────────────────────
|
|
24
24
|
* The failure mode that would make this a security hole is an ambient background clip
|
|
25
25
|
* — muted, looping, autoplaying, running forever with nobody in the building. That is
|
|
26
|
-
* a
|
|
26
|
+
* a recognizable signature (`muted` AND `loop`) and it is refused. A film the owner is
|
|
27
27
|
* actually watching is not both at once; a muted film that loops is not something this
|
|
28
28
|
* fleet renders. Where a component knows better than the heuristic, it says so:
|
|
29
29
|
* `data-master-lock-presence="watch"` counts unconditionally, `"ignore"` never does.
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* ── 🔴 Why a decorative loop is NOT presence ────────────────────────────────
|
|
24
24
|
* The failure mode that would make this a security hole is an ambient background clip
|
|
25
25
|
* — muted, looping, autoplaying, running forever with nobody in the building. That is
|
|
26
|
-
* a
|
|
26
|
+
* a recognizable signature (`muted` AND `loop`) and it is refused. A film the owner is
|
|
27
27
|
* actually watching is not both at once; a muted film that loops is not something this
|
|
28
28
|
* fleet renders. Where a component knows better than the heuristic, it says so:
|
|
29
29
|
* `data-master-lock-presence="watch"` counts unconditionally, `"ignore"` never does.
|
|
@@ -112,7 +112,7 @@ export declare const MASTER_LOCK_STATE_HEADER = "x-master-lock-state";
|
|
|
112
112
|
* nothing in the fleet constructs one. So adding a required field to it is
|
|
113
113
|
* additive, and without this annotation it is the residual false MAJOR that
|
|
114
114
|
* Phase −1's M-B measured and had no cheap fix for (F4). `release/surface.ts`
|
|
115
|
-
* records the annotation; `release/classify.ts`
|
|
115
|
+
* records the annotation; `release/classify.ts` honors it. */
|
|
116
116
|
export interface MasterLockStatus {
|
|
117
117
|
/** Is a master password set at all? False only on an app that was never seeded,
|
|
118
118
|
* or — in per-person mode — for a person who has not chosen one yet. */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure MediaItem helpers — kind predicates + dimension/aspect resolution. The React
|
|
2
|
-
//
|
|
2
|
+
// catalog maps items to layout photos through these, so the "what size is this
|
|
3
3
|
// thumbnail" logic is tested here, not buried in a component.
|
|
4
4
|
export const isImageItem = (item) => item.type === 'image';
|
|
5
5
|
export const isVideoItem = (item) => item.type === 'video';
|
|
@@ -71,10 +71,10 @@ export type VideoProgressEvent = {
|
|
|
71
71
|
progressPercent?: number;
|
|
72
72
|
error?: string;
|
|
73
73
|
};
|
|
74
|
-
/** The three media kinds the
|
|
74
|
+
/** The three media kinds the catalog + lightbox understand. */
|
|
75
75
|
export type MediaKind = 'image' | 'video' | 'audio';
|
|
76
76
|
/**
|
|
77
|
-
* A URL-only
|
|
77
|
+
* A URL-only catalog item — the unit the `<MediaCatalogueViewer>` renders. It
|
|
78
78
|
* holds NO storage identity (no FsNode, no binary id): the app maps its own records
|
|
79
79
|
* to this and pre-resolves `url`/`thumbnailUrl` (signing private files as needed).
|
|
80
80
|
*/
|
|
@@ -95,7 +95,7 @@ export type MediaItem = {
|
|
|
95
95
|
durationSeconds?: number;
|
|
96
96
|
};
|
|
97
97
|
/**
|
|
98
|
-
* A gallery photo — a {@link MediaItem} specialized to images, plus the
|
|
98
|
+
* A gallery photo — a {@link MediaItem} specialized to images, plus the catalog affordances a
|
|
99
99
|
* photo grid renders (tags for the filter strip, createdAt for ordering, favorite for the ♥).
|
|
100
100
|
* Like every media-core unit it carries only resolved URLs — no storage identity.
|
|
101
101
|
*/
|
|
@@ -124,7 +124,7 @@ export type ProcessingStatus = 'pending' | 'processing' | 'ready' | 'failed';
|
|
|
124
124
|
/**
|
|
125
125
|
* A surface-agnostic description of a playable video with every URL already resolved —
|
|
126
126
|
* the unit the `<VideoPlayer>` primitive consumes (distinct from {@link MediaItem}, which
|
|
127
|
-
* is the lightbox
|
|
127
|
+
* is the lightbox catalog unit). It carries NO storage identity: a source pre-resolves
|
|
128
128
|
* the streaming URLs (signing/tokenising private ones), so the player couples to nothing.
|
|
129
129
|
* Richer than `MediaItem`: an HLS master + scrub-preview track for a real player HUD.
|
|
130
130
|
*/
|
|
@@ -43,7 +43,7 @@ export interface VideoCut {
|
|
|
43
43
|
* The shortest span worth cutting, in seconds.
|
|
44
44
|
*
|
|
45
45
|
* 🔴 Not zero, and not "whatever the person dragged". Below roughly a frame the request is
|
|
46
|
-
* indistinguishable from a mis-click, and it is a request ffmpeg cannot
|
|
46
|
+
* indistinguishable from a mis-click, and it is a request ffmpeg cannot honor anyway — a
|
|
47
47
|
* cut has to land on a frame boundary, so a 4 ms span asks the encoder to remove nothing and
|
|
48
48
|
* charges a full re-encode of the file for it. 40 ms is one frame at 25fps, the slowest rate
|
|
49
49
|
* anything in this library was shot at.
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
* The shortest span worth cutting, in seconds.
|
|
39
39
|
*
|
|
40
40
|
* 🔴 Not zero, and not "whatever the person dragged". Below roughly a frame the request is
|
|
41
|
-
* indistinguishable from a mis-click, and it is a request ffmpeg cannot
|
|
41
|
+
* indistinguishable from a mis-click, and it is a request ffmpeg cannot honor anyway — a
|
|
42
42
|
* cut has to land on a frame boundary, so a 4 ms span asks the encoder to remove nothing and
|
|
43
43
|
* charges a full re-encode of the file for it. 40 ms is one frame at 25fps, the slowest rate
|
|
44
44
|
* anything in this library was shot at.
|
|
@@ -252,7 +252,7 @@ export function toggleSegment(plan, segment, duration) {
|
|
|
252
252
|
splits: plan.splits,
|
|
253
253
|
// Drop every removal this segment sits inside, then put back the parts of a WIDER
|
|
254
254
|
// removal that are still meant to go — un-marking one piece of a span that was marked
|
|
255
|
-
// before it was split must not resurrect its
|
|
255
|
+
// before it was split must not resurrect its neighbors.
|
|
256
256
|
removed: plan.removed.flatMap((cut) => {
|
|
257
257
|
if (!(cut.start <= current.start + 1e-6 && cut.end >= current.end - 1e-6))
|
|
258
258
|
return [cut];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `cursedbelt-core/navigation` — the navigation contract + pure selectors. React-free and
|
|
2
2
|
// routing-free: the single source of truth (the IA tree) plus every derivation off
|
|
3
|
-
// it (side nav, hub tiles, breadcrumbs, search
|
|
3
|
+
// it (side nav, hub tiles, breadcrumbs, search catalog, coverage test, prefs
|
|
4
4
|
// migration). The React components in `cursedbelt/react` consume exactly this.
|
|
5
5
|
export { buildBreadcrumbTrail, } from './breadcrumbCompat.js';
|
|
6
6
|
export { createCoverageTest, findCoverageGaps, } from './coverageTest.js';
|
|
@@ -118,7 +118,7 @@ export interface NavDomain {
|
|
|
118
118
|
* again the same as on a {@link NavItem}.
|
|
119
119
|
*
|
|
120
120
|
* Both of these exist because a domain IS a destination: `deriveSearchCatalogue`
|
|
121
|
-
* emits one
|
|
121
|
+
* emits one catalog entry per domain, so a rail of leaf-shaped domains (an app
|
|
122
122
|
* whose places have no sub-items) had no way to be found by synonym while an
|
|
123
123
|
* otherwise identical rail of items did. That asymmetry was in the data model,
|
|
124
124
|
* not in the ranker.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The single navigation source of truth — a declarative Information-Architecture
|
|
2
2
|
// tree (Domain → Group → Item) every nav surface derives from: the side nav, hub
|
|
3
|
-
// landing tiles, breadcrumbs, the universal-search
|
|
3
|
+
// landing tiles, breadcrumbs, the universal-search catalog, and the command
|
|
4
4
|
// palette. Authoring a feature into the model is the *only* way to surface it, so
|
|
5
5
|
// the historical "four hand-maintained lists drift out of sync → features get
|
|
6
6
|
// buried" bug is structurally impossible (the coverage test enforces it in CI).
|
|
@@ -12,7 +12,7 @@ import { type GateFn, type NavDomain, type NavGroup, type NavItem, type Navigati
|
|
|
12
12
|
* stands before it and it starts with `/` — `/tools#tab=curl` is still `/tools`.
|
|
13
13
|
*/
|
|
14
14
|
export declare function normalizeNavPath(path: string): string;
|
|
15
|
-
/** A flattened, gated
|
|
15
|
+
/** A flattened, gated catalog entry — a leaf plus where it lives (for grouping). */
|
|
16
16
|
export interface SearchCatalogueEntry extends NavItem {
|
|
17
17
|
/** Owning domain id (absent for ungrouped items). */
|
|
18
18
|
domainId?: string;
|
|
@@ -57,7 +57,7 @@ export declare function deriveBreadcrumbTrail(model: NavigationModel, path: stri
|
|
|
57
57
|
* The flat list of searchable destinations — every visible (gated,
|
|
58
58
|
* non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
|
|
59
59
|
* results can be grouped. Drives HeaderSearch's instant pages layer *and* the
|
|
60
|
-
* command palette; replaces every hand-listed search
|
|
60
|
+
* command palette; replaces every hand-listed search catalog (so a feature is
|
|
61
61
|
* findable the moment it's registered).
|
|
62
62
|
*/
|
|
63
63
|
export declare function deriveSearchCatalogue(model: NavigationModel, gate?: GateFn): SearchCatalogueEntry[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure derivations off the {@link NavigationModel}. Every nav surface (side nav,
|
|
2
|
-
// hub tiles, breadcrumbs, search
|
|
2
|
+
// hub tiles, breadcrumbs, search catalog, palette, the coverage test) reads the
|
|
3
3
|
// model exclusively through these — there is no second list to keep in sync. They
|
|
4
4
|
// import nothing from React, so they run identically in the client, in a server
|
|
5
5
|
// test, and in the CI coverage check.
|
|
@@ -118,7 +118,7 @@ export function deriveBreadcrumbTrail(model, path, gate = allowAllGate) {
|
|
|
118
118
|
* The flat list of searchable destinations — every visible (gated,
|
|
119
119
|
* non-`deepLinkOnly`) domain landing + item, `searchOnly` ones included, each tagged with its domain/group so
|
|
120
120
|
* results can be grouped. Drives HeaderSearch's instant pages layer *and* the
|
|
121
|
-
* command palette; replaces every hand-listed search
|
|
121
|
+
* command palette; replaces every hand-listed search catalog (so a feature is
|
|
122
122
|
* findable the moment it's registered).
|
|
123
123
|
*/
|
|
124
124
|
export function deriveSearchCatalogue(model, gate = allowAllGate) {
|
|
@@ -12,8 +12,8 @@ export interface CatalogueResultGroup {
|
|
|
12
12
|
items: CatalogueHit[];
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Filter + rank a
|
|
16
|
-
* shows nothing until the user types). Stable: equal scores keep
|
|
15
|
+
* Filter + rank a catalog by `query`. Empty/blank query → `[]` (the instant layer
|
|
16
|
+
* shows nothing until the user types). Stable: equal scores keep catalog order.
|
|
17
17
|
*/
|
|
18
18
|
export declare function rankCatalogue(catalogue: SearchCatalogueEntry[], query: string): CatalogueHit[];
|
|
19
19
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Pure matcher + grouper for the instant client-side "pages" search layer. Ranks a
|
|
2
|
-
// derived
|
|
2
|
+
// derived catalog ({@link deriveSearchCatalogue}) against a query and buckets the
|
|
3
3
|
// hits by their owning domain. Kept pure + framework-free so it's unit-testable and
|
|
4
4
|
// shared by HeaderSearch and (task 14) the command palette. The palette upgrades the
|
|
5
5
|
// *ranking* to uFuzzy; this is the dependency-free baseline both can fall back to.
|
|
@@ -39,8 +39,8 @@ function scoreEntry(entry, q) {
|
|
|
39
39
|
return 0;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Filter + rank a
|
|
43
|
-
* shows nothing until the user types). Stable: equal scores keep
|
|
42
|
+
* Filter + rank a catalog by `query`. Empty/blank query → `[]` (the instant layer
|
|
43
|
+
* shows nothing until the user types). Stable: equal scores keep catalog order.
|
|
44
44
|
*/
|
|
45
45
|
export function rankCatalogue(catalogue, query) {
|
|
46
46
|
const q = norm(query);
|
|
@@ -53,7 +53,7 @@ export function rankCatalogue(catalogue, query) {
|
|
|
53
53
|
scored.push({ ...entry, score });
|
|
54
54
|
}
|
|
55
55
|
// Stable sort by score desc — Array.prototype.sort is stable in modern engines, so
|
|
56
|
-
// equal scores retain
|
|
56
|
+
// equal scores retain catalog (authoring) order.
|
|
57
57
|
return scored.sort((a, b) => b.score - a.score);
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
@@ -7,7 +7,7 @@ import type { NavItem } from '../navigation/index.js';
|
|
|
7
7
|
export declare function searchInitials(label: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* Rank `items` against `query`, best match first; non-matches dropped. Generic over the
|
|
10
|
-
* item type (so callers can pass plain {@link NavItem}s *or* richer
|
|
10
|
+
* item type (so callers can pass plain {@link NavItem}s *or* richer catalog entries
|
|
11
11
|
* that carry domain/group tags for grouping — the extra fields ride straight through).
|
|
12
12
|
* An empty/blank query returns `[]` (the palette shows its cold state instead).
|
|
13
13
|
*
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The command-palette ranking layer — uFuzzy over the nav
|
|
1
|
+
// The command-palette ranking layer — uFuzzy over the nav catalog. This is the
|
|
2
2
|
// *upgrade* HeaderSearch's `rankCatalogue` (a substring scorer) cannot match: uFuzzy
|
|
3
3
|
// scores by contiguous-match proximity, so abbreviated/typeahead input ranks the
|
|
4
4
|
// right page first. Pure + synchronous + React-free (uFuzzy is regex-based, no DOM),
|
|
@@ -42,7 +42,7 @@ function haystackFor(item) {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* The SECOND tier: what a destination holds rather than what it is called — its description,
|
|
45
|
-
* and the group and place it sits under when the entry carries them (
|
|
45
|
+
* and the group and place it sits under when the entry carries them (catalog entries do).
|
|
46
46
|
* Owner, 2026-10-09: *"I may not know where to go but typing something like 'api' brings up the
|
|
47
47
|
* relevant page links quickly"* — a page found only by its exact name is not found by someone
|
|
48
48
|
* who does not know the name.
|
|
@@ -63,7 +63,7 @@ function rankedIndexes(haystack, q) {
|
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* Rank `items` against `query`, best match first; non-matches dropped. Generic over the
|
|
66
|
-
* item type (so callers can pass plain {@link NavItem}s *or* richer
|
|
66
|
+
* item type (so callers can pass plain {@link NavItem}s *or* richer catalog entries
|
|
67
67
|
* that carry domain/group tags for grouping — the extra fields ride straight through).
|
|
68
68
|
* An empty/blank query returns `[]` (the palette shows its cold state instead).
|
|
69
69
|
*
|
|
@@ -36,7 +36,7 @@ export interface TokenDef<S extends z.ZodType = z.ZodType> {
|
|
|
36
36
|
subtypeOf?: string;
|
|
37
37
|
/** Canonical payload injected by the V7.4 wire smoke probe. Must satisfy `schema`. */
|
|
38
38
|
sample: z.infer<S>;
|
|
39
|
-
/** True when this token receives third-party traffic — triggers the V3
|
|
39
|
+
/** True when this token receives third-party traffic — triggers the V3 marshaler gates. */
|
|
40
40
|
external?: boolean;
|
|
41
41
|
deprecated?: TokenDeprecation;
|
|
42
42
|
description: string;
|
|
@@ -405,7 +405,7 @@ export const BASE_TOKENS = [
|
|
|
405
405
|
id: 'knowledge:records',
|
|
406
406
|
mode: 'value',
|
|
407
407
|
external: true,
|
|
408
|
-
description: 'Imported record rows (CSV/sheet/webhook) — external traffic,
|
|
408
|
+
description: 'Imported record rows (CSV/sheet/webhook) — external traffic, marshaler-gated (02 §7.1).',
|
|
409
409
|
schema: z.object({ rows: z.array(z.record(z.string(), z.unknown())) }),
|
|
410
410
|
sample: { rows: [{ id: '1' }] },
|
|
411
411
|
},
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A REAL
|
|
2
|
+
* A REAL marshaling worker entry, used by `marshalWorker.spec.ts` to prove the
|
|
3
3
|
* round-trip over an actual OS thread rather than over an injected fake.
|
|
4
4
|
*
|
|
5
5
|
* This is also the shape every production entry has, so it doubles as the
|
|
6
|
-
* reference an app author copies: construct the
|
|
6
|
+
* reference an app author copies: construct the marshaler, hand it to
|
|
7
7
|
* `serveMarshalWorker`, done. The host never imports this file — it names it,
|
|
8
8
|
* and the worker imports it.
|
|
9
9
|
*/
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This entry exists because of Law #1, for the same reason `client/index.ts`
|
|
5
5
|
* gives that reason for `createWireHub`: an ingress route has to be able to
|
|
6
|
-
* IMPORT the
|
|
6
|
+
* IMPORT the marshaling seam, and a worker entry has to be able to import
|
|
7
7
|
* `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
|
|
8
8
|
* export path at all — `marshalInbound` was reachable only from inside this
|
|
9
9
|
* package, which is most of why it went three arcs with zero callers.
|
package/dist/core/wire/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This entry exists because of Law #1, for the same reason `client/index.ts`
|
|
5
5
|
* gives that reason for `createWireHub`: an ingress route has to be able to
|
|
6
|
-
* IMPORT the
|
|
6
|
+
* IMPORT the marshaling seam, and a worker entry has to be able to import
|
|
7
7
|
* `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
|
|
8
8
|
* export path at all — `marshalInbound` was reachable only from inside this
|
|
9
9
|
* package, which is most of why it went three arcs with zero callers.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { type MarshallerRegistry } from './marshalling.js';
|
|
2
|
-
/** No
|
|
2
|
+
/** No marshaler is registered for the token an ingress was asked to feed. */
|
|
3
3
|
export declare const INGRESS_TOKEN_UNKNOWN = "INGRESS_TOKEN_UNKNOWN";
|
|
4
4
|
export interface MarshalledIngressOptions {
|
|
5
5
|
/** The registry the app's wire tokens were registered on. */
|
|
6
6
|
readonly registry: MarshallerRegistry;
|
|
7
7
|
/**
|
|
8
|
-
* Per-payload worker ceiling. A
|
|
8
|
+
* Per-payload worker ceiling. A marshaling worker that hangs would otherwise
|
|
9
9
|
* hold the request open for as long as the client is willing to wait.
|
|
10
10
|
*/
|
|
11
11
|
readonly workerTimeoutMs?: number;
|
|
@@ -21,12 +21,12 @@ export interface MarshalledIngress {
|
|
|
21
21
|
*
|
|
22
22
|
* @throws {AppError} `INGRESS_TOKEN_UNKNOWN` (404) when nothing is registered;
|
|
23
23
|
* `MARSHALLER_PAYLOAD_TOO_LARGE` (413/503), `MARSHALLER_VALIDATION_FAILED`
|
|
24
|
-
* (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the
|
|
24
|
+
* (400) or `MARSHALLER_SCHEMA_VIOLATION` (502) from the marshaler itself.
|
|
25
25
|
*/
|
|
26
26
|
ingest<Out = unknown>(tokenId: string, payload: unknown): Promise<Out>;
|
|
27
27
|
/**
|
|
28
28
|
* Read a `Request` body and marshal it for `tokenId`. JSON bodies are parsed;
|
|
29
|
-
* anything else is handed to the
|
|
29
|
+
* anything else is handed to the marshaler as bytes, because a marshaler for
|
|
30
30
|
* a CSV or a binary feed wants the bytes and re-encoding them would be lossy.
|
|
31
31
|
*/
|
|
32
32
|
ingestRequest<Out = unknown>(tokenId: string, request: Request): Promise<Out>;
|
|
@@ -13,25 +13,25 @@
|
|
|
13
13
|
* webhook endpoint that no product asked for would create a new orphan seam of
|
|
14
14
|
* exactly the kind Law #1 exists to prevent, and it would guess wrong about the
|
|
15
15
|
* part that actually differs between a webhook and an import (authentication,
|
|
16
|
-
* routing, what the
|
|
16
|
+
* routing, what the marshaled value is handed to). What does NOT differ — and
|
|
17
17
|
* what every ingress would otherwise re-derive, differently — is this:
|
|
18
18
|
*
|
|
19
19
|
* 1. reading the body as bytes without buffering a stream into RAM twice;
|
|
20
|
-
* 2. finding the
|
|
20
|
+
* 2. finding the marshaler for the token being fed;
|
|
21
21
|
* 3. owning a worker per `workerEntry` — a live OS thread, so exactly one
|
|
22
22
|
* thing must own disposing of it — and resolving it lazily and cached;
|
|
23
23
|
* 4. handing those failures up as errors that already KNOW their HTTP status,
|
|
24
|
-
* so no route re-derives the mapping. The
|
|
24
|
+
* so no route re-derives the mapping. The marshaling tier's codes are
|
|
25
25
|
* unchanged and deliberately distinct: 413 over the declared ceiling, 400
|
|
26
26
|
* for a payload `validate` refuses (the caller's shape is wrong), 502 when
|
|
27
27
|
* the TRANSFORM OUTPUT violates the token's canonical schema (an upstream
|
|
28
28
|
* provider changed shape under us — not our error budget), 503 for a
|
|
29
|
-
* payload that needs offload from a
|
|
29
|
+
* payload that needs offload from a marshaler with no worker.
|
|
30
30
|
*
|
|
31
31
|
* Step 3 is the one that would go wrong. `createMarshalWorkerClient` spawns a
|
|
32
32
|
* thread per call; a route handler that called it per request would leak one
|
|
33
33
|
* OS thread per webhook, and the symptom would be a server that gets slower for
|
|
34
|
-
* a week and then dies. Nothing about that is discoverable from the
|
|
34
|
+
* a week and then dies. Nothing about that is discoverable from the marshaling
|
|
35
35
|
* API, which is precisely why it belongs here once instead of in each ingress.
|
|
36
36
|
*
|
|
37
37
|
* ## Adding an ingress later
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
* });
|
|
48
48
|
* ```
|
|
49
49
|
*
|
|
50
|
-
* A CSV import is the same three lines with a different
|
|
50
|
+
* A CSV import is the same three lines with a different marshaler and a
|
|
51
51
|
* different service call. Neither needs to know that a worker exists.
|
|
52
52
|
*/
|
|
53
53
|
import { AppError } from '../../shared/errors.js';
|
|
54
54
|
import { createMarshalWorkerClient } from './marshalWorker.js';
|
|
55
55
|
import { marshalInbound, } from './marshalling.js';
|
|
56
|
-
/** No
|
|
56
|
+
/** No marshaler is registered for the token an ingress was asked to feed. */
|
|
57
57
|
export const INGRESS_TOKEN_UNKNOWN = 'INGRESS_TOKEN_UNKNOWN';
|
|
58
58
|
/**
|
|
59
59
|
* Build the ingress. One per app — it caches a worker per `workerEntry`, and two
|
|
@@ -68,7 +68,7 @@ export const createMarshalledIngress = (options) => {
|
|
|
68
68
|
if (entry === undefined)
|
|
69
69
|
return undefined;
|
|
70
70
|
if (disposed)
|
|
71
|
-
throw new AppError('This ingress has been disposed and cannot spawn a
|
|
71
|
+
throw new AppError('This ingress has been disposed and cannot spawn a marshaling worker.', {
|
|
72
72
|
code: 'INGRESS_DISPOSED',
|
|
73
73
|
status: 503,
|
|
74
74
|
isOperational: false,
|
|
@@ -88,10 +88,10 @@ export const createMarshalledIngress = (options) => {
|
|
|
88
88
|
const marshallerFor = (tokenId) => {
|
|
89
89
|
const marshaller = options.registry.forToken(tokenId);
|
|
90
90
|
if (!marshaller)
|
|
91
|
-
// 404, not 400: from outside, a token with no
|
|
91
|
+
// 404, not 400: from outside, a token with no marshaler and a token that
|
|
92
92
|
// does not exist are the same fact, and distinguishing them would make
|
|
93
93
|
// this endpoint an enumeration oracle for the app's wire surface.
|
|
94
|
-
throw new AppError(`No
|
|
94
|
+
throw new AppError(`No marshaler is registered for wire token '${tokenId}'.`, {
|
|
95
95
|
code: INGRESS_TOKEN_UNKNOWN,
|
|
96
96
|
status: 404,
|
|
97
97
|
context: { tokenId },
|
|
@@ -106,8 +106,8 @@ export const createMarshalledIngress = (options) => {
|
|
|
106
106
|
},
|
|
107
107
|
async ingestRequest(tokenId, request) {
|
|
108
108
|
const contentType = request.headers.get('content-type') ?? '';
|
|
109
|
-
// JSON is parsed here so the
|
|
110
|
-
// it would have to parse itself (and parse DIFFERENTLY per
|
|
109
|
+
// JSON is parsed here so the marshaler sees a value rather than a string
|
|
110
|
+
// it would have to parse itself (and parse DIFFERENTLY per marshaler).
|
|
111
111
|
// A malformed body is the CALLER's error — 400 — and must not read as a
|
|
112
112
|
// validation failure of a payload we never managed to decode.
|
|
113
113
|
const payload = contentType.includes('application/json')
|
|
@@ -58,7 +58,7 @@ export interface MarshalWorkerClient extends MarshalWorker {
|
|
|
58
58
|
dispose(): Promise<void>;
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* A long-lived worker for ONE
|
|
61
|
+
* A long-lived worker for ONE marshaler, multiplexing concurrent payloads.
|
|
62
62
|
*
|
|
63
63
|
* Long-lived rather than one-worker-per-payload deliberately: thread startup
|
|
64
64
|
* plus module load is tens of milliseconds, which on a burst of imports would
|