graphlin 0.1.3 → 0.2.1

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/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +12 -3
  4. package/docs/decision-service.md +393 -0
  5. package/docs/extension-authoring.md +553 -0
  6. package/docs/model-api.md +293 -0
  7. package/docs/usage.md +472 -0
  8. package/docs/visualizer-views.md +240 -0
  9. package/node_modules/@vscode/tree-sitter-wasm/LICENSE +21 -0
  10. package/node_modules/@vscode/tree-sitter-wasm/README.md +36 -0
  11. package/node_modules/@vscode/tree-sitter-wasm/SECURITY.md +41 -0
  12. package/node_modules/@vscode/tree-sitter-wasm/cgmanifest.json +16 -0
  13. package/node_modules/@vscode/tree-sitter-wasm/package.json +42 -0
  14. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm +0 -0
  15. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-c-sharp.wasm +0 -0
  16. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-cpp.wasm +0 -0
  17. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-css.wasm +0 -0
  18. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-go.wasm +0 -0
  19. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ini.wasm +0 -0
  20. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-java.wasm +0 -0
  21. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-javascript.wasm +0 -0
  22. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-php.wasm +0 -0
  23. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-powershell.wasm +0 -0
  24. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-python.wasm +0 -0
  25. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-regex.wasm +0 -0
  26. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-ruby.wasm +0 -0
  27. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-rust.wasm +0 -0
  28. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-tsx.wasm +0 -0
  29. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter-typescript.wasm +0 -0
  30. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.js +4075 -0
  31. package/node_modules/@vscode/tree-sitter-wasm/wasm/tree-sitter.wasm +0 -0
  32. package/node_modules/@vscode/tree-sitter-wasm/wasm/web-tree-sitter.d.ts +1027 -0
  33. package/package.json +78 -9
  34. package/plugin.json +4 -2
  35. package/runtime/architecture/analysis.mjs +344 -0
  36. package/runtime/architecture/controller.mjs +209 -0
  37. package/runtime/architecture/evidence.mjs +108 -0
  38. package/runtime/architecture/profile.mjs +56 -0
  39. package/runtime/core/evidence.mjs +43 -9
  40. package/runtime/core/graph.mjs +11 -6
  41. package/runtime/core/privacy.mjs +1 -0
  42. package/runtime/daemon/auth.mjs +7 -3
  43. package/runtime/daemon/diagnostics.mjs +1 -1
  44. package/runtime/daemon/extension-api.mjs +203 -0
  45. package/runtime/daemon/lineage.mjs +70 -0
  46. package/runtime/daemon/manager.mjs +9 -6
  47. package/runtime/daemon/model-api.mjs +728 -0
  48. package/runtime/daemon/model-persistence.mjs +220 -0
  49. package/runtime/daemon/server.mjs +81 -14
  50. package/runtime/daemon/settings.mjs +11 -3
  51. package/runtime/decisions/broker.mjs +349 -0
  52. package/runtime/decisions/contracts.mjs +179 -0
  53. package/runtime/decisions/evaluation.mjs +305 -0
  54. package/runtime/decisions/faults.mjs +32 -0
  55. package/runtime/decisions/index.mjs +818 -0
  56. package/runtime/decisions/profiles.mjs +93 -0
  57. package/runtime/decisions/questions.mjs +268 -0
  58. package/runtime/discovery/index.mjs +2 -0
  59. package/runtime/discovery/inventory.mjs +160 -0
  60. package/runtime/discovery/parser.mjs +40 -0
  61. package/runtime/discovery/structure.mjs +232 -0
  62. package/runtime/extensions/contracts.mjs +59 -0
  63. package/runtime/extensions/frame.mjs +64 -0
  64. package/runtime/extensions/index.mjs +9 -0
  65. package/runtime/extensions/manifest.mjs +95 -0
  66. package/runtime/extensions/packages.mjs +222 -0
  67. package/runtime/extensions/profiles.mjs +36 -0
  68. package/runtime/extensions/projection.mjs +130 -0
  69. package/runtime/extensions/registry.mjs +285 -0
  70. package/runtime/extensions/scene.mjs +105 -0
  71. package/runtime/extensions/sdk.d.ts +205 -0
  72. package/runtime/extensions/sdk.mjs +88 -0
  73. package/runtime/jev/index.mjs +13 -777
  74. package/runtime/jev/provider.mjs +101 -0
  75. package/runtime/jev/questions.mjs +16 -258
  76. package/runtime/jev/wire.mjs +17 -25
  77. package/runtime/model/changes.mjs +42 -0
  78. package/runtime/model/history.mjs +124 -0
  79. package/runtime/model/index.mjs +2 -0
  80. package/runtime/model/project-model.mjs +1020 -0
  81. package/runtime/model/records.mjs +240 -0
  82. package/runtime/pipeline.mjs +267 -55
  83. package/runtime/platform.mjs +254 -0
  84. package/runtime/visualizers/blocks.mjs +5 -0
  85. package/runtime/visualizers/c4.mjs +154 -0
  86. package/runtime/visualizers/changes.mjs +24 -0
  87. package/runtime/visualizers/code.mjs +5 -0
  88. package/runtime/visualizers/index.mjs +23 -0
  89. package/runtime/visualizers/structure.mjs +120 -0
  90. package/runtime/visualizers/timeline.mjs +66 -0
  91. package/runtime/web/app.js +225 -63
  92. package/runtime/web/extension-frame.js +128 -0
  93. package/runtime/web/index.html +38 -1
  94. package/runtime/web/model-client.js +162 -0
  95. package/runtime/web/platform.js +445 -0
  96. package/runtime/web/scene.js +111 -0
  97. package/runtime/web/style.css +51 -0
  98. package/schemas/graph.schema.json +4 -1
  99. package/scripts/arguments.mjs +5 -1
  100. package/scripts/build-packages.mjs +6 -2
  101. package/scripts/control.mjs +1 -1
  102. package/scripts/daemon.mjs +2 -1
  103. package/scripts/extensions.mjs +44 -0
  104. package/scripts/graphlin.mjs +23 -3
  105. package/scripts/onboarding.mjs +10 -3
  106. package/scripts/validate-packages.mjs +54 -8
