@ngis/plugin-ui 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.
Files changed (42) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.mjs +1 -1
  4. package/dist/src/api/analysis.d.ts +38 -1
  5. package/dist/src/api/crs.d.ts +110 -0
  6. package/dist/src/api/resources.d.ts +174 -10
  7. package/dist/src/api/scenes.d.ts +103 -4
  8. package/dist/src/api/workflows.d.ts +10 -1
  9. package/dist/src/components/plugin-ui/ChartCanvas.d.ts +3 -0
  10. package/dist/src/components/plugin-ui/ParamForm.d.ts +67 -5
  11. package/dist/src/components/plugin-ui/PickerField.d.ts +17 -0
  12. package/dist/src/components/plugin-ui/RunButton.d.ts +8 -0
  13. package/dist/src/components/plugin-ui/crs-issue-messages.d.ts +157 -0
  14. package/dist/src/components/plugin-ui/flow-ports.d.ts +17 -0
  15. package/dist/src/components/plugin-ui/param-widgets.d.ts +2 -2
  16. package/dist/src/components/plugin-ui/picker-model.d.ts +49 -0
  17. package/dist/src/components/plugin-ui/run-data.d.ts +32 -4
  18. package/dist/src/components/plugin-ui/w2-contracts.d.ts +61 -0
  19. package/dist/src/components/plugin-ui/w3-contracts.d.ts +16 -1
  20. package/dist/src/components/resources/CrsPicker/CrsChip.d.ts +60 -0
  21. package/dist/src/components/resources/CrsPicker/CrsParamControl.d.ts +10 -0
  22. package/dist/src/components/resources/CrsPicker/CrsPicker.d.ts +41 -0
  23. package/dist/src/components/resources/CrsPicker/crs-recent.d.ts +40 -0
  24. package/dist/src/components/resources/CrsPicker/index.d.ts +14 -0
  25. package/dist/src/components/resources/resource-library-crs.d.ts +117 -0
  26. package/dist/src/components/ui/dialog.d.ts +25 -0
  27. package/dist/src/domain/algorithms/flow-field-style.d.ts +68 -0
  28. package/dist/src/domain/map/types.d.ts +2 -0
  29. package/dist/src/domain/models/crs.d.ts +187 -0
  30. package/dist/src/domain/models/dataset-kind.d.ts +256 -0
  31. package/dist/src/domain/models/flow-field-style.d.ts +32 -0
  32. package/dist/src/features/gis/components/GisAttributeTable.d.ts +11 -3
  33. package/dist/src/features/gis/hooks/useDatasetFeatureWindow.d.ts +5 -9
  34. package/dist/src/features/gis/lib/attributeFilters.d.ts +24 -0
  35. package/dist/src/sdk/facets/types.d.ts +73 -2
  36. package/dist/src/sdk/map-api.d.ts +5 -2
  37. package/dist/src/types/analysis.d.ts +111 -7
  38. package/dist/src/types/extensions.d.ts +43 -0
  39. package/dist/src/types/gis.d.ts +1 -1
  40. package/package.json +6 -37
  41. package/LICENSE +0 -13
  42. package/README.md +0 -25
@@ -1,3 +1,4 @@
1
+ import type { NgisDatasetKind } from "../domain/models/dataset-kind";
1
2
  import type { WorkflowDagConfig } from "../types/workflow";
2
3
  export type { WorkflowDagConfig, WorkflowDagEdge, WorkflowDagGraph, WorkflowDagNode, WorkflowDagScriptConfig, WorkflowDagScriptPort, WorkflowDagView, WorkflowDagViewEdge, WorkflowDagViewNode, WorkflowDagViewPosition, WorkflowDagViewport, } from "../types/workflow";
3
4
  export type WorkflowDefinitionStatus = "DRAFT" | "PUBLISHED";
