@konitif/nodal-blockly 0.1.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.
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # Licence — @konitif/nodal-blockly
2
+
3
+ SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
4
+
5
+ The original KONITIF code in this v0.x package is made available under the
6
+ PolyForm Noncommercial License 1.0.0:
7
+ https://polyformproject.org/licenses/noncommercial/1.0.0
8
+
9
+ This notice incorporates the standard terms by reference; it is not a modified
10
+ version of the licence. This is source-available software, not OSI open source.
11
+ Third-party components retain their own licences and notices.
12
+
13
+ Uses outside the standard licence's permitted purposes require a separate
14
+ written licence. A public repository or npm download grants no additional
15
+ commercial rights. Any free professional authorization must be explicit,
16
+ nominative and limited to the agreed products; none is granted by this notice.
17
+
18
+ The KONITIF name, logo and official identity belong to Maxime Mouflard.
19
+ Descriptive reference is permitted. No third-party version may claim to be
20
+ official, certified or approved without authorization. This trademark notice
21
+ is separate from the software licence and does not amend its terms.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @konitif/nodal-blockly
2
+
3
+ A portable Blockly editing surface over KONITIF Nodal definitions and canonical Workflow Compositions. The package is distributed publicly on npm under the PolyForm Noncommercial License 1.0.0.
4
+
5
+ The package exposes a Tool declaration, a host-scoped contribution catalog, a semantic projection adapter, a disposable session and a DOM surface. It imports no application store, domain-specific dialect, runtime or framework component. `blockly@12.1.0` is owned by this package and loaded only when its surface mounts. It does not load any JavaScript generator or evaluate generated source code.
6
+
7
+ ## Host the surface
8
+
9
+ ```ts
10
+ import { BlocklyContributionCatalog } from '@konitif/nodal-blockly';
11
+ import { mountNodalBlocklySurface } from '@konitif/nodal-blockly/surface';
12
+
13
+ const catalog = new BlocklyContributionCatalog(); // Empty. No demonstration blocks are installed.
14
+ const surface = mountNodalBlocklySurface(element, { host, catalog });
15
+ await surface.ready;
16
+
17
+ surface.deactivate(); // Releases the workspace, definitions and subscriptions.
18
+ surface.activate(); // Re-reads the current canonical subject.
19
+ surface.dispose(); // Terminal, idempotent; safe before ready resolves.
20
+ catalog.dispose(); // Only the owner of this catalog disposes it.
21
+ ```
22
+
23
+ The explicit host supplies `read`, `subscribe` and `commit`. It resolves the existing Nodal dialect and owns persistence and admission. A snapshot revision must change on every canonical update relevant to editing. `commit` must compare the base revision and subject identity, validate, then publish atomically; rejection must preserve the current canonical workflow and unrelated host artifact fields.
24
+
25
+ The optional `runtime` port declares availability and delegates a run request to a common runtime. It is not required for editing. Without an admitted runtime provider, Run is disabled. Hosting the editor does not grant permission to operate external devices.
26
+
27
+ A provider may expose `observe`/`subscribe` for execution state, the active
28
+ canonical module ID, progress and errors, and `requestStop` for cancellation.
29
+ The surface highlights that occurrence and renders Run/Stop/progress without
30
+ owning execution or a clock. Per-block progress is projected top-to-bottom in
31
+ a narrow gutter to the left of each instruction, matching statement-stack time
32
+ without covering fields or changing Blockly geometry. Optional host `commands` expose explicit commands
33
+ such as creating a program; mounting never creates or replaces a workflow.
34
+
35
+ ## Supply a projection contribution
36
+
37
+ ```ts
38
+ const release = catalog.register('my-host-package', [{
39
+ id: 'my.number.block', dialectId: 'my.dialect', nodeType: 'my:number',
40
+ label: 'Number',
41
+ fields: [{ configKey: 'value', label: 'Value', editor: 'number' }]
42
+ }]);
43
+ // The matching definition, ports and defaultConfig come from host.read().dialect.
44
+ release(); // Removes this lease only; existing workflows remain untouched.
45
+ ```
46
+
47
+ Registration does not create a module occurrence. A placed block receives its own stable module ID and independent configuration. A contribution cannot change another occurrence's definition or invent a field missing from the shared schema. Catalog snapshots are copied, duplicate owners/projection IDs/node bindings are refused, and revoking an old lease cannot remove a later registration.
48
+
49
+ ## Supported editing projections
50
+
51
+ - Native statement stacks and readable containers, explicitly bound to shared
52
+ input/output port IDs by `contribution.statement`. `previous`/`next` map the
53
+ predecessor relationship; each container maps its shared input to the tail
54
+ output of its contained stack. Reordering changes canonical connections, not
55
+ occurrence identities. Parameter values use genuine Blockly value sockets;
56
+ a text field may expose host-supplied dropdown choices. Presentation checks
57
+ assist snapping but never replace shared graph validation. Fan-out and
58
+ incompatible native statement structures are refused rather than duplicated.
59
+ - Value-port trees with zero or one output per node, typed single-input connections and primitive configuration fields.
60
+ - Mixed dialects combine native value sockets with explicit port-reference menus. A single-use source with one value output and value-only ports nests into a value input; shared sources remain one occurrence addressed by reference. Control/event inputs and multiple outputs stay explicit references, never an inferred Blockly statement order. Simple value dialects retain native trees; structured dialects without eligible value outputs retain reference cards.
61
+ - Creation, field editing, connections and deletion, through common Nodal validation and `commitWorkflowComposition`, followed by host admission.
62
+ - Stable workflow, composition, module and existing connection identities. Existing opaque metadata and canonical ordering are preserved.
63
+ - Blockly coordinates and viewport are local to the projection. Moving a block does not update canonical state. The first version does not persist that layout after closing the surface.
64
+ - Unknown projections, schema drift, disabled/locked nodes, cycles and composite references are reported as non-editable. Tree mode also refuses shared children; reference cards permit fan-out. Multiple incoming connections to one input and multiplicity contracts remain unsupported. Canonical data is retained; no silent partial rewrite occurs.
65
+
66
+ This is an editing projection, not a runtime implementation. Control/event definitions must already exist in the host's shared dialect; this package does not install executors, infer scheduling or evaluate source code. The `BlocklyCompositeReference` extension records a definition revision, parameters, results, completion/error ports and the intended shared-definition/local-variant distinction. Composite editing is still explicitly refused. Hierarchical composition does not imply recursive runtime execution.
67
+
68
+ `BlocklyBlockReading.inputs` accepts a legacy tree child ID, `null`, or an explicit `{ moduleId, portId }` output reference. `BlocklyProjection.layout` reports `tree`, `references` or `mixed`. Native connections in mixed mode read the same exact endpoint references as menus. Choosing a reference replaces an attached value; attaching a value clears the reference. Compatibility is decided by shared validation, including dialect-specific wildcard inputs; an incompatible gesture is restored from the host. These are disposable projection readings, not a second persisted workflow format. Edits clone and patch the canonical artifact, validate the complete graph and composition, and submit through the same host admission contract. Existing connections keep their IDs and opaque metadata; removing a source card disconnects its references. Primitive fields are editable; unexposed object/null-valued configuration is preserved.
69
+
70
+ ## Theme the surface
71
+
72
+ The host may supply inherited CSS variables `--konitif-blockly-canvas`,
73
+ `--konitif-blockly-surface`, `--konitif-blockly-ink`, `--konitif-blockly-muted`,
74
+ `--konitif-blockly-border` and `--konitif-blockly-accent`. The workspace theme,
75
+ flyout and surface chrome read these per-instance variables. Changing them
76
+ does not recreate the workspace, register semantic definitions or commit a
77
+ workflow revision. Without host variables, the portable light defaults apply.
78
+
79
+ The paint-only Zelos specialization preserves native sockets, stacks and
80
+ container geometry. Each block receives a non-interactive SVG depth path and
81
+ a restrained gradient sheen; two shared gradient definitions are leased per
82
+ workspace and released with its renderer. No blur, backdrop filter, frame loop
83
+ or semantic observer is added. Base fills are slightly shaded to preserve
84
+ white-label contrast. Vendor disabled patterns, shadow blocks, insertion
85
+ markers, selection and execution highlighting retain their own meaning.
86
+
87
+ Hosts may also supply `--konitif-blockly-block-border`,
88
+ `--konitif-blockly-field-border`, `--konitif-blockly-depth` and
89
+ `--konitif-blockly-sheen` (default `0.08`). Prefer a small sheen strength to keep
90
+ labels readable. Forced-colour mode hides decorative paths and restores
91
+ system-colour outlines. Theme updates do not recreate block identities.
92
+
93
+ ## Lifecycle and isolation
94
+
95
+ Importing the package declares no workspace, subscription or registered block. Each mount installs uniquely namespaced block definitions; disposal removes only those names. Blockly's shared message table receives missing English defaults via a reference-counted mount lease; existing host translations win, and only still-owned additions are removed after the last release. Per-instance workspaces, resize observers, DOM handlers and subscriptions are released. Deactivation and reactivation are supported, and removal does not dispose a catalog owned by another host.
96
+
97
+ ## Verification
98
+
99
+ Use Node 22 or newer:
100
+
101
+ ```sh
102
+ npm ci --ignore-scripts
103
+ npm run build
104
+ npm test
105
+ ```
106
+
107
+ The tests exercise the real Blockly renderer/workspaces in jsdom, including genuine block creation and field changes. SVG layout and canvas text measurement are stubbed because jsdom has no layout engine. They do not establish browser rendering quality or complete Behavior Studio integration. Reference definitions and executors exist only in test fixtures and are never installed into the shipped catalog.
108
+
109
+ ## Distribution
110
+
111
+ The release authority is the standalone
112
+ [`LeMouf/konitif-nodal-blockly`](https://github.com/LeMouf/konitif-nodal-blockly)
113
+ repository. A merge does not publish the package. Releases require a reviewed
114
+ archive, a matching protected version tag and the repository's protected npm
115
+ publication workflow.
@@ -0,0 +1,12 @@
1
+ import type { BlocklyNodeContribution } from './contracts.js';
2
+ /** Host-scoped; import never registers global contributions. Empty by construction. */
3
+ export declare class BlocklyContributionCatalog {
4
+ private entries;
5
+ private listeners;
6
+ private disposed;
7
+ list(): readonly BlocklyNodeContribution[];
8
+ register(owner: string, contributions: readonly BlocklyNodeContribution[]): () => void;
9
+ subscribe(listener: () => void): () => void;
10
+ dispose(): void;
11
+ private notify;
12
+ }
@@ -0,0 +1,51 @@
1
+ /** Host-scoped; import never registers global contributions. Empty by construction. */
2
+ export class BlocklyContributionCatalog {
3
+ entries = new Map();
4
+ listeners = new Set();
5
+ disposed = false;
6
+ list() {
7
+ return structuredClone([...this.entries.values()].flat());
8
+ }
9
+ register(owner, contributions) {
10
+ if (this.disposed)
11
+ throw new Error('catalog-disposed');
12
+ if (!owner.trim() || this.entries.has(owner))
13
+ throw new Error('duplicate-owner');
14
+ const all = [...this.list(), ...contributions];
15
+ if (new Set(all.map(c => c.id)).size !== all.length)
16
+ throw new Error('duplicate-contribution');
17
+ if (new Set(all.map(c => `${c.dialectId}\0${c.nodeType}`)).size !== all.length)
18
+ throw new Error('duplicate-node-projection');
19
+ for (const c of contributions) {
20
+ if (!c.id || !c.nodeType || !c.dialectId)
21
+ throw new Error('invalid-contribution');
22
+ if (new Set(c.fields?.map(f => f.configKey)).size !== (c.fields?.length ?? 0))
23
+ throw new Error('duplicate-field');
24
+ }
25
+ const lease = structuredClone(contributions);
26
+ this.entries.set(owner, lease);
27
+ this.notify();
28
+ return () => {
29
+ if (this.entries.get(owner) !== lease)
30
+ return;
31
+ this.entries.delete(owner);
32
+ this.notify();
33
+ };
34
+ }
35
+ subscribe(listener) {
36
+ if (this.disposed)
37
+ throw new Error('catalog-disposed');
38
+ this.listeners.add(listener);
39
+ return () => { this.listeners.delete(listener); };
40
+ }
41
+ dispose() {
42
+ if (this.disposed)
43
+ return;
44
+ this.disposed = true;
45
+ this.entries.clear();
46
+ this.notify();
47
+ this.listeners.clear();
48
+ }
49
+ notify() { for (const listener of [...this.listeners])
50
+ listener(); }
51
+ }
@@ -0,0 +1,135 @@
1
+ import type { Workflow } from '@konitif/composition';
2
+ import type { NodalDialect } from '@konitif/nodal';
3
+ /** Presentation only: node meaning and port schema are resolved from the host dialect. */
4
+ export interface BlocklyNodeContribution {
5
+ id: string;
6
+ dialectId: string;
7
+ nodeType: string;
8
+ label?: string;
9
+ colour?: string;
10
+ fields?: readonly {
11
+ configKey: string;
12
+ label: string;
13
+ editor: 'number' | 'text' | 'boolean';
14
+ options?: readonly {
15
+ label: string;
16
+ value: string;
17
+ }[];
18
+ }[];
19
+ composite?: BlocklyCompositeReference;
20
+ /** Native statement projection. Container inputs reference the tail of their stack.
21
+ * The host's shared dialect owns the meaning of these ports, not Blockly. */
22
+ statement?: {
23
+ previous?: string;
24
+ next?: string;
25
+ check?: string | string[];
26
+ containers?: readonly {
27
+ portId: string;
28
+ label: string;
29
+ check?: string | string[];
30
+ }[];
31
+ };
32
+ }
33
+ /** Extension contract. This first projection deliberately refuses composite editing/execution. */
34
+ export interface BlocklyCompositeReference {
35
+ definition: {
36
+ workflowId: string;
37
+ revision: string;
38
+ };
39
+ parameters: readonly {
40
+ name: string;
41
+ portId: string;
42
+ }[];
43
+ results: readonly {
44
+ name: string;
45
+ portId: string;
46
+ }[];
47
+ completionPortId: string;
48
+ errorPortId: string;
49
+ editing: 'shared-definition' | 'local-variant';
50
+ }
51
+ export interface BlocklyWorkflowSnapshot {
52
+ /** Changes on any canonical update relevant to editing; opaque to the Tool. */
53
+ revision: string;
54
+ workflow: Workflow;
55
+ dialect: NodalDialect;
56
+ }
57
+ export interface BlocklyCommitRequest {
58
+ baseRevision: string;
59
+ workflowId: string;
60
+ candidate: Workflow;
61
+ }
62
+ export type BlocklyCommitResult = {
63
+ accepted: true;
64
+ } | {
65
+ accepted: false;
66
+ reason: string;
67
+ };
68
+ /** Ephemeral host observation, never persisted or compiled into the Workflow. */
69
+ export interface BlocklyPlaybackReading {
70
+ state: 'ready' | 'queued' | 'running' | 'paused' | 'completed' | 'failed';
71
+ progress: number;
72
+ warning?: {
73
+ message: string;
74
+ };
75
+ }
76
+ /** Host owns read, subscription, admission, persistence and optional common runtime. */
77
+ export interface NodalBlocklyHost {
78
+ read(): BlocklyWorkflowSnapshot | null;
79
+ subscribe(listener: () => void): () => void;
80
+ commit(request: BlocklyCommitRequest): BlocklyCommitResult;
81
+ /** Ephemeral focus intent, never a semantic edit or a runtime command. */
82
+ select?(request: {
83
+ workflowId: string;
84
+ revision: string;
85
+ moduleId: string | null;
86
+ }): void;
87
+ runtime?: {
88
+ availability(snapshot: BlocklyWorkflowSnapshot): {
89
+ available: boolean;
90
+ reason?: string;
91
+ };
92
+ requestRun(request: {
93
+ workflowId: string;
94
+ revision: string;
95
+ }): BlocklyCommitResult;
96
+ /** Seek the canonical host transport using normalized sequence progress. */
97
+ requestSeek?(progress: number): BlocklyCommitResult;
98
+ requestStop?(): void;
99
+ observe?(): {
100
+ state: 'idle' | 'preparing' | 'running' | 'completed' | 'stopped' | 'failed';
101
+ activeModuleId?: string | null;
102
+ progress: number;
103
+ message?: string;
104
+ blocks?: Readonly<Record<string, BlocklyPlaybackReading>>;
105
+ };
106
+ subscribe?(listener: () => void): () => void;
107
+ };
108
+ }
109
+ export interface BlocklyBlockReading {
110
+ id: string;
111
+ contributionId: string;
112
+ fields: Record<string, string | number | boolean>;
113
+ /** Tree child ID (legacy), explicit output reference, or disconnected input. */
114
+ inputs: Record<string, string | {
115
+ moduleId: string;
116
+ portId: string;
117
+ } | null>;
118
+ }
119
+ export interface BlocklyProjection {
120
+ /** Reference cards preserve graph ports without inventing statement order. */
121
+ layout?: 'tree' | 'references' | 'mixed';
122
+ editable: boolean;
123
+ issues: string[];
124
+ blocks: BlocklyBlockReading[];
125
+ }
126
+ export interface BlocklyEditorPort {
127
+ render(projection: BlocklyProjection, contributions: readonly BlocklyNodeContribution[], snapshot: BlocklyWorkflowSnapshot | null): void;
128
+ read(): BlocklyBlockReading[];
129
+ onSemanticChange(listener: () => void): () => void;
130
+ onSelectionChange?(listener: (moduleId: string | null) => void): () => void;
131
+ resize(): void;
132
+ highlight?(moduleId: string | null): void;
133
+ presentPlayback?(blocks: Readonly<Record<string, BlocklyPlaybackReading>>, label: (state: BlocklyPlaybackReading['state']) => string): void;
134
+ dispose(): void;
135
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ import type * as Blockly from 'blockly/core';
2
+ import type { BlocklyEditorPort } from './contracts.js';
3
+ /** Vendor adapter. All registered Blockly types are namespaced to this mount and released on disposal. */
4
+ export declare function createBlocklyEditor(api: typeof Blockly, element: HTMLElement, messages?: Record<string, string>): BlocklyEditorPort;