studio-engine 0.68.0 → 0.69.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc639657a629af209c7afe28ae5114aff1d6121a0c6e5fc456c0d7fec51a40b3
4
- data.tar.gz: 4875b0355b16ea935f873f4dca59d1657a2e8b7e2b7b7ae17f79df80ea194e48
3
+ metadata.gz: ec5eb4134f973228cde9577b93d24e6f7e7380f613669f7ef95396ae8f20b048
4
+ data.tar.gz: d51dbff7d2d0b87bbc7f99da58ed31759c23dfbf3296fb12962ac939dbcd17ab
5
5
  SHA512:
6
- metadata.gz: 867c4b70c5c76e7619c6b4ed1d087a79ccb1cd3c9d99770f109c4c039d56372ca7835a3281d00bcc1fc076cb1759c700156959db4f29010e4fa5239b3a344944
7
- data.tar.gz: 31eec34f181c1caec640f1085d97b3437bd6e0416e90ff4f4d23af29039b88406093ed42adbd11c973ef4a7687eeea76e208f5b57df88d25e66f37a937933bae
6
+ metadata.gz: '05658af267cec61bc5c54e4e68283bbfbe4a92fa401fc2e6c79d117bb65dd1f66b88ed9d1c4e1962b9131f138ef9fcffdc2948ff8fee796bb216033c953cd5c8'
7
+ data.tar.gz: 59538dc6601a43794bc1b700d3dfdace130bd9ac72a3a9cd507d1626a16e2679c0c7c81ab6fc36800403be4c7cf1dfd26258725971c30546899a7a2fb729e03b
data/CHANGELOG.md CHANGED
@@ -4,8 +4,29 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ### Added
8
+
9
+ - **Knowledge coverage view** — `/admin/knowledge/coverage` +
10
+ `Studio::KnowledgeExpectation`: the "what SHOULD exist" half of the
11
+ knowledge layer. Expectations carry an entity, folder, provenance
12
+ (`source_note`, e.g. "diligence tracker item 14"), and a cadence — `once`
13
+ (an LOI) or `monthly` (aging inventory), where each calendar month from
14
+ `start_on` is a slot and the view names missing months outright
15
+ (`2026-09 MISSING`). Documents fulfill expectations by an EXPLICIT
16
+ `expectation_id` set at triage (the show page grows a "fulfills expectation"
17
+ select; a fuzzy name-match would silently merge lookalikes, an id never
18
+ does), and superseded documents never fill a slot. Ships two reference
19
+ migrations (`create_studio_knowledge_expectations`,
20
+ `add_expectation_to_studio_knowledge_docs`); routes ride the existing
21
+ `Studio.draw_knowledge_routes` opt-in. Built for the Commercial Welding
22
+ 65-item diligence tracker; app-agnostic like the rest of the layer.
23
+
7
24
  ### Changed
8
25
 
