@tileflow/dev 0.1.0-alpha.3 → 0.1.0-alpha.31

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 (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +201 -5
  3. package/THIRD_PARTY_NOTICES.md +87 -0
  4. package/dist/artifacts.d.ts +265 -0
  5. package/dist/artifacts.js +81 -0
  6. package/dist/chunk-2QXKVQ6O.js +3062 -0
  7. package/dist/chunk-7C4F6GAX.js +170 -0
  8. package/dist/chunk-AQUGMUZW.js +324 -0
  9. package/dist/chunk-D4OE4ITL.js +13 -0
  10. package/dist/chunk-IR22ZTQW.js +2392 -0
  11. package/dist/chunk-KDFTE4Q5.js +156 -0
  12. package/dist/chunk-ONYQNLNA.js +1108 -0
  13. package/dist/chunk-UTRO4IBQ.js +671 -0
  14. package/dist/chunk-XHJBCVBB.js +241 -0
  15. package/dist/config.d.ts +33 -0
  16. package/dist/config.js +27 -0
  17. package/dist/fonts.d.ts +47 -0
  18. package/dist/fonts.js +16 -0
  19. package/dist/icons.d.ts +339 -0
  20. package/dist/icons.js +39 -0
  21. package/dist/inspect.d.ts +149 -0
  22. package/dist/inspect.js +805 -0
  23. package/dist/preview-assets-MVUJJ5XR.js +148 -0
  24. package/dist/preview.d.ts +71 -0
  25. package/dist/preview.js +2574 -0
  26. package/dist/server.d.ts +82 -0
  27. package/dist/server.js +2094 -0
  28. package/dist/tilesets.d.ts +89 -0
  29. package/dist/tilesets.js +6 -0
  30. package/dist/validation.d.ts +65 -0
  31. package/dist/validation.js +24 -0
  32. package/docs/artifact-lifecycle.md +152 -0
  33. package/docs/bounded-pmtiles-resource-inspection.md +21 -0
  34. package/docs/bounded-vector-feature-inspection.md +100 -0
  35. package/docs/local-comparison-workbench-and-compiler-inspection.md +227 -0
  36. package/docs/native-artifacts.md +285 -0
  37. package/docs/native-icons.md +129 -0
  38. package/docs/structured-config-inspection-and-diagnostics.md +39 -0
  39. package/package.json +77 -13
  40. package/dist/index.d.ts +0 -54
  41. package/dist/index.js +0 -314
@@ -0,0 +1,227 @@
1
+ # Local comparison workbench and compiler inspection
2
+
3
+ Start with the [@tileflow/dev guide](https://github.com/tileflow/tileflow-sdk/blob/main/packages/dev/README.md) for installation and a complete first example.
4
+
5
+ The CLI exposes the complete authoring surface through `tileflow preview --against-map ...` or
6
+ `--against-config ...`. Custom local servers can compose the same shell around two existing dev
7
+ handlers:
8
+
9
+ ```ts
10
+ import {createTileflowComparisonRequestHandler} from '@tileflow/dev/server';
11
+
12
+ const fetch = createTileflowComparisonRequestHandler({
13
+ left: {
14
+ basePath: '/left',
15
+ handler: leftHandler,
16
+ label: 'candidate / light',
17
+ previewUrl: '/left/',
18
+ sidecarUrl: '/left/__inspection/candidate/light.json',
19
+ },
20
+ right: {
21
+ basePath: '/right',
22
+ handler: rightHandler,
23
+ label: 'reference / light',
24
+ previewUrl: '/right/',
25
+ sidecarUrl: '/right/__inspection/reference/light.json',
26
+ },
27
+ });
28
+ ```
29
+
30
+ Each side handler retains ownership of its styles, fonts, sprites, events, diagnostics, and
31
+ last-known-good artifact generation. The comparison router only owns the root HTML and strict
32
+ dispatch to two disjoint same-origin route prefixes. Its synchronized camera, URL state,
33
+ side-by-side/split/overlay/blink modes, rendered-feature inspector, zoom-curve sampling, sprite
34
+ atlas, and scene/capture-command copying require no hosted service.
35
+
36
+ Set `inspection: true` on `createTileflowBuildArtifacts` or
37
+ `createTileflowArtifactSession` to compute `artifacts.styleInspections[map][theme]`. A dev handler
38
+ serves an available sidecar at `/__inspection/<map>/<theme>.json`; otherwise that route is 404. The
39
+ option is deliberately off by default. Sidecars remain in memory, do not alter Style bytes or
40
+ manifest hashes, and are excluded from `getTileflowArtifactFiles()` and every stable build output.
41
+
42
+ When a resolved map declares `fonts`, preparation reads its ordered directories and uses OpenType
43
+ full names as the canonical IDs referenced by `text-font`. TTF, OTF, and WOFF2 inputs are supported;
44
+ every contributing directory must contain `LICENSE.txt`. Later directories replace earlier faces
45
+ with the same exact ID, case-only collisions fail, and every primary font in the final style must be
46
+ present. `font` is an exact face ID; local `fallbacks` entries are exact face names or explicit CSS
47
+ generic families such as `sans-serif`. Preparation never synthesizes a face name from a family and
48
+ weight. Only selected primary faces and licenses are emitted as
49
+ content-addressed managed artifacts.
50
+
51
+ `fonts` and `glyphs` are mutually exclusive map fields. Omission inherits the parent's current text
52
+ provider; declaring either one replaces an inherited provider of either kind atomically. The font
53
+ pipeline is driven only by directory metadata and final style usage—there are no hard-coded map or
54
+ family names, and a URL provider comes only from the resolved map instead of being invented by the
55
+ pipeline. After inheritance resolves, a map with any text layer must have exactly one provider. A
56
+ `glyphs: {kind: 'url', ...}` provider enumerates the exact comma-joined MapLibre request keys
57
+ produced by the style's `text-font` arrays.
58
+
59
+ The resulting style records strict `tileflow:fontFaces` metadata. Preview, capture, and browser
60
+ framework adapters load those definitions before constructing MapLibre. Hosted deploy rejects a
61
+ prepared local-font bundle before authentication until managed font storage is available. A derived
62
+ map that declares `glyphs` replaces the inherited local provider atomically; the URL-backed map is
63
+ complete as declared and independent of its World selection.
64
+
65
+ With both controls off, the built-in preview draws the style as compiled, as Capture and the
66
+ published framework adapters draw it: flat trees, building layers marked for the 3D control left
67
+ hidden, and no street lights. This keeps an ordinary `tileflow preview` session WYSIWYG with
68
+ production and visual baselines.
69
+
70
+ The `3D ON/OFF` and `TREES ON/OFF` controls mount Core's 3D layers as an application does, from
71
+ the description the compiled style publishes. When the style describes 3D buildings
72
+ (`tileflow:buildings`), `3D ON` raises them with `createTileflowBuildings3D` in place of the
73
+ compiled extrusion and lights the streets with `createTileflowStreetLights` where the style
74
+ describes them. When it describes 3D trees (`tileflow:vegetation`), `TREES ON` stands them with
75
+ `createTileflowTrees3D`, which hides the style's flat trees while they stand. Every look the
76
+ preview shows comes from the map's modules; the preview holds no appearance of its own, and the
77
+ theme on screen supplies the sun. Neither mode is capture evidence. Both choices survive reloads
78
+ and shared links through the `buildings3d=on|off` and `trees3d=on|off` URL parameters.
79
+
80
+ The same control toggles any compiled layer whose metadata contains
81
+ `tileflow:3d-toggle = building`. It switches the building volumes independently from the camera,
82
+ so enabling 3D never changes the pitch and users remain free to tilt the map with pointer controls.
83
+
84
+ ### Detailed landmark models
85
+
86
+ A hidden style layer can opt the Streets preview into detailed GLB landmarks by setting
87
+ `tileflow:landmark-manifest-url` in its metadata. The bounded version-2 manifest indexes GLB entries
88
+ inside one or more PMTiles archives and is fetched only when the 3D building control is enabled near
89
+ the landmark runtime zoom. Archive URLs may be absolute or relative to the manifest URL.
90
+
91
+ ```json
92
+ {
93
+ "schemaVersion": 2,
94
+ "id": "madrid-landmarks-2026-08-19",
95
+ "minzoom": 16,
96
+ "maximumVisibleModels": 8,
97
+ "maximumCachedModels": 16,
98
+ "archives": [
99
+ {
100
+ "id": "madrid-v1",
101
+ "url": "./madrid-landmarks-v1.pmtiles",
102
+ "bytes": 14832912,
103
+ "sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
104
+ }
105
+ ],
106
+ "landmarks": [
107
+ {
108
+ "id": "palacio-cibeles",
109
+ "center": [-3.6921, 40.4193],
110
+ "bounds": [-3.693, 40.4186, -3.6912, 40.42],
111
+ "priority": 100,
112
+ "models": [
113
+ {
114
+ "minzoom": 16,
115
+ "archiveId": "madrid-v1",
116
+ "z": 0,
117
+ "x": 0,
118
+ "y": 0,
119
+ "bytes": 481024,
120
+ "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
121
+ "axisConvention": "EUN_Y_UP"
122
+ },
123
+ {
124
+ "minzoom": 18,
125
+ "archiveId": "madrid-v1",
126
+ "z": 1,
127
+ "x": 0,
128
+ "y": 0,
129
+ "bytes": 1276416,
130
+ "sha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
131
+ "axisConvention": "EUN_Y_UP"
132
+ }
133
+ ]
134
+ }
135
+ ]
136
+ }
137
+ ```
138
+
139
+ `maximumVisibleModels` is bounded to 1–64. `maximumCachedModels` defaults to twice that value,
140
+ must be at least the visible limit, and is bounded to 128. Higher `priority` values win when more
141
+ landmarks intersect the viewport than can be shown. `models[0]` is the base LOD and must start at the
142
+ manifest `minzoom`; later entries replace it at their distinct `minzoom`. The parser bounds manifest,
143
+ archive, landmark, model and GLB-entry sizes, rejects credential-bearing or non-HTTP archive URLs,
144
+ and verifies each extracted entry's declared byte length and SHA-256 before parsing it.
145
+
146
+ The preview starts the Three.js runtime and manifest request together one zoom before the landmark
147
+ layer becomes visible. A landmark entering directly at a close zoom always displays its base LOD
148
+ first and upgrades in place. Nearby base LODs are warmed in the parsed-model cache. Style layers
149
+ marked with `tileflow:landmark-fallback = true` remain visible until the corresponding active GLB is
150
+ ready, so a cold request never leaves an empty building volume.
151
+
152
+ Embedded Streets previews also accept a same-origin parent command without reloading the document:
153
+
154
+ ```js
155
+ iframe.contentWindow.postMessage(
156
+ {
157
+ type: 'tileflow:set-map-state',
158
+ schemaVersion: 1,
159
+ state: {
160
+ center: [-3.688344, 40.453053],
161
+ zoom: 17.75,
162
+ bearing: -24,
163
+ pitch: 58,
164
+ buildings3d: true,
165
+ trees3d: false,
166
+ visibleLayerGroups: ['labels', 'roads', 'buildings', 'landuse', 'water'],
167
+ },
168
+ },
169
+ location.origin,
170
+ );
171
+ ```
172
+
173
+ The command is applied atomically only for the embedding parent, with exact same-origin and numeric
174
+ range checks. `visibleLayerGroups` is optional and accepts only `labels`, `pois`, `roads`, `transit`,
175
+ `buildings`, `landuse`, and `water`; omitted groups are hidden while base background and attribution
176
+ remain. Camera and toggle state continue to be written to the preview URL after application.
177
+
178
+ GLB base colors, textures, emissive values, metalness, and roughness are authoritative and are not
179
+ rewritten by the preview. Models outside the active selection remain hidden in a least-recently-used
180
+ cache; leaving the cache disposes their geometry, materials, and textures.
181
+
182
+ The vector building represented by a detailed model must carry `hide_3d = true`. The ordinary
183
+ building extrusion and both screen-space shadow passes filter on that field, preventing the coarse
184
+ volume from intersecting the GLB while retaining the flat footprint below it. This flag belongs in
185
+ the vector-tile dataset; the landmark endpoint cannot substitute for it. Version model and GLB URLs
186
+ so they can be served with immutable caching. Cross-origin endpoints must allow credentialed GETs
187
+ because the development renderer requests the manifest and models with credentials.
188
+
189
+ If a map uses `icons: ['./icons/brand']`, build artifacts include the generated MapLibre
190
+ `sprite.json`, `sprite.png`, `sprite@2x.json`, and `sprite@2x.png` beside its style. Ordinary sources
191
+ are normalized into 24 px icon cells. A source named `<id>.pattern.svg` (or another supported raster
192
+ format with the same marker) instead keeps its intrinsic dimensions and is published as `<id>`.
193
+ This provides calibrated, non-square textures for line and fill patterns while the runtime still
194
+ consumes only the generated PNG sprite. Pattern widths must be a power of two from 2 through 512
195
+ pixels, matching MapLibre's seamless line-pattern contract.
196
+
197
+ `icons` is one ordered array. Omitting it inherits the parent's exact array, declaring it replaces
198
+ the array atomically, and `[]` means no icons. Use a spread to preserve imported directories while
199
+ adding or replacing files:
200
+
201
+ ```ts
202
+ import {defineMap} from '@tileflow/core';
203
+ import {streets} from '@tileflow/maps';
204
+
205
+ export default defineMap({
206
+ id: 'brand-map',
207
+ version: 1,
208
+ extends: streets,
209
+ icons: [...streets.icons, './icons/brand'],
210
+ });
211
+ ```
212
+
213
+ Directories apply left to right. `<id>.<ext>` publishes an icon as `<id>`;
214
+ `<id>.pattern.<ext>` publishes the intrinsic-size pattern as `<id>`. The published ID must already
215
+ be canonical lower-kebab-case. A later file replaces an earlier file only for the same exact ID;
216
+ case-only collisions fail. Package maps export `streetsIcons`, `baedekerIcons`, `civicaIcons`,
217
+ `ferrarisIcons`, `haradIcons`, `siegfriedIcons`, `soundingsIcons`, `blueprintIcons` directory descriptors from
218
+ `@tileflow/maps`. Baedeker, Cívica, and Siegfried also export
219
+ `baedekerFonts`, `civicaFonts`, and `siegfriedFonts`. Preparation
220
+ resolves package descriptors inside their owning package, checks real-path containment, and
221
+ compiles them through the same pipeline as a config-relative directory. There are no
222
+ built-in/source/sprite selectors, mappings, icon-specific inheritance, or compatibility aliases.
223
+
224
+ Local directories resolve from the directory containing the selected `tileflow.config.ts`, while
225
+ the workspace `cwd` remains their containment boundary. Canonical local syntax starts with `./` or
226
+ one or more leading `../` segments and then uses only non-empty segments other than `.` or `..`;
227
+ trailing or repeated slashes, backslashes, controls, and paths longer than 512 characters fail.
@@ -0,0 +1,285 @@
1
+ # Prepare native map artifacts
2
+
3
+ Use the existing artifact pipeline with `renderer: 'native'` to prepare the `native-v1` static
4
+ profile. The same config loader, inheritance resolver, semantic compiler, icon composition and
5
+ font pipeline are used. There is no separate native map language.
6
+
7
+ Install the packages and create a trusted `tileflow.config.ts` as described in the
8
+ [package README](../README.md). The source version containing this option must be installed;
9
+ a source branch or a successful build does not establish npm availability.
10
+
11
+ ## Prepare in memory
12
+
13
+ <!-- docs:check -->
14
+
15
+ ```ts
16
+ import {createTileflowBuildArtifacts} from '@tileflow/dev/artifacts';
17
+
18
+ const artifacts = await createTileflowBuildArtifacts({
19
+ config: 'tileflow.config.ts',
20
+ renderer: 'native',
21
+ });
22
+
23
+ try {
24
+ console.log(artifacts.manifest);
25
+ console.log(artifacts.nativeBuild);
26
+ } finally {
27
+ await artifacts.dispose?.();
28
+ }
29
+ ```
30
+
31
+ The pipeline checks every concrete theme. A rejected theme fails the complete generation before
32
+ production output is written. Native preparation applies the versioned transformations below;
33
+ other unsupported layers, fonts or protocols are not silently removed.
34
+ Local PMTiles are rejected before creating archive snapshots. URL checks do not fetch remote
35
+ TileJSON, glyphs or tiles and do not prove their content or availability.
36
+
37
+ Font preparation verifies the same licensed source bytes as web preparation, then maps exact full
38
+ names into Native's `font-faces` root property. It does not retain browser `tileflow:fontFaces`
39
+ metadata or claim a system font is equivalent. Sprites keep the original independently generated
40
+ 1x and 2x bytes; the native check verifies their pair, dimensions, IDs and existing size limits.
41
+
42
+ ## Versioned native lowering
43
+
44
+ Selecting `renderer: 'native'` applies `native-lowering-v1` after shared semantic compilation and
45
+ before final native validation. It never edits the authored map, the official map definitions or
46
+ the shared compiler. Web output is not lowered.
47
+
48
+ A fixed `projection: {type: 'globe'}` is explicitly normalized to Native's implicit Mercator. This
49
+ is a renderer-specific projection change, not equivalent globe rendering. An explicit fixed
50
+ Mercator declaration is also omitted. Adaptive/expression projections and unknown projection
51
+ options remain errors. Every style receipt states which normalization, if any, occurred.
52
+
53
+ For line layers, finite `line-cap` and `line-dasharray` decisions become contiguous physical layers
54
+ with constant values. Supported trees use constant leaves, `literal`, first-match `case`, disjoint
55
+ `match` labels and `step`. Predicates may combine boolean operations, scalar equality, membership,
56
+ `has`, geometry type and typed numeric comparisons/steps. Unchecked feature values are restricted
57
+ to vector sources, whose properties are scalar. Numeric coercion requires an explicit finite
58
+ fallback. Arbitrary feature-valued leaves, continuous interpolation, feature state, unsupported
59
+ operators and any unprovable decision fail with the original property pointer.
60
+
61
+ The partition preserves first-match priority. Each feature accepted by the original filter is
62
+ accepted by exactly one active replacement with the same cap and dash values; all other features
63
+ remain excluded. Existing legacy filters are converted with the pinned Style Spec converter before
64
+ intersection. Source, source-layer, metadata and other layout/paint values are preserved. Existing
65
+ `minzoom`/`maxzoom` are intersected with the decision intervals. Both target properties use integer
66
+ camera-zoom evaluation, so a step at 8.5 changes its value at zoom 9, while an original fractional
67
+ layer visibility bound remains fractional. New branch filters contain no camera-zoom expression.
68
+
69
+ Grouped outcomes use compact boolean decision trees rather than repeated lists of every earlier
70
+ arm's negated guard. Multi-arm cases remain flat to avoid trading size for excessive depth.
71
+ Identical outcomes and adjacent identical intervals are coalesced. IDs derive from the original
72
+ layer ID, the lowering version and deterministic branch order, with collision checks against the
73
+ whole input style. The original layer's replacements stay together; surrounding layers are not
74
+ reordered. Existing sort-key expressions are retained, not simplified or replaced.
75
+
76
+ Replacements shift the physical index of every later layer, so lowering rebinds the style's layer
77
+ references from the recorded spans. Each `tileflow:interaction-manifest` POI `priority` becomes its
78
+ layer's final index. An overlay placement anchor naming an expanded layer moves to that layer's first
79
+ replacement, because anchors mark insertion-before boundaries. Metadata that the spans cannot prove,
80
+ such as a POI priority that differs from its compiled layer index, a POI entry whose source does not
81
+ match its layer or an anchor naming a missing layer, fails with `NATIVE_UNSUPPORTED_STYLE` at the
82
+ style's `/metadata` pointer rather than being guessed. Local builds, watched sessions and native
83
+ preview share this step with Hosted publication.
84
+
85
+ MapLibre Native has no symbol overlap modes and reads only `icon-allow-overlap` and
86
+ `text-allow-overlap`. Lowering replaces each constant `icon-overlap` or `text-overlap` value with
87
+ the matching `*-allow-overlap`: `false` for `never` and `true` for `always` and `cooperative`. A
88
+ cooperative symbol therefore no longer gives way to symbols placed with `never` and can cover labels
89
+ it avoids on the web. The layer keeps its ID and position, so the build record lists it under
90
+ `symbolOverlap` rather than as a layer span. An overlap mode given as an expression fails with its
91
+ property pointer.
92
+
93
+ Work is bounded per property by depth 16, 512 decision visits, 64 decision paths, 16 distinct
94
+ outcomes and 64 dash elements; the product is limited to 32 physical layers and 32 zoom intervals
95
+ per logical layer. Raw compiler input must fit 160,000 JSON nodes before any lowering. Its expanded
96
+ native representation has a separate fixed 540,000-node ceiling, checked after lowering, after
97
+ font preparation and after generation URL retargeting. Both stages retain 4,096 layers, depth 64
98
+ and 8 MiB. No caller option raises either budget and no rejected input is retried under the output
99
+ allowance. A failure occurs before output writes and leaves the previous valid generation intact.
100
+
101
+ This is one shared authored map, not a second mobile catalog: the native-v1 representation adapts
102
+ only finite `line-cap`/`line-dasharray` decisions and constant symbol overlap modes, together with
103
+ its explicit fixed-projection policy. Unsupported advanced expressions or other properties fail
104
+ with their native diagnostic and JSON Pointer; their web behavior is unchanged. The raw and
105
+ prepared validators share their semantic checks and bounded traversal. See the
106
+ [Core budget contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/native-artifact-profile.md#input-and-prepared-style-budgets)
107
+ for the measured Streets sizes and the approximately 5.2% output margin.
108
+
109
+ The equivalence checks cover per-feature selection, cap/dash values and zoom ranges, **not draw
110
+ order across features**. Copying `line-sort-key` preserves ordering only inside each physical
111
+ layer. If a feature with sort key 1 enters the first branch and a feature with sort key 0 enters
112
+ the second, the physical layers draw them as 1 then 0 instead of 0 then 1. Both features can satisfy
113
+ mutually exclusive filters yet have overlapping geometry. The resulting composition can differ,
114
+ not merely its antialiasing. Even without a sort key, partitioning can change source feature order.
115
+
116
+ The native-lowering-v1 policy permits these bounded partitions without promising identical global
117
+ feature order. Applications requiring exact overlapping-feature order must not treat such an
118
+ artifact as order-equivalent to its web style. Automated checks establish feature selection,
119
+ cap/dash values, zoom ranges, determinism and resource bounds, not pixel equivalence.
120
+
121
+ A small directed iOS/Android rendering check of cap/dash/overlap is required for each accepted
122
+ candidate using the recipe below. Full visual qualification belongs to the general mobile release
123
+ gate; it is not claimed by artifact preparation. Browser capture cannot replace either native
124
+ check, and a few visual samples cannot prove preservation of arbitrary feature order.
125
+
126
+ ## Write a separate output
127
+
128
+ <!-- docs:check -->
129
+
130
+ ```ts
131
+ import {writeTileflowBuildArtifacts} from '@tileflow/dev/artifacts';
132
+
133
+ await writeTileflowBuildArtifacts({
134
+ config: 'tileflow.config.ts',
135
+ renderer: 'native',
136
+ outDir: 'public/tileflow',
137
+ });
138
+ ```
139
+
140
+ This writes `public/tileflow/native/manifest.json`, `build-manifest.json`, `native-build.json` and
141
+ the complete style/asset generation beneath `public/tileflow/native`. The writer adds `native` once
142
+ beneath `outDir`; pass the output parent, not an already renderer-qualified directory. The native
143
+ subdirectory owns a separate inventory. A web build at the parent neither replaces its manifest nor
144
+ collects the native generation, and vice versa.
145
+
146
+ The runtime manifest remains strict version 1 with the same map/theme shape. No renderer/profile
147
+ field is added to it. Native font declarations live in the native styles, not a web font loader.
148
+ `native-build.json` is a separate strict build record that identifies the renderer, profile, selected
149
+ engines, `validation: 'static-artifacts'` and the canonical `build-manifest.json` SHA-256. Its
150
+ schema version is 2 and its `preparationVersion` is `native-lowering-v1`.
151
+
152
+ The `transformations` array is sorted uniquely by map/theme. Each entry records input and lowered
153
+ style SHA-256 values, input/output layer counts and projection normalization. `layers` lists the
154
+ input ordinal, output start/count and affected properties of each partitioned line layer;
155
+ `symbolOverlap` lists the input ordinal and replaced properties of each symbol layer whose overlap
156
+ modes became `*-allow-overlap` values. Ordinals refer to the complete style layer arrays, including
157
+ unchanged layers. These spans make the deterministic
158
+ physical IDs traceable without duplicating layer definitions in the receipt. The input hash binds shared
159
+ compiler output; the lowered hash includes the rebound layer references and precedes native font
160
+ preparation and generation URL retargeting.
161
+ The build-manifest hash separately binds the final prepared styles and assets. All styles receive
162
+ an entry, including styles for which no lowering was needed. Schema v1 build records do not imply
163
+ this preparation policy and are not parsed as v2.
164
+
165
+ The existing logical map revision remains based on resolved cartography and source asset identity.
166
+ Representation changes, such as native font mapping, change the style hash rather than inventing
167
+ a second logical map. Transport credentials and device state never enter these artifacts.
168
+
169
+ File names in an in-memory plan remain relative to the renderer output root. Relative style/font
170
+ URLs remain relative to their owning documents. An explicit absolute or host-relative
171
+ `styleBaseUrl`/`assetBaseUrl`, such as `/tileflow`, receives the `/native` suffix. The pipeline uses
172
+ an inert `.invalid` document base only to check relative URL syntax; it does not publish that base
173
+ or infer the application's real delivery origin.
174
+
175
+ A served local preview can use an absolute HTTP base, such as `http://127.0.0.1:3333`, only when it
176
+ also passes that exact origin as `developmentOrigin`. Native checks then accept HTTP resource URLs,
177
+ such as the sprite and fonts, with that scheme, host and port. Every other origin still requires
178
+ HTTPS, and browser protocols remain rejected. The option is never inferred from a base URL.
179
+ Web preparation, `target: 'production'` and `writeTileflowBuildArtifacts()` reject it with
180
+ `NATIVE_RENDERER_UNSUPPORTED` at `/developmentOrigin`, so written artifacts never depend on it.
181
+
182
+ Omitting `renderer`, or specifying `renderer: 'web'`, preserves the existing web artifact bytes and
183
+ output layout. The native record is absent from web plans.
184
+
185
+ ## Retain the last valid generation
186
+
187
+ `createTileflowArtifactSession({renderer: 'native', watch: true})` uses the existing session lifecycle.
188
+ An incompatible edit publishes an `invalid` state with native diagnostic context. It retains the
189
+ previous valid artifacts but does not publish them again as a new successful generation. Acquiring,
190
+ releasing and disposing a generation follow the existing [artifact lifecycle](artifact-lifecycle.md).
191
+
192
+ This is artifact watching, not a native preview server. Do not pass these artifacts to browser
193
+ preview/capture as evidence of a native render. Native inspection sidecars, React Native components,
194
+ Hermes execution, iOS/Android qualification, Hosted authorization and deployment are not supplied.
195
+
196
+ ## Audit lowering size and feature order
197
+
198
+ From the SDK source root after installing and building the pinned workspace:
199
+
200
+ ```sh
201
+ pnpm exec tsx scripts/native-lowering-audit.ts > native-lowering-audit.json
202
+ ```
203
+
204
+ This diagnostic report compiles the unchanged full Streets family through the web preparation
205
+ path, then measures the native representation for **both** dark and light before applying output
206
+ budget assertions. It writes no native artifact generation and is not a compatibility verdict.
207
+ Each theme includes layer counts, visited JSON nodes, UTF-8 bytes and maximum depth before/after;
208
+ every transformed layer is listed with its ID, physical span, branch count and zoom intervals,
209
+ sort-key expression and paint indicators. Simultaneously active branches are marked as an
210
+ unproven feature-order boundary, not as a claim that their geometry is disjoint.
211
+
212
+ Subtree accounts separate copied unchanged layer payload from generated filters and input
213
+ cap/dash decisions. They identify whether predicate expansion or repetition of other properties
214
+ dominates; they are not an additive byte decomposition. The counters include array keys, matching
215
+ the profile's existing node convention. Retain the report with the checkout SHA. An over-budget
216
+ result must not be treated as a successful native build or fixed by dropping layers or weakening
217
+ validation. The test suite prints the two-theme size summary even when a budget assertion fails.
218
+
219
+ ## Prepare Streets for a directed native check
220
+
221
+ From a source checkout, install and build the pinned workspace first. The test-only
222
+ `test/fixtures/native-streets.config.ts` exports the unchanged official Streets definition. It
223
+ exists to make artifact generation repeatable without a second compiler, custom map or native
224
+ harness. From the SDK root:
225
+
226
+ ```sh
227
+ pnpm --filter @tileflow/dev exec node ../cli/dist/index.js validate \
228
+ --config test/fixtures/native-streets.config.ts --renderer native --target local --json
229
+ pnpm --filter @tileflow/dev exec node ../cli/dist/index.js build \
230
+ --config test/fixtures/native-streets.config.ts --renderer native --target local \
231
+ --out output/native-streets --json
232
+ pnpm exec tsx scripts/native-lowering-audit.ts > /tmp/native-lowering-audit.json
233
+ ```
234
+
235
+ The output root is `packages/dev/output/native-streets/native`. It contains
236
+ `styles/streets/dark.json`, `styles/streets/light.json`, their shared sprite/font assets, a version-1
237
+ `manifest.json` and the version-2 `native-build.json` receipt. Keep the complete directory together.
238
+ The manifest points to the content-addressed generation; use those URLs for an actual render.
239
+ Do not copy only the two style files and leave their relative assets behind. Record the checkout
240
+ SHA, both prepared style hashes, engine versions and the resource origin used for the check.
241
+ The fixture and output are source-checkout tools, not a published mobile component or a baseline.
242
+
243
+ Use an existing native test application with the pinned engines and a reachable artifact origin;
244
+ artifact generation starts no server and requests no deployment. Load the same generated manifest
245
+ family independently on iOS and Android. Resolve its style URL against the manifest URL, then each
246
+ relative style resource against that style's URL before passing the style to MapLibre Native. A raw
247
+ style URL does not transfer this ownership to the renderer. The directed artifact check may use a
248
+ test-owned resolver, but that does not qualify a future runtime resolver. A useful starting viewport
249
+ is a dense road/rail junction with visible paths and bridges; select one where the loaded vector data
250
+ actually contains features for the transformed layers listed in
251
+ `/tmp/native-lowering-audit.json`. Use the audit's exact physical IDs, source layers, branch counts
252
+ and zoom intervals rather than assuming that a map location exercises every branch.
253
+
254
+ Check both themes at the same center, viewport, pitch 0 and bearing 0. Exercise at least one cap
255
+ partition, one dashed/solid partition and an overlap between simultaneously active branches. Pan
256
+ slightly and inspect just below/at/above the relevant audit zoom cuts (for example 15.99/16/16.01
257
+ when a selected interval changes at 16). Check that caps and dash patterns are visible, layers do
258
+ not disappear or double-select a feature, theme replacement preserves the scene, and no resource
259
+ or style errors are reported. Inspect overlapping strokes for the documented composition
260
+ trade-off, not for equality with web drawing order. Keep brief per-platform observations and
261
+ explicitly distinguish an unexercised branch from a passed one.
262
+
263
+ This check has not been run by the artifact tests. It neither establishes full iOS/Android visual
264
+ qualification nor creates a screenshot baseline. Renderers, native view lifecycle, network
265
+ transport and mobile distribution remain outside these build tools.
266
+
267
+ ## Reproduce the official-map report
268
+
269
+ From an SDK source checkout after installing and building its pinned workspace:
270
+
271
+ ```sh
272
+ pnpm exec tsx scripts/native-catalog-report.ts > native-catalog-report.json
273
+ ```
274
+
275
+ The report evaluates all ten official maps and each of their themes through the shared pipeline.
276
+ Rows contain either a static-compatible style hash or exact incompatibility diagnostics, sorted by
277
+ map/theme with no timestamp or local path. Each row checks an individual theme; it is not a revision
278
+ receipt for a rewritten single-theme map. Unexpected compilation/tool failures stop report generation
279
+ rather than being relabeled as known incompatibilities. Retain the output together with the checkout
280
+ SHA and lockfile used to produce it. The test suite compares two independent evaluations and requires
281
+ Streets to pass. No unchecked catalog-wide compatibility assertion is implied by the presence of
282
+ this command, and this report is not a device-qualification result.
283
+
284
+ See the [Core profile contract](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/native-artifact-profile.md)
285
+ for the version intersection, diagnostic codes and unsupported representations.
@@ -0,0 +1,129 @@
1
+ # Author recolorable and adaptable icons
2
+
3
+ Use SDF for a monochrome silhouette whose fill and halo vary by map. Keep multicolor artwork as
4
+ fixed-color RGBA. Ordinary filenames and icon IDs stay unchanged.
5
+
6
+ In an icon directory already selected by your map, place `tileflow.icons.json` beside `health.svg`:
7
+
8
+ ```json
9
+ {
10
+ "schemaVersion": 1,
11
+ "icons": {
12
+ "health": {
13
+ "representation": "sdf",
14
+ "defaults": {"color": "#c43d35", "haloColor": "#ffffff", "haloWidth": 1, "haloBlur": 0}
15
+ }
16
+ }
17
+ }
18
+ ```
19
+
20
+ Only explicitly declared SDF assets are converted. Omitted entries, or `representation: "rgba"`,
21
+ preserve fixed-color rendering. SDF uses the source alpha silhouette; source RGB does not define its
22
+ default color. `defaults.color` is required. Omitted halo fields become transparent, zero width and
23
+ zero blur. Unknown IDs, duplicate JSON keys, unknown fields and invalid colors fail preparation.
24
+
25
+ The generated cell remains 24 × 24 logical pixels at both densities. Within that cell, the source
26
+ silhouette needs six fully transparent pixels on every side. Tileflow rejects edge-touching,
27
+ empty, or insufficiently defined silhouettes instead of adding padding or shrinking artwork.
28
+ Author opaque silhouettes with antialiased edges; gradients and translucent artwork should remain
29
+ RGBA. Thin shapes without a resolved interior fail verification. Generation rasterizes 1x and 2x
30
+ independently and encodes distance in white-RGB alpha. SDF is for symbol icons, not patterns.
31
+
32
+ For existing artwork, expand the source canvas explicitly before declaring SDF. A drawing bounded
33
+ by `viewBox="0 0 15 15"` fits the profile when its outer SVG uses `width="24" height="24"`
34
+ and `viewBox="-7.5 -7.5 30 30"`: the drawing occupies at most the central 12px. Review fine details
35
+ at the intended map size; use `icon.size` to choose the displayed size. Keep the original source for
36
+ fixed-color use. A transparent margin does not make an excessively thin silhouette suitable.
37
+
38
+ ## Keep each symbol layer homogeneous
39
+
40
+ One package may contain RGBA and SDF icons. Every icon selectable by one symbol layer, including
41
+ fallbacks, must use the same representation. Tileflow checks the effective composition after all
42
+ local overrides. It does not split layers or alter collision priority, coupled labels or IDs.
43
+
44
+ An unbounded selection such as a POI icon read from feature data can select any effective icon ID.
45
+ It therefore requires a homogeneous inventory. To use a mixed package, select one fixed icon or
46
+ an explicit `expr.match`/`expr.case` expression whose possible outputs and fallback are homogeneous.
47
+ Sampling the current feature data does not prove this condition.
48
+
49
+ `tileflow validate`, `tileflow preview`, `tileflow build`, framework preparation and Capture use the
50
+ same check. `TF_ICON_REPRESENTATION_MIXED` identifies the layer and image-selection path. Replace
51
+ the conflicting override, align all selectable representations, or bound the selection explicitly.
52
+ Compiled SDF admission rejects legacy token templates and function objects with
53
+ `TF_ICON_SELECTION_UNPROVEN`; rebuild using expression-based image selection.
54
+ Development reports an invalid generation and retains its last valid artifact until recovery.
55
+
56
+ ## Style each map independently
57
+
58
+ Use the existing `styles.<category>.icon` fields: `color`, `haloColor`, `haloWidth`, and `haloBlur`.
59
+ Theme tokens, explicit `fixed(...)` values and supported data, zoom and feature-state expressions
60
+ compile to native MapLibre paint. Feature state is supported in paint, not image selection.
61
+ Keep halo width within 0–2 logical pixels and blur within 0–1. Dynamic halo expressions must prove
62
+ these bounds; use `expr.min`/`expr.max` around unconstrained feature values. Otherwise compilation
63
+ reports `TF_ICON_HALO_BOUNDS`.
64
+
65
+ An omitted paint property uses that selected icon's declared default, including missing-ID
66
+ fallbacks. An override replaces only its property. It does not edit the shared revision or another
67
+ map. Image IDs remain stable; recoloring does not generate image variants. Existing size, opacity,
68
+ rotation and anchor controls retain their native semantics.
69
+
70
+ ## Fit a background around text
71
+
72
+ Declare optional `layout` beside the representation. It applies to RGBA and SDF independently.
73
+ For a 24px background whose central region can stretch, use this sidecar entry:
74
+
75
+ ```json
76
+ {
77
+ "schemaVersion": 1,
78
+ "icons": {
79
+ "price": {
80
+ "representation": "rgba",
81
+ "layout": {
82
+ "stretchX": [[9, 15]],
83
+ "stretchY": [[9, 15]],
84
+ "content": [7, 7, 17, 17]
85
+ }
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ Coordinates use the generated logical cell, not the original SVG viewBox. Each stretch interval
92
+ is `[start, end]`; `content` is `[left, top, right, bottom]`. Use positive, ordered, non-overlapping
93
+ integer ranges inside the cell. Each configured axis must overlap its content area. Put corners,
94
+ borders and pointers outside stretch regions. Tileflow scales metadata for the 2x index, verifies
95
+ both densities and rejects layout on patterns. SDF backgrounds still need the six-pixel guard.
96
+
97
+ Enable the existing map `icon.textFit` control (`width`, `height` or `both`) and set
98
+ `icon.textFitPadding` in top/right/bottom/left order. For centered price labels, also set
99
+ `text.anchor: 'center'` and `text.offset: [0, 0]`; the POI defaults place labels beside icons.
100
+ This style fragment belongs inside the selected category's `styles` entry:
101
+
102
+ ```ts
103
+ {
104
+ icon: {image: fixed('price', {reason: 'Price background'}), textFit: 'both', textFitPadding: [4, 6, 4, 6]},
105
+ text: {anchor: 'center', offset: [0, 0]},
106
+ }
107
+ ```
108
+
109
+ Import `fixed` from `@tileflow/core`. The same layer retains label coupling, collision priority and
110
+ interaction identity. Text fitting does not happen merely because an asset has metadata. Explicit
111
+ new local artwork replaces the shared asset's layout; omitted metadata never inherits stale bounds.
112
+
113
+ ## Inspect and distribute the result
114
+
115
+ Packages with SDF appearance or layout use `tileflow-icon-package-v2`; composition receipts use
116
+ `tileflow-icon-composition-v2` when an effective winner has either capability. Paired sprite JSON entries contain
117
+ `sdf: true` and verified `tileflow` appearance metadata. Defaults and layout participate in revision identity
118
+ and diffs even when pixel hashes match. Metadata-free RGBA packages and receipts keep their v1 identity.
119
+ Old readers reject v2; upgrade every producing and consuming package together.
120
+
121
+ `tileflow icons list --json` includes `appearances` and `layouts` for effective icons. Diff reports show default appearance;
122
+ downloadable PNGs remain the original distance fields. Bare MapLibre consumes the standard sprite
123
+ and compiled paint without a recoloring adapter. Capture uses the same prepared assets.
124
+
125
+ Hosted admission, Dashboard and Static Maps require coordinated Platform adoption. Additional
126
+ Static Maps symbols use asset defaults and retain adjacent icon/label layout; base-map symbols use
127
+ compiled map paint and text fitting. Device support
128
+ requires separate Native qualification. This contract does not provide independently parameterized
129
+ multicolor SVG, COLRv1, `textFitWidth`/`textFitHeight` metadata or an icon editor.