@@ -37,7 +38,15 @@ export interface WorkflowInstanceOutputDto {
37
38
  nodeId: string;
38
39
  fileId: string;
39
40
  fileName?: string | null;
40
- datasetType?: string | null;
41
+ /**
42
+ * **Stage 29 (F29.1).** The registered kind id of the dataset this node
43
+ * produced — the same wire fact as `DatasetInfo.datasetType`, so it is typed
44
+ * from the one type rather than as a bare `string`. `null` means *no kind
45
+ * resolved*; `"GENERIC"` is gone. The union is **open** — a workflow may
46
+ * produce a kind registered after this client shipped, and the output row must
47
+ * still render.
48
+ */
49
+ datasetType?: NgisDatasetKind | null;
41
50
  primary?: boolean | null;
42
51
  extracted?: boolean | null;
43
52
  extractTarget?: WorkflowOutputTarget | null;
@@ -1,5 +1,8 @@
1
+ import * as echarts from "echarts/core";
1
2
  import type { NgisChartSpec } from "../../sdk/host-api";
2
3
  import { type NgisChartTheme } from "./stat-chart-model";
4
+ /** Shared, modular ECharts runtime for host-controlled chart canvases. */
5
+ export { echarts as ngisEcharts };
3
6
  export interface ChartCanvasProps {
4
7
  spec: NgisChartSpec;
5
8
  theme: NgisChartTheme;
@@ -8,7 +8,24 @@ export interface NgisParamFormChange {
8
8
  readonly submission: NgisRunSubmission | null;
9
9
  /** Client rules (F14.9) refuse the submit. */
10
10
  readonly blockedByClient: boolean;
11
- /** The server's blocking messages, if it has answered. */
11
+ /**
12
+ * **Stage 25 (F25.6/F25.7), additive.** The server's verdict refuses the
13
+ * submit — `isSubmitBlocked` over the last answer it gave.
14
+ *
15
+ * It exists because a blocking **issue** can arrive with every F14.9 string
16
+ * array empty (the frozen `validate-execution-issues.json` is exactly that
17
+ * shape), so a caller gating on {@link blockingMessages}` .length` alone would
18
+ * leave Run live on a run the server had already refused. `validateBeforeSubmit`
19
+ * still refuses at press time either way; this is what lets a caller disable
20
+ * the control *with the reason*, which is F25.7 flow 1's second half.
21
+ */
22
+ readonly blockedByServer: boolean;
23
+ /**
24
+ * The server's blocking messages, if it has answered — F14.9's string arrays
25
+ * **and**, since F25.6, the localised text of every blocking issue. An issue
26
+ * and a string are the same thing to a reader; keeping them in one list is
27
+ * what stops a caller having to know which channel a refusal came down.
28
+ */
12
29
  readonly blockingMessages: readonly string[];
13
30
  /** `warnings` — never blocks (F14.9). */
14
31
  readonly advisoryMessages: readonly string[];
@@ -19,15 +36,13 @@ export interface NgisParamFormChange {
19
36
  */
20
37
  readonly validateBeforeSubmit: () => Promise<boolean>;
21
38
  }
22
- export interface ParamFormProps {
39
+ export interface ParamFormBaseProps {
23
40
  /** The 0.4 facade — always the one `createGisPluginHostFactory` minted. */
24
41
  host: NgisHostAPI;
25
42
  /** The flow port id (F14.5, `record` out port). Also seeds the field DOM ids. */
26
43
  id?: string;
27
44
  /** Plugin-authored; rendered **literally** (F13.11). */
28
45
  label?: string;
29
- /** The tool whose UI schema this form is generated from. */
30
- toolId: string;
31
46
  /**
32
47
  * Param names the form does not render — already bound by a flow port
33
48
  * (F14.22's `hide={["ELEV"]}`). **An unknown name survives and is surfaced**,
@@ -60,4 +75,51 @@ export interface ParamFormProps {
60
75
  /** Reports the exact layer chosen from a duplicate-named resource option. */
61
76
  onSameNamedLayerSelect?: (layerId: string) => void;
62
77
  }
63
- export declare function ParamForm({ host, id, label, toolId, hide, inputs, widgets, values: seed, onChange, sceneId, className, contentClassName, onSameNamedLayerSelect, }: ParamFormProps): import("react/jsx-runtime").JSX.Element;
78
+ /** Bind either an implementation id (host-owned code) or a manifest alias (a bundle). */
79
+ export type ParamFormProps = (ParamFormBaseProps & {
80
+ /** Legacy host-owned spelling. Third-party bundles should use `dependency`. */
81
+ toolId: string;
82
+ dependency?: never;
83
+ }) | (ParamFormBaseProps & {
84
+ /** Locally unique `manifest.toolDependencies[].alias`. */
85
+ dependency: string;
86
+ toolId?: never;
87
+ });
88
+ export declare function ParamForm(props: ParamFormProps): import("react/jsx-runtime").JSX.Element;
89
+ /**
90
+ * **F25.4's fixed "Platform" register, above the vendor parameters.**
91
+ *
92
+ * The problem this solves is not decoration: `ngis:outputCrs` appears on *every*
93
+ * tool form and belongs to none of them. Left inline among the tool's own
94
+ * parameters it reads as one of them — a buffer setting the tool author chose —
95
+ * and the user has no way to learn that the same control is on the next form
96
+ * too, in the same place, meaning the same thing.
97
+ *
98
+ * ## A register change, not a box
99
+ *
100
+ * The device is a **caption with a rule running off it**, the way a bench sheet
101
+ * separates the instrument's settings from the bench's. A card, a tinted panel
102
+ * or an accent bar would all read as *alert* — the visual grammar this app
103
+ * already uses for a warning — and would announce something on every form
104
+ * forever that is only ever ordinary.
105
+ *
106
+ * So the boundary is one hairline, and it closes below the group as well as
107
+ * opening above it: the vendor list resumes with **no caption at all**, because
108
+ * it is the default register and naming it would imply a symmetry that does not
109
+ * exist. The eyebrow is `text-muted-foreground`, i.e. **quieter than the field
110
+ * labels beneath it**, which inverts the usual section-header hierarchy on
111
+ * purpose. Persistent chrome earns less emphasis than the thing it frames, and
112
+ * the boldness this stage has to spend is already spent — on `CrsChip`'s unit
113
+ * sigil, where round means degrees and square means metres.
114
+ *
115
+ * **Exported for one reason only:** this repo has no DOM in tests
116
+ * (`S25-V127`), so a server render cannot drive `ParamForm` past its
117
+ * `loadToolUiSchema` effect into the field list — the group's markup is
118
+ * unreachable through the form. Exporting the section makes it assertable
119
+ * directly. It is **not** on `plugin-ui`'s barrel (`index.ts` names its exports
120
+ * one by one) and therefore not on any published surface.
121
+ */
122
+ export declare function PlatformGroup({ title, children, }: {
123
+ title: string;
124
+ children: React.ReactNode;
125
+ }): import("react/jsx-runtime").JSX.Element;
@@ -22,6 +22,23 @@ export interface NgisPickerNotice {
22
22
  /** Already-translated host text. */
23
23
  text: string;
24
24
  tone: NgisPickerNoticeTone;
25
+ /**
26
+ * **Stage 25 (F25.6), additive — no existing caller breaks.**
27
+ *
28
+ * The one-click remedy attached to a validation issue: F25.7's *"Use
29
+ * EPSG:32650"* and *"Set projection…"*. It is a field on the notice rather
30
+ * than a widening of `text`, and that choice is the contract:
31
+ *
32
+ * `text` stays a `string`, so this component still cannot invent copy the
33
+ * caller did not ask for. Widening `text` to `ReactNode` was **considered and
34
+ * rejected** at S25-02 — a smaller diff, but it reopens the rule in this
35
+ * file's own header that everything in `notices` is a host-translated string.
36
+ * A `{label, onClick}` pair keeps both halves owned by the caller.
37
+ */
38
+ action?: {
39
+ label: string;
40
+ onClick: () => void;
41
+ };
25
42
  }
26
43
  export interface PickerFieldProps {
27
44
  /** Control id; also `<label for>`. */
@@ -34,16 +34,24 @@ export type RunButtonProps = (RunButtonBaseProps & {
34
34
  */
35
35
  submission: NgisRunSubmission | null;
36
36
  toolId?: never;
37
+ dependency?: never;
37
38
  inputs?: never;
38
39
  }) | (RunButtonBaseProps & {
39
40
  submission?: never;
40
41
  /** The tool to run; its UI schema supplies each bound input's role,
41
42
  * requiredness and label — F14.10's provenance chain, unaltered. */
42
43
  toolId: string;
44
+ dependency?: never;
43
45
  /**
44
46
  * F14.4's bindings, keyed by tool parameter name, plus any number of
45
47
  * `...ngisFlowSpread("@ref")` directives. Needs an enclosing `<Flow>`.
46
48
  */
47
49
  inputs: Readonly<Record<string, NgisInputBinding>>;
50
+ }) | (RunButtonBaseProps & {
51
+ submission?: never;
52
+ /** Locally unique `manifest.toolDependencies[].alias`. */
53
+ dependency: string;
54
+ toolId?: never;
55
+ inputs: Readonly<Record<string, NgisInputBinding>>;
48
56
  });
49
57
  export declare function RunButton(props: RunButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Where a server verdict becomes a **field's** notice, and where a UI schema
3
+ * splits into the platform's parameters and the tool's (Stage 25, **F25.4 /
4
+ * F25.6**).
5
+ *
6
+ * `ParamForm.tsx` is the wiring and the chrome. Everything it *decides* about an
7
+ * issue or about a parameter's group lives here, pure and React-free, for the
8
+ * reason `w2-contracts.ts` and `param-form-model.ts` are: this repo has no DOM
9
+ * in tests (`S25-V127`), so a rule that lives inside a component render is a
10
+ * rule no test can reach. The three flows in design §5.3 are asserted against
11
+ * these functions **and** against the real form that calls them — which is only
12
+ * possible because the two are the same code.
13
+ *
14
+ * ## What this module deliberately does not do
15
+ *
16
+ * It never re-classifies. `severity` is the server's, decided from the
17
+ * requirement's *provenance* — only the 119 hand-authored seeds may block, and a
18
+ * classifier-derived requirement is advisory forever (F25.6, D25-5 as amended on
19
+ * the user's ruling). There is no branch on `code` here and no branch on
20
+ * `crsRequirementSource`, because a client that upgraded or downgraded one would
21
+ * reopen exactly what S25-01 measured: no classifier rule beat 0.800 precision,
22
+ * and a false refusal is strictly worse than a false permit (**G-U**).
23
+ *
24
+ * It also never *drops* one. An issue naming neither a `param` nor a `port` has
25
+ * no field to belong to and comes back from {@link unroutedIssues} for the
26
+ * form-level list. Silence would be the client deciding a refusal did not count.
27
+ *
28
+ * ## The name
29
+ *
30
+ * The W2 dispatch table carved this session a file called
31
+ * `plugin-ui/validation-messages.ts`. **Stage 25 has no such file and never had
32
+ * one** — the name was read off the cross-stage diff, where it is a file *Stage
33
+ * 24* creates (+44). Two stages independently creating the same path is a
34
+ * head-on collision at the `main` merge rather than the textual one D25-0 plans
35
+ * for, so this module carries a name only Stage 25 uses. Recorded as a
36
+ * correction of record against the dispatch table (`S25-V233`), not resolved
37
+ * silently.
38
+ */
39
+ import { type NgisValidationIssue } from "../../domain/models/crs";
40
+ import type { NgisToolUiParam } from "../../types/analysis";
41
+ import type { NgisPickerNoticeTone } from "./PickerField";
42
+ /**
43
+ * Is this parameter the **platform's** rather than the tool's?
44
+ *
45
+ * F14.2 normalizes `role` to `"input" | "option" | "output"` — three *data*
46
+ * roles, and `ngis:outputCrs` is an `option` by that measure like any other
47
+ * non-port parameter. The platform/vendor distinction is a different axis
48
+ * entirely and rides on `x-role`, which reaches the client verbatim in
49
+ * `extensions` (F14.2's escape hatch). Reading it there rather than widening
50
+ * `NgisParamRole` is deliberate: the two axes are independent, and a fourth
51
+ * member of `role` would make `ngis:outputCrs` stop being an option, which it
52
+ * still is.
53
+ */
54
+ export declare function isPlatformParam(param: NgisToolUiParam): boolean;
55
+ export interface NgisParamGroups {
56
+ /** `x-role: platform`, in schema order. Rendered **above** the vendor list. */
57
+ readonly platform: readonly NgisToolUiParam[];
58
+ /** Everything else, in schema order — the tool's own parameters, unchanged. */
59
+ readonly vendor: readonly NgisToolUiParam[];
60
+ }
61
+ /**
62
+ * The form's two groups, **in schema order within each** (F25.4).
63
+ *
64
+ * The order between them is fixed and is not a preference: the output CRS
65
+ * decides what the tool's numbers *mean* — at EPSG:4326 a buffer distance of 500
66
+ * is 500 degrees — so a user who reads the form top to bottom meets the unit
67
+ * before the number that carries it. Putting it after the vendor params would
68
+ * put the consequence after the cause.
69
+ */
70
+ export declare function splitPlatformParams(params: readonly NgisToolUiParam[]): NgisParamGroups;
71
+ /**
72
+ * `hide` with every platform parameter removed.
73
+ *
74
+ * **F25.4: a platform parameter "is never hidden by an author override".** A
75
+ * plugin author writes `hide={[…]}` to suppress a param another node already
76
+ * binds (F14.22); `ngis:outputCrs` is bound by no node and belongs to no tool,
77
+ * so hiding it can only ever remove the user's one way to say what projection
78
+ * the run computes in. Filtered here rather than only at render, because `hide`
79
+ * also drives `toSubmission` — a param hidden there is absent from the payload,
80
+ * and the form would then submit a run with the CRS decision silently dropped.
81
+ */
82
+ export declare function effectiveHide(hide: readonly string[], params: readonly NgisToolUiParam[]): readonly string[];
83
+ /**
84
+ * The issues that belong to one field.
85
+ *
86
+ * A field owns an issue when the issue names it as the **parameter that fixes
87
+ * it** (`param`) or as the **input port it is about** (`port`). Both are matched
88
+ * against the same name because a UI-schema input parameter *is* the port
89
+ * (F14.2): `port: "input"` and `param: "input"` address the same field, and a
90
+ * router that honoured only one of them would drop half the flows — R1's
91
+ * `CRS_UNKNOWN` and R5's `CRS_REPROJECT_NOTICE` are both per-port.
92
+ */
93
+ export declare function issuesForField(issues: readonly NgisValidationIssue[], paramName: string): readonly NgisValidationIssue[];
94
+ /**
95
+ * The issues no field claims — form-level, and **never discarded**.
96
+ *
97
+ * An issue naming neither a `param` nor a `port` is about the run as a whole
98
+ * (R8's `CRS_UNSUPPORTED_BY_PROVIDER` is the shipped example: the provider
99
+ * cannot compute in that CRS, which is not one field's fault). It renders in the
100
+ * form's own notice list. The complement of {@link issuesForField} over every
101
+ * rendered field, so an issue naming a field the form does not render still
102
+ * surfaces rather than vanishing with it.
103
+ */
104
+ export declare function unroutedIssues(issues: readonly NgisValidationIssue[], renderedNames: readonly string[]): readonly NgisValidationIssue[];
105
+ /**
106
+ * The notice tone for a severity — **the server's, rendered as given**.
107
+ *
108
+ * Two tones exist (`PickerField` has `info` and `warn`) and there are two
109
+ * severities, so this is a total map with nothing to decide. It is a named
110
+ * function anyway so that the mapping has one definition and the G-U rule has
111
+ * one place to be violated from, rather than an inline ternary at each of the
112
+ * three call sites.
113
+ */
114
+ export declare function issueNoticeTone(severity: NgisValidationIssue["severity"]): NgisPickerNoticeTone;
115
+ /**
116
+ * Is any of these issues a refusal?
117
+ *
118
+ * The same rule `isSubmitBlocked` applies to a whole verdict, over an arbitrary
119
+ * subset — used to ask "does *this field* carry the reason Run is off?" without
120
+ * re-deciding what blocking means.
121
+ */
122
+ export declare function hasBlockingIssue(issues: readonly NgisValidationIssue[]): boolean;
123
+ /**
124
+ * The **last hop** of a server-offered remedy: `fix.labelKey` + `fix.params` →
125
+ * the words on the button (Stage 25, `S25-V300`).
126
+ *
127
+ * S25-30b proved the `fix` reaches the *response boundary* on both the validate
128
+ * and the submit leg, and recorded that it had **not** been proved to the
129
+ * button. This is that hop, and it lived inside `ParamForm`'s render until now —
130
+ * where no gate could reach it, because this repo has no DOM in tests
131
+ * (`S25-V127`). Extracted for exactly that reason and for no other; the
132
+ * behaviour is unchanged.
133
+ *
134
+ * **A key the client does not carry renders as the key itself**, never as
135
+ * nothing and never as invented copy. That is the `ExtensionSettingsForm` rule
136
+ * (F14.9): a later stage may ship a `labelKey` this build has never heard of,
137
+ * and a button labelled `Crs.issues.fix.useUtm` is ugly but honest, while a
138
+ * button labelled nothing is a remedy the user cannot see and a sentence the
139
+ * client made up is worse than both.
140
+ *
141
+ * The translator is passed in rather than imported so this stays React-free and
142
+ * assertable — the `picker-model.ts` discipline. `next-intl`'s root translator
143
+ * satisfies it as-is.
144
+ */
145
+ export interface NgisIssueTranslator {
146
+ has(key: string): boolean;
147
+ (key: string, params?: Readonly<Record<string, string | number>>): string;
148
+ }
149
+ export declare function issueFixLabel(fix: NonNullable<NgisValidationIssue["fix"]>, translate: NgisIssueTranslator): string;
150
+ /**
151
+ * The same hop for an issue's own sentence. Paired with {@link issueFixLabel}
152
+ * because the two must agree about what an unknown key does — one of them
153
+ * falling back to the key while the other fell back to `""` would put a notice
154
+ * on screen whose text and whose button disagreed about whether the server had
155
+ * said anything.
156
+ */
157
+ export declare function issueText(issue: NgisValidationIssue, translate: NgisIssueTranslator): string;
@@ -48,6 +48,7 @@ import type { NgisPortSpec } from "./w3-contracts";
48
48
  export declare const NGIS_PICKER_SLOT = "ngis-picker-field";
49
49
  export declare const NGIS_FLOW_SLOTS: {
50
50
  readonly resourceSelect: "ngis-picker-field";
51
+ readonly crsSelect: "crs-picker";
51
52
  readonly modelSelect: "ngis-picker-field";
52
53
  readonly fieldSelect: "ngis-picker-field";
53
54
  readonly extentPicker: "ngis-picker-field";
@@ -89,6 +90,22 @@ export declare function extentPickerPorts(options: {
89
90
  readonly nodeId: string;
90
91
  readonly label?: string;
91
92
  }): readonly NgisPortSpec[];
93
+ /**
94
+ * `value: crs` (Stage 25, **F25.9**).
95
+ *
96
+ * The value form is the ref's **key string** — `"EPSG:32650"` — and not the ref
97
+ * object, so what leaves this port is exactly what a `crs` widget writes into a
98
+ * params payload and exactly what reaches `POST /api/analysis/tasks` as
99
+ * `outputCrs`. One spelling, three layers.
100
+ *
101
+ * No `valueType`: that member is `kind: "value"`-only by F14.5, and a `crs` port
102
+ * is not a `value` port. Its shape is stated by its kind.
103
+ */
104
+ export declare function crsSelectPorts(options: {
105
+ readonly nodeId: string;
106
+ readonly label?: string;
107
+ readonly required?: boolean;
108
+ }): readonly NgisPortSpec[];
92
109
  /**
93
110
  * `value: record` out, plus **one `in` port per required UI-schema param not
94
111
  * satisfied internally** (F14.5).
@@ -10,13 +10,13 @@
10
10
  * control that works, and F14.2 says an unknown widget is a render-time error
11
11
  * surfaced in place.
12
12
  *
13
- * YB1 emits only seven of the twelve keys in v1 (`field-select`, `extent`,
13
+ * YB1 emits only seven of the thirteen keys in v1 (`field-select`, `extent`,
14
14
  * `textarea`, `multi-select` and `resource-multi` are never emitted — `V-45`
15
15
  * and `src/types/analysis.ts`'s own note). The other five are reachable through
16
16
  * an author's `widgets` override and a recipe's promoted inputs, and they are
17
17
  * exercised by `src/sdk/__fixtures__/ui-schema/authored/w2-dependency-probe.json`.
18
18
  *
19
- * ## Four of the twelve are Y2's pickers, not new controls
19
+ * ## Five of the thirteen are pickers, not new controls
20
20
  *
21
21
  * `resource-select`, `resource-multi`, `field-select` and `extent` delegate to
22
22
  * `ResourceSelect` / `FieldSelect` / `ExtentPicker`. Rebuilding those controls
@@ -13,6 +13,7 @@
13
13
  * extracted from. `@ngis/plugin-ui` re-exports the components from here instead
14
14
  * (F14.14 — "the components' runtime lives in the host bundle").
15
15
  */
16
+ import type { NgisCrsRef } from "../../domain/models/crs";
16
17
  import { type NgisDataType, type NgisModelSearchResult, type NgisModelSummary, type NgisResourceField, type NgisResourceSummary, type NgisResourceType } from "../../sdk/facets/types";
17
18
  import type { NgisBounds } from "../../sdk/map-api";
18
19
  /** One row of any of the four pickers. Data only — no node, no callback. */
@@ -32,6 +33,26 @@ export interface NgisPickerOption {
32
33
  hint: string | null;
33
34
  /** Listed but not ready — greyed, still selectable, never hidden. */
34
35
  muted: boolean;
36
+ /**
37
+ * **Stage 25 (F25.5 / F25.9, `S25-V216`).** The dataset's CRS behind this
38
+ * row, for the resource picker only. Absent on model and field options, which
39
+ * have no such thing — an optional member rather than a `null` one, because
40
+ * "a tool has no CRS" and "this dataset has no projection" are not the same
41
+ * sentence and a reader of this type should not have to guess which.
42
+ */
43
+ crs?: NgisCrsRef | null;
44
+ /**
45
+ * **Stage 25 (`S25-V216` / `S25-V236`).** Whether this row is in the
46
+ * `NEEDS_CRS` state, i.e. whether binding it to a port produces a blocking
47
+ * `CRS_UNKNOWN`.
48
+ *
49
+ * It is a **separate field and not `crs === null`**, because the rule is not
50
+ * that: `needsProjection` reads the server's status *and* the ref, so a `DONE`
51
+ * row whose dataset read failed is also in this state. Deriving it at each
52
+ * call site is how the picker and the library would come to disagree about
53
+ * which rows carry the ⚠.
54
+ */
55
+ needsCrs?: boolean;
35
56
  /** A scene layer is currently hidden but remains a valid data input. */
36
57
  hidden?: boolean;
37
58
  /** Present only when another eligible layer has the same visible name. */
@@ -58,6 +79,34 @@ export declare function widenDataTypes(types?: readonly NgisDataType[]): NgisRes
58
79
  * this is a choice rather than a limitation, and it is the contract's choice.
59
80
  */
60
81
  export declare function resourceOptions(summaries: readonly NgisResourceSummary[]): NgisPickerOption[];
82
+ /**
83
+ * What `ResourceSelect` has to say about projection, as a decision rather than a
84
+ * render (Stage 25, **F25.5 / F25.7 flow 2 as the W3 kickoff amended it**).
85
+ *
86
+ * Three states, and the difference between the last two is the whole of
87
+ * `S25-V236`:
88
+ *
89
+ * - `"none"` — nothing listed is in `NEEDS_CRS`. Silence, not reassurance.
90
+ * - `"listed"` — rows exist in that state but the port is bound to none of them.
91
+ * The picker **says so and offers nothing**, because there is no dataset to
92
+ * attach an action to: "Set projection…" needs a subject.
93
+ * - `"bound"` — the port is bound to one. *This* is the run that will not start,
94
+ * and it is the only case where the affordance has a dataset to act on.
95
+ *
96
+ * The rule this encodes is the kickoff's ruling: the remedy is the **picker's**,
97
+ * because the picker is the one surface that knows which dataset a port carries.
98
+ * The server's `CRS_UNKNOWN` still arrives through `ParamForm`'s issue routing
99
+ * and is still what blocks Run — this function never decides that.
100
+ */
101
+ export type NgisResourceCrsState = {
102
+ readonly kind: "none";
103
+ } | {
104
+ readonly kind: "listed";
105
+ } | {
106
+ readonly kind: "bound";
107
+ readonly option: NgisPickerOption;
108
+ };
109
+ export declare function resourceCrsState(options: readonly NgisPickerOption[], selected: readonly string[]): NgisResourceCrsState;
61
110
  /**
62
111
  * Project a file-typed resource input onto the scene layers a person actually
63
112
  * recognises. The submitted value remains the resource's `effectiveFileId`;
@@ -31,17 +31,45 @@ import type { NgisExecutionValidation } from "../../types/analysis";
31
31
  import { type NgisRunGate } from "./run-model";
32
32
  import { type NgisRunSubmission } from "./w2-contracts";
33
33
  /** Run-shaping options a submission does not carry (they are the caller's, not
34
- * the generated form's). Mirrors `NgisTaskRunRequest` plus `targetSrid`, which
35
- * only the validate leg takes. */
34
+ * the generated form's). Mirrors `NgisTaskRunRequest`. */
36
35
  export interface NgisRunOptions {
37
36
  sceneId?: string;
38
37
  /** Lab-scene optimistic fence. Ordinary scene runs leave this absent. */
39
38
  expectedSceneGeneration?: number;
40
39
  outputMode?: "ADD_LAYER" | "UPDATE_RESOURCE";
41
40
  targetResourceId?: string;
42
- /** `validate-execution` only — `POST /api/analysis/tasks` has no such field. */
43
- targetSrid?: number;
41
+ /**
42
+ * **Stage 25 (F25.4), replacing `targetSrid?: number`** (the `S25-V124`
43
+ * ruling).
44
+ *
45
+ * An `NgisCrsRef` key. Two things changed at once and both matter:
46
+ *
47
+ * 1. **It is a string, not an integer.** An integer cannot name a custom CRS,
48
+ * and `formatCrs` exists because a bare number is not a CRS a user can
49
+ * read either.
50
+ * 2. **It goes on BOTH legs.** `targetSrid` reached `validate-execution` and
51
+ * stopped — so a run could validate against one projection decision and
52
+ * execute with none. {@link submitRun} now carries it too, which is what
53
+ * makes the validate verdict describe the run that actually happens.
54
+ *
55
+ * Absent is a real value: compute in the primary input's own CRS (R4).
56
+ */
57
+ outputCrs?: string;
44
58
  }
59
+ /**
60
+ * The output CRS a submission itself declares, if any.
61
+ *
62
+ * `ngis:outputCrs` is an ordinary parameter in the generated form (F25.4 puts it
63
+ * in the schema, F25.3 renders it with the `crs` widget), so a filled form
64
+ * already carries it in `submission.params`. Lifting it here is what makes the
65
+ * run body carry `outputCrs` **with no author involvement** — a plugin that
66
+ * never heard of Stage 25 still submits the user's choice.
67
+ *
68
+ * An explicit `options.outputCrs` wins, because a caller that passed one is
69
+ * making the decision on purpose. An empty string is absent, never `""` on the
70
+ * wire (F14.4's "empty is unbound").
71
+ */
72
+ export declare function outputCrsOf(submission: NgisRunSubmission, options?: NgisRunOptions): string | undefined;
45
73
  /**
46
74
  * The `host.models.validate` request for one submission.
47
75
  *
@@ -19,6 +19,7 @@
19
19
  * Contract: F14.2 (widget vocabulary), F14.3 (validation DTOs), F14.5 (ports),
20
20
  * F14.8 (presets), F14.9 (form lifecycle), F14.10 (no `disabled` prop).
21
21
  */
22
+ import type { NgisValidationIssue } from "../../domain/models/crs";
22
23
  import type { NgisExecutionValidation, NgisUiSchemaValueType, NgisWidgetKey } from "../../types/analysis";
23
24
  /**
24
25
  * A single generated-form value. Narrower than `unknown` on purpose: F14.2's
@@ -115,6 +116,66 @@ export type NgisWidgetRegistry<TWidget> = {
115
116
  * or `FAIL` port cannot run.
116
117
  */
117
118
  export declare function blockingValidationMessages(validation: NgisExecutionValidation): readonly string[];
119
+ /**
120
+ * **Every typed issue the server sent, from both halves of one verdict (F25.6).**
121
+ *
122
+ * F25.6 put `issues[]` on `NgisExecutionValidation` **and** on
123
+ * `NgisBindingValidation`, exactly as F14.9 put its string arrays on both —
124
+ * an execution-level issue is usually about a *parameter*, a binding-level one is
125
+ * about a *port*. They are one channel read at two levels, never two channels,
126
+ * and this function is the single statement of that.
127
+ *
128
+ * It exists because they were being read at one level in the gate and at two in
129
+ * the form (`S29-V234`): `isSubmitBlocked` counted only the execution-level
130
+ * half while `ParamForm` merged both for display, so a `severity: "blocking"`
131
+ * issue arriving on the binding half printed the server's refusal and left Run
132
+ * live beside it. That is the shape G-U forbids — the client does not get to
133
+ * decline to act on a severity the server declared — and Stage 26's shipped
134
+ * `unknown_crs_issue` (`binding_validation.py:187`) writes to precisely that
135
+ * half. Order is execution-first so the compiler's fuller statement of a fact
136
+ * precedes the binding pass's barer one, matching the server's own `dedupe`.
137
+ */
138
+ export declare function serverIssues(validation: NgisExecutionValidation): readonly NgisValidationIssue[];
139
+ /**
140
+ * **AMENDED additively by F25.6 (Stage 25).** F14.9's rule is unchanged and
141
+ * still counts: a `NOT_EXECUTABLE` verdict or any blocking string blocks the
142
+ * submit. What is added is a third input — a typed `issues[]` entry whose
143
+ * `severity` is `"blocking"`, on **either** half of the verdict.
144
+ *
145
+ * **`S29-V234` (Stage 29): "either half" was the contract and one half was the
146
+ * code.** This read `validation.issues` alone, while F25.6 declares `issues[]`
147
+ * on `NgisBindingValidation` too and the F14.9 rule it amends already reaches
148
+ * into the binding half for `errors`. It is {@link serverIssues} that is read
149
+ * now — not a widening of the rule, which was always stated over the channel
150
+ * rather than over one level of it, but the rule finally reading all of it.
151
+ *
152
+ * The client **never suppresses, downgrades or re-classifies a server issue**
153
+ * (G-U). There is deliberately no branch here on `code`, on provenance, or on
154
+ * whether the client thinks the situation warrants it: severity is decided
155
+ * server-side as a function of provenance (a `CLASSIFIER` requirement may only
156
+ * ever produce `"advisory"`), and a client that second-guessed it would be
157
+ * re-opening the very defect S25-01 measured — no classifier rule exceeded 0.800
158
+ * precision, and a false refusal is strictly worse than a false permit.
159
+ *
160
+ * **Stage 29 (F29.8), RULED SILENT by the user 2026-09-19 — this function gains
161
+ * NO second blocking source, and it has never had one.** A kind-`ADAPTABLE`
162
+ * binding applies its adapter with no confirmation step, so `KIND_ADAPTABLE` is
163
+ * an `advisory` the form renders and nothing more; the arms below are unchanged
164
+ * and there is deliberately no branch on `code` or on the `ADAPTABLE` verdict
165
+ * token. The reason is the same one G-U rests on, one level up: `"ADAPTABLE"` is
166
+ * the **same** token for Stage 26's CRS adaptation and Stage 29's kind
167
+ * adaptation, and today's CRS adaptation reprojects without asking — a gate on
168
+ * one of the two would make the token's meaning depend on which issue code
169
+ * happened to accompany it. **A session adding a confirm gate here is reopening
170
+ * a ruling, not fixing an omission**; `ParamForm.kindAdaptable.test.tsx` asserts
171
+ * this function's body against exactly that.
172
+ *
173
+ * What the ruling does **not** cover, stated because silence would look like
174
+ * coverage: it is about the `ADAPTABLE` verdict's consent model, not about data
175
+ * conversion generally. An adapter that mutated or replaced a user's
176
+ * authoritative data would be outside it and would need its own ruling
177
+ * (`S29-V215`).
178
+ */
118
179
  export declare function isSubmitBlocked(validation: NgisExecutionValidation): boolean;
119
180
  /** Advisory only — never blocks (F14.9). */
120
181
  export declare function advisoryValidationMessages(validation: NgisExecutionValidation): readonly string[];
@@ -41,7 +41,22 @@ import type { NgisDataType, NgisTaskOutput } from "../../sdk/facets/types";
41
41
  import type { NgisUiSchemaValueType } from "../../types/analysis";
42
42
  import type { NgisParamValue } from "./w2-contracts";
43
43
  export type NgisPortDirection = "in" | "out";
44
- export type NgisPortKind = "resource" | "resources" | "field" | "extent" | "task" | "value" | "record";
44
+ export type NgisPortKind = "resource" | "resources" | "field" | "extent" | "task" | "value" | "record" | "crs";
45
+ /**
46
+ * **Stage 25 (F25.9)** — `crs` is the eighth kind, and its value form is the
47
+ * ref's **key string** (`"EPSG:32650"`, `"custom:<ulid>"`), never an integer and
48
+ * never the ref object. That is the same value the `crs` widget puts in a params
49
+ * payload (F25.3) and the same one `ngis:outputCrs` carries to the submit body,
50
+ * so a port, a form field and a wire field all speak one spelling.
51
+ *
52
+ * It is a kind of its own rather than a `value` port with `valueType: "string"`
53
+ * for the reason every other kind here exists: the engine's generated reasons
54
+ * and the `data-port-*` attributes name what a port *is*, and "choose a CRS
55
+ * first" is not "choose a value first". Compatibility is a separate question,
56
+ * answered by {@link portKindsCompatible} — where `crs` is string-shaped and so
57
+ * interoperates with `resource`/`field`/`value`, because its wire form really is
58
+ * a bare string.
59
+ */
45
60
  /**
46
61
  * F14.5's port spec, verbatim but for one clarification.
47
62
  *