studio-engine 0.67.1 → 0.67.2
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 +15 -0
- data/app/controllers/studio/knowledge_docs_controller.rb +5 -2
- data/app/models/studio/knowledge_doc.rb +15 -3
- data/app/views/studio/knowledge_docs/_browser.html.erb +1 -5
- data/app/views/studio/knowledge_docs/_chip_styles.html.erb +10 -0
- data/app/views/studio/knowledge_docs/show.html.erb +2 -8
- data/db/migrate/20260901000001_create_studio_knowledge_docs.rb +7 -2
- data/lib/studio/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79895e24dd5ade937ec8c0b4afacd84605ad495530a4c37f94e053a2c1040170
|
|
4
|
+
data.tar.gz: 0f1716ef07d10fc5388ea15853767f6c7e8911e076e280a096a8424a494eba9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7c6e391302c996753b399019f2baeded267820a69d1f7451fe7ff68f82609fc625cc477cbdebb6127f74d0d5ed3a326ed4357d02c24b595e0fd1c213c3605895
|
|
7
|
+
data.tar.gz: 26bda400a871f3998a7baa64c72f43ed71a2d6d4ff177e02c4c6ae76d9bff65438e0bb33d48564802ea982bc2f628631906fc25e0a24167ec11d38796908310d
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Knowledge layer hardening** — the five findings from the 0.67.0 reviews:
|
|
10
|
+
upload keys carry a random suffix (same-named uploads in the same second no
|
|
11
|
+
longer overwrite the first object before the unique index can refuse);
|
|
12
|
+
`normalize_path` drops dot segments (`a/../b` → `a/b` — the S3 key mirrors
|
|
13
|
+
the path, and a `..` reads as traversal to every scanner); `access_for`
|
|
14
|
+
normalizes the queried agent the way writes normalize keys (a capitalized
|
|
15
|
+
`Studio.knowledge_agents` roster entry no longer reads `none`); the inbox
|
|
16
|
+
badge counts before the status filter (filtering to `filed` no longer zeroes
|
|
17
|
+
it); chip styles moved to one shared partial (`_chip_styles`); and reference
|
|
18
|
+
migrations are now written omakase-rubocop-compatible, with the convention
|
|
19
|
+
noted in the migration header (consumer copies are linted by the consumer's
|
|
20
|
+
rubocop — measured on the Industries adoption ship).
|
|
21
|
+
|
|
7
22
|
### Added
|
|
8
23
|
|
|
9
24
|
- **Knowledge layer primitive** — `Studio::KnowledgeDoc` + `/admin/knowledge`:
|
|
@@ -25,11 +25,14 @@ module Studio
|
|
|
25
25
|
|
|
26
26
|
scope = Studio::KnowledgeDoc.order(created_at: :desc)
|
|
27
27
|
scope = scope.for_entity(@entity) if @entity
|
|
28
|
-
scope = scope.where(status: @status) if @status
|
|
29
28
|
|
|
30
|
-
@entities
|
|
29
|
+
@entities = Studio::KnowledgeDoc.distinct.pluck(:entity).sort
|
|
30
|
+
# Counted BEFORE the status filter: the badge answers "what waits for
|
|
31
|
+
# triage in this entity", and filtering to status=filed must not zero it.
|
|
31
32
|
@inbox_size = scope.inbox.count
|
|
32
33
|
|
|
34
|
+
scope = scope.where(status: @status) if @status
|
|
35
|
+
|
|
33
36
|
if @view == "folders"
|
|
34
37
|
@folders = scope.folders_under(@folder)
|
|
35
38
|
@docs = scope.in_folder(@folder)
|
|
@@ -93,8 +93,13 @@ module Studio
|
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# "/a//b/" -> "a/b". Nil-safe; the empty string is the root folder.
|
|
96
|
+
# Dot segments are dropped ("a/../b" -> "a/b"): harmless server-side, but
|
|
97
|
+
# the S3 key mirrors the path and a ".." reads as traversal to every
|
|
98
|
+
# scanner and human who ever greps the bucket.
|
|
96
99
|
def normalize_path(value)
|
|
97
|
-
value.to_s.strip.squeeze("/").
|
|
100
|
+
value.to_s.strip.squeeze("/").split("/")
|
|
101
|
+
.reject { |segment| segment.empty? || segment == "." || segment == ".." }
|
|
102
|
+
.join("/")
|
|
98
103
|
end
|
|
99
104
|
|
|
100
105
|
def default_title(file)
|
|
@@ -105,8 +110,11 @@ module Studio
|
|
|
105
110
|
|
|
106
111
|
# --- access ---------------------------------------------------------------
|
|
107
112
|
|
|
113
|
+
# The queried agent is normalized the same way written keys are, so a
|
|
114
|
+
# capitalized Studio.knowledge_agents roster entry reads its real level
|
|
115
|
+
# instead of silently defaulting to "none".
|
|
108
116
|
def access_for(agent)
|
|
109
|
-
level = access.is_a?(Hash) ? access[agent.to_s] : nil
|
|
117
|
+
level = access.is_a?(Hash) ? access[agent.to_s.strip.downcase] : nil
|
|
110
118
|
ACCESS_LEVELS.include?(level) ? level : "none"
|
|
111
119
|
end
|
|
112
120
|
|
|
@@ -133,9 +141,13 @@ module Studio
|
|
|
133
141
|
content_type = file.respond_to?(:content_type) ? file.content_type : nil
|
|
134
142
|
body = file.respond_to?(:read) ? file.read : file.to_s
|
|
135
143
|
|
|
144
|
+
# The random suffix is load-bearing: the timestamp is second-granularity,
|
|
145
|
+
# so two same-named uploads in one second would otherwise write the same
|
|
146
|
+
# key — the second S3 PUT overwrites the first object BEFORE the unique
|
|
147
|
+
# index rejects the second row (found in PR #251 review, twice over).
|
|
136
148
|
key = [
|
|
137
149
|
"knowledge", entity, path.presence,
|
|
138
|
-
"#{Time.current.strftime('%Y%m%d%H%M%S')}-#{self.class.sanitize_filename(filename)}"
|
|
150
|
+
"#{Time.current.strftime('%Y%m%d%H%M%S')}-#{SecureRandom.hex(4)}-#{self.class.sanitize_filename(filename)}"
|
|
139
151
|
].compact.join("/")
|
|
140
152
|
|
|
141
153
|
Studio::S3.upload(key: key, body: body, content_type: content_type)
|
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
Styling note: gem-specific looks live in the scoped <style> below because a
|
|
12
12
|
host's Tailwind build may not scan this gem's views (same reasoning as the
|
|
13
13
|
geo grid). Layout leans on utilities every host already ships. %>
|
|
14
|
+
<%= render "studio/knowledge_docs/chip_styles" %>
|
|
14
15
|
<style>
|
|
15
|
-
.knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
|
|
16
|
-
font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
|
|
17
|
-
.knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
|
|
18
|
-
.knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
|
|
19
|
-
.knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
|
|
20
16
|
.knowledge-status { font-size: .7rem; padding: 0 .45rem; border-radius: .25rem; border: 1px solid rgb(148 163 184 / .5); }
|
|
21
17
|
.knowledge-status-inbox { border-color: rgb(245 158 11 / .6); color: rgb(180 83 9); }
|
|
22
18
|
.knowledge-status-superseded { text-decoration: line-through; opacity: .6; }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<%# The access-chip look, shared by the browser and the show page — one
|
|
2
|
+
definition so the two can never drift. Scoped <style> rather than utility
|
|
3
|
+
classes because a host's Tailwind build may not scan this gem's views. %>
|
|
4
|
+
<style>
|
|
5
|
+
.knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
|
|
6
|
+
font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
|
|
7
|
+
.knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
|
|
8
|
+
.knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
|
|
9
|
+
.knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
|
|
10
|
+
</style>
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
<%# /admin/knowledge/:id — one document: metadata, access, download, and the
|
|
2
2
|
triage form that files it out of the inbox. %>
|
|
3
|
-
|
|
4
|
-
.knowledge-chip { display: inline-block; padding: 0 .5rem; border-radius: 9999px;
|
|
5
|
-
font-size: .7rem; line-height: 1.4rem; border: 1px solid transparent; white-space: nowrap; }
|
|
6
|
-
.knowledge-chip-full { background: rgb(16 185 129 / .12); border-color: rgb(16 185 129 / .4); color: rgb(5 150 105); }
|
|
7
|
-
.knowledge-chip-aware { background: rgb(245 158 11 / .12); border-color: rgb(245 158 11 / .4); color: rgb(180 83 9); }
|
|
8
|
-
.knowledge-chip-none { background: rgb(148 163 184 / .12); border-color: rgb(148 163 184 / .4); color: rgb(100 116 139); }
|
|
9
|
-
</style>
|
|
3
|
+
<%= render "studio/knowledge_docs/chip_styles" %>
|
|
10
4
|
|
|
11
5
|
<div class="mb-4 text-sm">
|
|
12
6
|
<%= link_to "← Knowledge", admin_knowledge_path(folder: @doc.path.presence) %>
|
|
@@ -29,7 +23,7 @@
|
|
|
29
23
|
<div><dt class="opacity-60">Document date</dt><dd><%= @doc.document_date&.iso8601 || "—" %></dd></div>
|
|
30
24
|
<div><dt class="opacity-60">Uploaded</dt><dd><%= @doc.created_at&.to_date&.iso8601 %> by <%= @doc.uploaded_by.presence || "—" %></dd></div>
|
|
31
25
|
<div><dt class="opacity-60">Source</dt><dd><%= @doc.source_note.presence || "—" %></dd></div>
|
|
32
|
-
<div><dt class="opacity-60">File</dt><dd><%= @doc.file? ?
|
|
26
|
+
<div><dt class="opacity-60">File</dt><dd><%= @doc.file? ? [ @doc.mime_type.presence || "file", (number_to_human_size(@doc.byte_size) if @doc.byte_size) ].compact.join(" · ") : "metadata only" %></dd></div>
|
|
33
27
|
<div class="md:col-span-2"><dt class="opacity-60">Access</dt><dd><%= render "studio/knowledge_docs/access_chips", doc: @doc %></dd></div>
|
|
34
28
|
</dl>
|
|
35
29
|
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
# (`bin/rails studio_engine:install:migrations && bin/rails db:migrate`) so the
|
|
4
4
|
# table is created in the app's database. Do not hand-copy — a hand copy
|
|
5
5
|
# collides with the installed copy on `class CreateStudioKnowledgeDocs`.
|
|
6
|
+
#
|
|
7
|
+
# STYLE NOTE: reference migrations are written omakase-rubocop-compatible
|
|
8
|
+
# (spaces inside array brackets) because the installed copy is linted by the
|
|
9
|
+
# CONSUMER's rubocop, not this gem's — an engine-styled copy fails the
|
|
10
|
+
# consumer's cert untouched (measured on the Industries adoption ship).
|
|
6
11
|
class CreateStudioKnowledgeDocs < ActiveRecord::Migration[7.2]
|
|
7
12
|
def change
|
|
8
13
|
create_table :studio_knowledge_docs do |t|
|
|
@@ -41,8 +46,8 @@ class CreateStudioKnowledgeDocs < ActiveRecord::Migration[7.2]
|
|
|
41
46
|
t.timestamps
|
|
42
47
|
end
|
|
43
48
|
|
|
44
|
-
add_index :studio_knowledge_docs, [:entity, :status]
|
|
45
|
-
add_index :studio_knowledge_docs, [:entity, :path]
|
|
49
|
+
add_index :studio_knowledge_docs, [ :entity, :status ]
|
|
50
|
+
add_index :studio_knowledge_docs, [ :entity, :path ]
|
|
46
51
|
add_index :studio_knowledge_docs, :s3_key, unique: true
|
|
47
52
|
add_index :studio_knowledge_docs, :superseded_by_id
|
|
48
53
|
end
|
data/lib/studio/version.rb
CHANGED
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.67.
|
|
4
|
+
version: 0.67.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -440,6 +440,7 @@ files:
|
|
|
440
440
|
- app/views/studio/geo_settings/edit.html.erb
|
|
441
441
|
- app/views/studio/knowledge_docs/_access_chips.html.erb
|
|
442
442
|
- app/views/studio/knowledge_docs/_browser.html.erb
|
|
443
|
+
- app/views/studio/knowledge_docs/_chip_styles.html.erb
|
|
443
444
|
- app/views/studio/knowledge_docs/_doc_row.html.erb
|
|
444
445
|
- app/views/studio/knowledge_docs/_upload_form.html.erb
|
|
445
446
|
- app/views/studio/knowledge_docs/index.html.erb
|