26
+ - **`Gemfile.lock` resolves solana-studio 0.5.7, and a gate now keeps it there.** The lock had sat on **0.5.3 for four patch releases** while BOTH consumers shipped 0.5.7 (turf-monster `~> 0.5.3`, mcritchie-studio `~> 0.5`). Nothing was red and nothing could have been: engine CI installs with `bundler-cache: true`, so it resolves from the lock and never fresh — the drift does not self-correct and never surfaces as flakiness. It matters because `test/views/style_web3_specimens_test.rb` exists to prove "the style guide renders the REAL gem cards" and reads them off whatever the LOCK resolved; four versions behind, that guard certifies a card no consumer receives. It still passes — only its MEANING changes. MEASURED on this span, the gem's whole `app/` tree was byte-identical 0.5.3 → 0.5.7 (only `CHANGELOG.md`, `README.md` and `version.rb` differ), so this instance cost nothing, which is exactly why it went four releases unnoticed.
27
+ - **The constraint stays `>= 0.5.3`, deliberately.** 0.5.3 is a real FLOOR (0.5.2 shipped the credential partial without `solana_studio/modals/_wallet_connect`, which the web3 capability gate requires). Above it this engine claims no ceiling: it is the BASE half of the base/bolt-on split, and a pessimistic `~> 0.5.3` here would be NARROWER than mcritchie-studio's own `~> 0.5` — a dev-only dependency constraining a resolution it does not own. The defect was the LOCK, not the pin, so the fix is a gate rather than a tighter pin.
28
+ - **`bin/gem-drift-check`** — fails when this engine's lock resolves a tracked gem OLDER than a consumer's, wired into `consumer-ci.yml` after the consumer bundle install. That lane is the only place two repos' lockfiles exist at once (engine at `studio/`, consumer beside it); `test/lib/consumer_ci_shard_contract_test.rb` records the same constraint for its own cross-repo contract. Direction is ONE-WAY on purpose: engine behind FAILS, engine level or ahead passes (the engine is the producer and may test an unreleased gem), and a consumer bundling no tracked gem is a SKIP, not a failure — `mcritchie_industries` is the base half working as designed. Stdlib-only, and it names its one-command remedy (`bundle update solana-studio`) in the failure.
29
+
9
30
  - **The auth modal's Solana button moved to solana-studio, behind a CREDENTIAL
10
31
  SLOT.** `style/modals/_auth` no longer draws the button; it renders whatever
11
32
  resolves at `solana_studio/auth/wallet_credential` and nothing when that path
data/Gemfile CHANGED
@@ -52,6 +52,34 @@ end
52
52
  # The style guide self-gates on the partial RESOLVING (style/_modals.html.erb),
53
53
  # so an app without this gem renders /admin/style with the web3 cards badged
54
54
  # rather than raising a missing-template error.
55
+ #
56
+ # THE CONSTRAINT STAYS OPEN-ENDED, AND THAT IS A DECISION, NOT AN OVERSIGHT.
57
+ # ">= 0.5.3" is a FLOOR: 0.5.2 shipped the credential partial without
58
+ # solana_studio/modals/_wallet_connect, which the web3 capability gate requires,
59
+ # so 0.5.3 is the first version that can draw the button at all. Above it this
60
+ # engine deliberately claims no ceiling. It is the BASE half of the split; the
61
+ # bolt-on is the consumer's choice, and an engine that pinned the bolt-on more
62
+ # tightly than the app bolting it on would be dictating upward. Concretely: a
63
+ # pessimistic "~> 0.5.3" here would be NARROWER than mcritchie-studio's own
64
+ # "~> 0.5", so this dev-only dependency would start constraining a resolution it
65
+ # does not own.
66
+ #
67
+ # WHAT ACTUALLY WENT WRONG, and it was the LOCK, not the constraint. The lock sat
68
+ # on 0.5.3 while both consumers shipped 0.5.7 — four patch releases — and nothing
69
+ # was red, because engine CI installs with `bundler-cache: true` and never
70
+ # resolves fresh. Meanwhile test/views/style_web3_specimens_test.rb exists to
71
+ # prove "the style guide renders the REAL gem cards" and reads them off whatever
72
+ # the LOCK resolved. Behind, that guard certifies a card no consumer receives; it
73
+ # still passes, and only its MEANING has changed. (Measured on this span: the
74
+ # gem's whole app/ tree was byte-identical 0.5.3 -> 0.5.7, so this instance cost
75
+ # nothing — which is precisely why it went four releases unnoticed.)
76
+ #
77
+ # SO THE FIX IS A GATE, NOT A PIN. bin/gem-drift-check fails when this engine's
78
+ # lock TRAILS a consumer's, and consumer-ci.yml runs it — the only lane holding
79
+ # two repos' lockfiles at once. Direction is one-way: engine behind fails, engine
80
+ # ahead or level passes, a consumer bundling no solana-studio is a skip. Keeping
81
+ # up is now enforced rather than remembered, which is what lets the floor stay a
82
+ # floor. Guarded by test/lib/gem_drift_check_test.rb.
55
83
  group :development, :test do
