@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,123 @@
1
+ import { resolveDocumentCollectionElement } from "./collectionSelector.js";
2
+ import { joinPath, parseAnchorValue, resolveInCollection, resolveInGraph, splitPath, type GraphNode, type NeedleAddress } from "../index.js";
3
+
4
+ export type ProjectInspectionTarget = { ref: string; address?: NeedleAddress; pathPrefix?: string };
5
+ type Node = GraphNode & { parent?: Node | null; isObject3D?: boolean };
6
+ type Element = { name?: string; userData?: Record<string, unknown>; isMaterial?: boolean; isTexture?: boolean; isAnimationClip?: boolean };
7
+ type Scope = { root: WeakRef<object>; graph?: GraphNode; nodes: Map<GraphNode, WeakRef<object>>; elements: WeakRef<object>[] };
8
+
9
+ /** Reference membership is captured at placement, before parent overs move nodes. */
10
+ export class ProjectDocumentInspection {
11
+ private readonly scopes = new Map<string, Scope>();
12
+ private readonly targets = new WeakMap<object, ProjectInspectionTarget>();
13
+ ready: Promise<void> = Promise.resolve();
14
+
15
+ constructor(private readonly scene: Node) {}
16
+
17
+ registerReference(ref: string, root: object, elements: Iterable<object> = [], referenceRoots?: ReadonlySet<object>): void {
18
+ const nodes = new Map<GraphNode, WeakRef<object>>();
19
+ const scope: Scope = { root: new WeakRef(root), nodes, elements: [] };
20
+ const capture = (node: Node, path: string[]): GraphNode => {
21
+ const weak = new WeakRef(node);
22
+ const graph: GraphNode = {
23
+ get name() { return weak.deref()?.name; },
24
+ get userData() { return weak.deref()?.userData; },
25
+ children: node.children?.filter(child => !referenceRoots?.has(child))
26
+ .map(child => capture(child as Node, [...path, child.name ?? ""])),
27
+ };
28
+ nodes.set(graph, weak);
29
+ const id = node.userData?.["needle-id"];
30
+ const anchor = parseAnchorValue(node.name);
31
+ const address: NeedleAddress | undefined = node === root ? undefined : {
32
+ type: "node", path: joinPath(path), ...(typeof id === "string" ? { id } : {}), ...(anchor ? { anchor } : {}),
33
+ };
34
+ if (node === root || address) this.targets.set(node, { ref, ...(address ? { address } : {}) });
35
+ return graph;
36
+ };
37
+ if ((root as Node).isObject3D) scope.graph = capture(root as Node, []);
38
+ else this.targets.set(root, { ref });
39
+ for (const element of elements) {
40
+ const value = element as Element;
41
+ const type = value.isMaterial ? "material" : value.isTexture ? "texture" : value.isAnimationClip ? "animation" : null;
42
+ if (!type) continue;
43
+ scope.elements.push(new WeakRef(element));
44
+ const id = value.userData?.["needle-id"];
45
+ if (typeof id === "string") this.targets.set(element, { ref, address: { type, id, name: value.name } });
46
+ }
47
+ this.scopes.set(ref, scope);
48
+ }
49
+
50
+ resolve(target: ProjectInspectionTarget): object | null {
51
+ const scope = this.scopes.get(target.ref);
52
+ let object: object | null = target.ref === "$scene" ? this.scene : scope?.root.deref() ?? null;
53
+ if (target.address) {
54
+ const address = target.address;
55
+ if (address.type && address.type !== "node") {
56
+ const elements = scope?.elements.flatMap(weak => {
57
+ const value = weak.deref() as Element | undefined;
58
+ const matches = address.type === "material" ? value?.isMaterial : address.type === "texture" ? value?.isTexture : address.type === "animation" ? value?.isAnimationClip : false;
59
+ return value && matches ? [value] : [];
60
+ }) ?? [];
61
+ object = resolveInCollection(address, elements);
62
+ } else if (scope?.graph) {
63
+ const graph = resolveInGraph(address, scope.graph);
64
+ object = graph ? scope.nodes.get(graph)?.deref() ?? null : null;
65
+ if (object && !this.inScene(object as Node)) object = null;
66
+ } else object = target.ref === "$scene" ? resolveInGraph(address, this.scene) : null;
67
+ }
68
+ if (!object) return null;
69
+ if ((object as Node).isObject3D && !this.inScene(object as Node)) return null;
70
+ if (target.pathPrefix) {
71
+ const parts = splitPath(target.pathPrefix.replace(/\/$/, ""));
72
+ for (const part of parts) {
73
+ if (part === "components") object = (object as Node).userData?.components as object ?? null;
74
+ else if (Array.isArray(object) && part.startsWith("guid:")) {
75
+ const guid = part.slice(5);
76
+ object = resolveDocumentCollectionElement(object, `guid:${guid}`) as object ?? null;
77
+ } else return null;
78
+ if (!object) return null;
79
+ }
80
+ }
81
+ return object;
82
+ }
83
+
84
+ targetFor(object: object): ProjectInspectionTarget | null {
85
+ if (object === this.scene) return { ref: "$scene" };
86
+ const component = object as { gameObject?: object; needle_editor_guid?: string; guid?: string };
87
+ const guid = component.needle_editor_guid || component.guid;
88
+ if (component.gameObject && guid) {
89
+ const host = this.targetFor(component.gameObject);
90
+ return host ? { ...host, pathPrefix: `components/guid:${guid}/` } : null;
91
+ }
92
+ const target = this.targets.get(object);
93
+ return target && this.resolve(target) === object ? structuredClone(target) : null;
94
+ }
95
+
96
+ private inScene(object: Node): boolean {
97
+ // Liveness only; reference ownership comes from the placement scope above.
98
+ for (let node: Node | null | undefined = object; node; node = node.parent) if (node === this.scene) return true;
99
+ return false;
100
+ }
101
+ }
102
+
103
+ const inspections = new WeakMap<object, ProjectDocumentInspection>();
104
+ let observers = 0;
105
+
106
+ /** Opt in before loading a document; ordinary playback allocates no inspection index. */
107
+ export function enableProjectDocumentInspection(): { dispose(): void } {
108
+ observers++;
109
+ let disposed = false;
110
+ return { dispose() { if (!disposed) { disposed = true; observers--; } } };
111
+ }
112
+
113
+ export function createProjectDocumentInspection(scene: Node): ProjectDocumentInspection | undefined {
114
+ return observers ? new ProjectDocumentInspection(scene) : undefined;
115
+ }
116
+
117
+ export function getProjectDocumentInspection(context: object): ProjectDocumentInspection | undefined {
118
+ return inspections.get(context);
119
+ }
120
+
121
+ export function registerProjectDocumentInspection(context: object, inspection: ProjectDocumentInspection): void {
122
+ inspections.set(context, inspection);
123
+ }
@@ -0,0 +1,66 @@
1
+ import type { GraphNode } from "../address.js";
2
+ import type { GltfAssociation } from "../loadedGltf.js";
3
+ import type { ProjectEvaluationIssue } from "./types.js";
4
+
5
+ export interface DocumentVector {
6
+ fromArray(value: number[]): unknown;
7
+ toArray(): number[];
8
+ }
9
+
10
+ /** The scene operations used by composition, independent of a three.js build. */
11
+ export interface DocumentNode extends GraphNode {
12
+ name: string;
13
+ isObject3D: boolean;
14
+ userData: Record<string, unknown>;
15
+ parent: DocumentNode | null;
16
+ children: DocumentNode[];
17
+ position: DocumentVector;
18
+ quaternion: DocumentVector;
19
+ scale: DocumentVector;
20
+ add(...nodes: DocumentNode[]): unknown;
21
+ removeFromParent(): unknown;
22
+ traverse(visitor: (node: DocumentNode) => void): void;
23
+ }
24
+
25
+ export interface DocumentModel {
26
+ scene?: DocumentNode;
27
+ scenes?: DocumentNode[];
28
+ animations?: object[];
29
+ parser?: {
30
+ json?: Record<string, unknown> & { materials?: Array<{ name?: string }> };
31
+ associations?: Map<object, GltfAssociation>;
32
+ getDependency?(type: string, index: number): Promise<unknown>;
33
+ };
34
+ }
35
+
36
+ export type DocumentIssueReporter = (arc: ProjectEvaluationIssue["arc"], message: string) => void;
37
+
38
+ export interface DocumentDefContext {
39
+ scene?: DocumentNode;
40
+ createNode(): DocumentNode;
41
+ applyComponent?(node: DocumentNode, data: { name: string } & Record<string, unknown>, issue: (arc: "def", message: string) => void): void;
42
+ }
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?: { resolveUrl(url: string): string }): Promise<DocumentModel | null>;
51
+ instantiate(template: DocumentNode, referenceId: string): DocumentNode | null;
52
+ parkTemplate(node: DocumentNode): void;
53
+ activateInstance(node: DocumentNode): void;
54
+ disposeTemplate(node: DocumentNode): void;
55
+ /** Run after template deserialization, before any instance activates its components. */
56
+ schedule(work: () => void): void;
57
+ loadEnvironment(url: string, options: { format?: "pmrem"; name?: string }): Promise<object | null>;
58
+ createDefContext?(root: DocumentNode, baseUrl?: string): DocumentDefContext;
59
+ getComponents?(node: DocumentNode): object[];
60
+ matchesComponentType?(component: object, name: string): boolean;
61
+ matchesComponentGuid?(component: object, guid: string): boolean;
62
+ deriveComponentGuid?(referenceId: string, fileGuid: string): string;
63
+ removeComponent?(component: object, owner: DocumentNode | null): void;
64
+ /** Identity used by the optional inspection registry; defaults to the runtime itself. */
65
+ inspectionOwner?: object;
66
+ }
@@ -0,0 +1,263 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://needle.tools/schemas/project-document-0.1.json",
4
+ "title": "Needle project document",
5
+ "description": "Composition-only document: it references glTF assets, places them (references), adds components (defs) and patches properties (overs). Content itself always lives in glTF — the document owns zero content. Spec: docs/project-format.md. Types: modules/needle-engine/js/package~/src/engine/project-document/engine_project_document.ts.",
6
+ "type": "object",
7
+ "required": ["needle:project"],
8
+ "properties": {
9
+ "needle:project": {
10
+ "type": "string",
11
+ "minLength": 1,
12
+ "description": "Format version discriminator. Its presence IS the document sniff (isProjectDocument / parseSync). Current: \"0.1\"."
13
+ },
14
+ "metadata": {
15
+ "type": "object",
16
+ "description": "Layer metadata (USD layer-metadata analog). App-level, IGNORED by the runtime evaluator, preserved byte-faithfully on rewrite. Open set; known keys: name, description, image. RETIRED (2026-08-06): editorCamera and selection are per-user view state in the editor's local storage — readers may still find them in old documents (fallback-read), writers must not emit them."
17
+ },
18
+ "assets": {
19
+ "type": "object",
20
+ "description": "The loadable-resource POOL — the \"file\" half of every (file, id) address. An asset may exist here WITHOUT a reference (e.g. loaded only for a material an over binds).",
21
+ "additionalProperties": { "$ref": "#/definitions/asset" }
22
+ },
23
+ "remotes": {
24
+ "type": "object",
25
+ "description": "Named remote origins for `remote://<name>/<path>` srcs (the vscode filesystem model: the scheme routes, the AUTHORITY names the remote). Maps remote name → base url; consumers resolve srcs against it and MAY override known names per environment (the dev editor maps \"needle-cloud\" to its host-relative proxy). Repointing a project — dev↔prod, migrated storage — is one entry here, never a src rewrite.",
26
+ "additionalProperties": { "type": "string" }
27
+ },
28
+ "references": {
29
+ "type": "array",
30
+ "description": "USD reference arcs. Without an `address` a reference PLACES the asset's scene subtree; WITH an `address` it is an ENTITY reference instantiated into the composition's entity pool and never placed.",
31
+ "items": { "$ref": "#/definitions/reference" }
32
+ },
33
+ "bindings": {
34
+ "type": "array",
35
+ "description": "Host-owned roots required by this document. The host matches every binding before applying any edit and supplies the resulting objects to the evaluator.",
36
+ "items": { "$ref": "#/definitions/binding" }
37
+ },
38
+ "defs": {
39
+ "type": "array",
40
+ "description": "Add components to nodes, and (with `create`) DEFINE new nodes.",
41
+ "items": { "$ref": "#/definitions/def" }
42
+ },
43
+ "overs": {
44
+ "type": "array",
45
+ "description": "Property patches on existing content. Applied in document order — later entries win on the same target.",
46
+ "items": { "$ref": "#/definitions/over" }
47
+ },
48
+ "meta": {
49
+ "not": {},
50
+ "description": "RETIRED — renamed to `metadata` (clean break, no compat shim). Present only to fail loudly if a writer regresses."
51
+ }
52
+ },
53
+ "additionalProperties": true,
54
+
55
+ "definitions": {
56
+ "binding": {
57
+ "type": "object",
58
+ "required": ["id", "kind", "signature"],
59
+ "properties": {
60
+ "id": { "type": "string", "minLength": 1, "description": "Document-local reference used by targets. `$`-prefixed ids are reserved." },
61
+ "kind": { "const": "scene" },
62
+ "signature": { "type": "string", "minLength": 1, "description": "Stable structural identity used by the host to match this root before applying edits." },
63
+ "name": { "type": "string", "description": "Informational label for diagnostics only; never used as identity." },
64
+ "references": { "type": "array", "items": { "type": "string", "minLength": 1 } }
65
+ },
66
+ "additionalProperties": true
67
+ },
68
+ "asset": {
69
+ "type": "object",
70
+ "required": ["src"],
71
+ "properties": {
72
+ "src": {
73
+ "type": "string",
74
+ "minLength": 1,
75
+ "description": "Where the asset lives. Relative srcs resolve against the document (project folder); http(s) srcs stay absolute; `remote://<name>/<path>` srcs resolve via the document's `remotes` table (readers resolve BEFORE evaluation — the runtime never sees the scheme)."
76
+ },
77
+ "guid": {
78
+ "type": "string",
79
+ "description": "The file's STABLE identity (plans/file-identity-and-text-editing.md) — a client-minted uuid homed in the project's file registry (cloud: the revision manifest's guid column; local: .needle/files.json). Present for FOLDER FILES only; `src` is pure location, so a move/rename updates src while every reference keeps meaning this file. Served urls have no folder identity and omit it."
80
+ },
81
+ "asset": {
82
+ "type": "object",
83
+ "required": ["id"],
84
+ "description": "Provider IDENTITY for assets whose resources (textures, buffers) live at provider-specific urls `src` alone cannot reach (e.g. PolyHaven). The HOST recovers the provider's resolver from this at load time; the engine evaluator ignores it. Self-contained assets omit it.",
85
+ "properties": {
86
+ "id": { "type": "string", "minLength": 1 },
87
+ "provider": {
88
+ "type": "string",
89
+ "description": "Provider key hint — lets a cold session route straight to the owning provider instead of probing."
90
+ }
91
+ },
92
+ "additionalProperties": true
93
+ },
94
+ "kind": {
95
+ "type": "string",
96
+ "enum": ["model", "material", "hdr", "texture", "image", "video", "audio", "code", "text", "document", "archive"],
97
+ "description": "Semantic KIND, decided at USE time by the writer — the same file can be a texture on a material slot or a plain image in UI, and downstream processing (compression at publish) follows the use, not the extension. The engine evaluator ignores it."
98
+ },
99
+ "etag": {
100
+ "type": "string",
101
+ "description": "INFORMATIONAL: the server ETag observed when the document captured the asset — lets a future reader detect that the upstream file changed since. Never used for loading decisions."
102
+ }
103
+ },
104
+ "additionalProperties": true
105
+ },
106
+
107
+ "reference": {
108
+ "type": "object",
109
+ "required": ["id", "asset"],
110
+ "properties": {
111
+ "id": {
112
+ "type": "string",
113
+ "minLength": 1,
114
+ "description": "Project-local id — the resolution SCOPE for every target naming it, and the entity handle for `{ ref }` values. Must be unique (duplicates are reported and skipped). Also the seed for the placed copy's stable guids. `$`-prefixed ids are RESERVED (see $scene)."
115
+ },
116
+ "asset": {
117
+ "type": "string",
118
+ "minLength": 1,
119
+ "description": "Key into the document's `assets` map."
120
+ },
121
+ "address": {
122
+ "$ref": "#/definitions/address",
123
+ "description": "ENTITY-reference discriminator. When present, the addressed element INSIDE the asset is instantiated into the entity pool (never placed). An address of only a `type` means the asset ITSELF as that element kind (e.g. { type: \"hdr\" })."
124
+ },
125
+ "parent": {
126
+ "not": {},
127
+ "description": "RETIRED (2026-08-03, days after shipping) — structure is PURE OVERS: `{ target: { ref, path: \"parent\" }, value: { ref | \"$scene\", address?, index? } }`. One rail for mounts, reparenting and sibling order. Present only to fail loudly if a writer regresses."
128
+ },
129
+ "transform": {
130
+ "not": {},
131
+ "description": "RETIRED — placement is PURE OVERS (`{ ref, path: \"position\" }` etc.) so there is no precedence ambiguity between a transform field and a position over. Present only to fail loudly if a writer regresses."
132
+ }
133
+ },
134
+ "additionalProperties": true
135
+ },
136
+
137
+ "def": {
138
+ "type": "object",
139
+ "required": ["target"],
140
+ "anyOf": [
141
+ { "required": ["create"] },
142
+ { "required": ["components"] }
143
+ ],
144
+ "properties": {
145
+ "target": { "$ref": "#/definitions/target" },
146
+ "create": {
147
+ "type": "object",
148
+ "description": "The def DEFINES a node under the resolved target (composition root when the target is empty) and the components apply to IT. ADOPT semantics: when a child of that name already exists, the def COMPOSES onto it (transform applied, component data deserialized onto a matching existing component) — re-evaluation is idempotent, no twins.",
149
+ "properties": {
150
+ "id": { "type": "string", "minLength": 1, "description": "Stable needle-id assigned to the created node." },
151
+ "name": { "type": "string" },
152
+ "position": { "type": "array", "items": { "type": "number" }, "minItems": 3 },
153
+ "quaternion": { "type": "array", "items": { "type": "number" }, "minItems": 4 },
154
+ "scale": { "type": "array", "items": { "type": "number" }, "minItems": 3 },
155
+ "copy": {
156
+ "type": "object",
157
+ "description": "Recreate a hierarchy duplicate from an addressed source. The seed reproduces the hierarchy identities. Entries sharing batch are cloned together in index order.",
158
+ "required": ["source", "seed"],
159
+ "anyOf": [
160
+ { "required": ["batch", "index"] },
161
+ {
162
+ "allOf": [
163
+ { "not": { "required": ["batch"] } },
164
+ { "not": { "required": ["index"] } }
165
+ ]
166
+ }
167
+ ],
168
+ "properties": {
169
+ "source": { "$ref": "#/definitions/target" },
170
+ "seed": { "type": "string", "minLength": 1 },
171
+ "reference": { "type": "boolean" },
172
+ "batch": { "type": "string", "minLength": 1 },
173
+ "index": { "type": "integer" }
174
+ },
175
+ "additionalProperties": true
176
+ }
177
+ },
178
+ "additionalProperties": true
179
+ },
180
+ "components": {
181
+ "type": "array",
182
+ "description": "VERBATIM NEEDLE_components wire entries — no parallel vocabulary. Unknown component types are reported and skipped.",
183
+ "items": {
184
+ "type": "object",
185
+ "required": ["name"],
186
+ "properties": {
187
+ "name": {
188
+ "type": "string",
189
+ "minLength": 1,
190
+ "description": "TypeStore name. NOTE: runtime constructor names are decorator-wrapped (\"_DragControls\") — match by guid, not by ctor name."
191
+ },
192
+ "guid": { "type": "string" }
193
+ },
194
+ "additionalProperties": true
195
+ }
196
+ }
197
+ },
198
+ "additionalProperties": true
199
+ },
200
+
201
+ "over": {
202
+ "type": "object",
203
+ "required": ["target", "value"],
204
+ "properties": {
205
+ "target": { "$ref": "#/definitions/overTarget" },
206
+ "value": {
207
+ "description": "Primitive (including explicit null — on $scene slots like environment/background null means CLEAR; absence of the over means the boot default), array (three value types are patched in place via fromArray), or an entity-ref `{ ref }`. Other reference-valued forms ({ node }, { guid }, { asset, address }) are reported and SKIPPED, never raw-assigned."
208
+ }
209
+ },
210
+ "additionalProperties": true
211
+ },
212
+
213
+ "target": {
214
+ "type": "object",
215
+ "description": "Where an arc applies. `ref` selects the resolution scope; `address` locates an element inside it; `component` selects a component on the resolved node.",
216
+ "properties": {
217
+ "ref": {
218
+ "type": "string",
219
+ "description": "Reference id the address resolves WITHIN. Omitted → the composition root. Reserved: \"$scene\" = the context scene (scene-level opinions such as background/environment, or — with an address — a node inside the scene); \"$renderer\" = the context renderer (toneMapping, toneMappingExposure — no address allowed)."
220
+ },
221
+ "address": { "$ref": "#/definitions/address" },
222
+ "component": {
223
+ "not": {},
224
+ "description": "RETIRED — a component is reached through the PATH now (\"components/guid:<guid>/<field>\"), not a dedicated selector. Present only to fail loudly if a writer regresses."
225
+ },
226
+ "path": {
227
+ "type": "string",
228
+ "minLength": 1,
229
+ "description": "\"/\"-separated property path within the resolved element (e.g. \"material/map/repeat\"). A segment landing on a COLLECTION may be a \"<field>:<value>\" qualifier instead of an index — \"components/type:DragControls/enabled\", \"components/guid:6f1f…/enabled\" — so a document never depends on array order. A bare numeric segment on an array is still an index. \"components\" is a synthesized segment on a node (the live userData layout stays private). Escaping follows RFC 6901 (\"~1\" = \"/\", \"~0\" = \"~\"). Reserved LAST segment: \"active\" (value false removes whatever the rest of the path resolved to — a node or a component — from the composed result; USD deactivation, referenced content is never deleted)."
230
+ }
231
+ },
232
+ "additionalProperties": true
233
+ },
234
+
235
+ "overTarget": {
236
+ "allOf": [
237
+ { "$ref": "#/definitions/target" },
238
+ {
239
+ "type": "object",
240
+ "required": ["path"],
241
+ "description": "An over without a target path is reported and skipped by the evaluator."
242
+ }
243
+ ]
244
+ },
245
+
246
+ "address": {
247
+ "type": "object",
248
+ "description": "Fat address (@needle-tools/addressing). Resolution ladder: id → anchor → path. Recording several identifiers is the point — content changes underneath live edits.",
249
+ "properties": {
250
+ "type": {
251
+ "type": "string",
252
+ "description": "Element kind, e.g. \"node\" | \"material\" | \"mesh\" | \"hdr\". Materials resolve against the scope's meshes (id → name), not the graph."
253
+ },
254
+ "id": { "type": "string", "description": "needle-id — stamped in the asset, or DERIVED (deriving == stamping: the same (type, name, occurrence) rule on both sides, so capture-time derivation and load-time stamping always agree)." },
255
+ "name": { "type": "string" },
256
+ "anchor": { "type": "string" },
257
+ "path": { "type": "string", "description": "Node names from the asset root (exclusive) to the target (inclusive). Slash-separated segments use JSON-Pointer name escaping (~0 = ~, ~1 = /). An optional ~2n suffix selects the zero-based occurrence among same-named siblings; unqualified ambiguous segments do not resolve. Empty segments address unnamed children. An empty string is a valid one-segment path; absence means no path rung." },
258
+ "index": { "type": "integer", "description": "Weakest, reorder-fragile positional fallback." }
259
+ },
260
+ "additionalProperties": true
261
+ }
262
+ }
263
+ }