@needle-tools/addressing 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/Readme.md +62 -0
  2. package/dist/address.d.ts +5 -0
  3. package/dist/address.js +25 -3
  4. package/dist/document/assets.d.ts +27 -0
  5. package/dist/document/assets.js +75 -0
  6. package/dist/document/collectionSelector.d.ts +9 -0
  7. package/dist/document/collectionSelector.js +29 -0
  8. package/dist/document/evaluate.d.ts +72 -0
  9. package/dist/document/evaluate.js +971 -0
  10. package/dist/document/index.d.ts +8 -0
  11. package/dist/document/index.js +8 -0
  12. package/dist/document/inspection.d.ts +30 -0
  13. package/dist/document/inspection.js +130 -0
  14. package/dist/document/runtime.d.ts +71 -0
  15. package/dist/document/runtime.js +1 -0
  16. package/dist/document/types.d.ts +225 -0
  17. package/dist/document/types.js +43 -0
  18. package/dist/document/validation.d.ts +36 -0
  19. package/dist/document/validation.js +219 -0
  20. package/dist/document/write.d.ts +31 -0
  21. package/dist/document/write.js +106 -0
  22. package/dist/id.d.ts +2 -0
  23. package/dist/id.js +4 -0
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +2 -1
  26. package/dist/loadedGltf.d.ts +32 -0
  27. package/dist/loadedGltf.js +62 -0
  28. package/dist/resolve.js +8 -6
  29. package/package.json +9 -1
  30. package/src/address.ts +28 -3
  31. package/src/document/assets.ts +77 -0
  32. package/src/document/collectionSelector.ts +37 -0
  33. package/src/document/evaluate.ts +998 -0
  34. package/src/document/index.ts +8 -0
  35. package/src/document/inspection.ts +123 -0
  36. package/src/document/runtime.ts +66 -0
  37. package/src/document/schema.json +263 -0
  38. package/src/document/types.ts +249 -0
  39. package/src/document/validation.ts +222 -0
  40. package/src/document/write.ts +112 -0
  41. package/src/id.ts +5 -0
  42. package/src/index.ts +3 -0
  43. package/src/loadedGltf.ts +93 -0
  44. package/src/resolve.ts +9 -7
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./runtime.js";
3
+ export * from "./evaluate.js";
4
+ export * from "./inspection.js";
5
+ export * from "./validation.js";
6
+ export * from "./write.js";
7
+ export * from "./assets.js";
8
+ export * from "./collectionSelector.js";
@@ -0,0 +1,8 @@
1
+ export * from "./types.js";
2
+ export * from "./runtime.js";
3
+ export * from "./evaluate.js";
4
+ export * from "./inspection.js";
5
+ export * from "./validation.js";
6
+ export * from "./write.js";
7
+ export * from "./assets.js";
8
+ export * from "./collectionSelector.js";
@@ -0,0 +1,30 @@
1
+ import { type GraphNode, type NeedleAddress } from "../index.js";
2
+ export type ProjectInspectionTarget = {
3
+ ref: string;
4
+ address?: NeedleAddress;
5
+ pathPrefix?: string;
6
+ };
7
+ type Node = GraphNode & {
8
+ parent?: Node | null;
9
+ isObject3D?: boolean;
10
+ };
11
+ /** Reference membership is captured at placement, before parent overs move nodes. */
12
+ export declare class ProjectDocumentInspection {
13
+ private readonly scene;
14
+ private readonly scopes;
15
+ private readonly targets;
16
+ ready: Promise<void>;
17
+ constructor(scene: Node);
18
+ registerReference(ref: string, root: object, elements?: Iterable<object>, referenceRoots?: ReadonlySet<object>): void;
19
+ resolve(target: ProjectInspectionTarget): object | null;
20
+ targetFor(object: object): ProjectInspectionTarget | null;
21
+ private inScene;
22
+ }
23
+ /** Opt in before loading a document; ordinary playback allocates no inspection index. */
24
+ export declare function enableProjectDocumentInspection(): {
25
+ dispose(): void;
26
+ };
27
+ export declare function createProjectDocumentInspection(scene: Node): ProjectDocumentInspection | undefined;
28
+ export declare function getProjectDocumentInspection(context: object): ProjectDocumentInspection | undefined;
29
+ export declare function registerProjectDocumentInspection(context: object, inspection: ProjectDocumentInspection): void;
30
+ export {};
@@ -0,0 +1,130 @@
1
+ import { resolveDocumentCollectionElement } from "./collectionSelector.js";
2
+ import { joinPath, parseAnchorValue, resolveInCollection, resolveInGraph, splitPath } from "../index.js";
3
+ /** Reference membership is captured at placement, before parent overs move nodes. */
4
+ export class ProjectDocumentInspection {
5
+ constructor(scene) {
6
+ this.scene = scene;
7
+ this.scopes = new Map();
8
+ this.targets = new WeakMap();
9
+ this.ready = Promise.resolve();
10
+ }
11
+ registerReference(ref, root, elements = [], referenceRoots) {
12
+ const nodes = new Map();
13
+ const scope = { root: new WeakRef(root), nodes, elements: [] };
14
+ const capture = (node, path) => {
15
+ const weak = new WeakRef(node);
16
+ const graph = {
17
+ get name() { return weak.deref()?.name; },
18
+ get userData() { return weak.deref()?.userData; },
19
+ children: node.children?.filter(child => !referenceRoots?.has(child))
20
+ .map(child => capture(child, [...path, child.name ?? ""])),
21
+ };
22
+ nodes.set(graph, weak);
23
+ const id = node.userData?.["needle-id"];
24
+ const anchor = parseAnchorValue(node.name);
25
+ const address = node === root ? undefined : {
26
+ type: "node", path: joinPath(path), ...(typeof id === "string" ? { id } : {}), ...(anchor ? { anchor } : {}),
27
+ };
28
+ if (node === root || address)
29
+ this.targets.set(node, { ref, ...(address ? { address } : {}) });
30
+ return graph;
31
+ };
32
+ if (root.isObject3D)
33
+ scope.graph = capture(root, []);
34
+ else
35
+ this.targets.set(root, { ref });
36
+ for (const element of elements) {
37
+ const value = element;
38
+ const type = value.isMaterial ? "material" : value.isTexture ? "texture" : value.isAnimationClip ? "animation" : null;
39
+ if (!type)
40
+ continue;
41
+ scope.elements.push(new WeakRef(element));
42
+ const id = value.userData?.["needle-id"];
43
+ if (typeof id === "string")
44
+ this.targets.set(element, { ref, address: { type, id, name: value.name } });
45
+ }
46
+ this.scopes.set(ref, scope);
47
+ }
48
+ resolve(target) {
49
+ const scope = this.scopes.get(target.ref);
50
+ let object = target.ref === "$scene" ? this.scene : scope?.root.deref() ?? null;
51
+ if (target.address) {
52
+ const address = target.address;
53
+ if (address.type && address.type !== "node") {
54
+ const elements = scope?.elements.flatMap(weak => {
55
+ const value = weak.deref();
56
+ const matches = address.type === "material" ? value?.isMaterial : address.type === "texture" ? value?.isTexture : address.type === "animation" ? value?.isAnimationClip : false;
57
+ return value && matches ? [value] : [];
58
+ }) ?? [];
59
+ object = resolveInCollection(address, elements);
60
+ }
61
+ else if (scope?.graph) {
62
+ const graph = resolveInGraph(address, scope.graph);
63
+ object = graph ? scope.nodes.get(graph)?.deref() ?? null : null;
64
+ if (object && !this.inScene(object))
65
+ object = null;
66
+ }
67
+ else
68
+ object = target.ref === "$scene" ? resolveInGraph(address, this.scene) : null;
69
+ }
70
+ if (!object)
71
+ return null;
72
+ if (object.isObject3D && !this.inScene(object))
73
+ return null;
74
+ if (target.pathPrefix) {
75
+ const parts = splitPath(target.pathPrefix.replace(/\/$/, ""));
76
+ for (const part of parts) {
77
+ if (part === "components")
78
+ object = object.userData?.components ?? null;
79
+ else if (Array.isArray(object) && part.startsWith("guid:")) {
80
+ const guid = part.slice(5);
81
+ object = resolveDocumentCollectionElement(object, `guid:${guid}`) ?? null;
82
+ }
83
+ else
84
+ return null;
85
+ if (!object)
86
+ return null;
87
+ }
88
+ }
89
+ return object;
90
+ }
91
+ targetFor(object) {
92
+ if (object === this.scene)
93
+ return { ref: "$scene" };
94
+ const component = object;
95
+ const guid = component.needle_editor_guid || component.guid;
96
+ if (component.gameObject && guid) {
97
+ const host = this.targetFor(component.gameObject);
98
+ return host ? { ...host, pathPrefix: `components/guid:${guid}/` } : null;
99
+ }
100
+ const target = this.targets.get(object);
101
+ return target && this.resolve(target) === object ? structuredClone(target) : null;
102
+ }
103
+ inScene(object) {
104
+ // Liveness only; reference ownership comes from the placement scope above.
105
+ for (let node = object; node; node = node.parent)
106
+ if (node === this.scene)
107
+ return true;
108
+ return false;
109
+ }
110
+ }
111
+ const inspections = new WeakMap();
112
+ let observers = 0;
113
+ /** Opt in before loading a document; ordinary playback allocates no inspection index. */
114
+ export function enableProjectDocumentInspection() {
115
+ observers++;
116
+ let disposed = false;
117
+ return { dispose() { if (!disposed) {
118
+ disposed = true;
119
+ observers--;
120
+ } } };
121
+ }
122
+ export function createProjectDocumentInspection(scene) {
123
+ return observers ? new ProjectDocumentInspection(scene) : undefined;
124
+ }
125
+ export function getProjectDocumentInspection(context) {
126
+ return inspections.get(context);
127
+ }
128
+ export function registerProjectDocumentInspection(context, inspection) {
129
+ inspections.set(context, inspection);
130
+ }
@@ -0,0 +1,71 @@
1
+ import type { GraphNode } from "../address.js";
2
+ import type { GltfAssociation } from "../loadedGltf.js";
3
+ import type { ProjectEvaluationIssue } from "./types.js";
4
+ export interface DocumentVector {
5
+ fromArray(value: number[]): unknown;
6
+ toArray(): number[];
7
+ }
8
+ /** The scene operations used by composition, independent of a three.js build. */
9
+ export interface DocumentNode extends GraphNode {
10
+ name: string;
11
+ isObject3D: boolean;
12
+ userData: Record<string, unknown>;
13
+ parent: DocumentNode | null;
14
+ children: DocumentNode[];
15
+ position: DocumentVector;
16
+ quaternion: DocumentVector;
17
+ scale: DocumentVector;
18
+ add(...nodes: DocumentNode[]): unknown;
19
+ removeFromParent(): unknown;
20
+ traverse(visitor: (node: DocumentNode) => void): void;
21
+ }
22
+ export interface DocumentModel {
23
+ scene?: DocumentNode;
24
+ scenes?: DocumentNode[];
25
+ animations?: object[];
26
+ parser?: {
27
+ json?: Record<string, unknown> & {
28
+ materials?: Array<{
29
+ name?: string;
30
+ }>;
31
+ };
32
+ associations?: Map<object, GltfAssociation>;
33
+ getDependency?(type: string, index: number): Promise<unknown>;
34
+ };
35
+ }
36
+ export type DocumentIssueReporter = (arc: ProjectEvaluationIssue["arc"], message: string) => void;
37
+ export interface DocumentDefContext {
38
+ scene?: DocumentNode;
39
+ createNode(): DocumentNode;
40
+ applyComponent?(node: DocumentNode, data: {
41
+ name: string;
42
+ } & Record<string, unknown>, issue: (arc: "def", message: string) => void): void;
43
+ }
44
+ /** Loading and component lifecycle belong to the host; document rules do not. */
45
+ export interface DocumentRuntime {
46
+ scene: DocumentNode;
47
+ renderer: object;
48
+ createNode(): DocumentNode;
49
+ createColor(value: number[]): object;
50
+ parse(data: ArrayBuffer | string, src: string, options?: {
51
+ resolveUrl(url: string): string;
52
+ }): Promise<DocumentModel | null>;
53
+ instantiate(template: DocumentNode, referenceId: string): DocumentNode | null;
54
+ parkTemplate(node: DocumentNode): void;
55
+ activateInstance(node: DocumentNode): void;
56
+ disposeTemplate(node: DocumentNode): void;
57
+ /** Run after template deserialization, before any instance activates its components. */
58
+ schedule(work: () => void): void;
59
+ loadEnvironment(url: string, options: {
60
+ format?: "pmrem";
61
+ name?: string;
62
+ }): Promise<object | null>;
63
+ createDefContext?(root: DocumentNode, baseUrl?: string): DocumentDefContext;
64
+ getComponents?(node: DocumentNode): object[];
65
+ matchesComponentType?(component: object, name: string): boolean;
66
+ matchesComponentGuid?(component: object, guid: string): boolean;
67
+ deriveComponentGuid?(referenceId: string, fileGuid: string): string;
68
+ removeComponent?(component: object, owner: DocumentNode | null): void;
69
+ /** Identity used by the optional inspection registry; defaults to the runtime itself. */
70
+ inspectionOwner?: object;
71
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,225 @@
1
+ import type { NeedleAddress } from "../address.js";
2
+ import type { DocumentNode, DocumentModel } from "./runtime.js";
3
+ /** the version discriminator key every project document must carry */
4
+ export declare const PROJECT_DOCUMENT_KEY = "needle:project";
5
+ export type ProjectDocumentAsset = {
6
+ /** where the asset lives — resolved against `EvaluateProjectDocumentOptions.baseUrl` */
7
+ src: string;
8
+ /**
9
+ * Optional PROVIDER IDENTITY for assets whose resources (textures, buffers) live at
10
+ * provider-specific urls that `src` alone can't reach (e.g. PolyHaven). The host recovers
11
+ * the provider's resolver from this identity at load time (see
12
+ * `EvaluateProjectDocumentOptions.resolveAssetUrl`). Self-contained assets omit it and
13
+ * load by `src`. The engine ignores it; only the host (editor) reads it.
14
+ */
15
+ asset?: {
16
+ id: string;
17
+ provider?: string;
18
+ };
19
+ /**
20
+ * Optional SEMANTIC KIND, decided at USE time by the writer ("model", "material",
21
+ * "hdr", "texture", "image", "video", "audio", "code", "text") — the same file can
22
+ * be a texture on a material slot or a plain image in UI, and downstream
23
+ * processing (compression at publish) follows the use, not the extension. The
24
+ * engine ignores it; only hosts/tooling read it.
25
+ */
26
+ kind?: string;
27
+ /**
28
+ * INFORMATIONAL: the server `ETag` observed when this document captured the
29
+ * asset — lets a future reader detect "the upstream file changed since".
30
+ * Never used for loading decisions; the engine ignores it.
31
+ */
32
+ etag?: string;
33
+ };
34
+ export type ProjectDocumentReference = {
35
+ /** project-local id of this reference — the resolution scope (or entity handle) for targets */
36
+ id: string;
37
+ /** key into the document's `assets` map */
38
+ asset: string;
39
+ /**
40
+ * ELEMENT REFERENCE discriminator: when present, this reference instantiates the
41
+ * addressed element INSIDE the asset (e.g. a material) into the composition's ENTITY
42
+ * POOL instead of placing a scene subtree. Entity references are first-class composed
43
+ * entities: overs can target them (`{ ref: <id>, path: "map/repeat" }`) and values can
44
+ * point at them (`{ ref: <id> }` — e.g. a mesh's material assignment). N consumers of
45
+ * the same entity share ONE runtime instance by construction (USD: a Material-prim
46
+ * reference; assignment ≈ material:binding).
47
+ */
48
+ address?: NeedleAddress;
49
+ /** @deprecated (2026-08-03, read-only shim — remove soon) replaced by parent OVERS
50
+ * (`{ target: { ref, path: "parent" }, value: { ref | "$scene", address?, index? } }`)
51
+ * days after shipping; only unreleased documents from that week carry this field.
52
+ * The writer no longer emits it; the evaluator still honors it at placement. */
53
+ parent?: {
54
+ ref: string;
55
+ address?: NeedleAddress;
56
+ };
57
+ };
58
+ /** A host-owned root that document opinions compose onto instead of instantiating. */
59
+ export type ProjectDocumentBinding = {
60
+ /** Document-local reference used by targets. `$`-prefixed ids are reserved. */
61
+ id: string;
62
+ /** The kind of host root required by this document. */
63
+ kind: "scene";
64
+ /** Stable structural identity used by the host to match this root before applying edits. */
65
+ signature: string;
66
+ /** Informational label for diagnostics only; never used as identity. */
67
+ name?: string;
68
+ /** Existing asset-reference ids whose instances belong to this host root. */
69
+ references?: string[];
70
+ };
71
+ export type ProjectDocumentTarget = {
72
+ /** which placed reference the address resolves WITHIN; omitted → the composition root */
73
+ ref?: string;
74
+ /** fat address of the element inside the reference (needle-id / anchor / path) */
75
+ address?: NeedleAddress;
76
+ /**
77
+ * Property path within the resolved element, "/"-separated.
78
+ *
79
+ * A segment landing on a COLLECTION may be a `<field>:<value>` qualifier instead of an
80
+ * index — `components/type:DragControls/enabled`, `components/guid:6f1f…/enabled` —
81
+ * so a document never depends on array order (`userData.components` reorders whenever
82
+ * a component is added or removed). A bare numeric segment on an array is still an
83
+ * index, but it is now an explicit opt-in rather than the only option.
84
+ * `components` is a synthesized segment on a node: the live layout
85
+ * (`userData.components`) stays an implementation detail.
86
+ *
87
+ * Reserved LAST segment: `active` — with `value: false` it removes whatever the rest
88
+ * of the path resolved to (the node itself, or a component) from the composed result.
89
+ */
90
+ path?: string;
91
+ };
92
+ export type ProjectDocumentDef = {
93
+ target: ProjectDocumentTarget;
94
+ /**
95
+ * When present, the def CREATES a new node (USD-proper: def defines prims) under the
96
+ * resolved target (composition root when the target is empty) and the components
97
+ * apply to IT — e.g. the runtime start camera. Without `create`, components apply to
98
+ * the resolved existing node.
99
+ */
100
+ create?: {
101
+ id?: string;
102
+ name?: string;
103
+ position?: number[];
104
+ quaternion?: number[];
105
+ scale?: number[];
106
+ /** Recreate a hierarchy duplicate from its stable source and seed. */
107
+ copy?: {
108
+ source: ProjectDocumentTarget;
109
+ seed: string;
110
+ reference?: boolean;
111
+ } & ({
112
+ batch: string;
113
+ index: number;
114
+ } | {
115
+ batch?: never;
116
+ index?: never;
117
+ });
118
+ };
119
+ /** VERBATIM NEEDLE_components wire entries ({ name, guid, ...fields }) */
120
+ components?: Array<{
121
+ name: string;
122
+ } & Record<string, unknown>>;
123
+ };
124
+ export type ProjectDocumentOver = {
125
+ target: ProjectDocumentTarget;
126
+ value: unknown;
127
+ };
128
+ export type ProjectDocument = {
129
+ [PROJECT_DOCUMENT_KEY]: string;
130
+ /** layer metadata (USD's "layer metadata" concept — e.g. `name`); free-form */
131
+ metadata?: Record<string, unknown>;
132
+ /** named remote origins for `remote://<name>/<path>` srcs — the scheme
133
+ * routes, the AUTHORITY names the remote, this table supplies the base.
134
+ * Resolved per DOCUMENT (a nested document resolves with ITS table), so
135
+ * the document works wherever it travels. Semantics mirror the editor's
136
+ * remoteSrc.ts — keep the two in sync. */
137
+ remotes?: Record<string, string>;
138
+ assets?: Record<string, ProjectDocumentAsset>;
139
+ /** Required host-owned roots. The host resolves these before evaluation. */
140
+ bindings?: ProjectDocumentBinding[];
141
+ references?: ProjectDocumentReference[];
142
+ defs?: ProjectDocumentDef[];
143
+ overs?: ProjectDocumentOver[];
144
+ /** unknown arcs are ignored by evaluation and must be preserved by writers */
145
+ [arc: string]: unknown;
146
+ };
147
+ /**
148
+ * Resolve a `remote://name/path` src via the document's remotes table
149
+ * (`overrides` win for known names — the host's per-environment repoint, e.g.
150
+ * the editor's dev proxy). Non-remote srcs pass through. An UNKNOWN remote
151
+ * name resolves to null — the caller reports it; guessing a base would load
152
+ * the wrong bytes silently. MIRRORS the editor's remoteSrc.ts resolveRemoteSrc.
153
+ */
154
+ export declare function resolveDocumentRemoteSrc(src: string, remotes: Record<string, string> | undefined, overrides?: Record<string, string>): string | null;
155
+ /** structural check for the version discriminator — the opener/sniffing entry point */
156
+ export declare function isProjectDocument(value: unknown): value is ProjectDocument;
157
+ export type ProjectEvaluationIssue = {
158
+ arc: "reference" | "def" | "over";
159
+ message: string;
160
+ };
161
+ /**
162
+ * Report evaluation issues to the console — in PRODUCTION too. Every issue
163
+ * means the document asked for something the runtime could not provide (a
164
+ * dropped def, an unresolved over, a failed asset), i.e. the loaded scene
165
+ * silently differs from what the author saved. The author debugs on the
166
+ * deployed page: gating this behind dev/?debugproject cost a full day of
167
+ * blind hunting when a deployed scene lost a component with a clean console
168
+ * (2026-09-01). A healthy document reports nothing.
169
+ */
170
+ export declare function reportProjectIssues(issues: readonly ProjectEvaluationIssue[], warn?: (message: string) => void): void;
171
+ export type ProjectEvaluationResult = {
172
+ /** the object all non-mounted references were added to */
173
+ root: DocumentNode;
174
+ /** Placed reference roots by reference id. Populated during the host's scheduled
175
+ * composition phase, after template deserialization; await `completed` first. */
176
+ references: Map<string, DocumentNode>;
177
+ /** per-reference Model views by reference id (`scene` is that reference's placed
178
+ * INSTANCE; parser etc. come from the shared template). Populated at `completed`,
179
+ * like `references`. */
180
+ models: Map<string, DocumentModel>;
181
+ /**
182
+ * misses and errors — an issue never aborts evaluation, the arc is skipped.
183
+ * Populated up to `completed` (defs/overs apply in the host's composition phase).
184
+ */
185
+ issues: ProjectEvaluationIssue[];
186
+ /**
187
+ * Resolves once defs/overs have been applied. Hosts schedule composition after
188
+ * asset deserialization and before component activation. Needle's adapter uses
189
+ * its pre-setup phase, requiring a context tick; a vanilla host can run immediately.
190
+ */
191
+ completed: Promise<void>;
192
+ };
193
+ export type EvaluateProjectDocumentOptions = {
194
+ /** Reuse existing roots as named resolution scopes. Bindings may supply external
195
+ * host roots that have no asset reference in the document. When supplied, every
196
+ * declared scene reference must also be bound; a missing binding is reported
197
+ * instead of loading another instance. */
198
+ referenceBindings?: ReadonlyMap<string, DocumentNode>;
199
+ /** where references are placed; defaults to `context.scene` */
200
+ parent?: DocumentNode;
201
+ /** resolves `assets.src` for the document — tests/tools provide in-memory data here */
202
+ resolveAsset?: (assetId: string, src: string) => Promise<ArrayBuffer | string>;
203
+ /** base for resolving relative `assets.src` urls with the default (fetch) resolver */
204
+ baseUrl?: string;
205
+ /** per-environment overrides for the document's `remotes` table (known
206
+ * names only — the editor maps needle-cloud to its dev proxy); the
207
+ * document's own table is the default */
208
+ remotes?: Record<string, string>;
209
+ /**
210
+ * Rewrites a resource url (texture / external buffer) encountered while parsing an
211
+ * asset — the asset service's `resolveUrl` re-entering at load time. Needed for provider
212
+ * assets (e.g. PolyHaven) whose textures live at urls that plain relative resolution
213
+ * against `assets.src` cannot reach. Passed to the host's parse operation;
214
+ * the editor opener supplies it from `IAssetsService` so this
215
+ * evaluator carries NO provider knowledge.
216
+ */
217
+ resolveAssetUrl?: (assetId: string, url: string) => string;
218
+ /**
219
+ * AGGREGATE download progress across the document's assets: `loaded` bytes summed
220
+ * over all asset downloads, `total` summed over the known Content-Lengths (0 while
221
+ * nothing reported a length). Fired per received chunk — the loader chain adapts it
222
+ * to the ProgressEvent shape the loading UI expects.
223
+ */
224
+ onProgress?: (loaded: number, total: number) => void;
225
+ };
@@ -0,0 +1,43 @@
1
+ /** the version discriminator key every project document must carry */
2
+ export const PROJECT_DOCUMENT_KEY = "needle:project";
3
+ const REMOTE_SRC_SCHEME = "remote://";
4
+ /**
5
+ * Resolve a `remote://name/path` src via the document's remotes table
6
+ * (`overrides` win for known names — the host's per-environment repoint, e.g.
7
+ * the editor's dev proxy). Non-remote srcs pass through. An UNKNOWN remote
8
+ * name resolves to null — the caller reports it; guessing a base would load
9
+ * the wrong bytes silently. MIRRORS the editor's remoteSrc.ts resolveRemoteSrc.
10
+ */
11
+ export function resolveDocumentRemoteSrc(src, remotes, overrides) {
12
+ if (!src.startsWith(REMOTE_SRC_SCHEME))
13
+ return src;
14
+ const rest = src.substring(REMOTE_SRC_SCHEME.length);
15
+ const slash = rest.indexOf("/");
16
+ if (slash <= 0)
17
+ return null;
18
+ const name = rest.substring(0, slash);
19
+ const path = rest.substring(slash); // keeps the leading "/"
20
+ const base = overrides?.[name] ?? remotes?.[name];
21
+ if (base === undefined)
22
+ return null;
23
+ // "" base = host-relative; trim a trailing slash otherwise
24
+ return (base.endsWith("/") ? base.substring(0, base.length - 1) : base) + path;
25
+ }
26
+ /** structural check for the version discriminator — the opener/sniffing entry point */
27
+ export function isProjectDocument(value) {
28
+ return !!value && typeof value === "object" && typeof value[PROJECT_DOCUMENT_KEY] === "string";
29
+ }
30
+ /**
31
+ * Report evaluation issues to the console — in PRODUCTION too. Every issue
32
+ * means the document asked for something the runtime could not provide (a
33
+ * dropped def, an unresolved over, a failed asset), i.e. the loaded scene
34
+ * silently differs from what the author saved. The author debugs on the
35
+ * deployed page: gating this behind dev/?debugproject cost a full day of
36
+ * blind hunting when a deployed scene lost a component with a clean console
37
+ * (2026-09-01). A healthy document reports nothing.
38
+ */
39
+ export function reportProjectIssues(issues, warn = console.warn) {
40
+ for (const issue of issues) {
41
+ warn(`[needle:project] ${issue.arc}: ${issue.message}`);
42
+ }
43
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Structural validation of Needle project documents against the JSON Schema in
3
+ * `schema.json` — the format's machine-readable spec.
4
+ *
5
+ * The schema is the SINGLE source of truth: this file interprets it, it does not
6
+ * restate it. That is the whole point — a hand-written second validator would drift
7
+ * from the schema exactly like the plan's example drifted from the types.
8
+ *
9
+ * Deliberately dependency-free (a JSON Schema library is a lot of surface for the
10
+ * handful of keywords this schema uses), and STRICT about that: any keyword the
11
+ * schema uses and this interpreter does not implement THROWS. A validator that
12
+ * silently ignores keywords it doesn't know is worse than no validator — it reports
13
+ * "valid" for documents it never actually checked. If you extend the schema with a
14
+ * new keyword, this file fails loudly until you implement it.
15
+ *
16
+ * This validates SHAPE, not resolvability: whether an address actually hits a node in
17
+ * the loaded content is the evaluator's job (it reports issues and skips the arc).
18
+ */
19
+ /** a JSON Schema node — `true`/`false` schema forms are not used by our schema */
20
+ type SchemaNode = Record<string, unknown>;
21
+ export type SchemaValidationError = {
22
+ /** "/"-separated path to the offending value, relative to the document root */
23
+ readonly path: string;
24
+ readonly message: string;
25
+ };
26
+ /**
27
+ * Validate `value` against `schema`. Returns every error found (not just the first) —
28
+ * a writer regression usually shows up as several related failures and seeing them
29
+ * together is what makes the cause obvious.
30
+ *
31
+ * @throws if the schema uses a keyword this interpreter does not implement.
32
+ */
33
+ export declare function validateAgainstSchema(schema: SchemaNode, value: unknown): SchemaValidationError[];
34
+ /** one-line-per-error rendering for assertion messages */
35
+ export declare function formatSchemaErrors(errors: readonly SchemaValidationError[]): string;
36
+ export {};