@markii/host 0.17.0-next.5 → 0.17.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 (39) hide show
  1. package/dist/browser-worker.classic.js +43 -43
  2. package/dist/browser-worker.js +43 -43
  3. package/dist/conformance/host/18-last-run-and-grants/expected.json +42 -0
  4. package/dist/conformance/host/18-last-run-and-grants/note.mk.md +5 -0
  5. package/dist/conformance/host/18-last-run-and-grants/scenario.json +27 -0
  6. package/dist/conformance/host/README.md +11 -4
  7. package/dist/conformance.d.ts +1 -1
  8. package/dist/conformance.js +1 -1
  9. package/dist/host/adapter.d.ts +21 -7
  10. package/dist/host/cascade-export-behavior.d.ts +3 -1
  11. package/dist/host/cascade-export-behavior.js +7 -4
  12. package/dist/host/conformance-runner.d.ts +8 -0
  13. package/dist/host/conformance-runner.js +40 -2
  14. package/dist/host/create-host.d.ts +231 -62
  15. package/dist/host/create-host.js +204 -98
  16. package/dist/host/export-behavior.d.ts +2 -0
  17. package/dist/host/export-behavior.js +10 -4
  18. package/dist/host/pack-archive.d.ts +7 -6
  19. package/dist/host/pack-archive.js +11 -7
  20. package/dist/host/pack-install.js +9 -7
  21. package/dist/host/pack-remove.d.ts +7 -18
  22. package/dist/host/pack-remove.js +2 -0
  23. package/dist/host/pack-trust-behavior.d.ts +13 -36
  24. package/dist/host/pack-trust-behavior.js +22 -1
  25. package/dist/host/run-behavior.d.ts +21 -34
  26. package/dist/host/run-behavior.js +23 -5
  27. package/dist/index.d.ts +5 -10
  28. package/dist/index.js +0 -1
  29. package/dist/internal.d.ts +9 -2
  30. package/dist/internal.js +3 -2
  31. package/dist/node-worker.js +3 -1
  32. package/dist/node.d.ts +1 -2
  33. package/dist/node.js +1 -1
  34. package/dist/run/node-isolate.d.ts +6 -2
  35. package/dist/run/node-isolate.js +2 -0
  36. package/dist/run/run-host.d.ts +2 -2
  37. package/dist/run/worker-entry-browser.d.ts +6 -1
  38. package/dist/run/worker-entry-browser.js +62 -5
  39. package/package.json +11 -11
@@ -0,0 +1,42 @@
1
+ {
2
+ "outcomes": [
3
+ { "kind": "lastRun", "values": {} },
4
+ {
5
+ "kind": "ran",
6
+ "values": { "total": 3 },
7
+ "failureKinds": [],
8
+ "traceOk": true
9
+ },
10
+ {
11
+ "kind": "lastRun",
12
+ "values": { "total": 3 },
13
+ "valueStatuses": { "total": "stale" },
14
+ "traceOk": true
15
+ },
16
+ {
17
+ "kind": "ran",
18
+ "values": { "total": 3 },
19
+ "failureKinds": [],
20
+ "traceOk": true
21
+ },
22
+ { "kind": "forgotten" },
23
+ {
24
+ "kind": "ran",
25
+ "values": { "total": 3 },
26
+ "failureKinds": [],
27
+ "traceOk": true
28
+ }
29
+ ],
30
+ "diagnostics": [
31
+ "manual run: net declaration mismatch",
32
+ "uses net access to api.example.com",
33
+ "manual run: net declaration mismatch",
34
+ "uses net access to api.example.com",
35
+ "manual run: net declaration mismatch",
36
+ "uses net access to api.example.com"
37
+ ],
38
+ "prompts": [
39
+ "This note's scripts can send data to api.example.com. Allow?",
40
+ "This note's scripts can send data to api.example.com. Allow?"
41
+ ]
42
+ }
@@ -0,0 +1,5 @@
1
+ # Last run and grants
2
+
3
+ ```lua {name=total}
4
+ return net.fetch_json("https://api.example.com/orders")
5
+ ```
@@ -0,0 +1,27 @@
1
+ {
2
+ "id": "last-run-and-grants",
3
+ "invariant": "a run's values and trace can be read back with no storage key, and clearing the stored grants makes the next manual run prompt again",
4
+ "clock": 1700000000000,
5
+ "actions": [
6
+ { "type": "lastRun" },
7
+ {
8
+ "type": "run",
9
+ "trigger": "manual",
10
+ "answers": ["allow"],
11
+ "simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
12
+ },
13
+ { "type": "lastRun" },
14
+ {
15
+ "type": "run",
16
+ "trigger": "manual",
17
+ "simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
18
+ },
19
+ { "type": "forgetGrants" },
20
+ {
21
+ "type": "run",
22
+ "trigger": "manual",
23
+ "answers": ["allow"],
24
+ "simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
25
+ }
26
+ ]
27
+ }
@@ -47,6 +47,8 @@ needs, is the runner's own `ACTION_CAPABILITY` map:
47
47
  |---|---|
