@ngis/plugin-sdk 0.4.0 → 0.4.1

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.
@@ -99,6 +99,8 @@ export interface SceneLayerHighlight {
99
99
  allFeatures?: boolean;
100
100
  /** Only meaningful with `allFeatures`; rows unchecked after select-all. */
101
101
  excludedFeatureIds?: string[];
102
+ /** Optional MapLibre predicate narrowing an all-matching table selection. */
103
+ attributeFilter?: HighlightFilterExpression;
102
104
  }
103
105
  export type SceneLayerStyle = Record<string, unknown>;
104
106
  export type SceneLayerRenderMode = "maplibre" | "custom-raster-color" | "custom-flow-field";
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Stage 25 (F25.1) — the frontend's CRS domain model.
3
+ *
4
+ * **Authored by the W1 kickoff: TYPES AND CONSTANTS ONLY. Read-only to every W1 session
5
+ * except S25-13, which owns this file and adds the function bodies below.**
6
+ *
7
+ * This is the TypeScript leg of the Stage 25 twin triple. The other two are `CrsDto`
8
+ * (ngis-file, `io.ngis.file.dto`) and `crs_ref.py` (ngis-geoanalysis). All three mirror
9
+ * `ngis-protos/crs.proto`'s `CrsRef` and the fixtures in `__fixtures__/crs/`, which are
10
+ * byte-identical across four copies (F25.14 — compare with `diff --strip-trailing-cr`,
11
+ * never an on-disk hash: the two repos normalise line endings differently).
12
+ *
13
+ * ## G-CRS lives here
14
+ *
15
+ * `key` is the identity and the only field a caller must round-trip. An absent CRS is
16
+ * `null` — never `key: ""`, never `epsg: 0`. Zero is not "unknown", it is absent. The whole
17
+ * stage exists because `crs.py:78` silently turned an undetectable vector CRS into 4326 and
18
+ * nothing downstream could tell the difference.
19
+ *
20
+ * @see docs/projection_management_stage/README.md — F25.1, F25.13 (frozen names)
21
+ */
22
+ /** How a CRS came to be attached to a dataset. Never absent on a stored CRS (G-CRS). */
23
+ export type NgisCrsSource = "DETECTED" | "SPEC_DEFAULT" | "USER_ASSIGNED" | "DERIVED";
24
+ /**
25
+ * The display accessor over `NgisCrsRef.geographic`. The boolean is right on the wire, but
26
+ * `!ref.geographic` reads as a double negative at every UI call site (F25.13).
27
+ */
28
+ export type NgisCrsKind = "geographic" | "projected";
29
+ /**
30
+ * What a tool requires of its inputs' CRS.
31
+ *
32
+ * `"uniform"` is **not** a member — D25-5 struck it. `"geographic"` is hand-authored only.
33
+ */
34
+ export type NgisCrsRequirement = "any" | "projected" | "geographic";
35
+ /**
36
+ * Where a tool's `crsRequirement` came from. A `CLASSIFIER` requirement may only ever raise
37
+ * an **advisory** issue (R10 / D25-5 as amended): no classifier rule measured better than
38
+ * 0.800 precision, and a false `"projected"` refuses a run the user is entitled to make.
39
+ */
40
+ export type NgisCrsRequirementSource = "HAND_AUTHORED" | "CLASSIFIER";
41
+ /** One coordinate reference system. `key` is `"EPSG:<code>"` or `"custom:<ulid>"`. */
42
+ export interface NgisCrsRef {
43
+ /** The identity, and the only field a caller must round-trip. */
44
+ readonly key: string;
45
+ /** Set iff `key` is an `EPSG:` ref. Never `0` — an absent CRS is a `null` ref. */
46
+ readonly epsg?: number;
47
+ /** Always on a resolved custom ref; on an EPSG ref only when asked. */
48
+ readonly wkt2?: string;
49
+ /** e.g. `"WGS 84 / UTM zone 50N"`. Present iff the oracle has resolved the ref. */
50
+ readonly name?: string;
51
+ readonly geographic?: boolean;
52
+ /** `"metre"` | `"degree"` | `"foot"` | `"us-foot"` | … */
53
+ readonly unit?: string;
54
+ readonly source?: NgisCrsSource;
55
+ }
56
+ /** Blocking refuses the submit with 400; advisory never gates Run (R9). */
57
+ export type NgisValidationIssueSeverity = "blocking" | "advisory";
58
+ /**
59
+ * Open by design — a later stage may add a code, and an unknown code must render as its
60
+ * `messageKey` rather than being dropped (the `ExtensionSettingsForm` rule, F14.9).
61
+ */
62
+ export type NgisCrsIssueCode = "CRS_UNKNOWN" | "CRS_PROJECTED_REQUIRED" | "CRS_GEOGRAPHIC_REQUIRED" | "CRS_REPROJECT_NOTICE" | "CRS_UNSUPPORTED_BY_PROVIDER" | (string & {});
63
+ /** The one-click remedy attached to a blocking issue. Carries the `NgisPickerNotice.action`. */
64
+ export interface NgisValidationIssueFix {
65
+ /** The parameter the fix sets — e.g. `"ngis:outputCrs"`. */
66
+ readonly param: string;
67
+ readonly value: string;
68
+ /** Client-localised under `Crs.issues.fix.*`. */
69
+ readonly labelKey: string;
70
+ readonly params?: Readonly<Record<string, string | number>>;
71
+ }
72
+ /**
73
+ * A typed, per-parameter validation issue (F25.6). **Additive** to F14.9's string arrays:
74
+ * `missingParameters` / `parameterErrors` / `outputErrors` / `warnings` keep their meaning.
75
+ *
76
+ * `param` routes it to that field's `notices`; `port` routes it to that input port.
77
+ */
78
+ export interface NgisValidationIssue {
79
+ readonly code: NgisCrsIssueCode;
80
+ readonly severity: NgisValidationIssueSeverity;
81
+ /** Client-localised under `Crs.issues.*`. */
82
+ readonly messageKey: string;
83
+ readonly params?: Readonly<Record<string, string | number>>;
84
+ readonly param?: string;
85
+ readonly port?: string;
86
+ readonly fix?: NgisValidationIssueFix;
87
+ readonly crs?: {
88
+ readonly input?: NgisCrsRef;
89
+ readonly suggested?: NgisCrsRef;
90
+ readonly compute?: NgisCrsRef;
91
+ };
92
+ }
93
+ /** One input as the compiled plan sees it. */
94
+ export interface NgisCrsPlanInput {
95
+ readonly port: string;
96
+ /** The CRS the stored dataset is in. */
97
+ readonly from: string;
98
+ /** The CRS it is staged into for this run. Equal to `from` when no staging is needed. */
99
+ readonly to: string;
100
+ /** Written back as a cached export variant above `STAGING_CACHE_MIN_BYTES` (20 MiB). */
101
+ readonly cacheable?: boolean;
102
+ }
103
+ /**
104
+ * The server-compiled CRS plan (F25.4). Every executor path executes **this** rather than
105
+ * re-deriving a CRS from dataset metadata or a fallback — that is G-U.
106
+ *
107
+ * `null` whenever a blocking issue was raised (R9).
108
+ */
109
+ export interface NgisCrsPlan {
110
+ /** The CRS the run computes in. */
111
+ readonly computeCrs: string;
112
+ /**
113
+ * The integer that reaches the PostGIS SQL, the `-t_srs` of ogr2ogr/gdalwarp and the
114
+ * iObjects `inputSrids`: the EPSG code, or the registry's `spatial_ref_sys` srid
115
+ * (>= 900001) for a custom CRS. No executor re-derives it (R7).
116
+ */
117
+ readonly pgSrid: number;
118
+ /** The CRS the result is registered in. */
119
+ readonly resultCrs: string;
120
+ /** Always `"DERIVED"` when present (R6). */
121
+ readonly resultCrsSource?: NgisCrsSource;
122
+ /** One entry per input that had to be reprojected for the run. Staging never mutates storage. */
123
+ readonly stage: readonly NgisCrsPlanInput[];
124
+ }
125
+ /** The one platform projection parameter, injected into every tool's UI schema (F25.4). */
126
+ export declare const NGIS_OUTPUT_CRS_PARAM = "ngis:outputCrs";
127
+ /** `x-role` of a synthetic platform parameter. `w2-fixture.test.ts:252` excludes these. */
128
+ export declare const NGIS_PLATFORM_ROLE = "platform";
129
+ /**
130
+ * The identifier half of the chip, alone — `"EPSG:32650"`, `"Custom"`, or the
131
+ * NEEDS_CRS text. It exists so the chip, the table cell and the picker footer
132
+ * cannot drift: `formatCrs` is this plus the name, never a second spelling.
133
+ *
134
+ * **G-CRS.** A bare integer is never returned. A key that *is* a bare integer —
135
+ * which is what a server still on the old numeric `srid` would send — is
136
+ * rendered `EPSG:<n>`, because the one thing the user must never see is a naked
137
+ * number they cannot tell from a feature count.
138
+ */
139
+ export declare function formatCrsKey(key: string | null, needsCrsText?: string): string;
140
+ /**
141
+ * The full chip string — `"EPSG:32650 · WGS 84 / UTM zone 50N"`, `"Custom · <name>"`.
142
+ *
143
+ * **This is where G-CRS lives on the client.** It never prints a bare integer,
144
+ * and `formatCrs(null)` returns the NEEDS_CRS text — never `"-"`, and never
145
+ * `"EPSG:4326"`, which is the silent default the whole stage exists to remove.
146
+ * A ref whose oracle lookup has not happened yet has no `name`, and renders as
147
+ * its identifier alone rather than borrowing one.
148
+ */
149
+ export declare function formatCrs(ref: NgisCrsRef | null, needsCrsText?: string): string;
150
+ /**
151
+ * `true` only for a ref the oracle has resolved *and* reported as projected.
152
+ *
153
+ * An **unresolved** ref (no `geographic`) is `false` here, exactly as
154
+ * `crs_ref.py`'s `.projected` is — but it is never `true`, so "we have not asked
155
+ * yet" can never read as "this is safe to measure distance in". That asymmetry
156
+ * is G-U, restated in one predicate.
157
+ */
158
+ export declare function isProjectedCrs(ref: NgisCrsRef | null): boolean;
159
+ /**
160
+ * The display accessor over `NgisCrsRef.geographic` (F25.13). The boolean is
161
+ * right on the wire; `!ref.geographic` reads as a double negative at every UI
162
+ * call site, and a UI that has to negate to say "projected" eventually forgets to.
163
+ *
164
+ * Reports `"projected"` on exactly the refs {@link isProjectedCrs} accepts, so
165
+ * the two cannot drift. An unresolved ref reports `"geographic"` — the
166
+ * conservative half — and the chip draws it with the unknown sigil instead,
167
+ * because {@link crsUnitGlyph} has no unit to show.
168
+ */
169
+ export declare function crsKind(ref: NgisCrsRef): NgisCrsKind;
170
+ /**
171
+ * The unit sigil's glyph: `°` · `m` · `ft` · `?`.
172
+ *
173
+ * Exported because the sigil is drawn in five places (full chip, compact chip,
174
+ * picker trigger, list row, "needs projection" filter) and an inlined map would
175
+ * drift between them. `"?"` is the honest answer for both an absent ref and a
176
+ * unit no client mapping knows — a guessed glyph on an unknown unit is exactly
177
+ * the silent default this stage removes.
178
+ */
179
+ export declare function crsUnitGlyph(ref: NgisCrsRef | null): string;
180
+ /**
181
+ * The wire value — the ref's `key`, or `null`. This is what the `crs` widget
182
+ * puts in the params payload and what `PUT /api/datasets/{id}/crs` echoes.
183
+ *
184
+ * An empty-string key is `null`, not `""`: `key: ""` is not a CRS, and letting
185
+ * it through is how an absent CRS gets laundered into a present one.
186
+ */
187
+ export declare function crsKeyOf(ref: NgisCrsRef | null): string | null;
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Stage 29 (F29.1 / F29.4 / F29.5 / F29.7 / F29.9) — the frontend's dataset-kind
3
+ * domain model.
4
+ *
5
+ * **This module is the one place in the client that names a dataset kind.** It is
6
+ * the F25.1 `crs.ts` precedent applied to the second vocabulary this platform
7
+ * spelled seven ways: a dataset's kind was spelled seven different ways, in three
8
+ * casings, derived from four different source fields, and no two of the seven had
9
+ * the same membership. After Stage 29 there is **one** derivation chain — the
10
+ * server's `datasetType` — and `G-KIND` is the rule that nothing outside a
11
+ * registry module branches on a kind id.
12
+ *
13
+ * Every shape below mirrors the frozen fixtures in
14
+ * `src/domain/models/__fixtures__/dataset-kind/`, which are byte-identical copies
15
+ * of `ngis-backend/stages/29-resource-plane/fixtures/` (F29.10 — compare with
16
+ * `diff --strip-trailing-cr`, never an on-disk hash: the two repos normalise line
17
+ * endings differently).
18
+ *
19
+ * ## The three-state rule lives here
20
+ *
21
+ * {@link NgisKindCapabilities} is a `Partial<>` and that is **load-bearing**
22
+ * (F29.4, inheriting F25.8). A flag **absent** from the block means *the server
23
+ * has not said*; `{supported: false, reasonKey}` means *this kind cannot*;
24
+ * `{supported: true, reasonKey: null}` means *it can*. A client that coalesces
25
+ * absent into `false` has re-introduced exactly the defect Stage 26 paid to
26
+ * learn — which is why {@link datasetKindCapability} is the only supported way to
27
+ * read a flag and why it returns `undefined` rather than fabricating a record.
28
+ *
29
+ * @see ngis-backend/stages/29-resource-plane/F29-contract.md — F29.1, F29.4,
30
+ * F29.5, F29.7, F29.9 (frozen names)
31
+ * @see docs/resource_plane_stage/README.md — the frontend plan of record
32
+ */
33
+ /**
34
+ * **The one client dataset-kind type (F29.1).** The wire field keeps its shipped
35
+ * name, `datasetType`, and its value is a registered kind id in UPPERCASE
36
+ * `SNAKE_CASE`.
37
+ *
38
+ * **The union is OPEN, and that is the whole point.** `D29-1` makes a kind
39
+ * *registered data*, so a client shipped today must render a kind registered
40
+ * tomorrow. The three named members exist for autocomplete and for the
41
+ * exhaustiveness of *today's* switch statements — they are **not** a membership
42
+ * assertion, and **no test may assert that the set of kinds equals those three**.
43
+ * (Stage 15's `Market.*` sub-namespace lesson, which cost a session on three
44
+ * consecutive waves: a closed-world assertion breaks on every sibling merge.)
45
+ *
46
+ * `null` — not a member of this union — keeps exactly one meaning after F29.3:
47
+ * *no kind resolved* (`status: "NEEDS_KIND"`). `"GENERIC"` is gone from the wire;
48
+ * a non-geo file reports `datasetType: null` with `status: "N/A"`.
49
+ */
50
+ export type NgisDatasetKind = "VECTOR" | "RASTER" | "FLOW_FIELD" | (string & {});
51
+ /**
52
+ * **The six capability flags, frozen (F29.4).**
53
+ *
54
+ * `renderable` is deliberately **not** a member, and that is not an oversight:
55
+ * `renderable` is a server-computed property of a dataset *row*
56
+ * (`ready && geometryType != null && capabilities.tileable.supported === true`,
57
+ * F29.6), never a declared capability of a *kind*. Stage 28 ships a
58
+ * `capabilities.renderable` on a **symbol template**, an unrelated object that
59
+ * happens to share the field name; keeping `renderable` out of this union is what
60
+ * stops a symbol payload from satisfying a dataset capability block
61
+ * (`S29-V073`).
62
+ */
63
+ export type NgisCapabilityFlag = "tileable" | "analyzable" | "shareable" | "copyable" | "quotaCountable" | "exportable";
64
+ /** The six flags in the F29.4 order. */
65
+ export declare const NGIS_CAPABILITY_FLAGS: readonly NgisCapabilityFlag[];
66
+ /**
67
+ * One declared capability of a kind (F29.4 / F29.5).
68
+ *
69
+ * **A boolean cannot say why a control is greyed**, which is why this is a record
70
+ * and never a bare boolean. `reasonKey` is an **i18n key the client localises** —
71
+ * the server never sends display text for a capability refusal — and `params`
72
+ * carries its interpolation values. The in-repo precedent is shipped:
73
+ * `ValidationIssue.message_key` / `params_json` and `IssueFix.label_key` /
74
+ * `params_json` (`ngis-protos/geoanalysis.proto:553-572`); `IssueFix.params_json`
75
+ * exists *because* a key without its interpolation values made the gRPC and REST
76
+ * twins of one fixture disagree.
77
+ */
78
+ export interface NgisCapability {
79
+ supported: boolean;
80
+ /** Required when `supported === false`; **must** be `null` when `true`. */
81
+ reasonKey: string | null;
82
+ /** Interpolation values for `reasonKey`; `{}` when the key takes none. */
83
+ params: Record<string, unknown>;
84
+ }
85
+ /**
86
+ * A kind's declared capability block (F29.4).
87
+ *
88
+ * **`Partial<>` is load-bearing — there are three states, not two.** An absent
89
+ * flag means *the server has not said*, and it must never render as
90
+ * *this kind cannot*. Read a flag with {@link datasetKindCapability} and nothing
91
+ * else.
92
+ *
93
+ * `{}` is the **empty** block and differs from an absent block: it means *no kind
94
+ * resolved, therefore no declared capabilities* (`fx-ds-0003`, `status:
95
+ * "NEEDS_KIND"`). Every flag is then in its third state.
96
+ *
97
+ * The block describes the **kind**, not the row's readiness. A `NEEDS_CRS` vector
98
+ * is still `tileable: true`; it is `ready: false` that stops it rendering.
99
+ * Conflating the two is how `renderable` came to name three kinds.
100
+ */
101
+ export type NgisKindCapabilities = Partial<Record<NgisCapabilityFlag, NgisCapability>>;
102
+ /**
103
+ * Read one declared capability — **the only supported way to read a flag.**
104
+ *
105
+ * Returns `undefined` for an absent flag and **never fabricates
106
+ * `{supported: false}`**. That is F29.4's three-state rule enforced in one
107
+ * function so no call site can get it wrong; a caller that wants a boolean must
108
+ * decide, explicitly and at its own site, what *"the server has not said"* means
109
+ * for the control it is drawing.
110
+ *
111
+ * The flag argument is typed {@link NgisCapabilityFlag}, so
112
+ * `datasetKindCapability(caps, "renderable")` is a **compile error**
113
+ * (`S29-V073`).
114
+ */
115
+ export declare function datasetKindCapability(caps: NgisKindCapabilities | null | undefined, flag: NgisCapabilityFlag): NgisCapability | undefined;
116
+ /**
117
+ * Why a dataset's time axis is or is not known (F29.7, `D29-3` generalised from
118
+ * CRS).
119
+ *
120
+ * There are **four** states, and the fourth is the `temporalExtent` field itself
121
+ * being `null`, which means *the server has not said* (F25.8) — distinct from
122
+ * `NOT_APPLICABLE`, which means *this kind has no time axis*.
123
+ */
124
+ export type NgisTemporalExtentState = "RESOLVED" | "UNRESOLVABLE" | "NOT_APPLICABLE";
125
+ /**
126
+ * A dataset's temporal extent (F29.7).
127
+ *
128
+ * **Never a bare range.** A defaulted range — epoch-to-now, the file's mtime,
129
+ * anything — is a contract violation, not a fallback (`D29-3`). `fx-ds-0005` is
130
+ * the fixture that proves it: `start` and `end` are `null` **and** `state` says
131
+ * why.
132
+ *
133
+ * What each state renders as (F29.7, frozen because `D29-3` forbids a default):
134
+ *
135
+ * | State | Renders as |
136
+ * |---|---|
137
+ * | the field is `null` | the temporal row is **not rendered at all** |
138
+ * | `NOT_APPLICABLE` | the temporal row is **not rendered at all** |
139
+ * | `UNRESOLVABLE` | the row renders, with the localised `reasonKey` where the range would be — never a dash, never an empty range |
140
+ * | `RESOLVED` | the range |
141
+ *
142
+ * `null` and `NOT_APPLICABLE` render identically **on purpose**: there is nothing
143
+ * useful to say to a user in either case. They stay distinct **on the wire**,
144
+ * because a test must be able to tell them apart and the array stage will need to.
145
+ */
146
+ export interface NgisTemporalExtent {
147
+ state: NgisTemporalExtentState;
148
+ /** ISO 8601; non-null iff `state === "RESOLVED"`. */
149
+ start: string | null;
150
+ /** ISO 8601; non-null iff `state === "RESOLVED"`. */
151
+ end: string | null;
152
+ /** An i18n key; non-null iff `state === "UNRESOLVABLE"`. */
153
+ reasonKey: string | null;
154
+ params: Record<string, unknown>;
155
+ }
156
+ /**
157
+ * Where a dataset came from (F29.7, `D29-5`).
158
+ *
159
+ * `authority` is `AUTHORITY` or `DERIVED`, never both. A `DERIVED` dataset
160
+ * carries `rebuildableFrom`; an `AUTHORITY` one carries `null` there.
161
+ *
162
+ * `sourceFileId` is **not** unique across datasets: `fx-ds-0005` and `fx-ds-0006`
163
+ * share one. That is `D29-2` — one file, many datasets.
164
+ */
165
+ export interface NgisDatasetLineage {
166
+ authority: "AUTHORITY" | "DERIVED";
167
+ sourceFileId: string | null;
168
+ derivedFromFileId: string | null;
169
+ producedByTaskId: string | null;
170
+ /** Non-null iff `authority === "DERIVED"`. */
171
+ rebuildableFrom: string | null;
172
+ }
173
+ /**
174
+ * The array plane's placeholder (F29.7, `D29-S1`).
175
+ *
176
+ * **Deliberately typed `null` and nothing else.** `dimensions` is declared on the
177
+ * wire so the array stage needs no second migration and no second contract
178
+ * freeze, and it is `null` for every kind Stage 29 registers. Typing it as the
179
+ * `null` literal makes *"a session that builds a time slider off it is out of
180
+ * contract"* a compile error rather than a code-review note; widening this alias
181
+ * is the array stage's job and nobody else's.
182
+ */
183
+ export type NgisDatasetDimensions = null;
184
+ /**
185
+ * **The dataset spine (F29.7), as one nominal shape.**
186
+ *
187
+ * *Recorded extra export, not in F29.9's table — see the S29-14 session note.*
188
+ * It exists for `S29-V073`: `renderable` and `ready` are members of a record that
189
+ * also requires `datasetType`, `status` and `lineage`, so a Stage 28 **symbol
190
+ * template** — which carries `capabilities.renderable` and none of those — cannot
191
+ * resolve against the client's **dataset** `renderable`. Two unrelated objects
192
+ * share one field name; this type and {@link NgisCapabilityFlag}'s exclusion of
193
+ * `renderable` are the two independent guards that keep them apart.
194
+ *
195
+ * `ready` and `renderable` are server-computed and keep their names (F29.6):
196
+ *
197
+ * ```
198
+ * ready = status ∈ kind.readyStates
199
+ * renderable = ready && geometryType != null && capabilities.tileable.supported === true
200
+ * ```
201
+ *
202
+ * Both are optional, and `undefined` ("the server has not said") must never render
203
+ * the same as `false` ("this kind cannot be tiled") — `SceneWorkspaceLayers.tsx`'s
204
+ * `resolveLayerCrs` is the in-repo precedent and its comment states the rule.
205
+ */
206
+ export interface NgisDatasetSpine {
207
+ /** `null` means *no kind resolved* — the only meaning `null` carries (F29.3). */
208
+ datasetType: NgisDatasetKind | null;
209
+ /**
210
+ * **An OPEN vocabulary (F29.7 R2).** A consumer that does not recognise a
211
+ * status renders the **raw token**, never `UNKNOWN`. The measured hazard is
212
+ * live: `ngis-file`'s `FileService` normalises a status through a whitelist
213
+ * whose default arm is `UNKNOWN`, so a `NEEDS_CRS` dataset already reports as
214
+ * `UNKNOWN` on the library listing — *"we know exactly what is wrong with it"*
215
+ * collapsed into *"we do not know"*, which is the very thing `D29-3` forbids.
216
+ */
217
+ status: string;
218
+ capabilities?: NgisKindCapabilities;
219
+ temporalExtent?: NgisTemporalExtent | null;
220
+ dimensions?: NgisDatasetDimensions;
221
+ lineage?: NgisDatasetLineage | null;
222
+ ready?: boolean;
223
+ renderable?: boolean;
224
+ }
225
+ /** The minimum an issue must carry for {@link isKindAdaptableIssue} to judge it. */
226
+ export interface NgisIssueCodeCarrier {
227
+ code?: string | null;
228
+ }
229
+ /**
230
+ * F29.8's rule A1, in one place: is this the **kind** adaptation, or Stage 26's
231
+ * **CRS** one?
232
+ *
233
+ * `"ADAPTABLE"` is the same verdict token for both. **Only the issue code
234
+ * distinguishes them** — a test that asserts only `verdict === "ADAPTABLE"`
235
+ * passes on a CRS adaptation and proves Stage 26's code path, not this stage's.
236
+ *
237
+ * The client never re-classifies a server verdict or severity (`G-U`): this is a
238
+ * predicate over a code the server sent, and nothing more.
239
+ */
240
+ export declare function isKindAdaptableIssue(issue: NgisIssueCodeCarrier | null | undefined): boolean;
241
+ /**
242
+ * The kind → scene-layer resource-kind projection.
243
+ *
244
+ * *Recorded extra export, not in F29.9's table — see the S29-14 session note.*
245
+ * It is what F29.1 row 6 means by *"S29-14 delegates it to `NgisDatasetKind`"*:
246
+ * `SceneLayerResourceKind` used to be reached by `geometry.includes("raster")`, a
247
+ * substring match on a geometry string, and that is a guess. The spine now
248
+ * carries the answer, and this is the one place the answer is projected onto the
249
+ * two-value vocabulary the scene-layer type table is written in.
250
+ *
251
+ * Returns `null` for a kind that is neither — including an unregistered kind the
252
+ * client has never seen, which is `NgisDatasetKind` being open — so a caller must
253
+ * decide what *"I cannot place this kind"* means rather than being handed
254
+ * `"vector"` by default.
255
+ */
256
+ export declare function sceneLayerResourceKindOfDatasetKind(kind: NgisDatasetKind | null | undefined): "vector" | "raster" | null;
@@ -0,0 +1,32 @@
1
+ /** Canonical, per-scene-layer presentation state for a FLOW_FIELD resource. */
2
+ export interface FlowFieldColorRamp {
3
+ /** A UI hint only. `colors` is the renderer authority. */
4
+ presetId: string;
5
+ /** Low-speed to high-speed colors. */
6
+ colors: string[];
7
+ }
8
+ export type FlowFieldSpeedRange = {
9
+ mode: "data";
10
+ } | {
11
+ mode: "manual";
12
+ min: number;
13
+ max: number;
14
+ };
15
+ export interface FlowFieldStyle {
16
+ renderer: "flow-field";
17
+ styleVersion: 1;
18
+ /** Compatibility-only identity for scenes created before SceneResource links. */
19
+ flowFieldId?: string;
20
+ opacity: number;
21
+ colorRamp: FlowFieldColorRamp;
22
+ speedRange: FlowFieldSpeedRange;
23
+ speedFactor: number;
24
+ particleCount: number;
25
+ lineWidth: number;
26
+ trailLength: number;
27
+ frameDurationMs: number;
28
+ paused: boolean;
29
+ maxParticleAge: number;
30
+ dropRate: number;
31
+ dropRateBump: number;
32
+ }
@@ -1,10 +1,35 @@
1
- export declare const SCENE_LAYER_TYPE_IDS: readonly ["MAPLIBRE_FILL", "MAPLIBRE_LINE", "MAPLIBRE_CIRCLE", "MAPLIBRE_RASTER", "CUSTOM_FLOW_FIELD", "DECK_MVT_POLYGON", "DECK_MVT_EXTRUDED_POLYGON", "DECK_COLUMN", "DECK_HEATMAP", "DECK_HEXAGON", "DECK_MVT_LINE", "DECK_MVT_POINT", "SYMBOL_POINT", "SYMBOL_LINE", "SYMBOL_POLYGON"];
1
+ import type { sceneLayerResourceKindOfDatasetKind } from "./dataset-kind";
2
+ export declare const SCENE_LAYER_TYPE_IDS: readonly ["MAPLIBRE_FILL", "MAPLIBRE_LINE", "MAPLIBRE_CIRCLE", "MAPLIBRE_RASTER", "CUSTOM_FLOW_FIELD", "DECK_MVT_POLYGON", "DECK_MVT_EXTRUDED_POLYGON", "DECK_COLUMN", "DECK_HEATMAP", "DECK_HEXAGON", "DECK_MVT_LINE", "DECK_MVT_POINT", "SYMBOL_POINT", "SYMBOL_LINE", "SYMBOL_POLYGON", "PLOTTING_SYMBOL", "DYNAMIC_LINE"];
2
3
  export type SceneLayerTypeId = (typeof SCENE_LAYER_TYPE_IDS)[number];