56
84
  gem "solana-studio", ">= 0.5.3"
57
85
  end
@@ -43,6 +43,47 @@ module Studio
43
43
  end
44
44
 
45
45
  def show
46
+ # Guarded like intake!'s MissingTable posture: a consumer that bumped the
47
+ # gem without installing the expectations migrations keeps its working
48
+ # doc pages — the link select simply doesn't render.
49
+ @linkable_expectations =
50
+ if Studio::KnowledgeExpectation.table_exists?
51
+ Studio::KnowledgeExpectation.active.for_entity(@doc.entity).order(:path, :title).to_a
52
+ else
53
+ []
54
+ end
55
+ end
56
+
57
+ # GET /admin/knowledge/coverage — expected vs uploaded, the gap named.
58
+ def coverage
59
+ unless Studio::KnowledgeExpectation.table_exists?
60
+ return redirect_to admin_knowledge_path,
61
+ alert: "The coverage view needs the studio_knowledge_expectations table — " \
62
+ "run `bin/rails studio_engine:install:migrations && bin/rails db:migrate`."
63
+ end
64
+
65
+ @entity = params[:entity].presence
66
+ scope = Studio::KnowledgeExpectation.active.order(:path, :title)
67
+ scope = scope.for_entity(@entity) if @entity
68
+ @expectations = scope.to_a
69
+ @entities = Studio::KnowledgeExpectation.distinct.pluck(:entity).sort
70
+ end
71
+
72
+ def create_expectation
73
+ expectation = Studio::KnowledgeExpectation.create!(expectation_params)
74
+ redirect_to admin_knowledge_coverage_path(entity: expectation.entity),
75
+ notice: "Expecting: #{expectation.title}."
76
+ rescue ActiveRecord::RecordInvalid => e
77
+ redirect_to admin_knowledge_coverage_path, alert: e.message
78
+ end
79
+
80
+ def update_expectation
81
+ expectation = Studio::KnowledgeExpectation.find(params[:id])
82
+ expectation.update!(expectation_params)
83
+ redirect_to admin_knowledge_coverage_path(entity: expectation.entity),
84
+ notice: "#{expectation.title} updated."
85
+ rescue ActiveRecord::RecordInvalid => e
86
+ redirect_to admin_knowledge_coverage_path, alert: e.message
46
87
  end
47
88
 
48
89
  def create
@@ -73,6 +114,12 @@ module Studio
73
114
 
74
115
  private
75
116
 
117
+ def expectation_params
118
+ params.require(:knowledge_expectation)
119
+ .permit(:entity, :title, :path, :category, :cadence, :start_on,
120
+ :source_note, :active)
121
+ end
122
+
76
123
  def set_doc
77
124
  @doc = Studio::KnowledgeDoc.find(params[:id])
78
125
  end
@@ -80,7 +127,8 @@ module Studio
80
127
  def doc_params
81
128
  permitted = params.require(:knowledge_doc)
82
129
  .permit(:title, :entity, :path, :category, :summary,
83
- :document_date, :source_note, :status, access: {})
130
+ :document_date, :source_note, :status,
131
+ :expectation_id, access: {})
84
132
  # The access map arrives as {"samson" => "full", ...}; drop blanks so an
85
133
  # untouched select doesn't write a "none" the map means by absence anyway.
86
134
  if permitted[:access]
@@ -35,6 +35,12 @@ module Studio
35
35
  ACCESS_LEVELS = %w[full aware none].freeze
36
36
  STATUSES = %w[inbox filed superseded].freeze
37
37
 
38
+ # The expectation this document fulfills (coverage view) — optional, set at
39
+ # triage. Guarded: consumers that installed the docs table before the
40
+ # expectations migration existed simply have no column yet.
41
+ belongs_to :expectation, class_name: "Studio::KnowledgeExpectation",
42
+ optional: true, inverse_of: :docs
43
+
38
44
  validates :title, presence: true
39
45
  validates :entity, presence: true
40
46
  validates :status, inclusion: { in: STATUSES }