48
48
  | `open` | (always supported) |
49
49
  | `run` | `isolate` |
50
+ | `lastRun` | (always supported) |
51
+ | `forgetGrants` | (always supported) |
50
52
  | `exportHtml` | `export-format` (`html`) |
51
53
  | `exportAnsi` | `export-format` (`ansi`) |
52
54
  | `exportLinkedSet` | `linked-set-export` (a `resolveLinkedSetTarget`) |
@@ -137,6 +139,9 @@ coarse, matching each `kind`:
137
139
  (`{name: value}`); `failureKinds` compares the closed `FailureKind`
138
140
  taxonomy only, never a raw message or a timing; an optional `traceOk`
139
141
  compares the recorded run trace's `ok` flag.
142
+ - `lastRun`: `values` compares the stored values shown on open, by name,
143
+ like `ran`. `valueStatuses` compares each value's status by name, and
144
+ `traceOk` compares the recorded trace's `ok` flag.
140
145
  - `exported`: `contains`/`excludes` are substrings checked against the
141
146
  written file's content, never the whole document, the render corpora
142
147
  already pin bytes. `valueCount` is how many last-run values were baked
@@ -147,7 +152,8 @@ coarse, matching each `kind`:
147
152
  of the folder the pack was unzipped into.
148
153
  - `declined`: `step` (`'consent'`/`'replace'`, or absent for the
149
154
  scripts-disabled synthetic outcome) and `reasonContains`, a substring of
150
- the user-facing reason.
155
+ the user-facing reason. A user's "no" to a prompt and a cancelled picker
156
+ are both `declined`.
151
157
  - `listed`: `enabled` and `notEnabled`, namespace lists, order-independent.
152
158
  - `enabled`, `disabled`, `placed`, `removed`: `namespace`.
153
159
  - `loaded`: `namespaces`, order-independent.
@@ -159,8 +165,8 @@ coarse, matching each `kind`:
159
165
  accepted container item.
160
166
  - `hover`: `contains`, a substring of the hover text. Optional `start` and
161
167
  `end` compare the hovered name's columns.
162
- - every other `kind` (`opened`, `none`, `not-found`, `failed`, `rejected`,
163
- `reserved`, `cancelled`, `inserted`): `kind` equality is the whole check.
168
+ - every other `kind` (`opened`, `none`, `not-found`, `failed`, `invalid`,
169
+ `reserved`, `forgotten`, `inserted`): `kind` equality is the whole check.
164
170
 
165
171
  An action whose capability the adapter under test does not declare is