3
4
  export type LegacySceneLayerTypeId = "VECTOR" | "RASTER";
4
5
  export type SceneLayerRendererKind = "maplibre" | "deck" | "custom";
5
- export type SceneLayerResourceKind = "vector" | "raster";
6
+ /**
7
+ * **RETIRED as an independent vocabulary (F29.1 row 6).**
8
+ *
9
+ * It was one of the seven spellings of a dataset's kind, and the worst-founded
10
+ * of them: nothing declared it, it was *reached* by `geometry.includes("raster")`
11
+ * — a substring match on a geometry string — so a resource whose geometry string
12
+ * was merely non-empty classified as `"vector"`. The spine now carries the
13
+ * answer, and the one projection from it lives in
14
+ * {@link sceneLayerResourceKindOfDatasetKind}.
15
+ *
16
+ * The **name and the two lowercase values stay**, because this is the vocabulary
17
+ * the layer-type table below is written in and renaming it is out of contract
18
+ * (F29.1's rule for row 1 applies equally here). What is retired is its
19
+ * *derivation*: it is now defined as the return of the one projection, so it can
20
+ * never again drift from {@link NgisDatasetKind} by being re-typed by hand, and
21
+ * widening it is a change to that function and nowhere else.
22
+ *
23
+ * It is a **layer-compatibility** vocabulary, not a kind: it answers *"which
24
+ * renderers can draw this"*, which is why it is deliberately narrower than the
25
+ * open kind union — a kind registered tomorrow projects to `null` here and the
26
+ * caller must decide what that means, rather than being handed `"vector"`.
27
+ */
28
+ export type SceneLayerResourceKind = NonNullable<ReturnType<typeof sceneLayerResourceKindOfDatasetKind>>;
6
29
  export type SceneLayerGeometryFamily = "point" | "line" | "polygon" | "raster";