@@ -0,0 +1,91 @@
1
+ module Studio
2
+ # One row per document the knowledge layer EXPECTS — the other half of the
3
+ # coverage question. Documents record what exists; expectations record what
4
+ # should, so the gap between them is computable instead of remembered.
5
+ #
6
+ # Two cadences:
7
+ #
8
+ # once — a single fulfilling document satisfies it forever (an LOI, a
9
+ # lease, tracker item 14).
10
+ # monthly — one document per calendar month from start_on onward (aging
11
+ # inventory, bank statements). Each month is a SLOT; a slot is
12
+ # filled when a linked doc's as-of date falls inside it, and the
13
+ # coverage view names the missing months outright.
14
+ #
15
+ # Fulfillment is an EXPLICIT link — Studio::KnowledgeDoc#expectation_id, set
16
+ # at triage — never a name match: a fuzzy matcher silently merges lookalike
17
+ # documents, an id never does.
18
+ #
19
+ # Installed per consumer like the other studio_* tables.
20
+ class KnowledgeExpectation < ApplicationRecord
21
+ self.table_name = "studio_knowledge_expectations"
22
+
23
+ CADENCES = %w[once monthly].freeze
24
+
25
+ has_many :docs, class_name: "Studio::KnowledgeDoc",
26
+ foreign_key: :expectation_id, inverse_of: :expectation,
27
+ dependent: :nullify
28
+
29
+ validates :entity, presence: true
30
+ validates :title, presence: true
31
+ validates :cadence, inclusion: { in: CADENCES }
32
+
33
+ before_validation :normalize_fields
34
+
35
+ scope :for_entity, ->(entity) { where(entity: entity) }
36
+ scope :active, -> { where(active: true) }
37
+
38
+ def monthly?
39
+ cadence == "monthly"
40
+ end
41
+
42
+ # The linked documents that can fill slots — superseded rows never count.
43
+ def fulfilling_docs
44
+ docs.active.to_a
45
+ end
46
+
47
+ # Monthly slot months, oldest first: start_on's month (or this row's
48
+ # creation month) through as_of's month. Nil for once-cadence.
49
+ def slot_months(as_of: Date.current)
50
+ return nil unless monthly?
51
+
52
+ first = (start_on || created_at&.to_date || as_of).beginning_of_month
53
+ last = as_of.beginning_of_month
54
+ return [] if first > last
55
+
56
+ months = []
57
+ cursor = first
58
+ while cursor <= last
59
+ months << cursor
60
+ cursor = cursor.next_month
61
+ end
62
+ months
63
+ end
64
+
65
+ # Coverage for the view, one shape per cadence:
66
+ # once: { filled: bool, docs: [...] }
67
+ # monthly: { slots: [{ month:, docs: [...] }], missing_months: [...] }
68
+ def coverage(as_of: Date.current)
69
+ linked = fulfilling_docs
70
+ return { filled: linked.any?, docs: linked } unless monthly?
71
+
72
+ slots = slot_months(as_of: as_of).map do |month|
73
+ { month: month,
74
+ docs: linked.select { |doc| doc.display_date&.beginning_of_month == month } }
75
+ end
76
+ { slots: slots, missing_months: slots.select { |slot| slot[:docs].empty? }.map { |slot| slot[:month] } }
77
+ end
78
+
79
+ def filled?(as_of: Date.current)
80
+ result = coverage(as_of: as_of)
81
+ monthly? ? result[:missing_months].empty? : result[:filled]
82
+ end
83
+
84
+ private
85
+
86
+ def normalize_fields
87
+ self.entity = entity.to_s.strip.downcase if entity
88
+ self.path = Studio::KnowledgeDoc.normalize_path(path)
89
+ end
90
+ end
91
+ end
@@ -43,6 +43,8 @@
43
43
  <span aria-hidden="true">·</span>
44
44
  <%= link_to "Show all", admin_knowledge_path(view: "flat", entity: entity, status: status),
45
45
  class: view == "flat" ? "font-semibold underline" : "" %>
