@markii/runtime 0.10.0 → 0.12.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/dist/doc.d.ts +201 -0
- package/dist/doc.js +389 -0
- package/dist/grant-key.d.ts +3 -3
- package/dist/grant-key.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run.d.ts +48 -1
- package/dist/run.js +31 -5
- package/package.json +2 -2
package/dist/doc.d.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The note-scoped, read-only `doc` view a script sees (GitHub issue #33).
|
|
3
|
+
*
|
|
4
|
+
* A script block already knows its own source. It knows nothing about the
|
|
5
|
+
* note it lives in, so a note that wants to collect what its author wrote
|
|
6
|
+
* — every `:::prep_q` block, say, and turn them into one quiz — has no way
|
|
7
|
+
* to say so. This module is the whole of that capability, and it is
|
|
8
|
+
* deliberately small: a listing of the note's directives, and a read of a
|
|
9
|
+
* value some EARLIER script in the same note already produced.
|
|
10
|
+
*
|
|
11
|
+
* Two properties make this safe enough to be tier-free (available to an
|
|
12
|
+
* `'auto'`/`'scheduled'` run exactly as to a manual one):
|
|
13
|
+
*
|
|
14
|
+
* 1. It grants no authority. Everything reachable through it is content
|
|
15
|
+
* the note already contains and values the same run already computed.
|
|
16
|
+
* There is no host, no file, no clock and no store beyond this note.
|
|
17
|
+
* 2. It is deterministic. `runDocumentScripts` runs a note's scripts
|
|
18
|
+
* sequentially in document order, so "the scripts above me have
|
|
19
|
+
* finished and the ones below me have not" is a fact, not a race.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is pure: no I/O, no engine, no knowledge of Lua. A
|
|
22
|
+
* concrete `ScriptExecutor` (`@markii/lua`) receives a `DocView` per
|
|
23
|
+
* script and decides how to present it in its own language.
|
|
24
|
+
*/
|
|
25
|
+
/** Which of the three directive forms (docs/format.md) a listed directive was written in. */
|
|
26
|
+
export type DirectiveForm = 'leaf' | 'container' | 'inline';
|
|
27
|
+
/**
|
|
28
|
+
* One directive as a script sees it. Every field is plain, already-capped
|
|
29
|
+
* data — a script can copy it, return it, or ignore it, and nothing here
|
|
30
|
+
* is a live handle back into the note.
|
|
31
|
+
*
|
|
32
|
+
* `text` is the directive's own inner text with markdown stripped: the
|
|
33
|
+
* text nodes of its subtree, blocks separated by a newline. A container
|
|
34
|
+
* that holds another directive therefore includes that inner directive's
|
|
35
|
+
* text too, because that text is genuinely inside it; the inner directive
|
|
36
|
+
* still gets its own entry in the listing.
|
|
37
|
+
*/
|
|
38
|
+
export interface DirectiveEntry {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly form: DirectiveForm;
|
|
41
|
+
/** The directive's attributes, values as written. A bare attribute reads as an empty string. */
|
|
42
|
+
readonly attributes: Readonly<Record<string, string>>;
|
|
43
|
+
readonly text: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A note's directives in document order, plus whether anything was left
|
|
47
|
+
* out to stay inside the caps. `truncated` covers every kind of shortfall
|
|
48
|
+
* — a dropped directive, a shortened text, dropped attributes — because a
|
|
49
|
+
* script that cares only ever wants the one answer: is this the whole
|
|
50
|
+
* note, or not?
|
|
51
|
+
*/
|
|
52
|
+
export interface DirectiveListing {
|
|
53
|
+
readonly directives: readonly DirectiveEntry[];
|
|
54
|
+
readonly truncated: boolean;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The size budget for one note's listing. These are not security
|
|
58
|
+
* boundaries in the sandbox's sense (the content is the user's own note,
|
|
59
|
+
* not a remote response); they exist so a pathological note cannot turn
|
|
60
|
+
* one `doc.directives()` call into a multi-megabyte string crossing the
|
|
61
|
+
* isolate boundary, and so the cost of the listing is knowable in advance.
|
|
62
|
+
*/
|
|
63
|
+
export interface DocListingLimits {
|
|
64
|
+
/** Total budget for the serialized listing. Directives past it are dropped. */
|
|
65
|
+
readonly maxTotalBytes: number;
|
|
66
|
+
/** Most directives listed, whatever their size. */
|
|
67
|
+
readonly maxDirectives: number;
|
|
68
|
+
/** Longest `text` per directive; a longer one is cut, never dropped. */
|
|
69
|
+
readonly maxTextBytes: number;
|
|
70
|
+
/** Most attributes kept per directive, in written order. */
|
|
71
|
+
readonly maxAttributes: number;
|
|
72
|
+
/** Longest attribute name kept. A longer one is dropped, since a cut name is a name nobody asked for. */
|
|
73
|
+
readonly maxAttributeNameBytes: number;
|
|
74
|
+
/** Longest attribute value kept; a longer one is cut. */
|
|
75
|
+
readonly maxAttributeValueBytes: number;
|
|
76
|
+
/** How deep the tree walk goes. Content nested deeper than this is not listed. */
|
|
77
|
+
readonly maxDepth: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The documented defaults. 512 KiB is the headline number: comfortably
|
|
81
|
+
* more than any hand-written note holds, and small enough that the worst
|
|
82
|
+
* case is a string, not a memory problem. The rest follow from it — a
|
|
83
|
+
* note with two thousand directives or an eight-kilobyte question is
|
|
84
|
+
* already past what the feature is for.
|
|
85
|
+
*/
|
|
86
|
+
export declare const DEFAULT_DOC_LISTING_LIMITS: DocListingLimits;
|
|
87
|
+
/** An empty listing, for a run whose host built none. Frozen: it is shared by every such run. */
|
|
88
|
+
export declare const EMPTY_DIRECTIVE_LISTING: DirectiveListing;
|
|
89
|
+
/**
|
|
90
|
+
* The shape `buildDirectiveListing` walks. Deliberately structural rather
|
|
91
|
+
* than an mdast import: this package parses nothing, and an mdast `Root`
|
|
92
|
+
* satisfies this as-is. Every read below is guarded anyway, so a foreign
|
|
93
|
+
* or hand-built tree degrades to a shorter listing instead of throwing.
|
|
94
|
+
*/
|
|
95
|
+
export interface DocumentTreeNode {
|
|
96
|
+
readonly type: string;
|
|
97
|
+
readonly children?: readonly DocumentTreeNode[];
|
|
98
|
+
/** `unknown` on purpose: mdast spells `value`, `name` and `attributes` differently across node types (an MDX element's `name` is `string | null`, its `attributes` an array), and this walk narrows every one of them at runtime anyway. */
|
|
99
|
+
readonly value?: unknown;
|
|
100
|
+
readonly name?: unknown;
|
|
101
|
+
readonly attributes?: unknown;
|
|
102
|
+
}
|
|
103
|
+
/** Bytes `text` occupies as UTF-8, without allocating an encoder or a buffer. */
|
|
104
|
+
export declare function utf8ByteLength(text: string): number;
|
|
105
|
+
/**
|
|
106
|
+
* Removes what must never reach a script's string: C0 control characters
|
|
107
|
+
* other than tab and newline, DEL, and unpaired surrogates.
|
|
108
|
+
*
|
|
109
|
+
* The NUL byte is the one with teeth. A Lua string is byte-clean, but the
|
|
110
|
+
* JS-to-Lua marshaling in the reference engine truncates a string at its
|
|
111
|
+
* first NUL (documented in `@markii/lua`'s `bytesToLuaString`), so a
|
|
112
|
+
* single NUL pasted into a note would silently cut the rest of the
|
|
113
|
+
* listing off. Dropping it here means one directive loses one invisible
|
|
114
|
+
* character instead.
|
|
115
|
+
*
|
|
116
|
+
* An unpaired surrogate becomes U+FFFD for the same class of reason: it
|
|
117
|
+
* cannot be encoded as UTF-8, so every layer below would have to invent
|
|
118
|
+
* its own repair.
|
|
119
|
+
*/
|
|
120
|
+
export declare function sanitizeText(text: string): string;
|
|
121
|
+
/**
|
|
122
|
+
* Walks a parsed note and returns every directive in it, in document
|
|
123
|
+
* order, as plain capped data.
|
|
124
|
+
*
|
|
125
|
+
* Nesting is flattened deliberately: a directive written inside another
|
|
126
|
+
* appears in the list right after its parent, at the position it occupies
|
|
127
|
+
* in the note. A script filtering by name therefore finds every match
|
|
128
|
+
* regardless of what wraps it, which is what a note author means by "all
|
|
129
|
+
* my questions".
|
|
130
|
+
*
|
|
131
|
+
* Never throws. A tree that is not a tree, a directive with no name, an
|
|
132
|
+
* `attributes` that is an array — each degrades to less listing, never to
|
|
133
|
+
* an exception, because this runs inside a run whose failure surface
|
|
134
|
+
* belongs to the script, not to the walk.
|
|
135
|
+
*/
|
|
136
|
+
export declare function buildDirectiveListing(tree: DocumentTreeNode | null | undefined, overrides?: Partial<DocListingLimits>): DirectiveListing;
|
|
137
|
+
/** A successful `doc.value` read. `value` is `undefined` for a name nothing produced. */
|
|
138
|
+
export interface DocValueSuccess {
|
|
139
|
+
readonly ok: true;
|
|
140
|
+
readonly value: unknown;
|
|
141
|
+
}
|
|
142
|
+
/** A refused `doc.value` read: the name belongs to a script that has not run yet. */
|
|
143
|
+
export interface DocValueRejection {
|
|
144
|
+
readonly ok: false;
|
|
145
|
+
readonly message: string;
|
|
146
|
+
}
|
|
147
|
+
export type DocValueRead = DocValueSuccess | DocValueRejection;
|
|
148
|
+
/**
|
|
149
|
+
* What one script sees of its note. Handed to the executor per script;
|
|
150
|
+
* never shared between two of them.
|
|
151
|
+
*/
|
|
152
|
+
export interface DocView {
|
|
153
|
+
readonly directives: DirectiveListing;
|
|
154
|
+
/** Reads the value of the script named `name`. Never throws — a refusal is a returned rejection. */
|
|
155
|
+
value(name: string): DocValueRead;
|
|
156
|
+
}
|
|
157
|
+
/** The listing half of `runDocumentScripts`' `doc` option: what the host built from the parsed note. */
|
|
158
|
+
export interface DocumentContext {
|
|
159
|
+
readonly directives: DirectiveListing;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The ONE wording for reading a value from a script that runs later in
|
|
163
|
+
* the note. It is a script-authoring mistake, not a permission problem
|
|
164
|
+
* and not a resource problem, so it classifies as an ordinary script
|
|
165
|
+
* error and the marker a host shows reads, in full: "script error: reads
|
|
166
|
+
* "quiz", which runs later in the note".
|
|
167
|
+
*
|
|
168
|
+
* Keeping the sentence here means the presentation layers keep their one
|
|
169
|
+
* job (naming the KIND of failure) and this package keeps its own (saying
|
|
170
|
+
* what happened), instead of a phrase being invented once per host.
|
|
171
|
+
*/
|
|
172
|
+
export declare function laterScriptReadMessage(name: string): string;
|
|
173
|
+
/** Per-script `DocView`s for one run, plus the recorder that advances "what has finished". */
|
|
174
|
+
export interface DocViewSource {
|
|
175
|
+
/** The view the script at `index` (document order) runs with. */
|
|
176
|
+
viewFor(index: number): DocView;
|
|
177
|
+
/** Records that the script at that index finished, with the value it stored (`undefined` when it failed). */
|
|
178
|
+
recordCompleted(name: string, value: unknown): void;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Builds the per-script views for one run.
|
|
182
|
+
*
|
|
183
|
+
* The rule `doc.value` enforces is "above me, already finished":
|
|
184
|
+
*
|
|
185
|
+
* - a name some earlier script in this run already produced reads back as
|
|
186
|
+
* that value (a script that FAILED produced nothing, so its name reads
|
|
187
|
+
* as nil — the failure is already reported against that script, and
|
|
188
|
+
* repeating it here would blame the reader);
|
|
189
|
+
* - a name belonging to a script at or after the caller's own position is
|
|
190
|
+
* refused, because the answer would otherwise depend on where the
|
|
191
|
+
* reader happened to be written;
|
|
192
|
+
* - anything else is simply nil, the same as reading an undefined name.
|
|
193
|
+
*
|
|
194
|
+
* A name written twice in one note is decided by what has already run: if
|
|
195
|
+
* an earlier block of that name finished, its value is what the reader
|
|
196
|
+
* sees, even when a later block will overwrite it afterwards.
|
|
197
|
+
*/
|
|
198
|
+
export declare function createDocViewSource(options: {
|
|
199
|
+
directives: DirectiveListing;
|
|
200
|
+
scriptNames: readonly string[];
|
|
201
|
+
}): DocViewSource;
|
package/dist/doc.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The note-scoped, read-only `doc` view a script sees (GitHub issue #33).
|
|
3
|
+
*
|
|
4
|
+
* A script block already knows its own source. It knows nothing about the
|
|
5
|
+
* note it lives in, so a note that wants to collect what its author wrote
|
|
6
|
+
* — every `:::prep_q` block, say, and turn them into one quiz — has no way
|
|
7
|
+
* to say so. This module is the whole of that capability, and it is
|
|
8
|
+
* deliberately small: a listing of the note's directives, and a read of a
|
|
9
|
+
* value some EARLIER script in the same note already produced.
|
|
10
|
+
*
|
|
11
|
+
* Two properties make this safe enough to be tier-free (available to an
|
|
12
|
+
* `'auto'`/`'scheduled'` run exactly as to a manual one):
|
|
13
|
+
*
|
|
14
|
+
* 1. It grants no authority. Everything reachable through it is content
|
|
15
|
+
* the note already contains and values the same run already computed.
|
|
16
|
+
* There is no host, no file, no clock and no store beyond this note.
|
|
17
|
+
* 2. It is deterministic. `runDocumentScripts` runs a note's scripts
|
|
18
|
+
* sequentially in document order, so "the scripts above me have
|
|
19
|
+
* finished and the ones below me have not" is a fact, not a race.
|
|
20
|
+
*
|
|
21
|
+
* Everything here is pure: no I/O, no engine, no knowledge of Lua. A
|
|
22
|
+
* concrete `ScriptExecutor` (`@markii/lua`) receives a `DocView` per
|
|
23
|
+
* script and decides how to present it in its own language.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The documented defaults. 512 KiB is the headline number: comfortably
|
|
27
|
+
* more than any hand-written note holds, and small enough that the worst
|
|
28
|
+
* case is a string, not a memory problem. The rest follow from it — a
|
|
29
|
+
* note with two thousand directives or an eight-kilobyte question is
|
|
30
|
+
* already past what the feature is for.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_DOC_LISTING_LIMITS = {
|
|
33
|
+
maxTotalBytes: 512 * 1024,
|
|
34
|
+
maxDirectives: 2_000,
|
|
35
|
+
maxTextBytes: 8 * 1024,
|
|
36
|
+
maxAttributes: 32,
|
|
37
|
+
maxAttributeNameBytes: 128,
|
|
38
|
+
maxAttributeValueBytes: 1024,
|
|
39
|
+
maxDepth: 200,
|
|
40
|
+
};
|
|
41
|
+
/** An empty listing, for a run whose host built none. Frozen: it is shared by every such run. */
|
|
42
|
+
export const EMPTY_DIRECTIVE_LISTING = Object.freeze({
|
|
43
|
+
directives: Object.freeze([]),
|
|
44
|
+
truncated: false,
|
|
45
|
+
});
|
|
46
|
+
const FORM_BY_TYPE = new Map([
|
|
47
|
+
['leafDirective', 'leaf'],
|
|
48
|
+
['containerDirective', 'container'],
|
|
49
|
+
['textDirective', 'inline'],
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Node types that start a new line in the extracted text. Everything else
|
|
53
|
+
* is phrasing content and runs on, so `**bold** words` reads back as
|
|
54
|
+
* `bold words` rather than as two lines.
|
|
55
|
+
*/
|
|
56
|
+
const BLOCK_TYPES = new Set([
|
|
57
|
+
'paragraph',
|
|
58
|
+
'heading',
|
|
59
|
+
'blockquote',
|
|
60
|
+
'list',
|
|
61
|
+
'listItem',
|
|
62
|
+
'code',
|
|
63
|
+
'table',
|
|
64
|
+
'tableRow',
|
|
65
|
+
'thematicBreak',
|
|
66
|
+
'definition',
|
|
67
|
+
'footnoteDefinition',
|
|
68
|
+
'html',
|
|
69
|
+
'yaml',
|
|
70
|
+
'toml',
|
|
71
|
+
'leafDirective',
|
|
72
|
+
'containerDirective',
|
|
73
|
+
]);
|
|
74
|
+
/** Node types whose `value` IS their text. A `code` fence contributes its body; an `html` node contributes nothing, since raw markup is not text the author wrote to be read. */
|
|
75
|
+
const VALUE_TYPES = new Set([
|
|
76
|
+
'text',
|
|
77
|
+
'inlineCode',
|
|
78
|
+
'code',
|
|
79
|
+
]);
|
|
80
|
+
/** Bytes `text` occupies as UTF-8, without allocating an encoder or a buffer. */
|
|
81
|
+
export function utf8ByteLength(text) {
|
|
82
|
+
let bytes = 0;
|
|
83
|
+
for (let i = 0; i < text.length; i++) {
|
|
84
|
+
const code = text.charCodeAt(i);
|
|
85
|
+
if (code < 0x80)
|
|
86
|
+
bytes += 1;
|
|
87
|
+
else if (code < 0x800)
|
|
88
|
+
bytes += 2;
|
|
89
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
90
|
+
// A well-formed surrogate pair is one 4-byte character; a lone high
|
|
91
|
+
// surrogate is replaced (see `sanitizeText`) and so costs 3.
|
|
92
|
+
const next = text.charCodeAt(i + 1);
|
|
93
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
94
|
+
bytes += 4;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
bytes += 3;
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
bytes += 3;
|
|
102
|
+
}
|
|
103
|
+
return bytes;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Removes what must never reach a script's string: C0 control characters
|
|
107
|
+
* other than tab and newline, DEL, and unpaired surrogates.
|
|
108
|
+
*
|
|
109
|
+
* The NUL byte is the one with teeth. A Lua string is byte-clean, but the
|
|
110
|
+
* JS-to-Lua marshaling in the reference engine truncates a string at its
|
|
111
|
+
* first NUL (documented in `@markii/lua`'s `bytesToLuaString`), so a
|
|
112
|
+
* single NUL pasted into a note would silently cut the rest of the
|
|
113
|
+
* listing off. Dropping it here means one directive loses one invisible
|
|
114
|
+
* character instead.
|
|
115
|
+
*
|
|
116
|
+
* An unpaired surrogate becomes U+FFFD for the same class of reason: it
|
|
117
|
+
* cannot be encoded as UTF-8, so every layer below would have to invent
|
|
118
|
+
* its own repair.
|
|
119
|
+
*/
|
|
120
|
+
export function sanitizeText(text) {
|
|
121
|
+
let out = '';
|
|
122
|
+
for (let i = 0; i < text.length; i++) {
|
|
123
|
+
const code = text.charCodeAt(i);
|
|
124
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
125
|
+
const next = text.charCodeAt(i + 1);
|
|
126
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
127
|
+
out += text[i] + text[i + 1];
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
out += '\uFFFD';
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (code >= 0xdc00 && code <= 0xdfff) {
|
|
136
|
+
out += '\uFFFD';
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (code === 0x09 || code === 0x0a) {
|
|
140
|
+
out += text[i];
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (code < 0x20 || code === 0x7f)
|
|
144
|
+
continue;
|
|
145
|
+
out += text[i];
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Cuts `text` to at most `maxBytes` UTF-8 bytes, never mid-character. */
|
|
150
|
+
function truncateToBytes(text, maxBytes) {
|
|
151
|
+
if (utf8ByteLength(text) <= maxBytes)
|
|
152
|
+
return { text, truncated: false };
|
|
153
|
+
let bytes = 0;
|
|
154
|
+
let end = 0;
|
|
155
|
+
for (const char of text) {
|
|
156
|
+
const size = utf8ByteLength(char);
|
|
157
|
+
if (bytes + size > maxBytes)
|
|
158
|
+
break;
|
|
159
|
+
bytes += size;
|
|
160
|
+
end += char.length;
|
|
161
|
+
}
|
|
162
|
+
return { text: text.slice(0, end), truncated: true };
|
|
163
|
+
}
|
|
164
|
+
/** Whether `value` is a walkable node — a plain object carrying a string `type`. */
|
|
165
|
+
function isNode(value) {
|
|
166
|
+
return (typeof value === 'object' &&
|
|
167
|
+
value !== null &&
|
|
168
|
+
typeof value.type === 'string');
|
|
169
|
+
}
|
|
170
|
+
/** Children of `node` that are themselves nodes, or an empty array. */
|
|
171
|
+
function childrenOf(node) {
|
|
172
|
+
const children = node.children;
|
|
173
|
+
return Array.isArray(children) ? children.filter(isNode) : [];
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The plain text of `node`'s subtree: text and inline code as written,
|
|
177
|
+
* fenced code as its body, blocks separated by a newline. Emphasis,
|
|
178
|
+
* links and images contribute their text and nothing else, which is the
|
|
179
|
+
* point — a script asking for a question's answer wants the answer, not
|
|
180
|
+
* its markup.
|
|
181
|
+
*/
|
|
182
|
+
function collectText(node, depth, max) {
|
|
183
|
+
if (depth > max)
|
|
184
|
+
return '';
|
|
185
|
+
if (VALUE_TYPES.has(node.type) && typeof node.value === 'string') {
|
|
186
|
+
return node.value;
|
|
187
|
+
}
|
|
188
|
+
if (node.type === 'break')
|
|
189
|
+
return '\n';
|
|
190
|
+
let out = '';
|
|
191
|
+
for (const child of childrenOf(node)) {
|
|
192
|
+
const part = collectText(child, depth + 1, max);
|
|
193
|
+
if (part === '')
|
|
194
|
+
continue;
|
|
195
|
+
if (out !== '' && BLOCK_TYPES.has(child.type))
|
|
196
|
+
out += '\n';
|
|
197
|
+
out += part;
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Reads a directive node's attributes into plain strings. A bare
|
|
203
|
+
* attribute (`{open}`) arrives as `null` from the parser and reads as an
|
|
204
|
+
* empty string, matching how the renderers already treat it.
|
|
205
|
+
*
|
|
206
|
+
* The result has a null prototype and is built with own-key assignment
|
|
207
|
+
* only, so an attribute literally named `__proto__` or `constructor` is
|
|
208
|
+
* an ordinary entry with no inherited meaning.
|
|
209
|
+
*/
|
|
210
|
+
function readAttributes(node, limits) {
|
|
211
|
+
const attributes = Object.create(null);
|
|
212
|
+
const raw = node.attributes;
|
|
213
|
+
// A plain object only: an mdast-flavored node whose `attributes` is an
|
|
214
|
+
// array (MDX) has no attributes in this format's sense, and reading one
|
|
215
|
+
// as a record would list `length` as if the author had written it.
|
|
216
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
217
|
+
return { attributes, truncated: false };
|
|
218
|
+
}
|
|
219
|
+
const record = raw;
|
|
220
|
+
let truncated = false;
|
|
221
|
+
let kept = 0;
|
|
222
|
+
for (const key of Object.keys(record)) {
|
|
223
|
+
if (kept >= limits.maxAttributes) {
|
|
224
|
+
truncated = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
const name = sanitizeText(key);
|
|
228
|
+
if (name === '' || utf8ByteLength(name) > limits.maxAttributeNameBytes) {
|
|
229
|
+
truncated = true;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const rawValue = record[key];
|
|
233
|
+
const value = typeof rawValue === 'string' ? sanitizeText(rawValue) : '';
|
|
234
|
+
const capped = truncateToBytes(value, limits.maxAttributeValueBytes);
|
|
235
|
+
if (capped.truncated)
|
|
236
|
+
truncated = true;
|
|
237
|
+
// Plain own-key assignment onto a null-prototype object: there is no
|
|
238
|
+
// inherited `__proto__` setter to trip over, so a directive written
|
|
239
|
+
// `{__proto__=x}` lands as an ordinary key with no side effect.
|
|
240
|
+
attributes[name] = capped.text;
|
|
241
|
+
kept++;
|
|
242
|
+
}
|
|
243
|
+
return { attributes, truncated };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Walks a parsed note and returns every directive in it, in document
|
|
247
|
+
* order, as plain capped data.
|
|
248
|
+
*
|
|
249
|
+
* Nesting is flattened deliberately: a directive written inside another
|
|
250
|
+
* appears in the list right after its parent, at the position it occupies
|
|
251
|
+
* in the note. A script filtering by name therefore finds every match
|
|
252
|
+
* regardless of what wraps it, which is what a note author means by "all
|
|
253
|
+
* my questions".
|
|
254
|
+
*
|
|
255
|
+
* Never throws. A tree that is not a tree, a directive with no name, an
|
|
256
|
+
* `attributes` that is an array — each degrades to less listing, never to
|
|
257
|
+
* an exception, because this runs inside a run whose failure surface
|
|
258
|
+
* belongs to the script, not to the walk.
|
|
259
|
+
*/
|
|
260
|
+
export function buildDirectiveListing(tree, overrides = {}) {
|
|
261
|
+
const limits = {
|
|
262
|
+
...DEFAULT_DOC_LISTING_LIMITS,
|
|
263
|
+
...overrides,
|
|
264
|
+
};
|
|
265
|
+
const directives = [];
|
|
266
|
+
let truncated = false;
|
|
267
|
+
let bytes = 0;
|
|
268
|
+
if (!isNode(tree))
|
|
269
|
+
return { directives, truncated };
|
|
270
|
+
const walk = (node, depth) => {
|
|
271
|
+
if (depth > limits.maxDepth) {
|
|
272
|
+
truncated = true;
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const form = FORM_BY_TYPE.get(node.type);
|
|
276
|
+
if (form !== undefined) {
|
|
277
|
+
const name = typeof node.name === 'string' ? sanitizeText(node.name) : '';
|
|
278
|
+
if (name !== '') {
|
|
279
|
+
if (directives.length >= limits.maxDirectives) {
|
|
280
|
+
truncated = true;
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const attrs = readAttributes(node, limits);
|
|
284
|
+
if (attrs.truncated)
|
|
285
|
+
truncated = true;
|
|
286
|
+
const rawText = sanitizeText(collectText(node, 0, limits.maxDepth));
|
|
287
|
+
const text = truncateToBytes(rawText, limits.maxTextBytes);
|
|
288
|
+
if (text.truncated)
|
|
289
|
+
truncated = true;
|
|
290
|
+
const entry = {
|
|
291
|
+
name,
|
|
292
|
+
form,
|
|
293
|
+
attributes: attrs.attributes,
|
|
294
|
+
text: text.text,
|
|
295
|
+
};
|
|
296
|
+
// Charged against the total budget by what the entry actually
|
|
297
|
+
// costs on the wire, so one enormous directive cannot crowd out
|
|
298
|
+
// the rest by accident and a note of small ones is not cut
|
|
299
|
+
// early by a pessimistic estimate.
|
|
300
|
+
const cost = entryCost(entry);
|
|
301
|
+
if (bytes + cost > limits.maxTotalBytes) {
|
|
302
|
+
truncated = true;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
bytes += cost;
|
|
306
|
+
directives.push(entry);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
for (const child of childrenOf(node))
|
|
312
|
+
walk(child, depth + 1);
|
|
313
|
+
};
|
|
314
|
+
walk(tree, 0);
|
|
315
|
+
return { directives, truncated };
|
|
316
|
+
}
|
|
317
|
+
/** What one entry costs against `maxTotalBytes`: its text, its attribute names and values, and its own name. */
|
|
318
|
+
function entryCost(entry) {
|
|
319
|
+
let cost = utf8ByteLength(entry.name) + utf8ByteLength(entry.text) + 32;
|
|
320
|
+
for (const [key, value] of Object.entries(entry.attributes)) {
|
|
321
|
+
cost += utf8ByteLength(key) + utf8ByteLength(value) + 8;
|
|
322
|
+
}
|
|
323
|
+
return cost;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* The ONE wording for reading a value from a script that runs later in
|
|
327
|
+
* the note. It is a script-authoring mistake, not a permission problem
|
|
328
|
+
* and not a resource problem, so it classifies as an ordinary script
|
|
329
|
+
* error and the marker a host shows reads, in full: "script error: reads
|
|
330
|
+
* "quiz", which runs later in the note".
|
|
331
|
+
*
|
|
332
|
+
* Keeping the sentence here means the presentation layers keep their one
|
|
333
|
+
* job (naming the KIND of failure) and this package keeps its own (saying
|
|
334
|
+
* what happened), instead of a phrase being invented once per host.
|
|
335
|
+
*/
|
|
336
|
+
export function laterScriptReadMessage(name) {
|
|
337
|
+
return `reads "${name}", which runs later in the note`;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Builds the per-script views for one run.
|
|
341
|
+
*
|
|
342
|
+
* The rule `doc.value` enforces is "above me, already finished":
|
|
343
|
+
*
|
|
344
|
+
* - a name some earlier script in this run already produced reads back as
|
|
345
|
+
* that value (a script that FAILED produced nothing, so its name reads
|
|
346
|
+
* as nil — the failure is already reported against that script, and
|
|
347
|
+
* repeating it here would blame the reader);
|
|
348
|
+
* - a name belonging to a script at or after the caller's own position is
|
|
349
|
+
* refused, because the answer would otherwise depend on where the
|
|
350
|
+
* reader happened to be written;
|
|
351
|
+
* - anything else is simply nil, the same as reading an undefined name.
|
|
352
|
+
*
|
|
353
|
+
* A name written twice in one note is decided by what has already run: if
|
|
354
|
+
* an earlier block of that name finished, its value is what the reader
|
|
355
|
+
* sees, even when a later block will overwrite it afterwards.
|
|
356
|
+
*/
|
|
357
|
+
export function createDocViewSource(options) {
|
|
358
|
+
const { directives, scriptNames } = options;
|
|
359
|
+
// Maps, not objects: a script named `__proto__` or `constructor` is a
|
|
360
|
+
// legal name (`@markii/core`'s charset allows it) and must be an
|
|
361
|
+
// ordinary key here, never a reach into a prototype.
|
|
362
|
+
const completed = new Map();
|
|
363
|
+
const lastIndexByName = new Map();
|
|
364
|
+
scriptNames.forEach((name, index) => {
|
|
365
|
+
lastIndexByName.set(name, index);
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
viewFor(index) {
|
|
369
|
+
return {
|
|
370
|
+
directives,
|
|
371
|
+
value(name) {
|
|
372
|
+
if (typeof name !== 'string')
|
|
373
|
+
return { ok: true, value: undefined };
|
|
374
|
+
if (completed.has(name)) {
|
|
375
|
+
return { ok: true, value: completed.get(name) };
|
|
376
|
+
}
|
|
377
|
+
const last = lastIndexByName.get(name);
|
|
378
|
+
if (last !== undefined && last >= index) {
|
|
379
|
+
return { ok: false, message: laterScriptReadMessage(name) };
|
|
380
|
+
}
|
|
381
|
+
return { ok: true, value: undefined };
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
},
|
|
385
|
+
recordCompleted(name, value) {
|
|
386
|
+
completed.set(name, value);
|
|
387
|
+
},
|
|
388
|
+
};
|
|
389
|
+
}
|
package/dist/grant-key.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* docs/security.md ("Security model"): "Grants are remembered per note, keyed
|
|
3
3
|
* by a hash of the note's full *executable closure* — its inline scripts,
|
|
4
|
-
* `src=` script files, required bundle-local modules,
|
|
5
|
-
*
|
|
4
|
+
* `src=` script files, required bundle-local modules, and the versions of
|
|
5
|
+
* any pack modules it requires. If any of that
|
|
6
6
|
* code changes, the grant is stale and the host re-prompts; otherwise
|
|
7
7
|
* edited shared code would silently inherit grants that were made to
|
|
8
8
|
* different code."
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This module is that hash. It is deliberately inert: nothing here parses
|
|
11
11
|
* markdown, reads a file, or touches the network — the host (the piece that
|
|
12
12
|
* already knows how to walk a note's scripts, resolve its `src=` files,
|
|
13
|
-
* follow its `require`s into the bundle and
|
|
13
|
+
* follow its `require`s into the bundle and its packs, and read the
|
|
14
14
|
* installed pack manifest) assembles a `GrantClosure` and hands it in.
|
|
15
15
|
* `@markii/core`'s `ScriptBlock` is NOT imported here on purpose — this
|
|
16
16
|
* package stays independent of the parser layer (see AGENTS.md's import
|
package/dist/grant-key.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* docs/security.md ("Security model"): "Grants are remembered per note, keyed
|
|
3
3
|
* by a hash of the note's full *executable closure* — its inline scripts,
|
|
4
|
-
* `src=` script files, required bundle-local modules,
|
|
5
|
-
*
|
|
4
|
+
* `src=` script files, required bundle-local modules, and the versions of
|
|
5
|
+
* any pack modules it requires. If any of that
|
|
6
6
|
* code changes, the grant is stale and the host re-prompts; otherwise
|
|
7
7
|
* edited shared code would silently inherit grants that were made to
|
|
8
8
|
* different code."
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* This module is that hash. It is deliberately inert: nothing here parses
|
|
11
11
|
* markdown, reads a file, or touches the network — the host (the piece that
|
|
12
12
|
* already knows how to walk a note's scripts, resolve its `src=` files,
|
|
13
|
-
* follow its `require`s into the bundle and
|
|
13
|
+
* follow its `require`s into the bundle and its packs, and read the
|
|
14
14
|
* installed pack manifest) assembles a `GrantClosure` and hands it in.
|
|
15
15
|
* `@markii/core`'s `ScriptBlock` is NOT imported here on purpose — this
|
|
16
16
|
* package stays independent of the parser layer (see AGENTS.md's import
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { createValueStore, type StoredValue, type ValueStatus, type ValueStore,
|
|
|
2
2
|
export { FAILURE_KINDS, normalizeFailureKind, type FailureKind, } from './failure.js';
|
|
3
3
|
export { computeGrantKey, type GrantClosure, type GrantClosurePack, type GrantClosureScript, } from './grant-key.js';
|
|
4
4
|
export { createVaultStore, type CreateVaultStoreOptions, type VaultPublishFailure, type VaultPublishResult, type VaultPublishSuccess, type VaultStore, type VaultStoreHandle, type VaultWriter, } from './vault.js';
|
|
5
|
+
export { DEFAULT_DOC_LISTING_LIMITS, EMPTY_DIRECTIVE_LISTING, buildDirectiveListing, createDocViewSource, laterScriptReadMessage, sanitizeText, utf8ByteLength, type DirectiveEntry, type DirectiveForm, type DirectiveListing, type DocListingLimits, type DocValueRead, type DocValueRejection, type DocValueSuccess, type DocView, type DocViewSource, type DocumentContext, type DocumentTreeNode, } from './doc.js';
|
|
5
6
|
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
CHANGED
|
@@ -2,4 +2,5 @@ export { createValueStore, } from './store.js';
|
|
|
2
2
|
export { FAILURE_KINDS, normalizeFailureKind, } from './failure.js';
|
|
3
3
|
export { computeGrantKey, } from './grant-key.js';
|
|
4
4
|
export { createVaultStore, } from './vault.js';
|
|
5
|
+
export { DEFAULT_DOC_LISTING_LIMITS, EMPTY_DIRECTIVE_LISTING, buildDirectiveListing, createDocViewSource, laterScriptReadMessage, sanitizeText, utf8ByteLength, } from './doc.js';
|
|
5
6
|
export { runDocumentScripts, tierForTrigger, } from './run.js';
|
package/dist/run.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ScriptBlock } from '@markii/core';
|
|
2
|
+
import { type DocumentContext, type DocView } from './doc.js';
|
|
2
3
|
import { type FailureKind } from './failure.js';
|
|
3
|
-
import type { ValueStore } from './store.js';
|
|
4
|
+
import type { StoredValue, ValueStore } from './store.js';
|
|
4
5
|
import type { VaultWriter } from './vault.js';
|
|
5
6
|
/**
|
|
6
7
|
* Slice 2 of the scripting-usability layer (docs/scripting.md): the run
|
|
@@ -67,6 +68,19 @@ export type ExecuteResult = ExecuteSuccess | ExecuteFailure;
|
|
|
67
68
|
export type ScriptExecutor = (input: {
|
|
68
69
|
code: string;
|
|
69
70
|
tier: ExecutionTier;
|
|
71
|
+
/**
|
|
72
|
+
* This script's read-only view of the note it lives in (`./doc.ts`,
|
|
73
|
+
* GitHub issue #33): the note's directives as plain data, and a read of
|
|
74
|
+
* what the scripts ABOVE this one already produced. Optional so an
|
|
75
|
+
* executor written before this existed still satisfies the type; every
|
|
76
|
+
* executor that does expose it must expose it as read-only data and must
|
|
77
|
+
* turn a rejected `value()` read into an ordinary failed execution.
|
|
78
|
+
*
|
|
79
|
+
* It carries no authority and is therefore NOT tier-gated: the same view
|
|
80
|
+
* is handed to a script under `'auto'` as under `'manual'`, because
|
|
81
|
+
* everything in it is the note's own content and this run's own values.
|
|
82
|
+
*/
|
|
83
|
+
doc?: DocView;
|
|
70
84
|
}) => Promise<ExecuteResult>;
|
|
71
85
|
/** One script's outcome from a `runDocumentScripts` batch, in document order. */
|
|
72
86
|
export interface RunSummaryEntry {
|
|
@@ -152,6 +166,39 @@ export interface RunDocumentScriptsOptions {
|
|
|
152
166
|
* (successfully) produced.
|
|
153
167
|
*/
|
|
154
168
|
vault?: VaultWriter;
|
|
169
|
+
/**
|
|
170
|
+
* Called once per script, immediately after its `StoredValue` was written
|
|
171
|
+
* to `store` and in the same document order the scripts ran in (GitHub
|
|
172
|
+
* issue #35). This is the hook a host uses to show a value the moment it
|
|
173
|
+
* lands rather than at the end of the batch: `runDocumentScripts` runs
|
|
174
|
+
* scripts sequentially, so by the time the batch resolves, the first
|
|
175
|
+
* script's number has often been known for seconds.
|
|
176
|
+
*
|
|
177
|
+
* Purely observational. It cannot change what is stored, what is
|
|
178
|
+
* reported, or whether the batch continues: `entry` is a SHALLOW COPY of
|
|
179
|
+
* the summary entry (so a callback cannot mutate the run's own
|
|
180
|
+
* bookkeeping, and so it never later grows the publish fields, which are
|
|
181
|
+
* decided after this point), and anything this callback throws is
|
|
182
|
+
* swallowed — `runDocumentScripts` never throws, and a host's progress
|
|
183
|
+
* reporting failing must not cost the user the rest of the run.
|
|
184
|
+
*
|
|
185
|
+
* Called for a FAILED script too, with the error `StoredValue` that was
|
|
186
|
+
* stored for it: a failure is as much a result as a number, and a host
|
|
187
|
+
* that only heard about successes would leave that script's component
|
|
188
|
+
* looking like it was still running.
|
|
189
|
+
*/
|
|
190
|
+
onValue?: (name: string, value: StoredValue, entry: RunSummaryEntry) => void;
|
|
191
|
+
/**
|
|
192
|
+
* The note itself, for the `doc` view a script sees (`./doc.ts`). The
|
|
193
|
+
* caller builds this once from the tree it already parsed to find
|
|
194
|
+
* `scripts`; this package parses nothing.
|
|
195
|
+
*
|
|
196
|
+
* Absent, scripts still get a `doc` view, with an empty listing — the
|
|
197
|
+
* ORDERING rules `doc.value` enforces do not depend on it, so a host
|
|
198
|
+
* that never builds a listing still cannot let one script read another
|
|
199
|
+
* that has not run.
|
|
200
|
+
*/
|
|
201
|
+
doc?: DocumentContext;
|
|
155
202
|
}
|
|
156
203
|
/**
|
|
157
204
|
* Runs every script block in `scripts`, in document order, against
|
package/dist/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createDocViewSource, EMPTY_DIRECTIVE_LISTING, } from './doc.js';
|
|
1
2
|
import { normalizeFailureKind } from './failure.js';
|
|
2
3
|
/**
|
|
3
4
|
* docs/scripting.md's trigger x capability table, expressed as a pure lookup —
|
|
@@ -56,7 +57,7 @@ function describeThrown(err) {
|
|
|
56
57
|
* all, so rewriting every capability-kind auto-tier failure that way was
|
|
57
58
|
* simply wrong).
|
|
58
59
|
*/
|
|
59
|
-
async function runOne(script, executor, tier, loadSource) {
|
|
60
|
+
async function runOne(script, executor, tier, loadSource, doc) {
|
|
60
61
|
let code;
|
|
61
62
|
try {
|
|
62
63
|
if (script.src !== undefined) {
|
|
@@ -90,7 +91,7 @@ async function runOne(script, executor, tier, loadSource) {
|
|
|
90
91
|
}
|
|
91
92
|
let result;
|
|
92
93
|
try {
|
|
93
|
-
result = await executor({ code, tier });
|
|
94
|
+
result = await executor({ code, tier, doc });
|
|
94
95
|
}
|
|
95
96
|
catch (err) {
|
|
96
97
|
const message = describeThrown(err);
|
|
@@ -161,18 +162,43 @@ async function runOne(script, executor, tier, loadSource) {
|
|
|
161
162
|
* `RunSummary.duplicateNames`.
|
|
162
163
|
*/
|
|
163
164
|
export async function runDocumentScripts(options) {
|
|
164
|
-
const { scripts, executor, trigger, store, loadSource, vault } = options;
|
|
165
|
+
const { scripts, executor, trigger, store, loadSource, vault, onValue } = options;
|
|
165
166
|
const tier = tierForTrigger(trigger);
|
|
167
|
+
// GitHub issue #33: one source of `doc` views for the whole batch. Built
|
|
168
|
+
// unconditionally, even with no `options.doc`, because it also owns the
|
|
169
|
+
// "a script may only read what ran above it" rule, which is a property
|
|
170
|
+
// of this loop's ordering rather than of the listing.
|
|
171
|
+
const docViews = createDocViewSource({
|
|
172
|
+
directives: options.doc?.directives ?? EMPTY_DIRECTIVE_LISTING,
|
|
173
|
+
scriptNames: scripts.map((script) => script.name),
|
|
174
|
+
});
|
|
166
175
|
const results = [];
|
|
167
176
|
const seenNames = new Set();
|
|
168
177
|
const duplicateNames = new Set();
|
|
169
|
-
for (const script of scripts) {
|
|
178
|
+
for (const [index, script] of scripts.entries()) {
|
|
170
179
|
if (seenNames.has(script.name)) {
|
|
171
180
|
duplicateNames.add(script.name);
|
|
172
181
|
}
|
|
173
182
|
seenNames.add(script.name);
|
|
174
|
-
const outcome = await runOne(script, executor, tier, loadSource);
|
|
183
|
+
const outcome = await runOne(script, executor, tier, loadSource, docViews.viewFor(index));
|
|
175
184
|
store.set(script.name, outcome.storedValue);
|
|
185
|
+
// GitHub issue #35: the value is announced the instant it is stored,
|
|
186
|
+
// before publishing and before the batch moves on to the next script,
|
|
187
|
+
// so a host can render it while the rest of the run is still going.
|
|
188
|
+
// Guarded because this function's never-throws contract covers
|
|
189
|
+
// everything a caller supplies, this callback included.
|
|
190
|
+
if (onValue) {
|
|
191
|
+
try {
|
|
192
|
+
onValue(script.name, outcome.storedValue, { ...outcome.entry });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// Deliberately ignored: see `onValue`'s doc comment.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Recorded AFTER the run, so a script can never see its own name, and
|
|
199
|
+
// recorded for a failed run too (as `undefined`), so a later reader
|
|
200
|
+
// gets nil rather than a refusal blaming it for someone else's error.
|
|
201
|
+
docViews.recordCompleted(script.name, outcome.storedValue.value);
|
|
176
202
|
// Publishing (docs/scripting.md): only for a block that both asked to
|
|
177
203
|
// publish (bare `publish` on its fence — see `ScriptBlock.publish`) and
|
|
178
204
|
// actually succeeded. A failed run has nothing to publish; `runOne`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@markii/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
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
5
|
"keywords": [
|
|
6
6
|
"markdown",
|
|
@@ -42,6 +42,6 @@
|
|
|
42
42
|
"lint": "eslint ."
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@markii/core": "0.
|
|
45
|
+
"@markii/core": "0.12.0"
|
|
46
46
|
}
|
|
47
47
|
}
|