@markii/runtime 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 sadigaxund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @markii/runtime
2
+
3
+ Host-side scripting glue for [Mark](https://github.com/sadigaxund/markii) documents: a null-prototype value store and `runDocumentScripts`, which executes a document's embedded scripts and gates execution by trigger tier (auto/scheduled triggers stay read-only). Framework-agnostic — the actual script executor (e.g. `@markii/lua`) is injected by the host.
4
+
5
+ See the [repository](https://github.com/sadigaxund/markii) for the format spec and the reference library as a whole.
@@ -0,0 +1,2 @@
1
+ export { createValueStore, type StoredValue, type ValueStatus, type ValueStore, } from './store.js';
2
+ export { runDocumentScripts, tierForTrigger, type ExecuteFailure, type ExecuteResult, type ExecuteSuccess, type ExecutionTier, type RunDocumentScriptsOptions, type RunSummary, type RunSummaryEntry, type RunTrigger, type ScriptExecutor, } from './run.js';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createValueStore, } from './store.js';
2
+ export { runDocumentScripts, tierForTrigger, } from './run.js';
package/dist/run.d.ts ADDED
@@ -0,0 +1,120 @@
1
+ import type { ScriptBlock } from '@markii/core';
2
+ import type { ValueStore } from './store.js';
3
+ /**
4
+ * Slice 2 of the scripting-usability layer (DESIGN.md §8): the run
5
+ * orchestrator that executes a document's script blocks and writes their
6
+ * results into the `ValueStore` Slice 1 built. This module never imports a
7
+ * concrete language runtime (no `@markii/lua`, no wasmoon) — it only knows
8
+ * about the `ScriptExecutor` shape below, which a language-specific package
9
+ * adapts to (see `@markii/lua`'s `createLuaExecutor`). That keeps the run
10
+ * path pluggable: any future script language plugs into the same
11
+ * `runDocumentScripts` without this package ever changing.
12
+ */
13
+ /**
14
+ * How a batch of scripts was invoked — DESIGN.md §8's three triggers:
15
+ * - `'manual'` — an explicit run/run-all click.
16
+ * - `'auto'` — opt-in run-on-open.
17
+ * - `'scheduled'` — opt-in periodic run.
18
+ */
19
+ export type RunTrigger = 'manual' | 'auto' | 'scheduled';
20
+ /**
21
+ * DESIGN.md §8's two-tier capability gate a concrete script executor (e.g.
22
+ * `@markii/lua`'s `runScript`) enforces: `'manual'` unlocks every manifest
23
+ * grant, including effectful ops; `'auto'` is read-only regardless of what
24
+ * was granted.
25
+ */
26
+ export type ExecutionTier = 'manual' | 'auto';
27
+ /** Maps a `RunTrigger` to the `ExecutionTier` it is allowed to run at. Pure, total, exported for direct unit testing — see `TIER_BY_TRIGGER`'s doc comment for why this is the security gate. */
28
+ export declare function tierForTrigger(trigger: RunTrigger): ExecutionTier;
29
+ /** A successful script execution: the (unmarshaled) return value. */
30
+ export interface ExecuteSuccess {
31
+ ok: true;
32
+ value: unknown;
33
+ }
34
+ /**
35
+ * A failed script execution. `kind` is intentionally a plain `string`
36
+ * (not a fixed union) here — `@markii/runtime` stays language-agnostic and
37
+ * does not know the specific failure taxonomy of any one language runtime
38
+ * (e.g. `@markii/lua`'s `'limit' | 'capability' | 'marshal' | 'runtime'`).
39
+ * `runDocumentScripts` only ever special-cases the literal string
40
+ * `'capability'`, by convention every executor is expected to use for a
41
+ * denied-capability failure.
42
+ */
43
+ export interface ExecuteFailure {
44
+ ok: false;
45
+ error: {
46
+ kind: string;
47
+ message: string;
48
+ };
49
+ }
50
+ export type ExecuteResult = ExecuteSuccess | ExecuteFailure;
51
+ /**
52
+ * The runtime-owned execution primitive: given a script's resolved code and
53
+ * the tier it's allowed to run at, execute it and report the outcome.
54
+ * Providers/grants (net hosts, cache, bundle access, resource limits, ...)
55
+ * are NOT parameters here — a concrete executor (e.g. `@markii/lua`'s
56
+ * `createLuaExecutor`) closes over them at construction time, so this
57
+ * package never depends on any specific language runtime or capability
58
+ * shape.
59
+ */
60
+ export type ScriptExecutor = (input: {
61
+ code: string;
62
+ tier: ExecutionTier;
63
+ }) => Promise<ExecuteResult>;
64
+ /** One script's outcome from a `runDocumentScripts` batch, in document order. */
65
+ export interface RunSummaryEntry {
66
+ name: string;
67
+ status: 'fresh' | 'error';
68
+ error?: string;
69
+ }
70
+ /**
71
+ * The result of one `runDocumentScripts` call. `results` has one entry per
72
+ * script block actually run, in document order — including every
73
+ * duplicate-named attempt, not deduplicated. When `scripts` contained
74
+ * repeated `name`s, `duplicateNames` lists which ones; per DESIGN.md §8
75
+ * ("`name`s land in one note-scoped value store regardless of position"),
76
+ * the store ends up holding whatever the LAST run for that name produced
77
+ * (document order) — `results` still records every individual attempt.
78
+ */
79
+ export interface RunSummary {
80
+ trigger: RunTrigger;
81
+ tier: ExecutionTier;
82
+ results: RunSummaryEntry[];
83
+ freshCount: number;
84
+ errorCount: number;
85
+ duplicateNames: string[];
86
+ }
87
+ export interface RunDocumentScriptsOptions {
88
+ scripts: ScriptBlock[];
89
+ executor: ScriptExecutor;
90
+ trigger: RunTrigger;
91
+ store: ValueStore;
92
+ /**
93
+ * Resolves a `src=` long-script reference (DESIGN.md §8) to its Lua
94
+ * source text. Optional: a document with only inline script blocks never
95
+ * needs it. If a block has `src` set and this is not provided, that one
96
+ * block is recorded as an error (never a thrown exception).
97
+ */
98
+ loadSource?: (src: string) => Promise<string> | string;
99
+ }
100
+ /**
101
+ * Runs every script block in `scripts`, in document order, against
102
+ * `executor`, and writes each outcome into `store`. This is the RUN PATH:
103
+ * "Rendering is pure; running is an event" (DESIGN.md §8) — this function
104
+ * is the event. It never throws; a single script failing (bad `src`,
105
+ * `loadSource` throwing, the executor rejecting/throwing, or an ordinary
106
+ * `ok: false` result) is recorded as that one script's error status and the
107
+ * batch continues.
108
+ *
109
+ * `trigger` is mapped to an `ExecutionTier` via `tierForTrigger` exactly
110
+ * once, up front, and that same tier is used for every script in the
111
+ * batch — the security gate is applied per batch-invocation, not
112
+ * per-script, matching DESIGN.md §8 (a run is manual, auto, or scheduled as
113
+ * a whole; individual scripts don't choose their own tier).
114
+ *
115
+ * Duplicate `name`s: every attempt gets its own `RunSummary.results` entry,
116
+ * but `store.set` is called once per script in document order, so the LAST
117
+ * run for a given name is what's left in the store afterward — see
118
+ * `RunSummary.duplicateNames`.
119
+ */
120
+ export declare function runDocumentScripts(options: RunDocumentScriptsOptions): Promise<RunSummary>;
package/dist/run.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * DESIGN.md §8's trigger x capability table, expressed as a pure lookup —
3
+ * THIS IS THE SECURITY GATE for the whole run path. `'manual'` is the only
4
+ * trigger that can ever produce the full-grants `'manual'` tier; `'auto'`
5
+ * and `'scheduled'` both map to the read-only `'auto'` tier, unconditionally.
6
+ * Typing this as `Record<RunTrigger, ExecutionTier>` means the mapping is
7
+ * exhaustive by construction — a future `RunTrigger` member is a compile
8
+ * error here until it's given an explicit (and reviewable) tier, rather
9
+ * than silently falling through to some default. See `run.test.ts` for
10
+ * exhaustive coverage of the property that `'auto'`/`'scheduled'` can never
11
+ * yield `'manual'`.
12
+ */
13
+ const TIER_BY_TRIGGER = {
14
+ manual: 'manual',
15
+ auto: 'auto',
16
+ scheduled: 'auto',
17
+ };
18
+ /** Maps a `RunTrigger` to the `ExecutionTier` it is allowed to run at. Pure, total, exported for direct unit testing — see `TIER_BY_TRIGGER`'s doc comment for why this is the security gate. */
19
+ export function tierForTrigger(trigger) {
20
+ return TIER_BY_TRIGGER[trigger];
21
+ }
22
+ function describeThrown(err) {
23
+ return err instanceof Error ? err.message : String(err);
24
+ }
25
+ /**
26
+ * §8: "An effectful call under an auto trigger fails cleanly; the
27
+ * consuming component shows a 'requires manual run' marker." When the tier
28
+ * a script ran at was the read-only `'auto'` tier and the executor reports
29
+ * a `'capability'`-kind failure, the stored error message is rewritten to
30
+ * clearly say so, so a component (or a human reading the value store) does
31
+ * not have to re-derive "this needs a manual run" from a generic capability
32
+ * message.
33
+ */
34
+ function messageForFailure(tier, error) {
35
+ if (tier === 'auto' && error.kind === 'capability') {
36
+ return `${error.message} (requires manual run: this capability is only available on a manual run)`;
37
+ }
38
+ return error.message;
39
+ }
40
+ /**
41
+ * Runs exactly one script block and never throws: every way it can fail
42
+ * (missing `loadSource` for a `src=` reference, `loadSource` itself
43
+ * throwing, the executor rejecting/throwing, or the executor reporting
44
+ * `ok: false`) is caught here and turned into an `error`-status outcome, so
45
+ * one bad script can never abort the rest of a `runDocumentScripts` batch.
46
+ */
47
+ async function runOne(script, executor, tier, loadSource) {
48
+ let code;
49
+ try {
50
+ if (script.src !== undefined) {
51
+ if (!loadSource) {
52
+ throw new Error(`script "${script.name}" references src "${script.src}" but no loadSource was provided`);
53
+ }
54
+ code = await loadSource(script.src);
55
+ }
56
+ else {
57
+ code = script.code;
58
+ }
59
+ }
60
+ catch (err) {
61
+ const message = describeThrown(err);
62
+ const ranAt = Date.now();
63
+ return {
64
+ entry: { name: script.name, status: 'error', error: message },
65
+ storedValue: { value: undefined, status: 'error', error: message, ranAt },
66
+ };
67
+ }
68
+ let result;
69
+ try {
70
+ result = await executor({ code, tier });
71
+ }
72
+ catch (err) {
73
+ const message = describeThrown(err);
74
+ const ranAt = Date.now();
75
+ return {
76
+ entry: { name: script.name, status: 'error', error: message },
77
+ storedValue: { value: undefined, status: 'error', error: message, ranAt },
78
+ };
79
+ }
80
+ const ranAt = Date.now();
81
+ if (result.ok) {
82
+ return {
83
+ entry: { name: script.name, status: 'fresh' },
84
+ storedValue: { value: result.value, status: 'fresh', ranAt },
85
+ };
86
+ }
87
+ const message = messageForFailure(tier, result.error);
88
+ return {
89
+ entry: { name: script.name, status: 'error', error: message },
90
+ storedValue: { value: undefined, status: 'error', error: message, ranAt },
91
+ };
92
+ }
93
+ /**
94
+ * Runs every script block in `scripts`, in document order, against
95
+ * `executor`, and writes each outcome into `store`. This is the RUN PATH:
96
+ * "Rendering is pure; running is an event" (DESIGN.md §8) — this function
97
+ * is the event. It never throws; a single script failing (bad `src`,
98
+ * `loadSource` throwing, the executor rejecting/throwing, or an ordinary
99
+ * `ok: false` result) is recorded as that one script's error status and the
100
+ * batch continues.
101
+ *
102
+ * `trigger` is mapped to an `ExecutionTier` via `tierForTrigger` exactly
103
+ * once, up front, and that same tier is used for every script in the
104
+ * batch — the security gate is applied per batch-invocation, not
105
+ * per-script, matching DESIGN.md §8 (a run is manual, auto, or scheduled as
106
+ * a whole; individual scripts don't choose their own tier).
107
+ *
108
+ * Duplicate `name`s: every attempt gets its own `RunSummary.results` entry,
109
+ * but `store.set` is called once per script in document order, so the LAST
110
+ * run for a given name is what's left in the store afterward — see
111
+ * `RunSummary.duplicateNames`.
112
+ */
113
+ export async function runDocumentScripts(options) {
114
+ const { scripts, executor, trigger, store, loadSource } = options;
115
+ const tier = tierForTrigger(trigger);
116
+ const results = [];
117
+ const seenNames = new Set();
118
+ const duplicateNames = new Set();
119
+ for (const script of scripts) {
120
+ if (seenNames.has(script.name)) {
121
+ duplicateNames.add(script.name);
122
+ }
123
+ seenNames.add(script.name);
124
+ const outcome = await runOne(script, executor, tier, loadSource);
125
+ results.push(outcome.entry);
126
+ store.set(script.name, outcome.storedValue);
127
+ }
128
+ let freshCount = 0;
129
+ let errorCount = 0;
130
+ for (const entry of results) {
131
+ if (entry.status === 'fresh')
132
+ freshCount++;
133
+ else
134
+ errorCount++;
135
+ }
136
+ return {
137
+ trigger,
138
+ tier,
139
+ results,
140
+ freshCount,
141
+ errorCount,
142
+ duplicateNames: [...duplicateNames],
143
+ };
144
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Slice 1's pure read path (DESIGN.md §8): the note-scoped value store that
3
+ * rendering reads from. Nothing here executes a script, fetches anything,
4
+ * or knows Lua exists — this module only holds whatever a future runner
5
+ * (Slice 2) or a bundle-cache hydration step has already written, keyed by
6
+ * the script's declared `name`. "Rendering is pure; running is an event" —
7
+ * this store is the pure side of that split.
8
+ */
9
+ /**
10
+ * Freshness of one stored value:
11
+ * - `fresh` — produced by the most recent successful run.
12
+ * - `stale` — produced by an earlier run; still shown, marked stale.
13
+ * - `error` — the producing run failed; no usable value.
14
+ * - `missing` — no run has ever produced this name.
15
+ */
16
+ export type ValueStatus = 'fresh' | 'stale' | 'error' | 'missing';
17
+ /** One named value plus its freshness bookkeeping. */
18
+ export interface StoredValue {
19
+ value: unknown;
20
+ status: ValueStatus;
21
+ error?: string;
22
+ ranAt?: number;
23
+ }
24
+ /**
25
+ * Read/write access to the note-scoped value store. Script blocks "may
26
+ * appear anywhere markdown may... but `name`s land in one note-scoped value
27
+ * store regardless of position" (DESIGN.md §8) — this interface is that
28
+ * store. Rendering only ever calls `get`/`has`/`snapshot`; `set` exists for
29
+ * whatever publishes values into the store (a script runner, a bundle
30
+ * cache-loader) — entirely out of scope for Slice 1.
31
+ */
32
+ export interface ValueStore {
33
+ get(name: string): StoredValue | undefined;
34
+ has(name: string): boolean;
35
+ set(name: string, entry: StoredValue): void;
36
+ snapshot(): Record<string, StoredValue>;
37
+ }
38
+ /**
39
+ * Simple in-memory `ValueStore`. Backed by a null-prototype object so a
40
+ * script `name` that collides with an inherited `Object.prototype` member
41
+ * (`constructor`, `toString`, `hasOwnProperty`, `valueOf`, `__proto__`, ...)
42
+ * can never resolve to that inherited member instead of a real (or
43
+ * correctly-absent) entry — the same class of defense `@markii/react`
44
+ * already applies to its directive registry (`createRegistry`) and hast-tag
45
+ * lookup (`URL_ATTRIBUTE_BY_TAG`).
46
+ */
47
+ export declare function createValueStore(initial?: Record<string, StoredValue>): ValueStore;
package/dist/store.js ADDED
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Slice 1's pure read path (DESIGN.md §8): the note-scoped value store that
3
+ * rendering reads from. Nothing here executes a script, fetches anything,
4
+ * or knows Lua exists — this module only holds whatever a future runner
5
+ * (Slice 2) or a bundle-cache hydration step has already written, keyed by
6
+ * the script's declared `name`. "Rendering is pure; running is an event" —
7
+ * this store is the pure side of that split.
8
+ */
9
+ /**
10
+ * Simple in-memory `ValueStore`. Backed by a null-prototype object so a
11
+ * script `name` that collides with an inherited `Object.prototype` member
12
+ * (`constructor`, `toString`, `hasOwnProperty`, `valueOf`, `__proto__`, ...)
13
+ * can never resolve to that inherited member instead of a real (or
14
+ * correctly-absent) entry — the same class of defense `@markii/react`
15
+ * already applies to its directive registry (`createRegistry`) and hast-tag
16
+ * lookup (`URL_ATTRIBUTE_BY_TAG`).
17
+ */
18
+ export function createValueStore(initial = {}) {
19
+ const values = Object.create(null);
20
+ for (const [name, entry] of Object.entries(initial)) {
21
+ values[name] = entry;
22
+ }
23
+ return {
24
+ get(name) {
25
+ return Object.hasOwn(values, name) ? values[name] : undefined;
26
+ },
27
+ has(name) {
28
+ return Object.hasOwn(values, name);
29
+ },
30
+ set(name, entry) {
31
+ values[name] = entry;
32
+ },
33
+ // Shallow copy: this is a new plain object, but each `StoredValue` it
34
+ // holds is the same object reference already in the store — mutating a
35
+ // returned entry in place would be visible to the store too.
36
+ snapshot() {
37
+ return { ...values };
38
+ },
39
+ };
40
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@markii/runtime",
3
+ "version": "0.1.0",
4
+ "description": "Host-side scripting glue for Mark (.mk.md): a null-proto value store and document-script execution with trigger-tier gating (auto/scheduled stay read-only). Framework-agnostic; the script executor is injected by the host (e.g. @markii/lua).",
5
+ "keywords": [
6
+ "markdown",
7
+ "mark",
8
+ "mk.md",
9
+ "scripting",
10
+ "value-store"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "sadigaxund",
14
+ "homepage": "https://github.com/sadigaxund/markii#readme",
15
+ "bugs": "https://github.com/sadigaxund/markii/issues",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/sadigaxund/markii.git",
19
+ "directory": "packages/markii-runtime"
20
+ },
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "test": "vitest run",
40
+ "build": "tsc --noEmit -p tsconfig.json",
41
+ "build:dist": "rm -rf dist && tsc -p tsconfig.build.json",
42
+ "lint": "eslint ."
43
+ },
44
+ "dependencies": {
45
+ "@markii/core": "0.1.0"
46
+ }
47
+ }