46
+ <span aria-hidden="true">·</span>
47
+ <%= link_to "Coverage", admin_knowledge_coverage_path(entity: entity), id: "knowledge-coverage-link" %>
46
48
  </div>
47
49
  </div>
48
50
 
@@ -0,0 +1,109 @@
1
+ <%# The coverage table — expected vs uploaded, gaps named. Locals:
2
+ expectations: active Studio::KnowledgeExpectation rows to render
3
+ entity: entity filter or nil
4
+ entities: all entity slugs (filter row) %>
5
+ <style>
6
+ .coverage-mark { font-size: .8rem; padding: 0 .45rem; border-radius: .25rem; white-space: nowrap; }
7
+ .coverage-filled { color: rgb(5 150 105); }
8
+ .coverage-missing { color: rgb(220 38 38); border: 1px solid rgb(220 38 38 / .5); font-weight: 600; }
9
+ .coverage-slot { display: inline-block; margin-right: .4rem; }
10
+ </style>
11
+
12
+ <div class="flex flex-wrap items-center justify-between gap-3 mb-4">
13
+ <h1 class="text-xl font-semibold">Coverage</h1>
14
+ <div class="text-sm"><%= link_to "← Knowledge", admin_knowledge_path(entity: entity) %></div>
15
+ </div>
16
+
17
+ <% if entities.length > 1 || entity %>
18
+ <div class="flex flex-wrap items-center gap-2 mb-3 text-sm">
19
+ <%= link_to "All entities", admin_knowledge_coverage_path, class: entity ? "" : "font-semibold underline" %>
20
+ <% entities.each do |candidate| %>
21
+ <%= link_to candidate, admin_knowledge_coverage_path(entity: candidate),
22
+ class: candidate == entity ? "font-semibold underline" : "" %>
23
+ <% end %>
24
+ </div>
25
+ <% end %>
26
+
27
+ <% if expectations.any? %>
28
+ <div class="overflow-x-auto mb-6">
29
+ <table id="knowledge-coverage" class="w-full text-sm">
30
+ <thead>
31
+ <tr class="text-left border-b">
32
+ <th class="py-2 pr-3">Expected</th>
33
+ <th class="py-2 pr-3">Folder</th>
34
+ <th class="py-2 pr-3">Cadence</th>
35
+ <th class="py-2 pr-3">Coverage</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody>
39
+ <% expectations.each do |expectation| %>
40
+ <% result = expectation.coverage %>
41
+ <tr id="coverage-exp-<%= expectation.id %>" class="border-b align-top">
42
+ <td class="py-2 pr-3">
43
+ <span class="font-medium"><%= expectation.title %></span>
44
+ <% if expectation.source_note.present? %>
45
+ <div class="text-xs opacity-60"><%= expectation.source_note %></div>
46
+ <% end %>
47
+ </td>
48
+ <td class="py-2 pr-3 text-xs opacity-80"><%= expectation.path.presence || "root" %></td>
49
+ <td class="py-2 pr-3"><%= expectation.cadence %></td>
50
+ <td class="py-2 pr-3">
51
+ <% if expectation.monthly? %>
52
+ <% result[:slots].each do |slot| %>
53
+ <span class="coverage-slot coverage-mark <%= slot[:docs].any? ? "coverage-filled" : "coverage-missing" %>"
54
+ id="coverage-slot-<%= expectation.id %>-<%= slot[:month].strftime("%Y-%m") %>">
55
+ <%= slot[:month].strftime("%Y-%m") %> <%= slot[:docs].any? ? "✓" : "MISSING" %>
56
+ </span>
57
+ <% end %>
58
+ <% elsif result[:filled] %>
59
+ <span class="coverage-mark coverage-filled">✓ filled —
60
+ <% result[:docs].first(2).each do |doc| %>
61
+ <%= link_to doc.title, admin_knowledge_doc_path(doc) %>
62
+ <% end %>
63
+ </span>
64
+ <% else %>
65
+ <span class="coverage-mark coverage-missing">MISSING</span>
66
+ <% end %>
67
+ </td>
68
+ </tr>
69
+ <% end %>
70
+ </tbody>
71
+ </table>
72
+ </div>
73
+ <% else %>
74
+ <p id="coverage-empty" class="text-sm opacity-70 mb-4">Nothing is expected yet — add the first expectation below.</p>
75
+ <% end %>
76
+
77
+ <div id="coverage-new" class="p-4 border rounded">
78
+ <h2 class="font-semibold mb-3">Expect a document</h2>
79
+ <%= form_with url: admin_knowledge_expectations_path, method: :post, local: true do |form| %>
80
+ <div class="grid gap-3 md:grid-cols-3">
81
+ <label class="block text-sm">
82
+ <span class="block mb-1">Title</span>
83
+ <%= form.text_field "knowledge_expectation[title]", required: true, class: "block w-full border rounded px-2 py-1" %>
84
+ </label>
85
+ <label class="block text-sm">
86
+ <span class="block mb-1">Entity</span>
87
+ <%= form.text_field "knowledge_expectation[entity]", value: entity, required: true, class: "block w-full border rounded px-2 py-1" %>
88
+ </label>
89
+ <label class="block text-sm">
90
+ <span class="block mb-1">Folder</span>
91
+ <%= form.text_field "knowledge_expectation[path]", class: "block w-full border rounded px-2 py-1" %>
92
+ </label>
93
+ <label class="block text-sm">
94
+ <span class="block mb-1">Cadence</span>
95
+ <%= form.select "knowledge_expectation[cadence]",
96
+ [["once", "once"], ["monthly", "monthly"]], {}, class: "block w-full border rounded px-2 py-1" %>
97
+ </label>
98
+ <label class="block text-sm">
99
+ <span class="block mb-1">Starts <span class="opacity-60">(monthly only)</span></span>
100
+ <%= form.date_field "knowledge_expectation[start_on]", class: "block w-full border rounded px-2 py-1" %>
101
+ </label>
102
+ <label class="block text-sm">
103
+ <span class="block mb-1">Source <span class="opacity-60">("tracker item 14")</span></span>
104
+ <%= form.text_field "knowledge_expectation[source_note]", class: "block w-full border rounded px-2 py-1" %>
105
+ </label>
106
+ </div>
107
+ <div class="mt-3"><%= form.submit "Add expectation", class: "px-3 py-1.5 border rounded font-medium" %></div>
108
+ <% end %>
109
+ </div>
@@ -0,0 +1,4 @@
1
+ <%# /admin/knowledge/coverage — bare content wrapper; the table partial is the
2
+ primitive (renderable + testable on its own, like the browser). %>
3
+ <%= render "studio/knowledge_docs/coverage_table",
4
+ expectations: @expectations, entity: @entity, entities: @entities %>
@@ -69,6 +69,15 @@
69
69
  class: "block w-full border rounded px-2 py-1" %>
