graphlin 0.1.3 → 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.
Files changed (102) 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 +465 -0
  8. package/docs/visualizer-views.md +199 -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 +74 -9
  34. package/plugin.json +4 -2
  35. package/runtime/core/evidence.mjs +43 -9
  36. package/runtime/core/graph.mjs +11 -6
  37. package/runtime/core/privacy.mjs +1 -0
  38. package/runtime/daemon/auth.mjs +7 -3
  39. package/runtime/daemon/diagnostics.mjs +1 -1
  40. package/runtime/daemon/extension-api.mjs +203 -0
  41. package/runtime/daemon/lineage.mjs +70 -0
  42. package/runtime/daemon/manager.mjs +9 -6
  43. package/runtime/daemon/model-api.mjs +728 -0
  44. package/runtime/daemon/model-persistence.mjs +220 -0
  45. package/runtime/daemon/server.mjs +70 -12
  46. package/runtime/daemon/settings.mjs +11 -3
  47. package/runtime/decisions/broker.mjs +349 -0
  48. package/runtime/decisions/contracts.mjs +179 -0
  49. package/runtime/decisions/evaluation.mjs +305 -0
  50. package/runtime/decisions/faults.mjs +32 -0
  51. package/runtime/decisions/index.mjs +818 -0
  52. package/runtime/decisions/profiles.mjs +93 -0
  53. package/runtime/decisions/questions.mjs +268 -0
  54. package/runtime/discovery/index.mjs +2 -0
  55. package/runtime/discovery/inventory.mjs +160 -0
  56. package/runtime/discovery/parser.mjs +40 -0
  57. package/runtime/discovery/structure.mjs +232 -0
  58. package/runtime/extensions/contracts.mjs +59 -0
  59. package/runtime/extensions/frame.mjs +64 -0
  60. package/runtime/extensions/index.mjs +9 -0
  61. package/runtime/extensions/manifest.mjs +95 -0
  62. package/runtime/extensions/packages.mjs +222 -0
  63. package/runtime/extensions/profiles.mjs +36 -0
  64. package/runtime/extensions/projection.mjs +130 -0
  65. package/runtime/extensions/registry.mjs +285 -0
  66. package/runtime/extensions/scene.mjs +105 -0
  67. package/runtime/extensions/sdk.d.ts +205 -0
  68. package/runtime/extensions/sdk.mjs +88 -0
  69. package/runtime/jev/index.mjs +13 -777
  70. package/runtime/jev/provider.mjs +101 -0
  71. package/runtime/jev/questions.mjs +16 -258
  72. package/runtime/jev/wire.mjs +17 -25
  73. package/runtime/model/changes.mjs +42 -0
  74. package/runtime/model/history.mjs +124 -0
  75. package/runtime/model/index.mjs +2 -0
  76. package/runtime/model/project-model.mjs +889 -0
  77. package/runtime/model/records.mjs +239 -0
  78. package/runtime/pipeline.mjs +127 -48
  79. package/runtime/platform.mjs +254 -0
  80. package/runtime/visualizers/blocks.mjs +5 -0
  81. package/runtime/visualizers/c4.mjs +52 -0
  82. package/runtime/visualizers/changes.mjs +24 -0
  83. package/runtime/visualizers/code.mjs +5 -0
  84. package/runtime/visualizers/index.mjs +23 -0
  85. package/runtime/visualizers/structure.mjs +120 -0
  86. package/runtime/visualizers/timeline.mjs +66 -0
  87. package/runtime/web/app.js +225 -63
  88. package/runtime/web/extension-frame.js +128 -0
  89. package/runtime/web/index.html +36 -1
  90. package/runtime/web/model-client.js +162 -0
  91. package/runtime/web/platform.js +337 -0
  92. package/runtime/web/scene.js +111 -0
  93. package/runtime/web/style.css +49 -0
  94. package/schemas/graph.schema.json +4 -1
  95. package/scripts/arguments.mjs +5 -1
  96. package/scripts/build-packages.mjs +6 -2
  97. package/scripts/control.mjs +1 -1
  98. package/scripts/daemon.mjs +2 -1
  99. package/scripts/extensions.mjs +44 -0
  100. package/scripts/graphlin.mjs +23 -3
  101. package/scripts/onboarding.mjs +10 -3
  102. package/scripts/validate-packages.mjs +54 -8
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Local architecture and activity viewer with passive, bounded event hooks.",
5
5
  "author": {
6
6
  "name": "Graphlin contributors"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphlin",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Local architecture and activity diagrams from observable coding-agent work.",
5
5
  "author": {
6
6
  "name": "Graphlin contributors"
package/README.md CHANGED
@@ -7,7 +7,7 @@ Live architecture diagrams while **Claude Code or Codex** explores and builds yo
7
7
  ## Get started
8
8
 
9
9
  **You need:** macOS or Linux, Node.js 22.14+, and Claude Code or Codex CLI.
10
- Source classification also needs a TypeSafe API key; metadata mode needs no key.
10
+ Local parsing needs no key. Optional AI classification uses a TypeSafe API key.
11
11
 
12
12
  ### 1. Start the viewer
13
13
 
@@ -17,13 +17,14 @@ In your project's terminal:
17
17
  npx --yes graphlin@latest
18
18
  ```
19
19
 
20
- Graphlin asks which host to install, offers **source or metadata** mode for this
20
+ Graphlin asks which host to install, offers **local, source, or metadata** mode for this
21
21
  project, and accepts your key at a **masked prompt** if needed. It stores the key
22
22
  privately and builds stable plugins outside the npm cache. The browser opens
23
23
  automatically. Keep this terminal running; **Ctrl+C** stops the viewer.
24
24
 
25
25
  Source mode permits locally filtered source excerpts, user prompts, and public
26
26
  agent messages to be sent to TypeSafe for classification.
27
+ Local mode parses JavaScript, TypeScript, TSX, and Python on your machine.
27
28
 
28
29
  ### 2. Start your agent
29
30
 
@@ -54,6 +55,14 @@ if you selected a custom data directory.
54
55
  For setup alone, append `init` to the command. Append `uninstall` to remove
55
56
  Graphlin's host plugins across projects while keeping your saved key and history.
56
57
 
58
+ Choose **Code**, nested **Blocks**, **C4**, **Changes**, or **Activity timeline**.
59
+ Search with `/`, expand a source scope, or set a task baseline. Architecture
60
+ documents are optional; uncertain boundaries stay marked as unknown.
61
+
62
+ Build another view with the [visualizer SDK](docs/extension-authoring.md).
63
+ Install it with `npx graphlin extensions add package-name@version`, then approve
64
+ its project access in the viewer.
65
+
57
66
  ## Just looking?
58
67
 
59
68
  Try the offline demo—no key or agent required:
@@ -64,7 +73,7 @@ npx --yes graphlin@latest demo
64
73
 
65
74
  ## More
66
75
 
67
- [User guide](docs/usage.md) · [Design](docs/graphlin-design.md) ·
76
+ [User guide](docs/usage.md) · [Views](docs/visualizer-views.md) · [Design](docs/graphlin-design.md) ·
68
77
  [Contributing](CONTRIBUTING.md) · [Releasing](docs/releasing.md) · [MIT license](LICENSE)
69
78
 
70
79
  Graphlin visualizes observable actions and code evidence. It does not capture
@@ -0,0 +1,393 @@
1
+ # Provider-independent decisions
2
+
3
+ The daemon owns one decision service. A provider supplies bounded answers, never
4
+ bundles, graph mutations, source access, consent, evidence freshness, or grants.
5
+ `runtime/decisions/` has no dependency on `runtime/jev/`. Jev's adapter owns HTTP,
6
+ endpoint restrictions, credentials, model validation, wire types, and response
7
+ parsing. Alternate providers require none of those vendor conventions.
8
+
9
+ ## Integration factories
10
+
11
+ ```js
12
+ import { createDecisionService } from '../runtime/decisions/index.mjs';
13
+ import { createJevProvider } from '../runtime/jev/provider.mjs';
14
+
15
+ const decisions = createDecisionService({
16
+ provider: createJevProvider({ apiKey, fetchImpl }), // Replace this adapter only.
17
+ // Existing: materializeBundle, buildRelationProposals, clock, limits,
18
+ // intakePolicy, admissionPolicy.
19
+ // Optional: profiles: [], cache: { maxEntries: 512, maxBytes: 8388608, ttlMs: 60000 }.
20
+ });
21
+ ```
22
+
23
+ `createJevProvider({ apiKey, model = 'jev-1.13.0',
24
+ fetchImpl = globalThis.fetch, endpoint })` uses the existing production
25
+ endpoint by default. Endpoint overrides are restricted to injected loopback
26
+ test transports. Its provenance is
27
+ `{ id: 'jev', version: '1', model: 'jev-1.13.0' }`; the selected model is
28
+ reported if overridden. Mode is `live`, or `demo` for the existing branded
29
+ fixture transport. Missing credentials return `unavailable / missing_key`.
30
+ An injected alternate provider needs no Jev model, URL, key, or environment
31
+ variable. Model provenance is optional for alternate providers.
32
+
33
+ The old `runtime/jev/index.mjs` exports remain available:
34
+ `createDecisionService({ apiKey, fetchImpl, model, endpoint, ...coreOptions })`
35
+ explicitly selects Jev. It also exports `createJevProvider`,
36
+ `createFixtureTransport`, and the existing limit/policy constants. Jev's
37
+ question-builder and wire-validator exports remain compatible. No factory reads
38
+ environment keys.
39
+
40
+ The unchanged pipeline accepts this service through its existing
41
+ `decisionService` option. `classify({ event, candidates, policy, deadlineAt,
42
+ signal })`, `stats()`, and `close()` retain their existing contracts.
43
+
44
+ ## Broker evaluation API and SDK descriptors
45
+
46
+ `evaluate(input)` and `decide(input)` are aliases. These are trusted daemon
47
+ interfaces, not extension message handlers. The broker first validates the
48
+ installed profile, grant, consent, replay state, metadata projection, and
49
+ ownership. It supplies only approved model metadata and identifiers/relations.
50
+ Do not pass extension-provided state, prompts, paths, URLs, or raw source here.
51
+ No render mount, replay, layout, or view switch should implicitly invoke them.
52
+
53
+ ```js
54
+ const result = await decisions.evaluate({
55
+ state: {
56
+ entities: [{ id: 'entity-1', kind: 'module' }],
57
+ relations: [],
58
+ },
59
+ questions: [
60
+ {
61
+ id: 'boundary', kind: 'choice',
62
+ question: 'Which supplied boundary describes `entities[0]`?',
63
+ options: [
64
+ { id: 'component', label: 'A supported component boundary' },
65
+ { id: 'unknown', label: 'Insufficient boundary evidence' },
66
+ ],
67
+ requiredMetrics: ['probabilities', 'confidence'],
68
+ },
69
+ { id: 'supported', kind: 'boolean', question: 'Is this boundary supported?',
70
+ requiredMetrics: ['probability'] },
71
+ { id: 'relevance', kind: 'score', question: 'Rate relevance to the supplied scope.',
72
+ options: ['Low', 'Medium', 'High'] },
73
+ ],
74
+ profile: { id: 'example.boundaries', version: '1' }, // Optional.
75
+ cacheContext: { // Optional; all six fields required when provided.
76
+ projectId: 'project-1', worktreeId: 'worktree-1', lineage: 'branch-generation-1',
77
+ policyVersion: 'policy-1', evidenceVersion: 'metadata-revision-7', taskScope: null,
78
+ },
79
+ signal, deadlineAt,
80
+ });
81
+ ```
82
+
83
+ Question fields:
84
+
85
+ | Field | Contract |
86
+ | --- | --- |
87
+ | `id` | Unique bounded identifier |
88
+ | `kind` | `boolean`, `choice`, or `score` |
89
+ | `question` | Host-validated question text, up to 8192 characters |
90
+ | `focus` | Optional additional instruction, up to 8192 characters |
91
+ | `options` | Omitted for boolean; choice has 2–128 distinct `{id,label}` entries; score has 2–128 ordered string labels |
92
+ | `requiredMetrics` | Optional array: boolean supports `probability`; choice/score support `probabilities` and `confidence` |
93
+
94
+ Score values use zero-based option indices. A probabilistic score must equal
95
+ its distribution's weighted index, within the existing rounding tolerance.
96
+ Questions in one request are independent; they cannot consume another answer
97
+ from that request. Dependent stages require an explicit subsequent broker call
98
+ within the broker's overall workflow budget.
99
+
100
+ Every result has this shape:
101
+
102
+ ```js
103
+ {
104
+ status: 'accepted', // Or abstained, invalid, unavailable, timeout, overloaded.
105
+ answers: [{
106
+ id: 'boundary', kind: 'choice', value: 'component',
107
+ probability: null,
108
+ probabilities: { component: 0.94, unknown: 0.06 },
109
+ confidence: 0.95,
110
+ }],
111
+ provenance: {
112
+ contractVersion: 1,
113
+ provider: { id: 'provider-id', version: '1' }, // Optional model if supplied.
114
+ profile: { id: 'example.boundaries', version: '1' }, // Or null.
115
+ cacheContext: { /* the exact supplied version context, or null */ },
116
+ inputHash: 'sha256-of-exact-provider-encoded-input',
117
+ completedAt: 1234,
118
+ usage: { inputTokens: 10, outputTokens: 5 }, // Or null.
119
+ mode: 'local', // live, demo, or local.
120
+ cacheKey: 'sha256-of-versioned-request-context', // Or null.
121
+ },
122
+ diagnostics: {
123
+ code: 'ok', calls: 1,
124
+ cache: { status: 'miss', key: 'sha256-of-versioned-request-context' },
125
+ // Existing bounded timing, question counts, usage and trace metadata.
126
+ },
127
+ }
128
+ ```
129
+
130
+ Boolean `value` is a supplied boolean or null; `probability` is the probability
131
+ that the proposition is true. Neither is inferred from the other. Choice
132
+ `value` is an option ID; score `value` is the numeric score. All unavailable
133
+ metrics are null, including irrelevant metric fields. Never interpret missing
134
+ confidence or a deterministic selection as probability 1. A provider can
135
+ return an answer without probabilities only when the question does not require
136
+ them. Missing required metrics yield `abstained / missing_answer_metrics`;
137
+ unsupported declared capabilities yield `abstained / unsupported_capability`
138
+ with `diagnostics.capabilityLimitations`. Invalid results have no answers.
139
+
140
+ `accepted` means the bounded answer contract passed. Core still decides whether
141
+ the answer supports an interpretation, a graph change, or no change. This does
142
+ not establish execution, verification, or authoritative architectural truth.
143
+ Provider-added text, explanations, bundles, judgments, and arbitrary usage
144
+ fields are discarded.
145
+
146
+ The metadata API rejects non-JSON values, cycles, excessive depth/size, and
147
+ obvious source/credential fields such as `code`, `text`, `snippet`, `transcript`,
148
+ `prompt`, and `apiKey`. This guard is not a substitute for broker authorization
149
+ or core's local secret filtering; approved metadata must not hide raw content
150
+ under another field name. No raw metadata state is retained in completed cache
151
+ entries or diagnostics.
152
+
153
+ ## Extension analysis broker
154
+
155
+ ```js
156
+ import { createAnalysisBroker } from '../runtime/decisions/broker.mjs';
157
+
158
+ const runAnalysis = createAnalysisBroker({
159
+ service: decisions,
160
+ model: pipeline.model,
161
+ policy, // A policy object, or () => currentPolicy for a changing policy.
162
+ projectId,
163
+ registry,
164
+ });
165
+
166
+ const result = await runAnalysis({
167
+ projectId, extensionId, digest,
168
+ profile, // Installed declarative profile from runtime/extensions/profiles.mjs.
169
+ entityIds, // Nonempty unique canonical model IDs.
170
+ revision, // Current live model revision.
171
+ grant, // Grant snapshot already checked by the HTTP helper.
172
+ signal,
173
+ });
174
+ // { status: 'complete', requestId, interpretationIds }
175
+ // or { status: 'unavailable' }
176
+ ```
177
+
178
+ These are the exact five factory options. `service` supplies `evaluate`;
179
+ `model` supplies synchronous `snapshot()` and `observeInterpretations()`.
180
+ `registry` supplies async `getAssets(extensionId, { digest })` and
181
+ `getGrant(extensionId)`. Use the same policy and project model as the pipeline.
182
+ The factory returns the async function directly, with no extra object wrapper,
183
+ queue, cache, source reader, or transport.
184
+
185
+ The parent HTTP helper authenticates explicit POST requests with exactly
186
+ `{id,digest,profileId,entityIds,revision}` and resolves the current installed
187
+ profile and grant before this callback. It must never accept extension-supplied
188
+ state or raw source. It owns response notification after the callback. Mounting,
189
+ rendering, viewing, switching layouts, and replay never invoke analysis.
190
+
191
+ The broker independently checks `analysis.request`, the immutable package
192
+ digest and installed profile, approved profile/field/entity scope, current
193
+ grant equality, and `transmitSource`. Read-only source consent is insufficient.
194
+ It rejects replay/checkpoint snapshots. It repeats authority, policy,
195
+ revision, metadata, and exact-reference checks after evaluation before core
196
+ admission; it checks authority and resulting current records again before
197
+ returning IDs. Revocation, stale evidence, malformed responses, cancellation,
198
+ or exceeded bounds produce only `unavailable`.
199
+
200
+ Profile questions are `{id,kind,question,options?,interpretationKind?,
201
+ interpretationLabel?}`. Choice options are strings,
202
+ mapped to neutral `{id: 'option-N', label}` entries. Extension scores have no
203
+ options and a 0–1 range; the broker supplies neutral `['Low','High']` criteria.
204
+ No taxonomy, provider name, model, key, endpoint, or vendor wire type is part of
205
+ the extension profile schema. Questions remain independent.
206
+
207
+ Only selected, current, locally filtered model metadata is sent. Entity fields
208
+ are `id/kind/label/basis/parentId`; relation fields are
209
+ `id/source/target/kind/basis`. Optional interpretation context contains bounded
210
+ IDs, namespace/version, kind, safe label, entity IDs, and support. Relationships
211
+ and interpretations cannot escape the selected entity scope. Paths,
212
+ qualified names, source references, raw source, and arbitrary extra fields
213
+ are omitted from transmission. Unsafe labels are replaced locally and make
214
+ support unknown. Profile text is locally secret-filtered before dispatch.
215
+
216
+ The exported `ANALYSIS_LIMITS` are 256 entities, 128 relations, 64 context
217
+ interpretations, 16 exact source references, 16 questions, and 32 KiB metadata.
218
+ The service's lower configured limits also apply. Oversized requests fail
219
+ closed, without implicit batching or truncation. Nonempty references require
220
+ a current, fresh, present artifact with exactly matching hash/generation and a
221
+ known, safe, nonexcluded relative path; that path stays local. Public intent
222
+ references cannot support analysis. Entities without references can receive
223
+ only unknown interpretations.
224
+
225
+ Evaluation provenance uses profile `{id: extensionId + '.' + profile.id,
226
+ version: digest}`. The cache context binds project/worktree to the canonical
227
+ project ID and lineage to `snapshot.coverage.lineage.id ?? projectId`. Policy
228
+ version binds the effective policy plus the exact grant; evidence version binds
229
+ revision plus the metadata/reference/lineage hash; task scope binds
230
+ extension/profile/digest. A changed opaque lineage ID rejects an in-flight
231
+ result even if source and revision are otherwise identical. Branch names and
232
+ HEAD values are never sent to the provider; the opaque lineage ID stays in
233
+ local cache provenance. The parent also invalidates/cancels work when replacing
234
+ the project model.
235
+
236
+ Profile `interpretationKind` may explicitly name `application`, `container`,
237
+ `component`, `system`, `external_system`, `actor`, `person`, `context`, or
238
+ `datastore`. `selected-choice` is allowed only for choice questions whose
239
+ options are those exact kinds or `unknown`. Only supported, accepted answers
240
+ with nonempty current references receive the declared kind. The broker derives
241
+ it from the validated declaration and selected option ID, never from answer
242
+ text or an arbitrary label. All other records keep their generic analysis kind.
243
+ Without a declaration, choosing the string `application` cannot create a C4
244
+ boundary; the default view remains unknown.
245
+
246
+ An optional `interpretationLabel` requires a mapping and is limited to 80
247
+ locally filtered characters. Otherwise the boundary uses the first selected
248
+ entity's filtered core label. The label and mapping stay out of provider
249
+ question descriptors; their installed digest binds them into grant and cache
250
+ provenance. They do not change provider capabilities or wire contracts.
251
+
252
+ Interpretations use namespace `extensionId + '.' + profile.id`, version
253
+ `digest`, basis `decision`, and exact current references retained locally.
254
+ The combined namespace must fit the model's 80-character limit. Missing
255
+ probabilities or required confidence always produce `support: 'unknown'` and
256
+ `classification: 'unknown'`, even if a deterministic provider selected a value.
257
+ Probability thresholds reuse core admission defaults. Incomplete metadata
258
+ support also remains unknown. These records do not establish runtime success.
259
+
260
+ The broker reads back model-assigned IDs and returns only records that match
261
+ this request, namespace, version, references, and current validity. It respects
262
+ the core display-label projection. Capacity rejection cannot fabricate IDs;
263
+ partial retention returns only the IDs actually retained. Results expose no
264
+ provider response fields, grants, prompts, or metadata. The wider HTTP callback
265
+ contract also permits `accepted` and `pending`; this synchronous admission
266
+ broker currently returns only `complete` or `unavailable`.
267
+
268
+ ## Scheduling, cache, cancellation, and freshness
269
+
270
+ All entry points share one concurrency limit, pending queue, cooldown, request
271
+ and response byte limits, question limits, and deadline clock. Source work
272
+ reserves two calls; a broker evaluation reserves one. Defaults remain two active
273
+ workflows, 32 queued workflows, 2000 ms, 40 questions per request, 64 KiB request,
274
+ and 256 KiB response. Requests do not retry. Adapter serialization is synchronous
275
+ and side-effect-free; transport and response-body work must honor the shared
276
+ abort signal. The service also bounds providers that ignore it.
277
+ Queued broker evaluations yield to queued source/intake work.
278
+
279
+ Evaluation caching requires all six `cacheContext` fields. They are bounded
280
+ opaque identifiers or nonnegative integer versions; `taskScope` may be null.
281
+ The broker must bind `evidenceVersion` to every supplied record/reference and
282
+ `lineage` to the selected worktree/branch. Exact metadata, questions, options,
283
+ required metrics, profile ID/version, provider ID/version/model/mode and
284
+ capabilities also enter the key. Different taxonomy options therefore cannot
285
+ reuse the same result. No context means no cache.
286
+
287
+ Only successful normalized evaluation results are cached. Defaults are 512
288
+ entries, 8 MiB total serialized results, and 60 seconds, with LRU eviction.
289
+ Set any cache bound to zero to disable caching and coalescing. Cache entries
290
+ contain answers and provenance, not source, state, or approved bundle objects.
291
+ Source `classify` and `analyze` never reuse serialized approval capabilities.
292
+ The parent may retain their original branded bundles under its own bounded
293
+ admission lifecycle.
294
+
295
+ Concurrent equivalent evaluations share one queued/active workflow. Each
296
+ subscriber has its own cancellation and deadline; losing one subscriber does
297
+ not cancel another. The underlying work stops when no subscribers remain.
298
+ The first workflow's service deadline cannot be extended by later subscribers.
299
+ Subscriber count is bounded by concurrency plus queue capacity. Stats expose
300
+ cache hits, shared hits, evictions, entries/bytes, and active subscribers.
301
+
302
+ `invalidateCache()` clears retained evaluations and aborts shared evaluations
303
+ as `stale_evidence`; use it on revocation or scope invalidation, together with
304
+ the broker's owner cancellation. It invalidates the whole service cache.
305
+ Every consumer must still revalidate policy, lineage, evidence versions, task
306
+ scope, and grant before applying either a fresh or cached result. Noncached
307
+ calls are cancelled through their supplied signal. `close()` cancels all work
308
+ and clears cached results. A cache hit reports zero new calls/usage; provenance
309
+ retains the original provider usage and completion time.
310
+
311
+ ## Source A/B and registered source profiles
312
+
313
+ `classify` keeps the existing neutral architecture catalog: immutable candidate
314
+ snapshots, shared evidence sensitivity, per-entity relevance, core
315
+ `materializeBundle`, exact approved B evidence, independent relation/context
316
+ questions, and unchanged admission thresholds. The returned bundle is the
317
+ exact branded core object. Providers never receive core functions, candidates'
318
+ private IDs/digests, or the bundle capability. Source evidence and public intent
319
+ stay distinct. The unchanged pipeline performs final version revalidation.
320
+
321
+ Optional host `profiles` registrations support source-backed analysis through
322
+ `analyze({ ...classifyInput, profileId })`. Each registration has
323
+ `{ id, version, scope: 'entity' | 'bundle', questions }`; `questions` is an object
324
+ keyed by question ID, containing neutral
325
+ `{ type, instructions: { question, focus? }, criteria, requiredMetrics? }`.
326
+ Boolean criteria have `true`/`false` descriptions, choice criteria map option
327
+ IDs to descriptions, and score criteria are ordered string labels.
328
+ Entity instructions use `{{entity}}` and `{{evidence}}` placeholders, expanded
329
+ to exact approved array paths. The registry is a trusted host operation, not
330
+ an extension callback.
331
+
332
+ This path always runs A first and uses only the exact core bundle for its B
333
+ questions. It returns `abstained / profile_answers` plus
334
+ `analysis: { profileId, profileVersion, status: 'answered', answers, subjects }`
335
+ and no graph judgments. `subjects` maps question IDs to local approved candidate
336
+ IDs (null for bundle questions); core owns interpretation admission. Budgets
337
+ can reject a large profile rather than adding hidden stages or weakening intake.
338
+ The low-level array descriptor API above is the broker/SDK integration seam;
339
+ these source registrations are an additional internal A/B facility.
340
+
341
+ ## Adapter contract v1
342
+
343
+ A provider supplies:
344
+
345
+ ```js
346
+ {
347
+ contractVersion: 1, id: 'example', version: '1', mode: 'local',
348
+ // model is optional opaque provenance, not a required vendor name.
349
+ capabilities: {
350
+ boolean: { probability: true },
351
+ choice: { probabilities: true, confidence: true },
352
+ score: { probabilities: true, confidence: true },
353
+ },
354
+ unavailableCode: null, // Or provider_unavailable; Jev compatibility uses missing_key.
355
+ encode(request) { return JSON.stringify(request); },
356
+ async execute(encoded, { signal, deadlineAt, maxResponseBytes, now, reportTransport }) {
357
+ // Return { answers: { questionId: normalizedAnswer }, usage: null | counts }.
358
+ },
359
+ }
360
+ ```
361
+
362
+ Absent capability types are unsupported; omitted/false metric flags mean the
363
+ provider cannot supply that metric. The service exposes its immutable snapshot
364
+ as `service.capabilities`. Internal request descriptors use the neutral
365
+ `type/instructions/criteria/requiredMetrics` shape described above; adapters
366
+ receive frozen projections. `encode` returns the exact outbound string, which
367
+ core measures and hashes before dispatch. `execute` must use that string and
368
+ make at most one bounded provider request. Credentials remain in the adapter's
369
+ closure, not encoded requests, provenance, capabilities, or diagnostics.
370
+
371
+ Internal boolean answers are `{ type:'boolean', value?, probability? }`;
372
+ choice answers are `{ type:'choice', choice, probabilities?, confidence? }`;
373
+ score answers are `{ type:'score', score, probabilities?, confidence? }`.
374
+ Usage is optional `{ inputTokens, outputTokens }`. The service validates IDs,
375
+ types, option coverage, finite metrics, distributions, winners, score
376
+ consistency, and usage; Jev also retains its stricter wire validation.
377
+
378
+ Adapters may throw `DecisionFault(code, status, { retryAfterMs })`; overload
379
+ delays are clamped by core. Unknown fault strings are sanitized. Optional
380
+ `reportTransport({httpStatus})` supports legacy diagnostics; non-HTTP providers
381
+ need not call it. Providers are trusted daemon code, not sandboxed extensions.
382
+
383
+ ## Packaging and verification
384
+
385
+ The parent owns pipeline/server integration and package allowlists. Include all
386
+ `runtime/decisions/*.mjs` files and `runtime/jev/provider.mjs` in the root package
387
+ file list and generated host packages. No version bump or release is made here.
388
+
389
+ Run `npm test -- tests/decisions/*.test.mjs tests/jev/*.test.mjs` plus the
390
+ pipeline/runtime integration tests. The unchanged Jev wire fixtures pin exact
391
+ requests and rubrics. Deterministic provider tests exercise the same caller,
392
+ pipeline, profiles, filtering, branded materialization, deadlines, stale
393
+ rejection, capabilities, and cache behavior without credentials or live calls.