janela 0.12.0 → 0.14.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -1
- data/README.md +31 -26
- data/UPGRADING.md +60 -1
- data/app/assets/javascripts/janela/chart_controller.js +54 -3
- data/app/assets/stylesheets/vitral.css +21 -0
- data/app/controllers/janela/application_controller.rb +2 -2
- data/app/controllers/janela/panes_controller.rb +1 -1
- data/app/controllers/janela/queries_controller.rb +1 -0
- data/app/controllers/janela/snapshot_queries_controller.rb +1 -0
- data/app/helpers/janela/frames_helper.rb +23 -5
- data/app/models/janela/pane.rb +1 -1
- data/app/models/janela/query.rb +30 -2
- data/app/views/janela/frames/_pane.html.erb +3 -2
- data/app/views/janela/panes/_form.html.erb +6 -0
- data/app/views/janela/queries/_query.html.erb +17 -15
- data/config/locales/en.yml +6 -0
- data/db/migrate/20261006000001_add_value_labels_to_janela_panes.rb +7 -0
- data/docs/agents.md +97 -0
- data/docs/composing.md +14 -14
- data/docs/decisions/020-formatting-belongs-to-the-measure.md +4 -1
- data/docs/decisions/053-an-agent-reaches-janela-through-tools-the-host-scopes.md +187 -0
- data/docs/decisions/054-a-chart-reads-numbers-the-way-its-measure-formats-them.md +221 -0
- data/docs/decisions/INDEX.md +11 -9
- data/docs/multi-tenancy.md +22 -4
- data/docs/roadmap.md +37 -13
- data/docs/theming.md +6 -4
- data/lib/janela/doctor.rb +66 -8
- data/lib/janela/tools.rb +221 -0
- data/lib/janela/version.rb +1 -1
- data/lib/janela.rb +16 -0
- metadata +16 -8
|
@@ -66,6 +66,12 @@
|
|
|
66
66
|
<%= form.select :prominence, Janela::Pane::PROMINENCES.map { |step| [ t("janela.prominences.#{step}"), step ] }, include_blank: t("janela.prominences.automatic") %>
|
|
67
67
|
</div>
|
|
68
68
|
|
|
69
|
+
<div class="janela-field">
|
|
70
|
+
<%= form.check_box :value_labels %>
|
|
71
|
+
<%= form.label :value_labels %>
|
|
72
|
+
<span class="janela-hint"><%= t("janela.value_labels.hint") %></span>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
69
75
|
<div class="janela-field">
|
|
70
76
|
<%= form.label :span %>
|
|
71
77
|
<%= form.select :span, Janela::Pane::SPANS.to_a %>
|
|
@@ -15,21 +15,23 @@
|
|
|
15
15
|
<figcaption class="janela-chart-title" id="<%= title_id %>"><%= query.title %></figcaption>
|
|
16
16
|
<%# A height puts the canvas in a box of fixed size for the chart to fill;
|
|
17
17
|
with none there is no box and the chart is what it always was (ADR 047). %>
|
|
18
|
-
<% canvas =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
18
|
+
<% canvas = tag.canvas(
|
|
19
|
+
class: "janela-chart", role: "img",
|
|
20
|
+
aria: { labelledby: title_id, description: (t("janela.time.click_hint") if query.time? && query.clickable?) },
|
|
21
|
+
data: {
|
|
22
|
+
controller: "janela--chart",
|
|
23
|
+
action: "janela--chart:toggle->janela--frame#toggle",
|
|
24
|
+
"janela--chart-type-value": query.renderer,
|
|
25
|
+
"janela--chart-title-value": query.title,
|
|
26
|
+
"janela--chart-selected-value": query.selected_values.to_json,
|
|
27
|
+
"janela--chart-labels-value": result.keys.to_json,
|
|
28
|
+
"janela--chart-values-value": result.values.map(&:to_f).to_json,
|
|
29
|
+
"janela--chart-formatted-value": result.values.map { |measured| query.format(measured) }.to_json,
|
|
30
|
+
"janela--chart-tick-format-value": query.tick_format.to_json,
|
|
31
|
+
"janela--chart-filters-value": query.filters_for(result.keys).to_json,
|
|
32
|
+
"janela--chart-fixed-height-value": (true if query.boxed?),
|
|
33
|
+
"janela--chart-value-labels-value": (true if query.labelled?)
|
|
34
|
+
}) %>
|
|
33
35
|
<%= query.boxed? ? tag.div(canvas, class: "janela-chart-box janela-h-#{query.height}") : canvas %>
|
|
34
36
|
</figure>
|
|
35
37
|
<% else %>
|
data/config/locales/en.yml
CHANGED
|
@@ -22,9 +22,13 @@ en:
|
|
|
22
22
|
span: Width
|
|
23
23
|
height: Height
|
|
24
24
|
prominence: Prominence
|
|
25
|
+
value_labels: Show each bar's value
|
|
25
26
|
companions: Beside the label
|
|
26
27
|
title: Title
|
|
27
28
|
janela:
|
|
29
|
+
errors:
|
|
30
|
+
not_found: There is no such pane.
|
|
31
|
+
bad_request: That request is not allowed on this pane.
|
|
28
32
|
actions:
|
|
29
33
|
add_pane: "Add a %{name}"
|
|
30
34
|
cancel: Cancel
|
|
@@ -71,6 +75,8 @@ en:
|
|
|
71
75
|
measures: Measures
|
|
72
76
|
dimensions: Dimensions
|
|
73
77
|
hint: Up to three, drawn as columns in a table. A dimension shows a value only where every row of its group shares one.
|
|
78
|
+
value_labels:
|
|
79
|
+
hint: Draws the value at the end of each bar of a bar chart, if every one fits. Other charts ignore it.
|
|
74
80
|
prominences:
|
|
75
81
|
automatic: Automatic
|
|
76
82
|
"1": Footnote
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddValueLabelsToJanelaPanes < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
# Whether a bar chart draws each bar's value on the bar (ADR 054). Null is
|
|
4
|
+
# what every existing pane is: unset, and drawn exactly as before.
|
|
5
|
+
add_column :janela_panes, :value_labels, :boolean
|
|
6
|
+
end
|
|
7
|
+
end
|
data/docs/agents.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
Topics: agents, mcp, tools, authorisation, host-integration
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Giving an Agent Access to Janela
|
|
6
|
+
|
|
7
|
+
Frames and panes are rows (ADR 012), so an agent could always arrange a
|
|
8
|
+
dashboard by writing them from a console. This page is for giving it tools
|
|
9
|
+
instead: read a frame, read a pane's values, and, if you choose, add, change,
|
|
10
|
+
remove and move panes. The reasoning is in ADR 053.
|
|
11
|
+
|
|
12
|
+
The short version: Janela gives you the tool definitions as plain Ruby. You
|
|
13
|
+
build them with the answer to "what may this caller read", and you register
|
|
14
|
+
them with whatever MCP library your application already runs. Janela
|
|
15
|
+
registers nothing, detects nothing and serves nothing, because it cannot
|
|
16
|
+
know who is asking and your application can.
|
|
17
|
+
|
|
18
|
+
## Building the tools
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) })
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`scope:` is required. It takes a model class and returns a relation: the same
|
|
25
|
+
answer your `policy_scope` gives a Janela controller (ADR 032). Leave it out
|
|
26
|
+
and `Janela::Unscoped` is raised when the tools are built, because a tool with
|
|
27
|
+
no scope would read every row of every model. Every read the tools make passes
|
|
28
|
+
that relation, and frames and panes are found through it too, so a caller can
|
|
29
|
+
change only what it can see.
|
|
30
|
+
|
|
31
|
+
Build the tools where you know who is asking, which for an MCP server is when a
|
|
32
|
+
tool is called. They are cheap to construct.
|
|
33
|
+
|
|
34
|
+
## What they do
|
|
35
|
+
|
|
36
|
+
| Tool | Does | Needs `write: true` |
|
|
37
|
+
| --- | --- | --- |
|
|
38
|
+
| `describe_vocabulary` | The models, measures, dimensions, renderers and granularities the `janela` blocks declare. An agent should offer nothing else. | no |
|
|
39
|
+
| `list_frames` | The frames the scope returns: id, name, owner, pane count. | no |
|
|
40
|
+
| `get_frame` | One frame with its panes in position order. | no |
|
|
41
|
+
| `read_pane` | A pane's values, through the scope. Filters are Ransack predicates on declared dimensions, bounded as a reader's are (ADR 025). | no |
|
|
42
|
+
| `add_pane` | Add a pane to the end of a frame. | yes |
|
|
43
|
+
| `update_pane` | Change a pane. An invalid change leaves it as it was. | yes |
|
|
44
|
+
| `remove_pane` | Remove a pane and close the gap. | yes |
|
|
45
|
+
| `move_pane` | Move a pane one place up or down. | yes |
|
|
46
|
+
|
|
47
|
+
The write tools are off unless you build the tools with `write: true`. Their
|
|
48
|
+
inputs are the pane's own attributes, the ones the README
|
|
49
|
+
names, and nothing else: an attribute that is not a pane's is refused rather
|
|
50
|
+
than dropped. A pane Janela would refuse is refused with its own reason, for
|
|
51
|
+
example `Measure "nonsense" is not a measure of Order`.
|
|
52
|
+
|
|
53
|
+
There is no tool to create a frame or take a snapshot. Your code finds or makes
|
|
54
|
+
a frame with `Janela::Frame.for` (ADR 041), and a snapshot needs an owner and a
|
|
55
|
+
scope you have named (ADR 033, ADR 034).
|
|
56
|
+
|
|
57
|
+
## Registering them
|
|
58
|
+
|
|
59
|
+
`Janela::Tools.all` is the list of definitions, and needs no scope: each has a
|
|
60
|
+
`name`, a `description`, an `input_schema` (JSON Schema) and `read_only`.
|
|
61
|
+
`tools.call(name, arguments)` runs one and returns a Hash, or raises a
|
|
62
|
+
`Janela::Error` (`NotFound`, `BadRequest` or `Unscoped`) that an adapter reports
|
|
63
|
+
to the agent as the tool's own error.
|
|
64
|
+
|
|
65
|
+
For the official Ruby SDK, the `mcp` gem, an adapter looks like this. It is run
|
|
66
|
+
by Janela's own tests, so it cannot drift from the code. `server_context` is
|
|
67
|
+
whatever your server was built with, and is where you keep the caller; the
|
|
68
|
+
scope is built from it at the moment of the call.
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
def janela_server(write: false)
|
|
72
|
+
tools = Janela::Tools.all(write: write).map do |tool|
|
|
73
|
+
MCP::Tool.define(name: tool.name, description: tool.description, input_schema: tool.input_schema,
|
|
74
|
+
annotations: { read_only_hint: tool.read_only }) do |server_context:, **arguments|
|
|
75
|
+
# The scope is built per call, because who is asking is known only now.
|
|
76
|
+
scope = ->(model) { server_context[:scope].call(model) }
|
|
77
|
+
result = Janela::Tools.new(scope: scope, write: write).call(tool.name, arguments)
|
|
78
|
+
MCP::Tool::Response.new([ { type: "text", text: result.to_json } ])
|
|
79
|
+
rescue Janela::Error => error
|
|
80
|
+
MCP::Tool::Response.new([ { type: "text", text: error.message } ], error: true)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
MCP::Server.new(name: "janela", tools: tools, server_context: { scope: yield })
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`mcp` is not a dependency of Janela, and a host using another library writes the
|
|
89
|
+
same few lines against it.
|
|
90
|
+
|
|
91
|
+
## What Janela does not do
|
|
92
|
+
|
|
93
|
+
It does not serve MCP, detect a server, edit your configuration or install
|
|
94
|
+
anything. It holds no credentials and has no idea who your agent is. Whether an
|
|
95
|
+
agent may reach your MCP server at all is your server's authentication, which
|
|
96
|
+
Janela never sees. Everything it adds is the question it already asks: what may
|
|
97
|
+
this caller read?
|
data/docs/composing.md
CHANGED
|
@@ -23,12 +23,12 @@ with your own classes plus the hooks in [Theming Janela](theming).
|
|
|
23
23
|
| `janela_frame do ... end` | anywhere inside the block | the page needs a heading, text, links or a layout of its own |
|
|
24
24
|
| `janela_frame @frame` | none | the dashboard is data an analyst edits without a deploy |
|
|
25
25
|
|
|
26
|
-
A stored frame renders panes and
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
A stored frame renders panes, and a pane is either a query, a few words
|
|
27
|
+
the analyst writes (a heading, a sentence, a link), or a partial you
|
|
28
|
+
wrote and they place by name (ADR 039). What an analyst writes is
|
|
29
|
+
escaped, never markup, so a row cannot put arbitrary content on the page
|
|
30
|
+
(ADR 012). Anything richer than that, an icon, an image, a layout of your
|
|
31
|
+
own, goes in the page around the frame, or use the block form.
|
|
32
32
|
|
|
33
33
|
```erb
|
|
34
34
|
<h2>Orders</h2>
|
|
@@ -58,9 +58,9 @@ same classes a stored frame gets from its integers:
|
|
|
58
58
|
|
|
59
59
|
Every direct child is a grid item, your own elements included, so a
|
|
60
60
|
heading can take a whole row and a text box can sit between two panes.
|
|
61
|
-
`janela_pane` takes no class of its own
|
|
62
|
-
(
|
|
63
|
-
|
|
61
|
+
`janela_pane` takes no class of its own, and will not: arranging panes
|
|
62
|
+
beyond the shipped grid is the host's (see the roadmap). So wrap a pane
|
|
63
|
+
to span it:
|
|
64
64
|
|
|
65
65
|
```erb
|
|
66
66
|
<div class="janela-span-2"><%= janela_pane Order, :revenue, by: :status, as: :bar %></div>
|
|
@@ -195,11 +195,11 @@ and number, so the figure needs a `<div>` rather than a `<p>` around it,
|
|
|
195
195
|
and a line of CSS to put the numbers side by side. Without it they
|
|
196
196
|
stack.
|
|
197
197
|
|
|
198
|
-
A done out of total figure
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
A done out of total figure over a yes or no column is one pane: a
|
|
199
|
+
`ratio:` measure is the share of rows where a boolean is true (ADR 038).
|
|
200
|
+
When the condition is not a column, a measure has no condition of its own,
|
|
201
|
+
so there is no pane for the done half; compute it as the progress bar
|
|
202
|
+
below does.
|
|
203
203
|
|
|
204
204
|
### A progress bar
|
|
205
205
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Date: 2026-09-16
|
|
3
3
|
Status: Accepted
|
|
4
4
|
Related: ADR 002, ADR 009, ADR 012, ADR 018
|
|
5
|
+
Superseded in part by: ADR 054
|
|
5
6
|
Triggers:
|
|
6
7
|
- a number rendering with more precision than it means
|
|
7
8
|
- adding an option to a pane, a frame or a pane URL
|
|
@@ -74,7 +75,9 @@ back under a format declared today.
|
|
|
74
75
|
and a chart tooltip all show the string the measure produced, and the
|
|
75
76
|
chart is handed those strings rather than formatting a second time in
|
|
76
77
|
JavaScript. The chart still plots raw numbers, because an axis is a
|
|
77
|
-
scale and not a label.
|
|
78
|
+
scale and not a label. (Superseded in part by
|
|
79
|
+
ADR 054: the axis is formatted by the measure too. The chart still plots
|
|
80
|
+
raw numbers.)
|
|
78
81
|
|
|
79
82
|
## Consequences
|
|
80
83
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
Date: 2026-10-02
|
|
3
|
+
Status: Accepted
|
|
4
|
+
Related: ADR 001, ADR 010, ADR 012, ADR 014, ADR 017, ADR 019, ADR 025, ADR 032, ADR 033, ADR 034, ADR 037, ADR 041
|
|
5
|
+
Triggers:
|
|
6
|
+
- proposing an MCP server, an agent tool or any machine facing surface for Janela
|
|
7
|
+
- an agent needs to read or arrange a frame without a browser
|
|
8
|
+
- adding a dependency on an MCP library
|
|
9
|
+
- deciding whether Janela should detect, register or install anything in a host's own tooling
|
|
10
|
+
- running a Janela query outside a controller, from a tool, a job or a console
|
|
11
|
+
- changing what an agent may do to a pane
|
|
12
|
+
Topics: ai, agents, mcp, tools, authorisation, scope, host-integration, frames, dependencies, 1.0
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# ADR 053: An Agent Reaches Janela Through Tools the Host Scopes, and Janela Registers None
|
|
16
|
+
|
|
17
|
+
## Context
|
|
18
|
+
|
|
19
|
+
Janela dashboards are meant to be composed by agents, and an agent in a
|
|
20
|
+
host application has no way in. Frames and panes are rows (ADR 012), so
|
|
21
|
+
the only route is `Janela::Frame` and `Janela::Pane` written from a
|
|
22
|
+
console. A host that already runs an MCP server writes its own wrappers
|
|
23
|
+
by hand, and a host without one gets nothing. #73 asks for an MCP
|
|
24
|
+
surface, and ADR 010 named it as "one ADR away" and as one of the two
|
|
25
|
+
conditions for shipping Jan.
|
|
26
|
+
|
|
27
|
+
The issue proposed that Janela detect a host's MCP server and offer to
|
|
28
|
+
install tools into it. Four facts, measured on the demo before this was
|
|
29
|
+
written, change what that should mean.
|
|
30
|
+
|
|
31
|
+
**Reading a pane outside a controller reads everything.** The refusal in
|
|
32
|
+
ADR 032 lives in `Janela::ApplicationController`, where `policy_scope`
|
|
33
|
+
is asked. `Query#result` underneath takes an optional `on:` relation,
|
|
34
|
+
and without it reads the model's default scope. With two orders in the
|
|
35
|
+
demo, `pane.query.result` returned both statuses, and
|
|
36
|
+
`pane.query.result(on: Order.where(status: "paid"))` returned one. A tool
|
|
37
|
+
that called the first form would show an agent, and so a person, rows
|
|
38
|
+
the host never scoped, with no error. This is the same shape as ADR 034:
|
|
39
|
+
a thing that runs outside a request and has to be told what may be read.
|
|
40
|
+
|
|
41
|
+
**The host's scope is an answer given at the point of asking.** ADR 019
|
|
42
|
+
found that a decision needing the current user and tenant belongs inside
|
|
43
|
+
a request rather than in a setting, and ADR 032 rejected a global
|
|
44
|
+
`Janela.scope = ->(model, controller)` for it. An MCP call is not a
|
|
45
|
+
request to Janela's controllers. Who is asking arrives through whatever
|
|
46
|
+
authentication the host's own MCP server has, which Janela does not own
|
|
47
|
+
and cannot see.
|
|
48
|
+
|
|
49
|
+
**A pane row already refuses what it should.** A pane naming a measure or
|
|
50
|
+
model that is not declared is invalid, and says so in words an agent can
|
|
51
|
+
act on: `Measure "nonsense" is not a measure of Order` and `Model
|
|
52
|
+
"no_such_model" is not a janela model`. The tools do not need a
|
|
53
|
+
validation layer of their own.
|
|
54
|
+
|
|
55
|
+
**Tools can be built from data.** The official Ruby SDK, `mcp` 1.6.1,
|
|
56
|
+
builds a tool from a name, a description, a JSON schema and a block
|
|
57
|
+
(`MCP::Tool.define`), and hands the caller's context to the block as
|
|
58
|
+
`server_context`. Other libraries take the same four things. A tool
|
|
59
|
+
definition is therefore plain data plus a call, and none of it needs to
|
|
60
|
+
live in an MCP library to be written once.
|
|
61
|
+
|
|
62
|
+
One more fact corrects the issue. #73 describes the install task as "in
|
|
63
|
+
the same spirit as `rails janela:install:skill`". That task does not
|
|
64
|
+
exist. ADR 010 records the skill, the install task and the agent
|
|
65
|
+
definition as not implemented, and `docs/skills/` is absent. This would
|
|
66
|
+
be the first install task Janela has, not the second.
|
|
67
|
+
|
|
68
|
+
### What was considered
|
|
69
|
+
|
|
70
|
+
**Janela serves MCP itself**, as an engine endpoint. Rejected. A served
|
|
71
|
+
endpoint needs authentication, a notion of who is calling and a way to
|
|
72
|
+
answer ADR 032's question for them, and Janela has none of the three: its
|
|
73
|
+
controllers are exactly as authenticated as the host's (ADR 010). Adding
|
|
74
|
+
them is the subsystem ADR 001 refused, and getting scope wrong shows one
|
|
75
|
+
tenant's numbers to another.
|
|
76
|
+
|
|
77
|
+
**Detect the host's MCP server and install into it**, as #73 proposed.
|
|
78
|
+
Rejected for the reason ADR 034 rejected detecting the kind of host: the
|
|
79
|
+
signal does not distinguish anything. A host's MCP setup is a gem, an
|
|
80
|
+
endpoint or a hand-written server, and the one thing Janela can reliably
|
|
81
|
+
know is none of those. It would also be Janela editing a host's tooling,
|
|
82
|
+
which ADR 010 says it never does.
|
|
83
|
+
|
|
84
|
+
**A `janela:install:mcp` task that writes tool files into the host.**
|
|
85
|
+
Rejected. Once copied the files are the host's, they drift from the
|
|
86
|
+
gem's public API, and the doctor would have to grow a check for each
|
|
87
|
+
release to say so. A host that wants the shape reads it in the guide and
|
|
88
|
+
owns the ten lines.
|
|
89
|
+
|
|
90
|
+
**A global scope setting**, so the tools need no argument. Rejected on
|
|
91
|
+
ADR 019 and ADR 032, above.
|
|
92
|
+
|
|
93
|
+
**`mcp` as a dependency of the gem.** Rejected. It is the right library
|
|
94
|
+
today and is not the only one a host may use, so a hard dependency would
|
|
95
|
+
make Janela's choice the host's. It stays out of the gemspec, the way the
|
|
96
|
+
demo's markdown renderer does, and the adapter below is optional.
|
|
97
|
+
|
|
98
|
+
**Writing and reading in one step.** Rejected as the shipped shape, not
|
|
99
|
+
as an idea. The write tools change records a person arranged, so they are
|
|
100
|
+
off until asked for.
|
|
101
|
+
|
|
102
|
+
## Decision
|
|
103
|
+
|
|
104
|
+
**Janela ships its agent tools as plain Ruby that the host builds with a
|
|
105
|
+
scope and registers wherever it likes. Janela registers nothing,
|
|
106
|
+
detects nothing and serves nothing.**
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
tools = Janela::Tools.new(scope: ->(model) { policy_scope(model) }, write: true)
|
|
110
|
+
|
|
111
|
+
tools.all # name, description, input_schema, read_only
|
|
112
|
+
tools.call("add_pane", frame_id: 3, model: "orders", measure: "revenue")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**`scope:` is required and has no default.** It is a callable that takes
|
|
116
|
+
a model class and returns a relation: the same answer `policy_scope`
|
|
117
|
+
gives. Leaving it out raises `Janela::Unscoped` when the tools are built,
|
|
118
|
+
not when one is called, which is ADR 032's refusal moved to the place
|
|
119
|
+
this surface begins. Every read the tools make passes the relation it
|
|
120
|
+
returns as `on:`, and frames and panes are read through the same callable
|
|
121
|
+
(`scope.call(Janela::Frame)`). It is an argument and not a setting,
|
|
122
|
+
because it is built where the host knows who is asking, as ADR 034's
|
|
123
|
+
`scope_for` is.
|
|
124
|
+
|
|
125
|
+
**Read tools first, and writes only when asked for.** The 1.0 set:
|
|
126
|
+
|
|
127
|
+
| Tool | Does |
|
|
128
|
+
| --- | --- |
|
|
129
|
+
| `describe_vocabulary` | The models, measures, dimensions, renderers and granularities `Janela.definitions` declares, so an agent offers only what can be asked |
|
|
130
|
+
| `list_frames` | Id, name, owner and pane count of the frames the scope returns |
|
|
131
|
+
| `get_frame` | One frame with its panes in position order |
|
|
132
|
+
| `read_pane` | A pane's values, through the host's scope, with `q[...]` filters bounded as ADR 025 bounds them |
|
|
133
|
+
| `add_pane`, `update_pane`, `remove_pane`, `move_pane` | Only with `write: true` |
|
|
134
|
+
|
|
135
|
+
Inputs are the pane's own attributes and the README's own names. There is
|
|
136
|
+
no agent only vocabulary (ADR 010). A rejected row returns the pane's
|
|
137
|
+
validation messages unchanged. A frame is found or made by the host with
|
|
138
|
+
`Frame.for` (ADR 041) or by an analyst on the engine's pages, so there is
|
|
139
|
+
no `create_frame` in 1.0.
|
|
140
|
+
|
|
141
|
+
**A tool definition is data, and an adapter is a few lines the host
|
|
142
|
+
owns.** `Janela::Tools#all` carries what any library needs. The guide
|
|
143
|
+
shows the mapping for the official `mcp` SDK, and a host using another
|
|
144
|
+
writes the same few lines. Janela adds no MCP library to its gemspec.
|
|
145
|
+
|
|
146
|
+
**Snapshots are not in 1.0.** A snapshot persists and publishes, so it
|
|
147
|
+
needs an owner (ADR 033) and a scope the caller has named (ADR 034). That
|
|
148
|
+
is two more decisions an agent should not be making by default, and it
|
|
149
|
+
waits for a host that wants it.
|
|
150
|
+
|
|
151
|
+
**Jan still does not ship.** ADR 010's second condition, that an MCP
|
|
152
|
+
surface exists, is met by this. Its first reason, that a skill loaded
|
|
153
|
+
agent already does the work, has not been tested, because the skill was
|
|
154
|
+
never built. The skill ships first and teaches these tools. An agent
|
|
155
|
+
definition is revisited after a host has used both, and ADR 010 is not
|
|
156
|
+
superseded here.
|
|
157
|
+
|
|
158
|
+
## Consequences
|
|
159
|
+
|
|
160
|
+
- **It ships as 0.13.0, with its own short soak.** This adds public
|
|
161
|
+
surface, and ADR 037 says anything that changes the surface resets the
|
|
162
|
+
soak clock in #71. The earliest 1.0 tag moves by the length of that
|
|
163
|
+
soak. The rule for what 1.0 then contains is set now, before anyone has
|
|
164
|
+
seen the result: if a real host has exercised the write tools and found
|
|
165
|
+
nothing wrong, 1.0 ships with them. If no host has used them by the end
|
|
166
|
+
of the soak, 1.0 ships read only and the write tools wait for 1.1.
|
|
167
|
+
Freezing four schemas that nobody has tried is what a soak is for
|
|
168
|
+
avoiding.
|
|
169
|
+
- **The tool names and input schemas join the contract at 1.0**, so the
|
|
170
|
+
set is small on purpose and every name is a verb and a README noun. A
|
|
171
|
+
tool added after 1.0 is a minor release. A tool changed is a major one.
|
|
172
|
+
- **A host does the registering.** That is a few lines in the place its
|
|
173
|
+
MCP server already lives, and the guide says where. A host with no MCP
|
|
174
|
+
server gets nothing from this, which is the honest answer: Janela does
|
|
175
|
+
not run servers.
|
|
176
|
+
- **The first test is the measured failure.** A tool built with a scope
|
|
177
|
+
that returns `Order.none` reads no values, and tools built without a
|
|
178
|
+
scope raise. Both belong in the suite before the code.
|
|
179
|
+
- **No doctor check.** Construction raises, so there is nothing for the
|
|
180
|
+
doctor to see that the raise does not say (ADR 035).
|
|
181
|
+
- **The pane `order` option (#75) reaches agents for free**, since the
|
|
182
|
+
write tools take a pane's attributes. It does not change this ADR.
|
|
183
|
+
- **What would change this decision:** two hosts writing the same
|
|
184
|
+
adapter by hand, which would earn a shipped one for that library; or a
|
|
185
|
+
host with no MCP server that needs one, which would be the day Janela
|
|
186
|
+
is asked to run something and has to answer ADR 032's question for
|
|
187
|
+
itself.
|