jazari 0.7.0 → 0.8.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 +27 -0
- data/README.md +31 -9
- data/exe/jazari +96 -0
- data/lib/generators/jazari/install/install_generator.rb +4 -3
- data/lib/generators/jazari/install/templates/create_jazari_tables.rb +25 -8
- data/lib/generators/jazari/upgrade/upgrade_generator.rb +1 -1
- data/lib/jazari/checklist.rb +2 -2
- data/lib/jazari/mcp/actions.rb +58 -13
- data/lib/jazari/mcp/stdio_server.rb +191 -0
- data/lib/jazari/operations.rb +2 -0
- data/lib/jazari/protocol/dispatcher.rb +271 -0
- data/lib/jazari/runs.rb +2 -0
- data/lib/jazari/standalone/database.rb +54 -0
- data/lib/jazari/standalone.rb +36 -0
- data/lib/jazari/version.rb +1 -1
- data/protocol/v1/capability.schema.json +22 -0
- data/protocol/v1/error.schema.json +18 -0
- data/protocol/v1/fixtures/actor-attribution/scenario.json +8 -0
- data/protocol/v1/fixtures/daily-idempotency/scenario.json +8 -0
- data/protocol/v1/fixtures/evidence/scenario.json +8 -0
- data/protocol/v1/fixtures/operator-edit-wins/scenario.json +10 -0
- data/protocol/v1/fixtures/resolve-default/scenario.json +7 -0
- data/protocol/v1/fixtures/revision-conflict/scenario.json +7 -0
- data/protocol/v1/fixtures/run-closed/scenario.json +9 -0
- data/protocol/v1/fixtures/run-snapshot/scenario.json +9 -0
- data/protocol/v1/request.schema.json +215 -0
- data/protocol/v1/response.schema.json +124 -0
- metadata +37 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1553aebe3407e26d9ef3813203a9836b0ed5d0dec3169ec2bd4d52153e144d4
|
|
4
|
+
data.tar.gz: f5d8638f94e8bcfa9e1f484962ab7e45c0aff74d3bac6e0627cdf528ec15d4ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 374ee2a88370020196c89063900b11f1c8e0bf1e945421094087ad3a4c58efa20d68240e262b4120a00685334287ce9c55dc49b0a346a901d7d59ec3f8dc122e
|
|
7
|
+
data.tar.gz: a44421016fbaf7b9ba237bb5a3bc6936ae5d9f8aa5ae08510f6eff9afe65666341d515a149891098aa2872bc153455a211dfffafbd04795e52e47583dfa6b83b
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,33 @@ codes, the resolved-value shape, how revisions are computed, and the schema the
|
|
|
8
8
|
generator emits — changes to any of those are breaking even when the method
|
|
9
9
|
signatures do not move.
|
|
10
10
|
|
|
11
|
+
## [0.8.0] - 2026-09-05
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- An on-demand `jazari` executable backed by SQLite: JSON calls, recipe seeding,
|
|
16
|
+
capability discovery, and MCP stdio with no daemon or network listener.
|
|
17
|
+
- The versioned `jazari/v1` request, response, error, and capability JSON
|
|
18
|
+
Schemas, plus backend-neutral conformance fixtures for revisions, daily
|
|
19
|
+
idempotency, snapshots, attribution, evidence, operator edits, and closed
|
|
20
|
+
runs.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The shipped migration now preserves its constraints on PostgreSQL while
|
|
25
|
+
emitting equivalent SQLite types and UTC-date enforcement.
|
|
26
|
+
- Run and runbook optimistic-lock races map to `revision_conflict` on both
|
|
27
|
+
adapters.
|
|
28
|
+
- JSON and MCP schemas now close each action independently, so fields accepted
|
|
29
|
+
by a published schema are accepted by the matching runtime action.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- Standalone schema creation is atomic under concurrent first use, and local
|
|
34
|
+
anchor targets cannot register or overwrite a Rails host's anchor scopes.
|
|
35
|
+
- The dispatcher and MCP stdio server own their load-time dependencies and keep
|
|
36
|
+
internal failures inside the closed `jazari/v1` error envelope.
|
|
37
|
+
|
|
11
38
|
## [0.7.0] - 2026-08-30
|
|
12
39
|
|
|
13
40
|
### Changed
|
data/README.md
CHANGED
|
@@ -6,7 +6,8 @@ Recipes as data, per-subject runbooks, stable names for rituals that outlive any
|
|
|
6
6
|
record, and per-run evidence — so *"did last night's run actually complete?"*
|
|
7
7
|
is a query rather than a guess.
|
|
8
8
|
|
|
9
|
-
Requires Ruby 3.2
|
|
9
|
+
Requires Ruby 3.2+. Embed it in Rails 7.1+ with PostgreSQL, or run the
|
|
10
|
+
on-demand standalone CLI with SQLite.
|
|
10
11
|
|
|
11
12
|
---
|
|
12
13
|
|
|
@@ -18,6 +19,12 @@ is the operating manual. Its source lives in [`guide/`](guide/), including
|
|
|
18
19
|
different than you expect), [adoption](guide/02-adoption.md),
|
|
19
20
|
[anchors](guide/03-anchors.md), [runs and evidence](guide/04-runs.md),
|
|
20
21
|
[MCP](guide/05-mcp.md), and [migrating an existing checklist](guide/06-migrating.md).
|
|
22
|
+
[Standalone SQLite, JSON, and MCP stdio](guide/07-standalone.md) covers the
|
|
23
|
+
on-demand executable.
|
|
24
|
+
|
|
25
|
+
The public [`jazari/v1` JSON contract](protocol/v1/) and its conformance fixtures
|
|
26
|
+
are shipped with the gem. Development plans and agent coordination stay out of
|
|
27
|
+
this public repository.
|
|
21
28
|
|
|
22
29
|
## The problem
|
|
23
30
|
|
|
@@ -65,6 +72,20 @@ The generator copies one migration. **Jazari never auto-appends migrations** to
|
|
|
65
72
|
your schema — a shared operations table appearing in someone's next
|
|
66
73
|
`db:migrate` without them asking is how a gem loses trust in a production fleet.
|
|
67
74
|
|
|
75
|
+
For a local standalone database:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
jazari recipes seed config/recipes --database .jazari/jazari.sqlite3
|
|
79
|
+
jazari capabilities --database .jazari/jazari.sqlite3 --json
|
|
80
|
+
jazari call --database .jazari/jazari.sqlite3 --request request.json
|
|
81
|
+
jazari mcp stdio --database .jazari/jazari.sqlite3 --recipes config/recipes
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The process starts for the command or MCP stdio session and exits with its
|
|
85
|
+
caller. It opens no port, installs no daemon, and stores no credentials. Queue
|
|
86
|
+
and anchor targets are supported; an embedding host still owns authorization
|
|
87
|
+
and record-backed targets.
|
|
88
|
+
|
|
68
89
|
## Thirty seconds
|
|
69
90
|
|
|
70
91
|
```ruby
|
|
@@ -211,7 +232,7 @@ Jazari::Mcp::Handler.new.call(action: "get", target: target)
|
|
|
211
232
|
with its own subject vocabulary and permissions; this handler is the shared
|
|
212
233
|
implementation underneath. Domain failures cross the wire as codes from a closed
|
|
213
234
|
set — `target_not_found`, `invalid_runbook`, `revision_conflict`,
|
|
214
|
-
`item_not_found`, `read_only_target`, `run_closed` — never as messages that
|
|
235
|
+
`item_not_found`, `item_not_in_snapshot`, `read_only_target`, `run_closed` — never as messages that
|
|
215
236
|
could disclose a record or whether a target exists.
|
|
216
237
|
|
|
217
238
|
`Handler.actions_for("read")` returns the read-only subset, so a read-scoped
|
|
@@ -251,15 +272,16 @@ That removes the subject's runbook. **Runs are deliberately preserved** — a ru
|
|
|
251
272
|
records something that actually happened, and deleting the subject does not
|
|
252
273
|
un-happen it.
|
|
253
274
|
|
|
254
|
-
## PostgreSQL
|
|
275
|
+
## PostgreSQL and SQLite
|
|
255
276
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
277
|
+
Rails hosts keep PostgreSQL's `jsonb`, `timestamptz`, CHECK constraints, and
|
|
278
|
+
partial expression index. Standalone mode uses SQLite JSON columns, UTC date
|
|
279
|
+
checks, foreign keys, WAL, a busy timeout, and the same partial expression
|
|
280
|
+
index. Adapter syntax differs; observable revision, idempotency, snapshot,
|
|
281
|
+
attribution, and evidence behavior does not.
|
|
260
282
|
|
|
261
|
-
The test suite runs **the migration the gem ships
|
|
262
|
-
|
|
283
|
+
The test suite runs **the migration the gem ships** against both adapters and
|
|
284
|
+
replays the same JSON fixtures against PostgreSQL and SQLite.
|
|
263
285
|
|
|
264
286
|
## What this is not
|
|
265
287
|
|
data/exe/jazari
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "jazari/standalone"
|
|
7
|
+
|
|
8
|
+
module Jazari
|
|
9
|
+
module Standalone
|
|
10
|
+
class CLI
|
|
11
|
+
def initialize(argv, input: $stdin, output: $stdout, error: $stderr)
|
|
12
|
+
@argv = argv.dup
|
|
13
|
+
@input = input
|
|
14
|
+
@output = output
|
|
15
|
+
@error = error
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
command = @argv.shift
|
|
20
|
+
case command
|
|
21
|
+
when "capabilities" then capabilities
|
|
22
|
+
when "call" then call
|
|
23
|
+
when "recipes" then recipes
|
|
24
|
+
when "mcp" then mcp
|
|
25
|
+
else usage!("unknown command #{command.inspect}")
|
|
26
|
+
end
|
|
27
|
+
rescue OptionParser::ParseError, ArgumentError, Errno::ENOENT, JSON::ParserError => exception
|
|
28
|
+
@error.puts("jazari: #{exception.message}")
|
|
29
|
+
2
|
|
30
|
+
rescue Jazari::Error => exception
|
|
31
|
+
@error.puts("jazari: #{exception.code}: #{exception.message}")
|
|
32
|
+
1
|
|
33
|
+
rescue ActiveRecord::ActiveRecordError, RuntimeError => exception
|
|
34
|
+
@error.puts("jazari: #{exception.message}")
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def capabilities
|
|
41
|
+
options = parse_common!
|
|
42
|
+
@output.puts(JSON.pretty_generate(app(options).capabilities))
|
|
43
|
+
0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call
|
|
47
|
+
options = parse_common!({ request: "-" }) do |parser, values|
|
|
48
|
+
parser.on("--request PATH", "JSON request file, or - for stdin") { |value| values[:request] = value }
|
|
49
|
+
end
|
|
50
|
+
body = options[:request] == "-" ? @input.read : File.read(options[:request])
|
|
51
|
+
response = app(options).call(JSON.parse(body))
|
|
52
|
+
@output.puts(JSON.pretty_generate(response))
|
|
53
|
+
response["ok"] ? 0 : 1
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def recipes
|
|
57
|
+
usage!("expected `recipes seed PATH`") unless @argv.shift == "seed"
|
|
58
|
+
options = parse_common!(positionals: 1)
|
|
59
|
+
path = @argv.shift or usage!("recipe file or directory is required")
|
|
60
|
+
app(options)
|
|
61
|
+
records = RecipeRegistry.seed!(RecipeFiles.load(path))
|
|
62
|
+
@output.puts(JSON.pretty_generate(protocol: Protocol::Dispatcher::VERSION, seeded: records.length))
|
|
63
|
+
0
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def mcp
|
|
67
|
+
usage!("expected `mcp stdio`") unless @argv.shift == "stdio"
|
|
68
|
+
options = parse_common!
|
|
69
|
+
Mcp::StdioServer.new(dispatcher: app(options).dispatcher).serve(input: @input, output: @output)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_common!(defaults = {}, positionals: 0)
|
|
73
|
+
options = { database: ENV["JAZARI_DATABASE"], recipes: [] }.merge(defaults)
|
|
74
|
+
parser = OptionParser.new do |flags|
|
|
75
|
+
flags.on("--database PATH", "SQLite database path") { |value| options[:database] = value }
|
|
76
|
+
flags.on("--recipes PATH", "Seed recipes from a file or directory") { |value| options[:recipes] << value }
|
|
77
|
+
flags.on("--json", "Emit JSON (machine commands already do)") {}
|
|
78
|
+
yield flags, options if block_given?
|
|
79
|
+
end
|
|
80
|
+
parser.parse!(@argv)
|
|
81
|
+
raise ArgumentError, "--database PATH or JAZARI_DATABASE is required" if options[:database].to_s.empty?
|
|
82
|
+
extras = @argv.drop(positionals)
|
|
83
|
+
raise OptionParser::InvalidArgument, extras.join(" ") if extras.any?
|
|
84
|
+
options
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def app(options) = App.new(database: options.fetch(:database), recipes: options.fetch(:recipes))
|
|
88
|
+
|
|
89
|
+
def usage!(message)
|
|
90
|
+
raise ArgumentError, "#{message}\nusage: jazari capabilities|call|recipes seed|mcp stdio --database PATH"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
exit Jazari::Standalone::CLI.new(ARGV).run
|
|
@@ -14,7 +14,7 @@ module Jazari
|
|
|
14
14
|
|
|
15
15
|
source_root File.expand_path("templates", __dir__)
|
|
16
16
|
|
|
17
|
-
desc "Copies the jazari migration into
|
|
17
|
+
desc "Copies the jazari migration into a PostgreSQL Rails host."
|
|
18
18
|
|
|
19
19
|
def copy_migration
|
|
20
20
|
migration_template "create_jazari_tables.rb", "db/migrate/create_jazari_tables.rb"
|
|
@@ -27,8 +27,9 @@ module Jazari
|
|
|
27
27
|
say " 2. Seed your own recipes — the gem ships none by design."
|
|
28
28
|
say " 3. Jazari.configure { |c| c.anchor_scopes = { ... } } at boot."
|
|
29
29
|
say ""
|
|
30
|
-
say "
|
|
31
|
-
say "partial unique index over a COALESCEd polymorphic subject."
|
|
30
|
+
say "Rails hosts use PostgreSQL: jsonb, timestamptz, CHECK constraints, and a"
|
|
31
|
+
say "partial unique index over a COALESCEd polymorphic subject. The standalone"
|
|
32
|
+
say "executable manages its own SQLite schema instead."
|
|
32
33
|
say ""
|
|
33
34
|
end
|
|
34
35
|
end
|
|
@@ -3,12 +3,18 @@
|
|
|
3
3
|
# Host-adopted deliberately: jazari never auto-appends its migrations.
|
|
4
4
|
class CreateJazariTables < ActiveRecord::Migration[7.1]
|
|
5
5
|
def change
|
|
6
|
+
adapter = connection.adapter_name
|
|
7
|
+
unless %w[PostgreSQL SQLite].include?(adapter)
|
|
8
|
+
raise ActiveRecord::AdapterNotSpecified, "jazari supports PostgreSQL and SQLite, not #{adapter}"
|
|
9
|
+
end
|
|
10
|
+
postgres = adapter == "PostgreSQL"
|
|
11
|
+
|
|
6
12
|
create_table :jazari_recipes do |t|
|
|
7
13
|
t.string :recipe_id, null: false
|
|
8
14
|
t.integer :version, null: false, default: 1
|
|
9
15
|
t.string :topic, null: false
|
|
10
16
|
t.text :description, null: false, default: ""
|
|
11
|
-
t.jsonb
|
|
17
|
+
postgres ? t.jsonb(:checklist, null: false, default: []) : t.json(:checklist, null: false, default: [])
|
|
12
18
|
t.string :run_policy, null: false, default: "unrestricted"
|
|
13
19
|
t.timestamps
|
|
14
20
|
t.index :recipe_id, unique: true
|
|
@@ -30,7 +36,7 @@ class CreateJazariTables < ActiveRecord::Migration[7.1]
|
|
|
30
36
|
t.string :recipe_id, null: false
|
|
31
37
|
t.string :topic, null: false
|
|
32
38
|
t.text :description, null: false, default: ""
|
|
33
|
-
t.jsonb
|
|
39
|
+
postgres ? t.jsonb(:checklist, null: false, default: []) : t.json(:checklist, null: false, default: [])
|
|
34
40
|
# Provenance, host-defined and nullable. A runbook that exists because a
|
|
35
41
|
# backfill created it is a migration artifact; one that exists because an
|
|
36
42
|
# operator edited it is a decision. Both differ from the canon, so
|
|
@@ -49,21 +55,32 @@ class CreateJazariTables < ActiveRecord::Migration[7.1]
|
|
|
49
55
|
t.string :subject_type
|
|
50
56
|
t.bigint :subject_id
|
|
51
57
|
t.string :actor_ref, null: false
|
|
52
|
-
t.timestamptz :started_at, null: false
|
|
58
|
+
postgres ? t.timestamptz(:started_at, null: false) : t.datetime(:started_at, null: false)
|
|
53
59
|
t.date :started_on, null: false
|
|
54
60
|
t.string :idempotency_policy, null: false, default: "unrestricted"
|
|
55
|
-
t.timestamptz :finished_at
|
|
61
|
+
postgres ? t.timestamptz(:finished_at) : t.datetime(:finished_at)
|
|
56
62
|
t.string :outcome
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
if postgres
|
|
64
|
+
t.jsonb :checklist_snapshot, null: false
|
|
65
|
+
t.jsonb :ticks, null: false, default: []
|
|
66
|
+
t.jsonb :evidence, null: false, default: []
|
|
67
|
+
else
|
|
68
|
+
t.json :checklist_snapshot, null: false
|
|
69
|
+
t.json :ticks, null: false, default: []
|
|
70
|
+
t.json :evidence, null: false, default: []
|
|
71
|
+
end
|
|
60
72
|
t.integer :lock_version, null: false, default: 0
|
|
61
73
|
t.timestamps
|
|
62
74
|
t.index %i[recipe_id started_at]
|
|
63
75
|
t.index %i[subject_type subject_id started_at]
|
|
64
76
|
t.check_constraint "idempotency_policy IN ('unrestricted', 'once_per_calendar_day')",
|
|
65
77
|
name: "jazari_runs_idempotency_policy_chk"
|
|
66
|
-
|
|
78
|
+
utc_date_expression = if postgres
|
|
79
|
+
"started_on = (started_at AT TIME ZONE 'UTC')::date"
|
|
80
|
+
else
|
|
81
|
+
"started_on = date(started_at)"
|
|
82
|
+
end
|
|
83
|
+
t.check_constraint utc_date_expression,
|
|
67
84
|
name: "jazari_runs_started_on_utc_chk"
|
|
68
85
|
t.check_constraint "(subject_type IS NULL AND subject_id IS NULL) OR " \
|
|
69
86
|
"(subject_type IS NOT NULL AND subject_id IS NOT NULL)",
|
|
@@ -14,7 +14,7 @@ module Jazari
|
|
|
14
14
|
|
|
15
15
|
source_root File.expand_path("templates", __dir__)
|
|
16
16
|
|
|
17
|
-
desc "Copies
|
|
17
|
+
desc "Copies schema changes into an already-installed PostgreSQL Rails host."
|
|
18
18
|
|
|
19
19
|
def copy_migration
|
|
20
20
|
migration_template "add_jazari_runbook_origin.rb", "db/migrate/add_jazari_runbook_origin.rb"
|
data/lib/jazari/checklist.rb
CHANGED
|
@@ -12,8 +12,8 @@ module Jazari
|
|
|
12
12
|
#
|
|
13
13
|
# 50 was inherited from a host that already had it, never benchmarked. A
|
|
14
14
|
# 50-item procedure measured live serializes to 17,252 bytes at 278
|
|
15
|
-
# characters per step (
|
|
16
|
-
#
|
|
15
|
+
# characters per step (the suite's synthetic equivalent is 16,691, since
|
|
16
|
+
# real ids and text are not uniform), so the row cap was rejecting documents four times smaller than
|
|
17
17
|
# what actually hurts — and would have accepted 50 steps of 500 characters,
|
|
18
18
|
# which is nearly twice as large again.
|
|
19
19
|
#
|
data/lib/jazari/mcp/actions.rb
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
# Checklist even when it is required on its own — which the standalone-require
|
|
6
6
|
# test exists to catch, and did.
|
|
7
7
|
require "jazari/checklist"
|
|
8
|
+
require "jazari/operations"
|
|
9
|
+
require "jazari/runs"
|
|
8
10
|
|
|
9
11
|
module Jazari
|
|
10
12
|
module Mcp
|
|
@@ -25,14 +27,29 @@ module Jazari
|
|
|
25
27
|
# :additive — writes, but cannot destroy prior state
|
|
26
28
|
# :overwrite — may replace an operator's content; annotate cautiously
|
|
27
29
|
# :destructive — removes something; gate it
|
|
30
|
+
REQUIRED_ARGUMENTS = {
|
|
31
|
+
"get" => [], "last_run" => [],
|
|
32
|
+
"set" => %i[expected_revision topic checklist],
|
|
33
|
+
"add_item" => %i[expected_revision text],
|
|
34
|
+
"remove_item" => %i[expected_revision item_id],
|
|
35
|
+
"check_item" => %i[expected_revision item_id done],
|
|
36
|
+
"reset" => %i[expected_revision confirm],
|
|
37
|
+
"start" => %i[actor_ref],
|
|
38
|
+
"tick" => %i[run_id expected_revision item_id done],
|
|
39
|
+
"evidence" => %i[run_id expected_revision kind value],
|
|
40
|
+
"finish" => %i[run_id expected_revision outcome]
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
28
43
|
Action = Data.define(:name, :scope, :effect, :summary, :params, :confirm) do
|
|
29
44
|
def read? = scope == :read
|
|
30
45
|
def confirm? = confirm == true
|
|
46
|
+
def required = REQUIRED_ARGUMENTS.fetch(name)
|
|
31
47
|
def to_h = { name: name, scope: scope, effect: effect, summary: summary,
|
|
32
48
|
params: params, confirm: confirm }
|
|
33
49
|
end
|
|
34
50
|
|
|
35
51
|
REVISION = { type: "string",
|
|
52
|
+
minLength: 1,
|
|
36
53
|
description: "The revision from the read immediately before this call." }.freeze
|
|
37
54
|
# JSON Schema patterns are ECMAScript, which has no `\A`/`\z`. Publishing
|
|
38
55
|
# Ruby's source verbatim hands validators an anchor they may reject or,
|
|
@@ -40,7 +57,7 @@ module Jazari
|
|
|
40
57
|
# else derived from ID_FORMAT, which stays the single source of truth.
|
|
41
58
|
ID_PATTERN = Checklist::ID_FORMAT.source.sub('\\A', "^").sub('\\z', "$").freeze
|
|
42
59
|
|
|
43
|
-
ITEM_ID = { type: "string", description: "Opaque checklist item id." }.freeze
|
|
60
|
+
ITEM_ID = { type: "string", minLength: 1, description: "Opaque checklist item id." }.freeze
|
|
44
61
|
|
|
45
62
|
# An `array` with no `items` is not a contract, it is a guess. A client
|
|
46
63
|
# generating from that descriptor is free to infer `string[]`, send
|
|
@@ -75,8 +92,8 @@ module Jazari
|
|
|
75
92
|
description: "Full checklist; replaces the existing one. There is no row limit, " \
|
|
76
93
|
"but the serialized checklist must be at most " \
|
|
77
94
|
"#{Checklist::MAX_PAYLOAD} bytes." }.freeze
|
|
78
|
-
RUN_ID = { type: "integer", description: "The run returned by start." }.freeze
|
|
79
|
-
ACTOR = { type: "string", description: "Opaque identity of who is acting." }.freeze
|
|
95
|
+
RUN_ID = { type: "integer", minimum: 1, description: "The run returned by start." }.freeze
|
|
96
|
+
ACTOR = { type: "string", minLength: 1, description: "Opaque identity of who is acting." }.freeze
|
|
80
97
|
|
|
81
98
|
ALL = [
|
|
82
99
|
Action.new(name: "get", scope: :read, effect: :read, confirm: false,
|
|
@@ -88,13 +105,16 @@ module Jazari
|
|
|
88
105
|
Action.new(name: "set", scope: :write, effect: :overwrite, confirm: false,
|
|
89
106
|
summary: "Replace this subject's procedure. Materialises an override on first use.",
|
|
90
107
|
params: { expected_revision: REVISION,
|
|
91
|
-
topic: { type: "string",
|
|
92
|
-
|
|
108
|
+
topic: { type: "string", minLength: 1, maxLength: Operations::MAX_TOPIC,
|
|
109
|
+
description: "Short title." },
|
|
110
|
+
description: { type: "string", maxLength: Operations::MAX_DESCRIPTION,
|
|
111
|
+
description: "Markdown body." },
|
|
93
112
|
checklist: CHECKLIST }),
|
|
94
113
|
Action.new(name: "add_item", scope: :write, effect: :additive, confirm: false,
|
|
95
114
|
summary: "Append one checklist step.",
|
|
96
115
|
params: { expected_revision: REVISION,
|
|
97
|
-
text: { type: "string",
|
|
116
|
+
text: { type: "string", minLength: 1, maxLength: Checklist::MAX_TEXT,
|
|
117
|
+
description: "The step." },
|
|
98
118
|
required: { type: "boolean", description: "Whether the step is required. Default true." } }),
|
|
99
119
|
Action.new(name: "remove_item", scope: :write, effect: :destructive, confirm: false,
|
|
100
120
|
summary: "Delete one checklist step.",
|
|
@@ -102,30 +122,35 @@ module Jazari
|
|
|
102
122
|
Action.new(name: "check_item", scope: :write, effect: :additive, confirm: false,
|
|
103
123
|
summary: "Mark a step done or not done.",
|
|
104
124
|
params: { expected_revision: REVISION, item_id: ITEM_ID,
|
|
105
|
-
done: { type: "boolean", description: "
|
|
125
|
+
done: { type: "boolean", description: "Whether the step is done." } }),
|
|
106
126
|
Action.new(name: "reset", scope: :write, effect: :destructive, confirm: true,
|
|
107
127
|
summary: "Discard this subject's override and reveal the current canon.",
|
|
108
128
|
params: { expected_revision: REVISION,
|
|
109
|
-
confirm: { type: "boolean",
|
|
129
|
+
confirm: { type: "boolean", const: true,
|
|
130
|
+
description: "Must be true — this discards operator content." } }),
|
|
110
131
|
Action.new(name: "start", scope: :write, effect: :additive, confirm: false,
|
|
111
132
|
summary: "Open a run. Under a once-per-day recipe this returns the existing run instead of erroring.",
|
|
112
133
|
params: { actor_ref: ACTOR }),
|
|
113
134
|
Action.new(name: "tick", scope: :write, effect: :additive, confirm: false,
|
|
114
135
|
summary: "Record a step done within a run. Does not touch the subject's own checklist.",
|
|
115
136
|
params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
|
|
116
|
-
done: { type: "boolean", description: "
|
|
137
|
+
done: { type: "boolean", description: "Whether the step is done." },
|
|
117
138
|
actor_ref: ACTOR,
|
|
118
|
-
note: { type: "string",
|
|
139
|
+
note: { type: "string", maxLength: Checklist::MAX_TEXT,
|
|
140
|
+
description: "Optional free text." } }),
|
|
119
141
|
Action.new(name: "evidence", scope: :write, effect: :additive, confirm: false,
|
|
120
142
|
summary: "Attach evidence to a run: output, url, sha, count, or note.",
|
|
121
143
|
params: { run_id: RUN_ID, expected_revision: REVISION, item_id: ITEM_ID,
|
|
122
|
-
kind: { type: "string",
|
|
123
|
-
|
|
144
|
+
kind: { type: "string", enum: Runs::EVIDENCE_KINDS,
|
|
145
|
+
description: "The evidence kind." },
|
|
146
|
+
value: { type: "string", maxLength: Runs::MAX_EVIDENCE,
|
|
147
|
+
description: "The evidence itself." },
|
|
124
148
|
actor_ref: ACTOR }),
|
|
125
149
|
Action.new(name: "finish", scope: :write, effect: :additive, confirm: false,
|
|
126
150
|
summary: "Close a run with an outcome: completed, abandoned, or failed.",
|
|
127
151
|
params: { run_id: RUN_ID, expected_revision: REVISION,
|
|
128
|
-
outcome: { type: "string",
|
|
152
|
+
outcome: { type: "string", enum: %w[completed abandoned failed],
|
|
153
|
+
description: "How the run ended." } })
|
|
129
154
|
].freeze
|
|
130
155
|
|
|
131
156
|
NAMES = ALL.map(&:name).freeze
|
|
@@ -162,6 +187,26 @@ module Jazari
|
|
|
162
187
|
{ enum: actions.map(&:name), properties: properties }
|
|
163
188
|
end
|
|
164
189
|
|
|
190
|
+
# A complete schema for a flat action-enum tool. Each action is its own
|
|
191
|
+
# closed variant, so a field that belongs to `tick` is not accidentally
|
|
192
|
+
# advertised as legal for `get` merely because both share one tool.
|
|
193
|
+
def input_schema(target_schema:, scope: :write)
|
|
194
|
+
{
|
|
195
|
+
type: "object",
|
|
196
|
+
oneOf: all(scope: scope).map do |action|
|
|
197
|
+
{
|
|
198
|
+
type: "object",
|
|
199
|
+
required: [ :action, :target, *action.required ],
|
|
200
|
+
properties: {
|
|
201
|
+
action: { const: action.name },
|
|
202
|
+
target: target_schema
|
|
203
|
+
}.merge(action.params),
|
|
204
|
+
additionalProperties: false
|
|
205
|
+
}
|
|
206
|
+
end
|
|
207
|
+
}
|
|
208
|
+
end
|
|
209
|
+
|
|
165
210
|
# Human-readable action list for a tool description or a paired skill.
|
|
166
211
|
def summaries(scope: :write)
|
|
167
212
|
all(scope: scope).map { |a| "#{a.name} — #{a.summary}" }
|