166
172
  never diffed against `expected.json` at all (see "Capability handling"
@@ -200,7 +206,7 @@ run the whole corpus honestly: its result records say `unsupported`
200
206
  where it genuinely offers nothing, which is the truth and is visible,
201
207
  rather than a skipped test or a faked empty answer.
202
208
 
203
- ## The sixteen scenarios
209
+ ## The scenarios
204
210
 
205
211
  | id | what it proves |
206
212
  |---|---|
@@ -221,6 +227,7 @@ rather than a skipped test or a faked empty answer.
221
227
  | `15-load-packs-engine-views` | loading packs reports every installed component and, as `unavailable`, the ones this host's engine has no view for |
222
228
  | `16-pack-trust` | a folder that arrives without consent is listed as present and never loaded; enable, disable, re-enable and remove move the device trust list; a host without the list reports the whole scenario `unsupported` |
223
229
  | `17-export-linked-set` | a note and the notes it links to export as one archive with an index page; a host with no linked-set target reports `unsupported` |
230
+ | `18-last-run-and-grants` | a run's values and trace read back through `lastRun` with no storage key, and clearing the grants makes the next manual run prompt again |
224
231
 
225
232
  The capabilities an adapter declares decide which scenarios report
226
233
  `unsupported` on that host. That is a reported gap and not a corpus defect.
@@ -1,2 +1,2 @@
1
- export { ACTION_CAPABILITY, assertHostScenario, containsInOrder, createAnswerQueue, hostConformanceDir, hostDeclaresCapability, listHostScenarioNames, loadHostScenario, outcomeMismatches, runHostScenario, zipArchiveDirectory, } from './host/conformance-runner.js';
1
+ export { assertHostScenario, createAnswerQueue, hostConformanceDir, listHostScenarioNames, loadHostScenario, runHostScenario, } from './host/conformance-runner.js';
2
2
  export type { HostScenario, HostScenarioAction, HostScenarioCapability, HostScenarioExpected, HostScenarioRunResult, LoadedHostScenario, RunSimulation, } from './host/conformance-runner.js';
@@ -2,4 +2,4 @@
2
2
  // the corpus from disk and drives a real adapter). A host's adapter test
3
3
  // imports this to run `conformance/host/` against its own `HostAdapter`.
4
4
  // The corpus ships inside the package at `dist/conformance/host/`.
5
- export { ACTION_CAPABILITY, assertHostScenario, containsInOrder, createAnswerQueue, hostConformanceDir, hostDeclaresCapability, listHostScenarioNames, loadHostScenario, outcomeMismatches, runHostScenario, zipArchiveDirectory, } from './host/conformance-runner.js';
5
+ export { assertHostScenario, createAnswerQueue, hostConformanceDir, listHostScenarioNames, loadHostScenario, runHostScenario, } from './host/conformance-runner.js';
@@ -54,9 +54,9 @@ export type HostIsolate = {
54
54
  */
55
55
  workerPath(): string;
56
56
  /**
57
- * Creates the worker-thread isolate: `workerThreadIsolate` from
58
- * `@markii/host/node`. Injected so the shared run path imports
59
- * nothing from `node:*`.
57
+ * Creates the worker-thread isolate. `createNodeHostIsolate()` from
58
+ * `@markii/host/node` supplies it. Injected so the shared run path
59
+ * imports nothing from `node:*`.
60
60
  */
61
61
  readonly spawnIsolate: IsolateSpawner;
62
62
  } | {
@@ -155,16 +155,30 @@ export interface HostPackSource {
155
155
  }
156
156
  /** Every export format any host offers today. */
157
157
  export type HostExportFormat = 'html' | 'pdf' | 'ansi' | 'md-plain';
158
+ /**
159
+ * What a host declares to offer exports. A request can carry its own
160
+ * `targetPath`, so a host with no picker (the command line tool, or a call
161
+ * site that already knows where the file goes) leaves the resolvers out.
162
+ *
163
+ * A single-note export needs `exports` declared with the format in
164
+ * `formats`, and a destination: either a `resolveTarget` or the request's
165
+ * `targetPath`. Without a destination it answers `unsupported`.
166
+ *
167
+ * The linked-set export is offered when `exports` is declared and the host
168
+ * has a `resolveLinkedSetTarget` or the request carries a `targetPath`.
169
+ * Without either it answers `unsupported`. A host that declares `exports`
170
+ * with neither resolver, such as the command line tool, therefore offers
171
+ * the linked-set export only to a caller that names the path.
172
+ */
158
173
  export interface HostExportCapabilities {
159
174
  readonly formats: ReadonlySet<HostExportFormat>;
160
- /** GUI hosts show a picker; the CLI returns the path it was given. */
161
- resolveTarget(suggestedFileName: string, format: HostExportFormat): Promise<string | undefined>;
175
+ /** GUI hosts show a picker; the CLI returns the path it was given. Resolves `undefined` when the user cancels. Absent for a host whose callers always pass `targetPath`. */
176
+ resolveTarget?(suggestedFileName: string, format: HostExportFormat): Promise<string | undefined>;
162
177
  /** Only present on a host declaring `'pdf'`. The one Electron-touching seam. */
163
178
  htmlToPdf?(html: string): Promise<Uint8Array>;
164
179
  /**
165
180
  * Names the zip archive of a linked-set export, or resolves `undefined`
166
- * when the user cancels. Present only on a host that offers the
167
- * linked-set export; a host can offer single-note export without it
181
+ * when the user cancels. A host can offer single-note export without it
168
182
  * (the command line tool does), so it is a member of its own rather than
169
183
  * a `HostExportFormat`.
170
184
  */
@@ -124,6 +124,8 @@ export declare function cascadeDiagnosticLines(outcome: CascadeExportOutcome): s
124
124
  export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'readNote'> {
125
125
  /** The suggested archive file name handed to the host's picker. */
126
126
  readonly suggestedFileName: string;
127
+ /** Where to write the archive. When present the adapter's `resolveLinkedSetTarget` is not asked. */
128
+ readonly targetPath?: string;
127
129
  }
128
130
  /**
129
131
  * Exports a note and the notes it links to as one archive: resolves the
@@ -131,6 +133,6 @@ export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'read
131
133
  * note through `adapter.readFile`, writes it through `adapter.writeFile`,
132
134
  * and sends the diagnostics lines to `adapter.diagnostics`. Resolves
133
135
  * `undefined` when the host has no linked-set target or its picker was
134
- * cancelled. Never throws.
136
+ * cancelled. A `targetPath` skips the picker. Never throws.
135
137
  */
136
138
  export declare function exportLinkedSetViaAdapter(adapter: HostAdapter, inputs: LinkedSetExportInputs): Promise<CascadeExportOutcome | undefined>;
@@ -155,17 +155,20 @@ export function cascadeDiagnosticLines(outcome) {
155
155
  * note through `adapter.readFile`, writes it through `adapter.writeFile`,
156
156
  * and sends the diagnostics lines to `adapter.diagnostics`. Resolves
157
157
  * `undefined` when the host has no linked-set target or its picker was
158
- * cancelled. Never throws.
158
+ * cancelled. A `targetPath` skips the picker. Never throws.
159
159
  */
160
160
  export async function exportLinkedSetViaAdapter(adapter, inputs) {
161
161
  const resolveTarget = adapter.exports?.resolveLinkedSetTarget;
162
- if (resolveTarget === undefined)
162
+ const { suggestedFileName, targetPath, ...archiveRequest } = inputs;
163
+ if (targetPath === undefined && resolveTarget === undefined) {
163
164
  return undefined;
164
- const { suggestedFileName, ...archiveRequest } = inputs;
165
+ }
165
166
  let path;
166
167
  let outcome;
167
168
  try {
168
- path = await resolveTarget.call(adapter.exports, suggestedFileName);
169
+ path =
170
+ targetPath ??
171
+ (await resolveTarget?.call(adapter.exports, suggestedFileName));
169
172
  if (path === undefined)
170
173
  return undefined;
171
174
  const target = path;
@@ -31,6 +31,14 @@ export type HostScenarioAction = {
31
31
  /** When `true`, this action never reaches `host.run` at all — see this file's top comment on the scripts-disabled scenario. */
32
32
  readonly scriptsDisabled?: boolean;
33
33
  readonly simulate?: RunSimulation;
34
+ }
35
+ /** Reads what the note's last run left: the values to show on open and the trace. */
36
+ | {
37
+ readonly type: 'lastRun';
38
+ }
39
+ /** Clears the note's stored script grants. */
40
+ | {
41
+ readonly type: 'forgetGrants';
34
42
  } | {
35
43
  readonly type: 'exportHtml';
36
44
  } | {
@@ -44,7 +44,7 @@ import { usesPackTrust } from './pack-trust-behavior.js';
44
44
  import { join, relative, sep } from 'node:path';
45
45
  import { unzipSync, zipSync } from 'fflate';
46
46
  import { conformanceDir } from '@markii/core/corpus';
47
- import { createMarkiiHost, } from './create-host.js';
47
+ import { createMarkiiHostWithSeams, } from './create-host.js';
48
48
  import { scriptsDisabledDiagnosticLine, scriptsDisabledNotice, } from './script-execution.js';
49
49
  // --- corpus location ----------------------------------------------------
50
50
  /**
@@ -99,6 +99,8 @@ export function zipArchiveDirectory(archiveDir) {
99
99
  export const ACTION_CAPABILITY = {
100
100
  open: undefined,
101
101
  run: 'isolate',
102
+ lastRun: undefined,
103
+ forgetGrants: undefined,
102
104
  exportHtml: 'export-format',
103
105
  exportAnsi: 'export-format',
104
106
  exportLinkedSet: 'linked-set-export',
@@ -258,7 +260,7 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
258
260
  },
259
261
  };
260
262
  let currentSimulation;
261
- const host = createMarkiiHost(recording, {
263
+ const host = createMarkiiHostWithSeams(recording, {
262
264
  // The runner is Node-only test tooling, so it supplies the real Node
263
265
  // implementations of the seams the host layer takes by injection.
264
266
  htmlEngine: createHtmlExportEngine(),
@@ -324,6 +326,18 @@ async function runOneAction(host, adapter, action, ctx) {
324
326
  trigger: action.trigger,
325
327
  });
326
328
  }
329
+ case 'lastRun': {
330
+ const last = host.lastRun(ctx.notePath);
331
+ return {
332
+ kind: 'lastRun',
333
+ values: last.values,
334
+ persistedValues: last.persistedValues,
335
+ ...(last.trace !== undefined ? { trace: last.trace } : {}),
336
+ };
337
+ }
338
+ case 'forgetGrants':
339
+ await host.forgetGrants(ctx.notePath);
340
+ return { kind: 'forgotten' };
327
341
  case 'exportHtml':
328
342
  case 'exportAnsi': {
329
343
  const outcome = await host.exportNote({
@@ -475,6 +489,30 @@ export function outcomeMismatches(actual, expected) {
475
489
  }
476
490
  break;
477
491
  }
492
+ case 'lastRun': {
493
+ const wantValues = expected['values'] ?? {};
494
+ const gotValues = landedValues(actualRecord['values']);
495
+ if (JSON.stringify(gotValues) !== JSON.stringify(wantValues)) {
496
+ mismatches.push(`values: expected ${JSON.stringify(wantValues)}, got ${JSON.stringify(gotValues)}`);
497
+ }
498
+ if (expected['valueStatuses'] !== undefined) {
499
+ const statuses = {};
500
+ const raw = (actualRecord['values'] ?? {});
501
+ for (const [name, entry] of Object.entries(raw)) {
502
+ statuses[name] = entry.status;
503
+ }
504
+ if (JSON.stringify(statuses) !== JSON.stringify(expected['valueStatuses'])) {
505
+ mismatches.push(`value statuses: expected ${JSON.stringify(expected['valueStatuses'])}, got ${JSON.stringify(statuses)}`);
506
+ }
507
+ }
508
+ if (expected['traceOk'] !== undefined) {
509
+ const trace = actualRecord['trace'];
510
+ if (trace?.ok !== expected['traceOk']) {
511
+ mismatches.push(`trace.ok: expected ${String(expected['traceOk'])}, got ${String(trace?.ok)}`);
512
+ }
513
+ }
514
+ break;
515
+ }
478
516
  case 'exported': {
479
517
  if (expected['valueCount'] !== undefined &&
480
518
  actualRecord['valueCount'] !== expected['valueCount']) {