@opetope/devtools 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.
Files changed (106) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +21 -0
  3. package/README.md +105 -0
  4. package/README.ru.md +105 -0
  5. package/dist/activity-export.d.ts +4 -0
  6. package/dist/activity.d.ts +63 -0
  7. package/dist/conformance-activity.d.ts +8 -0
  8. package/dist/conformance.d.ts +37 -0
  9. package/dist/control-subject.d.ts +15 -0
  10. package/dist/control-ui-DMmVy9Ob.js +16 -0
  11. package/dist/control-ui-DMmVy9Ob.js.map +1 -0
  12. package/dist/control.d.ts +45 -0
  13. package/dist/corpus.d.ts +33 -0
  14. package/dist/decode-activity.d.ts +4 -0
  15. package/dist/decode-shapes.d.ts +32 -0
  16. package/dist/decode-values.d.ts +36 -0
  17. package/dist/decode.d.ts +41 -0
  18. package/dist/digest.d.ts +17 -0
  19. package/dist/export-run-DIQZax_d.js +2 -0
  20. package/dist/export-run-DIQZax_d.js.map +1 -0
  21. package/dist/export-snapshot-Buw-c6ZG.js +2 -0
  22. package/dist/export-snapshot-Buw-c6ZG.js.map +1 -0
  23. package/dist/export-snapshot.d.ts +34 -0
  24. package/dist/extension-points.d.ts +45 -0
  25. package/dist/fixture-plan.d.ts +62 -0
  26. package/dist/fixture-runtime.d.ts +37 -0
  27. package/dist/fixture.d.ts +23 -0
  28. package/dist/fold-snapshot-DjiURn8v.js +2 -0
  29. package/dist/fold-snapshot-DjiURn8v.js.map +1 -0
  30. package/dist/fold-snapshot.d.ts +15 -0
  31. package/dist/graph-index.d.ts +104 -0
  32. package/dist/graph-store.d.ts +77 -0
  33. package/dist/graph.d.ts +211 -0
  34. package/dist/index.d.ts +33 -0
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/inspector.d.ts +70 -0
  38. package/dist/install.d.ts +44 -0
  39. package/dist/install.js +2 -0
  40. package/dist/install.js.map +1 -0
  41. package/dist/integration.d.ts +42 -0
  42. package/dist/integration.js +2 -0
  43. package/dist/integration.js.map +1 -0
  44. package/dist/legend-DT7j0_pD.js +2 -0
  45. package/dist/legend-DT7j0_pD.js.map +1 -0
  46. package/dist/node-facts.d.ts +20 -0
  47. package/dist/node-state.d.ts +152 -0
  48. package/dist/optional.d.ts +9 -0
  49. package/dist/panel-5jvisQBr.js +367 -0
  50. package/dist/panel-5jvisQBr.js.map +1 -0
  51. package/dist/producer-ids.d.ts +15 -0
  52. package/dist/protocol-joKTjlMj.js +2 -0
  53. package/dist/protocol-joKTjlMj.js.map +1 -0
  54. package/dist/protocol.d.ts +238 -0
  55. package/dist/react/activity-rows.d.ts +12 -0
  56. package/dist/react/activity-view.d.ts +9 -0
  57. package/dist/react/control-locale.d.ts +25 -0
  58. package/dist/react/control-styles.d.ts +7 -0
  59. package/dist/react/control-ui.d.ts +37 -0
  60. package/dist/react/edge-filter.d.ts +36 -0
  61. package/dist/react/edge-filters.d.ts +17 -0
  62. package/dist/react/export-delivery.d.ts +33 -0
  63. package/dist/react/export-run.d.ts +20 -0
  64. package/dist/react/format.d.ts +17 -0
  65. package/dist/react/glyph.d.ts +20 -0
  66. package/dist/react/graph-layout.d.ts +67 -0
  67. package/dist/react/graph-view.d.ts +30 -0
  68. package/dist/react/graph-viewport.d.ts +48 -0
  69. package/dist/react/icons.d.ts +12 -0
  70. package/dist/react/inspector-view.d.ts +29 -0
  71. package/dist/react/label.d.ts +28 -0
  72. package/dist/react/lazy-module.d.ts +38 -0
  73. package/dist/react/legend-locale.d.ts +23 -0
  74. package/dist/react/legend.d.ts +10 -0
  75. package/dist/react/locale-context.d.ts +17 -0
  76. package/dist/react/locale.d.ts +115 -0
  77. package/dist/react/main-column.d.ts +22 -0
  78. package/dist/react/panel-header.d.ts +20 -0
  79. package/dist/react/panel-mode.d.ts +3 -0
  80. package/dist/react/panel-status.d.ts +19 -0
  81. package/dist/react/panel.d.ts +41 -0
  82. package/dist/react/search-results.d.ts +18 -0
  83. package/dist/react/selection.d.ts +15 -0
  84. package/dist/react/styles.d.ts +13 -0
  85. package/dist/react/theme.d.ts +39 -0
  86. package/dist/react/timeline-locale.d.ts +20 -0
  87. package/dist/react/timeline-view.d.ts +14 -0
  88. package/dist/react/tree.d.ts +26 -0
  89. package/dist/react/use-store.d.ts +11 -0
  90. package/dist/react.d.ts +16 -0
  91. package/dist/react.js +2 -0
  92. package/dist/react.js.map +1 -0
  93. package/dist/search-CC7avy6I.js +2 -0
  94. package/dist/search-CC7avy6I.js.map +1 -0
  95. package/dist/search.d.ts +20 -0
  96. package/dist/session-qjKT0E69.js +2 -0
  97. package/dist/session-qjKT0E69.js.map +1 -0
  98. package/dist/session.d.ts +59 -0
  99. package/dist/testing.d.ts +10 -0
  100. package/dist/testing.js +4 -0
  101. package/dist/testing.js.map +1 -0
  102. package/dist/timeline-view-ByPB1a7-.js +2 -0
  103. package/dist/timeline-view-ByPB1a7-.js.map +1 -0
  104. package/dist/timeline.d.ts +57 -0
  105. package/dist/visual-axes.d.ts +35 -0
  106. package/package.json +107 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # @opetope/devtools
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release. See the package README for the supported API and usage.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksei Berezin
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,105 @@
1
+ # @opetope/devtools
2
+
3
+ Inspector of the Opetope runtime graph: one projection of the compiled topology and current state, answering
4
+ "why is this node in this state". Observation is read-only; a separate optional control port permits condition
5
+ overrides and retrying cleanup. An optional development tool; keep its integration out of production bundles.
6
+
7
+ The normative document is [`devtools.md`](../runtime/docs/devtools.md) — protocol, observation architecture, panel and
8
+ condition control. Protocol vocabulary is the public vocabulary of [`spec.md`](../runtime/docs/spec.md) §3.
9
+
10
+ ## Installation
11
+
12
+ ```sh
13
+ npm install @opetope/core @opetope/runtime @opetope/devtools 'react@^19.0.0' 'react-dom@^19.0.0'
14
+ ```
15
+
16
+ Use matching Opetope versions. For release candidates, append `@next` to every `@opetope/*` package in the command.
17
+ The API is ESM-only; Node 20.19+ and React/React DOM `>=19.0.0 <20` are required (D251). Development check commands below apply to a contributor checkout.
18
+
19
+ The normative EN/RU guides are shipped in `@opetope/runtime`: after installing it, open
20
+ `node_modules/@opetope/runtime/docs/spec.md` or `spec.ru.md`; recipes are in `cookbook.md` and `cookbook.ru.md`.
21
+ No GitHub access is needed to read those installed guides.
22
+
23
+ ## Layers
24
+
25
+ | Layer | Files | What it holds |
26
+ | ------------------- | --------------------------------------------------- | --------------------------------------------------------------------- |
27
+ | Protocol | `protocol.ts`, `node-state.ts`, `graph.ts` | Executable schema: ids, hard and weak edges, six automata, three axes |
28
+ | Reducer and indexes | `graph-store.ts`, `graph-index.ts`, `timeline.ts` | Incremental apply, gap/duplicate/stale, bounded history |
29
+ | Projections | `inspector.ts`, `visual-axes.ts`, `search.ts` | Dependencies, owned nodes, causal chain, search over protocol fields |
30
+ | Session client | `session.ts` | Cursor pull, coalescing, resync, out-of-band failure |
31
+ | Export | `export-snapshot.ts` | Data-only JSON under limits, no network transport |
32
+ | Hostile boundary | `decode.ts`, `decode-shapes.ts`, `decode-values.ts` | Strict decoder with record and byte budgets |
33
+ | Protocol acceptance | `conformance.ts`, `digest.ts` | `assertInspectionSessionContract`, canonical JSON, digest |
34
+ | Performance corpus | `corpus.ts` | Generated graph, 1 330/5 008 and 26 331/70 040, deterministic |
35
+ | Panel | `react/*` | Drawer, graph and timeline, inspector, legend, locale |
36
+ | Fixture | `fixture*.ts`, `fold-snapshot.ts` | A `sampleApp` graph as a real `RuntimeInspectionSession` |
37
+
38
+ ## Package boundary
39
+
40
+ `@opetope/devtools` imports no host design-system package, and `RuntimeInspectionSession` is a _consumed_ interface:
41
+ the runtime owns the semantics and implements it behind its own trusted seam, so the binding is structural and the
42
+ runtime knows nothing about devtools. Only `integration.ts` reaches for an `@opetope/*` package, and only to open the
43
+ ports the application would otherwise have to open itself (D248); the protocol, the store, the projections and the
44
+ panel hold none. React is a peer of this workspace only; the headless entry works without a renderer.
45
+
46
+ ## Attaching
47
+
48
+ One call in the application composition root, with the `ApplicationExecution` that `openApplication` returned.
49
+ Feature code never imports `./integration`.
50
+
51
+ ```ts
52
+ import { installDevtools } from '@opetope/devtools/integration';
53
+ import { defaultTheme } from '@opetope/devtools/react';
54
+
55
+ const devtools = installDevtools({ execution, mode: 'control', theme: defaultTheme });
56
+ ```
57
+
58
+ The call opens the observation port itself, and the write port too under `mode: 'control'`; `devtools.close()` closes
59
+ both, so no override outlives the panel. The default `inspect` leaves the panel read-only and asks the runtime for no
60
+ authority. An application therefore never imports `@opetope/runtime/internal` for the sake of a debugger.
61
+
62
+ The panel mounts in its own React root outside the application tree: it belongs to no group that could switch it off,
63
+ and opening it never re-renders the application. `defaultTheme` from `@opetope/devtools/react` provides an explicit neutral palette;
64
+ the package defaults look acceptable without a design system.
65
+
66
+ ## Fixture and preview
67
+
68
+ The fixture is not a store mock but a real `RuntimeInspectionSession`: the snapshot is taken _before_ the cascade, so
69
+ connecting to it runs the reducer and fills the causal chain. `dropNext()` drops a committed frame from the servable
70
+ ring and reproduces `queue overflow → gap → resync`.
71
+
72
+ Its story: the session ends, the condition group retires, `sessions.confirmAction` closes cleanly and `tasks.resource`
73
+ fails the cleanup of its attachment and is quarantined, so the group never reaches `closed`. The feature that imports
74
+ tasks weakly keeps running on a `missing` lookup instead of failing with it.
75
+
76
+ ```ts
77
+ import { assertInspectionSessionContract, createFixtureSession } from '@opetope/devtools/testing';
78
+ ```
79
+
80
+ To see the panel without an application: `node packages/devtools/scripts/preview-server.mjs`, which bundles into a
81
+ gitignored `dist/` and serves `http://localhost:4319`.
82
+
83
+ ## Commands
84
+
85
+ ```bash
86
+ npm run ci:type # tsc --noEmit
87
+ npm run ci:eslint # boundaries, complexity, react-perf, sorting
88
+ npm run ci:test # jest, jsdom
89
+ npm run ci:size-limit # headless and panel consumers
90
+ npm run build # rollup: index, react, integration, testing
91
+ ```
92
+
93
+ Diagnostics, run by hand and never gates: `node --expose-gc ./scripts/retention-matrix.mjs` measures the
94
+ close/detach/GC matrix, and `node --expose-gc ./scripts/retainer-path.mjs` writes a heap snapshot and names what
95
+ holds a container.
96
+
97
+ ## Open
98
+
99
+ The graph/timeline tabs, edge filter chips and condition-control surface are implemented; their contracts are in
100
+ [`devtools.md`](../runtime/docs/devtools.md) §4–5. A host enables `quarantine` through `openApplication` to make failed
101
+ cleanup retryable (D182).
102
+
103
+ List virtualization and worker layout remain unimplemented design targets; the proposed thresholds require
104
+ implementation and measurement. D206 adds physical activity facts to inspection; application scenarios are available from `@opetope/react/testing`.
105
+ Coverage and observation limits are documented in [devtools.md](../runtime/docs/devtools.md).
package/README.ru.md ADDED
@@ -0,0 +1,105 @@
1
+ # @opetope/devtools
2
+
3
+ Инспектор рантайм-графа Opetope: одна проекция скомпилированной топологии и текущего состояния с ответом на вопрос
4
+ «почему этот узел сейчас в этом состоянии». Наблюдение read-only; отдельный опциональный control-порт разрешает
5
+ override условий и повтор уборки. Приватный workspace, в прод не попадает.
6
+
7
+ Нормативный документ — [`devtools.md`](../runtime/docs/devtools.ru.md): протокол, архитектура наблюдения, панель и управление
8
+ условиями. Словарь протокола — публичный словарь [`spec.md`](../runtime/docs/spec.ru.md) §3.
9
+
10
+ ## Установка
11
+
12
+ ```sh
13
+ npm install @opetope/core @opetope/runtime @opetope/devtools 'react@^19.0.0' 'react-dom@^19.0.0'
14
+ ```
15
+
16
+ Используйте согласованные версии Opetope. Для release candidate добавьте `@next` каждому пакету `@opetope/*` в команде.
17
+ API поставляется только в ESM; требуются Node 20.19+ и React/React DOM `>=19.0.0 <20` (D251). Команды разработки ниже относятся к contributor checkout.
18
+
19
+ Нормативные руководства EN/RU поставляются в `@opetope/runtime`: после его установки откройте
20
+ `node_modules/@opetope/runtime/docs/spec.md` или `spec.ru.md`; рецепты находятся в `cookbook.md` и `cookbook.ru.md`.
21
+ Для чтения установленных руководств доступ к GitHub не нужен.
22
+
23
+ ## Слои
24
+
25
+ | Слой | Файлы | Что держит |
26
+ | ------------------ | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
27
+ | Протокол | `protocol.ts`, `node-state.ts`, `graph.ts` | Executable schema: идентификаторы, жёсткие и слабые рёбра, шесть автоматов, три оси |
28
+ | Reducer и индексы | `graph-store.ts`, `graph-index.ts`, `timeline.ts` | Инкрементальный apply, gap/duplicate/stale, ограниченная история |
29
+ | Проекции | `inspector.ts`, `visual-axes.ts`, `search.ts` | Зависимости, владеемые узлы, причинная цепочка, поиск по полям протокола |
30
+ | Клиент сессии | `session.ts` | Pull по курсору, coalescing, resync, отказ вне очереди |
31
+ | Экспорт | `export-snapshot.ts` | Data-only JSON под лимитами, без сетевого транспорта |
32
+ | Враждебная граница | `decode.ts`, `decode-shapes.ts`, `decode-values.ts` | Строгий декодер с бюджетами по записям и байтам |
33
+ | Приёмка протокола | `conformance.ts`, `digest.ts` | `assertInspectionSessionContract`, канонический JSON, digest |
34
+ | Перф-корпус | `corpus.ts` | Генерируемый граф, 1 330/5 008 и 26 331/70 040, детерминированный |
35
+ | Панель | `react/*` | Drawer, граф и хронология, инспектор, легенда, локаль |
36
+ | Фикстура | `fixture*.ts`, `fold-snapshot.ts` | Граф `sampleApp` как настоящая `RuntimeInspectionSession` |
37
+
38
+ ## Граница пакета
39
+
40
+ `@opetope/devtools` не импортирует a host design system, а `RuntimeInspectionSession` это _потребляемый_
41
+ интерфейс: семантикой владеет рантайм и реализует её за своим доверенным швом, поэтому связь только структурная, а
42
+ рантайм ничего не знает о devtools. За пакетом `@opetope/*` тянется только `integration.ts`, и только чтобы открыть
43
+ порты, которые иначе пришлось бы открывать приложению (D248); ни протокол, ни стор, ни проекции, ни панель их не
44
+ держат. React — peer только этого workspace; headless-вход работает без рендерера.
45
+
46
+ ## Подключение
47
+
48
+ Один вызов в composition root приложения, с тем `ApplicationExecution`, который вернул `openApplication`. Код фич
49
+ никогда не импортирует `./integration`.
50
+
51
+ ```ts
52
+ import { installDevtools } from '@opetope/devtools/integration';
53
+ import { defaultTheme } from '@opetope/devtools/react';
54
+
55
+ const devtools = installDevtools({ execution, mode: 'control', theme: defaultTheme });
56
+ ```
57
+
58
+ Вызов сам открывает порт наблюдения, а при `mode: 'control'` и порт записи; `devtools.close()` закрывает оба, поэтому
59
+ override не переживает панель. Умолчание `inspect` оставляет панель read-only и не просит у рантайма authority,
60
+ которой не воспользуется. Приложению поэтому вообще не нужно импортировать `@opetope/runtime/internal` ради отладчика.
61
+
62
+ Панель монтируется в собственный React root вне дерева приложения: она не принадлежит группе, которая могла бы её
63
+ выключить, и её открытие не перерисовывает приложение. `defaultTheme` из `@opetope/devtools/react` задаёт нейтральную палитру;
64
+ умолчания пакета выглядят приемлемо и без дизайн-системы.
65
+
66
+ ## Фикстура и превью
67
+
68
+ Фикстура это не мок стора, а настоящая `RuntimeInspectionSession`: снимок берётся _до_ каскада, поэтому подключение
69
+ прогоняет reducer и наполняет причинную цепочку. `dropNext()` роняет зафиксированный кадр из отдаваемого кольца и
70
+ воспроизводит `queue overflow → gap → resync`.
71
+
72
+ Её сюжет: сессия заканчивается, группа условий уходит, `sessions.confirmAction` закрывается чисто, а `tasks.resource`
73
+ проваливает уборку своего attachment-а и уходит в карантин, поэтому группа так и не доходит до `closed`. Фича,
74
+ которая импортирует tasks слабым ребром, продолжает работать на `missing`, а не падает вместе с ним.
75
+
76
+ ```ts
77
+ import { assertInspectionSessionContract, createFixtureSession } from '@opetope/devtools/testing';
78
+ ```
79
+
80
+ Посмотреть панель без приложения: `node packages/devtools/scripts/preview-server.mjs` — соберёт бандл в gitignored
81
+ `dist/` и отдаст страницу на `http://localhost:4319`.
82
+
83
+ ## Команды
84
+
85
+ ```bash
86
+ npm run ci:type # tsc --noEmit
87
+ npm run ci:eslint # границы, complexity, react-perf, сортировки
88
+ npm run ci:test # jest, jsdom
89
+ npm run ci:size-limit # headless и panel консьюмеры
90
+ npm run build # rollup: index, react, integration, testing
91
+ ```
92
+
93
+ Диагностики, запускаются руками и гейтами не являются: `node --expose-gc ./scripts/retention-matrix.mjs` снимает
94
+ матрицу close/detach/GC, а `node --expose-gc ./scripts/retainer-path.mjs` пишет heap snapshot и называет того, кто
95
+ держит контейнер.
96
+
97
+ ## Что осталось
98
+
99
+ Вкладки графа/хронологии, чипы фильтра рёбер и поверхность управления условиями реализованы; их контракты описаны в
100
+ [`devtools.md`](../runtime/docs/devtools.ru.md) §4–5. Хост включает `quarantine` через `openApplication`, чтобы отказавшую
101
+ уборку можно было повторить (D182).
102
+
103
+ Виртуализация списков и worker layout остаются нереализованными проектными целями; предложенные пороги требуют
104
+ реализации и измерения. D206 добавляет наблюдение физической activity; сценарии приложения доступны из `@opetope/react/testing`.
105
+ Покрытие и ограничения наблюдения описаны в [devtools.md](../runtime/docs/devtools.ru.md).
@@ -0,0 +1,4 @@
1
+ import type { RuntimeActivitySnapshot } from './activity.js';
2
+ /** Export caps are independent from the live producer's recording cap. */
3
+ declare function clipActivity(snapshot: RuntimeActivitySnapshot, limit: number): RuntimeActivitySnapshot;
4
+ export { clipActivity };
@@ -0,0 +1,63 @@
1
+ /** D206: bounded, data-only facts from the existing inspection port; never product inputs, values or error objects. */
2
+ type ActivityOwner = Readonly<{
3
+ feature: string;
4
+ generation: number;
5
+ instance: string;
6
+ }>;
7
+ type FeatureActivity = ActivityOwner & Readonly<{
8
+ body: 'eager' | 'loaded' | 'loading' | 'not-loaded' | 'unknown';
9
+ demandCount: 'unknown';
10
+ id: string;
11
+ phase: 'draining' | 'failed' | 'fenced' | 'opening' | 'quarantined' | 'ready';
12
+ }>;
13
+ type CallActivity = Readonly<{
14
+ callId: string;
15
+ declaration: string;
16
+ id: string;
17
+ lane: Readonly<{
18
+ blockedBy: readonly string[] | 'unknown';
19
+ declaration: string;
20
+ id: string;
21
+ }> | null;
22
+ owner: ActivityOwner;
23
+ pendingWaiters: number;
24
+ state: 'draining' | 'queued' | 'running';
25
+ }>;
26
+ type ResourceLoadActivity = Readonly<{
27
+ attempt: number;
28
+ generation: number;
29
+ id: string;
30
+ state: 'draining' | 'running' | 'scheduled';
31
+ }>;
32
+ type ResourceActivity = Readonly<{
33
+ generation: number;
34
+ id: string;
35
+ kind: 'resource' | 'stream';
36
+ loads: readonly ResourceLoadActivity[] | 'unknown';
37
+ owner: ActivityOwner;
38
+ retainers: 'unknown' | number;
39
+ retention: 'observer' | 'scope' | 'unknown';
40
+ state: 'error' | 'fenced' | 'idle' | 'opening' | 'ready' | 'refreshing';
41
+ }>;
42
+ type RuntimeActivitySnapshot = Readonly<{
43
+ calls: readonly CallActivity[];
44
+ /** Shared bound for feature, call, resource and nested load records. */
45
+ capacity: number;
46
+ /** True only after application close completes its physical drain successfully. */
47
+ closed: boolean;
48
+ coverage: Readonly<{
49
+ calls: 'feature-and-owned-model';
50
+ hostDemand: 'unknown';
51
+ resources: 'registered-resource-controllers';
52
+ streams: 'state-only';
53
+ uiModels: 'unknown';
54
+ }>;
55
+ executionId: string;
56
+ features: readonly FeatureActivity[];
57
+ /** Session close keeps a final data snapshot; it cannot establish subsequent ownership or liveness. */
58
+ freshness: 'live' | 'stale';
59
+ resources: readonly ResourceActivity[];
60
+ schemaRevision: 'opetope.runtime-activity/1';
61
+ truncated: boolean;
62
+ }>;
63
+ export type { ActivityOwner, CallActivity, FeatureActivity, ResourceActivity, ResourceLoadActivity, RuntimeActivitySnapshot, };
@@ -0,0 +1,8 @@
1
+ import type { RuntimeActivitySnapshot } from './activity.js';
2
+ /**
3
+ * D216: compare the same work in different applications without mistaking execution-local names for behavior.
4
+ * Keep declarations, owner feature/instance/generation, load attempt/generation, order, counts and all states.
5
+ * Call references are remapped through the same table as their subjects; unresolved references stay observable.
6
+ */
7
+ declare function comparableActivity(activity: RuntimeActivitySnapshot | undefined): RuntimeActivitySnapshot | undefined;
8
+ export { comparableActivity };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The acceptance list of `docs/devtools.md` §8, made executable.
3
+ *
4
+ * A runtime that implements `RuntimeInspectionSession` runs this kit against its own port and learns whether it holds
5
+ * the contract, without reading a line of devtools. The kit deliberately does not depend on a test framework: it
6
+ * throws one error listing every failed check, so it works from jest, from `node --test` and from a script.
7
+ *
8
+ * What it cannot check from outside is stated here rather than pretended: it observes demand neutrality only through
9
+ * the snapshots the producer publishes, and it observes atomicity only as "no partial frame was ever readable".
10
+ * Proving that no record is allocated without an observer belongs to the runtime's own gates (§6).
11
+ */
12
+ import type { RuntimeInspectionSession } from './session.js';
13
+ /**
14
+ * What the kit needs beyond the session itself: a way to make the producer commit. A real port is driven by the
15
+ * application, so the harness is whatever the caller uses to provoke one transaction.
16
+ */
17
+ type InspectionSessionHarness = Readonly<{
18
+ /** Commits one transaction. Returns `false` when the corpus is exhausted. */
19
+ commit: () => boolean;
20
+ /** Commits one transaction and drops it from the servable ring, provoking the `gap → resync` path of §7. */
21
+ dropCommit?: () => boolean;
22
+ session: RuntimeInspectionSession;
23
+ }>;
24
+ type InspectionSessionFactory = () => InspectionSessionHarness;
25
+ type ContractCheckStatus = 'failed' | 'passed' | 'skipped';
26
+ type ContractCheck = Readonly<{
27
+ detail?: string;
28
+ name: string;
29
+ status: ContractCheckStatus;
30
+ }>;
31
+ declare class InspectionContractError extends Error {
32
+ readonly checks: readonly ContractCheck[];
33
+ constructor(checks: readonly ContractCheck[]);
34
+ }
35
+ declare function assertInspectionSessionContract(factory: InspectionSessionFactory): readonly ContractCheck[];
36
+ export { assertInspectionSessionContract, InspectionContractError };
37
+ export type { ContractCheck, ContractCheckStatus, InspectionSessionFactory, InspectionSessionHarness };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether the selected node is something the write port can address at all (`docs/devtools.md` §5.1).
3
+ *
4
+ * It lives here, outside the control surface, because two callers need the same answer at different moments: the
5
+ * panel, to decide whether the surface is worth fetching, and the surface itself, to decide what to draw. One
6
+ * definition keeps those two from drifting into a panel that loads a chunk it will not use, or one that has a chunk
7
+ * and refuses to use it.
8
+ */
9
+ import type { DevtoolsGraphStore } from './graph-store.js';
10
+ import type { InspectorProjection } from './inspector.js';
11
+ /** A group is addressable through its conditions, and a group without any is not addressable at all (§5.1). */
12
+ type ControlSubject = 'condition-group' | 'quarantined-instance';
13
+ declare function controlSubjectOf(store: DevtoolsGraphStore, projection: InspectorProjection | undefined): ControlSubject | undefined;
14
+ export { controlSubjectOf };
15
+ export type { ControlSubject };
@@ -0,0 +1,16 @@
1
+ import{jsx as r,jsxs as b,Fragment as N}from"react/jsx-runtime";import{useState as g,useCallback as p}from"react";import{q as R,n as s,r as q}from"./panel-5jvisQBr.js";const y={controlActions:"Control actions",controlApplied:(o,e,n)=>`control \xB7 ${o} ${e} \xB7 applied \u2192 frame ${n}`,controlAudit:"Control commands this panel sent",controlCancel:"Cancel",controlConfirm:"Confirm",controlConfirmQuestion:(o,e)=>`${o} ${e}?`,controlRejected:(o,e,n)=>`control \xB7 ${o} ${e} \xB7 rejected \xB7 ${n}`,controlResume:"Resume",controlRetryCleanup:"Retry cleanup",controlSuspend:"Suspend"},k={controlActions:"\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F",controlApplied:(o,e,n)=>`control \xB7 ${o} ${e} \xB7 applied \u2192 \u043A\u0430\u0434\u0440 ${n}`,controlAudit:"\u041A\u043E\u043C\u0430\u043D\u0434\u044B \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F, \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0435 \u043F\u0430\u043D\u0435\u043B\u044C\u044E",controlCancel:"\u041E\u0442\u043C\u0435\u043D\u0430",controlConfirm:"\u041F\u043E\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044C",controlConfirmQuestion:(o,e)=>`${o} ${e}?`,controlRejected:(o,e,n)=>`control \xB7 ${o} ${e} \xB7 rejected \xB7 ${n}`,controlResume:"\u0412\u043E\u0437\u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C",controlRetryCleanup:"\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u0443\u0431\u043E\u0440\u043A\u0443",controlSuspend:"\u041F\u0440\u0438\u043E\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C"};function a(o){return o==="ru"?k:y}const w=`
2
+ .odt-control { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
3
+ .odt-control-confirm { display: inline-flex; align-items: center; gap: 8px; }
4
+ .odt-control-question { font-size: 12px; line-height: 1.333; color: var(--odt-text); }
5
+ .odt-btn-danger { background: var(--odt-bad-surface); color: var(--odt-bad); }
6
+ .odt-audit {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: 2px;
10
+ flex-shrink: 0;
11
+ padding: 8px 16px;
12
+ border-bottom: 1px solid var(--odt-line);
13
+ }
14
+ .odt-audit-row { font-size: 12px; line-height: 1.333; color: var(--odt-text-muted); }
15
+ `;function C(){R("data-opetope-devtools-control",w)}let h=0;function j(o,e,n){return e==="suspend"?o.suspend(n):e==="resume"?o.resume(n):o.retryCleanup(n)}function L(o,e,n,t){return h+=1,{command:o,id:h,target:e,targetLabel:n,...t.kind==="applied"?{sequence:t.sequence}:{reason:t.reason}}}function x({command:o,label:e,onAudit:n,session:t,target:l,targetLabel:d}){const i=a(s()),[c,u]=g(!1),[f,m]=g(!1),$=p(()=>u(!0),[]),v=p(()=>u(!1),[]),A=p(()=>{m(!0),j(t,o,l).then(S=>{n(L(o,l,d,S)),u(!1),m(!1)})},[o,n,t,l,d]);return c?b("span",{className:"odt-control-confirm",children:[r("span",{className:"odt-control-question",children:i.controlConfirmQuestion(e,d)}),r("button",{className:"odt-btn odt-btn-small odt-btn-danger",disabled:f,type:"button",onClick:A,children:i.controlConfirm}),r("button",{className:"odt-btn odt-btn-small odt-btn-quiet",disabled:f,type:"button",onClick:v,children:i.controlCancel})]}):r("button",{className:"odt-btn odt-btn-small",type:"button",onClick:$,children:e})}function O(o){const e=new Set;for(const n of o)n.sequence!==void 0&&(n.command==="suspend"&&e.add(n.target),n.command==="resume"&&e.delete(n.target));return e}function Q({audit:o,conditions:e,onAudit:n,session:t}){const l=a(s()),d=O(o);return r(N,{children:e.map(({atom:i,label:c})=>r(x,{command:d.has(i)?"resume":"suspend",label:d.has(i)?l.controlResume:l.controlSuspend,session:t,target:i,targetLabel:c,onAudit:n},i))})}function z({override:o}){if(o!=="none")return r("span",{className:"odt-tag odt-tag-warn odt-mono",children:`override \xB7 ${o}`})}function B(o,e){const n=o.graph.planNode(e);return(n==null?void 0:n.kind)!=="condition-group"?[]:n.conditions.map(t=>{var l;return{atom:t,label:((l=o.graph.condition(t))==null?void 0:l.label)??t}})}function F({audit:o,onAudit:e,place:n,projection:t,session:l,store:d}){const i=a(s());C();const c=q(d,t);if(!(c===void 0||t===void 0)){if(c==="condition-group")return b("span",{"aria-label":i.controlActions,className:"odt-control",role:"group",children:[n==="inspector"?r(z,{override:t.override}):void 0,r(Q,{audit:o,conditions:B(d,t.id),session:l,onAudit:e})]});if(n==="inspector")return r("span",{"aria-label":i.controlActions,className:"odt-control",role:"group",children:r(x,{command:"retry-cleanup",label:i.controlRetryCleanup,session:l,target:t.id,targetLabel:t.label,onAudit:e})})}}function D({entries:o}){const e=a(s());if(C(),o.length!==0)return r("div",{"aria-label":e.controlAudit,className:"odt-audit",role:"log",children:[...o].reverse().map(n=>r("div",{className:"odt-audit-row odt-mono",children:n.sequence===void 0?e.controlRejected(n.command,n.targetLabel,n.reason??"stale"):e.controlApplied(n.command,n.targetLabel,n.sequence)},n.id))})}export{D as ControlAudit,F as ControlSlot};
16
+ //# sourceMappingURL=control-ui-DMmVy9Ob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"control-ui-DMmVy9Ob.js","sources":["../src/react/control-locale.ts","../src/react/control-styles.ts","../src/react/control-ui.tsx"],"sourcesContent":["/**\n * The control surface's own dictionary, kept out of the panel's (§5).\n *\n * A build that never receives a write port should not ship the words for buttons it will never draw, so these words\n * live in the lazy chunk with the code that uses them. The discipline is the panel's: English is the source, the\n * Russian record is typed against it, and a missing key is a type error rather than an English word in a Russian\n * panel. The protocol's own vocabulary — the command names, `applied`, `rejected`, the rejection reasons and the\n * override value — is not translated in either.\n */\n\nimport type { DevtoolsLocale } from './locale';\n\nconst en = {\n controlActions: 'Control actions',\n controlApplied: (command: string, target: string, sequence: number): string =>\n `control · ${command} ${target} · applied → frame ${sequence}`,\n controlAudit: 'Control commands this panel sent',\n controlCancel: 'Cancel',\n controlConfirm: 'Confirm',\n controlConfirmQuestion: (action: string, target: string): string => `${action} ${target}?`,\n controlRejected: (command: string, target: string, reason: string): string =>\n `control · ${command} ${target} · rejected · ${reason}`,\n controlResume: 'Resume',\n controlRetryCleanup: 'Retry cleanup',\n controlSuspend: 'Suspend',\n};\n\ntype ControlStrings = typeof en;\n\nconst ru: ControlStrings = {\n controlActions: 'Действия управления',\n controlApplied: (command: string, target: string, sequence: number): string =>\n `control · ${command} ${target} · applied → кадр ${sequence}`,\n controlAudit: 'Команды управления, отправленные панелью',\n controlCancel: 'Отмена',\n controlConfirm: 'Подтвердить',\n controlConfirmQuestion: (action: string, target: string): string => `${action} ${target}?`,\n controlRejected: (command: string, target: string, reason: string): string =>\n `control · ${command} ${target} · rejected · ${reason}`,\n controlResume: 'Возобновить',\n controlRetryCleanup: 'Повторить уборку',\n controlSuspend: 'Приостановить',\n};\n\nfunction controlStringsFor(locale: DevtoolsLocale): ControlStrings {\n return locale === 'ru' ? ru : en;\n}\n\nexport { controlStringsFor };\n","/**\n * The control surface's own rules, injected when its chunk lands (§5). They sit here rather than in the panel's\n * stylesheet for the same reason its dictionary does: a panel installed without a write port should carry neither\n * the code nor the bytes of a surface it will never draw.\n */\n\nimport { injectStylesheet } from './styles';\n\nconst controlStylesheet = `\n.odt-control { display: inline-flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }\n.odt-control-confirm { display: inline-flex; align-items: center; gap: 8px; }\n.odt-control-question { font-size: 12px; line-height: 1.333; color: var(--odt-text); }\n.odt-btn-danger { background: var(--odt-bad-surface); color: var(--odt-bad); }\n.odt-audit {\n display: flex;\n flex-direction: column;\n gap: 2px;\n flex-shrink: 0;\n padding: 8px 16px;\n border-bottom: 1px solid var(--odt-line);\n}\n.odt-audit-row { font-size: 12px; line-height: 1.333; color: var(--odt-text-muted); }\n`;\n\nfunction ensureControlStylesheet(): void {\n injectStylesheet('data-opetope-devtools-control', controlStylesheet);\n}\n\nexport { ensureControlStylesheet };\n","/**\n * The control surface of `docs/devtools.md` §5, and the only part of the panel that can write to the application.\n *\n * It is a chunk of its own, loaded when a host passes a control session and never otherwise. That is not only a size\n * decision: a build with no write port must not carry the code that would use one, so a panel that never loaded this\n * module is read-only by construction rather than by a disabled button.\n *\n * Every command asks for confirmation first. Suspending a condition stops a real application, so the button that\n * does it is deliberately two presses away, and the second press names what it is about to do.\n */\n\nimport type { ReactElement } from 'react';\nimport { useCallback, useState } from 'react';\n\nimport type { ControlAuditEntry, ControlCommand, ControlOutcome, RuntimeControlSession } from '../control';\nimport { controlSubjectOf } from '../control-subject';\nimport type { DevtoolsGraphStore } from '../graph-store';\nimport type { InspectorProjection } from '../inspector';\nimport type { DiagnosticConditionId, DiagnosticNodeId, DiagnosticOverride } from '../protocol';\n\nimport { controlStringsFor } from './control-locale';\nimport { ensureControlStylesheet } from './control-styles';\nimport { useLocale } from './locale-context';\n\n/** Monotonic within the page: the audit is ordered by when the panel sent, not by when the runtime committed. */\nlet auditCounter = 0;\n\nfunction runCommand(\n session: RuntimeControlSession,\n command: ControlCommand,\n target: DiagnosticConditionId | DiagnosticNodeId,\n): Promise<ControlOutcome> {\n if (command === 'suspend') return session.suspend(target);\n\n return command === 'resume' ? session.resume(target) : session.retryCleanup(target);\n}\n\nfunction auditOf(\n command: ControlCommand,\n target: DiagnosticConditionId | DiagnosticNodeId,\n targetLabel: string,\n outcome: ControlOutcome,\n): ControlAuditEntry {\n auditCounter += 1;\n\n return {\n command,\n id: auditCounter,\n target,\n targetLabel,\n ...(outcome.kind === 'applied' ? { sequence: outcome.sequence } : { reason: outcome.reason }),\n };\n}\n\ntype CommandButtonProps = Readonly<{\n command: ControlCommand;\n label: string;\n onAudit: (entry: ControlAuditEntry) => void;\n session: RuntimeControlSession;\n target: DiagnosticConditionId | DiagnosticNodeId;\n targetLabel: string;\n}>;\n\n/**\n * One command, two presses. The first arms it and says what it is about to do; the second sends it. A command in\n * flight disables both, because a second send while the first settles would race the scheduler the runtime\n * serialises commands on.\n */\nfunction CommandButton({ command, label, onAudit, session, target, targetLabel }: CommandButtonProps): ReactElement {\n const strings = controlStringsFor(useLocale());\n const [armed, setArmed] = useState(false);\n const [sending, setSending] = useState(false);\n const arm = useCallback(() => setArmed(true), []);\n const cancel = useCallback(() => setArmed(false), []);\n const send = useCallback(() => {\n setSending(true);\n void runCommand(session, command, target).then(outcome => {\n onAudit(auditOf(command, target, targetLabel, outcome));\n setArmed(false);\n setSending(false);\n });\n }, [command, onAudit, session, target, targetLabel]);\n\n if (!armed) {\n return (\n <button className=\"odt-btn odt-btn-small\" type=\"button\" onClick={arm}>\n {label}\n </button>\n );\n }\n\n return (\n <span className=\"odt-control-confirm\">\n <span className=\"odt-control-question\">{strings.controlConfirmQuestion(label, targetLabel)}</span>\n <button className=\"odt-btn odt-btn-small odt-btn-danger\" disabled={sending} type=\"button\" onClick={send}>\n {strings.controlConfirm}\n </button>\n <button className=\"odt-btn odt-btn-small odt-btn-quiet\" disabled={sending} type=\"button\" onClick={cancel}>\n {strings.controlCancel}\n </button>\n </span>\n );\n}\n\n/**\n * Which conditions this panel is holding down, read back from its own audit. A snapshot says _that_ a group is\n * overridden, never by which condition, so the only honest source for which button to offer is the record of the\n * commands this panel got applied.\n */\nfunction heldConditions(audit: readonly ControlAuditEntry[]): ReadonlySet<string> {\n const held = new Set<string>();\n\n for (const entry of audit) {\n if (entry.sequence === undefined) continue;\n\n if (entry.command === 'suspend') held.add(entry.target);\n\n if (entry.command === 'resume') held.delete(entry.target);\n }\n\n return held;\n}\n\ntype ConditionControlProps = Readonly<{\n audit: readonly ControlAuditEntry[];\n /** The atom to address and the name to read: a command travels by the first and is confirmed by the second. */\n conditions: readonly Readonly<{ atom: DiagnosticConditionId; label: string }>[];\n onAudit: (entry: ControlAuditEntry) => void;\n session: RuntimeControlSession;\n}>;\n\n/**\n * One button per condition of the group, because the port controls a condition and a group may read several. A\n * single group-wide button would have had to pick one of them and call it \"the\" condition.\n */\nfunction ConditionControl({ audit, conditions, onAudit, session }: ConditionControlProps): ReactElement {\n const strings = controlStringsFor(useLocale());\n const held = heldConditions(audit);\n\n return (\n <>\n {conditions.map(({ atom, label }) => (\n <CommandButton\n key={atom}\n command={held.has(atom) ? 'resume' : 'suspend'}\n label={held.has(atom) ? strings.controlResume : strings.controlSuspend}\n session={session}\n target={atom}\n targetLabel={label}\n onAudit={onAudit}\n />\n ))}\n </>\n );\n}\n\n/** The badge reads the group's own snapshot, so it says what the runtime holds, not what the panel asked for. */\nfunction OverrideBadge({ override }: { readonly override: DiagnosticOverride }): ReactElement | undefined {\n if (override === 'none') return undefined;\n\n return <span className=\"odt-tag odt-tag-warn odt-mono\">{`override · ${override}`}</span>;\n}\n\ntype ControlSlotProps = Readonly<{\n audit: readonly ControlAuditEntry[];\n onAudit: (entry: ControlAuditEntry) => void;\n /** The bar over the canvas carries the group's commands; the inspector carries those and the override badge. */\n place: 'bar' | 'inspector';\n projection: InspectorProjection | undefined;\n session: RuntimeControlSession;\n store: DevtoolsGraphStore;\n}>;\n\n/**\n * The conditions of a group, each as the atom the port is addressed by and the label a reader recognises. The\n * producer escapes a declaration id before it publishes it, so the two differ whenever the id holds `/` or `:`.\n */\nfunction groupConditions(\n store: DevtoolsGraphStore,\n id: DiagnosticNodeId,\n): readonly Readonly<{ atom: DiagnosticConditionId; label: string }>[] {\n const node = store.graph.planNode(id);\n\n if (node?.kind !== 'condition-group') return [];\n\n return node.conditions.map(atom => ({ atom, label: store.graph.condition(atom)?.label ?? atom }));\n}\n\n/**\n * What may be controlled about the selected node, and nothing else (§5.1): a condition group through its conditions,\n * and a quarantined instance through the exact frontier its cleanup left. Everything else has no command at all, so\n * the slot renders nothing rather than a disabled button that would have to explain itself.\n */\nfunction ControlSlot({\n audit,\n onAudit,\n place,\n projection,\n session,\n store,\n}: ControlSlotProps): ReactElement | undefined {\n const strings = controlStringsFor(useLocale());\n\n ensureControlStylesheet();\n\n const subject = controlSubjectOf(store, projection);\n\n if (subject === undefined || projection === undefined) return undefined;\n\n if (subject === 'condition-group') {\n return (\n <span aria-label={strings.controlActions} className=\"odt-control\" role=\"group\">\n {place === 'inspector' ? <OverrideBadge override={projection.override} /> : undefined}\n <ConditionControl\n audit={audit}\n conditions={groupConditions(store, projection.id)}\n session={session}\n onAudit={onAudit}\n />\n </span>\n );\n }\n\n // Retrying a cleanup is not a thing to do twice by reflex, so it is offered in the inspector only.\n if (place !== 'inspector') return undefined;\n\n return (\n <span aria-label={strings.controlActions} className=\"odt-control\" role=\"group\">\n <CommandButton\n command=\"retry-cleanup\"\n label={strings.controlRetryCleanup}\n session={session}\n target={projection.id}\n targetLabel={projection.label}\n onAudit={onAudit}\n />\n </span>\n );\n}\n\n/**\n * The panel's own audit trail, above the runtime's timeline and visibly not part of it: an applied command points at\n * the frame that carries it, and a rejected one has no frame to point at, because nothing was committed.\n */\nfunction ControlAudit({ entries }: { readonly entries: readonly ControlAuditEntry[] }): ReactElement | undefined {\n const strings = controlStringsFor(useLocale());\n\n ensureControlStylesheet();\n\n if (entries.length === 0) return undefined;\n\n return (\n <div aria-label={strings.controlAudit} className=\"odt-audit\" role=\"log\">\n {[...entries].reverse().map(entry => (\n <div key={entry.id} className=\"odt-audit-row odt-mono\">\n {entry.sequence === undefined\n ? strings.controlRejected(entry.command, entry.targetLabel, entry.reason ?? 'stale')\n : strings.controlApplied(entry.command, entry.targetLabel, entry.sequence)}\n </div>\n ))}\n </div>\n );\n}\n\nexport { ControlAudit, ControlSlot };\n"],"names":["en","command","target","sequence","action","reason","ru","controlStringsFor","locale","controlStylesheet","ensureControlStylesheet","injectStylesheet","auditCounter","runCommand","session","auditOf","targetLabel","outcome","CommandButton","label","onAudit","strings","useLocale","armed","setArmed","useState","sending","setSending","arm","useCallback","cancel","send","_jsxs","_jsx","heldConditions","audit","held","entry","ConditionControl","conditions","_Fragment","atom","OverrideBadge","override","groupConditions","store","id","node","_a","ControlSlot","place","projection","subject","controlSubjectOf","ControlAudit","entries"],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The write half of the devtools seam, as this package consumes it (`docs/devtools.md` §5).
3
+ *
4
+ * Like `RuntimeInspectionSession`, this is a _consumed_ interface: the runtime owns the semantics and hands a
5
+ * session in at attach, so the binding stays structural and this package still imports no `@opetope/*` module. The
6
+ * host creates exactly one session per execution and this package closes it when the panel detaches; a second
7
+ * session on a live execution is the host's error to report, not a state the panel can be in.
8
+ *
9
+ * Nothing here has a runtime representation. The types cost the panel no bytes, and the surface that does — the
10
+ * buttons, the confirmation and the audit rows — loads only when a host actually passes a control session.
11
+ */
12
+ import type { DiagnosticConditionId, DiagnosticNodeId, DiagnosticSequence } from './protocol.js';
13
+ type ControlRejection = 'not-controllable' | 'stale' | 'unknown-target';
14
+ /**
15
+ * Data, never an exception. An applied command names the frame its effect is visible in, so the panel can point at
16
+ * the producer's own record instead of keeping a second account of what happened.
17
+ */
18
+ type ControlOutcome = Readonly<{
19
+ kind: 'applied';
20
+ sequence: DiagnosticSequence;
21
+ }> | Readonly<{
22
+ kind: 'rejected';
23
+ reason: ControlRejection;
24
+ }>;
25
+ type RuntimeControlSession = Readonly<{
26
+ close: () => void;
27
+ resume: (conditionId: string) => Promise<ControlOutcome>;
28
+ retryCleanup: (instanceId: string) => Promise<ControlOutcome>;
29
+ suspend: (conditionId: string) => Promise<ControlOutcome>;
30
+ }>;
31
+ type ControlCommand = 'resume' | 'retry-cleanup' | 'suspend';
32
+ /**
33
+ * One row of the panel's own audit trail. A rejection emits no frame — a frame that commits nothing would be a lie —
34
+ * so the panel is the only place a refused command is recorded at all (§5.4).
35
+ */
36
+ type ControlAuditEntry = Readonly<{
37
+ command: ControlCommand;
38
+ /** Monotonic within the panel: the audit is ordered by when the panel sent, not by when the runtime committed. */
39
+ id: number;
40
+ reason?: ControlRejection;
41
+ sequence?: DiagnosticSequence;
42
+ target: DiagnosticConditionId | DiagnosticNodeId;
43
+ targetLabel: string;
44
+ }>;
45
+ export type { ControlAuditEntry, ControlCommand, ControlOutcome, ControlRejection, RuntimeControlSession };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A synthetic application graph for the performance gates of `docs/devtools.md` §6, and the
3
+ * frames that move it.
4
+ *
5
+ * It is generated, not captured, and deterministic by construction: no clock, no randomness, node and edge counts a
6
+ * pure function of the shape asked for. That is what lets §6's "deterministic digest" be an assertion rather than
7
+ * a hope, and it lets one corpus be compared against another run, another machine and another revision.
8
+ *
9
+ * The shape mirrors a real application rather than a star or a chain: features are split across condition groups,
10
+ * each feature imports a host contract and a few peers, a quarter of them provide a port, and half contribute.
11
+ */
12
+ import type { RuntimeGraphFrame, RuntimeGraphSnapshot } from './graph.js';
13
+ type CorpusShape = Readonly<{
14
+ features: number;
15
+ groups: number;
16
+ /** Peers each feature imports. Drives the edge count without changing the node count. */
17
+ importsPerFeature: number;
18
+ }>;
19
+ type Corpus = Readonly<{
20
+ /** One frame per changed instance, each carrying one operation: the `O(delta)` case of §6. */
21
+ frames: readonly RuntimeGraphFrame[];
22
+ shape: CorpusShape;
23
+ snapshot: RuntimeGraphSnapshot;
24
+ }>;
25
+ declare function generateCorpus(shape: CorpusShape, frames?: number): Corpus;
26
+ /** Roughly 1k nodes and 5k edges: the interactive budget of §6. */
27
+ declare const smallCorpusShape: CorpusShape;
28
+ /** Roughly 20k nodes and 60k edges: the headless budget of §6. */
29
+ declare const largeCorpusShape: CorpusShape;
30
+ declare function countNodes(snapshot: RuntimeGraphSnapshot): number;
31
+ declare function countEdges(snapshot: RuntimeGraphSnapshot): number;
32
+ export { countEdges, countNodes, generateCorpus, largeCorpusShape, smallCorpusShape };
33
+ export type { Corpus, CorpusShape };
@@ -0,0 +1,4 @@
1
+ import type { DecodeContext } from './decode-shapes.js';
2
+ /** The shared budget includes nested loads, not just top-level controllers. */
3
+ declare function readActivity(value: unknown, path: string, context: DecodeContext): unknown;
4
+ export { readActivity };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The schema of the protocol, expressed as shapes a decoder can walk. Every node kind, every state of every automaton
3
+ * and every edge kind is listed once here, so a payload that names a state the runtime cannot be in is rejected
4
+ * rather than passed through as an unknown string (§7).
5
+ *
6
+ * A shape is a record of readers. `readShape` requires exactly those keys — a missing one is malformed and an extra
7
+ * one is malformed too, because the schema revision, not tolerance, is what carries compatibility forward.
8
+ */
9
+ type DecodeContext = Readonly<{
10
+ maxEntries: number;
11
+ maxStringLength: number;
12
+ }>;
13
+ type Reader = (value: unknown, path: string, context: DecodeContext) => unknown;
14
+ type Shape = Readonly<Record<string, Reader>>;
15
+ declare const optionalMarker: unique symbol;
16
+ type OptionalReader = Reader & {
17
+ readonly [optionalMarker]?: true;
18
+ };
19
+ declare function optional(reader: Reader): OptionalReader;
20
+ declare const readNodeId: Reader;
21
+ declare const readEdgeId: Reader;
22
+ declare const readNumber: Reader;
23
+ declare function readShape(value: unknown, shape: Shape, path: string, context: DecodeContext): Record<string, unknown>;
24
+ declare function listOf(reader: Reader): Reader;
25
+ declare const readCause: Reader;
26
+ declare const readEdge: Reader;
27
+ declare const readRuntimeNode: Reader;
28
+ /** Readers bound to one kind, for the buckets and for `plan.application`, whose kind is equally fixed. */
29
+ declare function planNodeOf(kind: string): Reader;
30
+ declare function runtimeNodeOf(kind: string): Reader;
31
+ export { listOf, optional, planNodeOf, readCause, readEdge, readEdgeId, readNodeId, readNumber, readRuntimeNode, readShape, runtimeNodeOf, };
32
+ export type { DecodeContext, Reader, Shape };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Primitive readers for the hostile boundary of `docs/devtools.md` §7 and `spec.md` §4
3
+ * ("hostile-boundary валидация только на входе внешних данных").
4
+ *
5
+ * Two hostile shapes are handled here rather than anywhere else. A property that is not a *data* descriptor is
6
+ * rejected before it is read — a field of a record and an entry of an array alike — so a getter cannot run code
7
+ * inside the decoder; and the three keys that reach `Object.prototype` are rejected outright, so a decoded record
8
+ * can never pollute it.
9
+ *
10
+ * There is no depth budget because there is no unbounded recursion to budget: the decoder only ever descends into
11
+ * shapes it declares, so a deeply nested payload is rejected at the first field that is not of the declared type.
12
+ */
13
+ declare class DevtoolsDecodeError extends Error {
14
+ readonly code: 'budget-exceeded' | 'malformed' | 'unknown-kind' | 'unsafe-key';
15
+ readonly path: string;
16
+ constructor(code: 'budget-exceeded' | 'malformed' | 'unknown-kind' | 'unsafe-key', path: string, message: string);
17
+ }
18
+ declare function malformed(path: string, message: string): never;
19
+ /** Own enumerable data fields only: an accessor would let the payload execute code during decoding. */
20
+ declare function dataFields(value: unknown, path: string): ReadonlyMap<string, unknown>;
21
+ declare function readString(value: unknown, path: string, maxLength: number): string;
22
+ declare function readCount(value: unknown, path: string): number;
23
+ /** Epoch milliseconds also have to fit Date's range before a consumer can format them. */
24
+ declare function readTimestamp(value: unknown, path: string): number;
25
+ /** D212: ordering priority is any finite number; sequence/revision/count fields keep their integer domain. */
26
+ declare function readFiniteNumber(value: unknown, path: string): number;
27
+ declare function readBoolean(value: unknown, path: string): boolean;
28
+ /**
29
+ * Entries are copied out by index through their own descriptors, never by `map` or any other method of the incoming
30
+ * array: an element can be an accessor just as a field can, and the array's own methods can be replaced. The result
31
+ * is a fresh array the callers may iterate with their own `map` safely.
32
+ */
33
+ declare function readArray(value: unknown, path: string, budget: number): readonly unknown[];
34
+ /** A literal from a closed set. Anything else is an unknown kind, which is a rejected record, never a default. */
35
+ declare function readLiteral<Value extends string>(value: unknown, path: string, allowed: readonly Value[]): Value;
36
+ export { dataFields, DevtoolsDecodeError, malformed, readArray, readBoolean, readCount, readFiniteNumber, readLiteral, readString, readTimestamp, };