graphlin 0.1.2 → 0.2.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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +12 -3
- package/docs/decision-service.md +393 -0
- package/docs/extension-authoring.md +553 -0
- package/docs/model-api.md +293 -0
- package/docs/usage.md +465 -0
- package/docs/visualizer-views.md +199 -0
- package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
- package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
- package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
- package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
- package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
- package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
- package/package.json +74 -9
- package/plugin.json +4 -2
- package/runtime/core/evidence.mjs +43 -9
- package/runtime/core/graph.mjs +11 -6
- package/runtime/core/privacy.mjs +1 -0
- package/runtime/daemon/auth.mjs +7 -3
- package/runtime/daemon/diagnostics.mjs +1 -1
- package/runtime/daemon/extension-api.mjs +203 -0
- package/runtime/daemon/lineage.mjs +70 -0
- package/runtime/daemon/manager.mjs +9 -6
- package/runtime/daemon/model-api.mjs +728 -0
- package/runtime/daemon/model-persistence.mjs +220 -0
- package/runtime/daemon/server.mjs +70 -12
- package/runtime/daemon/settings.mjs +11 -3
- package/runtime/decisions/broker.mjs +349 -0
- package/runtime/decisions/contracts.mjs +179 -0
- package/runtime/decisions/evaluation.mjs +305 -0
- package/runtime/decisions/faults.mjs +32 -0
- package/runtime/decisions/index.mjs +818 -0
- package/runtime/decisions/profiles.mjs +93 -0
- package/runtime/decisions/questions.mjs +268 -0
- package/runtime/discovery/index.mjs +2 -0
- package/runtime/discovery/inventory.mjs +160 -0
- package/runtime/discovery/parser.mjs +40 -0
- package/runtime/discovery/structure.mjs +232 -0
- package/runtime/extensions/contracts.mjs +59 -0
- package/runtime/extensions/frame.mjs +64 -0
- package/runtime/extensions/index.mjs +9 -0
- package/runtime/extensions/manifest.mjs +95 -0
- package/runtime/extensions/packages.mjs +222 -0
- package/runtime/extensions/profiles.mjs +36 -0
- package/runtime/extensions/projection.mjs +130 -0
- package/runtime/extensions/registry.mjs +285 -0
- package/runtime/extensions/scene.mjs +105 -0
- package/runtime/extensions/sdk.d.ts +205 -0
- package/runtime/extensions/sdk.mjs +88 -0
- package/runtime/jev/index.mjs +13 -777
- package/runtime/jev/provider.mjs +101 -0
- package/runtime/jev/questions.mjs +16 -258
- package/runtime/jev/wire.mjs +17 -25
- package/runtime/model/changes.mjs +42 -0
- package/runtime/model/history.mjs +124 -0
- package/runtime/model/index.mjs +2 -0
- package/runtime/model/project-model.mjs +889 -0
- package/runtime/model/records.mjs +239 -0
- package/runtime/pipeline.mjs +127 -48
- package/runtime/platform.mjs +254 -0
- package/runtime/visualizers/blocks.mjs +5 -0
- package/runtime/visualizers/c4.mjs +52 -0
- package/runtime/visualizers/changes.mjs +24 -0
- package/runtime/visualizers/code.mjs +5 -0
- package/runtime/visualizers/index.mjs +23 -0
- package/runtime/visualizers/structure.mjs +120 -0
- package/runtime/visualizers/timeline.mjs +66 -0
- package/runtime/web/app.js +369 -86
- package/runtime/web/extension-frame.js +128 -0
- package/runtime/web/index.html +123 -80
- package/runtime/web/model-client.js +162 -0
- package/runtime/web/platform.js +337 -0
- package/runtime/web/scene.js +111 -0
- package/runtime/web/style.css +152 -142
- package/schemas/graph.schema.json +4 -1
- package/scripts/arguments.mjs +5 -1
- package/scripts/build-packages.mjs +6 -2
- package/scripts/control.mjs +1 -1
- package/scripts/daemon.mjs +2 -1
- package/scripts/extensions.mjs +44 -0
- package/scripts/graphlin.mjs +23 -3
- package/scripts/onboarding.mjs +10 -3
- package/scripts/validate-packages.mjs +54 -8
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
# Authoring a Graphlin visualizer
|
|
2
|
+
|
|
3
|
+
An extension receives a policy-filtered model and returns a small scene, or
|
|
4
|
+
renders a custom view in an isolated browser frame. It never runs inside the
|
|
5
|
+
Graphlin daemon. It needs no collector, agent plugin, API key, source access, or
|
|
6
|
+
runtime dependency installation.
|
|
7
|
+
|
|
8
|
+
Extension API **1**, model schema **2**, and scene schema **1** are separate
|
|
9
|
+
versions. The browser SDK is `runtime/extensions/sdk.mjs`; its public types are
|
|
10
|
+
in `runtime/extensions/sdk.d.ts`. The runtime entry is
|
|
11
|
+
`runtime/extensions/index.mjs`. Package authors can subscribe to the browser
|
|
12
|
+
protocol directly, so a build tool is optional.
|
|
13
|
+
|
|
14
|
+
The public package export `graphlin/extensions/sdk` resolves to that browser
|
|
15
|
+
entry and its types; `graphlin/extensions/scene` exposes scene validation alone.
|
|
16
|
+
Bundle these imports into the single executable asset.
|
|
17
|
+
|
|
18
|
+
## Create an independent package
|
|
19
|
+
|
|
20
|
+
Create a directory containing only these package files:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
my-c4/
|
|
24
|
+
graphlin.extension.json
|
|
25
|
+
package.json
|
|
26
|
+
dist/
|
|
27
|
+
visualizer.js
|
|
28
|
+
profiles/
|
|
29
|
+
c4.json
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The optional `README.md` and `LICENSE` files are also allowed. Other files must
|
|
33
|
+
be declared JS/JSON assets. Keep authoring sources, environment files,
|
|
34
|
+
dependencies, maps, and build tools outside this install directory. Symlinks,
|
|
35
|
+
hard links, special files, undeclared files, and `node_modules` are rejected.
|
|
36
|
+
|
|
37
|
+
Save this as `dist/visualizer.js`. This small C4 projection reads explicitly
|
|
38
|
+
typed, supported application/datastore interpretations. A class, directory,
|
|
39
|
+
generic analysis answer, or label does not create an application boundary.
|
|
40
|
+
If the model has no supported interpretations, it abstains.
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
window.addEventListener('graphlin:connect', ({ detail: { port } }) => {
|
|
44
|
+
const kinds = { application: 'service', container: 'service', datastore: 'datastore' };
|
|
45
|
+
port.onmessage = ({ data: message }) => {
|
|
46
|
+
if (message.type === 'graphlin:dispose') { port.close(); return; }
|
|
47
|
+
if (message.type !== 'graphlin:project') return;
|
|
48
|
+
const model = message.model;
|
|
49
|
+
const byId = new Map(model.entities.map(entity => [entity.id, entity]));
|
|
50
|
+
const chosen = model.interpretations.filter(value =>
|
|
51
|
+
Object.hasOwn(kinds, value.kind) &&
|
|
52
|
+
value.validity === 'current' && value.support === 'supported' &&
|
|
53
|
+
value.classification === 'accepted' && value.sourceRefs.length &&
|
|
54
|
+
value.entityIds.length && value.entityIds.every(id => byId.get(id)?.validity === 'current')
|
|
55
|
+
).slice(0, 64);
|
|
56
|
+
const groups = chosen.map(value => ({
|
|
57
|
+
id: `boundary-${value.id}`, entityIds: value.entityIds,
|
|
58
|
+
membershipId: value.id, label: value.label,
|
|
59
|
+
kind: kinds[value.kind], parentId: null, collapsed: true
|
|
60
|
+
}));
|
|
61
|
+
const owner = new Map(groups.flatMap(group => group.entityIds.map(id => [id, group.id])));
|
|
62
|
+
const relationKinds = new Set([
|
|
63
|
+
'calls', 'reads', 'writes', 'publishes', 'consumes', 'depends_on'
|
|
64
|
+
]);
|
|
65
|
+
const edges = model.relations.filter(relation =>
|
|
66
|
+
owner.has(relation.source) && owner.has(relation.target) &&
|
|
67
|
+
owner.get(relation.source) !== owner.get(relation.target) &&
|
|
68
|
+
relation.validity === 'current' && relationKinds.has(relation.kind)
|
|
69
|
+
).slice(0, 256).map(relation => ({
|
|
70
|
+
id: `edge-${relation.id}`,
|
|
71
|
+
source: owner.get(relation.source),
|
|
72
|
+
target: owner.get(relation.target),
|
|
73
|
+
kind: relation.kind,
|
|
74
|
+
relationIds: [relation.id],
|
|
75
|
+
count: 1
|
|
76
|
+
}));
|
|
77
|
+
port.postMessage({
|
|
78
|
+
type: 'graphlin:scene', apiVersion: 1,
|
|
79
|
+
instanceId: message.instanceId, projectId: message.projectId,
|
|
80
|
+
revision: message.revision, viewEpoch: message.viewEpoch,
|
|
81
|
+
requestId: message.requestId,
|
|
82
|
+
scene: {
|
|
83
|
+
sceneVersion: 1, nodes: [], edges, groups,
|
|
84
|
+
coverage: {
|
|
85
|
+
shown: owner.size, total: model.entities.length,
|
|
86
|
+
truncated: model.entities.length > owner.size,
|
|
87
|
+
label: groups.length ? 'Supported interpretations only' : 'Application boundaries unknown'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
port.start();
|
|
93
|
+
});
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Save this as `profiles/c4.json`. The optional `interpretationKind` mapping
|
|
97
|
+
declares the meaning of a supported answer. The broker uses the selected
|
|
98
|
+
entity's core label for the resulting boundary, not the word `application`.
|
|
99
|
+
`unknown` cannot create a C4 boundary.
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"id": "architecture",
|
|
104
|
+
"questions": [
|
|
105
|
+
{
|
|
106
|
+
"id": "role",
|
|
107
|
+
"kind": "choice",
|
|
108
|
+
"question": "Which role, if any, is supported for the selected entities by the supplied current model metadata? Choose unknown when the metadata does not establish a boundary.",
|
|
109
|
+
"options": ["application", "datastore", "unknown"],
|
|
110
|
+
"interpretationKind": "selected-choice"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"selectors": {
|
|
114
|
+
"fields": ["entities", "relations"],
|
|
115
|
+
"candidateIds": []
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This profile runs only after explicit host activation, profile approval, and
|
|
121
|
+
source-transmission consent. Opening or rendering the extension never runs it.
|
|
122
|
+
Metadata often cannot establish an architectural boundary; the default C4
|
|
123
|
+
view and this example remain unknown until core admits a supported mapping.
|
|
124
|
+
|
|
125
|
+
Save `package.json`:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"name": "my-graphlin-c4",
|
|
130
|
+
"version": "1.0.0",
|
|
131
|
+
"files": ["graphlin.extension.json", "dist/visualizer.js", "profiles/c4.json"]
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Generate the manifest from the final asset bytes. Run this in `my-c4`:
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
node --input-type=module <<'JS'
|
|
139
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
140
|
+
import { createHash } from 'node:crypto';
|
|
141
|
+
const entry = 'dist/visualizer.js';
|
|
142
|
+
const assets = {};
|
|
143
|
+
for (const name of [entry, 'profiles/c4.json']) {
|
|
144
|
+
assets[name] = 'sha256-' + createHash('sha256').update(await readFile(name)).digest('hex');
|
|
145
|
+
}
|
|
146
|
+
await writeFile('graphlin.extension.json', JSON.stringify({
|
|
147
|
+
manifestVersion: 1,
|
|
148
|
+
id: 'example.c4',
|
|
149
|
+
name: 'My C4 view',
|
|
150
|
+
version: '1.0.0',
|
|
151
|
+
graphlinApi: '1',
|
|
152
|
+
modelSchema: '2',
|
|
153
|
+
requiredFeatures: ['canonical-mappings', 'scene-groups'],
|
|
154
|
+
entry,
|
|
155
|
+
assets,
|
|
156
|
+
decisionProfiles: ['profiles/c4.json'],
|
|
157
|
+
views: ['applications'],
|
|
158
|
+
renderer: { kind: 'graphlin-scene', sceneVersion: '1' },
|
|
159
|
+
capabilities: ['model.read', 'selection.request', 'analysis.request']
|
|
160
|
+
}, null, 2));
|
|
161
|
+
JS
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
The manifest filename and keys are exact. Unknown keys, capabilities, required
|
|
165
|
+
features, and incompatible versions fail closed. Asset paths are relative
|
|
166
|
+
without `./`, `..`, encoded characters, URL suffixes, or backslashes.
|
|
167
|
+
|
|
168
|
+
API 1 capabilities are `model.read`, `activity.read`, `history.read`,
|
|
169
|
+
`selection.request`, `inspection.request`, and `analysis.request`.
|
|
170
|
+
Supported required features are `containment`, `canonical-mappings`,
|
|
171
|
+
`scene-groups`, `activity`, and `checkpoints`. A feature name is a compatibility
|
|
172
|
+
requirement, not a grant.
|
|
173
|
+
|
|
174
|
+
The renderer is `{ "kind": "graphlin-scene", "sceneVersion": "1" }` or
|
|
175
|
+
`{ "kind": "custom" }`. There is exactly one executable `.js` asset: the entry.
|
|
176
|
+
Bundle any SDK or third-party library into that classic script. ES module
|
|
177
|
+
imports/exports, source maps, runtime dependencies, and external assets are not
|
|
178
|
+
supported. Additional declared `.json` assets are inert data supplied in the
|
|
179
|
+
`graphlin:connect` event.
|
|
180
|
+
|
|
181
|
+
## Install, update, inspect, and remove
|
|
182
|
+
|
|
183
|
+
The parent CLI calls `runExtensions(args, { projectRoot, dataDir })` from
|
|
184
|
+
`scripts/extensions.mjs`. With that dispatch integrated, the commands are:
|
|
185
|
+
|
|
186
|
+
```sh
|
|
187
|
+
graphlin extensions add ./my-c4
|
|
188
|
+
graphlin extensions list
|
|
189
|
+
graphlin extensions doctor
|
|
190
|
+
graphlin extensions dev ./my-c4
|
|
191
|
+
graphlin extensions add my-graphlin-c4@1.0.0
|
|
192
|
+
graphlin extensions update my-graphlin-c4@1.0.1
|
|
193
|
+
graphlin extensions remove example.c4
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`dev` requires an explicit directory, copies a development snapshot, and marks
|
|
197
|
+
it visibly in the catalogue. Rerunning it reloads the directory. For a live
|
|
198
|
+
authoring session, the integration may use
|
|
199
|
+
`registry.dev(directory, { watch: true, signal, onChange })`. Its returned
|
|
200
|
+
`close()` stops watching; `done` resolves when closed. Invalid intermediate
|
|
201
|
+
builds report an error and retain the installed version. Successful changes
|
|
202
|
+
have new digests, require new grants, and require frame replacement.
|
|
203
|
+
|
|
204
|
+
To produce an npm archive locally:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
npm pack --ignore-scripts
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Publishing is a separate, explicitly authorized author operation. Graphlin
|
|
211
|
+
remote installation accepts only an exact `name@version` or
|
|
212
|
+
`@scope/name@version`. It runs `npm pack --ignore-scripts` in a private temporary
|
|
213
|
+
directory with a clean npm configuration, then validates tar contents in
|
|
214
|
+
memory. No lifecycle scripts or dependency installation are run.
|
|
215
|
+
|
|
216
|
+
Archive validation rejects path traversal, links, device/sparse files, PAX/GNU
|
|
217
|
+
extended headers, duplicate or case-colliding paths, corrupt checksums,
|
|
218
|
+
truncation, and oversized contents. Packages requiring extended headers should
|
|
219
|
+
use shorter file paths. Package name/version must match the requested identity.
|
|
220
|
+
|
|
221
|
+
Limits are 32 assets, 128 package entries, 2 MiB per file, 8 MiB package content,
|
|
222
|
+
and 12 MiB compressed or expanded archive size. Hashes are SHA-256 hex strings
|
|
223
|
+
prefixed with `sha256-`. The bundle digest also covers the canonical manifest,
|
|
224
|
+
so changes to capabilities, profiles, or labels create a different identity.
|
|
225
|
+
|
|
226
|
+
Installed assets live under the private data directory at
|
|
227
|
+
`extensions/bundles/<id>/<version>/<digest>/`. The runtime never edits a stored
|
|
228
|
+
bundle. An atomic catalogue write activates a verified bundle. Failed validation,
|
|
229
|
+
cancelled transport, or a failed catalogue write leaves the active version and
|
|
230
|
+
its grants intact. Old bundle bytes are retained for recovery. Removal atomically
|
|
231
|
+
removes catalogue access and grants; retained bytes are unreachable through
|
|
232
|
+
the registry. The authoring runtime currently has no automatic bundle garbage
|
|
233
|
+
collection.
|
|
234
|
+
|
|
235
|
+
## Approve data explicitly
|
|
236
|
+
|
|
237
|
+
Installation alone delivers no project data. The authenticated host owns approval:
|
|
238
|
+
|
|
239
|
+
```js
|
|
240
|
+
await registry.grant('example.c4', {
|
|
241
|
+
digest: installed.digest,
|
|
242
|
+
fields: ['entities', 'relations', 'interpretations', 'coverage'],
|
|
243
|
+
history: false,
|
|
244
|
+
approved: true,
|
|
245
|
+
profiles: ['architecture']
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Allowed field groups are `entities`, `relations`, `interpretations`, `activity`,
|
|
250
|
+
`coverage`, `sessions`, and `checkpoints`. Each needs the corresponding declared
|
|
251
|
+
read capability. `checkpoints` requires `history: true`, and history requires
|
|
252
|
+
`history.read`. A grant can optionally approve named `profiles`.
|
|
253
|
+
HTTP session selectors also require history, because they can address earlier work.
|
|
254
|
+
|
|
255
|
+
Approval is bound to project, extension ID, exact bundle digest, fields, history,
|
|
256
|
+
and profiles. An update to different bytes clears earlier approvals across
|
|
257
|
+
projects. Rolling back does not restore approval automatically. An unchanged
|
|
258
|
+
bundle may keep its existing grant. `revoke(id)` stops current-project access;
|
|
259
|
+
removal stops access across projects.
|
|
260
|
+
|
|
261
|
+
For every delivery, the broker calls:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
const grant = await registry.getGrant(extensionId);
|
|
265
|
+
const projected = getExtensionDataProjection(currentPolicySnapshot, grant);
|
|
266
|
+
if (projected === null) {
|
|
267
|
+
// Destroy the frame and purge host-controlled scenes/caches.
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`getExtensionDataProjection(snapshot, grant)` is pure: it never reads storage,
|
|
272
|
+
mutates a model, or grants authority. It returns `null` for invalid, denied,
|
|
273
|
+
unapproved, or cross-project grants. A copied grant object is not a token; the
|
|
274
|
+
broker must get the current grant again and compare the mounted digest before
|
|
275
|
+
every delivery. Concurrent updates/revocations must invalidate in-flight work.
|
|
276
|
+
|
|
277
|
+
The input snapshot must already reflect current core policy. Reapply that
|
|
278
|
+
policy to history too. Mark historical inputs with `replay: true` or
|
|
279
|
+
`checkpointId` and check `grant.history`; the pure function cannot distinguish
|
|
280
|
+
an unmarked old snapshot from a live one.
|
|
281
|
+
|
|
282
|
+
The output shape is:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
{
|
|
286
|
+
schemaVersion: 2, projectId, revision, sequence,
|
|
287
|
+
entities: [{
|
|
288
|
+
id, label, kind, parentId, artifactId?, qualifiedName?,
|
|
289
|
+
sourceRefs, basis, validity, classification
|
|
290
|
+
}],
|
|
291
|
+
relations: [{id, source, target, kind, basis, validity, sourceRefs}],
|
|
292
|
+
interpretations, activity, coverage, sessions, checkpoints
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Unapproved collections are empty. Relations and interpretations only reference
|
|
297
|
+
delivered entities. Activity-only visualizers are supported. Every nested
|
|
298
|
+
record is built from allowed fields, including coverage, interpretation
|
|
299
|
+
namespaces, source references, sessions, and checkpoint metadata. Source
|
|
300
|
+
references contain bounded IDs, hashes, generations, and line numbers, never
|
|
301
|
+
source text.
|
|
302
|
+
|
|
303
|
+
No API 1 grant includes source, excerpts, prompts, transcripts, absolute paths,
|
|
304
|
+
raw hook bodies, credentials, arbitrary object namespaces, or arbitrary
|
|
305
|
+
inspection responses. Labels are bounded and filtered for secrets and absolute
|
|
306
|
+
locators. Only the host inspector can display separately authorized evidence.
|
|
307
|
+
Parsed structure, public intent, interpretation, and execution remain distinct;
|
|
308
|
+
an extension cannot upgrade an attempted operation into observed success.
|
|
309
|
+
|
|
310
|
+
## Validate scenes and messages
|
|
311
|
+
|
|
312
|
+
Use `validateScene(scene, { model: deliveredProjection })` in the host.
|
|
313
|
+
`validateScene(scene)` checks structure alone and is useful for author tests;
|
|
314
|
+
it is insufficient at the data boundary.
|
|
315
|
+
|
|
316
|
+
A scene has `sceneVersion: 1`, `nodes`, `groups`, `edges`, and optional `coverage`.
|
|
317
|
+
Nodes have `id`, `entityId`, `label`, and `kind`. Groups have `id`, `entityIds`,
|
|
318
|
+
and `label`; optional `membershipId` is an opaque membership reference.
|
|
319
|
+
Both accept `parentId`, finite layout hints, and finite style/shape tokens.
|
|
320
|
+
Parent IDs always refer to scene groups. Group cycles and missing parents fail.
|
|
321
|
+
|
|
322
|
+
Edges have `id`, `source`, `target`, and `kind`. Under model validation,
|
|
323
|
+
`relationIds` must identify delivered relations of that exact kind connecting
|
|
324
|
+
the represented endpoint entities. A group's `entityIds` is its bounded
|
|
325
|
+
membership set. `count`, when present, equals the supplied relation count.
|
|
326
|
+
Never combine calls, reads, and writes into a stronger relation. The renderer
|
|
327
|
+
can lay out a legacy-compatible node kind while retaining group parent links.
|
|
328
|
+
|
|
329
|
+
Limits are 256 nodes, 64 groups, 768 edges, 256 members per mapping, 240 label
|
|
330
|
+
characters, and 1 MiB per serialized scene. Coordinates must be finite within
|
|
331
|
+
±100,000; dimensions must be positive and at most 20,000. Arbitrary HTML, SVG,
|
|
332
|
+
CSS, URLs, event handlers, and unknown fields are rejected. Styles are tokens
|
|
333
|
+
such as `default`, `tentative`, `stale`, `active`, and `discovered`, not CSS.
|
|
334
|
+
The complete token sets are exported from `sdk.mjs`.
|
|
335
|
+
|
|
336
|
+
Each asynchronous message carries this context:
|
|
337
|
+
|
|
338
|
+
```js
|
|
339
|
+
{
|
|
340
|
+
apiVersion: 1,
|
|
341
|
+
instanceId, projectId, revision, viewEpoch, requestId
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The SDK supports:
|
|
346
|
+
|
|
347
|
+
| Type | Payload |
|
|
348
|
+
| --- | --- |
|
|
349
|
+
| `graphlin:project` | `model`, optional `settings`, optional `selection` |
|
|
350
|
+
| `graphlin:scene` | `scene` |
|
|
351
|
+
| `graphlin:status` | `status`: `ready`, `busy`, `empty`, or `error`; `itemCount`: integer 0–20,000 |
|
|
352
|
+
| `graphlin:select` | `selection`: exactly one `entityId`, `relationId`, or `activityId` |
|
|
353
|
+
| `graphlin:error` | Fixed diagnostic `code` |
|
|
354
|
+
| `graphlin:dispose` | No payload |
|
|
355
|
+
|
|
356
|
+
The host calls `validateMessage(message, { model: deliveredProjection, context })`
|
|
357
|
+
before handling responses. Supplying `context` verifies instance, project,
|
|
358
|
+
revision, view epoch, request, and API version. Selection validation requires
|
|
359
|
+
the granted projection and rejects targets absent from it. The host additionally
|
|
360
|
+
checks the active `selection.request`/`inspection.request` capability, current
|
|
361
|
+
grant, message rate, outstanding requests, and projection deadline.
|
|
362
|
+
|
|
363
|
+
When bundled, `connectExtension({ project, mount, dispose })` handles the basic
|
|
364
|
+
project/scene lifecycle and discards superseded asynchronous results. A custom
|
|
365
|
+
renderer may update its DOM during `project` and return no scene, then post
|
|
366
|
+
bounded status and selection messages using the same protocol. Host-owned
|
|
367
|
+
controls, keyboard access, readable labels, reduced motion, and teardown remain
|
|
368
|
+
custom-renderer responsibilities.
|
|
369
|
+
|
|
370
|
+
## Declare neutral analysis profiles
|
|
371
|
+
|
|
372
|
+
The C4 package above declares `analysis.request`,
|
|
373
|
+
`decisionProfiles: ["profiles/c4.json"]`, and the profile file's SHA-256 in
|
|
374
|
+
`assets`. Each file contains one profile. The SDK's `DecisionQuestion` and
|
|
375
|
+
`DecisionProfile` types describe the same validated fields.
|
|
376
|
+
|
|
377
|
+
Question kinds are `boolean`, `choice`, and `score` (0–1). Limits are 8 profiles,
|
|
378
|
+
16 questions/profile, 400 characters/question, 2–16 choice options of at most
|
|
379
|
+
80 characters, and 256 candidate IDs. Selectors may name only `entities`,
|
|
380
|
+
`relations`, and `interpretations`; an empty candidate list requests a bounded
|
|
381
|
+
host-selected scope. Source/path/URL fields, thresholds, and executable
|
|
382
|
+
callbacks are not part of the schema.
|
|
383
|
+
|
|
384
|
+
Questions may declare `interpretationKind` as `application`, `container`,
|
|
385
|
+
`component`, `system`, `external_system`, `actor`, `person`, `context`, or
|
|
386
|
+
`datastore`. A choice question may instead declare `selected-choice`, but
|
|
387
|
+
every option must then be one of those exact lowercase kinds or `unknown`.
|
|
388
|
+
Arbitrary choice labels cannot select a semantic kind. Without this field,
|
|
389
|
+
answers retain the generic `analysis-boolean`, `analysis-choice`, or
|
|
390
|
+
`analysis-score` kind and never implicitly create C4 boundaries.
|
|
391
|
+
|
|
392
|
+
For a fixed mapping, an independently answerable question can be:
|
|
393
|
+
|
|
394
|
+
```json
|
|
395
|
+
{
|
|
396
|
+
"id": "application-supported",
|
|
397
|
+
"kind": "boolean",
|
|
398
|
+
"question": "Does the supplied current metadata support the selected entities as one application boundary?",
|
|
399
|
+
"interpretationKind": "application",
|
|
400
|
+
"interpretationLabel": "Order processing"
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
`interpretationLabel` is optional, requires an interpretation mapping, and is
|
|
405
|
+
limited to 80 characters with no markup, controls, or URLs. Core also applies
|
|
406
|
+
its local secret and locator filters. If omitted, the broker uses the first
|
|
407
|
+
selected entity's filtered core label. An explicit label can describe a
|
|
408
|
+
selection containing several entities. The label never determines the kind
|
|
409
|
+
and is never treated as provider evidence.
|
|
410
|
+
|
|
411
|
+
Only a supported, accepted answer with exact current source references can
|
|
412
|
+
receive a semantic kind. Missing probabilities/confidence, insufficient
|
|
413
|
+
support, or selecting `unknown` records a generic unknown interpretation.
|
|
414
|
+
Contradicted answers also keep their generic kind. Fixed mappings express
|
|
415
|
+
the author's intended meaning; they cannot override core thresholds or grant
|
|
416
|
+
authority. Mapping or label changes alter the hashed bundle and require fresh
|
|
417
|
+
approval. Score descriptors remain neutral 0–1 questions; the broker maps them
|
|
418
|
+
to `['Low', 'High']` independently of these output semantics.
|
|
419
|
+
|
|
420
|
+
`getAssets()` returns validated profiles with namespace
|
|
421
|
+
`<extension-id>.<profile-id>`. Installation validates their structure and
|
|
422
|
+
integrity; it cannot establish the truth or neutrality of natural-language
|
|
423
|
+
questions. The parent decision broker separately approves profile activation,
|
|
424
|
+
checks the current project/digest/profile grant, restricts candidate IDs to the
|
|
425
|
+
delivered scope, and applies source consent and local filtering before any
|
|
426
|
+
provider request. It records namespaced interpretations. Projection, view
|
|
427
|
+
switching, and replay never implicitly activate analysis.
|
|
428
|
+
|
|
429
|
+
## Parent integration contract
|
|
430
|
+
|
|
431
|
+
Initialize `await createExtensionRegistry({ dataDir, projectId })` outside the
|
|
432
|
+
synchronous hook path. Methods are asynchronous: `list`, `install`, `remove`,
|
|
433
|
+
`grant`, `revoke`, `getGrant`, `getAssets`, `doctor`, and `dev`.
|
|
434
|
+
|
|
435
|
+
Tests inject `transport(spec, { directory, signal })`, which returns tarball
|
|
436
|
+
bytes or a tarball path inside that private temporary directory. Production uses
|
|
437
|
+
the bounded npm pack transport. Injected transport is trusted host code and
|
|
438
|
+
does not become an extension manifest capability.
|
|
439
|
+
|
|
440
|
+
Pinned host routes:
|
|
441
|
+
|
|
442
|
+
```text
|
|
443
|
+
GET /api/extensions
|
|
444
|
+
POST /api/extensions/grant
|
|
445
|
+
POST /api/extensions/revoke
|
|
446
|
+
POST /api/extensions/analysis
|
|
447
|
+
GET /api/extensions/data/<id>
|
|
448
|
+
GET /api/extensions/frame/<id>?nonce=<fresh>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
The parent owns all authentication, strict route/query parsing, the frame host
|
|
452
|
+
UI, grant prompts, selection/inspection, and live/replay subscriptions. Extension
|
|
453
|
+
frames are not authenticated API principals and receive no viewer credentials.
|
|
454
|
+
|
|
455
|
+
`runtime/daemon/extension-api.mjs` provides
|
|
456
|
+
`createExtensionAPI({ registry, getSnapshot, projectId, runAnalysis? })`, returning
|
|
457
|
+
`{ handle }`. Call `await handle(req, res, { viewerAuthorized })` after the
|
|
458
|
+
parent's existing Host/Origin and viewer-cookie checks, before a legacy
|
|
459
|
+
blanket rejection of query strings. It returns `true` for handled extension
|
|
460
|
+
routes and `false` for unrelated routes. External read bearer tokens and opaque
|
|
461
|
+
origins must never satisfy `viewerAuthorized`.
|
|
462
|
+
|
|
463
|
+
`getSnapshot({ scopeId?, sessionId?, checkpointId?, persistent: false })` must
|
|
464
|
+
return the current-policy snapshot synchronously, just as the model API does.
|
|
465
|
+
The helper accepts only `scope`, `session`, and `checkpoint` on data routes
|
|
466
|
+
and `nonce` on frame routes. Duplicate, empty, unknown, or malformed parameters
|
|
467
|
+
are rejected. It tags requested checkpoints as replay before applying the
|
|
468
|
+
grant. It applies every frame response header and removes an inherited
|
|
469
|
+
`X-Frame-Options: DENY` only when delivering a valid authorized frame document.
|
|
470
|
+
|
|
471
|
+
The grant body is `{ id, digest, fields, history, approved, profiles? }`;
|
|
472
|
+
revocation takes `{ id }`. The analysis body is
|
|
473
|
+
`{ id, digest, profileId, entityIds, revision }`. Analysis requires the current
|
|
474
|
+
profile/field grant, a declared profile, current revision, and candidates in
|
|
475
|
+
the granted projection and the profile's candidate selector. It accepts no
|
|
476
|
+
caller-supplied questions, paths, source, or URLs.
|
|
477
|
+
|
|
478
|
+
The injected `runAnalysis` callback receives
|
|
479
|
+
`{ projectId, extensionId, digest, profile, entityIds, revision, grant, signal }`.
|
|
480
|
+
It returns `{ status, requestId?, interpretationIds? }`, where status is
|
|
481
|
+
`accepted`, `pending`, `complete`, or `unavailable`. The HTTP response includes
|
|
482
|
+
only those fields, and only interpretation IDs already recorded under the
|
|
483
|
+
profile namespace and visible through the current grant. The helper rechecks
|
|
484
|
+
grants after asynchronous asset/provider operations and sends no stale result
|
|
485
|
+
after revocation. Provider errors become fixed public errors. The callback
|
|
486
|
+
still owns source consent, local filtering, evidence version checks, provider
|
|
487
|
+
budgets, interpretation recording, and shared-job ownership.
|
|
488
|
+
|
|
489
|
+
Construct a frame response with:
|
|
490
|
+
|
|
491
|
+
```js
|
|
492
|
+
const installed = await registry.getAssets(extensionId);
|
|
493
|
+
const { body, headers } = createFrameDocument({ ...installed, nonce });
|
|
494
|
+
// Send every returned header and body; never serve body alone or as srcdoc.
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
`getAssets(id)` returns `{ manifest, assets, digest, profiles, development }`.
|
|
498
|
+
An optional `{ digest }` checks a pinned current digest. Optional
|
|
499
|
+
`{ assetPath }` returns `{ bytes, contentType, digest, assetPath }` only for
|
|
500
|
+
that exact installed, hash-verified declared file. Never fall through to a
|
|
501
|
+
filesystem/static-server path. A missing install or mismatched digest fails.
|
|
502
|
+
|
|
503
|
+
`createFrameDocument` returns a response-level CSP containing
|
|
504
|
+
`sandbox allow-scripts`, no network sources, no forms, workers, child frames,
|
|
505
|
+
CSS, media, or evaluated code, and SHA-256 allowances for the exact trusted
|
|
506
|
+
prelude and bundled script. It sets no-store, no-referrer, and nosniff.
|
|
507
|
+
The frame must also use `sandbox="allow-scripts"` without `allow-same-origin`.
|
|
508
|
+
Its document remains sandboxed when opened directly.
|
|
509
|
+
|
|
510
|
+
Bootstrap uses a fresh unpredictable base64url nonce of 24–128 characters:
|
|
511
|
+
|
|
512
|
+
1. Bind the installed digest, approved grant, instance, and nonce to this frame
|
|
513
|
+
document. The host must await its initial load.
|
|
514
|
+
2. The host creates a `MessageChannel` and posts
|
|
515
|
+
`{ type: "graphlin:bootstrap", apiVersion: 1, nonce }` with exactly one port
|
|
516
|
+
to the expected frame window. An opaque-origin target requires `"*"`;
|
|
517
|
+
that does not authorize a different window.
|
|
518
|
+
3. The prelude accepts only `event.source === parent`, the matching nonce,
|
|
519
|
+
one port, and its first bootstrap. It emits `graphlin:connect` with the port,
|
|
520
|
+
nonce, API version, and inert JSON assets, then sends
|
|
521
|
+
`{ type: "graphlin:ready", apiVersion: 1, nonce }` on the port.
|
|
522
|
+
4. The host checks the ready nonce and current grant/digest before sending
|
|
523
|
+
projected model data. Each subsequent message is context-bound and bounded.
|
|
524
|
+
5. On navigation, replacement, disposal, revocation, policy tightening, or
|
|
525
|
+
digest change, close old ports, cancel frame-owned work, clear revoked
|
|
526
|
+
scenes/caches, and bootstrap a new document only after approval. Shared
|
|
527
|
+
decision jobs retain their other authorized owners.
|
|
528
|
+
|
|
529
|
+
A sandboxed extension still reads the data the user grants it. CSP and opaque
|
|
530
|
+
origins are not a universal zero-disclosure guarantee, including frame
|
|
531
|
+
self-navigation. Revocation stops future delivery and clears host-controlled
|
|
532
|
+
copies; it cannot recall copied data. Iframes also do not guarantee hard CPU
|
|
533
|
+
or memory isolation. The parent browser integration must test actual browser
|
|
534
|
+
behavior, navigation replacement, direct opening, timeouts, and recovery before
|
|
535
|
+
enabling custom renderers.
|
|
536
|
+
|
|
537
|
+
## Offline conformance checks
|
|
538
|
+
|
|
539
|
+
From the Graphlin checkout:
|
|
540
|
+
|
|
541
|
+
```sh
|
|
542
|
+
npm test -- tests/extensions/*.test.mjs
|
|
543
|
+
npm run build
|
|
544
|
+
npm run check:packages
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The tests use original synthetic C4 data and injected registry transport. They
|
|
548
|
+
cover unknown features/capabilities, integrity, install rollback, cancellation,
|
|
549
|
+
unsafe paths/links/tar entries, digest-scoped grants, revocation, history,
|
|
550
|
+
allowlisted nested projection, scene mappings and bounds, nonce bootstrap,
|
|
551
|
+
custom status/selection, and SDK cancellation. They use no keys, live decision
|
|
552
|
+
provider, or private project. Browser isolation and npm-installed end-to-end
|
|
553
|
+
selection remain parent integration gates.
|