@@ -0,0 +1,293 @@
1
+ # Local model API v1
2
+
3
+ `runtime/daemon/model-api.mjs` is a standalone transport module. It does not
4
+ install routes, select a global session, capture source, invoke a classifier,
5
+ persist state, or change a model. The parent daemon owns integration.
6
+
7
+ ```js
8
+ const api = createModelAPI({
9
+ projectId,
10
+ getSnapshot: options => model.snapshot(options),
11
+ getSessions: () => sessions, // optional; only if snapshots omit sessions
12
+ createCheckpoint: options => model.checkpoint(options), // optional
13
+ });
14
+
15
+ // AFTER validating the remote loopback address and the exact Host header:
16
+ if (await api.handle(req, res, { viewerAuthorized: auth.authorized(req) })) return;
17
+ // Continue existing routes and their existing authentication checks.
18
+
19
+ // After accepted model, activity, checkpoint, or disclosure-policy changes:
20
+ api.notify();
21
+ // During daemon shutdown:
22
+ api.close();
23
+ ```
24
+
25
+ `handle` resolves to `false` outside `/api/model/v1/` and otherwise sends the
26
+ response and resolves to `true`. The model callback is synchronous and returns
27
+ schema 2: `{schemaVersion: 2, projectId, revision, sequence, entities, relations,
28
+ interpretations, activity, coverage, sessions, checkpoints}`. It accepts
29
+ `{sessionId?, checkpointId?, scopeId?, persistent: false}`. Checkpoint creation
30
+ is also synchronous and returns a fixed marker with `id`, `projectId`,
31
+ `revision`, `sequence`, optional `label`/`sessionId`, and `at`. Replay calls
32
+ the snapshot callback with that checkpoint ID; it never reconstructs history
33
+ from the current model. Unknown markers should throw
34
+ `MODEL_CHECKPOINT_UNAVAILABLE`; capacity refusal may throw
35
+ `MODEL_CHECKPOINT_CAPACITY`.
36
+
37
+ ## Integration authentication requirements
38
+
39
+ Preserve loopback binding, remote-address checks, exactly one allowed Host,
40
+ header/request timeouts, and connection limits in the outer HTTP server.
41
+ Dispatch this module before the legacy blanket query-string rejection and
42
+ viewer-cookie gate. For this prefix only, delegate Origin and bearer checks
43
+ to this module after the address/Host checks. An outer same-origin-only gate
44
+ would prevent explicitly paired browser origins from working.
45
+
46
+ `viewerAuthorized` must mean a successfully authenticated host viewer. Never
47
+ derive it from an external grant. Host POSTs additionally require an exact
48
+ nonopaque Origin matching the validated Host. The outer host must validate
49
+ the scheme and its normal viewer CSRF rules before setting `viewerAuthorized`.
50
+ External bearer tokens are recognized only on this module's GET routes.
51
+ Never make them credentials for legacy endpoints, controls, capture, extension
52
+ installation, or the host evidence inspector. Any bearer header prevents a
53
+ request from acquiring viewer privileges, even alongside a viewer cookie.
54
+
55
+ The snapshot provider must apply **current** local-read/display and path
56
+ exclusion policy on every read, including checkpoints. The transport applies
57
+ a second, fixed field allowlist; it cannot infer project consent. Notify it
58
+ after policy tightening, before further model delivery. Viewer-cookie expiry
59
+ and host shutdown remain outer-server lifecycle responsibilities.
60
+
61
+ ## Routes
62
+
63
+ All paths below are relative to `/api/model/v1/`.
64
+
65
+ | Method and path | Result |
66
+ | --- | --- |
67
+ | `GET capabilities` | Versions, granted fields, limits and stream bounds |
68
+ | `GET bootstrap` | Initial bounded snapshot plus capabilities |
69
+ | `GET snapshot` | Bounded schema-2 snapshot and continuation cursors |
70
+ | `GET entities`, `relations`, `interpretations`, `activity`, `sessions` | One collection page |
71
+ | `GET entities/:id` | One projected entity |
72
+ | `GET entities/:id/children` | Direct children, paged |
73
+ | `GET history` | Checkpoint markers; `kind=activity` selects observations |
74
+ | `GET events` | SSE scoped snapshot stream |
75
+ | `POST checkpoints` | Viewer-only fixed checkpoint creation |
76
+ | `POST grants` | Viewer-only external-client pairing |
77
+ | `POST grants/revoke` | Viewer-only revocation |
78
+ | `OPTIONS` on GET routes | Narrow preflight for an actively paired Origin |
79
+
80
+ Read selectors are `scope`, `session`, and `checkpoint`. Scope includes the
81
+ named entity, descendants and reachable ancestors; relations and interpretations
82
+ remain inside that selection. Session selects activity/session history, not a
83
+ different project inventory. These selectors are client-local and never invoke
84
+ `selectSession`. Clients retain camera, selected entity, and follow state locally.
85
+
86
+ Snapshot, bootstrap, stream and page routes accept `limit=1..200`. Collection,
87
+ children and history pages accept `cursor`. Capabilities and POST routes accept
88
+ no query parameters. Unknown, empty and duplicate parameters, malformed IDs,
89
+ invalid limits, unexpected JSON fields and duplicate JSON keys are rejected.
90
+ Bodies must be UTF-8 JSON objects of at most 4 KiB; request URLs at most 4 KiB.
91
+ Tokens in query strings are never accepted. Checkpoint POST body:
92
+ `{"label":"Task baseline","sessionId":"session-example"}`; both keys are optional.
93
+
94
+ ## Pages, coverage and revision consistency
95
+
96
+ Each JSON response and SSE frame is bounded by 512 KiB. Snapshots contain at
97
+ most 200 records **across all six collections**, reserving room for activity
98
+ even with a large entity inventory. Byte limits can reduce this further.
99
+ `partial` and `pages.<collection>.{total,returned,nextCursor}` expose omitted
100
+ pages. A snapshot cursor goes to the corresponding collection route;
101
+ checkpoint cursors go to `history`. Keep exactly the same selectors.
102
+
103
+ Pages return `{items, page: {total, offset, returned, complete, nextCursor}}`
104
+ along with model revision, model sequence, selection and transport metadata.
105
+ Entities use deterministic breadth-first containment order: roots and ancestors
106
+ precede children, with siblings sorted by ID. The initial bounded snapshot
107
+ therefore preserves retained ancestor groups. Subsequent pages extend that
108
+ same assembly; they do not repeat its ancestors. Other collections use stable
109
+ ID ordering; activity and checkpoint ordering uses model sequence and then ID.
110
+ Cursors are signed and bind project/daemon epoch,
111
+ principal, collection or parent, selectors, revision, model sequence, and
112
+ the complete safe projection fingerprint. They expire after five minutes.
113
+ Changes, policy redaction, or expiry produce `409 stale_cursor`: discard the
114
+ partial assembly and obtain a fresh snapshot. Never merge inconsistent pages.
115
+ A cursor for a different principal, scope, or collection is invalid.
116
+
117
+ A 20,000-entity / 40,000-relation model is paged rather than rejected for
118
+ snapshot size. Inventory coverage exports allowlisted counts, completeness,
119
+ deferred counts, relationship counts, detail counts and parsing status.
120
+ `coverage.parsing` preserves `queued`, `active`, `deferred`, `parsed`, `failed`,
121
+ `stale`, `omitted`, and a bounded error code. It never contains parser source.
122
+
123
+ `coverage.lineage` preserves a bounded `id`, exactly one of the statuses `git`,
124
+ `not_git`, or `unavailable`, and optional safe `branch` and 40- or 64-character
125
+ hexadecimal `head`. Invalid required fields omit the lineage record; invalid
126
+ optional fields and unknown properties are stripped. Checkpoint GET and SSE
127
+ reads preserve the lineage recorded in that checkpoint, subject to current
128
+ disclosure policy. The API does not overlay the live branch or infer freshness;
129
+ the model owns branch/HEAD reconciliation. Lineage requires the `coverage`
130
+ grant field and participates in page consistency checks.
131
+
132
+ Entity `createdAtSequence` preserves a positive integer creation-observation
133
+ marker. `coverage.enumerations` preserves strictly validated certificates:
134
+ artifact/scope IDs, hash/generation, extractor and version, identity version,
135
+ covered line ranges, omission codes, capability and completeness. No arbitrary
136
+ certificate properties or range payloads are copied. A certificate cannot be
137
+ complete with invalid/missing ranges, unknown versions, omissions or a
138
+ nonparsed capability. Duplicate artifact certificates are withheld.
139
+
140
+ Certificates are bounded to 64 records/64 KiB and count toward the snapshot's
141
+ 200-record budget. At most half of a caller's smaller `limit` is allocated to
142
+ certificates. Scoped reads retain certificates for artifacts supporting the
143
+ selected entities. `coverage.enumerationCoverage` reports `total`, `returned`,
144
+ `omitted` and `truncated`; omitted proof must remain unknown in comparisons.
145
+ Clients can narrow the scope to recover relevant proof in a large project.
146
+ Inventory file/artifact arrays are still represented by counts.
147
+ `coverage.projection.omitted` counts records
148
+ withheld by record validation. Unknown fields and nested extension payloads are
149
+ never copied. A transport page being complete does not establish complete
150
+ discovery, source support, or runtime execution.
151
+
152
+ ## Stream and reconnect
153
+
154
+ Use streaming `fetch` with an Authorization header for external clients.
155
+ Native `EventSource` does not supply an arbitrary bearer header.
156
+
157
+ The SSE `snapshot` event contains the same bounded scoped schema as a snapshot
158
+ GET. Its ID is `epoch:transportSequence:selectionKey`; model `sequence` and
159
+ `revision` remain separate. Each `notify()` advances the transport sequence,
160
+ including activity-only and disclosure changes. Delivery is scheduled outside
161
+ the notifying call and can coalesce updates. The initial subscriber is
162
+ registered before its synchronous snapshot is obtained, without an intervening
163
+ await, so updates cannot fall between snapshot and subscription.
164
+
165
+ Reconnect with `Last-Event-ID`. The last 128 transport positions are retained.
166
+ A matching epoch, principal/selection and retained position yields one **fresh,
167
+ full replacement** snapshot with `transport.resume.fromSequence` and
168
+ `coalesced: true`. This is snapshot continuity, not replay of every intermediate
169
+ transition. The retained ring stores only positions, never old payloads.
170
+ Activity history comes from the model's ordered observations and named
171
+ checkpoints. Clients needing every observation must inspect model coverage and
172
+ history bounds; transport continuity does not imply unlimited model history.
173
+
174
+ Wrong lineage/selection, a future/evicted position or missing history permission
175
+ yields a `reset` event containing a reason and retained bounds, then a fresh
176
+ snapshot. Drop the old partial page assembly and scene. Duplicate snapshot
177
+ positions can be ignored within the same live selection. No semantic deltas
178
+ are currently emitted. Checkpoint streams continue to read their fixed marker.
179
+ Revocation/policy changes cannot recover data already copied by a client.
180
+
181
+ There are at most 16 open streams. A stream whose queued bytes plus its next
182
+ frame would exceed 512 KiB is disconnected; it can recover by reconnecting.
183
+ Snapshot failures close affected streams with `unavailable`. `notify()` and
184
+ `close()` are safe after shutdown. The provider remains responsible for keeping
185
+ its synchronous snapshot work bounded.
186
+
187
+ ## Pairing an external client
188
+
189
+ An authenticated same-origin host POST can create a grant:
190
+
191
+ ```json
192
+ {
193
+ "projectId": "project-example",
194
+ "fields": ["entities", "relations", "activity", "coverage"],
195
+ "history": false,
196
+ "ttlSeconds": 900,
197
+ "origins": []
198
+ }
199
+ ```
200
+
201
+ The response contains public grant metadata and a one-time plaintext `token`.
202
+ Only its SHA-256 hash is retained for authentication. A token is short-lived,
203
+ project-bound and read-only. `fields` is an explicit nonempty subset of
204
+ `entities`, `relations`, `interpretations`, `activity`, `coverage`, `sessions`,
205
+ and `checkpoints`. TTL is 1–3600 seconds (default 900); at most 32 grants exist.
206
+ Grants are memory-only and all expire on daemon restart.
207
+
208
+ `history: false` allows current snapshots, including the current activity
209
+ window; it forbids checkpoint/session selectors, history routes and historical
210
+ stream resume, and withholds checkpoint markers. Requesting an ungranted
211
+ collection is forbidden. Empty arrays stand in for ungranted snapshot fields;
212
+ coverage contains only transport withholding counts unless granted.
213
+
214
+ Native clients omit Origin. Browser clients must use an exact HTTP(S) origin
215
+ listed in the grant, such as `https://visualizer.example`. Wildcards, URL paths,
216
+ credentials and opaque `null` origins are rejected. CORS responses echo only
217
+ that origin, allow GET and Authorization/Last-Event-ID headers, and never allow
218
+ credentials. A preflight reveals no model and requires an active paired origin.
219
+ The subsequent GET must still authenticate its individual token and Origin.
220
+
221
+ Revoke with `POST grants/revoke`, body `{"grantId":"grant-example"}`.
222
+ Revocation ends existing streams immediately. Independent expiry timers plus
223
+ an idle sweep end expired streams without requiring a model update or another
224
+ request. Neither external bearer credentials nor a paired origin can create
225
+ checkpoints/grants, revoke other clients, install extensions or control capture.
226
+
227
+ Every route, history response and stream uses the same record allowlists.
228
+ There is no raw source, excerpt, prompt, transcript, hook input, credential,
229
+ absolute locator, or arbitrary namespace payload field in this contract.
230
+ Labels are approved display metadata supplied by the core, additionally checked
231
+ by the local secret/text filter. This is not a general-purpose sanitizer for
232
+ arbitrary source embedded in labels; providers must never supply such content.
233
+
234
+ ## Separate model persistence
235
+
236
+ `runtime/daemon/model-persistence.mjs` is an optional, separate parent integration.
237
+ It neither opens nor changes legacy `state.json`. Use the canonical private
238
+ project-data directory already established by the daemon:
239
+
240
+ ```js
241
+ const modelStore = createModelPersistence(
242
+ path.join(paths.directory, 'model-state.json'),
243
+ );
244
+ const restoredModel = await modelStore.load(); // undefined when absent/unusable
245
+ // Pass restoredModel to the model constructor; it revalidates evidence/policy.
246
+ modelStore.schedule(model.snapshot({ persistent: true }));
247
+ await modelStore.flush(); // explicit durability point
248
+ await modelStore.close(); // stop scheduling and drain before releasing daemon lock
249
+ ```
250
+
251
+ The interface is `createModelPersistence(filename, {projectId?, maxBytes?,
252
+ debounceMs?, now?} = {})`. The filename-only call matches the legacy factory.
253
+ An omitted project ID binds to the first successfully loaded or scheduled
254
+ snapshot; rejected data never binds it. Later cross-project data is refused.
255
+ Passing `{projectId: paths.projectId}` additionally validates the project on
256
+ the first load. Full SHA-256 project IDs are supported.
257
+ Its methods are `load`, `schedule`, `flush`, `close` and
258
+ `stats`. `schedule` synchronously captures immutable JSON and returns whether
259
+ the snapshot was accepted. Calls coalesce within a 100 ms window; a running
260
+ write retains at most one replacement snapshot, so continuous updates do not
261
+ starve persistence. `flush` and `close` wait for accepted writes, including a
262
+ replacement queued during a write. The parent supplies its existing exclusive
263
+ daemon lock; this module is not a multiprocess lock service.
264
+
265
+ The disk envelope is `{schemaVersion: 2, savedAt, projectId, snapshot}`.
266
+ The **complete envelope** must fit 48 MiB; `maxBytes` can lower but never raise
267
+ that ceiling. Byte counting precedes full JSON encoding, including UTF-8 and
268
+ escape expansion. Oversized or invalid input is refused intact: no records,
269
+ checkpoint states or support are silently trimmed. The previous accepted file
270
+ and pending snapshot survive refusal. Persistent `.storage` and checkpoint
271
+ states are preserved; they still cannot pass through HTTP API projection.
272
+
273
+ The parent must supply `model.snapshot({persistent: true})` under current
274
+ policy, rather than a display snapshot or raw model internals. Persistence does
275
+ not grant new source access, reinterpret evidence, or replace policy filtering.
276
+ Undefined object fields used for withheld paths are omitted using normal JSON
277
+ semantics; unsupported values, accessors, cycles and oversized nesting are refused.
278
+ Loading likewise does not establish current evidence validity; restore through
279
+ the core model and reapply current policy before serving anything.
280
+
281
+ Writes use an exclusive 0600 temporary file in the same private directory,
282
+ file sync, atomic rename and directory sync where supported. The target and
283
+ directory must be owned by the current user, private, and free of symlink or
284
+ hard-link substitutions. The directory must already exist at its canonical
285
+ path. The legacy `state.json` filename is explicitly rejected. Failed writes
286
+ clean their temporary file and report aggregate `persistenceFailures` through
287
+ `stats`; no contents, credentials or paths are logged.
288
+
289
+ Missing, malformed, oversized, incompatible, wrong-project or unsafe model
290
+ files return `undefined`. They are never automatically removed, and valid
291
+ model files have no implicit age expiry. The parent can retain legacy-only
292
+ operation when model load fails. Rollback continues to read untouched
293
+ `state.json`; the parent owns any deliberate migration and package rollback.