@tileflow/core 0.1.0-alpha.3 → 0.1.0-alpha.30

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 (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +270 -26
  3. package/THIRD_PARTY_NOTICES.md +327 -0
  4. package/dist/browser.d.ts +334 -0
  5. package/dist/browser.js +5694 -0
  6. package/dist/build.d.ts +574 -0
  7. package/dist/build.js +25405 -0
  8. package/dist/capture-scene-D6soWztE.d.ts +161 -0
  9. package/dist/capture.d.ts +9 -0
  10. package/dist/capture.js +276 -0
  11. package/dist/config-reference.json +32261 -0
  12. package/dist/icon-composition-BiOTwrvD.d.ts +1331 -0
  13. package/dist/index.d.ts +1112 -162
  14. package/dist/index.js +18690 -3479
  15. package/dist/manifest-types-CTT2eLbp.d.ts +790 -0
  16. package/dist/manifest.d.ts +9 -0
  17. package/dist/manifest.js +368 -0
  18. package/dist/native-profile.d.ts +188 -0
  19. package/dist/native-profile.js +9316 -0
  20. package/dist/native.d.ts +222 -0
  21. package/dist/native.js +4862 -0
  22. package/dist/runtime.d.ts +1 -0
  23. package/dist/runtime.js +1317 -0
  24. package/docs/authoring-model.md +226 -0
  25. package/docs/capture-scenes.md +36 -0
  26. package/docs/compiled-style-performance.md +263 -0
  27. package/docs/data-is-separate-from-design.md +345 -0
  28. package/docs/globe-atmosphere.md +83 -0
  29. package/docs/hosted-session-authorization.md +108 -0
  30. package/docs/map-and-terrain-examples.md +149 -0
  31. package/docs/native-artifact-profile.md +145 -0
  32. package/docs/native-resource-urls.md +367 -0
  33. package/docs/public-api-and-browser-subpath.md +93 -0
  34. package/docs/shared-visual-primitives.md +81 -0
  35. package/docs/theme-transitions-and-blends.md +165 -0
  36. package/docs/themes-and-module-styles.md +83 -0
  37. package/package.json +58 -8
@@ -0,0 +1,149 @@
1
+ # Map and terrain examples
2
+
3
+ Start with the [@tileflow/core guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/README.md) for installation and a complete first example.
4
+
5
+ Typed Tileflow map contracts, semantic modules, and deterministic MapLibre style compilation.
6
+
7
+ Every public `tileflow.config.ts` exports one map. Most maps import an existing map and override only
8
+ the design fields they own:
9
+
10
+ ```ts
11
+ import {
12
+ defineMap,
13
+ defineTheme,
14
+ fixed,
15
+ labels,
16
+ poi,
17
+ roads,
18
+ token,
19
+ water,
20
+ zoom,
21
+ } from '@tileflow/core';
22
+ import {streets, streetsThemes} from '@tileflow/maps';
23
+
24
+ const madridDark = defineTheme(streetsThemes.dark, {
25
+ id: 'madrid-dark',
26
+ version: 1,
27
+ colorScheme: 'dark',
28
+ tokens: {color: {'surface.land': '#0d1320', 'surface.water': '#081e2e'}},
29
+ });
30
+
31
+ export default defineMap({
32
+ id: 'madrid',
33
+ name: 'Madrid',
34
+ version: 1,
35
+ extends: streets,
36
+ themes: {light: streetsThemes.light, dark: madridDark},
37
+ defaultTheme: 'light',
38
+ systemThemes: {light: 'light', dark: 'dark'},
39
+ projection: 'globe',
40
+ modules: {
41
+ water: water({
42
+ bodies: {
43
+ fill: {
44
+ opacity: fixed(0.95, {reason: 'Madrid keeps this water density in every theme'}),
45
+ },
46
+ },
47
+ }),
48
+ roads: roads({
49
+ detail: 'streets',
50
+ hierarchy: 'strong',
51
+ classes: {
52
+ primary: {
53
+ surface: {
54
+ fill: {
55
+ color: fixed('#E4A85B', {reason: 'Madrid brand roads stay warm in every theme'}),
56
+ width: zoom.linear([
57
+ [7, fixed(0.6, {reason: 'Madrid keeps its primary-road hierarchy across themes'})],
58
+ [16, fixed(8, {reason: 'Madrid keeps its primary-road hierarchy across themes'})],
59
+ ]),
60
+ },
61
+ },
62
+ },
63
+ secondary: {surface: {fill: {color: token.color('roads.secondary')}}},
64
+ },
65
+ }),
66
+ labels: labels({
67
+ language: 'local',
68
+ places: 'all',
69
+ roads: 'streets',
70
+ styles: {
71
+ places: {
72
+ city: {
73
+ text: {
74
+ size: fixed(18, {reason: 'Madrid keeps its city-label scale across themes'}),
75
+ haloWidth: fixed(1.5, {reason: 'Madrid keeps its city-label halo across themes'}),
76
+ },
77
+ },
78
+ },
79
+ },
80
+ }),
81
+ poi: poi({categories: ['food-drink', 'arts-entertainment', 'transport'], color: 'category'}),
82
+ },
83
+ view: {center: [-3.7038, 40.4168], pitch: 35, zoom: 12},
84
+ });
85
+ ```
86
+
87
+ Map identity and scenes belong to the leaf and do not inherit. Hosted browser policy is configured
88
+ in Tileflow and does not enter cartographic authoring or compilation.
89
+
90
+ Set `projection: 'globe'` for MapLibre's adaptive globe preset. Global zooms render as a sphere,
91
+ then transition to Mercator between zoom 10 and 12 so detailed streets remain planar. Omit the
92
+ property, or set it to `'mercator'`, for a consistently flat map.
93
+
94
+ Terrain keeps the original `'none' | 'hillshade' | '3d'` shorthand. The object form can style every
95
+ MapLibre hillshade paint control, or generate vector contours in the browser from an explicit DEM
96
+ tile template. Contours require `demUrl`, `demMaxZoom`, and zoom-indexed `[minor, index]`
97
+ `thresholds`; the raster `url` remains the separate TileJSON endpoint. Set `mode: 'none'` to compile
98
+ contours without adding a raster source, hillshade, or 3D terrain:
99
+
100
+ ```ts
101
+ terrain: {
102
+ mode: 'none',
103
+ encoding: 'terrarium',
104
+ contours: {
105
+ demUrl: 'https://terrain.example.test/{z}/{x}/{y}.webp',
106
+ demMaxZoom: 13,
107
+ maxZoom: 15,
108
+ overzoom: 2,
109
+ thresholds: {
110
+ 9: [100, 500],
111
+ 11: [50, 250],
112
+ 13: [20, 100],
113
+ 15: [10, 50],
114
+ },
115
+ minor: {
116
+ color: token.color('terrain.contour.minor'),
117
+ width: token.number('terrain.contour.minor-width'),
118
+ },
119
+ index: {
120
+ color: token.color('terrain.contour.index'),
121
+ width: token.number('terrain.contour.index-width'),
122
+ },
123
+ labels: {
124
+ color: token.color('terrain.contour.label'),
125
+ haloColor: token.color('terrain.contour.halo'),
126
+ },
127
+ },
128
+ }
129
+ ```
130
+
131
+ Declare those `terrain.*` color and number roles with the same keys in every map theme. The
132
+ compiled source is ordinary MapLibre vector data with source layer `contours`; its tile URL
133
+ contains the safely encoded DEM template and complete generation parameters. Register the generic
134
+ protocol before MapLibre reads the style. It lazily initializes the pinned, locally bundled
135
+ `maplibre-contour@0.1.0` browser module on the first contour tile; no public CDN runtime is needed.
136
+ `overzoom` may not exceed the lowest threshold zoom, so generated DEM requests never use a negative
137
+ zoom. Every index interval must be a whole multiple of its minor interval. Contour labels emit the
138
+ scaled numeric elevation without assuming a display unit. Source and layer visibility cannot begin
139
+ before the first configured threshold zoom. To bound main-thread work, effective minor intervals
140
+ must be at least 250 units at z0–4, 100 at z5–7, 50 at z8–10, 20 at z11–12, and 10 from z13 onward.
141
+ Use a trusted, size-bounded DEM tile service; the protocol intentionally accepts author-supplied
142
+ HTTP(S) templates rather than proxying or republishing terrain data.
143
+
144
+ ```ts
145
+ import maplibregl from 'maplibre-gl';
146
+ import {registerTileflowContourProtocol} from '@tileflow/core/browser';
147
+
148
+ registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
149
+ ```
@@ -0,0 +1,145 @@
1
+ # Validate native artifacts
2
+
3
+ `@tileflow/core/native-profile` checks a resolved MapLibre style against the static `native-v1`
4
+ artifact profile. It is a build-time entry point, separate from the dependency-free URL helpers in
5
+ `@tileflow/core/native`. Install Core as described in the [package README](../README.md).
6
+
7
+ This is not a mobile SDK. It does not fetch a manifest, execute a renderer, start a commercial
8
+ session, or qualify Hermes, iOS or Android. A passing result establishes static artifact checks,
9
+ not native pixels, data availability, Hosted access, publication or mobile product availability.
10
+
11
+ ## Check a style
12
+
13
+ <!-- docs:check -->
14
+
15
+ ```ts
16
+ import {validateTileflowNativeStyle} from '@tileflow/core/native-profile';
17
+
18
+ const diagnostics = validateTileflowNativeStyle({
19
+ version: 8,
20
+ sources: {},
21
+ layers: [{id: 'background', type: 'background', paint: {'background-color': '#ffffff'}}],
22
+ });
23
+
24
+ console.log(diagnostics);
25
+ ```
26
+
27
+ The result is an empty array for a style that passes this profile, or bounded structured errors.
28
+ `assertTileflowNativeStyle()` throws `TileflowNativeCompatibilityError` for the same errors.
29
+ Neither function modifies the input. Both enforce the raw input budget, even when given a style
30
+ that was already prepared. Relative resources require the explicit `documentUrl` option
31
+ and resolve against that declaring style; it is not inferred from a browser or Metro address.
32
+
33
+ Use the [Dev artifact pipeline](https://github.com/tileflow/tileflow-sdk/blob/main/packages/dev/docs/native-artifacts.md)
34
+ for authored maps. It resolves inheritance and semantic modules once, prepares icons and fonts,
35
+ checks every theme and returns a complete generation. Do not call a style validator instead of
36
+ preparing the referenced resources.
37
+
38
+ ## Profile and metadata
39
+
40
+ The exported `tileflowNativeProfile` targets MapLibre React Native 11.3.10 with Native Android 13.2.0
41
+ and iOS 6.26.0. These are static compatibility targets, not a supported application dependency matrix.
42
+ The wrapper's Style Spec version is 26.2.1. Tileflow retains its existing compiler and parser at
43
+ 24.8.5: `native-v1` checks that compiler's vocabulary and the intersection of its recorded Android
44
+ and iOS support. It does not expose every feature of Style Spec 26.2.1 or upgrade the web compiler.
45
+
46
+ The checker reads the full `sdk-support` and expression metadata from the pinned Style Spec
47
+ package's `dist/latest.json`. Its minified JavaScript exports omit that information. A missing
48
+ expression-support entry, issue link or required native version newer than either selected engine
49
+ is not evidence of support. The metadata source is the
50
+ [pinned specification](https://github.com/maplibre/maplibre-style-spec/blob/v24.8.5/src/reference/v8.json).
51
+
52
+ `tileflowNativeProfileSchema`, `tileflowNativeDiagnosticSchema`, `tileflowRendererSchema` and
53
+ `tileflowNativeBuildRecordSchema` are strict canonical Zod schemas. The map authoring grammar and
54
+ its generated language JSON are unchanged: renderer selection belongs to build tooling, not maps.
55
+
56
+ ## Included representations and explicit rejections
57
+
58
+ The profile admits fixed Mercator styles, standard vector/raster/GeoJSON/image sources, compatible
59
+ hillshade and building extrusions, one effective sprite, glyph URLs and complete prepared TTF/OTF
60
+ faces. Paint, layout and expression values must also pass the pinned semantic parser and native
61
+ support metadata. Hillshade and building extrusions do not establish 3D terrain support.
62
+
63
+ The raw style validator rejects globe or adaptive projection, terrain displacement, sky and global-state extensions,
64
+ unsupported source/layer/property values, legacy stop-function objects, browser contour and PMTiles protocols, local file URLs,
65
+ multiple sprite providers. The existing portable camera/view bounds are preserved; an artifact
66
+ does not configure a native map instance's camera constraints. It does not silently remove
67
+ an unsupported feature or replace a font. An explicit fixed-Mercator property can be omitted in the
68
+ native representation because Native has no GL JS projection controller; this does not change the
69
+ logical map revision.
70
+
71
+ Dev's `native-lowering-v1` preparation policy explicitly converts a fixed `{type: 'globe'}`
72
+ projection to Native's implicit Mercator before this validator runs. This is a recorded projection
73
+ change, not globe support or visual equivalence. One authored map serves web and native preparation;
74
+ no native-specific Streets definition is used. Preparation adapts only finite `line-cap` and
75
+ `line-dasharray` decisions into constant-property layers. Other unsupported properties have no implicit fallback. The raw validator continues to reject
76
+ unlowered unsupported expressions. See the [preparation contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/dev/docs/native-artifacts.md)
77
+ for the grammar, expansion limits and pending native visual checks. The lowering checks establish
78
+ per-feature selection and cap/dash values, not global feature draw order. A copied `line-sort-key`
79
+ cannot preserve ordering between simultaneous physical branches; overlapping features may produce
80
+ a different composition even when every property value is retained.
81
+
82
+ `native-build.json` uses strict schema version 2 and `preparationVersion: 'native-lowering-v1'`.
83
+ Its `transformations` array is ordered by map/theme and binds the input/lowered style hashes,
84
+ projection policy and physical layer spans. The runtime manifest remains version 1. An old v1
85
+ build-record decoder must reject v2 rather than silently ignore the preparation evidence.
86
+
87
+ Text must use a valid glyph template containing `{fontstack}` and `{range}`, or a complete static
88
+ font stack backed by prepared faces. Native preparation maps each exact OpenType full name to its
89
+ verified TTF/OTF URL. WOFF2 conversion, Unicode-range split providers, dynamic font-stack closure and
90
+ implicit system-font fallback are outside this artifact profile. A fallback name without a prepared
91
+ face fails instead of being silently substituted. The existing font pipeline still owns parsing,
92
+ font selection, content hashes and required licenses.
93
+
94
+ The `deferFontClosure` option is only for compiler preflight before font preparation. It deliberately
95
+ does not prove text-provider closure. The Dev pipeline always performs the final full validation
96
+ after preparation; external callers must not present a preflight result as a complete artifact check.
97
+
98
+ ## Input and prepared-style budgets
99
+
100
+ `tileflowNativeProfileLimits` and `validateTileflowNativeStyle()` keep untrusted compiler input at
101
+ **160,000 visited JSON nodes**. Dev applies this gate before lowering. The raw validator has no
102
+ option to borrow the output allowance, and preparation never retries rejected input with a larger
103
+ budget. Exceeding the input bound remains `NATIVE_UNSUPPORTED_STYLE` at the root style pointer.
104
+
105
+ `tileflowNativePreparedStyleLimits` and `validateTileflowNativePreparedStyle()` use a separate fixed
106
+ ceiling of **540,000 nodes** for the expanded representation. The latter performs the same semantic,
107
+ source, font and URL checks; it does not lower a style, relax unsupported properties or establish
108
+ that the input came from a trusted build. Use it to check prepared artifact JSON, not as the entry
109
+ gate for authoring. Dev uses it after lowering, after font preparation and after generation URL
110
+ retargeting, before writing the output generation.
111
+
112
+ Both stages use the same traversal. Each object key or array index and each value counts as a
113
+ node. Both retain **8 MiB of serialized UTF-8, depth 64, 128 sources, 4,096 layers, 16 prepared font
114
+ faces and 32 returned errors**. Cycles, getters, sparse arrays, non-JSON values and `toJSON` hooks
115
+ remain invalid. The prepared budget changes only the node allowance; per-decision and per-layer
116
+ expansion limits also remain mandatory.
117
+
118
+ The measured full Streets family expands from 122,253 to 513,321 nodes for dark and from 122,163 to
119
+ 513,231 for light. The 540,000-node ceiling leaves 26,679/26,769 nodes (about 5.2%) of headroom rather
120
+ than multiplying the raw allowance arbitrarily. This accommodates the bounded prepared
121
+ representation, not unbounded growth of custom styles. Prepared font bytes retain the existing
122
+ 1 MiB per-face bound; URL, icon and sprite limits retain their owning pipelines' bounds. These are
123
+ validation-work limits, not download, GPU-memory or billing limits.
124
+
125
+ Allowing the larger prepared representation increases the maximum traversal and parser workload.
126
+ The byte, depth, layer and decision bounds still apply; there is no caller-selected unlimited
127
+ mode. The node traversal stops at its ceiling before JSON serialization and semantic parsing.
128
+ These finite limits are not a latency guarantee for every machine or a substitute for admission
129
+ controls in an application accepting arbitrary artifacts.
130
+
131
+ ## Read diagnostics
132
+
133
+ Every native diagnostic has `code`, `phase`, `severity`, `path`, `renderer`, `profile`, `message`
134
+ and `suggestion`. `path` is a JSON Pointer into the style; artifact preparation prefixes it with the
135
+ map and theme. Error messages do not include rejected URLs, parser details or credentials. Unsafe
136
+ object-key text is represented by its ancestor pointer.
137
+
138
+ - `NATIVE_UNSUPPORTED_STYLE`: incompatible style semantics or artifact shape.
139
+ - `NATIVE_UNSUPPORTED_SOURCE`: incompatible source, URL or browser protocol.
140
+ - `NATIVE_FONT_UNAVAILABLE`: missing or incompatible native text-provider closure.
141
+ - `NATIVE_MANIFEST_REQUIRED`: no declaring document URL for relative resources.
142
+ - `NATIVE_RENDERER_UNSUPPORTED`: unavailable renderer/profile or native delivery target.
143
+
144
+ Dev sessions and CLI commands retain their existing diagnostic envelopes and add the native
145
+ `renderer`/`profile` context. Unchanged web calls do not gain extra diagnostic fields.
@@ -0,0 +1,367 @@
1
+ # Native resource URLs, sources and initial views
2
+
3
+ `@tileflow/core/native` provides URL helpers, source coordination and renderer-neutral initial
4
+ views for clients without a browser origin. Its published entry bundles a private WHATWG parser
5
+ with IDNA processing. It does not use or replace the ambient `URL`, `TextEncoder` or `TextDecoder`,
6
+ and has no renderer dependency.
7
+
8
+ The synchronous URL helpers resolve references only. Tileflow manifest sources use an injected
9
+ bounded acquisition adapter. None of these APIs renders maps or authorizes Hosted requests.
10
+ A successful resolution is not a claim that a style or feature works on a particular native renderer.
11
+ Install `@tileflow/core@alpha` as described in the [package README](../README.md); the examples use
12
+ only this package's public exports.
13
+
14
+ ## Resolve the manifest explicitly
15
+
16
+ A native client must provide the complete manifest URL. There is no implicit
17
+ `/tileflow/manifest.json`, Metro host, device address, or current application route.
18
+
19
+ <!-- docs:check -->
20
+
21
+ ```ts
22
+ import {resolveTileflowNativeManifestUrl} from '@tileflow/core/native';
23
+
24
+ const manifestUrl = resolveTileflowNativeManifestUrl(
25
+ 'https://maps.example.com/tileflow/native/manifest.json',
26
+ );
27
+ ```
28
+
29
+ The result is the validated, absolute HTTPS manifest URL; no network request is made.
30
+ The default policy requires HTTPS. Both input and resolved URLs are limited to 2,048 UTF-16 code
31
+ units. Empty values, leading or trailing whitespace, control characters, backslashes, fragments,
32
+ malformed percent escapes, embedded credentials, and protocol-relative URLs are rejected.
33
+ Local file URLs and custom renderer protocols are outside this helper's HTTP(S) contract.
34
+
35
+ ## Resolve resources from their owning document
36
+
37
+ Pass the URL of the document that declares the resource. A style URL is relative to its manifest;
38
+ a tile URL in TileJSON is relative to that TileJSON; a font URL in a style is relative to the style.
39
+ The helper uses normal URL path resolution and never guesses a deployment prefix.
40
+
41
+ <!-- docs:check -->
42
+
43
+ ```ts
44
+ import {resolveTileflowNativeResourceUrl} from '@tileflow/core/native';
45
+
46
+ const styleUrl = resolveTileflowNativeResourceUrl('styles/main/light.json', {
47
+ documentUrl: 'https://maps.example.com/tileflow/native/manifest.json',
48
+ });
49
+
50
+ const tileUrl = resolveTileflowNativeResourceUrl('./{z}/{x}/{y}.pbf', {
51
+ documentUrl: 'https://tiles.example.com/streets/tiles.json',
52
+ template: 'tile',
53
+ });
54
+ ```
55
+
56
+ `styleUrl` resolves to the `styles/main/light.json` path beneath the manifest's directory. `tileUrl`
57
+ resolves to the `streets/{z}/{x}/{y}.pbf` template on the tile server; no tile coordinates are chosen.
58
+
59
+ Use the actual document URL after any redirects that the application has approved. The declaring
60
+ URL is validated even when the resource is already absolute. External HTTPS resources retain their
61
+ own origin. The helper never adds delivery credentials or copies a parent's query parameters to a
62
+ child resource.
63
+
64
+ `template: 'tile'` preserves `{z}`, `{x}`, `{y}`, `{ratio}`, `{quadkey}`, `{bbox-epsg-3857}`, and
65
+ `{prefix}`. `template: 'glyphs'` preserves `{fontstack}` and `{range}`. These placeholders are allowed
66
+ only in the resource path or query, not the authority. They are not expanded or checked against a
67
+ renderer version. Unknown, mismatched, and incomplete placeholders are rejected. Without a template
68
+ option, literal braces are rejected; existing percent-encoded braces in paths and queries remain
69
+ encoded literal data. Manifest and declaring-document URLs cannot contain literal placeholders.
70
+ Hostname normalization must not introduce placeholders, including from percent escapes or Unicode
71
+ characters that normalize to braces.
72
+
73
+ ## Opt into one development origin
74
+
75
+ A debug integration can explicitly permit one HTTP origin:
76
+
77
+ <!-- docs:check -->
78
+
79
+ ```ts
80
+ import {resolveTileflowNativeManifestUrl} from '@tileflow/core/native';
81
+
82
+ const manifestUrl = resolveTileflowNativeManifestUrl(
83
+ 'http://192.168.1.5:8080/tileflow/manifest.json',
84
+ {developmentOrigin: 'http://192.168.1.5:8080'},
85
+ );
86
+ ```
87
+
88
+ Supply the same option when resolving that document's resources. Scheme, hostname, and port must
89
+ match the selected origin after URL normalization. An exception for `localhost` does not permit
90
+ `127.0.0.1`, another port, or another device. HTTPS remains allowed. The option must name an HTTP
91
+ origin without a path, query, fragment, wildcard, or credentials; an optional trailing slash is
92
+ accepted. Wildcards and placeholders are rejected before and after hostname normalization.
93
+
94
+ Keep this option in debug-only application configuration. The helper cannot determine whether the
95
+ application is a debug build, does not change iOS or Android transport settings, and does not expose
96
+ a development server. The application still owns any narrowly scoped debug transport exception.
97
+
98
+ ## Handle errors without logging the rejected URL
99
+
100
+ Failures throw `TileflowNativeUrlError`, a `TypeError` with a stable `code` and `field`.
101
+
102
+ - `NATIVE_URL_INVALID`: malformed, unsafe, unsupported-scheme, or oversized URL.
103
+ - `NATIVE_URL_ABSOLUTE_REQUIRED`: a document URL is missing its explicit HTTP(S) origin.
104
+ - `NATIVE_URL_HTTPS_REQUIRED`: HTTP does not match the selected development origin.
105
+ - `NATIVE_URL_DEVELOPMENT_ORIGIN_INVALID`: the development exception is not one valid HTTP origin.
106
+ - `NATIVE_URL_TEMPLATE_INVALID`: a template is unsupported, misplaced, or incomplete.
107
+
108
+ `field` is `manifestUrl`, `documentUrl`, `resourceUrl`, or `developmentOrigin`. Error messages do not
109
+ include the submitted URL or attach the original parser error. The application should preserve
110
+ that boundary when reporting failures: resource URLs may contain sensitive query parameters.
111
+
112
+ For JavaScript callers, missing or non-object resource options report `NATIVE_URL_INVALID` with
113
+ `field: 'documentUrl'`, just like an options object without its required `documentUrl`. Non-object
114
+ manifest options report `NATIVE_URL_DEVELOPMENT_ORIGIN_INVALID` with `field: 'developmentOrigin'`.
115
+ Omitting manifest options or passing `undefined` remains valid and uses the default HTTPS policy.
116
+ Neither function accepts `null` as an options object.
117
+
118
+ This is URL policy, not an authorization or server-side request-forgery defense. Callers still own
119
+ resource-origin authorization, redirect handling, response size limits, cancellation, and any
120
+ network request. Do not attach a credential merely because this helper accepted a URL.
121
+
122
+ ## Private parser and source builds
123
+
124
+ The native entry embeds the low-level parser from `whatwg-url` 15.1.0, `tr46` 6.0.0 (Unicode 17
125
+ IDNA processing) and Punycode.js 2.3.1. It exposes no URL class or parser configuration. Package
126
+ consumers only install Core; the published `dist/native.js` has no external runtime imports.
127
+ The parser is a development/build dependency, not an additional runtime package to install.
128
+ Canonical manifest validation also embeds the existing Zod dependency; it adds no package that
129
+ consumers must configure. Complete notices are included in [THIRD_PARTY_NOTICES.md](../THIRD_PARTY_NOTICES.md).
130
+
131
+ A React Native URL shim can concatenate references onto a filename or append a slash to `.json`.
132
+ The native helpers instead resolve against the owning document with the bundled parser, including
133
+ host percent-decoding, IDNA mapping, default ports and encoded dot segments. This does not require
134
+ an application-wide polyfill. The build-time native artifact validator uses the same Tileflow URL
135
+ policy with its existing host parser; it does not embed the mobile parser in its own bundle.
136
+
137
+ From an SDK source checkout, run the ordinary Core build, not a direct source import in a native
138
+ application. `tsup.native.config.ts` builds only the native entry with `platform: 'browser'`,
139
+ `target: 'es2020'`, ESM and no splitting. Its plugin redirects exactly the two relative
140
+ `./encoding` imports in the pinned parser modules to Core's private UTF-8 codec. It rejects an
141
+ unexpected dependency version, an unbound codec import or external runtime imports. It does not
142
+ patch packages on disk, configure a global alias or change another entry's build.
143
+
144
+ The codec uses replacement semantics for unmatched UTF-16 surrogates and invalid UTF-8 bytes and
145
+ preserves BOMs, matching `TextEncoder` and non-fatal `TextDecoder('utf-8', {ignoreBOM: true})`.
146
+ It processes at most 65,536 code units for encoding or 65,536 bytes for decoding per call. Private
147
+ placeholder sentinels are bounded well below that ceiling; the public URL ceiling stays at 2,048
148
+ code units. No parser or codec error is exposed as a cause containing the rejected URL.
149
+
150
+ ## Check the built package
151
+
152
+ From an installed and built SDK checkout:
153
+
154
+ ```sh
155
+ pnpm --filter @tileflow/core exec tsx --test test/native-runtime-url.test.ts test/native-url-provider.test.ts test/native-url-utf8.test.ts test/native-url-policy.test.ts test/native-source-view-package.test.ts
156
+ ```
157
+
158
+ The existing runtime regression checks the Node host and a React Native URL fixture independently.
159
+ Another test copies the built native file into an empty directory and executes it with ambient URL
160
+ and codec getters that throw. Neither test is a Hermes or device qualification.
161
+
162
+ For size comparisons, build Core in two isolated checkouts and retain an `npm pack --json` receipt
163
+ from each package directory. Pass the already-built Core directories and optional receipt paths to:
164
+
165
+ ```sh
166
+ node packages/core/scripts/measure-native-url.mjs /path/to/before/packages/core /path/to/after/packages/core /path/to/before-pack.json /path/to/after-pack.json
167
+ ```
168
+
169
+ The report separates raw native entry bytes, its runtime graph, a minified ES2020 browser consumer,
170
+ gzip size, compressed tarball bytes and unpacked package bytes. It performs no installation and is
171
+ not a Metro bundle, Hermes bytecode or device-memory measurement.
172
+
173
+ Native acceptance must install the exact tarball in an existing iOS/Android application and run
174
+ [`checkNativeUrlContract`](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/test/fixtures/native-url-contract.mjs)
175
+ with the installed `@tileflow/core/native` exports, leaving the application's URL global unchanged.
176
+ The [fixture procedure](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/test/fixtures/native-url-contract.md)
177
+ covers the two explicit development origins without network requests. Packaged Hermes execution
178
+ remains a separate qualification step; static parsing and Node tests do not establish it. URL-provider
179
+ checks do not qualify source/view behavior. This entry does not render maps, create transport
180
+ grants or make mobile service availability claims.
181
+
182
+ ## Acquire a bounded manifest
183
+
184
+ `loadTileflowNativeManifest(manifestUrl, {acquire, signal?, developmentOrigin?})` loads only the
185
+ runtime manifest. The application supplies `acquire`; Core never selects an ambient `fetch` or
186
+ starts a request at import time. The function returns an immutable `{manifestUrl, manifest}` result.
187
+ `manifestUrl` is the validated final response URL, not a guessed application or Metro address.
188
+
189
+ The acquisition adapter returns a request handle immediately. Its `response` promise resolves to
190
+ `{url, status, reader}`, and its `cancel()` method must be usable before the response arrives.
191
+ `url` must be the absolute final URL after redirects that the adapter has approved. Redirect and
192
+ resource-origin authorization remain application responsibilities; HTTPS syntax does not authorize
193
+ another origin. A missing or invalid final URL is an error, not a reason to reuse the request URL.
194
+
195
+ The reader exposes `read(maximumBytes)` and `cancel()`. Each read returns either `{done: true}` or
196
+ `{done: false, value: Uint8Array}` with between one and the requested number of bytes. Core requests
197
+ at most 64 KiB and copies each measured chunk before requesting another. At the 1 MiB ceiling it
198
+ requests one byte to distinguish EOF from overflow. Empty chunks, strings, malformed read results,
199
+ and chunks that exceed the requested allowance are rejected. The adapter must release its resources
200
+ at EOF; cancellation and failure also call its reader/request cancellation hooks.
201
+
202
+ An adapter must enforce bounded reads at its acquisition boundary. It must not implement this
203
+ interface by first calling an unlimited `response.text()`, `arrayBuffer()`, or native equivalent and
204
+ then slicing the completed body. A platform without a bounded byte reader needs an appropriate
205
+ acquisition adapter; this package does not supply a network bridge or a fallback buffering path.
206
+ The supplied request `maximumBytes` is the canonical 1 MiB cap, not an adjustable consumer budget.
207
+
208
+ Core counts actual bytes, never relies on Content-Length, and rejects overflow before JSON/schema
209
+ processing. Decoding is fatal UTF-8: malformed bytes and truncated sequences fail, including those
210
+ split across reads. One leading BOM is removed, matching the existing runtime reader; later BOMs
211
+ remain data. The private URL codec retains its existing replacement behavior for URLs. A depth
212
+ ceiling of 64 is checked before parsing JSON, and the canonical manifest schema still imposes all
213
+ map/theme/font/view/identity restrictions. Canonical and resolved manifest JSON must also remain
214
+ within 1 MiB; URL expansion cannot turn a small response into an unbounded result.
215
+
216
+ There is one version-1 grammar, shared with `parseTileflowRuntimeManifest`. Native supplies the
217
+ accepted private URL provider and byte-counting primitives to that grammar, without widening its
218
+ wire fields or accepting previously rejected relative URL forms. Every theme's `styleUrl` resolves
219
+ against the final manifest URL; font declarations retain the existing style-relative ownership.
220
+ The loader resolves those references but does not download styles, fonts, sprites, tiles or TileJSON.
221
+ Declared revisions and map/style IDs remain data, not inferred authorization or a session bootstrap.
222
+
223
+ There is no implicit cache, retry, request timeout or background task. Use the adapter's bounded
224
+ request policy or an external AbortSignal-compatible input for a deadline. Core does not construct
225
+ an AbortController, inspect `signal.reason`, install a polyfill, or wait for a broken cancellation
226
+ hook to finish. It retires an aborted operation immediately, observes late promise rejections and
227
+ cancels a reader that arrives after retirement. Physical cancellation of network work still depends
228
+ on the adapter honoring its cancellation methods.
229
+
230
+ ## Control a source
231
+
232
+ `createTileflowNativeSourceController({acquire})` coordinates a manifest-backed Tileflow source:
233
+ `{map, manifestUrl, developmentOrigin?}`. The map and manifest URL are required. The optional
234
+ development origin uses the exact HTTP exception described above and applies to manifest resources.
235
+ It belongs to the source descriptor, while theme, color scheme and cancellation remain replacement
236
+ options. Renderer discriminators and direct style inputs
237
+ are not part of this public source contract; the obsolete `kind` and `style` fields are rejected.
238
+ A completely unmanaged map uses upstream MapLibre directly, not another Tileflow source mode.
239
+
240
+ The controller's `state` is undefined until the first `replace()`. It then publishes only `loading`,
241
+ `ready`, or `error` snapshots, each with a monotonically increasing `generation`. The acquisition
242
+ adapter is required at construction. Core does not mount a renderer or subscribe to device appearance.
243
+
244
+ The following function expects an acquisition adapter implementing the bounded protocol above.
245
+ The example URL represents an existing manifest endpoint; no network adapter is supplied here.
246
+
247
+ <!-- docs:check -->
248
+
249
+ ```ts
250
+ import {
251
+ createTileflowNativeSourceController,
252
+ type TileflowNativeManifestAcquire,
253
+ } from '@tileflow/core/native';
254
+
255
+ export async function connectNativeManifest(acquire: TileflowNativeManifestAcquire) {
256
+ const controller = createTileflowNativeSourceController({acquire});
257
+ await controller.replace(
258
+ {
259
+ map: 'streets',
260
+ manifestUrl: 'https://maps.example.com/tileflow/native/manifest.json',
261
+ },
262
+ {theme: 'dark'},
263
+ );
264
+ const state = controller.state;
265
+ if (state?.status === 'ready') {
266
+ console.log(state.map.name, state.theme.name, state.theme.revision);
267
+ } else if (state?.status === 'error') {
268
+ console.error(state.error.code, state.error.field);
269
+ }
270
+ return controller;
271
+ }
272
+ ```
273
+
274
+ Narrow on `state.status` to access the resolved selection. Every `ready` snapshot retains the
275
+ immutable requested `source`, final `manifestUrl`, resolved `manifest`, selected `map` and concrete
276
+ `theme` as required fields. There is no renderer discriminator or direct-style state.
277
+
278
+ Map/theme identity and revisions come from the manifest. Map-level `apiUrl` takes precedence over
279
+ manifest-level `apiUrl`; no identity is guessed from URL paths. The map's canonical `view` is
280
+ preserved as metadata. The controller does not combine camera inputs.
281
+
282
+ Omitted `theme` uses `defaultTheme`; a concrete theme selects that exact declared name. `system`
283
+ uses the manifest's `systemThemes` mapping only when an explicit `colorScheme: 'light' | 'dark'`
284
+ is supplied. The same portable resolver owns this precedence in the existing runtime. Core never
285
+ reads device appearance. A missing map, missing theme or unresolved system selection is an error;
286
+ there is no first-map or first-theme fallback.
287
+
288
+ `replace(source, options)` snapshots inputs and retires the prior generation. A failed replacement
289
+ cannot publish the previous source as newly ready. Only the current generation can publish success
290
+ or error, even when responses arrive out of order or a transport ignores cancellation. Each valid
291
+ replacement reacquires its manifest. There is no hidden source cache. The promise settles when that
292
+ generation completes or is retired; source failures are reported in `state`, not as rejected
293
+ replacement promises. Calling it after disposal rejects with `NATIVE_SOURCE_DISPOSED`.
294
+
295
+ `subscribe(listener)` synchronously receives the current snapshot, when present, and subsequent
296
+ snapshots until its returned unsubscribe function is called. Observer exceptions are isolated from
297
+ acquisition and from other observers. Reentrant replacement/disposal prevents remaining observers
298
+ from receiving an obsolete notification. `dispose()` is idempotent: it retires active work, clears
299
+ listeners and retains the last snapshot for inspection without publishing another state. No late
300
+ completion or abort can notify a disposed controller.
301
+
302
+ ## Resolve an initial view without a renderer
303
+
304
+ `resolveTileflowNativeInitialView({view?, mapOptionsView?, manifestView?})` returns an immutable
305
+ `{center, zoom, bearing, pitch}`. All three inputs are partial renderer-neutral view objects, not
306
+ React Native props or MapLibre camera types. Precedence applies independently to each field:
307
+ `view` > `mapOptionsView` > `manifestView` > shared runtime defaults.
308
+
309
+ <!-- docs:check -->
310
+
311
+ ```ts
312
+ import {resolveTileflowNativeInitialView} from '@tileflow/core/native';
313
+
314
+ const initialView = resolveTileflowNativeInitialView({
315
+ manifestView: {center: [-3.7, 40.4], zoom: 10, bearing: 12},
316
+ mapOptionsView: {zoom: 11, pitch: 30},
317
+ view: {zoom: 13},
318
+ });
319
+ ```
320
+
321
+ The result is `{center: [-3.7, 40.4], zoom: 13, bearing: 12, pitch: 30}`. With no inputs, the defaults
322
+ are `{center: [0, 20], zoom: 2, bearing: 0, pitch: 0}`. Coordinates always use
323
+ `[longitude, latitude]`; Core neither swaps nor wraps them.
324
+
325
+ Validation reuses the manifest view grammar: longitude -180 to 180, latitude -90 to 90, zoom 0 to 24,
326
+ bearing -180 to 180 and pitch 0 to 85, all finite. Supplied fields with `undefined` are omitted;
327
+ `null`, out-of-range values, unknown fields, accessors and malformed tuples are errors. Every
328
+ supplied level is validated, even if a higher-precedence level would override it. Invalid explicit
329
+ input never silently falls back. The result owns a new frozen tuple, not a reference to an input or
330
+ to the shared defaults. Failures use `NATIVE_SOURCE_INVALID` with `field: 'view'` and no caller values.
331
+
332
+ The resolver is synchronous and independent of source control. Calling it again for a camera update
333
+ does not load a manifest, change the selected theme or increment a source generation. It does not
334
+ animate, read device appearance, install listeners or apply the result to a renderer.
335
+
336
+ ## Handle source errors
337
+
338
+ Source operations and initial-view resolution use `TileflowNativeSourceError` with stable `code`,
339
+ `field` and `kind`. An active external abort produces `kind: 'cancelled'` and `NATIVE_SOURCE_ABORTED`;
340
+ superseded or disposed work cannot publish that error into a newer generation. Acquisition,
341
+ response, schema and selection failures are terminal for that generation. A later explicit
342
+ `replace()` can retry. Tile-resource failures after a map is created are outside this controller
343
+ and do not get classified as manifest failures. This diagnostic `kind` classifies failure, not a
344
+ renderer or source mode.
345
+
346
+ - `NATIVE_SOURCE_INVALID`: invalid source, view, acquisition configuration or cancellation input.
347
+ - `NATIVE_SOURCE_ABORTED`, `NATIVE_SOURCE_DISPOSED`: cancellation or use after controller disposal.
348
+ - `NATIVE_MANIFEST_URL_INVALID`: invalid request/final URL or development-origin policy.
349
+ - `NATIVE_MANIFEST_REQUEST_FAILED`: acquisition/read failure or unsuccessful response.
350
+ - `NATIVE_MANIFEST_RESPONSE_INVALID`: invalid response, reader or chunk protocol.
351
+ - `NATIVE_MANIFEST_ACCESS_DENIED`, `NATIVE_MANIFEST_NOT_FOUND`: HTTP 401/403 or 404, respectively.
352
+ - `NATIVE_MANIFEST_TOO_LARGE`: actual body or resolved/canonical JSON exceeds the 1 MiB cap.
353
+ - `NATIVE_MANIFEST_UTF8_INVALID`, `NATIVE_MANIFEST_JSON_INVALID`: malformed UTF-8 or JSON.
354
+ - `NATIVE_MANIFEST_INVALID`: invalid version-1 structure, relationships or excessive nesting.
355
+ - `NATIVE_MANIFEST_RESOURCE_INVALID`: a manifest resource fails native URL policy.
356
+ - `NATIVE_MAP_NOT_FOUND`, `NATIVE_THEME_INVALID`: missing map or invalid/unresolved theme selection.
357
+
358
+ Fields identify only `source`, `signal`, `manifestUrl`, `response`, `body`, `map`, `theme` or `view`.
359
+ Errors never carry a rejected URL, response body, credential, remote exception, schema issue list or
360
+ abort reason. Successful snapshots necessarily contain resource URLs; treat them as potentially
361
+ sensitive when logging. The synchronous URL helpers retain their existing `TileflowNativeUrlError`
362
+ contract. A view error is thrown by the pure resolver and does not change controller state.
363
+
364
+ These APIs acquire manifests and resolve view data. They do not load styles, validate their
365
+ cartographic compatibility, fetch their resources, render maps, create Hosted authorization or
366
+ sessions, or provide a React Native component. Source and view behavior requires its own packed
367
+ Hermes checks; URL-only checks, Node tests and browser captures do not establish those results.