70
70
  </label>
71
71
  <% end %>
72
+ <% expectations = @linkable_expectations || [] %>
73
+ <% if expectations.any? %>
74
+ <label class="block text-sm" id="knowledge-expectation-select">
75
+ <span class="block mb-1">Fulfills expectation</span>
76
+ <%= form.select "knowledge_doc[expectation_id]",
77
+ [["— none —", ""]] + expectations.map { |e| [ "#{e.title} (#{e.cadence})", e.id ] },
78
+ { selected: @doc.expectation_id }, class: "block w-full border rounded px-2 py-1" %>
79
+ </label>
80
+ <% end %>
72
81
  <label class="block text-sm md:col-span-2">
73
82
  <span class="block mb-1">Summary (safe for "aware" agents)</span>
74
83
  <%= form.text_area "knowledge_doc[summary]", value: @doc.summary, rows: 3, class: "block w-full border rounded px-2 py-1" %>
@@ -28,7 +28,8 @@
28
28
  whenever a brand is absent or unrecognised, instead of hiding the avatar and
29
29
  giving the surface two different shapes.
30
30
 
31
- ONE CALLER DELIBERATELY DOES NOT: studio/modals/_wallet_connect paints a LETTER
31
+ ONE CALLER DELIBERATELY DOES NOT: solana-studio's
32
+ solana_studio/modals/_wallet_connect paints a LETTER
32
33
  TILE for a detected wallet whose brand it does not know, not se-wallet-default.
