@ngis/plugin-sdk 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +31 -0
  3. package/dist/index.cjs +76 -0
  4. package/dist/index.d.ts +77 -0
  5. package/dist/index.mjs +102 -0
  6. package/dist/src/api/analysis.d.ts +140 -0
  7. package/dist/src/api/resources.d.ts +530 -0
  8. package/dist/src/api/scenes.d.ts +251 -0
  9. package/dist/src/api/users.d.ts +22 -0
  10. package/dist/src/domain/algorithms/deck-layer-style.d.ts +34 -0
  11. package/dist/src/domain/algorithms/maplibre-style.d.ts +63 -0
  12. package/dist/src/domain/algorithms/raster-color-ramps.d.ts +37 -0
  13. package/dist/src/domain/algorithms/scene-layer-type.d.ts +25 -0
  14. package/dist/src/domain/algorithms/symbol-compile.d.ts +100 -0
  15. package/dist/src/domain/algorithms/symbol-params.d.ts +90 -0
  16. package/dist/src/domain/map/bounds.d.ts +8 -0
  17. package/dist/src/domain/map/constants.d.ts +40 -0
  18. package/dist/src/domain/map/layer-builders.d.ts +32 -0
  19. package/dist/src/domain/map/rendered-layers.d.ts +30 -0
  20. package/dist/src/domain/map/style-ready.d.ts +24 -0
  21. package/dist/src/domain/map/tile-url.d.ts +20 -0
  22. package/dist/src/domain/map/types.d.ts +128 -0
  23. package/dist/src/domain/models/deck-layer-style.d.ts +211 -0
  24. package/dist/src/domain/models/scene-layer-type.d.ts +23 -0
  25. package/dist/src/domain/models/symbol.d.ts +424 -0
  26. package/dist/src/lib/api.d.ts +82 -0
  27. package/dist/src/lib/auth-session.d.ts +22 -0
  28. package/dist/src/lib/raster-color-layer/RasterColorLayer.d.ts +48 -0
  29. package/dist/src/lib/raster-color-layer/TileGrid.d.ts +31 -0
  30. package/dist/src/lib/raster-color-layer/colorRamp.d.ts +18 -0
  31. package/dist/src/lib/raster-color-layer/shaders/raster-color.frag.d.ts +2 -0
  32. package/dist/src/lib/raster-color-layer/shaders/raster-color.vert.d.ts +2 -0
  33. package/dist/src/lib/raster-color-layer/zoomInterpolate.d.ts +2 -0
  34. package/dist/src/sdk/commands.d.ts +113 -0
  35. package/dist/src/sdk/context/keys.d.ts +148 -0
  36. package/dist/src/sdk/context/publishers.d.ts +198 -0
  37. package/dist/src/sdk/context/store.d.ts +78 -0
  38. package/dist/src/sdk/context/when.d.ts +69 -0
  39. package/dist/src/sdk/contributions/dispatch.d.ts +78 -0
  40. package/dist/src/sdk/contributions/panel-id.d.ts +25 -0
  41. package/dist/src/sdk/contributions/registry.d.ts +199 -0
  42. package/dist/src/sdk/contributions/symbol-renderers.d.ts +107 -0
  43. package/dist/src/sdk/contributions/types.d.ts +288 -0
  44. package/dist/src/sdk/contributions/views-bridge.d.ts +92 -0
  45. package/dist/src/sdk/define-plugin.d.ts +17 -0
  46. package/dist/src/sdk/facets/storage.d.ts +152 -0
  47. package/dist/src/sdk/facets/types.d.ts +370 -0
  48. package/dist/src/sdk/host-api.d.ts +256 -0
  49. package/dist/src/sdk/map-api.d.ts +288 -0
  50. package/dist/src/sdk/panels.d.ts +130 -0
  51. package/dist/src/sdk/plugin.d.ts +145 -0
  52. package/dist/src/sdk/rail-tools.d.ts +58 -0
  53. package/dist/src/sdk/scopes.d.ts +158 -0
  54. package/dist/src/systems/renderer/DeckSceneRenderer.d.ts +26 -0
  55. package/dist/src/systems/renderer/FlowFieldLayer.d.ts +68 -0
  56. package/dist/src/systems/renderer/LayerRendererRegistry.d.ts +132 -0
  57. package/dist/src/systems/renderer/authenticated-deck-load.d.ts +4 -0
  58. package/dist/src/systems/renderer/deck/deck-layer-builders.d.ts +29 -0
  59. package/dist/src/systems/renderer/deck/deck-layer-helpers.d.ts +21 -0
  60. package/dist/src/systems/renderer/deck/mvt-point-aggregation-layer.d.ts +107 -0
  61. package/dist/src/systems/renderer/flow-field/flow-field-resources.d.ts +84 -0
  62. package/dist/src/systems/renderer/flow-field/flow-field-shaders.d.ts +4 -0
  63. package/dist/src/systems/renderer/symbol/SpriteRegistry.d.ts +34 -0
  64. package/dist/src/systems/renderer/symbol/entries.d.ts +67 -0
  65. package/dist/src/types/analysis.d.ts +205 -0
  66. package/dist/src/types/common.d.ts +17 -0
  67. package/dist/src/types/extensions.d.ts +560 -0
  68. package/dist/src/types/gis.d.ts +242 -0
  69. package/dist/src/types/market-social.d.ts +378 -0
  70. package/package.json +47 -0