7
- export type SceneLayerStyleKind = "maplibre-fill" | "maplibre-line" | "maplibre-circle" | "maplibre-raster" | "custom-flow-field" | "deck-mvt-polygon" | "deck-mvt-extruded-polygon" | "deck-column" | "deck-heatmap" | "deck-hexagon" | "deck-mvt-line" | "deck-mvt-point" | "symbol";
30
+ export type SceneLayerStyleKind = "maplibre-fill" | "maplibre-line" | "maplibre-circle" | "maplibre-raster" | "custom-flow-field" | "deck-mvt-polygon" | "deck-mvt-extruded-polygon" | "deck-column" | "deck-heatmap" | "deck-hexagon" | "deck-mvt-line" | "deck-mvt-point" | "symbol"
31
+ /** S28: no-resource Canvas runtime, deliberately not a Stage 25 symbol style. */
32
+ | "plotting" | "dynamic-line";
8
33
  export interface SceneLayerTypeDefinition {
9
34
  id: SceneLayerTypeId;
10
35
  label: string;
@@ -17,6 +17,7 @@
17
17
  * unrelated store write (a loading flag, a style draft) costs one reference
18
18
  * compare rather than a re-derivation and a notification.
19
19
  */
20
+ import { type NgisDatasetKind } from "../../domain/models/dataset-kind";
20
21
  import type { NgisUnsubscribe } from "../map-api";
21
22
  import { type NgisContextValue, type NgisResourceType } from "./keys";
22
23
  import type { NgisContextStore } from "./store";
@@ -41,8 +42,23 @@ export interface SceneContextResource {
41
42
  * `"Raster"` for a RASTER dataset, the vector geometry type for a VECTOR
42
43
  * one, and `null` for everything else — which is the signal
43
44
  * `classifySceneResourceType` reads.
45
+ *
46
+ * **Stage 29 (F29.1 row 6):** it is no longer the *first* signal. See
47
+ * {@link SceneContextResource.datasetType}.
44
48
  */
45
49
  geometryType?: string | null;
50
+ /**
51
+ * `SceneResourceDto.datasetType` — the registered kind id, and **the answer
52
+ * this publisher should be reading** (F29.1 row 6).
53
+ *
54
+ * Optional because the producer is `createSceneStore()`
55
+ * (`src/store/scene-store.ts`), which `S29-15` owns and which does not carry it
56
+ * onto this slice yet. Until it does, {@link classifySceneResourceType} falls
57
+ * back to the geometry-string guess — absent is *"the store has not said"*, not
58
+ * *"this resource has no kind"*, so the fallback is a third state and not a
59
+ * default.
60
+ */
61
+ datasetType?: NgisDatasetKind | null;
46
62
  }
47
63
  export interface SceneContextScene {
48
64
  id: string;
@@ -79,6 +95,27 @@ export interface AuthContextSlice {
79
95
  * → `"vector"`, absent → `"file"` (which covers `GENERIC` and a dataset whose
80
96
  * metadata fetch failed). The authoritative join lives in X2's
81
97
  * `host.resources` facet; wiring this publisher to it is backlog row **V-21**.
98
+ *
99
+ * ## Classifier disposition (F29.1 row 6): DELEGATES, with a fallback
100
+ *
101
+ * The `geometry.includes("raster")` line below is **the substring match F29.1
102
+ * retires** — a guess about a dataset's kind made from a geometry string, which
103
+ * answered `"vector"` for anything non-empty that did not contain the letters
104
+ * `raster`. The kind id now answers first, through
105
+ * {@link sceneLayerResourceKindOfDatasetKind}, which is the one place the
106
+ * projection lives.
107
+ *
108
+ * The guess survives **only** as the `datasetType`-absent arm, because the
109
+ * producer of this slice is `src/store/scene-store.ts` — `S29-15`'s file, not
110
+ * this session's — so the field cannot be populated here. When `S29-15` carries
111
+ * it onto the slice, every branch below the first becomes unreachable for an
112
+ * enriched scene and the guess can be deleted outright. Carried as `S29-V108`.
113
+ *
114
+ * A kind that projects to neither `vector` nor `raster` — including a kind
115
+ * registered after this client shipped — falls through to the geometry arm
116
+ * rather than to `"file"`, because an unregistered *kind* is not evidence that
117
+ * the resource has no *geometry*; that is the same absent-is-not-false rule
118
+ * F29.4 states for capability flags.
82
119
  */
83
120
  export declare function classifySceneResourceType(resource: SceneContextResource): NgisResourceType;
84
121
  /**