33
34
  A detected wallet always has a name (it registered through Wallet Standard to
34
35
  get there), and its initial identifies it where a generic mark would not. The
@@ -182,8 +182,9 @@
182
182
  return { connecting: false, error: null, statusText: 'Connect Wallet',
183
183
  walletAvailable: false, isMobile: false, connect: function () {} };
184
184
  };
185
- // Connect stub for the promoted wallet picker (studio/modals/_wallet_connect,
186
- // configured by style/modals/_wallet_connect). Performs NO connect: it
185
+ // Connect stub for the wallet picker (solana-studio
186
+ // solana_studio/modals/_wallet_connect, configured by
187
+ // style/modals/_wallet_connect). Performs NO connect: it
187
188
  // resolves success after a beat so the picker's connecting state is
188
189
  // visible and its onConnected hook can continue the demo walk.
189
190
  window.dsWalletConnectDemo = window.dsWalletConnectDemo || function () {
@@ -1169,9 +1170,9 @@
1169
1170
  (<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
1170
1171
  so every app inherits them with no per-app icon files.
1171
1172
  The two <strong>Sign Wallet</strong> cards are a different kind of thing
1172
- from the rest of this section: they render
1173
- <code class="font-mono text-2xs">studio/modals/_web3_step_up</code> &mdash;
1174
- the <strong>REAL shared partial a host renders in production</strong>,
1173
+ from the rest of this section: they render solana-studio's
1174
+ <code class="font-mono text-2xs">solana_studio/modals/_web3_step_up</code>
1175
+ &mdash; the <strong>REAL shared partial a host renders in production</strong>,
1175
1176
  not a style-guide copy of it &mdash; so what you review here is the
1176
1177
  shipped card, and it cannot drift from what users see. They also carry the
1177
1178
  <strong>standard web3 auth button</strong>: a wallet row (brand mark, the
@@ -1,5 +1,6 @@
1
1
  <%#
2
- The style guide's pre-connect slot for studio/modals/_wallet_connect — the
2
+ The style guide's pre-connect slot for solana-studio's
3
+ solana_studio/modals/_wallet_connect — the
3
4
  legal-age attestation, sitting exactly where turf-monster puts its own.
4
5
 
5
6
  A PARTIAL rather than a block because the picker takes its slot as a named
@@ -0,0 +1,32 @@
1
+ # Reference migration for Studio::KnowledgeExpectation — the "what SHOULD
2
+ # exist" half of the knowledge layer's coverage view. Installed per consumer
3
+ # like the other studio_* tables (`bin/rails studio_engine:install:migrations
4
+ # && bin/rails db:migrate`); do not hand-copy.
5
+ #
6
+ # STYLE NOTE: reference migrations are written omakase-rubocop-compatible
7
+ # (spaces inside array brackets) because the installed copy is linted by the
8
+ # CONSUMER's rubocop, not this gem's.
9
+ class CreateStudioKnowledgeExpectations < ActiveRecord::Migration[7.2]
10
+ def change
11
+ create_table :studio_knowledge_expectations do |t|
12
+ # Which business expects the document (same vocabulary as knowledge docs).
13
+ t.string :entity, null: false
14
+ t.string :title, null: false
15
+ # Folder the fulfilling documents live under; display + default, not a matcher.
16
+ t.string :path, null: false, default: ""
17
+ t.string :category
18
+ # once: a single document satisfies it forever.
19
+ # monthly: one document per calendar month from start_on onward.
20
+ t.string :cadence, null: false, default: "once"
21
+ # First month a monthly expectation owes a document (defaults to creation month).
22
+ t.date :start_on
23
+ # Provenance: "diligence tracker item 14", "lender checklist", …
24
+ t.string :source_note
25
+ t.boolean :active, null: false, default: true
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ add_index :studio_knowledge_expectations, [ :entity, :active ]
31
+ end
32
+ end
@@ -0,0 +1,9 @@
1
+ # Links a knowledge doc to the expectation it fulfills — set at triage, an
2
+ # EXPLICIT pointer rather than name-matching (a fuzzy match silently merges
3
+ # lookalikes; an id never does). Installed per consumer; do not hand-copy.
4
+ class AddExpectationToStudioKnowledgeDocs < ActiveRecord::Migration[7.2]
5
+ def change
6
+ add_column :studio_knowledge_docs, :expectation_id, :bigint
7
+ add_index :studio_knowledge_docs, :expectation_id
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.68.0"
2
+ VERSION = "0.69.1"
3
3
  end
data/lib/studio.rb CHANGED
@@ -861,6 +861,14 @@ module Studio
861
861
  if Studio.draw_knowledge_routes
862
862
  get "admin/knowledge", to: "studio/knowledge_docs#index", as: :admin_knowledge
863
863
  post "admin/knowledge", to: "studio/knowledge_docs#create"
864
+ # Coverage (expected vs uploaded) — MUST precede the :id route below,
865
+ # or "coverage" is swallowed as a document id.
866
+ get "admin/knowledge/coverage", to: "studio/knowledge_docs#coverage",
867
+ as: :admin_knowledge_coverage
868
+ post "admin/knowledge/expectations", to: "studio/knowledge_docs#create_expectation",
869
+ as: :admin_knowledge_expectations
870
+ patch "admin/knowledge/expectations/:id", to: "studio/knowledge_docs#update_expectation",
871
+ as: :admin_knowledge_expectation
864
872
  get "admin/knowledge/:id", to: "studio/knowledge_docs#show", as: :admin_knowledge_doc
865
873
  patch "admin/knowledge/:id", to: "studio/knowledge_docs#update"
866
874
  get "admin/knowledge/:id/download", to: "studio/knowledge_docs#download",
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.68.0
4
+ version: 0.69.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -363,6 +363,7 @@ files:
363
363
  - app/models/studio/enumeral.rb
364
364
  - app/models/studio/geo_setting.rb
365
365
  - app/models/studio/knowledge_doc.rb
366
+ - app/models/studio/knowledge_expectation.rb
366
367
  - app/models/studio/link.rb
367
368
  - app/models/studio/model_page.rb
368
369
  - app/models/theme_setting.rb
@@ -441,8 +442,10 @@ files:
441
442
  - app/views/studio/knowledge_docs/_access_chips.html.erb
442
443
  - app/views/studio/knowledge_docs/_browser.html.erb
443
444
  - app/views/studio/knowledge_docs/_chip_styles.html.erb
445
+ - app/views/studio/knowledge_docs/_coverage_table.html.erb
444
446
  - app/views/studio/knowledge_docs/_doc_row.html.erb
445
447
  - app/views/studio/knowledge_docs/_upload_form.html.erb
448
+ - app/views/studio/knowledge_docs/coverage.html.erb
446
449
  - app/views/studio/knowledge_docs/index.html.erb
447
450
  - app/views/studio/knowledge_docs/show.html.erb
448
451
  - app/views/studio/links/confirm.html.erb
@@ -554,6 +557,8 @@ files:
554
557
  - db/migrate/20260813220000_add_standard_user_profile_columns.rb
555
558
  - db/migrate/20260818120000_create_studio_geo_settings.rb
556
559
  - db/migrate/20260901000001_create_studio_knowledge_docs.rb
560
+ - db/migrate/20260902000001_create_studio_knowledge_expectations.rb
561
+ - db/migrate/20260902000002_add_expectation_to_studio_knowledge_docs.rb
557
562
  - lib/studio-engine.rb
558
563
  - lib/studio.rb
559
564
  - lib/studio/cable.rb