@@ -0,0 +1,113 @@
1
+ import type { NgisHostAPI } from "./host-api";
2
+ import type { NgisMapAPI } from "./map-api";
3
+ import type { NgisContributionCommandTarget } from "./contributions/dispatch";
4
+ /** Context handed to a command's `invoke` — the full map facade, same object `NgisPluginHost.api`
5
+ * carries into `activate(host)`. Grows on demand, mirroring `NgisPluginHost`. */
6
+ export interface CommandInvokeContext {
7
+ api: NgisMapAPI;
8
+ /**
9
+ * What the contribution was invoked **over** — the layer or scene resource a
10
+ * `layer/context` / `resource/context` menu was opened on (Stage 13 X9c,
11
+ * `V-120`).
12
+ *
13
+ * `undefined` for a `statusItems` click, a `tools` launch, and every codeenv
14
+ * `scene.plugin(extId).invoke(...)` — none of those has a target, and a
15
+ * command must be able to tell "no target" from "a target I did not read".
16
+ *
17
+ * **Why the ctx and not `args`.** `args` is the *caller's* data channel: a
18
+ * scene script's mailbox row already puts arbitrary values there, and a
19
+ * command invocable from both codeenv and a menu could not tell a host-
20
+ * supplied target from its own arguments. The ctx is the *host's* channel —
21
+ * it already carries `api` — so adding a member here cannot change the shape
22
+ * any existing command receives, while merging into `args` would.
23
+ */
24
+ target?: NgisContributionCommandTarget;
25
+ /**
26
+ * The **command-owning** plugin's own 0.4 facade — the same scope-enforced
27
+ * object `activate(host)` received, resolved through the host's
28
+ * `createGisPluginHostFactory` and never assembled (`V-121`).
29
+ *
30
+ * Owner, not caller: derived from `commandPluginId(id)`, because v1 allows
31
+ * one plugin's contribution to invoke another's command and the ctx belongs
32
+ * to the code that runs.
33
+ *
34
+ * `undefined` when the host cannot resolve an owning plugin for the id — a
35
+ * host that installed only `api`, or an id whose namespace matches no
36
+ * registered plugin. Optional, so every existing `{ api }` ctx still
37
+ * type-checks and a 0.3-era command is unaffected.
38
+ */
39
+ host?: NgisHostAPI;
40
+ }
41
+ /** A plugin-declared, externally-invocable command (§19). `invoke` may be sync or async; its return
42
+ * value (or thrown error) is exactly what the codeenv plugin-invocation executor reports back to the
43
+ * scene script as the mailbox's `DONE` result (or `FAILED` error, verbatim). */
44
+ export interface CommandRegistration {
45
+ /** Namespaced id — **must contain a `.`** (validated at registration), mirrors the `ext_id`/rail-tool
46
+ * charset convention so a command id can never collide with a first-party reserved namespace. */
47
+ id: string;
48
+ title?: string;
49
+ /** Declarative JSON Schema — not enforced client-side beyond basic shape; informs the package
50
+ * manifest's capability summary and any future invocation UI. */
51
+ paramsSchema?: object;
52
+ invoke: (args: unknown, ctx: CommandInvokeContext) => unknown | Promise<unknown>;
53
+ }
54
+ /**
55
+ * The declaring plugin id, derived from the command id.
56
+ *
57
+ * Command ids are namespaced ({@link GisCommandRegistry.register} throws without a `.`) and, by the
58
+ * same convention `NgisPluginManifest.id` follows, the namespace *is* the plugin: `ngis.workflow.run`
59
+ * is `ngis.workflow`'s. F13.2 wants `pluginId` on `NgisCommandSummary` so "a plugin can see that a
60
+ * command is not its own"; deriving it from the id keeps that true without adding a field to the
61
+ * frozen `CommandRegistration` shape (E7's, not Stage 13's).
62
+ *
63
+ * Lives here rather than in `facets/commands.ts` (X4b's original home) because X8 made it the
64
+ * derivation *two* layers depend on — the summary's `pluginId` and the ctx's `host` owner — and the
65
+ * id convention it reads is this module's. `facets/commands.ts` re-exports it, so its published
66
+ * spelling is unchanged.
67
+ */
68
+ export declare function commandPluginId(commandId: string): string;
69
+ /**
70
+ * Resolves one plugin's scope-enforced 0.4 facade, or `null` when the host knows no such plugin.
71
+ *
72
+ * `null` rather than a minted facade is the whole contract (`V-121`): `createGisPluginHostFactory`
73
+ * is a memoizing factory that will happily build a host for an id nobody registered — one with no
74
+ * install row and, under F13.12, no grants — which would refuse every call for a reason the author
75
+ * cannot see. A resolver must answer from the set of plugins the host actually knows.
76
+ */
77
+ export type CommandHostResolver = (pluginId: string) => NgisHostAPI | null;
78
+ /**
79
+ * The ctx one invocation receives, with the owning plugin's facade attached when there is one
80
+ * (`V-121`).
81
+ *
82
+ * Every invoke site builds its ctx through here so the owner derivation happens once: X9c's `V-120`
83
+ * had to be fixed twice because two sites built the same object independently.
84
+ *
85
+ * `host` is spread conditionally, exactly as `target` is: an unresolvable owner leaves **no `host`
86
+ * key at all**, so "this host installed no 0.4 facade" stays distinguishable from `host: undefined`.
87
+ */
88
+ export declare function buildCommandInvokeContext(commandId: string, base: CommandInvokeContext, resolveHost?: CommandHostResolver | null): CommandInvokeContext;
89
+ type Listener = () => void;
90
+ type Disposer = () => void;
91
+ /**
92
+ * Registry instance. A process-wide singleton (`gisCommandRegistry`) is what the plugin runtime and
93
+ * the codeenv plugin-invocation executor use; the factory is exported so tests build isolated
94
+ * instances (mirrors `createGisRailToolRegistry`).
95
+ */
96
+ export interface GisCommandRegistry {
97
+ /** Register a command. Throws if `def.id` does not contain a `.`. Returns an unregister function
98
+ * (idempotent; never removes a different def re-registered under the same id). */
99
+ register(def: CommandRegistration): Disposer;
100
+ /** Registered commands in registration order — cached snapshot between mutations (safe as a
101
+ * `useSyncExternalStore` snapshot). */
102
+ list(): readonly CommandRegistration[];
103
+ /** Look a command up by id — the executor's primary entry point. */
104
+ get(id: string): CommandRegistration | null;
105
+ subscribe(listener: Listener): Disposer;
106
+ }
107
+ export declare function createGisCommandRegistry(): GisCommandRegistry;
108
+ /** Process-wide dynamic command registry for the `/gis` workspace. */
109
+ export declare const gisCommandRegistry: GisCommandRegistry;
110
+ /** Reactive list of registered commands — for a future invocation-management/debug surface, mirroring
111
+ * `useDynamicRailTools`. */
112
+ export declare function useGisCommands(registry?: GisCommandRegistry): readonly CommandRegistration[];
113
+ export {};
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Context-key vocabulary and value semantics (Stage 13, F13.6 / XD8;
3
+ * Stage 14, F14.11).
4
+ *
5
+ * The context store is the host's published view of "what is true right now",
6
+ * addressed by dotted keys — except for the two plugin-scoped namespaces, which
7
+ * F14.11 re-delimits with `:` (see `toPluginContextKey`). Contributions gate on
8
+ * it through `when` / `enabledWhen` expressions; a plugin reads it through
9
+ * `host.context`.
10
+ *
11
+ * Two rules bind everything in this folder:
12
+ *
13
+ * 1. **A key a plugin references but the host does not publish evaluates to
14
+ * `undefined` — and therefore falsy — it never throws.** Contributions come
15
+ * from third-party manifests; a typo in a `when` clause must degrade to
16
+ * "not shown", not to a broken workspace.
17
+ * 2. **Values are JSON-serializable scalars or flat arrays of scalars**
18
+ * (`NgisContextValue`). The whole 0.4 surface is worker-ready (F13.1 L6);
19
+ * a context value must survive `structuredClone`/RPC unchanged.
20
+ */
21
+ /** F13.2, verbatim. */
22
+ export type NgisContextValue = string | number | boolean | null | readonly (string | number)[];
23
+ /**
24
+ * The guaranteed key set (F13.6). X1 publishes exactly these; everything else
25
+ * is either a `config:<pluginId>:<key>` / `plugin:<pluginId>:<key>` namespace
26
+ * entry (F14.11) or absent.
27
+ *
28
+ * Every guaranteed key is **seeded with a default** at publisher start
29
+ * (`seedGuaranteedContextKeys`) so that "the scene has no layers" and "the
30
+ * context store has not booted yet" are distinguishable: the former publishes
31
+ * `0`, the latter leaves the key absent.
32
+ */
33
+ export declare const GUARANTEED_CONTEXT_KEYS: readonly ["scene.id", "scene.type", "scene.isOwner", "scene.resourceCount", "scene.layerCount", "selection.layerId", "selection.featureCount", "draw.mode", "resources.types", "resources.count", "tasks.activeCount", "tasks.hasFailed", "user.role"];
34
+ export type GuaranteedContextKey = (typeof GUARANTEED_CONTEXT_KEYS)[number];
35
+ /**
36
+ * Namespaces a plugin may not write into via `host.context.set`.
37
+ *
38
+ * `plugin` and `config` are listed too: `set` auto-namespaces to
39
+ * `plugin:<pluginId>:<key>`, so a caller passing `plugin.foo.bar` is either
40
+ * confused about the contract or trying to write into another plugin's
41
+ * namespace. F13.2 requires that to be **rejected, not silently re-prefixed**.
42
+ *
43
+ * `target` is reserved by F14.7 even though nothing populates it yet: it is a
44
+ * *per-invocation overlay*, answered by a reader wrapper rather than written to
45
+ * the store, so a plugin that grabbed the head first would shadow a namespace
46
+ * the host is about to own.
47
+ */
48
+ export declare const RESERVED_CONTEXT_NAMESPACES: readonly ["scene", "selection", "draw", "resources", "tasks", "user", "target", "config", "plugin"];
49
+ /**
50
+ * The four `NgisResourceType` values (F13.2). **`"table"` is never emitted in
51
+ * v1** (XD7) — see `classifySceneResourceType`.
52
+ *
53
+ * X1 and X2 both needed this union and independently declared identical copies;
54
+ * F13.2 places it in the resources facet, so `facets/types.ts` is the one source
55
+ * of truth and this is a type-only re-export. Resolved at the W1 merge.
56
+ */
57
+ export type { NgisResourceType } from "../facets/types";
58
+ /** The three values `resources.types` may contain in v1 (XD7). */
59
+ export declare const V1_EMITTED_RESOURCE_TYPES: readonly ["vector", "raster", "file"];
60
+ /**
61
+ * Thrown by `host.context.set` for a key that is empty, malformed, or carries
62
+ * a reserved namespace prefix.
63
+ *
64
+ * Typed rather than a bare `Error` because the caller is plugin code: it must
65
+ * be able to tell "you may not write there" from a transport failure.
66
+ */
67
+ export declare class NgisContextKeyError extends Error {
68
+ readonly code: "invalid-key";
69
+ readonly key: string;
70
+ constructor(key: string, message: string);
71
+ }
72
+ /**
73
+ * `true` when `key` is a well-formed dotted key — i.e. when it is spellable in
74
+ * a when-expression. `-` is a legal ident character because extension ids
75
+ * routinely contain one (`ngis.sample-coordinate-pin`), and the grammar has no
76
+ * subtraction operator for it to collide with.
77
+ *
78
+ * **This is deliberately colon-free, and it diverges from the `when` grammar's
79
+ * `key` production as of F14.11.** What it validates is the **bare** key a
80
+ * plugin passes to `host.context.set` — the host adds the namespace and the
81
+ * delimiter, so a bare key containing `:` is exactly the mistake
82
+ * `toPluginContextKey` must reject. The tokenizer, by contrast, has to *read*
83
+ * the namespaced form. Two different jobs, two different productions.
84
+ */
85
+ export declare function isValidContextKey(key: string): boolean;
86
+ /**
87
+ * `true` when `key` can be namespaced into a **plugin**-scoped context key —
88
+ * i.e. the non-throwing form of `toPluginContextKey`'s check, reserved head
89
+ * included.
90
+ *
91
+ * It is **not** `toConfigContextKey`'s check any more: F15.14/`V-241` dropped
92
+ * the reserved-head rejection there, so the config publisher asks
93
+ * `isValidContextKey` instead. Two different jobs — this one guards the key a
94
+ * plugin hands to `host.context.set`, where a reserved head means the author
95
+ * believes they can write host state.
96
+ */
97
+ export declare function isScopableContextKey(key: string): boolean;
98
+ /**
99
+ * Validates a plugin-supplied key and returns its canonical namespaced form
100
+ * `plugin:<pluginId>:<key>` (**F14.11**).
101
+ *
102
+ * The delimiter is `:` rather than `.` because the dotted join was not uniquely
103
+ * decodable — plugin `a.b` + key `c.d` and plugin `a.b.c` + key `d` both spell
104
+ * `plugin.a.b.c.d` (`X9-I-5`). `:` occurs in neither an extension id
105
+ * (`^[a-z0-9-]+(\.[a-z0-9-]+)+$`) nor a key segment, so splitting on the first
106
+ * two `:` yields exactly three fields and the key may still contain dots.
107
+ *
108
+ * @throws {NgisContextKeyError} on an empty/malformed key or one that already
109
+ * carries a reserved namespace prefix.
110
+ */
111
+ export declare function toPluginContextKey(pluginId: string, key: string): string;
112
+ /**
113
+ * The canonical `config:<pluginId>:<key>` spelling (fed by `host.storage`,
114
+ * W2/X3).
115
+ *
116
+ * **Validates shape, and — as of F15.14 — nothing else (`V-241`).** F14.11 gave
117
+ * it `toPluginContextKey`'s whole check, reserved head included, so that the two
118
+ * spellings of the dual-write could not disagree about what they accepted. With
119
+ * the dotted spelling gone, that reason is gone: `config:a.b:scene` collides
120
+ * with `scene.id`, with `config:a.b.scene:…` and with nothing else, because the
121
+ * namespace and the delimiter are both fixed by the host. A settings property
122
+ * named `scene` is an author's word for their own setting, not an attempt to
123
+ * write host state — the sibling's check exists to catch *that*, and this
124
+ * function is never reached from `host.context.set`.
125
+ *
126
+ * @throws {NgisContextKeyError} on an empty or malformed key.
127
+ */
128
+ export declare function toConfigContextKey(pluginId: string, key: string): string;
129
+ /**
130
+ * Value equality for the store's change guard and for the `==` operator.
131
+ *
132
+ * - Arrays compare element-wise (a re-derived `resources.types` array is a new
133
+ * reference on every store update but usually the same content — without
134
+ * this guard every unrelated scene-store write would wake every subscriber).
135
+ * - `undefined` and `null` are the same value. An absent key is "nothing", and
136
+ * `someKey == null` reading `true` for an unpublished key is the behaviour an
137
+ * expression author expects.
138
+ */
139
+ export declare function contextValueEquals(a: NgisContextValue | undefined, b: NgisContextValue | undefined): boolean;
140
+ /**
141
+ * Truthiness for a bare-key test (`when: "scene.isOwner"`).
142
+ *
143
+ * Deliberately **not** JavaScript truthiness for arrays: `[]` is truthy in JS,
144
+ * but `when: "resources.types"` plainly means "the scene has some resources".
145
+ * An empty array is falsy here, and that is a contract of the grammar rather
146
+ * than an implementation detail.
147
+ */
148
+ export declare function isContextValueTruthy(value: NgisContextValue | undefined): boolean;
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Guaranteed-key publishers (Stage 13, F13.6).
3
+ *
4
+ * Every publisher here is **dependency-injected and store-agnostic**: it takes
5
+ * a `{ getState, subscribe }` handle (the shape a Zustand store already has)
6
+ * or a facet-shaped observer, never an import of `@/store/*`. Two reasons:
7
+ *
8
+ * - `src/sdk/**` does not depend on `src/store/**` anywhere else — the C8 map
9
+ * facade takes injected bindings from `GisWorkspace` for exactly this
10
+ * reason, and the context store is the same kind of seam.
11
+ * - It is what makes "key-publisher tests against fake stores" a two-line
12
+ * fixture instead of a Zustand harness.
13
+ *
14
+ * Subscriptions use the **equality-guarded pattern** at
15
+ * `GisWorkspace.tsx:164-167`: compare the specific slice references between
16
+ * `state` and `prevState` and do nothing when they are identical, so an
17
+ * unrelated store write (a loading flag, a style draft) costs one reference
18
+ * compare rather than a re-derivation and a notification.
19
+ */
20
+ import type { NgisUnsubscribe } from "../map-api";
21
+ import { type NgisContextValue, type NgisResourceType } from "./keys";
22
+ import type { NgisContextStore } from "./store";
23
+ /** The subset of a Zustand store a publisher uses. */
24
+ export interface ContextStoreSource<T> {
25
+ getState(): T;
26
+ subscribe(listener: (state: T, prevState: T) => void): () => void;
27
+ }
28
+ /**
29
+ * Writes the default for every guaranteed key.
30
+ *
31
+ * Without this, "no scene is open" and "the context store has not booted" look
32
+ * identical to a `when` clause, and a plugin gating on `scene.layerCount > 0`
33
+ * would behave differently during the first frame than after it. Publishers
34
+ * overwrite these immediately; the defaults are the resting state.
35
+ */
36
+ export declare function seedGuaranteedContextKeys(store: NgisContextStore): void;
37
+ export interface SceneContextResource {
38
+ /**
39
+ * `SceneResourceDto.geometryType`. The scene store hydrates it from
40
+ * `DatasetInfo` (`scene-store.ts` `getDatasetGeometry`): the literal
41
+ * `"Raster"` for a RASTER dataset, the vector geometry type for a VECTOR
42
+ * one, and `null` for everything else — which is the signal
43
+ * `classifySceneResourceType` reads.
44
+ */
45
+ geometryType?: string | null;
46
+ }
47
+ export interface SceneContextScene {
48
+ id: string;
49
+ ownerId?: string | null;
50
+ type?: string | null;
51
+ }
52
+ /** The slice of `createSceneStore()`'s state the scene publisher reads. */
53
+ export interface SceneContextSlice {
54
+ sceneId: string | null;
55
+ scene: SceneContextScene | null;
56
+ resources: readonly SceneContextResource[];
57
+ layers: readonly unknown[];
58
+ selectedLayerId: string | null;
59
+ }
60
+ /** The slice of the auth store the publishers read. */
61
+ export interface AuthContextSlice {
62
+ user: {
63
+ id?: string;
64
+ role?: string;
65
+ } | null;
66
+ }
67
+ /**
68
+ * Scene-resource → `NgisResourceType` (XD7).
69
+ *
70
+ * **Never returns `"table"`.** The reserved fourth value needs
71
+ * `NgisFile.fileType`, i.e. a `GET /api/files/{fileId}` per resource, and no
72
+ * v1 dogfood plugin needs the distinction. `'table' in resources.types` is on
73
+ * the W3 grep-proof list precisely so nobody writes a plugin against a value
74
+ * this function cannot produce.
75
+ *
76
+ * Classification is a projection of `DatasetInfo.datasetType` as the scene
77
+ * store already recorded it: `RASTER` → `"raster"` (the store writes the
78
+ * literal `"Raster"` as the geometry type), any other non-empty geometry type
79
+ * → `"vector"`, absent → `"file"` (which covers `GENERIC` and a dataset whose
80
+ * metadata fetch failed). The authoritative join lives in X2's
81
+ * `host.resources` facet; wiring this publisher to it is backlog row **V-21**.
82
+ */
83
+ export declare function classifySceneResourceType(resource: SceneContextResource): NgisResourceType;
84
+ /**
85
+ * Distinct resource types, in the fixed `vector → raster → file` order.
86
+ *
87
+ * Deterministic ordering is not cosmetic: the store's equality guard compares
88
+ * arrays element-wise, so an unstable order would report a change on every
89
+ * scene-store write.
90
+ */
91
+ export declare function deriveResourceTypes(resources: readonly SceneContextResource[], classify?: (resource: SceneContextResource) => NgisResourceType): string[];
92
+ export interface SceneContextPublisherOptions {
93
+ /**
94
+ * Override the resource classifier. The seam X2/X3 use to swap in the
95
+ * dataset-backed join without editing this file.
96
+ */
97
+ classifyResource?: (resource: SceneContextResource) => NgisResourceType;
98
+ }
99
+ /**
100
+ * Publishes `scene.*`, `selection.layerId` and `resources.*`.
101
+ *
102
+ * `scene.isOwner` needs both stores, so the auth handle is optional but
103
+ * strongly recommended: without it ownership is unknowable and the key stays
104
+ * `false`, which is the safe reading (a read-only viewer).
105
+ *
106
+ * Note that `selection.layerId` is the **layer-list** selection
107
+ * (`selectedLayerId`), while `selection.featureCount` comes from the feature
108
+ * highlight (`publishHighlightContextKeys`). Those are two distinct host
109
+ * selection surfaces today — `NgisSelectionFacet` is highlight-only since
110
+ * 0.2.0 (decision D10) — and F13.6 does not require them to be the same
111
+ * selection. Recorded as a decision rather than papered over.
112
+ */
113
+ export declare function publishSceneContextKeys(store: NgisContextStore, scene: ContextStoreSource<SceneContextSlice>, auth?: ContextStoreSource<AuthContextSlice>, options?: SceneContextPublisherOptions): NgisUnsubscribe;
114
+ export interface TaskContextTask {
115
+ status: string;
116
+ }
117
+ export interface TaskContextSlice {
118
+ tasks: readonly TaskContextTask[];
119
+ }
120
+ /** Publishes `tasks.activeCount` and `tasks.hasFailed`. */
121
+ export declare function publishTaskContextKeys(store: NgisContextStore, tasks: ContextStoreSource<TaskContextSlice>): NgisUnsubscribe;
122
+ /**
123
+ * `user.role` is **presentational only and never an authorization decision.**
124
+ *
125
+ * The server stays authoritative for every capability: the market's ADMIN
126
+ * surface is established by calling the review queue and handling `403`, not
127
+ * by reading a role off the client. This key exists so a contribution can say
128
+ * "don't show this to a non-admin", which is a UI affordance, not a gate. A
129
+ * plugin that treats it as one is wrong, and the value is trivially forgeable
130
+ * from the console anyway (F13.6, ground-truth correction 7).
131
+ */
132
+ export declare function publishUserContextKeys(store: NgisContextStore, auth: ContextStoreSource<AuthContextSlice>): NgisUnsubscribe;
133
+ /** The `NgisDrawFacet` members the publisher needs (`host.api.draw`). */
134
+ export interface DrawContextSource {
135
+ getActiveMode(): string | null;
136
+ onDrawChange(listener: () => void): NgisUnsubscribe;
137
+ }
138
+ /** Publishes `draw.mode`. */
139
+ export declare function publishDrawContextKeys(store: NgisContextStore, draw: DrawContextSource): NgisUnsubscribe;
140
+ /** The `NgisHighlightFacet` members the publisher needs (`host.api.selection.highlight`). */
141
+ export interface HighlightContextSource {
142
+ get(): {
143
+ featureIds?: readonly string[];
144
+ } | null;
145
+ onChange(listener: (highlight: unknown) => void): NgisUnsubscribe;
146
+ }
147
+ /** Publishes `selection.featureCount` from the enumerated feature highlight. */
148
+ export declare function publishHighlightContextKeys(store: NgisContextStore, highlight: HighlightContextSource): NgisUnsubscribe;
149
+ /**
150
+ * The `config:<pluginId>:<key>` key source — **the W2 seam, frozen now**.
151
+ *
152
+ * F13.6 assigns `config:*` to `host.storage`, which is X3's in W2. X1 ships
153
+ * the interface and an empty implementation so that contributions can already
154
+ * reference `config:*` keys (they evaluate falsy, as an unpublished key must)
155
+ * and X3 wires a real source without touching this file or the store.
156
+ */
157
+ export interface NgisContextConfigSource {
158
+ /** `pluginId` → flat map of that plugin's scalar settings. */
159
+ read(): Readonly<Record<string, Readonly<Record<string, NgisContextValue>>>>;
160
+ subscribe(listener: () => void): NgisUnsubscribe;
161
+ }
162
+ /** The stub source: no config keys, no changes. Replaced by X3 in W2. */
163
+ export declare const emptyContextConfigSource: NgisContextConfigSource;
164
+ /**
165
+ * Publishes `config:<pluginId>:<key>`, removing keys the source drops.
166
+ *
167
+ * **A settings key that cannot be namespaced is skipped, not thrown on.** The
168
+ * keys here are the top-level members of a plugin's stored settings JSON —
169
+ * arbitrary author-chosen text, reaching this loop through
170
+ * `flattenConfigValues` with no ident check. `{"my key": 1}` would make
171
+ * `toConfigContextKey` throw *inside* the loop and kill `config:*` publication
172
+ * for **every** plugin, not just the one with the odd blob. Skipping is also
173
+ * what this path already does one level down: `flattenConfigValues` drops a
174
+ * value `NgisContextValue` cannot hold, and an absent key is falsy by design
175
+ * (F13.6).
176
+ *
177
+ * The guard is `isValidContextKey`, not `isScopableContextKey`: F15.14/`V-241`
178
+ * relaxed `toConfigContextKey`'s reserved-head rejection, so `{"scene": "x"}`
179
+ * now publishes as `config:<pluginId>:scene` instead of being dropped. It is a
180
+ * setting called `scene`, and the fixed `config:` namespace means it can reach
181
+ * nothing else.
182
+ */
183
+ export declare function publishConfigContextKeys(store: NgisContextStore, source: NgisContextConfigSource): NgisUnsubscribe;
184
+ export interface GuaranteedContextPublisherDeps {
185
+ scene?: ContextStoreSource<SceneContextSlice>;
186
+ auth?: ContextStoreSource<AuthContextSlice>;
187
+ tasks?: ContextStoreSource<TaskContextSlice>;
188
+ draw?: DrawContextSource;
189
+ highlight?: HighlightContextSource;
190
+ config?: NgisContextConfigSource;
191
+ classifyResource?: (resource: SceneContextResource) => NgisResourceType;
192
+ }
193
+ /**
194
+ * Seeds every guaranteed key and starts whichever publishers the host can
195
+ * supply sources for. Missing sources leave their keys at the seeded default —
196
+ * an absent publisher must never leave a guaranteed key undefined.
197
+ */
198
+ export declare function startGuaranteedContextPublishers(store: NgisContextStore, deps: GuaranteedContextPublisherDeps): NgisUnsubscribe;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * The context-key store (Stage 13, F13.6 / XD8).
3
+ *
4
+ * Host-published keys in, gated contributions out. Three properties are
5
+ * contractual, not incidental:
6
+ *
7
+ * 1. **Compile once.** An expression is parsed the first time it is seen and
8
+ * cached by its source text — including a *failed* parse, so a malformed
9
+ * `when` in a manifest costs one tokenizer run for the life of the session,
10
+ * not one per key change. `getCompileCount()` exists so a test can observe
11
+ * that rather than a comment asserting it.
12
+ * 2. **Batched per microtask.** Publishers write many keys per store update
13
+ * (a scene load moves `scene.id`, `scene.type`, `scene.layerCount`,
14
+ * `resources.*` at once). Writes accumulate and flush on one microtask, so
15
+ * N key changes cost exactly one notification and one re-evaluation.
16
+ * 3. **Dependency-indexed.** A watched expression re-evaluates only when a key
17
+ * in its own read-set changed. With dozens of contributions from a handful
18
+ * of plugins, the alternative — re-run everything on every change — is what
19
+ * makes declarative gating too slow to be the default.
20
+ *
21
+ * Writes are equality-guarded (`contextValueEquals`): a publisher re-deriving
22
+ * `resources.types` into a fresh array with identical content is not a change,
23
+ * and never wakes a subscriber.
24
+ */
25
+ import type { NgisUnsubscribe } from "../map-api";
26
+ import { type NgisContextValue } from "./keys";
27
+ import { type CompiledWhenExpression } from "./when";
28
+ /** The changed-key payload. A key that was removed reads back as `null`. */
29
+ export type ContextChangeListener = (changed: Readonly<Record<string, NgisContextValue>>) => void;
30
+ export interface NgisContextStore {
31
+ get(key: string): NgisContextValue | undefined;
32
+ /** Frozen snapshot; the same reference until the next write (`useSyncExternalStore`-safe). */
33
+ getAll(): Readonly<Record<string, NgisContextValue>>;
34
+ /** `undefined` removes the key. Equality-guarded; batched to a microtask. */
35
+ set(key: string, value: NgisContextValue | undefined): void;
36
+ setMany(entries: Readonly<Record<string, NgisContextValue | undefined>>): void;
37
+ /**
38
+ * Fires when any of `keys` changes. `null` subscribes to every key — the
39
+ * dev inspector's mode, not something a contribution should use.
40
+ */
41
+ subscribeKeys(keys: readonly string[] | null, listener: ContextChangeListener): NgisUnsubscribe;
42
+ /** Compiles (or returns the cached compilation of) an expression. */
43
+ compile(expression: string): CompiledWhenExpression;
44
+ /** Compile-and-evaluate. Throws `NgisWhenSyntaxError` on malformed input. */
45
+ evaluate(expression: string): boolean;
46
+ /**
47
+ * Re-evaluates `expression` whenever a key in its read-set changes, calling
48
+ * `listener` only when the boolean result actually flips. Does **not** fire
49
+ * on subscribe — call `evaluate` for the initial value.
50
+ */
51
+ watch(expression: string, listener: (value: boolean) => void): NgisUnsubscribe;
52
+ /** Test seam: how many times an expression has been parsed. */
53
+ getCompileCount(): number;
54
+ }
55
+ export interface NgisContextStoreOptions {
56
+ /**
57
+ * Compiler override. Production uses `compileWhenExpression`; tests wrap it
58
+ * to count parses and evaluations, which is how "compile once" and "one
59
+ * re-evaluation per microtask" are *observed* rather than asserted.
60
+ */
61
+ compile?: (expression: string) => CompiledWhenExpression;
62
+ /**
63
+ * Flush scheduler. Defaults to `queueMicrotask`. A test may pass a manual
64
+ * scheduler to drive flushes deterministically.
65
+ */
66
+ schedule?: (flush: () => void) => void;
67
+ }
68
+ export declare function createNgisContextStore(options?: NgisContextStoreOptions): NgisContextStore;
69
+ /**
70
+ * The host-wide context store.
71
+ *
72
+ * A module-level singleton for the same reason `workspacePanelRegistry` and
73
+ * `gisCommandRegistry` are: there is exactly one host, plugin facades are
74
+ * created per plugin but must all see the same world, and the dev inspector
75
+ * needs to reach it without a prop chain. Tests use `createNgisContextStore()`
76
+ * for isolation.
77
+ */
78
+ export declare const ngisContextStore: NgisContextStore;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * When-grammar v1 — lexer, recursive-descent parser, and compiler (F13.6 / XD8).
3
+ *
4
+ * ```
5
+ * expr := or
6
+ * or := and ( "||" and )*
7
+ * and := cmp ( "&&" cmp )*
8
+ * cmp := unary ( ( "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" ) unary )?
9
+ * unary := "!" unary | primary
10
+ * primary := "(" expr ")" | literal | key
11
+ * literal := 'single-quoted string' | number | true | false | null
12
+ * key := ident ( "." ident )* // bare key => truthiness test
13
+ * | ( "plugin" | "config" ) ":" ident ( "." ident )* ":" ident ( "." ident )*
14
+ * ```
15
+ *
16
+ * The second `key` alternative is **F14.11** and is additive: `:` was previously
17
+ * a lex error, so no existing valid expression changes meaning. It is positional
18
+ * and not a charset widening — `:` is legal at exactly two offsets of a key
19
+ * whose head is the bare ident `plugin` or `config`, which is what keeps `a:b`,
20
+ * `plugin:a`, `plugin:a:b:c` and `scene:id` lex errors. Implementing the clause
21
+ * as "add `:` to `isIdentPart`" would admit all four.
22
+ *
23
+ * Note the deliberate divergence from `isValidContextKey` (`keys.ts`): that
24
+ * function validates the **bare** key a plugin hands to `host.context.set`,
25
+ * which must stay colon-free because the host supplies the delimiter. This
26
+ * production reads the namespaced result.
27
+ *
28
+ * XD8 **amended** design-v2's asymmetric `&& || ! == != < >= in` set to add
29
+ * `>` and `<=`: an author writing `x <= 3` and getting a parse error for no
30
+ * reason is a footgun, and symmetry costs two parser cases.
31
+ *
32
+ * `cmp` takes **at most one** comparison — `a == b == c` is a syntax error, not
33
+ * a left-associative chain. That is the grammar as frozen, and XB1's validator
34
+ * plus X4's parser must round-trip the same text, so it is not negotiable here.
35
+ *
36
+ * **Compile once.** `compileWhenExpression` returns a closure plus the set of
37
+ * keys it reads; the store caches by expression text and never re-parses. The
38
+ * read-set is what makes dependency-indexed batching possible: an expression
39
+ * re-evaluates only when a key it actually mentions changes.
40
+ */
41
+ import { type NgisContextValue } from "./keys";
42
+ /** How a compiled expression reads the world. Missing key => `undefined`. */
43
+ export type ContextReader = (key: string) => NgisContextValue | undefined;
44
+ export interface CompiledWhenExpression {
45
+ /** The source text, verbatim — the cache key and the error-message subject. */
46
+ readonly expression: string;
47
+ /** Every key the expression reads, de-duplicated, in first-seen order. */
48
+ readonly keys: readonly string[];
49
+ /** Evaluates against a reader. Never throws for missing or odd-typed keys. */
50
+ evaluate(read: ContextReader): boolean;
51
+ }
52
+ /**
53
+ * A malformed expression. Typed so a contribution registrar can refuse one
54
+ * manifest entry with a precise reason instead of failing the whole plugin —
55
+ * and so the refusal is distinguishable from a host bug.
56
+ */
57
+ export declare class NgisWhenSyntaxError extends Error {
58
+ readonly code: "when-syntax";
59
+ readonly expression: string;
60
+ /** 0-based offset into `expression` where parsing stopped. */
61
+ readonly position: number;
62
+ constructor(expression: string, position: number, message: string);
63
+ }
64
+ /**
65
+ * Compiles a when-grammar v1 expression into a closure plus its read-set.
66
+ *
67
+ * @throws {NgisWhenSyntaxError} on malformed input.
68
+ */
69
+ export declare function compileWhenExpression(expression: string): CompiledWhenExpression;