From dda3d87073617bcb0993e22dfb693d4280530917 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Sun, 15 Nov 2026 16:18:27 +0900 Subject: [PATCH] =?UTF-8?q?=E5=A4=96=E9=83=A8=E3=82=BF=E3=82=B0=E3=81=AE?= =?UTF-8?q?=E5=B1=A5=E6=AD=B4=E7=AE=A1=E7=90=86=E6=96=B9=E5=BC=8F=E3=82=92?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=20(#445)=20(#480)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Codex Night Worker Reviewed-on: https://git.miteruzo.com/miteruzo/btrc-hub/pulls/480 Co-authored-by: miteruzo --- .gitea/ISSUE_TEMPLATE.md | 35 - .../controllers/post_versions_controller.rb | 11 +- backend/app/models/external_tag.rb | 25 +- backend/app/models/history_access.rb | 16 +- backend/app/models/nico_tag_version.rb | 7 +- .../representations/counts_version_repr.rb | 5 +- backend/app/services/counts_candidates.rb | 6 +- backend/app/services/counts_population.rb | 2 +- .../services/counts_resource_definition.rb | 21 +- backend/app/services/counts_state_query.rb | 4 +- backend/app/services/counts_table_query.rb | 2 + .../app/services/nico_tag_version_recorder.rb | 4 +- ...add_linked_tag_ids_to_nico_tag_versions.rb | 5 + ...igrate_linked_tags_in_nico_tag_versions.rb | 42 + ...move_linked_tags_from_nico_tag_versions.rb | 6 + backend/db/schema.rb | 5 +- backend/spec/models/external_tag_spec.rb | 51 +- backend/spec/models/history_access_spec.rb | 6 +- backend/spec/models/shared_tag_ids_spec.rb | 7 +- backend/spec/models/version_record_spec.rb | 20 +- backend/spec/requests/counts_table_spec.rb | 120 +- backend/spec/requests/nico_tags_spec.rb | 22 +- .../requests/post_history_privacy_spec.rb | 2 +- backend/spec/services/counts_export_spec.rb | 49 + .../nico_tag_version_recorder_spec.rb | 26 +- docs/SYSTEM_SPEC.md | 3366 +++++++++++++++++ docs/counts-exports.md | 16 +- 27 files changed, 3748 insertions(+), 133 deletions(-) delete mode 100644 .gitea/ISSUE_TEMPLATE.md create mode 100644 backend/db/migrate/20261108000000_add_linked_tag_ids_to_nico_tag_versions.rb create mode 100644 backend/db/migrate/20261108010000_migrate_linked_tags_in_nico_tag_versions.rb create mode 100644 backend/db/migrate/20261108020000_remove_linked_tags_from_nico_tag_versions.rb create mode 100644 docs/SYSTEM_SPEC.md diff --git a/.gitea/ISSUE_TEMPLATE.md b/.gitea/ISSUE_TEMPLATE.md deleted file mode 100644 index c29340a..0000000 --- a/.gitea/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,35 +0,0 @@ -## 背景 - -なぜ必要か。 - -## 対象範囲 - -- backend: -- frontend: -- docs: -- migration: - -## やること - -- [ ] - -## 受け入れ条件 - -- [ ] - -## 実行すべき確認 - -- [ ] `cd backend && bundle exec rspec` -- [ ] `cd frontend && npm run build` -- [ ] `cd frontend && npm run lint` - -## 禁止事項 - -- unrelated refactor はしない -- 既存 API response shape を壊さない -- 認証・認可・BAN を弱めない - -## Codex への指示 - -この issue を読んで実装してください。 -不明点があれば、実装前に調査結果と選択肢を提示してください。 diff --git a/backend/app/controllers/post_versions_controller.rb b/backend/app/controllers/post_versions_controller.rb index cf07119..963fee3 100644 --- a/backend/app/controllers/post_versions_controller.rb +++ b/backend/app/controllers/post_versions_controller.rb @@ -100,18 +100,9 @@ class PostVersionsController < ApplicationController def external_tag_names_for snapshots ids = snapshots.flatten.filter_map { _1['external_tag_id'] }.uniq - names = ExternalTag.where(id: ids).pluck(:id, :platform, :name).to_h { |id, platform, name| + ExternalTag.where(id: ids).pluck(:id, :platform, :name).to_h { |id, platform, name| [id, "#{ platform }:#{ name }"] } - missing_ids = ids - names.keys - - HistoryAccess.visible(NicoTagVersion.all) - .where(tag_id: missing_ids) - .order(:tag_id, version_no: :desc) - .pluck(:tag_id, :name) - .each { |id, name| names[id] ||= name } - - names end def snapshot_tag_literals snapshots, tag_names, external_tag_names diff --git a/backend/app/models/external_tag.rb b/backend/app/models/external_tag.rb index a1f03a2..16f368b 100644 --- a/backend/app/models/external_tag.rb +++ b/backend/app/models/external_tag.rb @@ -1,8 +1,11 @@ class ExternalTag < ApplicationRecord before_create { SharedResourceIdAllocator.assign!(self) } + # Must run before dependent association callbacks. + before_destroy :prevent_destroy enum :platform, nico: 'nico' validates :platform, presence: true, inclusion: { in: ExternalTag.platforms.keys } + validate :immutable_identity, on: :update has_many :post_external_tags, dependent: :delete_all has_many :posts, through: :post_external_tags @@ -16,11 +19,21 @@ class ExternalTag < ApplicationRecord has_many :linked_tags, through: :nico_tag_relations, source: :tag - def snapshot_linked_tag_names language_code = 'ja' - linked_tags - .joins(:tag_names) - .where(tag_names: { language_code:, primary_flg: true }) - .order('tag_names.name') - .pluck('tag_names.name') + def snapshot_linked_tag_ids = linked_tags.distinct.order(:id).pluck(:id) + + def delete + raise ActiveRecord::ReadOnlyRecord, '外部タグは削除できません.' + end + + private + + def immutable_identity + errors.add(:platform, 'は変更できません.') if will_save_change_to_platform? + errors.add(:name, 'は変更できません.') if will_save_change_to_name? + end + + def prevent_destroy + errors.add(:base, '外部タグは削除できません.') + throw(:abort) end end diff --git a/backend/app/models/history_access.rb b/backend/app/models/history_access.rb index 96203da..3500f50 100644 --- a/backend/app/models/history_access.rb +++ b/backend/app/models/history_access.rb @@ -106,21 +106,7 @@ class HistoryAccess < ActiveSupport::CurrentAttributes snapshot_reference(TagVersion, row), snapshot_reference(NicoTagVersion, row, external: true)] when 'nico_tag_versions' - # Legacy linked_tags snapshots contain names, not IDs. Match recorded names - # conservatively; never expose a private linked name through an external tag. - names = "JSON_MERGE_PRESERVE(JSON_ARRAY(private_tag.name), " + - "COALESCE(JSON_EXTRACT(private_tag.names, '$**.name'), JSON_ARRAY()))" - # DISTINCT prevents merging this independent name set into the correlated scan. - # Keep byte-distinct names even when the column collation considers them equal. - private_names = 'SELECT DISTINCT private_name.name, ' + - 'CAST(private_name.name AS BINARY) AS name_bytes FROM tag_versions private_tag ' + - "JOIN (#{ discarded_keys('tag_versions', 'tag_id') }) blocked " + - 'ON blocked.tag_id = private_tag.tag_id ' + - "JOIN JSON_TABLE(#{ names }, '$[*]' COLUMNS (name TEXT PATH '$')) private_name " + - "WHERE private_name.name <> ''" - ["NOT EXISTS (SELECT 1 FROM (#{ private_names }) private_name WHERE " + - "LOCATE(CONCAT(' ', private_name.name, ' '), " + - "CONCAT(' ', COALESCE(#{ row }.linked_tags, ''), ' ')) > 0)"] + [list_reference('tag_versions', 'tag_id', "#{ row }.linked_tag_ids")] when 'tag_versions' [list_reference('tag_versions', 'tag_id', "#{ row }.parent_tag_ids")] when 'tag_description_versions' diff --git a/backend/app/models/nico_tag_version.rb b/backend/app/models/nico_tag_version.rb index e83ecff..76358c9 100644 --- a/backend/app/models/nico_tag_version.rb +++ b/backend/app/models/nico_tag_version.rb @@ -3,5 +3,10 @@ class NicoTagVersion < ApplicationRecord belongs_to :external_tag, foreign_key: :tag_id, inverse_of: :nico_tag_versions - validates :name, presence: true + validates :event_type, inclusion: { in: ['create', 'update'] } + + def name + tag = external_tag + "#{ tag.platform }:#{ tag.name }" if tag + end end diff --git a/backend/app/representations/counts_version_repr.rb b/backend/app/representations/counts_version_repr.rb index 8073b7b..a1ef12b 100644 --- a/backend/app/representations/counts_version_repr.rb +++ b/backend/app/representations/counts_version_repr.rb @@ -139,6 +139,8 @@ class CountsVersionRepr result = {} table = model.table_name comparison = @inclusive ? '<=' : '<' + name = model == NicoTagVersion ? + CountsResourceDefinition.new('nico_tags').expression('name', table) : 'name' requests.each_slice(100) do |batch| queries = batch.map do |id, point| scope = HistoryAccess.visible(model.where(tag_id: id)) @@ -148,7 +150,8 @@ class CountsVersionRepr "AND newer.version_no > #{ table }.version_no)", point) quoted = model.connection.quote(point.iso8601(6)) names = model == TagVersion ? 'names' : 'JSON_OBJECT() AS names' - scope.select("tag_id, name, #{ names }, #{ quoted } AS reference_point").to_sql + scope.select("tag_id, #{ name } AS name, #{ names }, " + + "#{ quoted } AS reference_point").to_sql end next if queries.empty? diff --git a/backend/app/services/counts_candidates.rb b/backend/app/services/counts_candidates.rb index ded9d1d..9f5a548 100644 --- a/backend/app/services/counts_candidates.rb +++ b/backend/app/services/counts_candidates.rb @@ -28,16 +28,18 @@ class CountsCandidates [external, 'external', 'NULL', 'JSON_OBJECT()']] sources = sources.map do |population, kind, category, names| scope = population.scope.except(:select) + name = population.definition.expression('name') if @params['query'].present? pattern = "%#{ ApplicationRecord.sanitize_sql_like(@params['query']) }%" - scope = scope.where("(v.name LIKE ? OR CAST(v.tag_id AS CHAR) = ? OR EXISTS (" + + scope = scope.where("(#{ name } LIKE ? OR CAST(v.tag_id AS CHAR) = ? OR EXISTS (" + "SELECT 1 FROM JSON_TABLE(COALESCE(JSON_EXTRACT(#{ names }, '$**.name'), " + "JSON_ARRAY()), '$[*]' COLUMNS (name TEXT PATH '$')) candidate_name " + 'WHERE candidate_name.name LIKE ?))', pattern, @params['query'], pattern) end scope.select(Arel.sql( "CONCAT('#{ kind }:', v.tag_id) AS value, v.tag_id AS id, " + - "v.name AS name, #{ names } AS names, #{ category } AS category, '#{ kind }' AS kind")) + "#{ name } AS name, #{ names } AS names, " + + "#{ category } AS category, '#{ kind }' AS kind")) .to_sql end clauses = [] diff --git a/backend/app/services/counts_population.rb b/backend/app/services/counts_population.rb index 73260e9..32143ca 100644 --- a/backend/app/services/counts_population.rb +++ b/backend/app/services/counts_population.rb @@ -113,7 +113,7 @@ class CountsPopulation else raise ArgumentError unless definition.axes(history).include?(key) - scope = scope.where(definition.expression(key) => entry['value']) + scope = scope.where("#{ definition.expression(key) } <=> ?", entry['value']) end scope end diff --git a/backend/app/services/counts_resource_definition.rb b/backend/app/services/counts_resource_definition.rb index 448c3b2..82a37bc 100644 --- a/backend/app/services/counts_resource_definition.rb +++ b/backend/app/services/counts_resource_definition.rb @@ -105,12 +105,25 @@ class CountsResourceDefinition def time_column = 'created_at' def column(key) raise ArgumentError unless fields.key?(key) + # Keep the V1 Counts field name at the API boundary only. + return 'linked_tag_ids' if resource == 'nico_tags' && key == 'linked_tags' { 'resource_id' => keys.first, 'user' => 'created_by_user_id' }.fetch(key, key) end - def expression(key, row = 'v') = "#{ row }.#{ column(key) }" - def changes = fields.keys - COMMON.keys - ['updated_at', 'updated_by_user_id'] + def expression(key, row = 'v') + if resource == 'nico_tags' && key == 'name' + return "(SELECT CONCAT(external_tags.platform, ':', external_tags.name) " + + "FROM external_tags WHERE external_tags.id = #{ row }.tag_id)" + end + + "#{ row }.#{ column(key) }" + end + + def changes + columns = fields.keys - COMMON.keys - ['updated_at', 'updated_by_user_id'] + resource == 'nico_tags' ? columns - ['name'] : columns + end def axes(history) fields.reject { |_key, type| type == :json }.keys + (resource == 'posts' ? ['tags'] : []) + (history ? ['changed_field'] : []) @@ -118,7 +131,9 @@ class CountsResourceDefinition def metadata fields.map do |key, type| - { key:, type:, nullable: model.columns_hash.fetch(column(key)).null, + nullable = (resource == 'nico_tags' && key == 'name') || + model.columns_hash.fetch(column(key)).null + { key:, type:, nullable:, operators: OPERATORS.fetch(type, []) } end end diff --git a/backend/app/services/counts_state_query.rb b/backend/app/services/counts_state_query.rb index b141718..77abb8f 100644 --- a/backend/app/services/counts_state_query.rb +++ b/backend/app/services/counts_state_query.rb @@ -201,6 +201,8 @@ class CountsStateQuery end def listing + external_name = CountsResourceDefinition.new('nico_tags') + .expression('name', 'external_version') catalog = <<~SQL , tag_counts AS ( SELECT membership.kind, membership.tag_id, COUNT(*) AS post_count @@ -209,7 +211,7 @@ class CountsStateQuery SELECT 'internal' AS kind, tag_id, name, names, deprecated_at FROM tag_at WHERE event_type <> 'discard' UNION ALL - SELECT tc.kind, tc.tag_id, external_version.name, JSON_OBJECT() AS names, + SELECT tc.kind, tc.tag_id, #{ external_name } AS name, JSON_OBJECT() AS names, NULL AS deprecated_at FROM tag_counts tc LEFT JOIN tag_at internal_version diff --git a/backend/app/services/counts_table_query.rb b/backend/app/services/counts_table_query.rb index 1fcc66a..38eb9e0 100644 --- a/backend/app/services/counts_table_query.rb +++ b/backend/app/services/counts_table_query.rb @@ -182,6 +182,7 @@ class CountsTableQuery order = sort_sql { detail_expression(_1) } query = query.order(Arel.sql((order + ['v.id ASC']).join(', '))) .limit(limit).offset((page - 1) * limit) + query = query.preload(:external_tag) if @definition.resource == 'nico_tags' rows = query.to_a if ['posts', 'tags'].include?(@definition.resource) point = population.history ? population.at : @@ -486,6 +487,7 @@ class CountsTableQuery table = model.table_name scope = HistoryAccess.visible(model.where(tag_id: ids)).where('created_at <= ?', point) + scope = scope.includes(:external_tag) if model == NicoTagVersion bound = @connection.quote(point) if population.history && @definition.resource == 'posts' key = kind == 'internal' ? 'tag_id' : 'external_tag_id' diff --git a/backend/app/services/nico_tag_version_recorder.rb b/backend/app/services/nico_tag_version_recorder.rb index 149fb17..4fcb754 100644 --- a/backend/app/services/nico_tag_version_recorder.rb +++ b/backend/app/services/nico_tag_version_recorder.rb @@ -1,4 +1,5 @@ class NicoTagVersionRecorder < VersionRecorder + EVENT_TYPES = ['create', 'update'].freeze def self.record! external_tag:, event_type:, created_by_user: new(external_tag:, event_type:, created_by_user:).record! end @@ -14,8 +15,7 @@ class NicoTagVersionRecorder < VersionRecorder def record_key = :external_tag def snapshot_attributes - { name: "#{ @record.platform }:#{ @record.name }", - linked_tags: @record.snapshot_linked_tag_names.join(' ') } + { linked_tag_ids: @record.snapshot_linked_tag_ids.join(' ') } end def tracks_version_no_on_record? = false diff --git a/backend/db/migrate/20261108000000_add_linked_tag_ids_to_nico_tag_versions.rb b/backend/db/migrate/20261108000000_add_linked_tag_ids_to_nico_tag_versions.rb new file mode 100644 index 0000000..ac44b0e --- /dev/null +++ b/backend/db/migrate/20261108000000_add_linked_tag_ids_to_nico_tag_versions.rb @@ -0,0 +1,5 @@ +class AddLinkedTagIdsToNicoTagVersions < ActiveRecord::Migration[8.0] + def change + add_column :nico_tag_versions, :linked_tag_ids, :text + end +end diff --git a/backend/db/migrate/20261108010000_migrate_linked_tags_in_nico_tag_versions.rb b/backend/db/migrate/20261108010000_migrate_linked_tags_in_nico_tag_versions.rb new file mode 100644 index 0000000..2e1e8d4 --- /dev/null +++ b/backend/db/migrate/20261108010000_migrate_linked_tags_in_nico_tag_versions.rb @@ -0,0 +1,42 @@ +class MigrateLinkedTagsInNicoTagVersions < ActiveRecord::Migration[8.0] + class MigrationTagVersion < ActiveRecord::Base + self.table_name = 'tag_versions' + end + + class MigrationNicoTagVersion < ActiveRecord::Base + self.table_name = 'nico_tag_versions' + end + + def up + # 除票外部タグの恢復はないので除票履歴があれば全消しでよいはず. + execute <<~SQL + DELETE + t1 + FROM + nico_tag_versions t1 + INNER JOIN + nico_tag_versions t2 + ON + t1.tag_id = t2.tag_id + AND t2.event_type = 'discard' + SQL + + MigrationNicoTagVersion.find_each do |ntv| + linked_tag_ids = ntv.linked_tags.split.map { |name| + # 厳密には一意ぢゃないけどめんだぅだからこれでいいや. + MigrationTagVersion + .order(created_at: :desc, id: :desc) + .find_by("names ->> '$.ja.name' = ?", name) + &.tag_id + }.compact.uniq.sort.join(' ') + + ntv.update_columns(linked_tag_ids:) + end + + change_column_null :nico_tag_versions, :linked_tag_ids, false + end + + def down + raise ActiveRecord::IrreversibleMigration, '戻せません.' + end +end diff --git a/backend/db/migrate/20261108020000_remove_linked_tags_from_nico_tag_versions.rb b/backend/db/migrate/20261108020000_remove_linked_tags_from_nico_tag_versions.rb new file mode 100644 index 0000000..ebb4d40 --- /dev/null +++ b/backend/db/migrate/20261108020000_remove_linked_tags_from_nico_tag_versions.rb @@ -0,0 +1,6 @@ +class RemoveLinkedTagsFromNicoTagVersions < ActiveRecord::Migration[8.0] + def change + remove_column :nico_tag_versions, :linked_tags + remove_column :nico_tag_versions, :name + end +end diff --git a/backend/db/schema.rb b/backend/db/schema.rb index bf72236..723fe34 100644 --- a/backend/db/schema.rb +++ b/backend/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do +ActiveRecord::Schema[8.0].define(version: 2026_11_08_020000) do create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -301,10 +301,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_10_15_010000) do t.bigint "tag_id", null: false t.integer "version_no", null: false t.string "event_type", null: false - t.string "name", null: false - t.text "linked_tags", null: false t.datetime "created_at", null: false t.bigint "created_by_user_id" + t.text "linked_tag_ids", null: false t.index ["created_at"], name: "index_nico_tag_versions_on_created_at" t.index ["created_by_user_id", "created_at"], name: "index_nico_tag_versions_on_created_by_user_id_and_created_at", order: { created_at: :desc } t.index ["tag_id", "created_at"], name: "index_nico_tag_versions_on_tag_id_and_created_at", order: { created_at: :desc } diff --git a/backend/spec/models/external_tag_spec.rb b/backend/spec/models/external_tag_spec.rb index 3d0bd86..774398c 100644 --- a/backend/spec/models/external_tag_spec.rb +++ b/backend/spec/models/external_tag_spec.rb @@ -11,28 +11,63 @@ RSpec.describe ExternalTag, type: :model do expect(external.reload.name).to eq('raw tag[]') end - it 'deletes post associations without deleting posts or version history' do + it 'does not allow a persisted external tag to change its platform or name' do + external = create(:external_tag, name: 'immutable') + + expect { external.update!(name: 'renamed') } + .to raise_error(ActiveRecord::RecordInvalid) + expect(external.errors[:name]).to include('は変更できません.') + + external.reload + external.platform = nil + expect { external.save! }.to raise_error(ActiveRecord::RecordInvalid) + expect(external.errors[:platform]).to include('は変更できません.') + + expect(external.reload).to have_attributes(platform: 'nico', name: 'immutable') + end + + it 'rejects delete, which bypasses destroy callbacks' do + external = create(:external_tag) + + expect { external.delete }.to raise_error(ActiveRecord::ReadOnlyRecord) + expect(external.reload).to be_persisted + end + + it 'increments the counter through post associations without changing identity' do + external = create(:external_tag, name: 'counter') + post = create(:post) + + expect { + PostExternalTag.create!(post:, external_tag: external) + }.to change { external.reload.post_count }.by(1) + + expect(external.reload).to have_attributes(platform: 'nico', name: 'counter') + end + + it 'does not destroy external tags or their post associations and history' do external = create(:external_tag) post = create(:post) PostExternalTag.create!(post:, external_tag: external) version = NicoTagVersionRecorder.record!( external_tag: external, event_type: :create, created_by_user: nil) - external.destroy! + expect { external.destroy! }.to raise_error(ActiveRecord::RecordNotDestroyed) - expect(PostExternalTag.where(external_tag_id: external.id)).to be_empty - expect(post.reload.external_tags).to be_empty + expect(external.reload).to be_persisted + expect(PostExternalTag.exists?(post:, external_tag: external)).to be(true) + expect(post.reload.external_tags).to contain_exactly(external) expect(version.reload.tag_id).to eq(external.id) end - it 'deletes external links without deleting linked internal tags' do + it 'does not destroy an external tag or its links to internal tags' do external = create(:external_tag) tag = create(:tag) - NicoTagRelation.create!(nico_tag: external, tag:) + relation = NicoTagRelation.create!(nico_tag: external, tag:) - external.destroy! + expect { external.destroy! }.to raise_error(ActiveRecord::RecordNotDestroyed) - expect(NicoTagRelation.where(nico_tag_id: external.id)).to be_empty + expect(external.reload).to be_persisted + expect(relation.reload).to be_persisted expect(Tag.exists?(tag.id)).to be(true) end end diff --git a/backend/spec/models/history_access_spec.rb b/backend/spec/models/history_access_spec.rb index c087c35..385e5cd 100644 --- a/backend/spec/models/history_access_spec.rb +++ b/backend/spec/models/history_access_spec.rb @@ -76,7 +76,7 @@ RSpec.describe 'Counts artifact policy and external references' do end end - it 'does not expose a private linked tag name through an external history snapshot' do + it 'does not expose a private linked tag ID through an external history snapshot' do tag = create(:tag, primary_name: 'private-linked-name') TagVersion.create!(tag:, version_no: 1, event_type: 'discard', name: 'private-linked-name', category: 'general', aliases: '', parent_tag_ids: '', created_at: Time.utc(2024), @@ -84,7 +84,7 @@ RSpec.describe 'Counts artifact policy and external references' do auto_generated: false, aliases: [] } }) external = create(:external_tag, name: 'public-external') row = NicoTagVersion.create!(external_tag: external, version_no: 1, event_type: 'create', - name: 'nico:public-external', linked_tags: 'private-linked-name', + linked_tag_ids: tag.id.to_s, created_at: Time.utc(2024)) post = create(:post) snapshot = PostVersion.create!(post:, version_no: 1, event_type: 'create', @@ -99,7 +99,7 @@ RSpec.describe 'Counts artifact policy and external references' do expect(HistoryAccess.visible(NicoTagVersion.where(id: row.id))).to exist end public_row = NicoTagVersion.create!(external_tag: external, version_no: 2, event_type: 'update', - name: 'nico:public-external', linked_tags: 'private-linked-name-extra', + linked_tag_ids: '', created_at: Time.utc(2024, 7)) HistoryAccess.set(user: nil) do expect(HistoryAccess.visible(NicoTagVersion.where(id: public_row.id))).to exist diff --git a/backend/spec/models/shared_tag_ids_spec.rb b/backend/spec/models/shared_tag_ids_spec.rb index 378eebc..4aae92e 100644 --- a/backend/spec/models/shared_tag_ids_spec.rb +++ b/backend/spec/models/shared_tag_ids_spec.rb @@ -10,23 +10,22 @@ RSpec.describe 'Shared Tag and ExternalTag IDs', type: :model do expect(next_tag.id).to be > external.id end - it 'does not reuse physically deleted IDs in either table' do + it 'does not reuse IDs after internal deletion while external tags remain' do tag = create(:tag) external = create(:external_tag) TagName.where(tag_id: tag.id).delete_all tag.destroy! - external.destroy! + expect(ExternalTag.exists?(external.id)).to be(true) expect(create(:tag).id).to be > external.id expect(create(:external_tag).id).to be > external.id end - it 'retains existing overlapping IDs without renumbering on update or insert' do + it 'retains overlapping IDs after a tag update and a new allocation' do tag = create(:tag) external = create(:external_tag) external.update_columns(id: tag.id) tag.update!(category: :meta) - external.update!(name: 'updated_external') before_tag = tag.reload.attributes before_external = external.reload.attributes diff --git a/backend/spec/models/version_record_spec.rb b/backend/spec/models/version_record_spec.rb index 561c19b..cbb9267 100644 --- a/backend/spec/models/version_record_spec.rb +++ b/backend/spec/models/version_record_spec.rb @@ -47,24 +47,34 @@ RSpec.describe VersionRecord, type: :model do external_tag: nico_tag, version_no: 1, event_type: 'create', - name: "nico:#{ nico_tag.name }", - linked_tags: '', + linked_tag_ids: '', created_at: Time.current, created_by_user: nil ) expect { - version.update!(name: 'nico:changed') + version.update!(linked_tag_ids: '123') }.to raise_error(ActiveRecord::ReadOnlyRecord) end + + it 'rejects discard and restore versions for external tags' do + [:discard, :restore].each do |event_type| + version = NicoTagVersion.new( + external_tag: nico_tag, version_no: 1, event_type:, + linked_tag_ids: '', created_at: Time.current) + + expect(version).not_to be_valid + expect(version.errors[:event_type]).not_to be_empty + end + end + it 'prevents NicoTagVersion destroy' do version = NicoTagVersion.create!( external_tag: nico_tag, version_no: 1, event_type: 'create', - name: "nico:#{ nico_tag.name }", - linked_tags: '', + linked_tag_ids: '', created_at: Time.current, created_by_user: nil ) diff --git a/backend/spec/requests/counts_table_spec.rb b/backend/spec/requests/counts_table_spec.rb index 234092c..4f65e9d 100644 --- a/backend/spec/requests/counts_table_spec.rb +++ b/backend/spec/requests/counts_table_spec.rb @@ -19,7 +19,7 @@ RSpec.describe 'Counts table API', type: :request do names: { ja: { name: 'alpha', script_code: 'Jpan', auto_generated: false, aliases: [] } }, created_at: Time.utc(2023)) NicoTagVersion.create!(external_tag: external, version_no: 1, - event_type: 'create', name: 'beta', linked_tags: '', created_at: Time.utc(2023)) + event_type: 'create', linked_tag_ids: '', created_at: Time.utc(2023)) snapshots = [ { tag_id: tag.id, version_no: 1, name: 'alpha', category: 'general', sections: [] }, { external_tag_id: external.id }] @@ -50,6 +50,115 @@ RSpec.describe 'Counts table API', type: :request do def versions(result) = result.fetch('versions').map { _1.fetch('version_no') } + context 'nico link ID snapshots through the V1 field name' do + let(:base) do + super().merge(resource: 'nico_tags') + end + + before do + NicoTagVersion.create!(external_tag: external, version_no: 2, + event_type: 'update', linked_tag_ids: tag.id.to_s, + created_at: Time.utc(2024, 1, 1)) + NicoTagVersion.create!(external_tag: external, version_no: 3, + event_type: 'update', linked_tag_ids: '', + created_at: Time.utc(2024, 2, 1)) + end + + it 'returns ID values under linked_tags without exposing the physical column key' do + result = query + expect(versions(result)).to eq([1, 2, 3]) + expect(result['versions'].map { _1.fetch('linked_tags') }).to eq(['', tag.id.to_s, '']) + result['versions'].each do |row| + expect(row).not_to have_key('linked_tag_ids') + expect(row.fetch('name')).to eq('nico:beta') + end + end + + it 'preloads external names separately for aliased link-change details' do + statements = [] + listener = lambda do |_name, _start, _finish, _id, payload| + statements << payload[:sql] + end + ActiveSupport::Notifications.subscribed(listener, 'sql.active_record') do + result = query(axis('changed_field', 'linked_tags')) + expect(versions(result)).to eq([2, 3]) + expect(result.fetch('versions').map { _1.fetch('name') }) + .to eq(['nico:beta', 'nico:beta']) + end + + details = statements.find { _1.include?('AS counts_latest') } + expect(details).not_to be_nil + expect(details).not_to include('JOIN `external_tags`') + names = statements.select { _1.match?(/SELECT .* FROM `external_tags`/) } + expect(names.length).to eq(1) + end + + it 'advertises the legacy field, label and default columns only' do + get '/counts/definitions/nico_tags', params: { locale: 'ja' } + expect(response).to have_http_status(:ok) + body = response.parsed_body + ['fields', 'filter_fields', 'output_fields'].each do |collection| + keys = body.fetch(collection).map { _1.fetch('key') } + expect(keys).to include('linked_tags') + expect(keys).not_to include('linked_tag_ids') + end + field = body.fetch('output_fields').find { _1['key'] == 'linked_tags' } + expect(field.fetch('label')).to eq('counts:attributes.linked_tags') + body.fetch('axes').each_value do |keys| + expect(keys).to include('linked_tags') + expect(keys).not_to include('linked_tag_ids') + end + body.fetch('defaults').select { _1['mode'] == 'details' }.each do |entry| + settings = entry.fetch('params').to_h { [_1.fetch('key'), _1.fetch('value')] } + expect(settings.fetch('columns').split).to include('name', 'linked_tags') + expect(settings.fetch('columns').split).not_to include('linked_tag_ids') + end + end + + it 'filters IDs and legacy drills and compares link changes through linked_tags' do + predicate = { conditions: '', 'conditions.0.field' => 'linked_tags', + 'conditions.0.operator' => 'eq', 'conditions.0.value' => tag.id.to_s } + expect(versions(query(predicate))).to eq([2]) + expect(versions(query(axis('linked_tags', tag.id.to_s, root: 'drill')))).to eq([2]) + expect(versions(query(axis('changed_field', 'linked_tags')))).to eq([2, 3]) + expect(versions(query(predicate.merge('conditions.0.value' => 'alpha')))).to be_empty + end + + it 'sorts ID snapshots through the public field in both directions' do + expect(versions(query(sort: 'linked_tags:asc version_no:asc'))).to eq([1, 3, 2]) + expect(versions(query(sort: 'linked_tags:desc version_no:asc'))).to eq([2, 1, 3]) + end + + it 'restores historical link IDs before applying a latest snapshot filter' do + predicate = { definition: 'latest', at: '2024-01-15T00:00:00Z', + conditions: '', 'conditions.0.field' => 'linked_tags', + 'conditions.0.operator' => 'eq', 'conditions.0.value' => tag.id.to_s } + expect(versions(query(predicate))).to eq([2]) + expect(versions(query(predicate.except(:at)))).to be_empty + end + + it 'groups by linked_tags ID values and accepts the resulting drill condition' do + sign_in_as(create(:user, :admin)) + result = query(mode: 'summary', rows: 'linked_tags', cols: '', sort: '') + expect(result.fetch('cells')).to match_array([ + { 'values' => [''], 'count' => 2 }, + { 'values' => [tag.id.to_s], 'count' => 1 }]) + expect(versions(query(axis('linked_tags', tag.id.to_s)))).to eq([2]) + end + + it 'rejects the physical column as a public filter, axis, sort or change field' do + sign_in_as(create(:user, :admin)) + [ + axis('linked_tag_ids', tag.id.to_s), + axis('changed_field', 'linked_tag_ids'), + { mode: 'summary', rows: 'linked_tag_ids', cols: '', sort: '' }, + { sort: 'linked_tag_ids:asc' }].each do |invalid| + get '/counts/query', params: base.merge(invalid) + expect(response).to have_http_status(:bad_request), invalid.inspect + end + end + end + it 'identifies a totals timeout without exposing SQL in the error response' do allow_any_instance_of(CountsPivotTotals).to receive(:call) .and_raise(CountsPivotTotals::TimedOut, 'private SQL') @@ -223,11 +332,10 @@ RSpec.describe 'Counts table API', type: :request do expect(versions(query(tags: 'renamed-alpha'))).to eq([1, 2, 4]) end - it 'resolves external names from current registrations too' do - external.update!(name: 'renamed-beta') - expect(versions(query(tags: 'nico:renamed-beta'))).to eq([1]) - expect(versions(query(tags: 'nico:beta'))).to eq([]) - expect(versions(query(tags: 'not:nico:renamed-beta'))).to eq([2, 3, 4]) + it 'resolves immutable external names across historical snapshots' do + expect(versions(query(tags: 'nico:beta'))).to eq([1]) + expect(versions(query(tags: 'nico:renamed-beta'))).to eq([]) + expect(versions(query(tags: 'not:nico:beta'))).to eq([2, 3, 4]) end end diff --git a/backend/spec/requests/nico_tags_spec.rb b/backend/spec/requests/nico_tags_spec.rb index a554d26..264f844 100644 --- a/backend/spec/requests/nico_tags_spec.rb +++ b/backend/spec/requests/nico_tags_spec.rb @@ -38,12 +38,9 @@ RSpec.describe 'NicoTags', type: :request do end it 'filters by nico tag name, linked tag name, and link status' do - linked = create(:external_tag) - linked.update!(name: 'search_linked') - unlinked = create(:external_tag) - unlinked.update!(name: 'search_unlinked') - other = create(:external_tag) - other.update!(name: 'other') + linked = create(:external_tag, name: 'search_linked') + unlinked = create(:external_tag, name: 'search_unlinked') + other = create(:external_tag, name: 'other') destination = create(:tag, category: :general) primary_tag_name_for(destination, 'ja').update!(name: 'destination_search') NicoTagRelation.create!(nico_tag: linked, tag: destination) @@ -80,11 +77,9 @@ RSpec.describe 'NicoTags', type: :request do end it 'sorts by name and timestamps' do - older = create(:external_tag) - older.update!(name: 'ordered_a') + older = create(:external_tag, name: 'ordered_a') older.update_columns(created_at: 2.days.ago) - newer = create(:external_tag) - newer.update!(name: 'ordered_b') + newer = create(:external_tag, name: 'ordered_b') newer.update_columns(created_at: 1.day.ago) older_post_tag = PostExternalTag.create!(post: create(:post), external_tag: older) @@ -189,9 +184,8 @@ RSpec.describe 'NicoTags', type: :request do versions = nico_tag.reload.nico_tag_versions.order(:version_no) expect(versions.map(&:event_type)).to eq(['create', 'update']) - expect(versions.last.linked_tags.split).to match_array([ - 'nico_linked_a', - 'nico_linked_b']) + expect(versions.last.linked_tag_ids.split.map(&:to_i)) + .to match_array([linked_a.id, linked_b.id]) expect(versions.last.created_by_user_id).to eq(admin.id) end @@ -210,7 +204,7 @@ RSpec.describe 'NicoTags', type: :request do expect(json).to eq([]) expect(nico_tag.reload.linked_tags).to be_empty expect(nico_tag.nico_tag_versions.order(:version_no).last) - .to have_attributes(linked_tags: '', created_by_user: member) + .to have_attributes(linked_tag_ids: '', created_by_user: member) end it 'returns the tags field error when a nico tag is specified directly' do diff --git a/backend/spec/requests/post_history_privacy_spec.rb b/backend/spec/requests/post_history_privacy_spec.rb index 635fb76..ff9df89 100644 --- a/backend/spec/requests/post_history_privacy_spec.rb +++ b/backend/spec/requests/post_history_privacy_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'Post history privacy', type: :request do created_at: origin + index.hours) end NicoTagVersion.create!(external_tag: external, version_no: 1, event_type: 'create', - name: 'nico:external-history-tag', linked_tags: 'private-history-tag', + linked_tag_ids: tag.id.to_s, created_at: origin) ['create', 'discard', 'restore', 'update'].each_with_index do |event, index| snapshots = diff --git a/backend/spec/services/counts_export_spec.rb b/backend/spec/services/counts_export_spec.rb index 94b82c1..b890323 100644 --- a/backend/spec/services/counts_export_spec.rb +++ b/backend/spec/services/counts_export_spec.rb @@ -22,6 +22,55 @@ RSpec.describe 'Counts export contracts' do .and_return(@counts_export_dir) end + context 'nico link IDs with the V1 export field name' do + let(:tag) { create(:tag, primary_name: 'linked export tag') } + let(:external) { create(:external_tag, name: 'external export tag') } + let(:payload) do + super().merge('resource' => 'nico_tags', 'columns' => 'linked_tags', + 'headers' => '["linked_tags"]') + end + + before do + NicoTagVersion.create!(external_tag: external, version_no: 1, + event_type: 'create', linked_tag_ids: '', created_at: Time.utc(2023)) + NicoTagVersion.create!(external_tag: external, version_no: 2, + event_type: 'update', linked_tag_ids: tag.id.to_s, created_at: Time.utc(2024)) + end + + it 'exports recorded IDs with the legacy JSON key and metadata' do + result = JSON.parse(CountsTableExport.new(payload).body.to_a.join) + expect(result.fetch('data')).to eq([ + { 'linked_tags' => '' }, { 'linked_tags' => tag.id.to_s }]) + expect(result.fetch('metadata').fetch('columns')).to eq(['linked_tags']) + keys = result.fetch('metadata').fetch('fields').map { _1.fetch('key') } + expect(keys).to include('linked_tags') + expect(keys).not_to include('linked_tag_ids') + end + + it 'exports the selected legacy CSV header and ID values' do + body = CountsTableExport.new(payload.merge('format' => 'csv')).body.to_a.join + expect(CSV.parse(body.delete_prefix("\uFEFF"))) + .to eq([['linked_tags'], [''], [tag.id.to_s]]) + end + + it 'keeps the legacy link column in default JSON exports' do + options = payload.except('columns', 'headers') + result = JSON.parse(CountsTableExport.new(options).body.to_a.join) + expect(result.fetch('metadata').fetch('columns')).to include('linked_tags') + result.fetch('data').each do |row| + expect(row).to have_key('linked_tags') + expect(row).not_to have_key('linked_tag_ids') + end + end + + it 'rejects the physical column as a selected CSV or JSON field' do + ['csv', 'json'].each do |format| + options = payload.merge('format' => format, 'columns' => 'linked_tag_ids') + expect { CountsTableExport.new(options) }.to raise_error(ArgumentError) + end + end + end + context 'serialization after the authorised query' do let(:rows) { [{ title: "=formula\n\"quoted\"" }, { title: 'second' }] } let(:page) do diff --git a/backend/spec/services/nico_tag_version_recorder_spec.rb b/backend/spec/services/nico_tag_version_recorder_spec.rb index e48745b..d6c961e 100644 --- a/backend/spec/services/nico_tag_version_recorder_spec.rb +++ b/backend/spec/services/nico_tag_version_recorder_spec.rb @@ -8,21 +8,23 @@ RSpec.describe NicoTagVersionRecorder do described_class.record!(external_tag:, event_type:, created_by_user: member) end - it 'records the external association and platform-qualified name' do + it 'records the external association and derives the platform-qualified name' do version = record(:create) expect(version).to have_attributes( external_tag:, version_no: 1, event_type: 'create', - name: 'nico:raw tag[]', linked_tags: '', created_by_user: member) + name: 'nico:raw tag[]', linked_tag_ids: '', created_by_user: member) expect(external_tag.reload.nico_tag_versions).to contain_exactly(version) end it 'uses the latest history number when the record has no version_no column' do first = record(:create) - external_tag.update!(name: 'changed') + tag = create(:tag) + NicoTagRelation.create!(nico_tag: external_tag, tag:) second = record(:update) - expect(second).to have_attributes(version_no: 2, name: 'nico:changed') + expect(second).to have_attributes(version_no: 2, name: 'nico:raw tag[]', + linked_tag_ids: tag.id.to_s) expect(first.reload.name).to eq('nico:raw tag[]') expect(external_tag.reload.has_attribute?(:version_no)).to be(false) end @@ -40,7 +42,7 @@ RSpec.describe NicoTagVersionRecorder do expect(version).to have_attributes( external_tag: external, name: 'registered_external:foo', version_no: 1, - event_type: 'create', linked_tags: '', created_by_user: member) + event_type: 'create', linked_tag_ids: '', created_by_user: member) end it 'returns the latest version without appending an unchanged snapshot' do @@ -64,13 +66,21 @@ RSpec.describe NicoTagVersionRecorder do expect(external_tag.nico_tag_versions).to contain_exactly(first) end - it 'records sorted linked internal names and later link removal' do + + it 'rejects discard and restore events for external tags' do + [:discard, :restore].each do |event_type| + expect { record(event_type) } + .to raise_error(ArgumentError, "Invalid event_type: #{ event_type }") + end + end + + it 'records sorted linked internal IDs and later link removal' do tags = ['z_link', 'a_link'].map { |name| create(:tag, primary_name: name, category: :general) } tags.each { |tag| NicoTagRelation.create!(nico_tag: external_tag, tag:) } - expect(record(:create).linked_tags).to eq('a_link z_link') + expect(record(:create).linked_tag_ids).to eq(tags.map(&:id).sort.join(' ')) external_tag.linked_tags = [tags.first] - expect(record(:update)).to have_attributes(version_no: 2, linked_tags: 'z_link') + expect(record(:update)).to have_attributes(version_no: 2, linked_tag_ids: tags.first.id.to_s) end end diff --git a/docs/SYSTEM_SPEC.md b/docs/SYSTEM_SPEC.md new file mode 100644 index 0000000..60439a0 --- /dev/null +++ b/docs/SYSTEM_SPEC.md @@ -0,0 +1,3366 @@ +--- +title: 'BTRC Hub / タグ広場 システム仕様書' +subtitle: 'タグ広場の完成形を定義する統合規範仕様' +date: '2026-10-30' +lang: ja-JP +toc: true +toc-depth: 3 +numbersections: false +--- + +# 0. 文書管理 + +## 0.1 本書の目的 + +本書は、BTRC Hub / タグ広場の完成形を定義するシステム仕様書である。 + +タグ広場を構成するドメイン、データ、API、画面、権限、履歴、公開制御、外部連携、非機能要件を一冊に統合し、設計・実装・運用判断の規範とする。 + +本書は特定時点の実装説明書ではない。実装済みか未実装かを本文では区別せず、システムとして満たすべき状態を記述する。ただし、完成形として採用すること自体が決まっていない事項は、確定仕様へ混入させず「未確定事項」に記載する。 + +## 0.2 正本 + +本書の正本はリポジトリに置く `SYSTEM_SPEC.md` とする。 + +物理 table 定義の正本は、同じリポジトリで管理する `tables.zip` とする。`tables.zip` は一 table ごとの column、型、NULL、default、constraint、index、および table 定義の理解に必要な短い備考を扱う。 + +本書は resource の責務、identity、履歴・権限・公開・UI・API その他の完成要件と設計 rationale を扱い、個別 table の具体的な column 構成を重複して正本化しない。物理 schema の具体値は `tables.zip`、domain requirement と rationale は本書をそれぞれ正とする。両者が意味上衝突する場合は推測で統合せず、設計判断として解消する。 + +同内容を別の場所へ掲載する場合、その文書は副本であり、内容が食い違ったときは上記の役割分担に従う。 + +課題、実装順序、担当、進捗、作業手順は Gitea で管理し、本書へ重複して記載しない。 + +## 0.3 本書へ記載しないもの + +次は本書の対象外とする。 + +- 実装済み・未実装の一覧。 +- P0、P1 等の実装優先順位。 +- 個別課題の作業分解。 +- Codex その他の実装支援者への指示。 +- RSpec、Vitest 等を用いた具体的なテスト手順。 +- 特定時点のソース、Wiki、DB ダンプの調査記録。 +- 特定時点の本番データ件数。 +- 個別 table の column 一覧、物理型、NULL / default、PK / FK / UNIQUE / CHECK、index その他、`tables.zip` が正本とする具体的 schema 定義。 +- 変更内容と無関係な資材更新記録。 + +これらが必要な場合は、`tables.zip`、Gitea、リポジトリ内の開発文書、運用記録または調査報告として管理する。 + +## 0.4 更新原則 + +本書は差分更新を原則とする。 + +- 全文を毎回生成し直さない。 +- 変更の影響を受ける章だけを編輯する。 +- 意味の変わらない言い換え、表の並べ替え、見出し変更を無目的に行わない。 +- 自動整形による全面的な差分を発生させない。 +- 既存仕様を削除または変更した場合は、末尾の変更履歴へ概要を残す。 +- 章番号は参照識別子としない。会話、課題、設計資料では章名、機能名、データ名または要件名を併記する。 +- 新章の追加に伴い後続章の番号が変わってもよい。 + +## 0.5 情報の整合 + +本書、ソースコード、Wiki、Gitea 課題、過去の設計会話その他の情報に矛盾がある場合、推測で統合してはならない。 + +矛盾は設計者へ確認し、その判断を最終的な正とする。過去の設計判断は原則として有効な情報源とするが、古い判断、暫定的な発言、設計全体との不整合が疑われる場合も同様に確認する。 + +## 0.6 規範語 + +| 表記 | 意味 | +| --- | --- | +| 必須 | システムが満たさなければならない | +| 禁止 | システムが行ってはならない | +| 推奨 | 原則として採用する。採用しない場合は理由を Gitea に残す | +| 任意 | 採用してよいが、システム成立の必須条件ではない | + +## 0.7 用語 + +| 用語 | 意味 | +| --- | --- | +| 投稿 | 外部コンテンツを表すリンク単位。外部コンテンツ本体は原則として保持しない | +| タグ | 投稿の分類、検索、説明、関聯付けの中核となる概念 | +| タグ名 | Tag に属する language ごとの名称。primary name、alias、自動生成名を含む | +| タグ説明 | タグの意味、用法、背景を記載する説明文書 | +| ヘルプ | タグに直接対応しない、タグ広場の利用・運用・概念を説明する文書 | +| 素材 | タグに関聯付けられたファイルまたは外部 URL | +| 局所記載 | 動画・音声等の一部分に限定してタグを記載すること | +| 公開 | URL を知っていれば通常の権限で閲覧できる状態 | +| 公表 | 利用者へ明示的に案内し、一般利用を促す状態 | +| 除票 | 投稿を通常の公開面から削除相当として外し、履歴だけを保持する状態 | +| 索引抑止 | 外部検索エンジンへ索引させない状態。閲覧禁止とは異なる | +| 内部検索抑止 | タグ広場内の通常検索、一覧、候補から外す状態 | +| 公開抑止 | 通常利用者に内容を閲覧させない状態 | +| 廃止タグ | 新規付与を禁止したタグ。索引抑止や物理削除とは異なる | +| 正式名 | 一つの Tag を特定 language で代表する primary Tag Name | +| 別名 | 同一 Tag・同一 language の primary name 以外の Tag Name。直接 Tag へ解決される | +| スナップショット | ある時点の状態を、後続の変更から独立して復元できる形で保存した履歴 | +| event | 利用者レベルの一つの論理操作。複数リソースへ波及しても一つの操作として履歴上で束ねる | +| 表示 locale | UI の language と必要な presentation script を表す URL・表示上の locale | +| Reading | Tag Name に対する発音・読み。発音 language と notation を伴う | +| 自動生成名 | 対応 language から Tag を解決可能にするため system が生成した Tag Name | +| 未公証 | `auto_generated = true` の Tag Name。日本語 UI では名称に `(未公証)` を付け、人間の公証により `auto_generated = false` となる | +| 転居届 | 同一の外部 content が別 URL へ移転した場合に Post URL を変更するための申請。admin の受理により確定し、admin 自身もセルフ受理を含め同じ経路を用いる | +| タググループ | Tag および他の Tag Group を人為的なまとまりとして整理する、Tag domain 配下の collection resource | +| 代表素材 | Tag の概念を具体的・視覚的に示すため、その Tag が参照する Material。Material 自身の所属 Tag とは別概念 | +| 素材同期元 | 外部素材集から Material を取り込むために巡回する root。初期 V2 では Google Drive folder を対象とする | +| 素材取込元 | Material がどの同期元のどの file / path から取り込まれたかを表す現在状態兼来歴情報 | +| canonical export path | file Material を ZIP / Git その他の共通配布 tree のどこへ配置するかを表す、サイト内共通の一意な path | +| 集計表(Counts) | 正本の現在状態・immutable history から条件に従って集計表または明細表を生成する横断的な照会機能。独立した content resource ではない | +| 集計軸 | 表の行・列を分割する属性または日時粒度。値の表示と計数の権限規則を別々に持つ | +| 集計セル | 指定した行・列軸値の組合せに対応する件数。小計・合計は通常セルに定義した計数規則に従う | + + +## 0.8 「考え方・理由」の扱い + +各章および主要 resource に置く「考え方・理由」は、単なる理念・推奨事項ではなく、直前の具体的な仕様をなぜ採用したかを説明する設計 rationale とする。 + +- 個別の「考え方・理由」は、対応する要件を特定できる形で、「A だから B とする」「C を避けるため D とする」「E ではなく F を採るのは G のためである」のように、仕様と理由の因果関係を明示する。 +- 「一貫性を重視する」「分かりやすくする」「拡張しやすくする」その他の一般理念だけを、個別仕様の理由として置いてはならない。system-wide に共通する思想は共通原則の節へ置き、各 resource / feature の理由欄にはその項目固有の理由を書く。 +- 個別要件の文言だけでは複数の解釈が成立する場合、「考え方・理由」と整合する解釈を優先する。 +- 実装上の都合だけを理由に、「考え方・理由」が守ろうとしている domain boundary、identity、履歴性、命名原則その他を無言で崩してはならない。 +- 「考え方・理由」と確定要件が矛盾する場合は、どちらかを推測で優先せず設計者へ確認する。 +- 確定仕様を変更して設計判断を撤回する場合は、対応する「考え方・理由」も同時に更新し、変更履歴へ理由を残す。要件だけを書き換えて rationale を放置してはならない。 +- 新しい主要 resource、table、relation、共通 UI pattern を追加する場合は、その構造を採用した理由、避けたい代表的な代替、将来変更してよい境界を必要な範囲で記載する。 +- 本書の確定要件は本書だけで理解できなければならない。特定時点の source code、過去の画面、外部 service の UI その他を見なければ意味が成立しない「同じ挙動」「踏襲する」等の参照表現を規範要件として用いない。設計検討で別実装を参照した場合も、採用する挙動・能力を具体的な完成形仕様へ書き起こす。 +- 未確定事項では、採否を判断するための比較材料として「V1 では〜」「Danbooru では〜」等の参照記述を使用してよい。ただし、それらは完成要件ではなく調査・比較材料である。採用を確定して本文へ昇格するときは、参照先を知らなくても意味が閉じる具体的な完成形仕様へ書き換える。 + +この原則により、仕様の選択理由と system-wide な思想を混同せず、本書単体から「何を満たすか」と「なぜそうするか」を追跡できる状態を保つ。 + +## 0.9 日本語の漢字表記 + +タグ広場が所有する日本語 UI、文書、message、label その他の日本語表記は、原則として《当用漢字への書きかえ》に従わない。戦後の書換えによって別字・別語へ置換された語について、タグ広場で用いる語形が定まっている場合はその語形を正本とする。 + +- `編輯` を正本とし、その戦後書換え形をタグ広場が所有する日本語では用いない。`共同編輯`、`編輯画面`、`編輯権限` その他の複合語も同じ原則に従う。 +- `関聯` を正本とし、その戦後書換え形をタグ広場が所有する日本語では用いない。`関聯付け`、`関聯 resource` その他の複合語も同じ原則に従う。 +- 外部 service・規格・作品・固有名称、引用文、利用者が入力した原文その他、タグ広場が表記を所有しない文字列は原表記を保持してよい。 +- 英語 identifier、DB / API 名、外部 protocol の固定名には本節を適用しない。 +- 未登録の語について現代表記へ機械的に書き換えず、必要に応じて project vocabulary として正本表記を定める。単に字体を古く見せることを目的とした一括置換は行わない。 + +## 0.10 考え方・理由 + +本書を特定時点の実装説明ではなく完成形の規範仕様とするのは、migration 中の一時実装や Issue の進捗が完成要件へ混入すると、将来それが恒久仕様と誤認されるためである。rationale を仕様書内へ残すのは、完成形の選択理由は実装順序とは異なり、将来の設計変更時にも判断材料として必要だからである。 + +したがって、progress / task は Gitea、完成形とその理由は本書、特定時点の調査結果は調査記録という役割分担を維持する。 + +# 1. システムの目的と適用範囲 + +## 1.1 システム概要 + +タグ広場は、ぼざろクリーチャー関聯コンテンツを外部プラットフォーム横断で収集し、タグ体系、説明、履歴、素材および検索を通じて整理・発見できる共同編輯型の知識基盤である。 + +システムの中心は投稿そのものではなく、投稿を横断して成立するタグ体系と、その体系に基づく知識の蓄積である。 + +## 1.2 目的 + +タグ広場は次を目的とする。 + +1. 外部プラットフォーム上の関聯コンテンツを横断的に発見できるようにする。 +2. 外部サービスのタグ数、カテゴリ、表示、履歴等の制約を超えたタグ体系を提供する。 +3. タグの language ごとの primary name、alias、Reading、カテゴリ、親子関係、説明および履歴を保持する。 +4. 投稿のタイトル、URL、サムネイル、元作成日時、タグ、親投稿その他の変化を履歴として保持する。 +5. タグに関聯する素材を整理し、参照、配布、同期できるようにし、界隈で流通してきた素材集と同等以上の検索・選択・配布・制作利用を Web 上で提供する。 +6. 外部サービスとの同期を通じて投稿候補と外部タグ情報を取り込む。 +7. 問題のある情報を、物理削除だけに依存せず、廃止、除票、索引抑止、内部検索抑止、公開抑止として適切に制御する。 +8. API を通じて、タグ広場のデータを他のアプリケーションから安全に活用できるようにする。 + +## 1.3 非目的 + +次はタグ広場の主目的ではない。 + +- 汎用 SNS。 +- 雑談掲示板。 +- 外部コンテンツ本体の無制限な再配布。 +- 動画配信サービス。 +- 汎用的な共同視聴サービス。 +- 投稿当てゲームその他のゲーム基盤。 +- メールアドレスとパスワードを前提とした一般的な認証サービス。 +- 誰でも無制限に編輯できる匿名 Wiki。 + +## 1.4 中核ドメイン + +| ドメイン | 責務 | +| --- | --- | +| Users | session-based authentication、recovery code、旧 system からの account migration、role、BAN、利用者設定 | +| Posts | 外部 URL、タイトル、サムネイル、元作成日時、タグ、親投稿、履歴 | +| Tags | Tag 実体、language ごとの Tag Name、Reading、カテゴリ、親子関係、Tag Group、廃止、外部タグ対応 | +| Descriptions | タグ説明、ヘルプ、履歴、差分、検索 | +| Materials | タグに関聯するファイル・URL、履歴、同期、配布 | +| Search | 投稿・タグ・説明・素材の検索、類似度、索引制御 | +| Sync | 外部サービスからの投稿・タグ・素材の取込み | +| Moderation | BAN、除票、抑止、監査 | +| API | 中核データの参照・編輯および外部活用の契約 | + +## 1.5 システム境界 + +タグ広場本体は、中核データを利用する個別アプリケーションが存在しなくても成立しなければならない。 + +個別アプリケーションはタグ広場の API、投稿、タグ、説明、履歴、類似度その他のデータを利用してよい。ただし、タグ広場の中核モデルを個別アプリケーション固有の事情へ従属させてはならない。 + + +## 1.6 考え方・理由 + +タグ広場の中心を Post 単体ではなく Tag 体系とするのは、外部 platform が変化・消滅しても、界隈で共有される意味・名称・説明・関係を横断的に残すことが system の価値だからである。Post、Material、Description、外部 application はこの Tag knowledge graph を利用する側として設計する。 + +外部 application を本体から分離するのも、上映会や game 固有要件によって Tag / Post の中核 schema が歪むことを避けるためである。 + +# 2. 技術構成と共通原則 + +## 2.1 Backend + +タグ広場は Backend と Frontend を別個のアプリケーションとして構成せず、SvelteKit を用いた単一の full-stack application とする。 + +| 項目 | 仕様 | +| --- | --- | +| 言語 | TypeScript | +| Framework | SvelteKit | +| DB | PostgreSQL 18 系を標準とする | +| ORM | Drizzle | +| 本番実行 | Node.js + SvelteKit Node adapter を標準とする | +| ファイル | S3 互換 object storage を標準とし、履歴から参照される object の寿命を現在状態から独立して管理する | +| server-side rendering | 初回アクセス、直リンク、再読込では完成した HTML を server-side で生成する | +| application logic | route 固有処理へ閉じ込めず、server-side の共通 domain / application service へ集約する | + +内部画面のためだけに REST endpoint を作成することを既定としない。SvelteKit の server load、form action その他の server-side 機構から共通 application logic を呼び出す。 + +外部 client へ公開する API は内部画面の実装から分離し、versioned contract として維持する。 + +## 2.2 Frontend + +| 項目 | 仕様 | +| --- | --- | +| UI | Svelte / SvelteKit | +| 言語 | TypeScript | +| style | Tailwind CSS を標準とする | +| app 内遷移 | 初回 SSR 後は SvelteKit の client navigation を用いる | +| HTTP client | 明示的な HTTP 通信が必要な箇所では Axios を利用してよい。内部画面の通常取得を HTTP API 呼出しへ固定しない | +| 端末内保存 | localStorage | +| Markdown | CommonMark と GFM 相当を扱える構成 | +| head 制御 | route ごとに title、locale、robots その他を SSR 時点で確定できる構成 | + +ページ全体を一つの JavaScript bundle へ無制限に集約してはならない。route 単位の code splitting と server-side data loading を前提とし、低メモリ端末でも初回表示とページ遷移が成立することを重視する。 + +再利用可能な見た目・操作・意味を持つ UI は、page ごとに style class の組合せを増やして再現するのではなく、原則として責務を持つ Svelte component として定義する。navigation / menu、button、Tag 表示、form control、tab、panel、pagination、context menu、state marker、日時入力その他の共通 UI pattern は component 側へ集約し、各 page に同じ utility class 群や one-off class を散在させない。Tailwind CSS は component 内部の実装手段として利用してよいが、style class の追加だけを共通 UI concept の代替としてはならない。 + +## 2.3 データ設計原則 + +### 2.3.1 現在状態 + +現在状態を表すテーブルは、重複を避け、整合性をデータ構造と制約で保証する。 + +- 通常の現在状態は第三正規形を基準とする。 +- 同じ関係の active 行を複数保持しない。 +- 現在状態の取得に、履歴行の解釈を要求しない。 +- 一意性、外部キー、check constraint で表現できる規則は DB でも保証する。 + +### 2.3.2 履歴 + +履歴は当時の事実を保存するスナップショットとする。 + +- 履歴行は作成後に更新または削除しない。 +- 各 `*_versions` レコードは、原則としてその一レコードだけで当該 resource 自身のその時点の状態を理解・復元できる自己完結スナップショットとする。 +- 現在の関聯テーブルを参照しなければ当時の表示を復元できない設計を避ける。 +- 名前、カテゴリ、関係、添付、表示用情報を当時値として保持する。 +- 現在状態から消えた対象も履歴上では表示できなければならない。 +- 履歴対象自身の状態を、単なる正規化のために別の履歴レコードへ分解してはならない。別 resource との歴史的関係を表すために immutable version を参照する場合も、その参照は当該 resource 自身の状態を成立させるための必須依存ではなく、関係先の当時状態、provenance または navigation を特定するためのものとすることを原則とする。 +- 履歴対象の状態が自己完結している限り、その状態を発生させた `event` その他の因果関係を参照してよい。 +- 履歴テーブル名は原則として `_versions` を用いる。ただし、利用者レベルの操作履歴は `events` とする。 + +### 2.3.3 削除 + +データの性質に応じて、廃止、論理削除、除票、公開抑止、物理削除を区別する。 + +単に画面から見えなくするために履歴を破壊してはならない。一方、履歴として残す合理性のない従属データ、法的に保持できないデータ、独立した意味を持たない中間データは、定められた規則に従って物理削除できる。 + + +### 2.3.4 データベース命名規則 + +データベースの table、column、constraint、relation の名称は、個別実装者の好みではなく system-wide な規則に従う。V2 で新設する schema には次を適用する。 + +#### table 名 + +- table 名は原則として英語の複数形 `snake_case` とする。例: `posts`、`tag_names`、`material_sources`。 +- UI 上の日本語名と DB 名は分離する。例えば UI 上の「一角」は DB では `pools` とし、表示上の語彙を table 名へ直訳して schema を揺らさない。 +- relation table は、owner / source 側 resource を先に置き、その relation の対象が分かる名称とする。例: `post_tags`、`pool_posts`、`tag_group_tags`。 +- 有向関係や単なる membership ではない関係は、二 resource 名の機械的結合ではなく意味を表す語を用いる。例: `tag_implications`。 +- polymorphic な `type + id` 形式や一つの member table へ異種 resource を押し込む設計は既定としない。型ごとの relation table で自然に表現できる場合は分離する。system-wide infrastructure で本当に異種 resource を同一契約として扱う必要がある場合だけ、理由を明記した例外を許可する。 +- current state と history を同一 table に混在させない。履歴 table は原則 `_versions` とする。 + +#### `id`、`*_id`、`*_code`、自然 key + +- `id` は**タグ広場内部で発行・管理する resource identifier** に限定する。 +- `_id` は、タグ広場内部の `.id` を参照する foreign key にだけ用いる。 +- Google Drive、Nico、YouTube その他、外部 system が「ID」と呼ぶ値を DB へ保存する場合でも、タグ広場内部 ID と混同させず `*_code` とする。例: `folder_code`、`file_code`。 +- protocol や外部公開 API の field 名が固定されている場合、境界上では外部名称を受け入れてよいが、タグ広場所有 DB schema へそのまま持ち込むことを必須としない。 +- stable な自然 key 自体が resource の identity であり、surrogate ID を参照する利益がない場合は `id` を機械的に追加しない。locale の stable code、Material Source の source + external file identity、Help article / content の stable identifier + language identity その他、確定した自然 identity はそのまま利用する。 +- 反対に、他 table から継続的に参照される独立 resource、短い内部参照が有用な resource、identity が外部値の変更に引きずられるべきでない resource には内部 `id` を持たせる。 +- `*_no` は resource identity ではなく、resource 内の順序・版番号・slot 番号等に用いる。例: `version_no`、`slot_no`。 + +#### column 名と意味 + +- column 名は英語 `snake_case` とし、同じ概念には system-wide で同じ語を使う。 +- タグ広場が所有する schema 名では British English の綴りを優先する。例: `normalise` / `normalisation` 相当。外部 protocol、library、既存 migration source の固定名称は例外とする。 +- 日本語の語を Latin alphabet の識別子へ romanisation する必要がある場合は、タグ広場内の正書法として Segsyoxafu を用いる。Segsyoxafu 上の綴りが確定していない語を推測で identifier に採用しない。 +- `name` は人間がその resource を意味上識別する名称に限定する。file name、source path、外部 object key その他を semantic `name` へ流用しない。 +- `code` は stable な machine-readable identifier / namespace value に用いる。`name` と `code` を同義に扱わない。 +- `path` は location / hierarchy を表し、resource の semantic identity を暗黙に兼ねさせない。source path と canonical export path のように責務が違う path は別 column とする。 +- generic な `data`、`value`、`type`、`key` 等だけで意味を隠す名称を避け、domain 上の役割を名称へ出す。discriminator 用の `type` を不要に追加しない。 +- JSON column であっても `_json` のような物理型 suffix を原則付けず、`names`、`members`、`sources` のような意味名を用いる。 +- 時間量は単位を名称に含める。動画・音声の時間は `*_ms`、byte 数は `byte_size` を標準とする。単なる count は `*_count` とする。 +- 日時 instant は `*_at` とする。lower / upper を別 column で保持する half-open interval は用途に応じて `*_from` / `*_before` を用い、`*_before` は上限を含まない。PostgreSQL Range 一値として保持する interval は `*_range` または単位を含む `range_ms` のように Range であることと用途が分かる名称を用いる。 + +#### boolean・状態・timestamp + +- boolean column を「とりあえずの flag」として追加しない。row の存在・不在で表現できる membership は relation row で表し、時刻を伴う状態遷移は `*_at`、三状態以上を持つ概念は enum / code その他の明示状態で表すことを優先する。 +- `enabled` のように「row が存在するのにさらに有効か」を重ねる column は、無効 row を保持する domain 上の理由がある場合だけ採用する。row の DELETE で十分なら boolean を持たない。 +- boolean を採用する場合は、その二値が resource 自身の独立した意味を持つことを説明できなければならない。例: primary / alias の区別、auto-generated provenance、暦月が閏月かどうか。 +- `_flg` は DB 上の二値であることを明示する必要がある既存・制約用途で使用できるが、新規 column で機械的に付けない。自然な predicate 名で意味が明確なら `auto_generated` のように表現してよい。 + +#### audit column と履歴 + +- `created_at`、`updated_at`、`created_by_user_id`、`updated_by_user_id` を全 table にテンプレートとして自動付与しない。 +- `created_at` は作成時刻そのものに運用・監査上の意味がある場合に持つ。 +- `updated_at` は current row が更新され、その最終更新時刻に意味がある場合だけ持つ。immutable resource / cache / append-only ledger に惰性で付けない。 +- `created_by_user_id` / `updated_by_user_id` は human action の帰属や差戻し・監査に必要な resource に持つ。admin-only configuration や system-generated cache など、利用者 attribution を利用しない resource に機械的に追加しない。 +- immutable な `*_versions` は作成後に変化しないため `updated_at` / `updated_by_user_id` を持たない。 +- version number は `.version_no` と `_versions.version_no` を対応させ、単に履歴 row の surrogate `id` を version number とみなさない。 +- immutable version table で resource identity と `version_no` の組合せ自体が一意な版 identity になる場合は、`(_id, version_no)` その他の自然な複合 primary key を用い、参照上の独立した必要がない surrogate `id` を追加しない。 + +#### constraint と identity + +- schema 名だけでなく、UNIQUE、foreign key、check constraint も domain identity を表現するために利用する。 +- composite identity が自然な relation / subordinate resource に surrogate `id` を追加して、同じ関係の重複を別途許す構造にしない。 +- application validation が必要な規則でも、DB で保証できる「最大一件」「XOR」「非 NULL」「一意性」等は可能な限り DB constraint と二重に保証する。 +- DB collation や provider 固有の挙動へ identity の意味を丸投げしない。Tag Name identity その他、application algorithm が正となるものはその境界を明記する。 + +### 2.3.5 PostgreSQL 物理設計 + +V2 の標準 DB は PostgreSQL 18 系とし、MySQL は V1 からの migration source としてのみ扱う。 + +- database encoding は UTF-8 とする。 +- database default locale / collation は built-in provider の `PG_UNICODE_FAST` とし、deterministic な Unicode code point 順を generic な比較・整列の基礎とする。自然言語としての表示順、Tag Name の registration identity、search equivalence は database default collation に委ねず、それぞれ locale-specific ICU / CLDR policy または application algorithm で扱う。 +- 通常の文字列は `text` を既定とし、MySQL の index 制約その他に由来する 191 / 255 等の長さを domain 上の根拠なく持ち込まない。真に意味のある最大長が存在する場合だけ `varchar(n)` または check constraint その他で上限を定める。 +- system が解釈・更新する structured JSON は `jsonb` を標準とする。外部 source の受信表現そのものを provenance として厳密に残す必要がある場合は、正規化される `jsonb` とは別に `text` または `bytea` で raw representation を保持する。 +- binary data は `bytea` を用いる。SHA-256 digest は `bytea` とし、`octet_length(...) = 32` の check constraint で固定長を保証する。 +- IP address は `inet` を用い、IPv4 / IPv6 を同じ column で扱う。BAN 対象は個別 host address とし、subnet ban として解釈しない。 +- 実世界の instant は `timestamptz` を用い、DB / session timezone は UTC を標準とする。純粋な暦日は `date` を用いる。`timestamptz` は original timezone 名または入力 offset の provenance を保持する型とはみなさない。 +- 通常の内部 resource `id` は `integer GENERATED ALWAYS AS IDENTITY` を標準とし、それを参照する foreign key も `integer` とする。全 resource の操作が集約される `events.id` だけは `bigint GENERATED ALWAYS AS IDENTITY` とし、`event_id` および event 自己参照も `bigint` とする。 +- schema-level で安定した有限集合は PostgreSQL native ENUM を利用できる。利用者・管理者が運用中に任意追加・削除する分類は ENUM にせず table / relation で表す。 +- 条件付き一意性は partial UNIQUE index を優先する。Tag Name の language ごとの primary 最大一件は `(tag_id, language_code)` に対し `primary_flg = true` の row だけを対象とする partial UNIQUE index で保証する。 +- Range が domain 上一つの atomic value を表す場合は PostgreSQL Range 型を利用できる。複数の独立区間を一 column へ集合として押し込む Multirange は、独立 row で表現できる relation の代替として用いない。 +- column の物理配置順は完成仕様の意味としない。MySQL 固有の `AFTER` 相当を migration 規則として設けない。 + +### 2.3.6 resource 設計の記述原則 + +主要 resource の仕様は、「何を持つか」だけでなく「なぜその resource にその責務を置くか」を記述する。 + +- resource 自身の identity と、外部 source identity、表示名、path、履歴 identity を区別する。 +- 一対一 relation を別 table にする場合は、その relation が独立した寿命・複数化可能性・権限・履歴を持つ等の理由を説明する。理由がない 1:1 table は current resource へ統合することを優先する。 +- 将来の可能性だけを理由に、現時点で使わない抽象化・provider type・flag・profile・polymorphic relation を先行して持たない。実際に要求が発生した時点で migration により拡張できることを前提とする。 +- 一方、現在の要件から独立性が明らかな概念を「table を減らすため」だけに current resource へ押し込まない。正規化は table 数の少なさではなく責務と identity の分離を目的とする。 +- UI の現在形だけで schema を決めず、履歴、API、migration、削除、外部 source、将来の再利用を含めて resource boundary を決める。 + +## 2.4 API 表現 + +- 外部公開 API の JSON は snake_case を標準とする。 +- application 内部の server load / action と UI component の間では TypeScript の型を共有し、同一データの型定義を Backend / Frontend で二重管理しない。 +- Frontend 内部で camelCase を使用してよいが、外部 API の契約とは明確に境界を分ける。 +- 日時は timezone を含む ISO 8601 とする。 +- 動画・音声の内部時間単位はミリ秒とする。 +- ID は契約上の識別子であり、表示順や意味を暗黙に表してはならない。 +- validation error、競合、権限不足、未発見を区別する。 +- 内部画面のための endpoint と外部公開 API を同一の都合で増減させない。 + +## 2.5 権限の一元化 + +role、BAN、公開状態、編輯可能性の判定を画面ごと・route ごとに複製してはならない。 + +同じ判定規則は server-side の共通 policy または application service に集約し、page load、form action、外部 API のいずれから呼ばれても同じ結果になるようにする。Frontend は server-side の判定結果を表示へ反映する。 + + +## 2.6 考え方・理由 + +SvelteKit full-stack の単一 application とするのは、画面と server-side logic で TypeScript の型・validation・domain contract を共有し、内部 UI のためだけに同じ data contract を HTTP API として二重管理する必要を減らすためである。外部公開 API だけを独立した versioned contract とすることで、内部画面の変更速度と外部互換性の寿命を分離する。 + +データ設計で current identity、history、relation、external source、binary identity の境界を分けるのは、それぞれの寿命・一意性・削除条件が異なるためである。一方、将来使うかもしれない抽象化を先行して増やさないのは、未使用の provider abstraction や flag が現在の domain identity を曖昧にすることを避けるためである。 + +# 3. 認証・ユーザ・設定 + +## 3.1 認証方式 + +V2 の恒常的な認証と、旧 system から V2 への account migration を分離する。旧 system の引継ぎコードまたは `localStorage.user_code` を、V2 の通常 request ごとの認証方式として引き継がない。 + +### 3.1.1 V2 の恒常認証 + +通常の画面表示では、SSR 時点で current user を確定できる cookie-based session authentication を標準とする。 + +1. server-side request では cookie / session から current user を解決する。 +2. SSR で解決済みの current user を、hydration 後の client state の正とする。 +3. client-side の localStorage その他に恒常的な user identity credential を置くことを通常認証の前提としない。 +4. session と別の credential が示す user が食い違う場合、無言で別 user へ切り替えない。 +5. 外部 API の認証に recovery code または旧 system の引継ぎコードを bearer credential として使用してはならない。外部 API は application identity、API credential その他の専用認証境界を用いる。 + +### 3.1.2 recovery code + +V2 は、新しい端末への引継ぎまたは session を失った場合の復旧手段として recovery code を持てる。 + +- recovery code は通常 request の認証 credential として毎回送信しない。 +- recovery code の検証に成功した場合、その端末へ新しい session を発行し、以後は session authentication を用いる。 +- recovery code は user ごとに一意でなければならない。 +- recovery code は更新でき、更新前の code は以後利用できない。 +- recovery code を URL、Referer、log、analytics その他へ露出させてはならない。 + +### 3.1.3 旧 system からの account migration + +旧 `hub.nizika.monster` と V2 の `btr.moe` は origin が異なるため、V2 側から旧 origin の `localStorage.user_code` または cookie を直接読み取ることを前提としてはならない。 + +旧 system からの migration は、旧 origin を migration gate として利用する。 + +1. 旧 origin は自身の `localStorage.user_code` その他の旧 credential を読み取り、旧 user を検証する。 +2. 検証に成功した場合、短寿命かつ一回限りの migration ticket を発行する。 +3. migration ticket は旧 user の identity を V2 へ引き渡すためだけに使用し、旧 `user_code` 自体を V2 へ恒常 credential として持ち込まない。 +4. V2 は migration ticket を server-side で検証し、対応する V2 user の session を発行する。 +5. 使用済みまたは期限切れの migration ticket は再利用できない。 +6. migration ticket、旧 `user_code`、recovery code その他の secret を query parameter へ含めない。migration ticket の引渡しは form POST その他、URL へ secret を残さない方法を用いる。 +7. migration ticket が提示された request では、その user の session 確立を guest 自動作成より優先する。 +8. V2 へ直接アクセスした利用者について、旧 origin に credential が存在するかを V2 が推測してはならない。移行期間中は「旧タグ広場の account を引き継ぐ」明示導線から旧 origin の migration gate へ移動できるようにする。 +9. 旧 origin へアクセスした利用者については、移行期間中、migration gate を経由した migration-aware redirect により V2 へ移動できる。単純な redirect により旧 credential を利用する機会を失わせない。 +10. migration 成功後は V2 session を恒常認証の正とし、旧 origin の credential に依存しない。 + +旧 system からの migration は移行期間の互換機構であり、V2 の恒常認証仕様へ組み込んではならない。 + +## 3.2 role + +| role | 基本権限 | +| --- | --- | +| guest | 公開情報の閲覧、閲覧済み状態、既存素材の更新その他の限定操作 | +| member | 投稿、タグ、タグ説明、ヘルプ、素材の共同編輯 | +| admin | role、BAN、除票、抑止、監査その他の管理操作 | + +member 以上の判定は共通化する。 + +## 3.3 権限表 + +| 操作 | guest | member | admin | +| --- | ---: | ---: | ---: | +| 公開投稿の閲覧 | 可 | 可 | 可 | +| 投稿の作成・更新 | 不可 | 可 | 可 | +| 投稿の除票・復帰 | 不可 | 不可 | 可 | +| 索引抑止等の申請 | 不可 | 可 | 可 | +| 抑止の確定 | 不可 | 不可 | 可 | +| タグの作成・更新 | 不可 | 可 | 可 | +| タグ名・別名・読みの編輯 | 不可 | 可 | 可 | +| タグ親子関係の編輯 | 不可 | 可 | 可 | +| タグの廃止 | 不可 | 可 | 可 | +| タグの物理削除 | 不可 | 不可 | 可 | +| タグ説明の編輯 | 不可 | 可 | 可 | +| Help の canonical source 編輯 | 不可 | 不可 | 可 | +| Help 翻訳の編輯 | 不可 | 可 | 可 | +| 素材の作成 | 不可 | 可 | 可 | +| 既存素材の更新 | 可 | 可 | 可 | +| user の role 変更 | 不可 | 不可 | 可 | +| user・IP の BAN | 不可 | 不可 | 可 | +| 監査情報の閲覧 | 不可 | 原則不可 | 可 | + +## 3.4 BAN + +BAN は API の入口で判定する。 + +| 対象 | 判定 | 応答 | +| --- | --- | --- | +| IP | `ip_addresses.banned_at IS NOT NULL` | 403 | +| User | `users.banned_at IS NOT NULL` | 403 | + +IP は PostgreSQL `inet` で保存し、IPv4 と IPv6 を同じ column で扱う。BAN は個別 host address の完全一致を対象とし、network prefix を subnet BAN として解釈しない。 + +## 3.5 guest 自動作成 + +guest 自動作成を継続する場合、bot その他により user が無制限に増加しないよう制御する。旧 system の migration ticket が有効な request では guest を先に作成せず、migration された user の session 確立を優先する。 + +- `POST /users` に rate limit を設ける。 +- bot と判断できる client には原則として user を発行しない。 +- user agent、作成元 IP、最終利用日時等を、掃除と不正対策に必要な範囲で保持する。 +- 編輯、投票その他の永続的活動がない古い guest を削除または圧縮できるようにする。 +- client-side storage の利用可否にかかわらず、同一閲覧中に guest 発行を連打しない。 +- V2 へ直接アクセスした後に利用者が明示的な旧 account migration を実行した場合は、migration された user の session へ切り替える。先に自動作成された guest と旧 user を暗黙に統合しない。永続的活動のない guest は通常の guest cleanup の対象とできる。 + +## 3.6 利用者設定の責務境界 + +利用者設定について、account に追従させる設定、browser / device に閉じる設定、system-wide default または管理設定として扱う設定を区別する必要がある。 + +ただし、設定 domain 全体については設計レビューをまだ完了していない。`settings` table、user theme の保存構造、server-side / client-side の保存先、設定 UI / API の構造その他を、既存実装または既存 table 定義だけを理由に確定仕様として扱わない。具体的な設定項目と保存 scope は 17.3 で確定する。 + +個別機能について本書の他節ですでに保存 scope が明示的に確定している場合は、その個別仕様を優先する。設定 domain の再設計を理由に、独立して確定済みの機能要件まで自動的に未確定へ戻さない。 + +## 3.7 端末固有 state + +route 遷移中だけ必要な一時 state、端末・画面固有の表示 state、利用者 identity に追従させる必要のない preference は、server-side account data と区別する。 + +永続化が必要な端末固有 state に `localStorage`、cookie、IndexedDB その他のどれを使うか、共通 namespace / key を設けるか、各設定を永続化するかは一律に固定せず、17.3 および各機能の個別仕様で定める。 + +## 3.8 ユーザ theme の保存 model + +標準 light / dark theme と主要 navigation の visual identity は 13章の確定仕様に従う。利用者が theme を customisation できる範囲は 17.24 で検討する。 + +user theme を DB へ保存するか、slot を設けるか、slot 数、token の物理表現、account-portable / device-local の境界は未確定とする。既存実装の `user_theme_slots` や `tokens` JSON / JSONB を完成仕様として採用しない。 + +## 3.9 表示 locale・言語・表記体系 + +表示 locale、content language、発音言語、presentation script を同一概念として扱ってはならない。 + +- URL は `/ja/`、`/en/`、`/zh-Hans/`、`/zh-Hant/`、`/sr-Cyrl/`、`/sr-Latn/` のような stable locale code を持つ。 +- `languages` は言語、`scripts` は表記体系、`locales` はシステムが実際に扱う language + script の組合せを表す。 +- Language / Script / Locale は通常の mutable な業務マスタではなく、stable identity を持つ参照資源として扱う。identity を表す code は作成後に別 code へ変更せず、不要になった場合も code 変更または物理削除ではなく deprecated 状態として扱う。deprecation その他の lifecycle state 自体は更新できる。 +- Language / Script は、DB に任意の row を追加すれば system が理解できるものではない。文字種の認識、正規化、transliteration / transcription、表記変換、自動 Tag Name 生成その他の application-side logic が必要になり得るため、application が対応実装を持つ code だけを登録可能とする。allowed code を application 上でどう表現するかは未確定事項に従う。 +- Locale は stable な locale code を identity とし、language と presentation script を必ず参照する。Language / Script / Locale の利用者向け表示名を DB と Frontend i18n のどちらで管理するか、および `locales.name` を保持するかは未確定事項に従う。 +- 表示 locale は page の UI 言語と presentation script を決めるが、編輯対象データの language / script を暗黙決定してはならない。日本語 UI から `zh-Hant` の Tag Name その他を編輯できる。 +- Tag Name は `language_code` と実際の `name` を multilingual な正本の中心とし、`script_code` は現在保存されている文字列の表記 script を示す補助情報として必須で保持する。`script_code` は `language_code` と同格の identity / translation 単位ではない。 +- 各 language は必要に応じて preferred storage script の policy を持てる。中国語は繁体字(`Hant`)を preferred とし、安全かつ一意に変換できる場合は正本表記を繁体字へ寄せる。一意な変換を保証できない場合は実際に保存した表記を維持し、その `script_code` を記録して人間の修正を待てる。全 language に共通する単一の canonical script 規則は要求しない。 +- 同一 language 内の script variant は別翻訳として二重管理せず、表示時の presentation conversion を基本とする。 +- presentation conversion が一対多になる場合は、既知の対応関係から一意な候補を選べるときはそれを用いる。なお一意に選べない場合は、Unicode code point 順その他の決定論的規則で暫定表示を一つ選び、必要な修正は Tag Name 管理で原表記を更新して反映する。script-specific override 用の独立 table は設けない。 +- 対応する presentation script へ変換した結果、別の Tag Name と同一表示になる名称は採用しない。登録・改名・投稿編輯時の新規名称解決では registration error とし、単なる warning にはしない。修正は Tag Name 管理から行う。 +- 旗は Language / Locale / 国家 / 地域の identity と同一視せず、独立した `flags` resource の stable identity として扱う。flag は国旗に限定せず、地域旗、民族旗その他の旗を扱えるものとし、ISO 3166 の国 code だけで identity を定義しない。 +- Language は nullable な `default_flag_code` により system default の Flag を参照できる。これは Tag Name 編輯その他の UI で Language を省スペースに視覚識別するための presentation metadata であり、Language の意味論的所属、国家、地域、話者集団その他を表す relation として解釈してはならない。 +- Locale と Script には default Flag を持たせない。Locale の代表名または identity を Flag で表現してはならない。 +- 将来、利用者は Language ごとに表示する Flag を system default から自由に override できる。override は semantic validation を行わず、任意の Language と任意の Flag を対応させられるものとし、利用者設定が存在する場合は system default より優先する。具体的な保存 schema は未確定事項に従う。 +- ISO 15924 の複合 script code を利用できる。例として、日本語の漢字仮名混じりは `Jpan`、韓国語の Hangul + Han は `Kore` を使用できる。 +- 将来、Tag Name の preferred language order を user setting として持てる。初期実装では requested language、既定言語、その他の安定した順序で fallback してよい。 + +## 3.10 日付・暦表示 + +DB の日付・日時は表示暦から独立した PostgreSQL の `date`、`timestamptz` その他の適切な型で保持する。和暦、拡張天保暦その他の表示文字列を正本として保存しない。暦は入力・表示時の conversion / presentation layer として扱う。 + +日付表示は少なくとも次の二軸を分離する。 + +- 年の表し方・年号系: 西暦、和暦、仏暦その他。 +- 月日を決める暦法: Gregorian calendar、Julian calendar、拡張天保暦その他。 + +利用者設定により複数の日付表現を同時に併記でき、表示順も設定できる。locale と暦設定は独立し、暦によって URL を分けない。和暦その他の元号表示を独立した暦法とみなさず、月日を決める calendar system と year presentation を分離する。 + +### 3.10.1 拡張天保暦 + +日本における太陰太陽暦は、いわゆる「旧暦」を正式名称とせず「拡張天保暦」と呼ぶ。拡張天保暦は、天保暦が廃止されず日本の暦法として現在まで継続したものと仮定し、天保暦本来の規則を可能な限り保存しながら、将来にわたり一意な暦日を構成できるよう補充規則を加えた暦法とする。 + +拡張天保暦は次を満たす。 + +- 太陽・月の天体位置計算の基礎として JPL DE440 を用いる。将来これを置き換える場合は、採用する天体暦を明示し、既存の変換 cache を再生成する。 +- 朔、二十四節気、中気その他の天文学的定義は国立天文台の定義に準拠する。 +- 算出結果は、国立天文台が公表値を提供する範囲で暦要項、暦象年表、朔弦望その他の公表値と照合できなければならない。 +- 日付への割当ては日本標準時を基準とする。UI locale または閲覧者の time zone から拡張天保暦の日界を暗黙変更しない。 +- 実装 library は固定しない。Astronomy Engine、Skyfield その他を利用できるが、library 固有の結果を暦法の定義そのものとみなしてはならない。 + +置閏は、まず天保暦本来の規則を適用する。天保暦の規則だけで暦月と閏月を一意かつ矛盾なく決定できる場合は、その結果を採用する。天保暦の規則だけでは一意に決定できない場合に限り、時憲暦の置閏規則を補充規則として適用する。補充規則では、冬至を含む月を11月とし、次の冬至月までに13暦月を含む場合は、その間で最初の中気を含まない月を閏月とする。 + +2033年から2034年にかけて生じる天保暦のいわゆる2033年問題は、この補充規則により国立天文台案1に相当する暦を採用し、2033年12月22日から始まる月を閏11月とする。2033年だけを hard-code した例外とはせず、将来同種の不定・矛盾が生じた場合にも同じ補充原則を適用する。 + +### 3.10.2 暦変換 cache + +暦変換結果は導出可能な情報であり正本ではないが、拡張天保暦の天文学的計算を通常 request ごとに繰り返さないため、再生成可能な永続 cache として DB に保持する。 + +初期 V2 の conversion cache は拡張天保暦専用とし、将来の calendar system を先回りした共通 schema へ押し込まない。Gregorian date と、対応する拡張天保暦の年・月・日・閏月情報を一意に対応付けられなければならない。元号その他の year presentation はこの cache に保存せず、presentation layer で扱う。 + +この cache は system-generated であり、user attribution や current-resource 型の更新履歴を持たない。拡張天保暦の計算規則を変更した場合は cache row を版管理せず、cache を破棄して新しい規則で再生成する。 + +cache miss 時は、要求された日付を表示するために必要な変換だけを同期的に計算して応答可能にする。応答を不必要に遅延させず、その後 background job により要求日から将来おおむね10年間の変換結果を先行生成して cache を補完できる。cache 補完の具体的な batch 単位、計算 library、INSERT 方法その他は実装詳細とする。 + +将来、月日境界や暦月構造が異なる別の calendar system を追加する場合は、その calendar に必要な domain fields と identity を改めて設計する。拡張天保暦用 schema が他の暦にも適合すると仮定して共通 table を先行作成しない。 + +拡張天保暦に対応する英語 identifier で「天保」を romanise する場合は、Segsyoxafu の確定表記 `tenpou` を用いる。 + +## 3.11 考え方・理由 + +認証では恒常 session、recovery、旧 system migration を別の寿命を持つ credential として扱う。旧方式を互換性のために恒常認証へ残すと、domain migration が終わっても legacy credential が system boundary に居座るためである。 + +利用者設定では、利用者 identity に追従すべき情報と端末・画面固有の state を区別する必要がある。ただし、その分類と保存方式自体は未確定であり、既存実装の DB / localStorage 分割をそのまま完成仕様とみなさない。 + +# 4. 公開制御・削除・監査 + +## 4.1 状態の分離 + +| 状態 | 閲覧 | 内部検索 | 外部検索 | 履歴 | 主な用途 | +| --- | --- | --- | --- | --- | --- | +| 通常 | 可 | 表示 | index 可 | 保持 | 通常公開 | +| 索引抑止 | 可 | 原則表示 | noindex | 保持 | 検索流入だけを避ける | +| 内部検索抑止 | 直リンク可 | 非表示 | policy に従う | 保持 | ノイズ、確認待ち | +| 公開抑止 | 通常不可 | 非表示 | noindex | 保持 | 権利、個人情報、重大問題 | +| 除票 | 通常不可 | 非表示 | noindex | 保持 | 投稿の削除相当 | +| 廃止タグ | タグページ可 | policy に従う | policy に従う | 保持 | 分類体系の整理 | + +廃止、索引抑止、内部検索抑止、公開抑止および除票を同一の boolean または同一の概念へ統合してはならない。 + +## 4.2 投稿の除票 + +投稿は通常操作で物理削除しない。除票は次を意味する。 + +- 一般の投稿一覧と投稿検索から除外する。 +- 通常の投稿詳細 API と画面は role にかかわらず 404 とする。 +- admin も通常の投稿詳細画面では閲覧せず、監査・復帰専用導線から扱う。 +- sitemap から除外する。 +- 類似度の通常候補、外部活用候補、ランダム取得その他の利用対象から除外する。 +- 除票または復帰を一つの `event` として記録し、生成された `post_versions` はその `event_id` を保持する。 +- 除票理由は一般利用者へ表示しない。 +- 復帰に必要な現在状態と履歴を保持する。 + +## 4.3 索引抑止 + +索引抑止は閲覧可能性を変えず、外部検索エンジンへの索引だけを抑止する。 + +- HTML に `` を出す。 +- sitemap から除外する。 +- 必要に応じて `X-Robots-Tag: noindex` を返す。 +- `robots.txt` の `Disallow` だけで代用しない。 +- client-side route 遷移後にも robots meta を更新する。 +- server-side が判定した policy を UI が表示へ反映する。 +- `nofollow` は既定で付与せず、必要な場合だけ明示する。 + +索引抑止された情報は、内部検索抑止が別に設定されていない限り、タグ広場内の通常検索へ表示してよい。 + +## 4.4 タグによる索引抑止 + +タグに索引抑止を設定した場合、少なくとも次を判定対象とする。 + +1. 当該タグの詳細。 +2. 当該タグの説明。 +3. 当該タグに関聯する素材。 +4. 当該タグが直接付与された投稿。 +5. 親タグ展開により当該タグを含むと判断される投稿。 +6. 当該タグの正式名または別名が表示本文に現れる説明ページ。 +7. 当該タグ名がタイトル、タグ一覧、親子タグ表示、関聯タグ表示その他の索引対象本文に現れるページ。 + +タグ自身の親ページ・子ページを、親子関係だけを理由に自動で索引抑止してはならない。 + +## 4.5 文字列判定 + +索引抑止の文字列判定では、次を考慮する。 + +- 対象 Tag の全 language の primary name。 +- alias。 +- registration identity で同一となる comparison form。 +- 実際の表示で生成される presentation-script variant。 + +既定は token 単位の完全一致とする。system-wide search equivalence を索引抑止へ無条件に流用して、意味の異なる類似語まで抑止してはならない。substring match は admin が phrase rule として明示した場合だけ許可する。 + +Markdown の URL、code span、code block その他、表示上の自然言語本文ではない部分を誤判定してはならない。 + +## 4.6 公開抑止 + +公開抑止された対象は、通常利用者に内容を見せない。 + +- guest と member の詳細 API は 404 とする。 +- admin は専用管理導線から理由付きで確認できる。 +- 一覧、内部検索、候補取得、sitemap から除外する。 +- HTML を返す場合は noindex とする。 +- 履歴は admin だけが参照できる。 + +公開抑止を単なる検索流入対策に使用してはならない。その場合は索引抑止を使用する。 + +集計表は例外として、公開抑止・除票等により通常は閲覧できない履歴も、集計対象の全体件数には含める。ただし、集計の数値を公開することは、対象の個別属性、ID、名称、本文、参照先、履歴内容その他を公開する許可ではない。集計表における公開可能な軸・条件・出力については「集計表(Counts)」章のより限定的な規則を適用する。 + +## 4.7 内部検索抑止 + +内部検索抑止は、通常の検索、一覧、autocomplete、関聯候補から対象を外すが、公開抑止されていなければ直リンク閲覧を許可する。 + +投稿、タグ、タグ説明、ヘルプおよび素材について個別に設定できる。 + +## 4.8 policy の一元判定 + +索引、内部検索、公開および sitemap の判定は server-side の共通 policy resolver に集約する。 + +出力は少なくとも次を含む。 + +```json +{ + "robots": "noindex", + "sitemap": false, + "internal_search_hidden": false, + "public_hidden": false, + "reasons": [ + { "type": "tag", "id": 456, "name": "対象タグ" } + ] +} +``` + +UI、sitemap 生成、検索処理が別々の規則で推測してはならない。 + +## 4.9 event と操作履歴 + +`event` は、利用者レベルで意味上ひとまとまりとなる一つの論理操作を表す immutable な履歴である。 + +一つの操作が投稿、タグ、親子関係、素材その他の複数リソースへ波及しても、それらを別々の操作へ分解せず、一つの `event` として束ねる。 + +`events` は少なくとも、操作主体、利用者から見た操作種別、任意の理由、差戻し元、操作固有の補助情報、IP address、作成日時を保持できなければならない。物理 column、型、constraint は `tables.zip` を正本とする。 + +`events` は少なくとも次の規則に従う。 + +- 作成後に更新または削除しない。 +- event type は利用者から見た論理操作全体の種類を表す。各 resource version が持つ版生成理由とは、同名でも別の enum domain として区別できなければならない。 +- event と、その操作で確定する現在状態および version は同一の論理操作として整合して保存する。操作が失敗した場合、成功した event だけを履歴へ残してはならない。 +- 差戻し操作は差戻し元 event を参照できる。 +- 補助 metadata は操作固有の情報を保持できるが、version 管理対象の状態スナップショットを代替してはならない。 +- version 管理されるリソースに変更が生じた場合、生成された各 version は同一 event を参照する。 +- 一つの event は、同一の version 管理対象について原則一つの version だけを生成する。 +- event 内の各リソース変更は順序を持たない集合として扱い、処理順を履歴上の意味として保存しない。 +- DB 制約その他の都合で変更適用に実装上の順序が必要でも、その順序を event 内の業務的な時系列とみなしてはならない。 +- event 一覧は作成日時を主順序として表示する。同一時刻の安定表示に内部 ID を tie-breaker として利用してよいが、ID 自体へ業務上の時系列や因果順序を持たせない。 + +少なくとも BAN と解除、role 変更、publication policy の変更、投稿・タグ・素材等の作成・更新・削除相当操作、複数リソースへ波及する一括操作を event として記録する。 + +`events` は横断的な操作履歴と監査の正本である。ただし、投稿、タグ、素材その他の当時状態は各 immutable version snapshot を正本とする。 + +## 4.10 event 単位の差戻し + +過去の `event` を差し戻す場合、過去の event または version を更新・削除してはならない。差戻し自体を新しい `event` として記録する。 + +- 一つの event に属する差戻し可能な変更をまとめて差し戻せる。 +- 差戻し可能な変更は、その変更前後を immutable な version 履歴から復元できなければならない。履歴を持たない変更を、推測によって差し戻してはならない。 +- event に属する変更対象から一部だけを選択して差し戻せる。 +- 必要なリソースでは、変更対象のうち特定フィールドだけを選択する部分差戻しを許可できる。 +- 差戻し結果は現在状態へ適用し、その結果を新しい完全な version スナップショットとして保存する。 +- 差戻しによって生成された version は、新しい差戻し event の `event_id` を保持する。 +- 差戻し元 event の変更後に同じ対象または同じフィールドが別 event で変更されている場合、現在状態を無条件に上書きしてはならない。base、差戻し元操作後、現在状態を比較し、競合として利用者へ提示する。 +- 一部差戻しでは、選択されなかった後続変更を保持する。 +- 差戻し処理に必要な DB 操作順は実装上の都合であり、event 内の履歴順序として保存しない。 + +この差戻しは、過去へ時間を巻き戻す操作ではなく、過去の操作を打ち消す新しい操作として扱う。 + + +## 4.11 考え方・理由 + +削除・廃止・索引抑止・内部検索抑止・公開抑止を分けるのは、「見せない」という表面的な結果だけで data retention と利用可能性を一つの boolean にすると、後から理由と復帰可能性を復元できなくなるためである。 + +policy は server-side で一元判定し、event は logical operation、version は resource state と役割を分ける。この分離により、moderation、監査、差戻し、SEO を同じ状態 column の組合せに埋め込まずに扱える。 + +# 5. 投稿 + +## 5.1 基本 + +投稿は外部コンテンツへの URL を中心とした resource である。URL は必須かつ一意とし、title、外部 thumbnail URL、タグ広場が保持する thumbnail、作成者、元作成日時、現在版、除票状態その他の Post 自身の current state を管理できなければならない。具体的な column と物理型は `tables.zip` を正本とする。 + +外部コンテンツ本体は原則として保存しない。ただし、履歴で必要となる thumbnail image は別扱いとし、Blob / object storage の保持規則に従う。 + +動画投稿の動画固有属性は Post 本体から分離した video subtype で管理する。少なくとも動画長、width、height を保持できる。動画でない投稿はこの subtype を持たない。動画長を含む動画固有属性は同期用 metadata であり、利用者が Post の基本データとして直接編輯する値ではない。 + +Post 作成・編輯時に title または thumbnail が欠けていることは warning とし、その欠損だけを理由に保存を拒否してはならない。自動取得中・取得不能・利用者による意図的な空値を UI 上どう区別するかは未確定事項に従う。 + +### 5.1.1 オリジナルの作成日時 + +投稿の「オリジナルの作成日時」は、一つの PostgreSQL Range value に半開区間 `[from, before)` として保持する。下位の日時要素が不詳である場合も、その日時が存在し得る区間へ正規化して同じ Range value を用いる。DB / session timezone は UTC を標準とするが、この Range は original source の timezone 名や入力 offset の provenance を保持するものではない。 + +元作成日時について、時刻部分まで判明しているかを独立して保持する。 + +- 時刻既知の場合、分まで判明している通常日時のほか、「分不詳」により時まで判明している場合も時刻部分を意味のある情報として扱う。 +- 時刻不詳の場合、年・月・日から区間境界を作る時刻は UTC の `00:00` に正規化するが、この `00:00` 自体を元情報として判明していた時刻とは扱わない。 +- 元作成日時全体が不詳の場合は Range 自体を持たない。 + +単一日時として入力された情報は、判明している最小単位に応じて次の半開区間へ正規化する。 + +| 入力状態 | 保存する意味 | 時刻部分 | +| --- | --- | --- | +| 年月日時分まで判明 | 入力した分の開始以上、次の分の開始未満 | 既知 | +| 分不詳 | 入力した時の開始以上、次の時の開始未満 | 既知 | +| 時刻不詳 | 入力した日の UTC 00:00 以上、翌日の UTC 00:00 未満 | 不詳 | +| 日不詳 | 入力した月の初日 UTC 00:00 以上、翌月初日 UTC 00:00 未満 | 不詳 | +| 月日不詳 | 入力した年の1月1日 UTC 00:00 以上、翌年1月1日 UTC 00:00 未満 | 不詳 | +| 年月日不詳 | 値なし | 不詳 | + +利用者が明示的な範囲として入力した場合も DB 上は同じ Range へ正規化し、「単一日時」と「範囲指定」の入力 mode 自体を独立して保存しない。保存済み Range が上表の canonical な単一日時表現に一致する場合は単一日時として、それ以外は範囲指定として復元できる。 + +利用者向けの範囲入力・表示では終了日時を含むように見せる。分単位の UI では、DB Range の排他的 upper bound と利用者向け inclusive end の間を1分ずらして相互変換する。 + +## 5.2 URL 正規化 + +保存前に次を行う。 + +- 前後の空白を除去する。 +- scheme は HTTP または HTTPS だけを許可する。 +- host を小文字化する。 +- 不要な末尾 slash を除去する。 +- platform 固有の揺れを normaliser で正規化する。 +- 同一コンテンツを指す URL を重複登録しない。 +- DB の index と API の上限を超える異常に長い URL を拒否する。 + +Nico、YouTube その他、識別方法が安定している platform は専用 normaliser を持つ。 + +## 5.3 新規投稿フロー + +新規投稿では URL を主入力とする。 + +1. 利用者は `/posts/new` で一件または複数件の URL を入力する。 +2. URL からタイトル、サムネイル、元作成日時、動画長、外部タグその他の取得可能情報を自動取得する。 +3. 取得結果と登録予定値を同じ `/posts/new` の確認状態で表示する。 +4. 利用者は誤りまたは不足がある項目だけを修正する。 +5. server-side の共通 validation logic で全件を検証する。 +6. 検証に成功した行だけでなく、成功件数、警告件数、失敗件数と各理由を表示する。 +7. 利用者の確定操作後に登録する。 + +投稿追加の入力・確認その他、利用者が画面として区別する作業段階は query parameter ではなく route によって識別する。入力中の URL 群、自動取得結果、手動編輯値、skip、仮登録その他の一時的な作業 state は URL へ詰め込まず、Svelte client state と `sessionStorage` その他の session-scoped storage を用いて保持する。reload や同一 tab 内の遷移で作業を復元できることを基本とし、長大データや一時取得値を bookmark / share 用 URL の一部にしない。 + +URL から取得可能な基本情報について、通常の既定動作を「利用者が取得ボタンを押したときだけ取得する」方式にしてはならない。 + +## 5.4 自動取得値と手動値 + +- 利用者が明示的に入力した値は、自動取得値より優先する。 +- 自動取得失敗は、手動入力可能な項目について投稿全体の作成を必ずしも妨げない。 +- `video_posts.duration_ms`、`width`、`height` その他の動画固有属性は自動取得・同期で管理し、投稿追加時の初期入力項目および Post 基本データリストの編輯可能項目にしない。同期で値が変化した場合に必要な従属状態の補正は application logic が行う。 +- 確認画面では最終的に保存される値を表示し、自動取得元の生値だけを表示してはならない。 +- PC の double click だけに依存せず、touch device でも編輯可能な導線を設ける。 +- Post に汎用的な「備考」/ `remarks` field または備考 tab を設けない。Tag の記載、transcription、Tag Description、申請 / event その他、意味と責務が独立している情報はそれぞれの resource / relation で扱う。新しい自由記述需要が生じた場合も、generic な何でも欄へ集約せず、その意味に応じた構造化属性または専用 resource として検討する。 + +## 5.5 作成 + +投稿作成時は次を必須とする。 + +- member 以上であること。 +- URL が正規化後に一意であること。 +- タグ入力を共通の Tag input service で解釈し、formatting、category prefix、別名、他言語名、読み、廃止状態その他を一貫して処理すること。 +- 廃止タグおよび手入力禁止の外部タグを拒否すること。 +- 親投稿の存在と非循環性を検証すること。 +- 投稿作成を一つの `event` として記録し、`post_versions` に `event_id`、`event_type = create`、`version_no = 1` のスナップショットを記録すること。 + +route / controller 相当の層へタグ作成・名称解決の業務規則を直接蓄積してはならない。 + +## 5.6 更新と競合制御 + +投稿更新では `base_version_no` を要求する。 + +- 現在版と一致しない場合は 409 conflict を返す。 +- conflict 応答は base、current、mine および競合項目を含む。 +- 競合しない集合差分は明示操作により merge できる。 +- 強制上書きは admin または明示された権限者だけに許可する。 +- force と merge を同時に指定してはならない。 + +競合判定対象には、タイトル、URL、サムネイル、元作成日時、タグ、親投稿および局所記載を含める。`video_posts.duration_ms` その他の同期用 metadata は利用者の基本データ編輯対象に含めず、同期処理側の concurrency / integrity contract に従う。独立 resource として確定した情報は、その resource 自身の concurrency contract に従う。 + +### 5.6.1 Post URL の転居 + +Post URL は通常の Post 編輯から直接変更しない。同一の外部 content が別 URL へ移転した場合に限り「転居届」を提出し、admin の受理によって URL 変更を確定する。別 content への差し替えを転居として扱ってはならない。 + +- member は変更元 Post、変更先 URL、理由その他の必要情報を転居届として申請する。 +- admin 自身が URL を変更する場合も直接更新せず、同じ転居届を提出して自ら受理する。admin 専用の裏口となる直接変更経路を UI / application service に設けない。 +- 受理時には変更先 URL の normalisation、既存 Post との重複確認、同一 content の移転として扱えることの確認を行う。 +- 受理による URL 更新、Post Version の生成、event の記録は一つの論理操作として整合して保存する。 +- 却下された転居届は Post の現在 URL を変更しない。 + +## 5.7 投稿タグ + +現在の投稿タグは `post_tags` に保持する。 + +- 同一投稿・同一タグの active 重複を禁止する。 +- 現在状態の `post_tags` に非 active の履歴行を保持しない。 +- タグの追加・削除履歴は `post_versions` のタグ snapshot を根拠とする。 +- タグ入力は formatting、category prefix、別名・名称解決を通す。 +- 同一 language の別名は、同じ Tag へ自動的に解決してよい。 +- 現在の UI language と異なる language の既存タグ名が生入力された場合は、無言で解決してはならない。既存 Tag の現在 language の名称へ置換するか、その入力を新しい Tag として扱うかを利用者が選択できる警告を表示する。 +- autocomplete / search は他言語名および読みから候補を提示してよい。候補を明示選択した場合は Tag ID を確定し、現在の表示 language の primary name へ置換してよい。 +- 廃止タグは新規付与できない。 +- 外部タグは定められた同期または管理経路以外から付与できない。 +- 親タグを再帰的に展開する。 +- 親タグ展開後の重複を排除する。 + +### 5.7.1 別 Post からの Tag import + +別 Post の Tag を現在編輯中の Post へ取り込む機能を採用する。import は通常の Tag input / resolution / parent expansion / validation を迂回する裏口にせず、確定結果は通常の Post 更新と同じ履歴・event 経路を通す。 + +add / replace、preview と選択、局所記載・廃止 Tag・外部 Tag・system / workflow Tag の扱いその他の詳細 UX は未確定事項に従う。 + +## 5.8 局所記載 + +局所記載は、投稿全体ではなく動画・音声等の一部へタグを付与する機能である。 + +### 5.8.1 時刻入力 + +次の形式を受け付ける。 + +```text +12 +12.345 +1:23 +1:23.456 +1:02:03 +1:02:03.456 +``` + +- 単独数値は秒と解釈する。 +- `分:秒` と `時:分:秒` を許可する。 +- 内部保存はミリ秒へ正規化する。 + +### 5.8.2 タグ記法 + +```text +タグ名[開始-終了] +タグ名[開始-] +タグ名[-終了] +タグ名[-] +タグ名[0:00-0:12] +``` + +- `タグ名[-]` は通常タグと等価とする。 +- 開始省略は 0 とする。 +- 終了省略は動画末尾までを表し、保存する Range の upper bound を unbounded とする。 +- 開始と終了が逆の場合は並べ替える。 +- 同一値の場合は最小 1 ms の範囲とする。 +- 同一タグの重複、隣接、包含する section は統合する。 +- 全範囲となった section は保存せず、通常タグとして扱う。 + +### 5.8.3 データ構造 + +一つの局所記載 section は一つの row とし、複数 section を PostgreSQL Multirange 一値へまとめない。時間範囲は millisecond 単位の半開 Range `[begin, end)` とし、開始は有限かつ 0 以上、終了だけを unbounded にできる。 + +同一 Post・Tag の重複 Range は PostgreSQL の `WITHOUT OVERLAPS` を用いた declarative constraint で DB でも拒否する。具体的な temporal primary key、型、CHECK、必要 extension は `tables.zip` を正本とする。 + +- upper bound なしの Range はその時点から動画末尾までを意味する。 +- empty Range は許可しない。 +- 隣接する Range は DB constraint 上は共存できるが、application は保存時に結合して一つの Range へ正規化する。 +- 動画長が取得済みの場合、局所記載が動画長を超えないよう application invariant として整合させる。video subtype と section の cross-table trigger / constraint trigger は初期仕様では設けない。 +- 外部 metadata の再同期等で動画長が変化した場合も、application の同期処理が既存の局所記載を新しい動画長と整合する状態へ補正する。利用者が動画長を基本データとして直接編輯する経路は設けない。 +- 局所タグの親タグ展開は同じ section へ適用する。 + +### 5.8.4 投稿表示 + +Post の通常閲覧 UI では、局所記載された Tag を編輯画面だけに閉じ込めず、通常の Tag 一覧 / sidebar に表示する。 + +- 局所記載 Tag は現在の再生位置が section 外であっても一覧から除去せず、Tag row の出現・消滅による layout shift を起こさない。 +- playback position を取得できる場合、現在位置がその Tag のいずれかの `range_ms` に含まれるときは通常の表示強度とし、どの Range にも含まれないときは薄い表示にする。 +- 局所記載 Tag には、現在 active か否かとは独立して局所記載であることを識別できる marker を常時付ける。category colour は通常 Tag と同じ semantics を維持し、局所記載状態の表現へ流用しない。 +- mouse hover、keyboard focus、touch 操作のいずれからも、その Tag に設定された全 section の時間範囲を確認できる。 +- playback position を取得できない provider / 表示状態では、active / inactive の動的な濃淡判定を行わない。この場合も局所記載 marker と section 情報は表示する。 +- Tag 名本体の通常の遷移 semantics は局所記載によって変更しない。 + + +## 5.9 親子投稿 + +`post_implications` は子投稿から親投稿への関係を表す。 + +- 自己参照を禁止する。 +- 存在しない投稿を参照できない。 +- 循環を禁止する。 +- 親投稿変更を投稿履歴へ記録する。 +- 現在画面では親投稿の現在タイトルを表示できる。 +- 履歴画面では当時の親投稿タイトルを復元できる。 + +## 5.10 投稿検索 + +投稿検索は少なくとも次を扱う。 + +| 条件 | 仕様 | +| --- | --- | +| `url` | 部分一致 | +| `title` | 部分一致 | +| `tags` | 空白区切り。別名解決あり | +| `match` | `all` または `any` | +| `not:` | 除外タグ | +| `original_creation_range` | 元作成日時 | +| `created_from/to` | 作成日時 | +| `updated_from/to` | 更新日時。タグ更新を含む | +| `visibility` | 通常、索引抑止、内部検索抑止、公開抑止 | +| `order` | title、url、元作成日時、作成日時、更新日時 | +| `page`, `limit` | ページネーション | + +通常検索は除票、公開抑止、内部検索抑止を除外する。索引抑止は内部検索抑止がない限り通常検索へ表示する。 + +## 5.11 投稿履歴 + +Post の version history は immutable な自己完結スナップショットとし、owner Post 内の version number により版を特定する。version row 固有の surrogate ID は使用しない。具体的な table key と column は `tables.zip` を正本とする。 + +各版では、少なくとも title、URL、外部 thumbnail URL、当時の thumbnail Blob、Tag と局所記載、親 Post、元作成日時、動画 subtype、操作主体、event その他、その Post の当時状態を後続の current row へ依存せず復元できなければならない。 + +Tag snapshot は投稿側で Tag 名・category を重複保存するのではなく、当時の immutable Tag Version を一意に特定できる参照と局所記載を保持する。Tag 名変更、category 変更、Tag 物理削除後も、Post history は当時の表示を復元できなければならない。 + +投稿または現在の thumbnail を削除しても、各 Post Version が参照する当時の thumbnail Blob を失ってはならない。 + +## 5.12 考え方・理由 + +Post は外部 content への参照を基本とし、URL identity、取得 metadata、人間の編輯値、Tag relation、immutable history を分離する。外部 platform の一時的な値を人間の確定値より強くしないのは、同期 system が共同編輯の成果を上書きしないためである。 + +競合制御を version number で明示するのも、複数人編輯を last-write-wins にせず、何を merge / overwrite するかを利用者が判断できるようにするためである。 + +# 6. タグ・タグ名・外部タグ + +## 6.1 論理構造 + +タグ体系は、language-independent な Tag 実体と、Tag に属する一つ以上の Tag Name を分離して扱う。 + +| 概念 | 責務 | +| --- | --- | +| タグ | category、投稿件数、廃止状態、親子関係、履歴 | +| タグ名 | language ごとの名称、primary、alias、自動生成状態、登録元 script | +| 読み | Tag Name の発音・読みによる検索入口、および言語に応じた並べ替え情報 | + +`tag_names` は Tag を参照し、Tag 側から特定の Tag Name を外部キーで固定しない。 + +現在有効な `locales` が表す distinct language ごとに、各 Tag は primary Tag Name を必ずちょうど一件持たなければならない。0件も2件以上も invariant violation とする。DB では `(tag_id, language_code)` に対し `primary_flg = true` の row だけを対象とする PostgreSQL partial UNIQUE index により「最大一件」を保証し、「最低一件」は application invariant とする。 + +通常利用可能な Tag は、active language ごとの正式な primary Tag Name を欠いてはならない。Tag 作成、Locale / Language の有効化、名称編輯その他によって不足が生じる場合は自動生成で補い、この invariant を維持する。名称候補の審査・staging が必要な場合は正式な `tag_names` とは分離して扱う。最後の正式名称を失う Tag を恒久的な tombstone として残すことを要件とせず、物理削除後も immutable な Tag Version から過去表示を復元できることを優先する。 + +## 6.2 カテゴリ + +| category | 日本語表示名 | 意味 | +| --- | --- | --- | +| `deerjikist` | ニジラー | 作者、投稿者等 | +| `meme` | 原作・ネタ元・ミーム等 | 原作、ネタ元、ミーム | +| `character` | キャラクター | キャラクター | +| `general` | 一般 | 一般分類 | +| `material` | 素材 | 素材差分、部品、素材分類 | +| `meta` | メタタグ | 管理、形式、状態等のメタ情報 | + +Tag category は一つの Tag に一つだけ設定し、その Tag の主たる役割を示す粗い semantic class とする。category だけで Tag の持つ意味、関係、用途のすべてを表現しようとしてはならない。 + +`category` の enum code は data / API 上の stable identifier とし、画面では locale ごとの表示名を用いる。enum code をそのまま利用者向け label として表示することを標準としない。 + +`material` category は、タグ広場における主たる役割が素材そのもの・素材差分・部品・素材分類である Tag に用いる。画像・音声・動画その他の媒体種別は category ではなく、関聯する Material / Blob の `content_type` その他から判定する。通常利用される `material` category Tag は Material resource と強く対応させることを原則とする。 Material 自身には名称を持たせず、素材概念の名称は Material Tag / Tag Name が担う。同一 Tag に複数 Material を所属させられるが、人間が意味上の名称で区別する必要がある差は Tag の粒度として表現する。外部素材集の folder 名を再現するだけの曖昧な umbrella Tag を作成せず、`_` を semantic hierarchy の delimiter として自動解釈しない。 + +Nico その他の外部タグを内部 Tag の category として表現しない。外部タグは専用台帳で扱う。 + +## 6.3 システムタグ + +| タグ | 用途 | +| --- | --- | +| `タグ希望` | タグ不足 | +| `bot操作` | 投稿の現在状態に bot、同期、自動処理その他の機械操作による影響が含まれ、その影響を人間がまだ確認していない状態 | +| `ニジラー情報不詳` | 投稿者・作者情報不足 | +| `動画` | 動画投稿 | +| `ニコニコ` | Nico 由来 | +| `YouTube` | YouTube 由来 | + +`bot操作` は「過去に bot が作成・更新した」という provenance を表すタグではない。人間が現在状態を確認した時点で手動解除でき、その後 bot、同期、自動分類その他が投稿状態へ新たな未確認影響を与えた場合は再度付与する。 + +システムロジックは `tag_names.name` その他の表示文字列へ依存せず、stable role identifier と通常の Tag ID の一対一対応で特別な意味を識別する。対応 table は `system_tag_roles` とする。システムタグ自体は通常の Tag であり、名称・翻訳・Reading・説明・履歴・implication その他の通常機能を維持する。role が安定識別を担うため、既定 primary name をシステム都合だけで固定する必要はない。 + +`system_tag_roles` の具体的な column・型・constraint は `tables.zip` を正本とする。 + +`role` は schema で定義する小文字 `snake_case` の列挙値とし、利用者が任意の role を追加できない。role と Tag は一対一とし、active な role に対応する Tag を、role の解除または再割当てなしに廃止・物理削除してはならない。mapping は admin が扱う固定的な設定に近い resource とし、専用 version table、`created_by_user_id`、`updated_by_user_id`、`updated_at` は設けない。 + +## 6.4 タグ名の formatting・identity・許可文字 + +Tag Name の保存表記、登録上の同一性、検索上の同定を混同してはならない。 + +### 6.4.1 formatting + +formatting は保存する `name` 自体を整える処理である。 + +- `tag_name_formatting_rules` は少なくとも `source` と `replacement` を持つ。 +- `source` / `replacement` は一文字に限定せず、複数文字の短い文字列を扱える。旧字体・異体表記その他、文字列単位での整形を表現できる。 +- 同じ `source` に `replacement` が一件だけ存在する場合は自動適用できる。複数存在する場合は自動確定せず候補を利用者へ提示する。 +- formatting rule は連鎖適用できる。一意な置換を適用した結果が別 rule の `source` に一致する場合、確定形へ到達するまで処理を継続できる。 +- rule graph の循環を許可しない。循環を生じる rule set は管理時に拒否する。 +- 候補が複数に分岐した時点では自動連鎖を停止し、利用者が候補を選んだ後に続きの一意な rule を適用する。 +- NFKC その他、意味のある文字差まで一括して失わせる Unicode normalisation を保存値へ無条件適用しない。 + +### 6.4.2 identity + +identity は、同一 language の名称を登録上同じとみなすかを判定する application-side algorithm とする。 + +- Latin alphabet の case その他、登録上同一と定めた差だけを comparison form へ一時変換する。 +- `identity_key` を正本 column として保持しない。 +- 登録・改名は低頻度操作であるため、必要に応じて同一 language の既存 Tag Name を application 側で変換して比較してよい。 +- `PG_UNICODE_FAST` の deterministic な DB comparison / UNIQUE だけでは保証できない identity equivalence があることを許容し、Tag Name identity は application validation / identity algorithm を正とする。 +- search equivalence と一致しても identity が一致しない場合は、登録禁止ではなく類似 Tag 警告として扱える。 +- presentation conversion 後の同名衝突は search warning と区別し、登録エラーとする。 + +### 6.4.3 allowed characters + +Tag Name の入力可否は、登録対象の locale ごとに許可文字を定める。 + +- `tag_name_allowed_rules` は `locale_code` と実際の `character` を基本とする。 +- `Han`、`Hiragana` その他の抽象 script class だけを rule 値として保存せず、管理 UI で script 群を選んだ場合も最終的には許可文字へ展開して保持する。 +- 判定対象は現在表示中の UI locale ではなく、登録しようとしている Tag Name の target locale とする。日本語 UI から `zh-Hant` の名称を編輯する場合は `zh-Hant` の rule を適用する。 +- `?`、`*`、`:` その他の禁止文字は allowed rule から除外できる。 +- 二個以上連続する `_`、先頭・末尾の `_`、予約 prefix `Tag_#` その他の複数文字 pattern は application validation で扱う。 +- Unicode が標準化している canonical composition / decomposition を、数千件の個別 rule として手作業で列挙しない。 + +`search_rules`、`tag_name_formatting_rules`、`tag_name_allowed_rules` は管理可能な現在状態だけを保持し、現時点では rule 自体の version history / event snapshot を作らない。 + +## 6.5 primary・別名・自動生成名 + +- 同じ `tag_id + language_code` に属する Tag Name のうち `primary_flg = 1` の一件を、その language の primary name とする。 +- 同じ `tag_id + language_code` に属する非-primary name を alias とする。 +- alias から別の alias への chain は存在しない。全 Tag Name は直接 `tag_id` を参照する。 +- 同一 language の alias 入力は同じ Tag へ自動解決してよい。 +- タグ名変更時、旧 primary は alias として残してよい。 +- 独立した Tag 実体または説明を持つ名称を、情報を失う形で alias へ吸収してはならない。 + +対応 language に人間が確定した primary name がない場合、Tag をその language から解決可能にするため Tag Name を自動生成する。 + +- 現在有効な `locales` が表す distinct language ごとに primary Tag Name が一件存在する状態を常時 invariant とし、Tag 作成時だけの補完規則としない。 +- 既に人間または自動生成の primary が存在する language には重複生成しない。 +- 自動生成した Tag Name は staging を経由せず正式な `tag_names` に登録し、`auto_generated = true` とする。通常の人間入力、rename、alias その他の人間が確定した名称は原則として `auto_generated = false` とする。 +- 新しい active language / locale を system に追加した場合は、既存 Tag について不足する primary name を一括生成する。 +- 自動生成処理は少なくとも target Locale、対象 Tag、source Tag Name を入力として扱える contract とする。source を単なる文字列へ縮約せず、`source_tag_name.name`、`source_tag_name.language_code`、`source_tag_name.script_code` を参照できなければならない。これにより source language / script と target language / script の組合せに応じて translation、transliteration、transcription その他の処理を選択できる。 +- 自動生成は translation、transliteration、reading 由来表記その他を利用できる。 +- 通常の生成処理で名称を得られない場合は、Tag ID を使った deterministic fallback `Tag_#` を使用する。例: `Tag_#123`。 +- `Tag_#` は system 予約 prefix とし、人間が入力する Tag Name では使用を禁止する。 +- 人間が適切な primary name を設定した場合、その名称を優先する。置き換え前の auto-generated primary を alias として保持するか削除するかは、その編輯操作で利用者が選択する。 +- `auto_generated` を条件とした一覧・検索・絞込みを提供し、人間が自動生成名称を確認・修正できるようにする。 +- `auto_generated = true` の Tag Name を日本語 UI では「未公証」と呼ぶ。`auto_generated` と別に `certified` その他の公証状態 column を設けない。 +- 未公証の名称を人間が公証した場合は、その Tag Name の `auto_generated` を `false` にする。公証操作は Tag の version / event 履歴へ記録する。 + +## 6.6 タグ名の読み + +`tag_name_readings` は Tag Name に対する pronunciation / reading を保持する。 + +Reading は Tag Name、発音 language、notation、reading text を対応付ける。具体的な column・型・constraint は `tables.zip` を正本とする。 + +- `language_code` は UI locale ではなく発音言語を表し、`languages.code` を参照する。Tag Name 自体の content language と一致しなくてもよい。 +- `notation` は `kana`、`pinyin`、`bopomofo`、`jyutping` その他、その reading の表記体系を表す。現時点では notation 専用台帳を作らず code を直接保持する。 +- 同じ Tag Name に複数の発音言語、notation、reading を登録できる。 +- Reading は Tag Name と同様に検索対象とする。 +- 日本語では `ja + kana` の Reading を五十音系の並べ替えに利用することを基本とする。 +- Reading を全 language 共通の sort key とみなさない。 +- 中国語系 locale は CLDR / ICU の locale collation を既定とする。Latin 系その他の通常の文字列 sorting も、個別規則を必要としない限り CLDR / ICU の locale-aware collation を既定とする。 +- 実運用上の必要が生じた locale には、既定 collation を上書きする個別の並べ替え policy を追加できる。 +- locale 固有の並べ替え規則を適用できない場合は、再現可能な安定順を fallback とし、Reading の有無で不定な順序にならないようにする。 +- Reading の language / notation を現在の UI locale により制限してはならない。 + +## 6.7 親子関係 + +`tag_implications` は子タグから親タグへの意味上の含意を表す。子 Tag が成立するなら親 Tag も成立する関係だけを登録し、単なる所属、由来、部品、表示上の grouping その他の関係を代用してはならない。 + +例えば、ある Material Tag がキャラクター全体を成立させる場合は character Tag を imply できるが、顔を含まない胴体素材その他、その Tag 単体では character Tag の成立条件を満たさないものを「そのキャラクターの素材である」という理由だけで imply させてはならない。 + +- member 以上が編輯できる。 +- 自己参照を禁止する。 +- 循環を禁止する。 +- 親タグ展開は再帰的に行う。 +- `tag_implications` は member 以上が直接編輯でき、承認 workflow を設けない。 +- 親子関係を追加・変更しても、その時点ですでに Tag が記載されている既存 Post の current tag set を遡及して自動変更しない。親タグの再帰展開は、Post への Tag の新規記載・編輯その他、その Tag membership を現在状態へ反映する操作時に適用する。 +- 同一の親子関係を重複登録しない。 +- 廃止または削除された中間タグを表示上で貫通できる。 + +例: + +```text +A -> B -> C +B を削除 +A -> C を補完 +``` + + +## 6.8 Tag の合併・分離 + +Tag の合併と分離を正式な domain operation として扱う。単なる名称変更、alias 追加、物理削除の組合せで代用しない。 + +### 6.8.1 合併 + +Tag 合併は、複数の既存 Tag を一つの Tag へ統合する論理操作である。 + +- 統合対象の Post membership、Tag Name / alias、Tag Description、Material、`tag_implications`、Tag Group membership、履歴その他、Tag identity に依存する関係を対象に整合させる。 +- 同一関係の重複、名称 identity の衝突、implication cycle その他、統合によって新たに生じる constraint violation を事前に検出し、解決不能な状態を部分適用しない。 +- 合併により波及する変更は一つの `event` として束ね、各 version 管理対象はその event を参照する新しい snapshot を生成する。 +- immutable history は過去の Tag identity を保持し、過去 version を現在の統合先 Tag へ書き換えない。 + +### 6.8.2 分離 + +Tag 分離は、一つの Tag に混在していた複数概念を複数の Tag へ分ける論理操作である。 + +- 分離後の各 Tag へ、Post membership、Tag Name / alias、Tag Description、Material、implication、Tag Group membership その他の current relation を明示的に配分できる。 +- 一つの relation を複数の分離先へ複製するか、いずれか一つへ移すかは relation の domain semantics に従って決定し、機械的な全複製を既定としない。 +- 分離に伴う変更は一つの `event` として整合して保存し、途中状態を確定しない。 +- 分離前の immutable history は当時の一つの Tag として保持し、後から複数 Tag へ再解釈して書き換えない。 + +### 考え方・理由 + +合併・分離を正式操作とするのは、Tag identity の整理が名称だけでなく Post membership、説明、素材、implication、group membership 等へ横断的に波及するためである。個別 table の手動更新へ分解すると、同じ概念整理が複数 event に裂け、履歴と current state の因果関係が失われるため、一つの domain operation / event として扱う。 + +## 6.9 タグ廃止 + +タグ廃止は新規利用を止める分類上の状態である。 + +- 廃止タグを新規投稿へ付与できない。 +- 既存投稿から自動で除去しない。 +- 通常の投稿表示では廃止タグを表示対象から外してよい。 +- 廃止タグを条件とした既存投稿検索は可能とする。 +- autocomplete の通常候補から除外する。 +- 類似度計算の特徴量から除外する。 +- タグ説明では廃止状態を表示する。 +- 廃止と索引抑止を連動させない。 + +## 6.10 タグの物理削除 + +タグの物理削除は admin の明示操作とし、次の規則に従う。 + +- active な `post_tags` は削除する。 +- 履歴スナップショット内のタグ情報は削除しない。 +- `tag_implications` は削除する。 +- `A -> B -> C` の中間 B を削除する場合、循環や重複を生じない限り `A -> C` を補完する。 +- 当該 Tag を分類先としていた Material は未分類状態へ戻し、素材本体を削除しない。 +- タグを主体とする説明その他の従属データは削除する。 +- 当該タグに属する `deerjikists` は削除する。 +- 影響投稿から deerjikist category のタグがなくなる場合は、`ニジラー情報不詳` を付与する。 +- 物理削除操作と、その操作から波及した変更を同一の `event` として記録する。 + +## 6.11 タグ履歴 + +Tag Version は当時の Tag 状態を immutable な自己完結スナップショットとして保持し、Tag 内の version number により版を特定する。version row 固有の surrogate ID は使用しない。具体的な table key と column は `tables.zip` を正本とする。 + +少なくとも category、全 language の Tag Name と primary / alias / auto-generated state、保存 script、Reading、親 Tag、外部 Tag 対応、deerjikist 固有情報、廃止状態、操作主体、event、版生成種別を復元できなければならない。 + +ニジラー情報は独立した version resource を持たず、deerjikist category Tag の current domain data として扱い、その変更履歴を Tag Version の snapshot に併合する。 + +現在の Tag Name、Reading、親子関係、deerjikist data だけを参照して過去表示を構築してはならない。 + +## 6.12 外部タグ + +外部タグは通常の内部 `tags` から物理的・論理的に分離した名称台帳として扱う。Nico タグは `nico_tags` その他の専用 table に保持し、`category = 'nico'` を用いない。 + +- 通常の手入力から作成・付与できない。 +- 外部同期または専用管理導線だけが更新できる。 +- 外部タグ同士を内部 Tag として含意させない。 +- 外部タグは廃止できない。 +- 外部タグに通常のタグ説明や素材を直接持たせない。 +- source の名称を内部 Tag Name 用 formatting、identity、allowed-character policy で勝手に改変しない。 +- 原文名称は source の値を正として保持する。翻訳が存在する場合だけ共通 translation resource を nullable に参照し、翻訳によって原文を上書きしない。 +- Nico Tag 自体に翻訳目的だけの version table を設けない。翻訳の履歴と翻訳時点の原文情報は translation 側の immutable version が保持する。 +- 上流 system の collation 等ですでに失われた文字差は復元できないものとして受け入れるが、タグ広場内で追加の差を失わせない。 +- 外部サービス上の名称変更・削除・再付与を必要に応じて履歴として記録できる。 + +## 6.13 外部タグと内部タグの対応 + +外部タグと内部 Tag の対応は、多対多または明示された対応関係として保持する。 + +- 対応先は内部 Tag だけとする。 +- 対応変更を履歴へ記録する。 +- 廃止された内部 Tag への対応は警告する。 +- 対応があることだけで、全投稿へ無条件に内部 Tag を付与しない。同期規則と抑止規則を適用する。 + +## 6.14 外部タグ連携抑止 + +次の単位で外部タグ連携を抑止できる。 + +- platform 全体。 +- 外部タグ単位。 +- 投稿単位。 +- 同期元単位。 +- 特定の外部タグと内部 Tag の組合せ。 + +抑止理由、操作者、作成日時、失効日時を保持する。 + +## 6.15 タググループ + +Tag Group は、Tag および他の Tag Group を人為的なまとまりとして整理する structured collection resource とする。DB 上の resource 名は `tag_groups` とし、UI 上では Tag domain 配下の第二級 resource として扱う。 + +- Tag Group は通常の Tag と同一 resource ではない。 +- Tag Group は Tag を含められる。 +- Tag Group は他の Tag Group を含められる。 +- Tag Group は curated collection であり、`tag_implications` が表す semantic entailment と混同してはならない。 +- Tag と Tag Group の membership は polymorphic な共通 member table へ押し込まず、`tag_group_tags` と `tag_group_groups` に分離する。 +- nested Tag Group の自己参照および循環参照は禁止し、Tag Group の包含関係は DAG とする。 +- membership は明示的な `position` を持てるが、順序は必須ではない。Tag Group 本体に `sort_mode` は持たず、`position = NULL` は明示順を持たないことを表す。 +- 同一親 Tag Group の `tag_group_tags.position` と `tag_group_groups.position` は、値を持つ場合は共通の論理的な順序空間として扱う。二 table を跨ぐ整合は application invariant とする。 +- 明示順を持たない membership の表示には、当該表示 locale に対する通常の locale-specific sorting policy を利用する。 +- Tag Group は `name` と nullable な `description` を持ち、それぞれ翻訳が存在する場合だけ `name_translation_id` / `description_translation_id` から共通 `translations` を参照する。 +- Tag Group は immutable な version history を持ち、その版における直接 membership を JSON snapshot として保持する。member が Tag の場合は `tag_id` と対象 Tag の `version_no`、nested Tag Group の場合は `tag_group_id` と対象 Tag Group の `version_no` を保持し、必要に応じて `position` も保持する。discriminator 用の `type` は持たない。 +- nested Tag Group の中身を親 Tag Group Version の `members` へ再帰展開しない。親はその時点の直接 membership を snapshot し、関聯先の immutable version は当時の関聯 resource を特定するために利用する。 +- 旧 Wiki の Tag Group は、単なる Markdown のタグ一覧として残さず、Tag link、Tag Group link、見出し、自由記述その他を可能な範囲で structured Tag Group、nested Tag Group、description へ移行する。現行 Wiki が自由記述を許すため、完全な無人・機械 migration は要件とせず、人間による確認・修正を許容する。 + + +## 6.16 考え方・理由 + +Tags domain で「意味」「名称」「読み」「semantic implication」「curated grouping」「外部 service のタグ」を別概念として維持するのは、これらを一つの relation / name model へ統合すると、検索、自動展開、履歴、翻訳のいずれかで意味が崩れるためである。 + +Tag Name の multilingual / script 設計で表示変換を正本 data の identity にしないのは、presentation script の変更によって Tag identity や translation 単位まで変化させないためである。Tag Group で implication を代用しないのも、collection membership と semantic entailment は自動展開時の意味が異なるためである。 + +category の enum code と利用者向け表示名を分けるのは、data / API の stable identifier を locale ごとの UI vocabulary 変更に巻き込まないためである。 + +# 7. タグ説明・ヘルプ + +## 7.1 位置づけ + +従来の汎用 Wiki をタグ広場の独立目的として扱わない。旧 Wiki が担っていた情報は、内容に応じて Tag Description、Help、structured Tag Group その他の明示された resource へ分離する。 + +説明文書は次を中心とする。 + +- Tag の意味、背景、用法を記載するタグ説明。 +- タグ広場の利用方法、運用、概念を説明する Help。 +- 明示された名称空間に属する体系説明。 + +Tag Group は説明文書ではなく Tag domain 配下の structured collection resource とし、Markdown 本文だけを正本として Tag membership を表現しない。 + +任意の話題について無制限にページを作成できる一般 Wiki として運用してはならない。 + +## 7.2 名称空間と Help の構成 + +説明文書は少なくとも次の名称空間を扱える。 + +| 名称空間 | 用途 | +| --- | --- | +| Tag | 個別 Tag の説明 | +| `help:` | 利用方法、運用、概念説明 | +| `pool_group:` | 将来の pool 群に関する説明 | + +名称空間の追加は、用途と既存名称との衝突規則を定めた上で行う。 + +Help は少なくとも次の情報へ到達できる構成とする。 + +- タグ広場について。 +- 投稿を見る・探す、投稿を追加・編輯する方法。 +- Tag を付ける方法、Tag Name・alias・Reading の考え方、命名方針。 +- 検索の基本と検索構文。 +- 素材。 +- 履歴・差戻し。 +- account・権限・設定。 +- Help の編輯・翻訳方針。 + +「Help の Help」を navigation 上の再帰的なカテゴリとして作らない。Help 自体の説明が必要な場合は通常の Help article として扱う。 + +## 7.3 タグ説明 + +タグ説明は Tag と content language の組合せごとに一つの current content を持ち、language ごとの immutable version history を持つ。Tag Name の改名に description identity を従属させない。current content と version history の具体的な table key / foreign key は `tables.zip` を正本とする。 + +- 同じ Tag の異なる language の説明は独立した article として編輯でき、内容の完全一致を要求しない。 +- requested language の説明がない場合、別 language の revision から機械翻訳を表示用 fallback として生成してよいが、それだけで正式 revision として保存してはならない。 +- 利用者が別 language の revision を基に正式な翻訳説明を作成する場合、翻訳元の language と version number を provenance として保持する。 +- translation provenance は必ず同じ Tag の source revision を指し、別 Tag の version を翻訳元として指定できてはならない。 +- source presentation script / locale は追加 provenance として保持しない。 +- 一度正式な翻訳説明として作成された後は、元 language の更新だけを理由に自動上書きしない。 +- 別名からアクセスした場合も同じ Tag の説明へ解決する。 +- 廃止 Tag の説明には廃止状態を表示する。 +- 外部タグには通常のタグ説明を持たせない。 +- 空本文は外部検索へ索引させない。 + +## 7.4 Help の多言語管理 + +Help は stable な article identifier を identity とし、その下に language ごとの current content と immutable version history を持つ。Help は日本語を canonical source とする。 + +利用者が通常閲覧する Help resource の単位は current locale / content language の Help content とする。別 language の Help content は locale を切り替えたときに同じ article の対応内容として表示し、通常の一画面で全 language の本文を一括編輯する resource とは扱わない。このため version sequence は article 全言語で一本にせず、language ごとの Help content を単位として進める。 + +Help current content は article identifier と content language の組合せを自然 identity とし、surrogate content ID は持たない。Help Version も同じ article / language 内の version number で版を特定し、version row 固有の surrogate ID は持たない。具体的な physical key は `tables.zip` を正本とする。 + +- canonical source の編輯は admin に限定する。 +- 翻訳版は member 以上が編輯できる。 +- 翻訳 revision は、同じ Help article のどの source language / version を翻訳元としたか追跡できなければならない。 +- translation provenance は source language と source version number だけを保持し、source presentation script / locale は保持しない。 +- provenance は同じ Help article の source revision 以外を参照できてはならない。 +- canonical source が進んだ場合、翻訳版が stale であることを source version との関係から判定・表示できる。stale flag を重複して正本化しない。 +- 同一 language 内の script variant は別翻訳を二重管理することを原則とせず、表示時変換を用いる。 + +## 7.5 作成・更新 + +説明文書の作成・更新は本章の権限規則に従う。 + +- CRLF を LF へ正規化する。 +- 不正 UTF-8 は安全な置換を行う。 +- 編輯開始時の base revision number により競合を検出する。 +- 現在の revision と一致しない場合は 409 conflict とする。 +- 保存時に immutable な履歴を作成する。 +- title、body、language、namespace、操作者、関聯 Tag、translation provenance その他の必要情報を履歴へ保持する。 + +## 7.6 履歴と差分 + +説明文書の履歴は、過去版を完全に復元できなければならない。 + +- 行単位差分を表示できる。 +- 現在の Tag Name 変更後も当時の表示に必要な情報を保持する。 +- 履歴は更新または上書きしない。 +- Help 翻訳では canonical source revision との対応を表示できる。 +- 物理削除が必要な場合も、保持可否を公開状態と法的要件に基づいて判断する。 + +## 7.7 本文検索 + +検索順位は次を基準とする。 + +1. title 完全一致。 +2. title 前方一致。 +3. title 部分一致。 +4. body 部分一致。 + +本文検索にも共通 search transformation を適用する。UI locale と異なる language の文書を検索対象から機械的に除外しない。 + +内部検索抑止された文書を通常検索へ表示しない。廃止 Tag の説明は、廃止状態を明示した上で検索可能とする。 + +## 7.8 自動リンク + +説明本文中の Tag Name を自動リンクできる。 + +次を破壊してはならない。 + +- Markdown link の URL。 +- 既存 link の label と構造。 +- code span。 +- code block。 +- raw HTML の属性値。 + +長い名称と短い名称が重なる場合、tokenize 後に最長一致を優先する。alias、他 language 名、Reading を自動リンク対象へ含める場合も、曖昧な Tag を無言で選択してはならない。 + +## 7.9 asset + +説明文書は画像その他の asset を持てる。 + +- ページ内で安定した番号を採番する。 +- file の SHA-256 を保持する。 +- 同一文書内の同一 SHA-256 重複を禁止する。 +- object storage に保存する。 +- alt text を必須または強く推奨する。 +- asset 追加・削除も履歴へ記録する。 +- 索引抑止または公開抑止された文書の asset に同等の policy を適用する。 + + +## 7.10 考え方・理由 + +Tag Description、Help、structured Tag Group を分離するのは、自由記述 document 一種類に Tag の意味説明、system help、machine-readable membership を兼任させると、編輯権限・履歴単位・検索・relation の正本が曖昧になるためである。文章として残す知識と、構造化して機械処理する relation を別 resource にする。 + +履歴を immutable revision と current row の二層にするのは、現在内容を単純に取得しながら、過去版を後続編輯から独立して復元するためである。翻訳 provenance と stale 判定を revision relation から導出することで、翻訳本文へ「古い/新しい」という mutable flag を重複保持しない。 + +## 7.11 法的文書 + +利用規約、privacy policy その他の法的文書は Help とは別の第一級 resource とする。Help の一種として扱わず、`/:locale/help` の「文書」domain に統合しない。 + +法的文書本文は source-controlled static document とし、repository 内の source file を正本として管理する。法的文書の current / version 本文を DB の文書 table で正本管理しない。更新は source の差分 review と deployment を通じて行える構成とする。 + +タグ広場自身から現在版だけでなく過去版を閲覧できなければならない。各 language の版は明示的な version を持ち、翻訳版は翻訳元の source language と source version number を記録する。翻訳元より canonical source が進んでいる場合、利用者へその翻訳がどの source version に基づくかを表示できなければならない。source presentation script / locale は翻訳 provenance として要求しない。 + +法的文書の source file format を MDX のままとするか Markdown + metadata その他とするか、および user acceptance history / 重要改定時の再同意機能を採用するかは未確定事項に従う。 + +### 考え方・理由 + +法的文書を Help から分離するのは、利用方法の説明と、利用条件・privacy その他の法的 publication は resource identity、公開責任、revision semantics が異なるためである。一方、Markdown rendering、diff、language/version navigation 等の表示 infrastructure は必要に応じて Help と共有してよい。 + +法的文書を source-controlled static document とするのは、共同編輯 content と異なり、変更を source diff と review に載せる方が責任境界を明確にできるためである。ただし Git history だけを利用者向け version history の代わりにはせず、application から過去版と translation provenance を照会可能にする。 + +# 8. 素材 + +## 8.1 基本 + +Material は、タグ広場で管理する**名前を持たない具体的な素材 resource**である。意味上の名称は Material 自身へ持たせず、その Material が所属する `material` category Tag と Tag Name が担う。file Material は共通 Blob 台帳を、URL-only Material は外部 URL を実体参照として持つ。 + +素材機能は、界隈で流通してきた素材集と同等以上の検索・選択・配布・制作利用を Web 上で提供する。外部素材集から取り込んだ binary はタグ広場自身の Blob / object storage に保持し、同期元が利用不能になっても、それだけを理由にタグ広場側の Material を失わせない。 + +Material は、分類先 Tag、file Blob または外部 URL、thumbnail、canonical export path、current version、作成・更新 attribution 等を必要に応じて管理する。具体的な column、排他 CHECK、UNIQUE その他は `tables.zip` を正本とする。 + +- 通常の分類済み Material は `material` category Tag に所属する。同期取込み直後は未分類状態を許可する。 +- file と URL-only は排他的な Material kind とし、一つの Material が両方を同時に実体参照として持たない。 +- file Material は canonical export path を持ち、URL-only Material は持たない。 +- 一つの Tag に複数 Material が所属できる。 +- Material 自身に semantic name、親 Material、論理削除状態、同期元 identity を直接兼任させない。 +- Material の通常削除は物理削除とする。削除前の状態は immutable Material Version により復元可能にし、Material の削除だけを理由に Blob を purge しない。 + +file / URL-only の排他、および file Material に canonical export path が存在することは DB constraint と application validation の双方で保証する。 + +## 8.2 Material と Tag の意味分担 + +Material の意味・名称・実データの識別を混同しない。 + +```text +意味が違う + ↓ +Tag を分ける + +意味は同じだがデータ表現が違う + ↓ +同じ Tag に複数 Material + +個々の実データを機械的に特定する + ↓ +Material ID + +binary content identity + ↓ +Blob.sha256 +``` + +- Material 自身に人間向けの名称を設けない。source filename、同期元 path、export filename を Material の semantic name とみなしてはならない。 +- 同じ `material` Tag に複数 Material が所属する場合、それらは「名前で区別する必要がない程度には同じ素材概念」でなければならない。人間が意味上の名称を付けて区別する必要がある場合は Material を命名せず、Tag をより細かく分ける。 +- `伊地知ニジカ_パーツ` のような曖昧な umbrella Tag を、外部 folder hierarchy を再現するためだけに作成してはならない。必要に応じて `伊地知ニジカ_顔`、`伊地知ニジカ_胴体_左`、さらに向き・表情等まで含む十分に細粒度な Material Tag を用いる。 +- `_` は Tag Name の文字として使用できるが、semantic hierarchy、part-of、implication その他を自動推論する delimiter として扱ってはならない。 +- Material Tag が別 Tag の成立条件を満たす場合だけ `tag_implications` を利用する。「そのキャラクターの素材である」「その作品に属する」等の文脈だけを理由に implication を作ってはならない。 +- Material 同士を親子・従属・代表関係で grouping しない。旧「包摂素材」の概念は採用せず、`materials.parent_id`、同一 Tag 内の primary / representative Material、sibling position その他を設けない。同じ Tag の Material は対等な resource とする。 + +`character` その他の非-`material` Tag が、自身の概念を具体的・視覚的に示すため Material を代表素材として参照することはできる。この関係は Material の所属 Tag、同一 Material Tag 内の複数 Material の整理、投稿 Tag の自動付与とは別概念とする。 + +## 8.3 権限 + +- 素材の新規作成は member 以上とする。URL-only であっても guest は新規作成できない。 +- 既存素材の更新は guest、member、admin が行える。guest の更新も member と同じ version / event 記録経路を通し、履歴を残さない更新経路を設けない。 +- guest 更新の成立条件として、更新前後を immutable version から復元でき、操作を `events` から追跡・差戻しできなければならない。 +- file upload を guest に許可しない。 +- 同期抑止の登録は member 以上とする。 +- 同期元の登録・編輯は admin を標準とする。 +- 素材 download には公開 policy と rate limit を適用する。 + +## 8.4 作成・更新・削除 + +素材の作成・更新では次を行う。 + +1. 権限を確認する。 +2. Tag を解決する。 +3. file がある場合は SHA-256 を計算し、共通 Blob 台帳から同一 binary を解決する。 +4. purge 済み SHA-256 の file を拒否する。 +5. MIME その他の file metadata を検証する。 +6. Material 本体を保存する。 +7. file Material では canonical `export_path` を検証・保存する。 +8. material version を記録する。 +9. 画像または動画の場合は thumbnail を生成する。 + +Material として直接保持する file は image、video、audio を対象とする。text、archive、Google native document その他の非対象 file を、同期経路だけの例外として Material 化してはならない。同じ validation を手動作成と同期取込みで共有する。 + +Material の `blob_id` を変更する更新は、同じ Material の file 内容が変更されたことを表す。変更前の Blob 参照は Material Version に残す。Material を物理削除しても、履歴または他 resource から参照される Blob を通常 cleanup として削除してはならない。 + +## 8.5 一覧・検索 + +素材検索は少なくとも次を扱う。 + +| 条件 | 仕様 | +| --- | --- | +| `q` | Tag Name、URL、source path、export path その他の検索可能な表示情報 | +| `tag_state` | `all`, `tagged`, `untagged` | +| `media_kind` | image、video、audio、URL-only | +| `tag_id` | 指定 Tag | +| `include_descendants` | 子孫 Tag を含める | +| `created_from/to` | 作成日時 | +| `updated_from/to` | 更新日時 | +| `sort` | 作成日時、更新日時、Tag Name、媒体種別、byte size、version、ID | +| `direction` | `asc`, `desc` | +| `page`, `limit` | ページネーション | + +未分類素材を通常素材と区別して表示する。同一 Tag に複数 Material がある場合は preview、media kind、寸法・duration その他の客観的 metadata、byte size、Material ID 等により識別できるようにし、Material-local name を導入しない。 + +## 8.6 API 表現 + +素材 API は少なくとも次を返す。 + +- ID。 +- version number。 +- nullable な Tag の ID、requested display locale で解決した name、category、廃止状態。 +- URL または file URL。 +- thumbnail URL。 +- media kind。 +- content type。 +- file byte size。 +- 同期元・取込元情報。 +- canonical export path。 +- 作成者・更新者。 +- 対応 Tag の説明本文。 + +## 8.7 履歴 + +Material Version は immutable な自己完結スナップショットとし、Material 内の version number により版を特定する。version row 固有の surrogate ID は使用しない。具体的な table key と column は `tables.zip` を正本とする。 + +各版では少なくとも、当時の Tag とその version、Blob または外部 URL、thumbnail、全 Material Source、canonical export path、操作主体、event、版生成種別を復元できなければならない。 + +Blob は immutable な content identity ledger であるため、Material Version は Blob identity を参照し、Blob metadata を各 version へ重複 snapshot することを必須としない。Material Source の current state が後から変更・削除されても、当時どの同期元・外部 file・source path から来たかを復元できなければならない。 + +現在の Material を更新または物理削除しても、各 Material Version が参照する Blob identity と当時の source / export path を失ってはならない。 + +## 8.8 object storage と thumbnail + +file 本体は S3 互換 object storage に保存し、共通 Blob 台帳を介して参照する。Blob は binary content identity を表し、同一 SHA-256 の binary は素材、thumbnail その他の利用箇所を跨いで同一 Blob を再利用する。元 file 名その他の利用箇所固有 metadata は Blob ではなく参照側に保持する。 + +- Material の `blob_id` は UNIQUE とし、同一 binary を複数の active Material として重複保持しない。 +- 公開抑止された素材を public URL で無制限に公開しない。 +- 署名 URL その他の access policy を使用できる。 +- purge 済み Blob は新しい Material、thumbnail その他から参照できない。 +- Material の通常削除と Blob purge を分離する。Material の削除は Blob の binary 削除を意味しない。 +- admin が権利侵害、機密、違法性、malware その他の理由で binary 自体を保持すべきでないと判断した場合は、Blob 台帳の content purge を利用する。 + +thumbnail は次を標準とする。 + +| 対象 | 処理 | +| --- | --- | +| image | 180 px 程度の JPEG preview | +| video | 1 秒地点、失敗時は 0 秒地点から抽出 | +| audio | 初期 V2 では専用 thumbnail を必須としない | +| URL-only | 原則として生成しない | + +thumbnail 生成失敗は素材本体の保存失敗とせず、再生成可能なエラーとして記録する。 + +## 8.9 canonical export path と ZIP + +file Material は、サイト内で共通の canonical export path を**一つだけ**持つ。初期 V2 では user ごと・profile ごとに複数の canonical path を持たせず、Material 自身の canonical export path を正とする。 + +- canonical export path は file Material ごとに一件であり、system 全体で一意とする。 +- absolute path、drive letter、backslash、NUL、`.`、`..`、連続 slash、末尾 slash を禁止する。 +- URL-only Material は canonical export path を持たず、ZIP へ含めない。 +- 通常の全体 ZIP、Tag / subtree、検索結果、画面上で複数選択した任意の Material の ZIP のいずれでも、ZIP 内 path は同じ canonical export path を使用する。 +- folder resource は設けない。folder は各 Material の canonical export path に含まれる `/` 区切りから導出するため、空 folder は保持・出力しない。 +- folder を整理する操作は、配下 Material の canonical export path を一括変更する操作として実現できる。 +- path rewrite rule は初期 V2 では設けない。同期元の旧 folder に後から新規 file が追加され、canonical tree に旧 folder が再登場する可能性は許容し、必要に応じて人間が再整理する。 +- path 重複、Blob 消失その他の不整合を明示的な error とする。 +- 公開抑止された Material を通常 export しない。索引抑止だけの Material は export してよい。 + +同期取込みによる Material の初回作成では、同期元に設定された export path prefix と、その Material Source の source path から canonical export path の初期値を作れる。**これは初期値に限る。** 人間が canonical export path を変更した後、同期元の rename / move / 再同期を理由に自動で元へ戻してはならない。 + +将来、user-specific な素材 collection、用途別 export profile、動画編輯 starter package その他を追加してよいが、それらはサイト共通の canonical export path を置き換えず、別 resource として設計する。 + +## 8.10 同期抑止 + +初期 V2 の素材同期抑止は、同期元 root からの `source_path` prefix により subtree を除外する構造を基本とする。 + +- suppression は source path prefix を保持し、該当 subtree の新規取込み・更新を抑止できる。 +- suppression 登録時に、現在の Material がその抑止対象だけを根拠として保持されている場合は、規定された操作として Material を物理削除できる。ただし Blob は通常 purge しない。 +- binary 自体を保持禁止とする必要がある場合は、別の import-block table を重複して持つことを必須とせず、Blob 台帳の SHA-256 と terminal content purge を利用する。 +- 外部 path は同期元からの相対 path に限定し、absolute path、`.`、`..`、NUL、連続 slash、末尾 slash を禁止する。 +- suppression は解除できなければならない。 + +抑止理由は、著作権上の懸念、権利者からの削除要請、機微情報、個人情報、malware、source owner の要請その他を記録できる。 + +## 8.11 同期元・Material Source + +初期 V2 の自動素材同期は Google Drive folder を対象とする。Google Drive を永続的に特権化することを設計原則とはせず、他 storage service や Git その他の同期が実際に必要になった時点で migration により拡張する。初期 schema を、利用予定のない provider の抽象化だけのために複雑化しない。 + +同期元 resource は、同期 root の人間向け名称、Google Drive folder code、初回 canonical export path の生成補助、最終同期完了日時その他、同期 root 自身に属する current state を持つ。外部 identifier はタグ広場内部 ID と混同しない。具体的な column と constraint は `tables.zip` を正本とする。 + +- URI 単体、Google Drive 単一 file、未使用 source kind 等を初期 V2 の同期元 resource として先行保持しない。 +- 同期による Tag 自動付与を行わず、新規取込み Material は必要に応じて未分類状態とする。 +- export path prefix は初回 canonical export path の生成補助であり、同期元 path と canonical export path を恒久的に同一視しない。 + +Material ごとの取込元・来歴は Material 本体から分離した Material Source relation に保持する。外部 source object の identity は同期元と外部 file code の組合せとし、source row 専用の surrogate ID は必要としない。source path は同期元 root から見た相対 path であり、Material の名称でも canonical export path でもない。 + +一つの Material は複数の Material Source を持てる。同一 binary が別 source / path から検出された場合は Blob SHA-256 により同じ Material へ収束させることを基本とする。source file の内容が変更された場合は新しい binary を Blob 台帳で解決し、Material の content または source relation を整合させる。 + +同期元から既存 file が消失しても、それだけを理由に Material、Blob、Material Source を自動削除・変更しない。Material Source は「そこから取り込まれた」という来歴として保持する。初期 V2 では presence tracking を必須としない。 + +Google Drive 連携は read-only scope を標準とし、service account の権限を必要最小限にする。Google native document を Material として取り込まない。同期は一日一回程度を標準とし、root folder を走査して変化がある file だけを Material / Blob の更新対象とする。 + +## 8.12 Git その他による配布 + +Git と ZIP は別々の canonical folder 構成を持たず、Material の `export_path` が表す同じ配布 tree を共有する。 + +Git を用いる場合、repository は manifest、索引、取得 script その他を管理できる。大容量 binary を repository 自体へ無制限に保持することは要件とせず、object storage から取得する構成を利用できる。 + +- manifest は少なくとも Material ID、Blob SHA-256、content type、byte size、canonical `export_path` その他の取得・検証に必要な情報を表現できる。 +- Git 用 manifest / script の都合で、ZIP と異なる第二の canonical path を導入してはならない。 +- binary を materialise する場合は Material の canonical export path と同じ tree を使用する。 +- 任意の push を検証なしで本番 storage へ反映してはならない。 +- MIME、size、SHA-256、path、権限を検証する。 + + +## 8.13 考え方・理由 + +Material domain では、意味の名前を Tag、具体 resource を Material、binary identity を Blob、取込元を Material Source、配布位置を canonical `export_path` として明確に分ける。source folder をそのまま domain hierarchy とみなしたり、file 名を Material 名へ昇格させたりしない。 + +同期は external source の mirror ではなく、タグ広場へ Material を取り込む入口である。したがって source から消えたことだけでは Material を消さず、人間が編輯した Tag / export path を同期が勝手に戻さない。Google Drive は初期 V2 で実際に使う provider だから実装するだけで、恒久的な特権 provider とする思想ではない。 + +# 9. 外部取得・同期・Preview + +## 9.1 投稿同期の共通原則 + +- URL を正規化して重複を検出する。 +- 外部取得値と利用者の明示編輯値を区別する。 +- 既存投稿を同期だけで除票から自動復帰させない。 +- 同期由来の変更も投稿履歴へ記録する。 +- 外部タグは外部タグ台帳へ記録し、対応規則を通じて内部タグへ反映する。 +- 同期元の一時的な欠損を、利用者が入力した値の削除理由にしない。 +- 外部文字参照や HTML entity は、表示上の文字へ適切に decode して保存する。 +- 過去の履歴スナップショットを外部同期の都合で更新してはならない。 + +## 9.2 Nico 同期 + +- 外部 API または同期 script から動画情報を取得する。 +- 投稿が存在しない場合は新規作成する。 +- `タグ希望`、`bot操作`、`ニコニコ`、`動画` を必要に応じて付与する。 +- Nico 上の生タグを外部タグとして記録する。 +- 外部タグと内部タグの対応および抑止を適用する。 +- 投稿履歴と外部タグ履歴を記録する。 + +## 9.3 YouTube 同期 + +- 検索語、playlist その他の定められた入力から候補動画を取得する。 +- URL normaliser で重複を検出する。 +- `タグ希望`、`bot操作`、`YouTube`、`動画` を必要に応じて付与する。 +- channel と deerjikist の対応がある場合は deerjikist tag を付与する。 +- 対応が不明な場合は `ニジラー情報不詳` を付与する。 + +## 9.4 bot 操作投稿の整理・自動 Tag 補完 + +同期、自動取得、規則、AI その他の機械処理だけでは十分な分類にならない投稿を、人間の確認と自動分類支援を組み合わせて整理できる。自動分類は、既存 Tag を無制限に推測する一つの汎用 AI へ依存せず、根拠の強さに応じた複数の検出・対応機構を組み合わせる。 + +- `bot操作` を含む投稿を検索できる。 +- `bot操作` は現在状態に未確認の機械操作の影響が残っていることを示し、作成者 provenance として扱わない。 +- 別投稿からタグ集合を import できる。 +- 既存の内部 Tag 全体を探索対象とし、外部タグとの固定対応だけに依存せず Tag 候補を提示できる。 +- 自動分類の主要な根拠の一つとして既知 Material の content detection を用いる。投稿内で Material を検出した場合、その Material が属する `material` category Tag を付与または候補化し、通常の `tag_implications` により親 Tag を展開できる。 +- Material detection は Blob の SHA-256 完全一致だけに限定しない。画像、音声、動画その他の媒体に応じた fingerprint、perceptual feature その他により、再 encode、切抜き、resize、音量変更、mix その他の加工後利用も可能な範囲で検出できる構成とする。 +- Material が動画・音声の特定区間で検出された場合は、その検出範囲を `post_tag_sections` の局所記載候補として利用できる。 +- character、meme その他、検出された material Tag から `tag_implications` により導ける Tag は、別の画像認識による重複推定を必須としない。 +- `meta` は system / platform metadata や rule、`deerjikist` は投稿者・channel その他の既知対応を主要な根拠として補完できる。 +- Material や明示 metadata から直接導けない `general` その他の Tag は、必要に応じて画像・動画・音声の content recognition、multimodal model その他で既存 Tag から候補を探索できる。ただしこれを Material detection より優先する必須経路とはしない。 +- 外部タグ、metadata、既存 Tag 関係、類似投稿その他も候補発見 signal として利用できるが、Post / Tag similarity を主たる分類根拠とはせず、候補 retrieval その他の補助 signal とする。 +- 人間が現在状態を確認して整理完了と判断した時点で `bot操作` を外せる。 +- 人間確認後に機械処理が新たに投稿状態へ影響を加えた場合は、再度 `bot操作` を付与する。 +- Material detection その他の解析は、低い storage 使用量と server resource 使用量で運用できる構成を重視する。解析頻度、frame sampling、fingerprint algorithm、保持する feature size その他は、実測に基づく実装・性能調整事項とする。 +- 進捗は Gitea の課題または運用 dashboard で確認できる。 + +### 9.4.1 楽曲 Tag の自動付与 + +`bot操作` その他の自動整理対象に対して、楽曲 Tag を自動付与する機能を採用する。自動付与も通常の Tag membership validation と event / history の経路を通し、誤付与を差戻し可能にする。 + +trigger、判定根拠、confidence、human review の要否その他の具体的方法は未確定事項に従う。 + +## 9.5 Preview API の共通要件 + +Preview API はサーバから外部 URL へ接続するため、SSRF と resource abuse を前提に設計する。 + +- HTTP と HTTPS だけを許可する。 +- redirect 先を毎回検査する。 +- DNS 解決後の IP を検査する。 +- timeout、response size、redirect 回数を制限する。 +- cache を使用する。 +- IP、user、target host ごとに rate limit を設ける。 +- 同時実行数と memory 使用量を制限する。 + +## 9.6 title preview + +- HTML だけを処理対象とする。 +- content length 上限を設ける。 +- title を安全に decode・正規化する。 +- script 実行を必要としない取得を優先する。 +- 取得失敗を利用者が理解できる error として返す。 + +## 9.7 thumbnail preview + +- headless browser または安全な画像取得処理を利用できる。 +- title preview より厳しい rate limit を設ける。 +- browser context を request ごとに隔離する。 +- download、popup、permission request その他を無効化する。 +- 出力画像の寸法と file size を制限する。 + +## 9.8 SSRF 防御 + +少なくとも次への接続を拒否する。 + +- loopback。 +- private address。 +- link-local。 +- multicast。 +- cloud metadata address。 +- UNIX socket その他の local resource。 +- `file:`、`ftp:`、`gopher:` その他の非 HTTP(S) scheme。 +- redirect 後に上記へ到達する URL。 + +## 9.9 外部取得 metadata の翻訳 + +外部 service から取得した title、description その他の文字列 metadata は、取得元の原文を各 resource の正本として保持する。翻訳によって原文を上書きしない。 + +- 翻訳が一件も存在しない resource に translation record を先行作成しない。 +- 翻訳が発生した場合だけ、対象 field から共通 translation resource を nullable に参照する。 +- 翻訳の現在値と履歴は共通 translation 機構で管理し、translation version は翻訳時点の原文情報を復元できる immutable snapshot とする。 +- resource 自体に既存の履歴がないことを理由に、翻訳専用の resource version table を新設しない。 + + + +## 9.10 考え方・理由 + +外部取得・同期は、外部 source を正本としてタグ広場全体を mirror する機能ではない。外部 data は candidate / provenance / 自動取得値として扱い、人間が確定した current state を無言で破壊しない。 + +Preview は convenience 機能であって network boundary を緩める理由にはならないため、SSRF 防御を共通 service に集約する。platform ごとの差異は adapter / normaliser に閉じ込め、中核 Post / Tag model を provider 固有 schema に従属させない。 + +# 10. 検索・類似度・索引 + +## 10.1 検索共通原則 + +- 投稿、Tag、Tag Name、Reading、タグ説明、Help、素材を用途に応じて検索できる。 +- UI locale に検索対象 language を固定せず、異なる language の Tag Name からも候補へ到達できる。 +- 正本 table に汎用 `search_key` column を持たせることを必須としない。 +- 検索時または search index 作成時に、検索語と対象値へ同じ search transformation を適用する。 +- Unicode が標準で定義する canonical equivalence、composition / decomposition その他の機械的処理は Unicode 対応 library / algorithm へ委ねる。個別文字の大量 rule として DB へ列挙しない。 +- タグ広場固有の検索同定は `search_rules` として DB 管理する。各 rule は `value` を人間が選んだ `representative` へ一段だけ写像し、rule 同士を chain しない。代表値を変更する場合は既存 rule を新代表値へ直接向け直す。rule は特定 locale に閉じず、必要な全 language の検索へ共通適用することを基本とする。 +- search equivalence は registration identity より緩くできる。既存 Tag Name と search 上同一・類似になる新規名称には警告を出せるが、それだけで登録禁止とはしない。 +- 正本へ `search_key` を持たせない場合も、再生成可能な search index / cache に transformed value を保持してよい。 +- search rule 変更時は search index を再生成できなければならない。 +- 廃止、索引抑止、内部検索抑止、公開抑止、除票を区別する。 +- 大量結果を無制限に返さず、安定したページネーションを行う。 +- sort key が同値の場合の tie-breaker を定める。 +- 同じ query が画面ごとに異なる意味にならないよう共通 parser を使用する。 + +## 10.2 autocomplete + +- primary name、alias、他 language の Tag Name、Reading から検索できる。 +- alias を明示選択した場合は同じ Tag の primary name へ解決する。 +- 他 language の名称または Reading を候補として明示選択した場合は Tag ID を確定し、現在の表示 language の primary name へ置換してよい。 +- 他 language の名称を生入力しただけの場合は、同綴異義語を考慮し、無言で現在 language の Tag へ解決しない。 +- generated name も候補・検索対象とし、`auto_generated` による絞込みを可能とする。 +- 廃止 Tag を通常候補から除外する。 +- 内部検索抑止、公開抑止された Tag を候補から除外する。 +- 外部タグを通常の投稿 Tag 入力候補へ混入させない。 +- category、廃止状態、language、primary / alias、generated その他、候補の意味を判断するために必要な属性を返す。 + +## 10.3 類似度 + +投稿類似度とタグ類似度は、計算結果を cache として保持できる。類似度は意味上の同一性を保証する正本ではなく、関聯候補・分類候補を発見するための派生情報として扱う。 + +### 10.3.1 投稿類似度 + +- 通常の投稿類似度はタグ広場内部の分類体系に基づく Tag 関係を特徴量とする。 +- 外部タグおよび `bot操作`、`タグ希望` その他の workflow / system state を、通常の内部分類 Tag と同列の特徴量へ混入させない。 +- `bot操作` が残っている Post は、人間未確認の機械判断を自己増幅させないため reference corpus / 教師側から原則除外する。 +- `bot操作` が残っている Post 自体を類似検索・分類支援の対象から除外しない。人間確認済み corpus と比較される query target として利用できる。 +- 廃止 Tag を通常の特徴量から除外する。 +- 除票・公開抑止された投稿を通常の類似候補から除外する。 + +### 10.3.2 タグ類似度 + +- 内部 Tag 同士の共起関係を関聯 Tag・候補 Tag の発見に利用できる。 +- 外部 Tag と内部 Tag の共起類似度も、外部 Tag の対応候補や既存内部 Tag の発見 signal として利用できる。 +- 外部 Tag と内部 Tag の名称が同一であること、または類似度が高いことだけを、意味の同一性・同義関係・無条件の自動対応または自動付与の根拠としてはならない。各内部 Tag のタグ広場上の定義を優先する。 + +### 10.3.3 共通 + +- similarity は Material detection、明示 metadata、固定 mapping その他の直接的な根拠を置き換えるものではない。自動 Tag 補完では候補 retrieval、関聯候補の順位付けその他の補助 signal として利用する。 +- 計算方法、対象版、再計算日時を識別できる。 +- cache を履歴の根拠として使用しない。 +- 類似度は候補 retrieval の一 signal として、将来の自動分類・Tag 補完機構から利用できる。 + +## 10.4 sitemap + +sitemap には通常公開かつ索引可能な detail page を含める。 + +除外対象: + +- 除票。 +- 公開抑止。 +- 索引抑止。 +- 空本文の説明文書。 +- 設定画面。 +- error page。 +- query parameter を含む検索結果。 +- 認証・管理画面。 + +## 10.5 robots meta + +| route 種別 | 既定 | +| --- | --- | +| 投稿詳細 | index。policy により noindex | +| タグ詳細 | index。policy により noindex | +| タグ説明 | index。空本文・policy により noindex | +| ヘルプ | index。policy により noindex | +| 素材詳細 | index。policy により noindex | +| 設定・管理 | noindex | +| error | noindex | + + +## 10.6 考え方・理由 + +検索上の「同じように探せる」と、登録上の「同一である」を分離する。検索 recall を高める transformation を identity constraint に流用すると、意味の違う表記まで登録不能になり得るためである。 + +類似度も正本 relation ではなく導出情報として扱い、廃止・公開 policy と整合する candidate だけを利用する。sitemap / robots は同じ publication policy resolver の結果から生成し、SEO 専用の別 truth を作らない。 + +# 11. API + +## 11.1 共通応答 + +本章は外部 client へ提供する versioned API contract を定める。SvelteKit の内部画面は、本章の HTTP endpoint を経由することを必須とせず、server load / action から同じ application logic を直接利用してよい。 + +- validation error は `type`、`message`、`errors`、`base_errors` を返す。 +- optimistic concurrency conflict は 409 とする。 +- forbidden は 403 とする。 +- not found は 404 とする。 +- public から存在を推測させない対象は 404 とする。 +- page、limit、total、next page の表現を API 全体で統一する。 +- 一覧と detail で同じ属性名に異なる意味を持たせない。 +- API version を跨いで互換性のない変更を無言で導入しない。 + +## 11.2 投稿 API + +少なくとも次を提供する。 + +| endpoint | 役割 | +| --- | --- | +| `GET /posts` | 投稿検索・一覧 | +| `GET /posts/:id` | 投稿詳細 | +| `POST /posts` | 投稿作成 | +| `PUT/PATCH /posts/:id` | 投稿更新 | +| `GET /posts/versions` | 投稿履歴 | +| `POST/DELETE /posts/:id/viewed` | 閲覧済み状態 | +| `POST /posts/:id/discard` | 除票 | +| `POST /posts/:id/restore` | 復帰 | +| `PUT /posts/:id/index_policy` | 公開・索引 policy | +| 投稿 validation endpoint | 新規投稿・一括取込みの事前検証 | + +## 11.3 タグ API + +外部 API は少なくとも次の論理機能を提供する。 + +- Tag の検索・一覧・詳細。 +- language を指定した primary name / alias の取得。 +- Tag Name、Reading、generated name を含む autocomplete / search。 +- Tag の更新、廃止、廃止解除、admin による物理削除。 +- Tag Name の追加・更新、primary 切替、alias 管理、generated name の確認。 +- Reading の追加・更新・削除。 +- 親子関係を含む取得・編輯。 +- Tag Version の取得。 +- Tag Group の一覧・詳細・編輯、および Tag / Tag Group membership の管理。 +- Nico その他の外部タグ台帳と内部 Tag 対応の管理。 + +名称から Tag を取得する API は language を曖昧にしてはならない。同じ文字列が複数 language で別 Tag を指し得ることを前提とする。 + +## 11.4 タグ説明・ヘルプ API + +外部 API は論理的に次を提供する。 + +- Tag と language によるタグ説明の取得。 +- Help identifier と language による取得。 +- namespace / language による一覧。 +- 作成と更新。 +- 本文検索。 +- 変更履歴。 +- revision 間の差分。 +- translation provenance と stale 状態の取得。 +- asset の追加・取得・削除。 +- title / identifier の存在確認。 + +既存の `/wiki` 命名を互換目的で維持する場合も、API が扱う概念は汎用 Wiki ではなく、本章の Tag Description / Help に限定する。 + +## 11.5 素材 API + +| endpoint | 役割 | +| --- | --- | +| `GET /materials` | 素材検索・一覧 | +| `GET /materials/:id` | 素材詳細 | +| `POST /materials` | 素材作成 | +| `PUT/PATCH /materials/:id` | 素材更新 | +| `DELETE /materials/:id` | Material の物理削除。immutable history と Blob 台帳は規則どおり保持 | +| `GET /materials/download.zip` または同等の export 導線 | 全体、Tag / subtree、検索結果、または明示選択した Material の ZIP export。内部 path は Material の canonical export path を使用 | +| Material project export endpoint | 選択 Material から AviUtl project file その他の制作向け package を生成。user-specific package policy は canonical export path と分離 | +| `GET /materials/versions` | 素材履歴 | +| `GET/POST /materials/suppressions` | 同期抑止 | +| 同期元管理 endpoint | 同期元の管理 | +| manifest endpoint | Git・外部配布向け manifest | + +## 11.6 user・設定 API + +| endpoint | 役割 | +| --- | --- | +| `POST /users` | guest 作成 | +| recovery code 検証 endpoint | recovery code を検証して session を発行 | +| recovery code 更新 endpoint | recovery code を更新 | +| account migration endpoint | 旧 origin が発行した一回限りの migration ticket を検証して session を発行 | +| `GET /users/me` | current user | +| `PUT/PATCH /users/:id` | user 更新 | + +設定専用 API の有無、endpoint、resource boundary は 17.3 の settings domain 再設計後に定める。 + +## 11.7 Preview API + +| endpoint | 役割 | +| --- | --- | +| `GET /preview/title` | 外部 page title の安全な取得 | +| `GET /preview/thumbnail` | 外部 page thumbnail または screenshot の安全な取得 | + +## 11.8 外部 application 向け API + +上映会およびグカネータはタグ広場本体の機能に含めず、独立 application とする。タグ広場は、それらを含む外部 client が必要とする一般的な投稿・Tag・動画属性・公開状態・類似度その他を versioned public API として提供できる。 + +- 外部 application 固有の session、comment、game state、質問、score その他をタグ広場 DB に所有させない。 +- 外部 application からタグ広場の状態を更新する必要がある場合は、内部 table を共有せず、認証・認可された公開 API を経由する。 +- write API は current user / application identity、role / scope、BAN、rate limit、`events`、version history その他の通常 policy を迂回してはならない。 + + + +## 11.9 集計表 API と内部 data contract + +集計表の data contract は、resource 定義、抽出条件、行・列軸、時点、表示列、集計結果、明細結果、export job の状態を分離する。外部 API として公開する場合は共通の versioned API 規則を適用し、内部 SvelteKit 画面の取得を HTTP API 経由へ固定しない。 + +- resource 定義の取得では、軸候補、型、演算子、表示可能な列、階層列、既定設定、上限を提供する。画面が resource ごとの物理 schema や SQL を推測してはならない。 +- 集計問合せでは、`latest` / `history`、`summary` / `details`、抽出条件、行・列軸、時間帯・暦・時点を明示的に区別する。結果には確定した対象時点と、実際に適用した条件・集計単位を対応付ける。 +- export job は作成・進捗照会・取消・download 開始確認の責務を区別する。job 識別子と一時 download 権限は resource の恒久 ID ではない。 +- 利用者へ示せる入力不正、集計上限、処理能力不足、失効、権限不足は、機械判定可能な error code と適切な HTTP status または server action の失敗種別に対応付ける。 +- 全 endpoint と内部 server load は同じ server-side の policy / validation / result projection を使用する。クライアント側のマスクを情報保護の唯一の境界としてはならない。 + +## 11.10 考え方・理由 + +Public API は internal SvelteKit page data の副産物ではなく、外部 application が依存する versioned contract とする。内部画面は application service を直接利用できるため、API の都合で内部 data flow を HTTP 化しない。 + +API の resource 名・ID・error・認証は domain model と整合させるが、外部 client の互換性を守るため、内部 table 構造をそのまま露出することもしない。 + +# 12. データモデル + +本章は data model の resource boundary、ownership、current/history の役割を定義する。個別 table の column、物理型、NULL / default、constraint、index は `tables.zip` を正本とする。 + +## 12.1 Users + +Users domain は、利用者 identity、IP、credential その他を、それぞれの寿命と cardinality に応じて分離する。通常 identity は server-side session で確定し、recovery code は復旧用、旧 system の credential は migration 用に寿命を限定する。利用者設定を Users domain のどの resource として保持するかは 17.3 で定める。 + +### 考え方・理由 + +User identity、認証 credential、IP 履歴は同じものではない。設定 domain については、User 一行へ押し込むことも、意味のない設定ごとの table 分割も先に決めず、各設定の ownership と寿命を確定してから物理 schema を定める。 + +## 12.2 Events + +Events domain は user-level logical operation を表し、resource の immutable version と因果関係を結ぶ。event は resource state の snapshot の代用品にはせず、各 version は event を参照しなくても当時状態を復元できなければならない。 + +### 考え方・理由 + +複数 resource が一操作で変化しても、個別 version history だけでは利用者レベルの操作単位を復元しにくい。このため操作 identity と resource state history を二層に分ける。 + +## 12.3 Posts + +Posts domain は URL を中心とした current Post、immutable Post Version、current Tag membership、局所記載、親子 Post relation、video subtype、user ごとの閲覧済み状態を分離する。current relation に inactive history を混在させず、過去状態は Post Version の snapshot で復元する。 + +### 考え方・理由 + +Post は外部 content 本体ではなく、その content を指す knowledge resource である。動画固有 metadata や user-private state を全 Post の共通 row へ押し込まず、それぞれ意味のある subordinate resource / relation として分離する。 + +## 12.4 Tags + +Tags domain は language-independent な Tag identity、language ごとの Tag Name、Reading、Tag Version、semantic implication、Tag Group、外部 Tag 台帳と mapping、外部連携抑止、deerjikist 固有 current data を分離する。 + +Tag Name の primary / alias はすべて直接 Tag を参照し、alias chain を作らない。Tag Group membership と implication は別 relation とする。deerjikist 固有情報は独立した history resource を持たず Tag Version に snapshot する。 + +### 考え方・理由 + +Tag は意味の identity、Tag Name はその意味への language ごとの入口である。名称、発音、semantic entailment、人為的 collection、外部 source identity を同じ relation にまとめると、それぞれの意味と履歴が曖昧になるため分離する。 + +## 12.5 Descriptions + +Descriptions domain は Tag Description と Help を別 resource とする。Tag Description は Tag × content language、Help は stable article identifier の下の language ごとの content を current resource boundary とし、それぞれ独立した immutable version sequence を持つ。 + +翻訳された Tag Description / Help content は source language と source version number を provenance として保持し、同一 owner の source revision だけを参照できなければならない。source presentation script / locale は provenance として保持しない。asset は本文とは別の binary lifecycle を持つ。 + +### 考え方・理由 + +旧 Wiki を汎用 document resource として温存せず、Tag の意味説明、system help、structured membership を責務分解する。current content と immutable version を分けることで、通常表示を単純にしつつ履歴を後続編輯から独立させる。 + +## 12.6 Materials + +Materials domain は具体 file / URL resource、immutable Material Version、外部取込元、同期 root、同期抑止、representative Material relation、共通 Blob ledger を役割ごとに分離する。semantic name は Material Tag / Tag Name、binary identity は Blob、外部 source identity は Material Source、配布位置は canonical export path が担う。 + +### 考え方・理由 + +source filename、source path、semantic name、binary identity、export location は寿命も identity も異なる。これらを一つの Material name や source field に兼任させない。 + +## 12.7 Publication policies + +公開、索引、内部検索、sitemap の判定は横断 publication policy と server-side resolver に集約する。頻繁な検索条件に materialized state を持つ場合も、policy の派生 cache として扱う。 + +### 考え方・理由 + +同じ Tag に起因する抑止は Post、Description、Material 等へ波及するため、各 resource に独立 boolean を増やすと規則が分裂する。横断 abstraction はこの system-wide semantics が必要な範囲に限定する。 + +## 12.8 Blob 台帳と object storage + +Blob は upload event や添付先ではなく、タグ広場が認識した binary content identity を表す system-wide ledger とする。SHA-256 を content identity の正とし、同一 binary は resource や history を跨いで再利用する。storage location、filename、source URL 等を Blob identity と混同しない。 + +通常の参照消失だけを理由に Blob / object を削除しない。binary 自体を保持すべきでない場合だけ terminal content purge を行い、object を物理削除しても content identity と purge の事実は ledger に残す。 + +### 考え方・理由 + +resource ごとの attachment 管理では同一 binary が履歴・thumbnail・Material 間で重複し、current row の削除が過去 binary の消失へ直結する。content identity と利用箇所を分離することで retention と purge を別問題として扱える。 + +## 12.9 object storage の参照寿命 + +current resource から file 参照を外しても、Blob または object を参照消失だけを理由に自動削除してはならない。immutable history は Blob identity を継続して参照できなければならない。 + +### 考え方・理由 + +current reference がないことと、binary を保持する価値・権利がないことは別問題である。reference count だけの GC により history integrity を壊さない。 + +## 12.10 Localisation・text rules + +language、script、locale、Reading、formatting、registration identity、search equivalence は別責務として扱う。stable namespace code 自体を identity とする resource には不要な surrogate ID を追加しない。text rule は用途ごとに分離し、保存表記、登録同一性、検索同一性を一つの normalisation pipeline に混ぜない。 + +Language / Script / Locale は stable identity を持つ参照資源とし、identity code の変更・再利用を行わない。利用されなくなった identity は削除や別 code への付替えではなく deprecation により lifecycle を表現する。Language / Script については、application が対応実装を持つ code だけを system-supported として登録できるようにする。DB row の追加だけを新しい Language / Script の対応完了とみなしてはならない。 + +Flag は Language、Locale、国家、地域その他から独立した stable resource とする。国旗だけでなく地域旗、民族旗その他を扱えるものとし、国家 code を Flag identity の唯一の根拠としない。Language は UI presentation 用の system default として nullable な `default_flag_code` から Flag を参照できるが、この relation は semantic affiliation を意味しない。Locale と Script には default Flag を持たせない。 + +Language / Script / Locale の利用者向け表示名の保存先、および `locales.name` の扱いは未確定事項とする。確定するまで multilingual name table または `locales.name` その他を完成形 schema として本書から推定してはならない。 + +### 考え方・理由 + +language、script、locale、Reading、formatting、identity、search equivalence は似て見えるが異なる責務である。これらを一つの normalisation として扱うと、保存表記を失ったり、検索上似ているだけの名称を登録上同一と誤認したりするため、resource / rule を分離する。 + +Language / Script / Locale を mutable な業務マスタとして扱わず deprecation により lifecycle を表すのは、履歴・翻訳・Tag Name その他から参照される identity を code 変更や削除で別物へすり替えないためである。Language / Script を application-supported code に限定するのは、文字種認識、正規化、transliteration / transcription、表記変換、自動 Tag Name 生成等の振舞いを、DB row の存在だけでは成立させられないためである。 + +Flag を独立 resource とするのは、旗を Language / Locale / 国家の identity と同一視せず、presentation 上の対応を後から差し替え可能にするためである。Language にだけ system default を置くのは、Tag Name 編輯その他で Language を省スペースに視覚識別する要求が主目的であり、Locale や Script の意味論へ Flag を埋め込む必要がないためである。 + +## 12.11 一角(Pools) + +一角は利用者が明示的に選んだ Post membership と順序そのものを data とする ordered collection resource であり、saved search ではない。series / collection という内容上の category と、public / private という visibility を別軸として持つ。current membership と immutable Pool Version を分離する。 + +### 考え方・理由 + +内容の性質と閲覧・編輯範囲を一つの status に畳み込まず、membership の追加・除外・並べ替えを明示的な current relation と history snapshot で扱う。 + +## 12.12 共通 translation + +原 resource 自身の multilingual 正本モデルを持たない文字列には共通 Translation resource を利用できる。原文は owner resource を正本とし、翻訳が発生した場合だけ owner から Translation を参照する。Translation 側に polymorphic source identity を持たせない。 + +Translation は複数 language の Note を一つの resource として束ね、一つの version sequence を持つ。Note は language ごとの current translation であり、独立 version tree を作らない。Note ごとに、Translation Version に保存されたどの source snapshot を根拠にしたかを保持し、language 単位で stale 判定できなければならない。 + +Translation Version は source text と全 Note を immutable snapshot として自己完結して保持する。ある Note を変更・再確認しても、変更されていない Note の source provenance を勝手に最新へ進めない。 + +### 考え方・理由 + +version の主体を Translation 全体のまま維持しつつ source provenance を Note ごとに持つことで、複数 language を一括編輯できる resource boundary と、language 単位の stale 状態を両立する。 + +## 12.13 暦変換 cache + +初期 V2 は拡張天保暦専用の再生成可能 conversion cache を持つ。汎用 calendar table を先行して作らない。Gregorian date と拡張天保暦日を一意に対応させ、計算規則を変更した場合は履歴 version を作らず再生成する。「天保」の identifier romanisation には `tenpou` を用いる。 + +### 考え方・理由 + +この cache は正本 data ではなく高コスト計算の再利用である。将来の暦法が同じ fields を持つと仮定して未使用 abstraction を先行作成しない。 + +## 12.14 考え方・理由 + +データモデル全体では、current state は正規化して一意な現在を表し、history は denormalised でも自己完結した immutable snapshot とする。この二つを同じ正規化規則へ無理に揃えない。 + +surrogate ID、audit column、boolean、provider abstraction、1:1 table はテンプレートとして追加せず、identity・参照・寿命・履歴の必要性から決める。逆に、意味の異なる resource を table 数削減だけのために混ぜない。schema の「美しさ」は column 数の少なさではなく、各名前から domain の責務が読めることを基準とする。 + +# 13. Frontend + +## 13.1 route + +通常の public route は表示 locale prefix を持つ。 + +| path | 役割 | +| --- | --- | +| `/:locale/counts` | 横断集計表の resource 選択・入口 | +| `/:locale/counts/:resource` | resource 別の集計表・明細表(同一画面で mode 切替) | +| `/:locale/posts` | 投稿一覧 | +| `/:locale/posts/new` | URL 入力、確認、編輯、一括登録 | +| `/:locale/posts/search` | 投稿の詳細検索 | +| `/:locale/posts/random` | 現在の検索条件に一致する Post を server-side で一件抽選し、Post detail へ redirect する navigation-only route | +| `/:locale/posts/:id` | 投稿詳細。必要な権限がある場合は同一詳細画面内で編輯 view へ切り替える | +| `/:locale/posts/changes` | 投稿履歴 | +| `/:locale/tags` | Tag 一覧 | +| `/:locale/tags/:id` | Tag 閲覧 | +| `/:locale/tags/changes` | Tag 履歴 | +| `/:locale/tags/nico` その他 | 外部タグ対応管理 | +| `/:locale/tags/groups` | Tag Group 一覧・検索 | +| `/:locale/tags/groups/:id` | Tag Group 表示・編輯 | +| `/:locale/help` | 文書(Help)の目次・検索 | +| `/:locale/help/
` | 文書(Help)表示。`
` を numeric current-content ID と stable article identifier のどちらで表すかは未確定事項に従う | +| `/:locale/pools` | 一角の一覧・検索 | +| `/:locale/pools/:id` | 一角の表示 | +| Tag Description route | Tag × language の説明表示・編輯 | +| 説明履歴・差分 route | revision と差分 | +| `/:locale/materials` | 素材一覧 | +| `/:locale/materials/new` | 素材作成 | +| `/:locale/materials/:id` | 素材詳細 | +| `/:locale/materials/changes` | 素材履歴 | +| `/:locale/materials/suppressions` | 素材同期抑止 | +| 管理 route | BAN、抑止、監査その他 | + +Tag Name / alias / Reading は `/:locale/tags/:id` の同一画面内の編輯 state で扱い、`/:locale/tags/:id/edit`、`/:locale/tags/:id/names`、全 Tag Name 専用の `/:locale/tags/names` を標準 route としない。投稿編輯も独立した `/:locale/posts/:id/edit` を標準 route とせず、投稿詳細内の一時的な編輯 view とする。 + +locale prefix を省略した URL は、利用者設定、browser language、既定 locale その他の規則から表示 locale を決定し、対応する prefixed URL へ遷移させる。具体的な優先順位・fallback・redirect status は未確定事項に従う。 + +## 13.2 UI 共通要件 + +- 高い情報密度、text link 中心の操作、一覧性を基本とし、視覚的な装飾や大きな余白によって同一画面で把握できる情報量を不必要に減らさない。 +- desktop では最大三段の top navigation と main content を基本骨格とする。sidebar は全画面へ機械的に置かず、検索、絞込み、関聯情報、階層表示その他を常時参照する価値がある画面で用いる。特定 domain だから sidebar を一律に置く/置かないとは定義しない。 +- sidebar を持つ画面では、利用者が sidebar の幅を変更できる。変更した幅は device-local setting として保持し、再訪時に復元する。既定幅へ戻す操作を提供する。 +- 狭い画面では navigation hierarchy を維持したまま menu 型その他の compact な表現へ変形し、主要操作への到達性を保つ。 +- Material Design は visual style の全面採用ではなく、form、focus、state、autocomplete、filter、responsive layout、accessibility の一貫した設計資料として参照する。 +- 同じ役割を持つ UI の外観・interaction は page-local な style class の追加で合わせるのではなく、共通 Svelte component へ集約することを優先する。各 page は共通 component を組み合わせ、同一 concept の markup / utility class 群を独自に再実装しない。 +- 標準 theme の主要 menu / navigation は『ぼっち・ざ・ろっく!』の主要四人のイメージカラーに由来する黄色・赤・ピンク・青の四系統を visual identity の基礎とする。緑・ライムその他の別 palette を標準 navigation の新しい基調色として導入してはならない。 + - light theme は、desktop root background `#fefce8`、mobile / active / submenu background `#fef08a`、brand link `#db2777`、menu link `#1d4ed8` を標準とする。 + - dark theme は、root background `#230505`、active / submenu background `#450a0a`、brand link `#f9a8d4`、menu link `#93c5fd` を標準とする。 + - neutral background / border や warning / error 等の状態色、Tag category colour は別の semantic role として使用できるが、それらを主要 navigation の visual identity と置き換えてはならない。 +- 巨大な card、FAB その他、情報密度を不必要に下げる表現を標準としない。 +- 廃止、除票、索引抑止、内部検索抑止、公開抑止、generated、要確認その他の状態を画面ごとに異なる表現で増殖させず、共通 visual language を用いる。 +- 管理者には policy の理由を表示する。 +- 権限がない操作は、隠すか disabled とし、誤って保存可能に見せない。 +- validation error を field 単位と全体単位で表示する。 +- 日付ないし日時を入力する UI は、暦表示・暦入力を system-wide に一貫して扱える共通の自作コンポーネントを使用する。browser native の日付・日時入力だけへ仕様を依存させず、不詳日時を扱う画面も同じ component / conversion rule を利用する。 +- 共通日時入力 component は、不詳入力を許可する configuration の場合に「不詳」checkbox を表示する。「不詳」が有効な場合だけ「月日不詳」「日不詳」「時刻不詳」「分不詳」「年月日不詳」の radio button を表示し、選択した不詳単位より上位の、判明している日時項目だけを入力できるようにする。 +- 投稿 form では、この項目の日本語名称を「オリジナルの作成日時」とする。「範囲指定」checkbox が無効な場合は不詳入力を許可した共通日時入力 component を一つ表示する。「範囲指定」が有効な場合は不詳入力を許可しない共通日時入力 component を開始・終了の二つ表示する。 +- 「範囲指定」の終了日時は利用者には終端を含む値として扱わせる。保存時は終了日時へ1分を加えて `original_creation_range` の upper bound とし、表示時は `upper(original_creation_range)` から1分を引いて利用者向け終了日時へ戻す。 +- conflict 時に base、current、mine の差分を表示する。 +- 保存中、保存成功、保存失敗を区別する。 +- keyboard だけ、mouse だけ、touch だけに依存しない。 +- layout、label、focus、contrast、alt text その他の accessibility を確保する。 + +## 13.3 top navigation・locale switcher + +- top navigation は二段構成を基本とし、必要な場合に限り第三段を設けられる。第三段は site-wide な主要 navigation ではなく、第二段の特定項目に従属する contextual subnavigation とする。 +- 第一段の標準項目と順序は「広場」「タグ」「一角」「素材」「文書」「翻訳」「申請」「ユーザー」「抑止」とする。 +- 「文書」は Help resource だけを扱う `/:locale/help` domain の日本語表示名とする。法的文書その他の別 resource を「文書」という名称だけを理由に Help へ統合してはならない。 +- 「申請」には「御意見板」への導線を含める。御意見板を独立した第一段項目にはしない。 +- 「ユーザー」は第一段へ置く。ユーザー domain 内の具体的な一覧・検索・詳細その他は未確定事項で定める。 +- 「抑止」は監査・管理上の重要な入口として、admin の場合だけ第一段へ表示する。索引抑止、内部検索抑止、公開抑止、素材同期抑止その他の異なる抑止機構を横断して扱う UI domain とするが、underlying policy / state を一種類へ統合してはならない。 +- 「ニジラー」は第一段の独立項目としない。deerjikist category の Tag とニジラー情報への導線は Tag domain 側で扱う。 +- 媒体の文字抽出 / transcription resource を「文字」として第一段へ置かない。admin が扱う入口は管理者の設定画面に置く。 +- 第一段の利用者による並べ替えは現時点の完成要件とせず、将来拡張候補に留める。 +- 第二段は、検索機能を持つ domain では最左端に quick search box を置き、その右に現在 domain の resource navigation、区切りを挟んで右側に現在 resource / current page の contextual action を置く。 +- 同じ画面の sidebar その他の常時表示領域に同等の検索 box が存在する場合、第二段へ同じ検索 box を重複して置かない。投稿一覧のように sidebar から検索する画面はこの例外に含む。 +- current resource が存在しない page では第二段右側の contextual action を省略できる。 +- 第二段右側の contextual action section には、原則として route 遷移を伴う navigation item だけを置く。同一 page 内の編輯 state 切替、toggle、保存その他の page-local operation は置かず、page header または本文内の操作 UI に配置する。 +- Post domain は、Post detail を含め sidebar が個別 Post の情報・操作を担うため、第二段右側の contextual action section を設けない。 +- 第三段は、第二段の特定項目に独立した下位 navigation が必要な場合にだけ表示する。常設の第三段を要求しない。 +- `/more` は navigation index / site map への通常 link とする。desktop では `More` への hover または keyboard focus により、全体 navigation を overlay として表示できる。 +- `More` overlay は狭い viewport で第一段に収まらない項目の overflow 先を兼ねてよい。ただし、overflow された項目を意味上の「その他」カテゴリへ分類したものとは扱わない。 +- 第一段の overflow は標準順序の右側から順に退避する。ただし現在選択中の active domain は overflow させず第一段に残す。active domain を残すために必要なら、その分だけ他の項目を追加で overflow させる。 +- 狭い画面では第一段・第二段・必要な第三段という情報階層を維持したまま、button menu、drawer その他の compact な表現へ変形できる。単なる multi-row wrapping により navigation 階層を曖昧にしてはならない。 +- Wiki を独立した top-level navigation として設けない。 +- Tag Group は top-level domain とせず、「タグ」配下の第二級 resource / navigation として扱う。 +- 表示 locale switcher は top bar の global control とし、section navigation に混ぜない。 +- locale switcher に国旗を使用しない。言語・翻訳を示す icon と、開いた menu 内の locale の代表名を用いる。 +- 画面幅が狭い場合は switcher の label を省略して icon のみにできる。 +- locale を変更しても、同一 resource / 同等 route に留まれる場合はその位置を維持する。 + +## 13.4 Post 詳細・編輯 UI + +Post 詳細は media と Tag / Post 情報を同時に参照できる高密度な構成とする。 + +- desktop では左 sidebar に Tag と Post 情報、右 main area に embed / preview と Post 本体を配置する。 +- mobile では embed / preview と Post 本体を先に表示し、desktop sidebar 相当の Tag / Post 情報をその後へ縦方向に配置する。desktop sidebar を狭い画面へそのまま縮小して残さない。 +- sidebar の Tag は category ごとに group 化し、各 category 内では現在 locale の Tag sorting rule に従って並べる。`tagRelationDisplay` の既定は `grouped` とし、利用者は `flat` へ変更できる。 +- sidebar の Post 情報には少なくとも ID、登録者、タグ広場への登録日時、オリジナルの作成日時、外部 URL、動画であれば `duration_ms` / dimensions、version / 履歴への導線その他、Post の照会に常時有用な情報を置く。 +- Post 本体は tab の内側へ隠さず常時表示する。 +- 「関聯」と「編輯」は Post 詳細内の tab とする。履歴は tab とせず、Post domain の version 一覧へ遷移する。 +- Post の編輯は独立した `/:locale/posts/:id/edit` route を標準とせず、Post 詳細内の「編輯」tab で行う。 +- 編輯可能な基本データには title、Tag の記載、parent Post、オリジナルの作成日時その他の Post 基本属性を含める。Post URL は通常の編輯項目へ含めず、同一外部 content の所在 URL を変更する場合だけ「転居届」を用いる。`video_posts.duration_ms`、`width`、`height` は同期用 metadata であり、基本データリストの編輯可能項目に含めない。 +- Tag の時間・範囲等の局所的な効力は独立した「局所記載」編輯項目として分離せず、Tag の「記載」表現へ統合する。 +- Tag sidebar では局所記載 Tag も常時配置する。playback position を取得できる場合、現在 section 内の Tag は通常強度、section 外の Tag は薄く表示する。局所記載 marker は active / inactive と独立して常時表示し、hover / focus / touch から全 section 範囲を確認できる。playback position を取得できない場合は濃淡による active 判定を行わず、marker と範囲情報だけを表示する。 +- rich edit は Post 基本データ全体の別 editor mode とせず、Tag の「記載」を詳細に編輯するための表現として設ける。通常の Tag 記載編輯と rich edit は同じ Post Tag relation を操作する。 + +対応済み provider の外部 embed は常に自動読込みする。embed の読込み自体を `off / manual / auto` 等から利用者が選択する設定は設けない。利用者設定として持つのは自動再生だけとし、`embedAutoplay` の既定は `false` とする。provider または browser が autoplay を許可しない場合は、その制約を迂回して再生を強制しない。未知・未承認 provider の iframe を自動表示するか、利用者の明示操作後に表示するかその他の扱いは、利用者 preference ではなく provider / security policy として定める。 + +## 13.5 投稿追加 UI + +- 一件または複数 URL を入力できる。 +- URL 入力、自動取得、確認・修正、一括登録は同一の投稿追加作業として扱うが、利用者が画面として区別する段階は query parameter ではなく route で識別する。 +- 自動取得の進行状況と失敗理由を行単位で表示する。 +- 最終保存予定値を一覧で確認できる。 +- 項目を直接編輯できる。 +- 手動値と自動取得値の優先関係を明確にする。 +- 入力中の URL 群、自動取得結果、手動編輯値、skip、仮登録その他の一時 state を URL query へ格納しない。Svelte client state と `sessionStorage` その他の session-scoped storage を用い、reload や同一 tab 内での遷移から復元できることを基本とする。 +- 投稿追加途中の draft state を bookmark / share 可能な URL として公開することを初期要件としない。secret、長大データ、一時取得値を URL へ露出させない。 +- server-side temporary state は、client / session storage だけでは成立しない一時 resource が必要になった場合にだけ導入する。未確定事項の仮登録を導入する場合は、この作業 state と寿命・復旧境界を整合させる。 +- 一括登録は batch 全体を一つの transaction とせず、各 Post を独立して処理する。各行は `created`、既存 URL 等による `skipped`、`failed` を個別に返し、一部失敗によって他の成功行を rollback しない。 +- 一件の Post 作成については、Post 本体、thumbnail、Tag snapshot、Post Tag relation / Tag section、parent relation、Post Version その他、その Post の確定に必要な変更を一つの transaction として扱う。途中で失敗した場合は当該 Post の作成を rollback する。 +- recoverable な失敗行は確認画面へ残し、編輯・再試行できる。既存 Post と判定した行は `skipped` として既存 Post を示す。すべての対象行が `created` または `skipped` 等の完了状態になった場合は作業 state を消去し、投稿一覧へ自動遷移する。 + +## 13.6 投稿編輯の Tag 入力 + +- 投稿編輯の Tag 入力は空白区切りを標準とする。 +- Post へ Tag を入力する autocomplete の候補は、少なくとも一件の Post への有効な記載を持つ Tag、すなわち `postCount > 0` の Tag に限定する。Tag は Post への記載をもって効力を持ち、記載のない Tag を通常の Post Tag 候補として扱わない。 +- autocomplete は候補母集団の範囲内で primary name、alias、他 language name、Reading を横断して検索できる。 +- alias を選択または確定した場合は同じ Tag の現在 language primary へ置換する。 +- 他 language の既存 Tag Name を生入力した場合は警告を表示し、既存 Tag の現在 language name へ置換するか、新しい Tag を登録するかを選択できる。 +- autocomplete で他 language name / Reading の候補を明示選択した場合は、その Tag を選んだものとして扱い、言語 prefix の入力を要求しない。 +- Tag 入力では category prefix を利用できる。language prefix を通常入力の必須構文として追加しない。 +- autocomplete は `ArrowDown` / `ArrowUp` で候補を移動し、`Enter` で選択し、`Escape` で候補表示を閉じられる。 + +## 13.7 広場 sidebar の Tag 検索 + +広場の第二段には quick search を置かず、Post 一覧 sidebar の Tag 検索を主要な簡易検索入口とする。検索 grammar と基本 interaction は次とする。 + +- 入力は空白区切りの Tag literal とする。 +- `type:or` token がある場合は `match=any` とし、ない場合は `match=all` とする。`type:or` 自体は Tag literal として検索しない。 +- Tag literal の先頭に `not:` を付けた場合、その literal を否定条件として扱う。 +- `match=all` では positive literal の各集合を積み重ね、negative literal の Tag を持つ Post を除外する。 +- `match=any` では各 literal の relation を OR する。negative literal も「その Tag を持たない Post」の集合として OR の一項になる。 +- autocomplete は現在入力中の末尾 token を対象とし、`not:` prefix がある場合は prefix を除いた文字列で候補検索する。候補を選択した場合は元の `not:` を保持したまま Tag 名へ置換し、末尾へ空白を追加して次の Tag 入力へ進める。 +- autocomplete 候補は廃止されていない `postCount > 0` の Tag に限定する。候補検索は現在入力中 token の prefix match を基本とし、primary / canonical name と alias の双方を候補 source とする。Nico category の Tag は `nico:` prefix を含む名称に対しても同じ入力 prefix から候補化できる。 +- autocomplete 候補は投稿件数降順、同件数では名称順で安定して並べ、最大20件を表示する。alias で一致した場合は alias hit であることを候補情報として保持できる。 +- `ArrowDown` / `ArrowUp` で候補移動、`Enter` で候補選択、`Escape` で候補を閉じる。候補を選択していない状態で `Enter` を押すと検索を実行する。 +- 検索結果 URL では Tag 条件を `tags`、AND / OR を `match=all|any` として表現する。sidebar を再表示するときは URL の条件から入力文字列を復元し、`match=any` の場合は UI 上で `type:or` を付加して示す。 + +## 13.8 Tag 表示・Tag page + +- Tag category は Tag 表示の semantic colour により視覚的に区別する。Tag を表示する場合は、その category に対応する colour を適用する。 +- Tag 名の横へ `一般`、`(素材)` その他の category 名を常設せず、category を示すことだけを目的とした visible icon / marker も標準表示へ追加しない。category は並び順・検索・意味分類には用いるが、Tag 名より強い視覚情報として反復表示しない。 +- accessibility tree、ARIA 相当の metadata、machine-readable attribute その他、画面上の情報密度を増やさない非視覚情報として category を公開してよい。 +- category colour の具体的な色値は theme により変化してよいが、同一 theme 内で category ごとの視覚的な区別を失わせてはならない。 +- clickable な Tag 名は system-wide の共通 `TagLink` component / pattern を必ず経由する。画面ごとに bare anchor、独自の category colour 判定、独自の Tag marker 表現を作ってはならない。 +- `TagLink` の既定遷移は、その Tag を条件とした投稿一覧とする。ただし component は呼出し側が明示した destination へ遷移先を override できる。Tag 台帳ではこの override を用いて Tag 照会画面へ遷移する。 +- Tag 名が利用可能幅を超える場合、通常時は末尾を `…` で省略する。右端に投稿件数その他の固定情報を表示する構成では、その固定情報を縮小・押出しせず、Tag 名側を省略する。 +- mouse cursor が省略された Tag 名に重なった場合、隠れている文字列を確認できるよう、文字列を右から左へ電光掲示板状に移動させて全文を閲覧できるようにする。全文が収まる場合は移動させない。 +- 前項の Tag 名 reveal movement は、省略された情報を閲覧するための機能であり、`animationMode = off` / `reduced` によって停止させない。装飾的な画面遷移・並べ替え・drag motion と同一の animation policy に機械的に含めてはならない。 +- `?` / `!` その他の resource-state marker を category colour と別の意味として設けられる。 +- 廃止状態を category 色だけで表現しない。 +- 局所記載の範囲を Tag 全体への付与と区別し、Post detail の Tag sidebar では局所記載 marker と再生位置連動の表示強度により状態を示す。 +- 親子表示は flat と grouped を選択できる。 +- 保存順と画面表示順を分離する。 + +### 13.8.1 Tag 台帳 + +`/:locale/tags` は高密度な Tag 台帳として提供し、次を標準とする。 + +- 通常表示では `post_count_gte = 1` を既定 filter とし、少なくとも一件の Post への記載を持つ Tag を表示する。投稿 0 件 Tag も filter を変更すれば照会できる。 +- 既定 sort は投稿件数の降順とする。 +- 一 page の既定件数は 20 件とし、50 件 / 100 件へ変更できる。利用者が選択した limit と sort は Tag 台帳用の device-local setting として保持する。 +- filter は少なくとも名称、category、投稿件数の下限 / 上限、はじめて記載された日時、定義の更新日時、有効 / 廃止を扱う。これに加えて `auto_generated = true`、すなわち未公証の名称を持つ Tag だけへ絞り込める。 +- 名称検索は current locale の名称だけに限定せず、他 language の Tag Name と Reading も検索入口として利用できる。 +- columns は少なくとも Tag、投稿件数、category、状態、別名、上位 Tag、はじめて記載された日時、定義の更新日時、個別履歴への導線を持つ。 +- 「Tag」column は current locale の language の primary Tag Name を表示する。当該 primary が `auto_generated = true` の場合は名称に `(未公証)` を付ける。 +- 「別名」column は current locale の language の alias を基本表示とする。他 language の名称・alias・Reading を常設 column として横方向へ増殖させない。 +- Tag column の TagLink は `/:locale/tags/:id` へ遷移する。上位 Tag column の TagLink も、その上位 Tag の照会画面へ遷移する。 +- 個別履歴への導線は当該 Tag で絞り込んだ Tag version / history 一覧へ遷移する。 + +### 13.8.2 Tag 照会・編輯 + +`/:locale/tags/:id` は Tag の照会と編輯を同じ route 上で扱う。通常は照会 state とし、権限を持つ利用者が page 内の「編輯」操作を選んだ場合だけ編輯 state へ切り替える。編輯 state のためだけの `/edit` route または query parameter を設けず、reload / 再訪時は通常の照会 state を基準とする。 + +Tag 照会画面は、名称、category、別名、上位 Tag、廃止状態を一つの高密度な縦型画面で把握できる構成を基礎とし、専用 sidebar や page-local tab を前提としない。V2 で追加された multilingual 名称情報は同じ Tag の中へ統合する。 + +- current locale の language に対応する primary Tag Name を主名称として最初に表示する。 +- 当該 primary が `auto_generated = true` の場合、その名称に `(未公証)` を付ける。 +- current locale の alias と Reading を同じ名称領域で確認できるようにする。 +- 他 language の primary Tag Name、alias、Reading は折り畳み領域を展開したときに同じ Tag の情報として表示する。独立した名称 page へ移動させない。 +- category、上位 Tag、廃止状態その他の Tag 本体情報を照会できる。 +- Tag Group への所属、代表素材、Tag Description その他、V2 で独立 resource として存在する関聯情報は、Tag 本体の identity / 編輯責務へ混在させず、照会画面内の関聯情報として提示できる。各 resource 自身の詳細な管理・履歴・編輯はその resource の契約に従う。 +- 第二段の contextual navigation は、個別 Tag について route 遷移を伴う「広場(投稿件数)」と「履歴」を基本として維持する。page-local な「編輯」操作は第二段へ置かない。 +- 編輯 state への切替は page header または本文内の button 等から行う。 + +Tag Description は Tag とは別 resource であるため、Tag 編輯 state に説明本文の編輯を含めない。Tag 照会画面で関聯 resource として表示しても、Description の version / language / 権限 contract は Tag 本体へ統合しない。 + +## 13.9 Tag 名称・Reading 編輯 UI + +Tag Name / alias / Reading は独立した名称 page ではなく、`/:locale/tags/:id` の編輯 state 内の名称領域で編輯する。 + +- 現在の表示 locale の language に対応する field set を最初から表示する。 +- 各 language の field set は、primary Tag Name、alias、Reading その他の必要な名称情報を一まとまりとして扱う。 +- alias は language ごとに一つの入力 field を用意し、複数 alias を空白区切りで入力・編輯できる。 +- 現在の表示 locale の language 以外は折り畳み操作で展開できるようにし、同じ構造の field set を表示する。 +- UI locale と異なる language の名称・Reading も同じ Tag の編輯 state から編輯できる。 +- primary Tag Name が `auto_generated = true` の場合は「公証」checkbox を未選択状態として表示する。利用者がその名称を公証して保存した場合は `auto_generated = false` とし、別の `certified` / `verified` 状態を追加しない。 +- `auto_generated` の名称だけを Tag 台帳から検索・絞込みして確認・修正できる。 +- generated name から人間が確定した名称への修正・primary 切替を行える。 + +## 13.10 context menu + +Tag、Post その他の主要 link / item は、desktop で右クリックしたときに共通の custom context menu を表示できる。 + +- context menu は詳細表示、編輯、履歴、copy、関聯 page への移動その他、その resource に通常提供される操作への shortcut とする。 +- context menu からしか到達できない必須操作を作らない。 +- browser の標準 context menu を完全に奪わず、明示的な modifier key その他で利用できるようにする。 +- keyboard の Context Menu key / `Shift+F10` その他からも操作できることを考慮する。 +- touch device では long press による表示を利用できるよう検討するが、Safari その他の browser 標準 long-press action と競合するため、long press だけに依存しない。`⋮` その他の明示的な代替導線を設ける。 + +## 13.11 スマホの Tag D&D + +- touch では長押し後に drag 準備へ入る。 +- 縦方向の移動が強い場合は drag を cancel して scroll を優先する。 +- draggable node と droppable node を分ける。 +- 空 category にも drop slot を表示する。 +- animation off の場合は drop animation も表示しない。 +- drag 中は元の Tag を非表示にし、drag 終了後に再表示する。 +- animation 有効時に元位置へ不要な wipe-in を発生させない。 + +## 13.12 設定 UI + +利用者が変更可能な設定について専用 settings UI を設けるか、各 domain / component の文脈内で変更させるか、その併用とするかは未確定とする。 + +設定画面を設ける場合の route、分類、tab 構成、PC / mobile layout、保存操作、未保存変更の扱い、および各設定の account-portable / device-local / system scope は 17.3 および 17.25.7 で定める。既存実装の settings page 構造をそのまま完成仕様とはしない。 + +## 13.13 素材 UI + +- grid と list を切り替えられる。 +- tag state、media kind、Tag、子孫、grouping で絞り込める。 +- Material が画像、音声、動画、URL-only のいずれかを、Tag Name に埋め込まれた文字列ではなく Material / Blob metadata から明確に識別して表示する。 +- 未分類素材を明確に表示する。 +- Tag が representative Material を持つ場合、その Tag の説明・閲覧 UI から具体像として表示できる。 +- export path を編輯できる。 +- 複数 Material を選択し、選択対象だけを download / ZIP export できる。 +- canonical export path に基づく folder 構成を利用できる。将来の user-specific collection / project package は canonical path と分離して追加できる。 +- Blob purge / missing object、thumbnail 生成失敗、同期抑止を区別する。同期元から file が消えた事実の presence tracking は初期 V2 では行わない。 +- 履歴と現在状態を往来できる。 + + +## 13.14 御意見板・問い合わせ UI + +利用者が運営へ意見を送れる「御意見板」を設け、「申請」domain 内の機能・導線として扱う。御意見板は利用者同士の交流を目的とする forum / SNS とせず、意見、改善提案その他を運営へ届ける一方向寄りの投稿面とする。御意見板自身の resource boundary を申請 resource と同一にすることまでは、この navigation 上の所属だけから要求しない。 + +- 他利用者が自由に返信 thread を形成すること、reaction / like、follow その他の social interaction を御意見板の基本機能としない。 +- 投稿時に screenshot を任意添付できるようにする。添付 file の上限、保存期間、公開範囲その他は未確定事項で定める。 +- 御意見板のために、通常利用中の click、route change、DOM mutation その他の client-side 操作履歴を常時記録する session replay / operation trace 機構を設けない。 +- 問い合わせを御意見板と別の入口 / resource とする案を採用候補とし、目的・公開範囲・回答方法その他の境界は未確定事項で定める。 + +## 13.15 投稿一覧 UI + +### 13.15.1 Sidebar の Tag 選出と表示順 + +投稿一覧 sidebar に表示する Tag は、候補選出と表示順の決定を分離する。 + +候補は全 category 合計で最大 25 件とする。 + +- 検索条件が内部 Tag 一件だけである場合、その検索 Tag 自身を必ず一件含め、残りを当該 Tag に対する Tag similarity の高い順から選出する。similarity 候補が現在表示中の Post に実際に含まれていることは要求しない。 +- 前項以外、すなわち検索条件なし、複数条件検索、単一 metatag その他の場合は、一 page 分の Post に出現する Tag を数え、page 内出現頻度の高い順から候補を選出する。頻度計算の母集団は常に一 page 単位とし、前後に読み込んだ複数 page を累積しない。無限 load で複数 page を同時表示している場合にどの page を sidebar の active page とするかは未確定事項で定める。 +- sidebar の候補を得るために利用者の検索語・検索回数を集計・保存せず、popular search を候補 source としない。 +- 同じ Tag を重複表示しない。単一 Tag 検索では検索 Tag 自身も 25 件の上限に含める。 + +候補選出後、category は次の固定順で表示する。 + +1. `deerjikist` +2. `meme` +3. `character` +4. `general` +5. `material` +6. `meta` + +category 内は次の規則で並べる。 + +- `deerjikist`、`meme`、`character`、`material` は、その Tag 自身の全体 `post_count` 降順とする。 +- `general`、`meta` は候補選出時の relevance 順を維持する。単一 Tag 検索では Tag similarity 順、それ以外では現在 page 内の出現頻度順となる。 + +sidebar の各 Tag に category 名の text label または category icon を併記せず、category は `TagLink` の colour で区別する。 + +### 13.15.2 Random + +投稿一覧は、現在の検索条件に一致する Post を一件 random に開く導線を持つ。 + +- Random は JavaScript click handler だけで成立する疑似 link とせず、通常の内部 link とする。link 先は locale routing 下の `/posts/random` 相当の navigation route とし、現在の検索条件を保持する。 +- Random route は data API ではなく navigation route とし、server-side で条件に一致する Post を一件選び、その Post detail へ一時 redirect する。 +- app 内の通常 click では SvelteKit の client navigation を維持し、Random route を経由するためだけの full document reload を発生させない。 +- browser history には Random route を独立した中間 entry として残さない。Random 結果から Back した場合は、Random を実行した投稿一覧へ戻る。 +- direct access、reload、新規 tab、JavaScript 無効環境でも同じ URL semantics で動作する。別々の navigation request は別々の random result を返してよい。 +- hover、touch、prefetch その他、利用者が navigation を確定する前の data preload によって random result を確定しない。Random link では抽選を発生させる data preload を無効化する。 + +### 13.15.3 Mobile sidebar + +mobile の投稿一覧では、無限 load を含む長い一覧の末尾へ sidebar content を追いやらず、一覧より前の上部領域に sidebar 相当の操作を置く。 + +- Tag 検索 box は主要操作として常時表示する。 +- Tag 一覧、Random その他の補助 sidebar content は一つの disclosure 領域へ入れ、初期状態を閉じた状態とする。 +- Random を disclosure の外へ特別扱いで常設しない。 +- disclosure の開閉状態は client-local state とし、URL、portable setting、device-local persistent setting のいずれにも保存しない。 +- disclosure control の具体的な visual 表現は未確定事項で定める。三角記号を反復することを仕様要件とはしない。 + +### 13.15.4 一覧件数と無限 load + +投稿一覧の一 page 当たりの件数は `20 / 50 / 100` から選択でき、既定値を 20 とする。 + +pagination 表示と無限 load 表示は同じ page 単位の data acquisition を共有する。無限 load では選択した件数を一回の page / batch の件数として用いる。`?page=N` を直接開いた場合は N page を初期 SSR page として扱い、無限 load 表示でもその page を基点として前後の page を追加取得できる。 + +一覧件数・表示方式をどの user setting scope へ保存するか、無限 load と browser history / scroll restoration の詳細は未確定事項で定める。 + +### 13.15.5 既定の時系列 + +投稿一覧の既定順は、元コンテンツの作成時系列を基準とする降順とする。オリジナルの作成日時の sort key は、`upper(original_creation_range) - 1 minute`、`lower(original_creation_range)`、タグ広場上の `created_at` の順で最初に利用可能な値を採用する。これは利用者向けに inclusive として表示する Range 終端を並び順へ用いる規則である。同一 sort key では Post `id` を同じ方向の tie-breaker として安定した順序を保証する。 + +`original_creation_range` が表す不詳日時の区間についても同じ sort key 規則を適用し、元コンテンツ側でより新しいものを先にする。Range は半開区間として扱い、不詳単位ごとに別の並び順規則を設けない。 + +### 13.15.6 Thumbnail grid と title + +投稿一覧の thumbnail grid は main area の利用可能幅に対して grid block 全体を中央へ配置する。一方、grid 内の各 row は左詰めとし、最終 row だけを独立して中央寄せしない。これにより各 column の位置を row 間で揃える。 + +- Post title は既定では thumbnail の下へ常設しない。 +- title 表示設定を有効にした場合、thumbnail が存在する Post は thumbnail の下へ title を表示する。 +- Post 自体に利用可能な thumbnail が存在しない場合は、thumbnail frame 内へ title を表示し、text thumbnail として fallback する。 +- thumbnail の標準寸法、gap、responsive な column 算出の具体値は未確定事項で定める。 + +### 13.15.7 Parent / child marker + +Post が parent / child relation を持つ場合、一覧上で判別できる控えめな edge marker を Post visual に表示する。 + +- child Post を持つ場合は緑系を用いる。 +- parent Post を持つ場合は黄系を用いる。 +- parent と child の両方を持つ場合は、一つの色で上書きせず、上辺・左辺を child の緑系、右辺・下辺を parent の黄系として同時に表現する。 + +動画 embed その他の interactive content を邪魔しないことを要件とする。marker の太さ・offset・表示位置、利用者が非表示または縮約できる設定、将来の自由配置 layout との統合は未確定事項で定める。 + +## 13.16 考え方・理由 + +高い情報密度を基本とするのは、Tag、件数、状態、relation、履歴への導線を同一画面で比較する用途が多く、大きな余白や card が一覧性を直接下げるためである。ただし visual spacing を詰めることと hit area を狭くすることは別問題なので、touch 操作可能領域は十分に確保する。 + +sidebar を全画面の固定骨格にせず必要な画面だけで使うのは、検索・filter・階層参照には常時表示が有効な一方、単純な form / detail では main content の幅を奪うためである。幅を利用者が変更・保存できるようにするのは、端末幅と表示する情報量によって sidebar に必要な幅が大きく変わるためである。 + +第二段の最左端へ quick search を置くのは、domain navigation から離れず検索へ入れるようにするためである。同等の検索 UI が sidebar に常時見えている場合に重複させないのは、同一操作を二箇所へ並べて navigation 幅を浪費しないためである。 + +Tag category colour を必須とするのは、一覧、autocomplete、投稿詳細、素材その他の異なる画面でも semantic class を文字列を読まず即座に判別できるようにするためである。共通 `TagLink` を必須とするのは、画面ごとの独自 Tag 表示によって category colour、state marker、長名処理、遷移先が欠落・分岐することを防ぐためである。`TagLink` を投稿一覧の Tag filter へ遷移させるのは、Tag 名をクリックする主目的を「その Tag が付いた content を探す」操作として一貫させ、Tag resource 自体の照会とは意味を分けるためである。 + +Tag category 名や category icon を Tag ごとに常設しないのは、colour と並びで既に伝えている粗い分類を text で反復すると、高密度な Tag 一覧で Tag 名そのものの探索性を下げるためである。accessibility 用の semantic metadata は視覚的反復とは分離する。 + +投稿一覧 sidebar で popular search を使わず現在 page の frequent Tag を使うのは、他利用者の検索行動を収集・蓄積せず、現在見ている content から次の探索先を提示する責務へ sidebar を限定するためである。単一 Tag 検索だけ similarity を用いるのは、その Tag と共起傾向が近い概念を現在 page の偶然の構成に限定されず提示できるためである。候補選出と category 内の表示順を分けるのは、探索上の relevance と、人物・作品・素材等で利用頻度の高い Tag を上に置く一覧性を両立するためである。 + +Random を server-side navigation route とするのは、通常 link の semantics、新規 tab、JavaScript 無効環境を保ったまま抽選処理を server に一元化し、onclick 専用実装を避けるためである。Random の data preload を止めるのは、hover 時ではなく利用者が navigation を実行した時点を抽選時点とするためである。 + +mobile で検索だけを常時表示し残りの sidebar を折り畳むのは、検索を一手で使える状態に保ちながら、補助情報が狭い viewport と無限 load を圧迫することを避けるためである。thumbnail grid block を中央へ置き row 内を左詰めにするのは、一覧全体の左右余白を均しながら column alignment を崩さないためである。title を既定非表示とするのは thumbnail 主体の密度を保ち、thumbnail がない場合だけ title を visual fallback として使うためである。 + +parent / child marker を edge に残すのは、detail を開かず relation の有無を一覧で判別する価値があるためである。両 relation を辺ごとの二色で同時表示するのは、どちらか一方を優先して情報を失わないためであり、marker を控えめにするのは動画 embed 等の content 自体を覆わないためである。 + +Tag 名の hover scroll を animation setting から除外するのは、これは装飾ではなく、省略によって見えなくなった情報を閲覧するための補完操作だからである。 + +Tag Description を Tag の編輯 state に含めないのは、Tag Description が独立した履歴・権限・resource identity を持つためである。一方、Tag 閲覧 page に Description を表示できるのは、関聯 resource の照会を一つの画面に集約しても編輯責務までは混在しないためである。 + +御意見板を social interaction の場にしないのは、タグ広場の主目的が共同編輯型の知識基盤であり、利用者同士の交流機能を本体へ拡張する必要がないためである。client-side operation trace を常時収集しないのは、問い合わせ補助のために通常利用時の memory / processing cost と行動記録を恒常化しないためである。 + +本章の要件は、本書だけから完成形を判断できる具体的な挙動として記述する。別実装や外部 UI を参照して設計した場合も、その参照先を知っていることを利用者・実装者への前提としない。 + +# 14. 集計表(Counts) + +## 14.1 位置づけ・対象と責務 + +集計表はタグ広場の標準機能とする。投稿・タグその他の resource の現在状態と記録済み履歴から、利用者が軸・条件・表示列を指定して集計表または明細表を生成できなければならない。集計表は横断的な照会機能であり、元の resource、immutable version、`event` を置き換える独立の正本 resource としない。 + +初期対象は少なくとも、投稿(`posts`)、タグ(`tags`)、外部タグ(`external_tags`)、タグ説明(`tag_descriptions`)、タググループ(`tag_groups`)、素材(`materials`)とする。V2 の外部タグ domain では内部 Tag と別の identity を使い、特定プラットフォームに依存した V1 の名称を標準 resource 名としない。他の version 管理 resource は同じ集計 contract に沿って追加できるが、追加するだけでその resource の公開 policy を緩和してはならない。 + +画面は `/:locale/counts` と `/:locale/counts/:resource` を持ち、選択中の resource、版定義、表示 mode、条件を確認・変更できる。既定の第一段 navigation の順序は変更しない。集計表への入口は site navigation index と関聯 domain の導線から到達できるものとする。 + +## 14.2 版定義・表示 mode・対象時点 + +集計は互いに独立した二つの選択軸を持つ。 + +| 軸 | 値 | 意味 | +| --- | --- | --- | +| 版定義 | `latest` | 指定した評価時点で対象 resource ごとに確定する最新状態・最新版を対象とする | +| 版定義 | `history` | 指定した評価時点までに記録された version を対象とする。複数版は異なる履歴行である | +| 表示 mode | `summary` | 指定した行・列軸ごとに件数を集約し、小計・合計を示す | +| 表示 mode | `details` | 条件に一致する閲覧許可済みの個別レコード・履歴版を明細表として示す | + +- `latest` は「今の正本」の参照と、過去の評価時点を指定した最新版照会を区別できるようにする。時点の確定に利用した基準は結果と再訪可能な状態で保持する。 +- `history` は version の immutable な当時値を使用し、現在の Tag Name、関連先、カテゴリ等で過去の記録値を黙って置き換えない。 +- 同一 `event` に属する複数 resource の変更を、`event` 一件と各 version 一件のどちらで数えるかは集計単位で区別する。`history` の通常の行は対象 version を単位とし、全操作横断の event 件数と混同しない。 +- 対象時点は実行ごとに固定し、列・小計・ページ・export の間で母集団をずらさない。表示 locale を変更しただけで対象時点や集計対象 ID が変化してはならない。 +- タイムゾーン・暦・集計粒度は query の明示的な条件として扱う。表示 locale の変更を隠れた WHERE 条件や集計時点の変更として使用しない。 +- 初期表示の `history` × `summary` は、記録月を行軸、操作種別を列軸とし、新しい月から表示する。`latest` × `summary` の初期表示は軸を設けず、対象の総件数を示す。利用者が明示した軸・条件・並び順・空選択は既定値で上書きしない。 + +## 14.3 クロス集計と計数規則 + +- 利用者は resource 定義に存在する属性を行軸・列軸へ任意の順番で配置できる。行だけ、列だけ、両方、軸なしを扱う。軸の重複と不正な組合せは server-side で拒否する。 +- 軸候補には resource identity、版番号、操作種別、記録時刻、分類、名称、Tag 関聯、利用者等のうち、その resource に実在し、公開制御を定義できる項目を含める。 +- 日時軸は年・四半期・月・週・日・時を選べる。暦と time zone は明示条件に従い、週境界と夏時間を含めて結果を再現できる。表示 locale だけで集計粒度や暦を変更しない。 +- NULL、不詳、空配列、関聯先の欠落、未登録の名称は別の状態として扱う。複数値属性は一つの元レコードが複数の通常セルへ寄与し得ることを明示する。 +- 通常セル、小計、行合計、列合計、綜合計は、同じ基準時点・抽出条件・公開許可された軸の意味に従う。小計行を再度足すことや、表示中の一ページだけを全体合計として扱うことは禁止する。 +- 単一レコードを数える通常の集計では、JOIN や多値展開で重複した元レコードを無条件に複数件と数えない。一方、**投稿集計で Tag を行軸に含める場合は例外**として、Tag ごとの通常セルの件数を加算した値を小計・合計・綜合計とする。 +- 例として、投稿1に Tag A・B、投稿2に A、投稿3に B・C が付いている場合、Tag 行軸の総合計は **5** とする。Tag を行軸に含めない同一母集団の投稿数は、異なる投稿 **3** とする。局所記載の複数 section は、それだけを理由に同一 Post・Tag の件数を重複させない。 +- Tag が列軸にだけある場合と、Tag が行軸にある場合の合計規則を混同しない。列ごとの集計と全体合計は各列に属する通常セルに基づき、列をまたぐ二重計上をしない。 +- 軸の合計数は **8**、生成する列組合せの上限は **100** を標準の安全上限とする。超過時は結果を途中で黙って省略せず、理由と上限を提示して実行を拒否する。上限値は Backend の検証を正とする。 +- 表示行数、総計、結果を構成する元レコード数は異なる概念であり、同じ「件数」として混同しない。 + +## 14.4 抽出条件・ソート・照会 state + +- 等値・不等値、範囲、空値、複数候補のいずれか・すべて・除外その他、resource とデータ型が許す条件を提供する。単一値と配列・関聯の検索意味論を区別し、複数値条件の不等値は「一致する要素がない」ことを表す。 +- 数値、真偽値、日時、範囲、NULL、Tag identity その他の型を URL、画面、server-side query まで保持する。NULL と文字列 `'null'`、未指定と明示的な空指定を区別する。 +- Tag 検索の表示文字列と確定した Tag / 外部 Tag ID を区別し、表示 locale や名称変更だけで別の Tag を指すことがないようにする。内部 Tag と外部 Tag の同じ数値 ID を混同してはならない。 +- 適用中の条件は、ドリルダウンによって生成されたものも含めて全件を画面上で確認・編輯・解除できる。画面から見えない条件を URL や一時 state に残して集計へ適用してはならない。 +- drilldown は集計セルの公開可能な軸値から条件を生成し、元の版定義と時点を引き継いだ明細表示へ進む。条件として使用できない秘匿軸値から、個別値や推測された ID を含む URL を生成しない。 +- URL には共有・再訪に意味のある resource、mode、軸、条件、sort、page、時点を保持できる。単純な複数選択は区切り文字で表し、構造化条件は番号付きの個別 key とし、状態一式を JSON、Base64、16進文字列等へ格納して隠蔽しない。 +- 番号付き条件の各 key は一意に解釈され、未指定・明示的空・不正値を server-side で区別する。旧形式の互換変換が必要な場合も、隠れた抽出条件を生んではならない。 +- sort、paging、条件候補検索は同じ query と権限制御に従う。翻訳済み表示名を暗黙の並び順・抽出条件に使用しない。安定した識別子・記録値によって同値時の順序を確定する。 + +## 14.5 集計件数の公開例外と個別情報の秘匿 + +本節は公開制御・除票・履歴閲覧制御に対する**集計値だけの限定例外**を定める。公開抑止・除票等で通常は見せないレコードも、`summary` の全件集計には含める。権限のない利用者にそのレコードの生値を見せてよいという意味ではない。 + +- 公開可能な同一条件、同一版定義、同一対象時点について、anonymous / guest / member / admin の**全体件数は一致**させる。閲覧できない履歴を母集団から削って数値を減らすことを禁止する。 +- `details` では、共通 publication / history policy によって閲覧が許されないレコード・version を除外する。`summary` の公開例外を `details` の個別レコード API、検索結果、候補、download へ拡大しない。 +- 集計に限り無条件の個別参照を伴わず公開できる軸値は、記録日時の集計粒度、版の操作種別等、明示的に許可した**非識別的な区分**に限定する。それ以外の Tag ID・名称・タイトル・URL・本文・操作者・関聯参照・旧値等は、通常の policy により公開可否を判定する。 +- 非公開履歴に由来する秘匿軸値は、結果を組み立てる **server-side で伏せ**、UI では赤字の **`********`**(半角アスタリスク8字)とする。hover または keyboard focus で「閲覧権限のない履歴に由来するため、この項目の値は表示できません。集計件数には含まれます。」という趣旨の説明を表示する。 +- 別々の秘匿値を個別グループとして残したり、非公開の名称・ID を sort key、候補値、列 key、HTML 属性、SSR data、CSV / JSON、clipboard、URL 等に含めたりしてはならない。秘匿値は識別不能な一つの安全な区分へ統合し、合計件数を減らさない。 +- 件数だけの公開であっても、非公開属性を条件・sort・検索候補・差分比較・段階的な絞込みに使用すると個別情報が推定できる。全件 `summary` で許可する条件は server-side で判定できる公開条件へ制限し、秘匿属性に対する条件・特定値の探索は認可しない。UI の無効化だけを防御境界としない。 +- 公開区分ごとの件数を示すことで起きる推論は、この公開例外で明示的に認める区分の範囲にとどめる。それ以上の細分化・交差条件・候補応答で秘匿内容を推定できる構成を禁止する。 +- 権限判定は session から server-side が確定した user・role・policy を用い、リクエスト引数で渡された role や admin flag を信用しない。権限や公開状態が変わった後は、古い query cache、SSR data、job 生成物、download token で以前の情報を再取得できないようにする。 +- `summary` の全件件数を返す場面では、権限由来の「閲覧できない N 件を除外した」という表示や `excluded_count` による誤解を生まない。秘匿表示の理由と実際の集計件数を分離する。 + +## 14.6 明細表・操作 UI・表示規則 + +- 行軸・列軸の配置と順番をドラッグ操作等で変更でき、keyboard と touch でも主要操作へ到達できる。項目一覧・行・列の配置枠は文字が端で切れない程度の小さな左右余白(各約4px)を持つ。 +- 表示 mode・版定義を切替できる。集計表は行・列の見出し、通常セル、小計、合計、綜合計を区別し、多数の列では横スクロールを用いて可読性を保つ。 +- `summary` 結果の上部には **「行数」**を表示し、通常出力行だけを数える。小計・合計行を含めず、元レコードの総件数とも区別する。`summary` に行番号の切替 UI・行番号列を設けない。旧 URL に行番号指定が残っていても `summary` では無効とする。 +- `details` は閲覧可能な version / record について列の選択・順序変更・並び替え、行番号の表示切替、ページングを行える。既定の表示件数は **100**、選択肢は **20・50・100** とする。 +- 版の明細で resource identity と version number を持つ場合、既定の順序は **resource ID 昇順、version number 昇順**とする。stable な tie-breaker を持つ。 +- resource 定義に複数値・親子関係・名称集合等がある場合、子列を選択して内表として表示できる。子列が一つの場合は読みやすい簡潔な表示とし、複数の場合は列見出しを持つ内表で示す。 +- JSON その他の階層値の表示は改行・単語途中の折返し・スクロールを用い、値を途中で省略して実データが欠落したように見せない。列幅と見出し幅は手動調整でき、sort indicator や resize handle の表示領域を考慮する。 +- 画面上の表示対象セル・行を選択してコピーできる。コピーでは行・列の順序、表示値、伏字、件数を画面と一致させ、非公開の元データを含めない。 +- 多言語の名称と alias は language を identity として取り扱い、script は実際の記録値の属性として保持する。初期表示は表示 locale の language に対応する名称を優先してよいが、他言語の記録値を消したり同じ language を locale ごとに重複列化したりしてはならない。 +- 不正な条件・上限超過・通信障害が発生しても、実行前の利用者による軸・条件・列の編輯内容を不必要に失わせない。SSR 初期表示と client navigation / hydration で同じ query state を解釈する。 + +## 14.7 CSV・JSON export と非同期処理 + +- `summary` と `details` の結果を **CSV** および **JSON** で export できる。export は画面の query と同じ評価時点、条件、母集団、秘匿規則、合計の意味に従い、単なる表示ページの一部を全体結果として出力しない。 +- CSV は利用者が選択した表示列、見出し、順序に従う。JSON は machine-readable な stable field key と階層構造を保持し、対象 Tag の全記録言語名・別名等、JSON 出力として定義した全属性を保持できる。翻訳された UI label を JSON の field identity へ置き換えない。 +- resource の version snapshot を出力するときは、現在の名称で履歴を黙って改変しない。非公開データは画面と同じ server-side policy で秘匿し、CSV の表示名や JSON の nested property を通じて漏らさない。 +- 大量 export は background job として生成し、queued / running / ready / failed / cancelled 等を利用者が区別できる。進捗、取消、失敗理由、再試行可能性を示す。job / temporary file の具体的な永続 schema は「未確定事項」の共通 Background Job 設計に従う。 +- 生成完了または進捗100%だけで download 完了としない。**実際の GET download が server-side で認可され、対象のファイル応答開始が job / download attempt に対応して確認された後**、正常時は進捗ダイアログを自動的に閉じる。 +- この開始確認は browser の受信完了・保存完了を保証しない。UI で「保存完了」と誤って表現してはならない。GET 開始失敗、期限切れ、権限変更、ファイル不在、確認 timeout、生成失敗では自動終了せず、利用者へ安全な理由と再試行方法を示す。 +- 重複 download、取消との競合、別 session / 別利用者による job の参照、token 再使用・期限切れ、受信中の切断を考慮する。job と一時 file の所有者・権限は作成時と取得時の双方で検証し、公開抑止の変更後に旧生成物から情報を再取得できないようにする。 +- ブラウザー・server の全体メモリに export file を丸ごと読み込ませない。生成処理の並列数・時間・サイズ・保存量に上限を設け、作業中断、stalled job、期限切れ file の cleanup を行う。 +- export の一時生成物は Material の正本 Blob 台帳と区別し、永続的な content resource として公開しない。 + +## 14.8 エラー、上限、性能と運用 + +- 異常は「データ取得エラー」に一律化せず、原因と利用者が取れる対処を区別する。少なくとも軸上限超過、列組合せ上限超過、worker 不在、query timeout、export 生成失敗、download 開始失敗、期限切れ、認証・権限不足を区別する。 +- 軸8個超過は **HTTP 400 相当**の `too_many_axes`、列組合せ100超過は **HTTP 422 相当**の `too_many_columns`、worker 不在は **HTTP 503 相当**の `export_worker_unavailable` を機械判定可能な error code とともに返す。内部 server action では同等の型付き失敗として扱える。 +- 詳細な入力条件、SQL、stack trace、secret、非公開の個別レコードを error message に含めず、機械判定可能な error code と locale に応じた UI 説明を分離する。公開できない対象の実在有無が 404 等の応答差から推定できないようにする。 +- query・候補取得・export には適切な rate limit、resource budget、timeout、paging を設け、極端な JOIN・高次元集計によって他利用者の通常閲覧を妨げない。 +- V2 の集計実装は PostgreSQL 上の現在・履歴 model を正本とし、PostgreSQL の適切な集計・索引・時刻処理を用いる。MySQL の JSON 展開関数、Rails の controller / worker API 等を完成仕様として移植しない。 +- cache や事前集計を用いる場合も再生成可能な派生データとし、権限・policy・評価時点・query の差を無視して古い個人情報を再利用しない。集計のために immutable history を書き換えない。 + +## 14.9 考え方・理由 + +集計表を標準機能とするのは、タグ広場の利用者が記録の蓄積量・分類の偏り・変更の時系列を、独立した外部分析環境へデータを複製せずに把握できるようにするためである。`summary` と `details` を分けるのは、数値の公開価値と個別履歴の閲覧権限が別だからである。 + +通常の重複排除件数と Tag 行軸の加算件数を分けるのは、複数 Tag を持つ投稿を「投稿数」と「Tag 別出現数」の同じ一値で表せないためである。合計を表示ページや中間小計でなく元の通常セルの規則から定義するのは、並び替え・ページング・列追加で答えが変わるのを防ぐためである。 + +非公開履歴を含めて総件数を公開する一方で、その属性・条件探索を遮断するのは、蓄積量の統計的公開を個別の削除・公開抑止の抜け道にしないためである。軸値の server-side マスク、非公開区分の統合、条件の認可を一つの規則として扱うのは、見た目だけの伏字では URL・CSV・JSON・差分照会等から情報が再流出するためである。 + +export の成功境界を「server-side のファイル応答開始確認」とするのは、iframe 等の browser native download では保存完了を確実に検知できない一方、進捗100%を成功としてダイアログを閉じるのも早すぎるためである。具体的な worker 永続 schema を標準要件へ固定しないのは、複数の大量処理で共通 job 基盤を再利用できる余地を保つためである。 + +# 15. セキュリティ・性能・運用 + +## 15.1 セキュリティ + +- form action、server endpoint、外部 API その他、全ての更新経路で認証と権限を server-side が検証する。 +- recovery code、旧 system の引継ぎコード、migration ticket その他の認証・移行 secret を log、URL、analytics へ露出させない。 +- Markdown と HTML を sanitize する。 +- 外部 URL 取得では SSRF 防御を行う。 +- upload file の MIME、size、SHA-256 を検証する。 +- path traversal を禁止する。 +- 署名 URL の有効期限を必要最小限にする。 +- CORS、CSRF、rate limit を API の認証方式に合わせて構成する。 +- 管理操作を `events` と必要な version スナップショットにより監査できる。 + +## 15.2 rate limit + +少なくとも次に個別の rate limit を設ける。 + +- guest user 作成。 +- Preview API。 +- 投稿・タグ・説明・素材の作成と更新。 +- file upload と ZIP download。 +- 検索と autocomplete の高頻度呼出し。 +- recovery code の検証・更新、および account migration ticket の検証。 + +rate limit は IP だけに依存せず、user、subnet、target host、resource その他を組み合わせられる。 + +## 15.3 性能 + +- 一覧取得はページネーションを必須とする。 +- N+1 query を避ける。 +- Tag parent 展開を無制限 recursion にしない。 +- route 単位の code splitting を行い、全 page の client code を初回に読み込ませない。 +- 初回・直リンクは SSR を用い、client-only rendering を必須にしない。 +- 大量の sitemap、ZIP、thumbnail、類似度計算は request 内で無制限に実行しない。 +- 大量処理は再開可能な job とし、進捗と失敗理由を記録する。 +- Tag Name identity / script rendering のような低頻度の登録検証は、通常検索や閲覧を複雑化させる恒久 column を増やすより、必要時の計算または再生成可能 index を優先してよい。 +- JSON snapshot、説明本文、search index その他の大きな値に適切な上限を設ける。 +- cache / search index / rendered-name index は再生成可能とし、正本と混同しない。 + +## 15.4 backup と復旧 + +backup は少なくとも次を対象とする。 + +- PostgreSQL database。 +- object storage の object。 +- 暗号化 key と credential の復旧手順。 +- export manifest。 + +復旧では、DB と object storage の時点差により version 参照が壊れていないことを検証する。 + +## 15.5 観測可能性 + +- request ID を API、job、外部取得 log で共有する。 +- Preview、同期、thumbnail、ZIP、upload の失敗理由を分類する。 +- rate limit、storage 使用量、job backlog、外部 API error を観測できる。 +- user の秘密情報、recovery code、旧 system の引継ぎコード、migration ticket を log に出さない。 + +## 15.6 互換性と migration + +- `hub.nizika.monster` から `btr.moe` への domain migration では、origin を跨いで localStorage または cookie を直接共有できることを前提にしない。 +- 旧 account の migration に必要な期間は、旧 origin に migration gate を維持し、旧 credential の検証後に短寿命・一回限りの migration ticket を V2 へ引き渡せるようにする。 +- migration gate を廃止する時点では、旧 origin に残る未移行利用者への扱いを明示し、単純 redirect 化によって予告なく account recovery 手段を失わせない。 +- schema 変更で履歴の復元可能性を失ってはならない。 +- V1 から既存内部 ID を維持して identity column へ移行する必要がある migration では、通常 application の INSERT 権限を緩めず、migration 専用経路で明示値を投入した後に identity sequence を既存最大値より先へ整合させる。完成 schema は `GENERATED ALWAYS AS IDENTITY` を維持する。 +- JSON snapshot の構造変更は versioning または migration 方針を持つ。 +- 大量 backfill は再実行可能かつ途中再開可能にする。 +- 現行データを黙って推測変換せず、解決不能な行を識別して停止または隔離する。 +- migration の都合で既存 version を更新してはならない。ただし、履歴として成立させるために欠損していた初期 snapshot を補完する場合は、根拠と再現性を明確にする。 + + +## 15.7 考え方・理由 + +security、performance、backup、observability は個別 feature の後付け要件ではなく、全 resource の成立条件である。特に外部 URL 取得、object storage、guest、自動同期は system boundary を広げるため、共通 policy と計測点を持たせる。 + +performance では単純な server throughput だけでなく、低メモリ client、bundle size、streaming / background job、再生成可能 cache を含めて考える。backup は DB だけでなく Blob ledger と object storage の対応整合を対象とする。 + +# 16. 受入条件 + +## 16.1 認証・権限 + +- 通常 request は V2 session から current user を解決し、旧 `user_code` または recovery code を request ごとの通常認証に使用しない。 +- recovery code が user ごとに一意であり、検証成功時に新しい session を発行できる。 +- `hub.nizika.monster` の旧 credential を旧 origin で検証し、短寿命・一回限りの migration ticket を介して `btr.moe` の対応 user へ移行できる。 +- migration ticket、旧 `user_code`、recovery code が URL または log へ露出しない。 +- 有効な migration ticket が提示された場合、不要な guest を先に作成せず migration user の session を確立できる。 +- BAN された user または IP が保護 API を利用できない。 +- guest、member、admin の権限境界が server-side で一貫している。 +- 管理操作が `events` を通じて監査でき、version 管理対象では同じ `event_id` から変更結果を横断して確認できる。 + +## 16.2 投稿 + +- URL 正規化後の重複を防げる。 +- URL から取得可能な基本情報が自動取得される。 +- 一括登録前に最終保存予定値を確認・修正できる。 +- optimistic concurrency により無言の上書きを防げる。 +- `post_versions` だけで当時のタグ、親投稿、サムネイルその他を復元できる。 +- 局所記載が `int8range` の半開区間へ正規化され、同一 Post・Tag の Range overlap を DB が拒否できる。動画長との整合は同期を含む application logic が維持する。 +- Post detail で局所記載 Tag を常時確認でき、再生位置を取得できる場合は section 内外を表示強度で判別できる。再生位置を取得できない場合も局所記載 marker と範囲を確認できる。 +- `video_posts.duration_ms` が同期用 metadata として更新され、Post 基本データの直接編輯項目にならない。 +- 除票投稿が通常の一覧、検索、詳細、sitemap、候補取得へ出ない。 + +## 16.3 event・履歴 + +- 一つの利用者レベル操作が複数リソースを変更しても、一つの `event` として横断表示できる。 +- 各 `*_versions` は `event_id` を持ちながら、その対象の当時状態を単独で復元できる。 +- event 内の各変更に業務上の順序を持ち込まない。 +- 同一 event が同一 version 管理対象について複数の中間 version を生成しない。 +- event 全体または選択した一部の変更を、新しい event として差し戻せる。 +- 後続 event と競合する変更を無言で上書きしない。 + +## 16.4 タグ + +- 同一 language における Tag Name identity の衝突を検出できる。 +- 対応 presentation script へ変換した結果が別 Tag Name と同一になる名称を registration error として拒否できる。 +- 同一 `tag_id + language_code` に primary name が最大一件であることを DB が保証し、名称が存在する場合にちょうど一件であることを application が保証できる。 +- alias が直接 Tag を参照し、alias chain が発生しない。 +- generated name を識別・検索・絞込み・修正できる。 +- Reading を発音 language と notation とともに保持し、Tag 検索へ利用できる。 +- 日本語では Reading に基づく読み順表示ができる。 +- allowed-character policy が現在の UI locale ではなく、登録対象 Tag Name の target locale に対して適用される。 +- search-equivalent な既存名称を類似 Tag として警告できる。 +- 親子関係の自己参照と循環を防げる。 +- `tag_implications` の変更が既存 Post の current tag set を遡及変更せず、その後の Tag membership 反映操作で再帰展開される。 +- Tag の合併・分離を一つの domain operation / event として実行し、Post membership、名称、説明、Material、implication、Tag Group membership その他の波及を整合させながら immutable history を書き換えずに保持できる。 +- 廃止 Tag を新規付与できない。 +- Nico その他の外部タグを通常入力から付与できず、内部 Tag category と混同しない。 +- Tag 物理削除時に親子関係と deerjikist 情報が規則どおり補正される。 +- 過去の Tag Version が現在状態に依存せず、当時の multilingual names / aliases / readings / category を表示できる。 +- 既知 Material を投稿内容から検出し、対応する material Tag と `tag_implications` による親 Tag の補完に利用できる。 +- similarity だけを直接的な事実認定として扱わず、Material detection その他の直接根拠より弱い候補 signal として扱える。 +- Tag Group を Tag domain 配下の第二級 resource として扱い、Tag と他の Tag Group を structured membership として整理できる。 +- Tag Group の membership と `tag_implications` の semantic entailment を混同しない。 + +## 16.5 タグ説明・ヘルプ + +- 汎用 Wiki ではなく、旧 Wiki の内容を Tag Description、Help、structured Tag Group その他の定められた resource へ分離して扱う。 +- Tag Description が Tag × content language として管理され、特定の Tag Name 変更に従属しない。 +- Help の canonical source と translation を区別でき、translation が参照した source revision を追跡できる。 +- source 更新時に stale translation を判定できる。 +- UI locale と異なる language の説明・Help translation を編輯できる。 +- 競合更新を検出できる。 +- 過去版と差分を表示できる。 +- 自動 link が Markdown link、code、URL を破壊しない。 +- 空本文と policy 対象が適切に noindex となる。 + +## 16.6 素材 + +- guest は既存素材を member と同じ version / event 記録経路で更新できる。 +- guest は URL-only を含む新規素材を作成できず、file upload もできない。 +- Material 自身に semantic name を持たせず、意味上の名称を `material` category Tag / Tag Name が担う。 +- file Material と URL-only Material を排他的に表現し、同じ Blob または同じ外部 URL を複数の active Material として重複保持しないことを保証できる。 +- 同じ Material Tag に複数 Material を所属させられる一方、意味上の名称で区別すべき差は Tag の粒度で表現できる。 +- Material 同士の親子・包摂・同一 Tag 内 representative / primary 関係を持たない。 +- 通常の分類済み Material が `material` category Tag に所属し、非-`material` Tag が Material を直接所有しない。 +- Tag の representative Material が、その Tag を `tag_implications` 上で imply する descendant material Tag に所属していることを検証できる。 +- `tag_implications` を単なる素材の所属・由来・部品関係の代用にしない。また `_` を semantic delimiter として implication を自動生成しない。 +- Material の媒体種別を Blob metadata から識別でき、同期経路も手動作成と同じ image / video / audio validation を通る。 +- file は共通 Blob 台帳を介して参照し、SHA-256 により同一 binary を一意に識別できる。同一 Blob を複数 active Material として重複保持しない。 +- Material の通常削除を物理削除とし、current row 削除後も immutable Material Version から当時状態を復元できる。 +- 通常の Material 削除・参照消失を理由に Blob / object を自動削除せず、content purge 後も履歴から Blob identity と purge state を確認できる。 +- purge 済み Blob の再 upload / import または新規参照を拒否できる。 +- Material Source は同期元と外部 file code の組合せで一意に識別でき、source filename / path を Material の semantic name とみなさない。 +- 同一 binary が別 source から検出された場合、Material を重複作成せず Material Source を追加できる。 +- 同期元から file が消失しても Material / Blob / Material Source を自動削除せず、来歴を保持できる。 +- 同期によって Tag を自動付与しない。 +- source path prefix による同期抑止を適用でき、解除もできる。 +- file Material が system-wide unique な canonical export path を一つ持ち、通常 ZIP・選択 ZIP・Git その他が同じ path を利用できる。 +- canonical export path の初期値を同期元 path から生成できる一方、人間による変更後に同期が source path へ強制的に戻さない。 +- folder resource や path rewrite rule を必須とせず、Material ごとの canonical export path だけで folder tree を構成できる。 +- 任意に複数選択した Material だけを download / export できる。大規模 package の background job / temporary object 管理は共通 Job 設計に従う。 + +## 16.7 公開制御・検索 + +- 廃止、索引抑止、内部検索抑止、公開抑止、除票が別状態として機能する。 +- noindex が外部 API、UI、sitemap で一貫する。 +- Tag の primary name、alias、Reading、generated name を検索できる。 +- UI locale にかかわらず他 language の Tag Name を search / autocomplete の入口として利用できる。 +- 正本 table に汎用 `search_key` を持たなくても、共通 search transformation と再生成可能 index により検索できる。 +- Unicode 標準処理とタグ広場固有 `search_rules` を分離できる。 +- Tag Name の primary / alias による索引抑止が適用される。 +- `robots.txt` だけで noindex を代用しない。 +- autocomplete と通常検索が廃止・抑止状態を正しく扱う。 +- `bot操作` が残る Post を similarity の reference corpus から除外しつつ、分類・類似検索の target として扱える。 +- Post similarity の通常特徴量へ外部 Tag / workflow state を混在させず、Tag similarity では external ↔ internal の共起を候補発見 signal として利用できる。 +- 高い Tag similarity を同義・同一 Tag または無条件の自動対応とみなさない。 + +## 16.8 セキュリティ・運用 + +- V2 の DB が PostgreSQL 18 系で動作し、database default に `PG_UNICODE_FAST` を用いる。 +- system-wide な ID 型方針と各 table の実際の物理型が `tables.zip` と整合している。 +- IP address、structured JSON、SHA-256 その他の物理表現が PostgreSQL 物理設計の共通原則と `tables.zip` に従っている。 +- Preview API が private address、redirect、large response、browser abuse を防ぐ。 +- upload と download に size・rate limit がある。 +- backup から DB と object storage を整合した状態へ復旧できる。 +- job の失敗と再実行状態を追跡できる。 +- 拡張天保暦を DE440、日本標準時、国立天文台準拠の朔・二十四節気定義に基づいて再現可能に変換できる。 +- 2033年問題で拡張天保暦の補充規則が適用され、国立天文台案1に相当する閏11月を得られる。 +- 拡張天保暦専用 conversion cache を正本から独立した再生成可能 cache として利用でき、cache miss 後に将来約10年分を background で補完できる。 + + +## 16.9 集計表 + +- `latest` / `history` と `summary` / `details` を独立に切替でき、同一評価時点で表示・集計・export の対象が一致する。 +- 行・列の軸と日時粒度を変えたクロス集計、通常セル、小計、合計、綜合計を再現できる。Tag を投稿の行軸に含める場合は実際の Tag 別通常セルを加算し、それ以外の distinct 投稿数と区別できる。 +- 公開可能な同一条件なら anonymous / guest / member / admin で summary の全体件数が一致し、details には権限外の履歴を出さない。 +- 秘匿軸値が server-side で統合・マスクされ、API、SSR、候補、条件、drilldown、clipboard、CSV、JSON のいずれからも元の識別子・値を復元できない。 +- 検索・ドリルダウンで発生したすべての適用条件を表示・編輯・解除でき、表示 locale を切り替えても対象 identity と件数が変わらない。 +- summary の行数が通常行だけを数え、行番号を表示せず、details の列・内表・行番号・ページングは正常に機能する。 +- CSV / JSON export は同じ query / policy / 出力値を用い、server-side の GET ファイル応答開始の確認後に進捗ダイアログが閉じる。生成失敗・download 開始失敗時は理由を示し、自動的に閉じない。 +- 軸上限・列上限・worker 不在を区別したエラーが通知される。高負荷 query・job の resource budget と一時生成物の期限切れ処理が機能する。 + +## 16.10 考え方・理由 + +受入条件は implementation checklist ではなく、完成形が外から観測可能な性質を列挙する。特定 framework API、test library、branch 構成を acceptance に混ぜないことで、実装手段が変わっても system requirement の判定基準を維持する。 + +各受入条件は本文の設計 rationale を置き換えるものではなく、最終的にその思想が observable behaviour と data integrity に現れているかを確認するために使う。 + +# 17. 未確定事項 + +本章は、完成形として採用する機能自体ではなく、確定済みの機能を実現する上で物理設計または詳細 policy がなお凍結されていない事項だけを記載する。 + +## 17.1 媒体の文字抽出・文字起こし + +画像、動画、音声その他の媒体に含まれる文字情報・発話内容を、共通 translation そのものへ直接埋め込まず、独立した文字抽出 / transcription resource として扱う方針は確定している。抽出された文字列は Tag Name と同様に language と表記 script を明示でき、翻訳が必要な場合だけ共通 `translations` を参照できる構造とする。 + +ただし次は未確定とする。 + +- 文字抽出 / transcription resource の正式名称、table 名、column 構成。 +- 画像内文字の矩形・polygon その他の座標表現。 +- 動画内文字について、画像座標と時間 range をどの単位で組み合わせるか。 +- 音声・動画音声の transcription における時間 range、speaker、発話分割その他の表現。 +- 同一箇所に複数 language / script の文字列が存在する場合の current model。 +- OCR / ASR 等の機械抽出値と、人間が確認・修正した文字列をどう区別するか。 +- 文字抽出 resource 自体の履歴・event・権限。 + +媒体座標を正規化して保持する場合は floating point ではなく 0 から 32767 の整数を用いる方向を維持するが、矩形・polygon その他の具体的な anchor schema は未確定とする。 + +## 17.2 Background Job と一時生成物の管理 + +ZIP export、thumbnail 再生成、同期、暦 cache 補完その他の request 内で完結させるべきでない処理を共通の background job として管理する方針は採用する。特に大量 Material の ZIP export は、全 file と完成 ZIP を request process の memory に保持する方式を採用せず、job として生成し、一時的な object を download 可能にする構成とする。 + +「集計表(Counts)」で定めた CSV / JSON の非同期 export とダウンロード応答開始の確認は確定した機能要件とし、worker / job / temporary object の共通化・保存 schema・有効期限等の物理方式については本節の未確定事項に含める。特定の Rails worker / filesystem spool / systemd unit を V2 の要件として固定しない。 + +Material の canonical export path、同期元、Blob identity その他の Material domain 自体は本章の Job 設計に依存させない。一時 ZIP は通常の永続 Blob 台帳へ混在させず、期限付き生成物として削除可能な storage lifecycle を持たせる方向とする。 + +ただし次は未確定とする。 + +- 共通 job resource / table の正式名称と column 構成。 +- queued / running / succeeded / failed 等の状態表現。 +- progress、error、retry、cancel の表現と retention policy。 +- job の作成者・閲覧権限と、system job / user-requested job の区別。 +- 一時 ZIP その他の temporary object の storage namespace、保持時間、download credential、cleanup policy。 +- 同期その他の定期 job と利用者要求 job を同じ current table で扱うか。 + + +## 17.3 利用者設定・保存 scope・user theme + +settings domain は、V1 / V2 の既存実装や既存 table 定義を AI により先行作成した経緯があり、設計者による domain 設計レビューをまだ完了していない。このため、現存する `settings` / `user_theme_slots` その他の schema、`localStorage` key、設定 UI / API を完成仕様の根拠としない。 + +まず各設定候補を、少なくとも次の ownership / lifetime に分類してから物理 schema と UI を決める。 + +- user account に追従し、別端末でも同期する設定。 +- browser / device / viewport その他に閉じ、account 間または端末間で同期しない設定。 +- 一時的な UI state であり、永続化自体を行わないもの。 +- system-wide default / admin configuration として扱うもの。 + +残る論点は次とする。 + +- `theme`、title / thumbnail 自動取得、editor mode、日付・暦表示、locale preference、一覧件数・sort、sidebar、keyboard shortcut、prefetch、animation、thumbnail 表示その他の各設定候補をどの scope に分類するか。 +- account-portable settings を一つの resource / table にまとめるか、意味と寿命の異なる設定を別 resource に分けるか。 +- device-local settings の永続化手段と namespace。すべてを `localStorage` の一 JSON object に集約することを前提としない。 +- user theme customisation を account-portable とするか device-local とするか、または一部を分離するか。 +- user theme の slot 概念を採用するか、採用する場合の slot 数と identity。 +- theme property を JSON / JSONB の `tokens` object として保持するか、typed field / relation その他で表現するか。JSON / JSONB は既存実装に存在するという理由だけでは採用しない。 +- settings 用 DB resource の natural identity、surrogate ID の要否、version / audit の必要性。 +- 専用 settings route / page を設けるか、各 domain の contextual settings とどう分担するか。 +- server-side settings API を設ける場合の resource boundary と endpoint。 +- default 値を DB row として materialise するか、row 不在を default と解釈するか。 +- guest の settings を server-side に保存する範囲と、guest cleanup / migration 時の扱い。 + +V1 の具体的な settings 実装は比較材料として調査できるが、既定値のまま存在する row や UI から利用されていない column を V2 採用の根拠としない。個別機能について別節ですでに保存 scope が明示的に確定している場合は、その決定を尊重しつつ settings domain 全体へ整合させる。 + +## 17.4 法的文書 + +法的文書を Help と別の第一級 resource とし、source-controlled static document として version 管理する方針は確定している。application から過去版を閲覧でき、翻訳版は source language / source version number を示し、canonical source が進んだ場合に古い source version に基づくことを表示できなければならない。 + +残る未確定事項は次とする。 + +- source file format を V1 と同じ MDX とするか、Markdown + metadata その他とするか。 +- metadata / manifest を各 version file とどう対応付けるか。 +- 利用規約等について user acceptance history を持つか。 +- 重要改定時に再同意を要求する product requirement を採用するか。 + +必要性が確定するまで acceptance 用 DB resource を先行作成しない。 + +## 17.5 Navigation overflow・`/more` + +`/more` は navigation index への通常 link とし、desktop では hover / focus overlay を表示し、第一段からの overflow 先も兼ねる。`/more` は route table の機械的な全列挙ではなく、利用者が直接到達する価値のある page / action を網羅する人間向け navigation index とする。 + +第一段から overflow させる場合は、確定した標準順序の右側から順に退避する。ただし、現在選択中の active domain は overflow させず第一段に残す。利用者が現在どの domain を照会しているかを第一段だけで判別できることを優先する。 + +残る論点は次とする。 + +- navigation index の大分類・小分類・並び順。 +- 各 domain について、一覧・検索・追加・履歴・設定その他のうち、どの page / action を「直接到達する価値のある入口」として掲載するか。 +- 第一段の active domain を残すために他項目を追加で overflow させる際の具体的な layout / measurement algorithm。 + +`/more` の大分類候補として、現時点では「広場」「ニジラー」「タグ」「素材」「翻訳」「一角」「申請」「文書」「ユーザー」「管理」「法規」を検討する。これは top navigation 第一段の項目集合と一致を必須としない。 + +広場配下では「一覧」「追加」「類似検索」「全体履歴」「ヘルプ」その他を候補とする。人気度・投票に関する入口は機能の要否と名称を含めて未確定とする。他 domain についても、一覧・追加・履歴・Help 等を機械的に揃えるのではなく、その domain に実際に成立する操作だけを配置する。 + +## 17.6 第二段・第三段 navigation と quick search + +第二段 navigation は、左側に現在 domain 内で共通する menu を並べる。個別 resource / page を照会している場合は separator を挟み、その右側へ当該個別 page に対する contextual navigation を列挙する。右側には原則として route 遷移を伴う navigation item だけを置き、page-local state の切替・toggle・保存その他の操作を置かない。domain 共通 navigation と個別対象への navigation を同列に混在させない。 + +第三段 navigation は、第二段のさらに内側に独立した domain が成立する場合に使用できる。ただし現時点で採用対象はないため、初期仕様では第三段を設けない。将来必要になった時点で、その内側 domain の責務を明確にしたうえで追加する。 + +quick search は、検索機能を持つ domain について、その domain の名称に相当する代表的な識別情報を素早く検索する用途を基本とする。同等の検索 UI が sidebar その他に常時表示される画面では重複させない。広場では sidebar に同様の検索 UI が存在するため、第二段 quick search を置かない。 + +Post domain は sidebar が個別 Post の情報・操作を担うため、第二段右側の contextual action section も設けない。 + +残る論点は次とする。 + +- quick search を置く具体的な domain と、それぞれ何を「名称に相当する検索対象」とするか。 +- Enter 時に一覧へ即時 filter を適用するか、検索結果 route へ遷移するか、詳細検索との関係。 +- 第二段の domain 共通 menu と contextual action の正式項目・順序。 + +Tag Name / alias / Reading は Tag の同一画面内の編輯 state に統合したため、名称管理を第三段の代表例として扱わない。 + +## 17.7 locale routing・locale switcher + +locale prefix のない URL へアクセスした場合は、少なくとも session / cookie に記録した直近 locale、browser の `Accept-Language`、system default locale を候補として表示 locale を決定する。user account に追従する locale preference を追加するか、その場合にどの順位へ置くかは settings domain と合わせて未確定とする。 + +locale を決定した後は locale-prefixed URL へ redirect し、prefix なし URL を同一 content の恒常的な別 URL として提供しない。 + +locale switch では、可能な限り現在照会している同一 resource / operation を維持した locale-prefixed URL へ移動する。 + +残る論点は次とする。 + +- 未対応 language の fallback。英語を fallback とする案を含めて確定する。 +- locale-prefixed URL への redirect status と初回 visitor の扱い。 +- public API の representation に locale path を持たせるか。 +- locale switch 時に同一 resource / state を維持できない場合の fallback destination。 + +## 17.8 URL state・history / version / diff routing + +route は resource identity と、共有・再訪に意味のある状態を表す。一時的な tab / accordion その他を URL へ機械的に載せないことを基本とする。 + +非文書系 resource の履歴は、原則として `//versions?id=` のように domain-wide version 一覧を query parameter で個別 resource へ絞り込む。個別 resource 配下の `/history` nested route は標準としない。 + +`?version=` による historical resource 表示および version 間 diff は、Tag Description / Help その他の文書系 resource に限って使用する方向とする。非文書系 resource について、通常 route の state として historical version viewer や diff を持たせることを既定としない。 + +残る論点は次とする。 + +- browser history、bookmark、share の対象とすべき state の system-wide 判定規則。 +- 文書系 resource の historical view / diff の exact query contract。 +- `hub.nizika.monster` から `btr.moe` への migration 期間に、どの route を redirect 対象とするか。互換 redirect を無条件の恒久要件とはしない。 + +## 17.9 Post embed provider / security policy + +Post 詳細の layout、編輯 tab、Tag 記載 rich edit、Post URL の転居届、汎用備考を設けないこと、対応済み provider の embed を常時自動読込みすること、自動再生だけを client setting として持ち既定を off とすることは確定済みであり、5章および13章に定める。 + +残る論点は、未知・未承認 site の URL について iframe を許可する provider / security policy である。利用者 preference として `embedAutoLoad` を復活させず、少なくとも次を決める。 + +- 未知 site の iframe を完全に禁止するか、allowlist / sandbox 条件を満たす場合にだけ利用者の明示 button 後に表示できるようにするか。 +- provider ごとの `sandbox`、`allow`、referrer policy その他の安全条件。 +- 外部 embed が利用不能な場合に thumbnail / external link へ fallback する表示。 + +## 17.10 投稿追加の作業 state + +投稿追加の作業段階を query parameter ではなく routing で識別すること、入力中 data や自動取得結果その他の一時 state を URL へ詰め込まず `sessionStorage` / client state を中心に保持すること、一括登録を Post 単位で独立処理して recoverable failure を編輯・再試行可能にすることは確定済みであり、5章および13章に定める。 + +残る論点は次とする。 + +- URL 入力、確認・修正その他の各作業段階に与える exact route。 +- 未確定事項として検討中の「仮登録」を採用する場合、その temporary resource と browser-side session state の責務、寿命、復旧境界。 +- tab / browser session を閉じた後まで draft を保持する機能を将来追加する場合の保存先・権限・retention。初期仕様では恒久 draft / shareable draft を要件としない。 + +## 17.11 TagLink 遷移 semantics・Tag 台帳の multilingual 補助表示 + +Tag 照会・編輯画面、Tag 台帳の既定 filter / sort / columns、未公証 filter、Tag 台帳から Tag 照会への遷移は確定済みであり、13章に定める。 + +残る論点は次とする。 + +- `TagLink` の system-wide な click destination を将来統一するか。現時点では既定遷移を Tag で絞り込んだ Post 一覧とし、呼出し側が destination を override できる能力を維持する。Tag 台帳では Tag 本体と上位 Tag の双方を Tag 照会画面へ遷移させる。統一自体は後の検討事項とし、既定 semantics を勝手に変更しない。 +- Tag 台帳で current locale の primary / alias に mouse cursor を重ねた場合、他 language の名称 / alias を補助表示するか。採用する場合の hover / focus / touch の等価操作と情報量。 +- Tag 台帳の未公証 filter が current locale の primary だけを対象とするか、指定 language または任意 language の `auto_generated = true` まで対象とするか。未公証 filter 自体を提供することは確定済みとする。 + +## 17.12 Tag Name の省略表示 + +category colour、visible な category text / icon を Tag ごとに併記しないこと、共通 `TagLink`、長名の ellipsis / hover scroll、`auto_generated = true` を「未公証」と表示して編輯時の「公証」操作で `false` にすることは確定済みである。 + +残る論点は次とする。 + +- mouse hover を利用できない touch / keyboard 環境で、省略された Tag Name 全文をどの操作で確認できるようにするか。 + +## 17.13 一角 UI・navigation・順序編輯 + +- domain navigation の正式項目。gallery / list / create / global changes / Help 等を候補とする。 +- 特定 Pool の投稿、編輯、履歴、順序編輯その他の contextual action。 +- `pool_posts.position` を利用者が編輯する具体的 UI と、drag / keyboard / touch 操作。 +- public / private、series / collection その他の状態を一覧・詳細でどう表現するか。 + +## 17.14 文書 navigation + +`/:locale/help` domain の日本語表示名を「文書」とし、扱う resource は Help のみに限定することは確定している。法的文書その他をこの domain へ統合しない。 + +残る論点は次とする。 + +- 文書の目次、検索、表示、編輯、履歴、diff への標準導線。 +- 各主要 domain の第二級 navigation に、その domain に対応する文書への入口を置く範囲。 +- `/more` 上で Help と法的文書をどの分類・順序で提示するか。resource boundary は統合しない。 + +## 17.15 Translation UI・routing + + +- Translation の一覧、検索、詳細、編輯、履歴 route。 +- 第一段の「翻訳」から入ることは確定済みとし、`/more` 上の配置と domain navigation。 +- stale translation、未翻訳、source change その他の状態表示。 +- owner resource から translation へ入る導線と、translation から owner へ戻る導線。 + +## 17.16 申請 + +申請を第一段の独立した主要作業入口として扱い、「御意見板」を申請 domain 内の機能・導線に含めることは確定している。申請 resource 自身の boundary と workflow は未確定とする。 + +Post URL の「転居届」は申請として扱うことを確定する。同一外部 content の URL 移転だけを対象とし、member は admin の受理を要する。admin 自身も同じ転居届を提出してセルフ受理し、直接 URL を更新する別経路は設けない。転居届を generic 申請 resource の一種類として物理的にどう表現するかは、本節の申請 model と合わせて確定する。 + +- 申請者、対象、種類、理由、公開範囲、状態、審査、event その他の current model。 +- 申請一覧、自分の申請、申請 form、審査 UI の route / navigation。 +- moderation request、権限 request その他を一つの申請 resource で扱う範囲。 +- 公開申請を許可する場合の公開 policy。 + +御意見板は申請 domain 配下に置くが、申請 resource と同一の data model へ機械的に統合しない。問い合わせを申請配下へ置くか、別入口 / resource とするかは引き続き検討する。 + +## 17.17 御意見板 + +御意見板は申請 domain 内に置く新設の意見投稿面であり、forum / SNS を目的としないこと、screenshot を任意添付できること、client-side operation trace を常時収集しないことは確定している。 + +- 投稿内容の種類。改善提案、意見、その他をどこまで分類するか。 +- 公開 / 非公開を利用者が選択できるようにするか、既定公開範囲。 +- 運営からの回答・処理状況をどのように表示するか。 +- 公開投稿を他利用者が閲覧できる場合の一覧・検索・moderation policy。 +- screenshot attachment の file type、size、storage、retention、公開範囲。 +- 申請 domain 内での申請 resource との相互 link / 変換、および問い合わせとの関係。 + +## 17.18 問い合わせ + +問い合わせを御意見板とは別に設ける案を検討する。 + +- 質問、困りごと、確認依頼その他、御意見板との目的境界。 +- 原則非公開とするか、公開可能な問い合わせを認めるか。 +- 回答者、状態、close / reopen、retention。 +- screenshot attachment を共通利用するか。 +- 申請へ昇格させる操作を設けるか。 + +## 17.19 User・ニジラー・操作ログの導線 + +「ユーザー」を第一段に置き、「ニジラー」は第一段の独立項目としないことは確定している。User domain の具体的内容と、deerjikist category Tag / ニジラー情報との relation は未確定とする。 + +- 自分以外の User page をどこまで公開するか。 +- User と `deerjikist` category Tag が同一人物を表す場合の関聯付け、自己申告、本人確認、管理者確認その他の provenance。 +- `events` を UI 上で「操作ログ」その他の名称で system-wide に閲覧する導線。 +- User page から actor を絞った操作ログへ遷移する導線。 +- 操作ログを `/more` の「ユーザー」「管理」その他のどの分類へ置くか。 + +## 17.20 抑止管理 UI + +「抑止」を監査・管理上の重要な入口として admin の場合だけ第一段へ表示し、横断 UI domain として提供することは確定している。索引抑止、内部検索抑止、公開抑止、素材同期抑止その他を一種類の状態へ統合しない。 + +- 抑止一覧の分類・filter・検索。 +- policy ごとの作成・解除・理由表示。 +- user 向け申請と admin の確定操作をどう分けるか。 +- `/more` の「管理」配下へ置く部分と、第一段「抑止」から直接到達させる部分の関係。 + +## 17.21 共通 UI pattern / component の境界 + +新しい page は system-wide の共通 UI pattern / component を優先して組み立て、同じ役割の page-local implementation や style class 群を安易に増殖させない。再利用可能な UI concept は原則として Svelte component に責務を集約し、Tailwind / CSS class はその component 内部の実装手段として扱う。日付・日時入力については、暦表示と不詳日時を扱える共通の自作 component を用いることも確定事項とする。 + +- application shell、top navigation、main area、sidebar、page title、tab group、form field、resource table、resource grid / list、TagLink、pagination、context menu、state marker、loading / empty / error / conflict 等のうち、どこまでを本書で canonical pattern として定義するか。 +- 実際の component 名・props・CSS contract を本書へ含める範囲。 +- implementation specification を別文書に分ける必要が生じる境界。文書数を増やすこと自体を目的にしない。 +- resource ごとの履歴画面を個別の table / layout として量産するか、差分・version・actor・日時・差戻し導線を含む system-wide の共通 history pattern を設けるか。resource 固有情報だけを拡張 slot として差し込む案を含めて比較する。 + +`TagLink` は category 表現と遷移 semantics の欠落を防ぐため、clickable Tag 表示の canonical component / pattern とすることが確定済みである。 + +## 17.22 Sidebar・responsive・mobile layout + +sidebar の有無は画面の用途ごとに決め、sidebar 幅を利用者が変更・device-local に保持・reset できることは確定している。 + +- Post detail、Material、検索画面その他、具体的に sidebar を持つ page と、その内容。Tag 照会・編輯画面は専用 sidebar を持たないことを確定済みとする。 +- sidebar の既定 side、幅、表示 / 非表示 toggle。 +- desktop と mobile で同じ sidebar content をどう再配置するか。ただし Post 一覧 mobile では検索を常時表示し、残りを上部 disclosure へ折り畳む構造は確定済みとする。 +- Post 一覧 mobile の disclosure control の label、icon、開閉 animation、hit area その他の具体表現。 +- Post thumbnail grid の標準寸法、gap、responsive な column 算出式。 +- Post 一覧で pagination / 無限 load のどちらを既定とするか、切替 UI、browser history、scroll restoration、追加 load 中の位置保持。 +- 無限 load で複数 page を同時表示する場合、sidebar の frequent Tag 算出に使う active page を URL 上の page、viewport 内の主 page、初期 page その他のどれで決めるか。ただし複数 page を累積母集団にはしない。 +- compact な visual spacing と十分な touch hit area の具体的基準。 +- Material hierarchy を狭い画面で `vertical-rl` 等の縦書き・右から左の rail として扱う場合、全 locale 共通、locale ごとの default、user setting のどれにするか。 + +## 17.23 Layout customization・workspace + +- sidebar の左右位置、top navigation 順序、panel 順序、table column、grid / list その他を利用者が変更できる範囲。 +- 各設定を device-local / portable のどちらへ保存するか。 +- 自由配置より先に、意味の固定された slot / panel の show / hide / reorder を採用する範囲。 +- Post その他の view を minimise、floating、dock、restore し、別 route を操作しながら再生・参照を継続できる workspace / window-manager 的 UI を初期 V2 に含めるか。 +- workspace state と route state の境界。 +- Post の parent / child edge marker について、動画 embed 等を妨げない標準の太さ・offset・配置、show / hide または縮約 setting を設けるか。将来の自由配置 layout が導入された場合に marker 配置をその system へ統合する範囲。 + +## 17.24 Theme・visual identity + +標準 theme の主要 menu / navigation が『ぼっち・ざ・ろっく!』由来の黄色・赤・ピンク・青を基礎とし、light / dark の標準配色を V1 と同じ semantic role で維持することは確定済みであり、13章に定める。 + +残る論点は user theme による customisation の境界とする。 + +- user theme が主要 navigation の四系統 token をどこまで変更できるか、識別性のため固定または制約する token。 +- Tag category colour と全体 theme colour の優先関係。category ごとの視覚的区別を user theme でも失わせないための制約。 + +## 17.25 V1 の能力・挙動の採否棚卸し + +本節は、V1 に具体的な能力・既定値・表示規則が存在する一方、V2 でそのまま採用するか、改めるか、廃止するかがまだ決まっていない事項の一時的な採否待機所である。 + +本節の「V1」は比較材料であり、V1 source を V2 の規範参照先とはしない。各項目を確定した場合は、採用する完成挙動を本文または対応する未確定事項の節へ自己完結した形で移し、本表へ重複して残さない。 + +V1 と V2 のどちらにも決定的な理由がなく、設計者が採否を決めきれない場合は Danbooru の UI / interaction を比較基準として調査してよい。ただし Danbooru の挙動を参照したという理由だけで規範化せず、採用する挙動そのものと採用理由を本文へ記述する。 + +No.1〜10 は完成仕様として本文へ昇格したため除外した。No.13〜23 は Post 詳細・編輯、広場 sidebar 検索、Tag autocomplete、embed policy の議論を経て、確定事項を13章へ、残件を17.9へ移したため本節から除外する。No.24〜27 は Tag 台帳の完成仕様として13章へ昇格した。No.28 は TagLink の確定 semantics により解決済み、No.46 は event 単位の差戻しへ置換済み、No.47 は 17.8 へ、No.52 は 17.5 へ吸収したため、本節から除外する。後続項目の No. は設計会話・調査記録との対応を保つため再採番しない。 + +### 17.25.1 投稿一覧・thumbnail + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 11 | `thumbnailMode` | `normal / light / off`。`light` は遅延読込み寄り、`off` は thumbnail を描画しない | **維持寄り。** mode 名だけでなく各 mode の意味を確定する必要がある | +| 12 | Tag 一件絞込み時の副 view | 広場と Wiki の tab を表示し、その Tag に対応する Wiki を同画面から確認できる | **概念維持・再構成。** V2 では Tag / Description の resource boundary を保った関聯 view として設計する | + +### 17.25.3 TagLink・Tag の視覚表現 + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 29 | alias hit の表示 | alias で検索 hit した場合に `入力 alias → primary` と表示する | **維持寄り。** 何が一致したかを利用者が理解できるため有用 | +| 30 | TagLink の件数 | Tag 名の右側へ投稿件数を表示する場面がある | **維持候補。** 高密度一覧では有用だが、全 TagLink に常設するかは context ごとに決める | +| 31 | `?` / `!` marker | 関聯 Wiki / Material / ニジラー情報等の有無を小 marker で示す | **意味から再設計。** V2 の Description / Material / ニジラー resource boundary に合わせて marker semantics を定義し直す | +| 32 | 親 Tag の視覚表現 | `↳` と indentation で階層を示す | **維持寄り。** compact な text UI と相性がよい | + +### 17.25.4 Pagination + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 33 | desktop pagination | first / last、current 周辺の page number、必要箇所の `…` を表示 | **維持寄り。** 高密度で現在位置も分かりやすい | +| 34 | mobile pagination | page number 群を省き、`first / previous / next / last` 相当の compact control にする | **維持寄り。** 狭い画面で横幅を消費しない | +| 35 | pagination keyboard | ← / → で前後 page へ移動 | **維持寄り。** ただし text input 中など shortcut 無効条件を共通化する | + +### 17.25.5 Keyboard shortcut・navigation feedback + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 36 | default keyboard shortcuts | `/` search、`?` Help、domain shortcut、←→ pagination 等 | **機構は維持、binding は再確認。** 各 action が V2 で確定してから default key を決める | +| 37 | shortcut customization | enable / disable、binding 変更、競合検出、Help dialog | **維持寄り。** user preference と accessibility の両方で有用 | +| 38 | link prefetch | `intent` 時に hover / touchstart 等で遷移先 data を先行取得 | **維持候補。** 低メモリ端末・低速回線での負荷を測り、既定値を決める | +| 39 | route loading overlay | client navigation の待機中に全面的な `Loading...` overlay を出す場面がある | **変更寄り。** 操作を過度に遮断しない loading pattern を共通化したい | + +### 17.25.6 Sidebar resize + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 40 | sidebar default width | 既定約 256 px、minimum 約 192 px、main area の minimum 等を持つ | **能力は維持、値は再検討。** responsive と user resize を前提に標準幅・制約を決める | +| 41 | sidebar width reset | resize separator の double click で既定幅へ戻す | **維持寄り。** resize 後に容易に復帰できる操作として有用 | +| 42 | touch sidebar resize | separator を一定時間 long press して resize mode に入る | **要再検討。** mobile sidebar 自体の形と browser gesture 競合を踏まえて決める | + +### 17.25.7 Settings interaction + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 43 | settings の未保存変更 | tab 切替・離脱時に未保存変更を検知し、破棄確認を行う | **維持寄り。** 入力喪失を防ぐ基本能力として残したい | +| 44 | settings tab keyboard | Arrow / Home / End 等で tab を移動できる | **維持寄り。** tab component の共通 keyboard contract として扱う候補 | + +### 17.25.8 History・Version UI + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 45 | history diff 表示 | previous 値を打消し線、current 値を併記する等、resource ごとに差分 table を持つ | **文書系中心に再設計。** resource ごとに独自 table を量産せず共通 history pattern を優先する | +| 48 | 古い version の前後 navigation | historical view 上部から古い版 / 新しい版へ移動できる | **維持寄り。** Tag Description / Help その他の文書系 version UI に共通化したい | + +### 17.25.9 Material navigation + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 49 | Material desktop sidebar | Tag hierarchy tree、lazy expansion、selected Tag highlight | **維持寄り。** Material 探索の中心 UI として有力 | +| 50 | Material mobile rail | `vertical-rl` を用い、hierarchy を右から左へ展開する | **維持寄り。** locale と user setting の扱いは別途確定する | +| 51 | Material sidebar filter | 素材 `有 / 無 / 全` 等を sidebar から切り替える | **発想維持寄り。** Material model 確定後に filter semantics を再定義する | + +### 17.25.10 横断 UI・一覧設定 + +| No. | 論点 | V1 の具体挙動 | V2 採否の現時点の第一印象 | +| ---: | --- | --- | --- | +| 53 | shared element / route motion | thumbnail と detail の shared transition、layout motion 等を用いる | **規範化しない寄り。** 低メモリ要件を優先し、装飾 motion は performance / user setting と合わせて判断する | +| 54 | list sort / limit の端末保存 | Post / Tag 等の一覧ごとに limit・sort その他を client settings へ保存 | **維持寄り。** どの設定を page-local に保持するか exact scope を決める | + +## 17.26 Backend・schema・history 実装差分の再監査 + +V1 / V2 移行および実装支援で生じた差分の採否監査は Frontend に限定しない。SYSTEM_SPEC に存在しない resource、column、状態、抽象化を、実装側で自然に見えるという理由だけで完成要件として追加してはならない。 + +現時点で特に再監査する事項は次とする。 + +- Post Version に generic な「公開状態」または `publication` 相当の snapshot を直接持たせる必要性。公開制御は独立した policy resource として定義されているため、Post の version field として重複保持する必要があるかを再検討する。 +- history UI を resource ごとに別々の table / component として作るか、system-wide な version / diff / actor / event / revert 表現を共通化するか。 +- schema / history の実装レビューでは、SYSTEM_SPEC に明示されていない generic field や generic SaaS 的な状態 model が追加されていないかを確認し、必要なら設計論点へ戻す。 + +`events` の column 名は未確定事項ではなく、`event_type`、`created_by_user_id`、`reverts_from` を採用する。実装が `action`、`actor_user_id`、`reverts_event_id` その他の名称を使用していても、それを理由に仕様を変更しない。 + +## 17.27 Gitea 残存課題から抽出した設計論点 + +Gitea の task state・priority・実装進捗は本書の規範情報としない。ただし、残存課題の棚卸しで完成形の採否または仕様詳細が未確定だと判明した事項は、本節へ設計論点として取り込む。Issue 番号は provenance としてのみ記載する。 + +### 17.27.1 ニジラー情報の ownership 詳細(#406 / #322 / #283) + +ニジラー情報を deerjikist Tag に従属させ、独立した version resource を持たず Tag Version に履歴を併合する方針は確定している。 + +残る論点は、複数 platform identity と一つの deerjikist Tag の cardinality、および編輯時に別 Tag の identity を暗黙に移し替えない ownership rule の詳細とする。物理 schema は `tables.zip` を正本とする。 + +### 17.27.2 title / thumbnail 欠損時の警告(#401) + +title または thumbnail の欠損は warning とし、その欠損だけを理由に保存を拒否しないことは確定している。残る論点は次とする。 + +- 自動取得中・取得不能・利用者による意図的な空値をどう区別するか。 +- warning の表示・確認 UX と、その状態を event / history に別情報として残す必要があるか。 + +### 17.27.3 Post Tag suggestion / Tag prediction(#367 / #65) + +- 類似 Post、Tag similarity、媒体 classifier その他のどの signal を candidate source とするか。 +- 外部 application の試行結果を本体の正本 data へ依存させるか、補助 signal としてのみ受けるか。 +- confidence / provenance、候補の保存有無、利用者による accept / reject、既に付与済み・廃止・外部 Tag の扱い。 +- suggestion を確定 Tag membership や無条件の自動付与と同一視しないための boundary。 + +### 17.27.4 Nico 連携入力の Tag autocomplete(#320) + +- Nico 連携 UI のどの入力へ autocomplete を適用するか。 +- 内部 Tag と外部 Nico Tag の候補 source・表示を混同しない構造。 +- system-wide Tag autocomplete と共有する keyboard / search contract の範囲。 + +### 17.27.5 Wildcard 検索(#279) + +- wildcard の grammar、escape、prefix / suffix / infix の許可範囲。 +- Tag Name、alias、Reading、多言語名、その他 resource のどこへ適用するか。 +- case、幅、かな等の search transformation と wildcard 展開の順序。 +- 展開件数上限、query cost、権限別 limit。 + +### 17.27.6 Autocomplete の prefix / suffix ranking(#272) + +- prefix match を優先しつつ suffix / infix match も候補へ含める場合の ranking。 +- primary / alias / Reading / 他 language name の match 種別を ranking にどう組み込むか。 +- 同点時の安定順。 + +### 17.27.7 個人用メモ(#270) + +- 個人用 memo 機能自体を V2 に採用するか。 +- Post / Tag / Material その他、memo を付与できる対象。 +- user-private を絶対条件とするか、共有可能性を持たせるか。 +- search、history、export、account migration、削除時 retention。 + +### 17.27.8 限定公開(#227) + +- 「限定公開」を独立した visibility として採用するか。 +- 同一 URL を登録・耕作しようとした利用者と admin 等だけに閲覧を許可する案の audience semantics。 +- URL 自体を権限のない利用者へ漏らさず、可能なら privileged operator にも可逆な平文として保持しないための identity / lookup 設計。 +- 索引抑止・内部検索抑止・公開抑止・除票その他の publication policy との関係。 + +### 17.27.9 別 Post からの Tag import(#170) + +source Post の Tag を target Post へ import する機能は採用する。残る詳細を次とする。 +- add / replace の別、preview、利用者による選択、parent expansion。 +- 廃止 Tag、外部 Tag、局所記載、system / workflow Tag を import 対象に含めるか。 +- import を一つの event として記録する範囲。 + +### 17.27.10 Embed provider(#152 / #151 / #150 / #149) + +Pixiv、ニジカ投稿局、bilibili、TikTok を含む provider ごとの embed 対応範囲を確定する。 + +- native embed / proxy preview / external link fallback の選択。 +- cookie、tracking、third-party script、CSP、sandbox、autoplay、低メモリ端末への影響。 +- provider が埋込みを拒否・変更した場合の fallback。 + +### 17.27.11 楽曲 Tag の自動付与(#138) + +楽曲 Tag の自動付与機能は採用する。残る詳細を次とする。 +- batch / background job の trigger、判定根拠、confidence、human review の要否。 +- 自動付与の provenance と event、誤付与時の差戻し。 + +### 17.27.12 議論 page(#110) + +- resource ごとの議論 page / thread 機能を設けるか。 +- 御意見板、問い合わせ、申請、Description の履歴・comment その他との境界。 +- 採用する場合も、タグ広場本体を汎用 SNS / 雑談掲示板へ変えないための scope、公開範囲、moderation。 + +### 17.27.13 Description 等への画像添付(#47) + +旧 Wiki に対する画像 upload 要望は、そのまま旧 resource を復活させる根拠としない。Tag Description / Help へ画像・asset を添付できる機能は採用する。残る詳細を次とする。 + +- attachment の対象 resource、inline syntax、alt text。 +- Blob ledger、history snapshot、permission、削除・purge・retention。 +- 外部画像 URL と upload Blob の扱い。 + +### 17.27.14 新規 Post 通知(#36) + +- 新規 Post の notification 機能を採用するか。 +- user subscription、Tag / query filter、delivery channel、既読状態。 +- guest / member の範囲、private / limited content を通知で漏らさない publication check。 +- bulk sync 時の notification storm を防ぐ集約規則。 + +## 17.28 Language / Script / Locale / Flag の残件 + +Language / Script / Locale を stable な参照資源とし、application が対応実装を持つ Language / Script code だけを登録可能とすること、Language だけが nullable な `default_flag_code` を持ち、Flag 自体は独立 resource とすること、user が Language と Flag の対応を自由に override できることは確定済みである。 + +残る論点は次とする。 + +- application が対応する Language / Script code の集合を、enum、定数 + inclusion validation、dedicated registry / policy object その他のどの形で表現するか。predicate 的な利用しやすさと、stable identity を別 identity へ変更する不自然な mutation API を生成しないことの両方を考慮する。V1 の Rails `enum` / whitelist / registry の選択は比較材料にはなるが、SvelteKit + TypeScript である V2 の完成形に Rails 固有 API を規範要件として持ち込まない。 +- Language / Script / Locale の利用者向け表示名をどこで管理するか。DB に multilingual name resource を設ける案と、Frontend i18n resource で UI translation として管理する案を比較する。現時点では Frontend i18n 寄りを有力候補とするが確定しない。 +- 既存設計上の `locales.name` を維持するか、削除するか、locale 自身の self-name として意味を限定するか、compatibility metadata として残すか。表示名管理方針を確定するまで `locales.name` を確定要件としない。 +- Tag Name 編輯その他で Language を省スペースに識別する標準 UI を、Flag、language code badge、Flag + language code、full language name のどれとするか。文字体系の代表文字から Language を推測させる方式は、複数 Language が同じ script を共有し得るため標準としない。自動生成 identicon その他の学習済み記号だけに依存する方式も標準候補としない。 +- user-level Language → Flag override の具体的な保存 schema と settings domain との統合方法。任意の Language と任意の Flag を対応可能とし、候補 Flag を意味論で制限しないという要件は確定済みである。 + +## 17.29 考え方・理由 + +未確定事項を細分化するのは、navigation、route、resource workflow、component、responsive layout 等を一つの「Frontend 詳細設計」にまとめると、決定済み部分と未決定部分の境界が見えなくなり、議論単位としても implementation の既成事実化を防ぐ単位としても大きすぎるためである。 + +V1 の具体挙動を採否監査表として残すのは、既に利用可能な能力を無意識に落とすことと、V1 の偶然の実装詳細を無批判に V2 へ固定することの両方を避けるためである。V1 / Danbooru は比較材料として利用できるが、確定要件は必ず本書だけで理解できる表現へ変換する。 + +一度確定した事項は本文へ昇格し、未確定事項へ重複して残さない。逆に、存在する画面や code を理由に未確定事項を確定扱いしない。 + +未確定事項には、完成形へ影響するが現時点で設計判断が残っている論点だけを置く。system-wide な思想は共通原則、確定した操作・表示は本文、実装順序・進捗は Gitea に置き、役割を混在させない。 + +Gitea の残存課題を本章へ丸ごと複写しないのは、bug 修正・index 追加・運用件数削減その他の task state を完成仕様へ固定しないためである。一方、課題の背後に機能採否・data ownership・privacy・UI contract の未決定がある場合は、その設計論点だけを抽出して本章へ残す。 + +# 付録A. 変更履歴 + +| 日付 | 変更 | +| --- | --- | +| 2026-10-30 | 集計表(Counts)を標準機能へ昇格。`latest` / `history` と `summary` / `details`、クロス集計・Tag 行軸の加算合計、全件件数公開と非公開軸の秘匿、条件・locale identity、明細・内表・CSV / JSON export、非同期 download 開始確認、上限・エラー・受入条件を独立章に定義。公開抑止の数値集計限定例外と Frontend route / API contract を整合。共通 Job の物理方式は未確定のまま維持。 | +| 2026-10-01 | Language / Script / Locale を stable identity を持つ参照資源として明文化し、code 変更・物理削除ではなく deprecation で lifecycle を扱う方針を確定。Language / Script は application が対応実装を持つ code だけを登録可能とした。active Locale 群が表す distinct Language ごとに各 Tag が primary Tag Name を常時ちょうど一件持つ invariant を明確化し、自動生成 contract が target Locale・Tag・source Tag Name を受け取ることを追加。独立 `flags` resource と Language の `default_flag_code` を追加し、Locale / Script には Flag を持たせず、user-level Flag override は自由対応を要件としつつ schema を未確定とした。Language / Script / Locale の表示名 storage と `locales.name`、allowed-code registry の実装方式、Tag Name 編輯時の Language 識別 UI を未確定事項へ追加。 | +| 2026-09-11 | settings domain は設計レビュー未実施であることを明確化し、AI により先行作成されていた `settings` / `user_theme_slots`、portable/client settings の具体項目、JSON/JSONB `tokens`、settings API / UI route 等を確定仕様から撤回して 16.3 の未確定事項へ移動。account-portable / device-local / ephemeral UI state / system-wide configuration の ownership 分類から再設計する方針とした。物理 schema 未確定のため `tables.zip` から `settings` / `user_theme_slots` の table 定義を一旦除外。 | +| 2026-09-10 | 物理 table 定義の正本を `tables.zip` へ分離し、SYSTEM_SPEC から具体的な column / 型 / constraint / index の重複記述を除去。Help は stable identifier を article identity とし、Help Content / Tag Description は owner + language の natural identity、各 version は owner + language + version number で識別し、翻訳 provenance は source language + source version number のみを保持して同一 owner の source revision だけを参照する方式を確定。ニジラー情報は独立 history を持たず Tag Version へ併合。Post の title / thumbnail 欠損を warning とし、別 Post からの Tag import、楽曲 Tag 自動付与、Tag Description / Help asset を採用。拡張天保暦の「天保」romanisation を `tenpou` と確定。Legal は Help と別の第一級 resource とし、source-controlled static document を基本に、過去版閲覧と source version 付き翻訳 provenance を持つ方針へ変更。 | +| 2026-09-08 | DB 標準を PostgreSQL 18 系へ変更し、UTF-8 + built-in `PG_UNICODE_FAST` を database default として確定。MySQL 固有の型・長さ・generated-column trick を引き継がず、PostgreSQL の型・constraint・identity を利用する物理設計原則へ変更。個別 table の具体的 schema は `tables.zip` を正本とする。 | +| 2026-09-08 | Post の元作成日時を PostgreSQL Range に統合し、局所記載も millisecond Range として一 section 一 row で保持し、同一 Post・Tag の overlap を DB で禁止する方針へ変更。局所記載 Tag の通常 sidebar 表示、再生位置による active / inactive 表現を確定し、video duration / dimensions を同期用 metadata として基本データの直接編輯対象から除外。動画長との section 整合は application invariant とした。 | +| 2026-09-08 | 共通 Translation は Translation 全体の単一 version sequence を維持しつつ、Note と Version の不要な surrogate ID を廃止し、自然な複合 identity を採用。Note ごとに参照原文 snapshot を追跡して language 単位に stale 判定する方式を確定。Help は language ごとの Help content を版管理単位とする方針を維持し、Help identity / translation provenance の物理構造と Legal resource boundary は再検討事項へ戻した。 | +| 2026-09-08 | `date_conversions` / `calendar_code` による未使用の汎用暦 abstraction を撤回し、初期 V2 は拡張天保暦専用 conversion cache とした。cache table の英語 identifier は Segsyoxafu による「天保」の正確な romanisation 確認後に確定する。Tag の合併・分離を正式な event 単位の domain operation として追加し、`tag_implications` の変更は既存 Post の current tag set を遡及変更せず、以後の Tag membership 反映操作で再帰展開することを明文化。 | +| 2026-09-04 | Post URL を通常編輯から分離し、同一 content の移転だけを admin 受理の「転居届」で変更する仕様を確定。admin 自身も転居届をセルフ受理する。Post の generic な備考 / `remarks` を不採用とし、対応済み embed は常時自動読込み、利用者設定は autoplay(既定 off)のみに整理。Tag 照会・編輯を同一 `/:locale/tags/:id` route の page-local state とし、current locale primary、他言語の折り畳み、`auto_generated = true` = 「未公証」、`(未公証)` 表示、「公証」checkbox による `auto_generated = false` 化を確定。Tag 台帳は投稿1件以上を既定 filter、投稿件数降順、20件既定・50/100切替等の V1 能力を具体仕様化し、未公証 filter を追加、Tag 本体・上位 Tag とも照会画面へ遷移する。TagLink の遷移統一と台帳 hover の多言語補助表示は将来検討として残した。主要 navigation は『ぼっち・ざ・ろっく!』由来の黄・赤・ピンク・青と V1 の light/dark 配色を標準とし、共通 UI は style class の増殖より Svelte component 化を優先する原則を明文化 | +| 2026-09-04 | オリジナルの作成日時を半開区間と時刻既知状態で表す仕様を確定し、不詳日時・単一入力・範囲入力・inclusive な利用者向け終端表示と共通日時入力 component を本文へ昇格。第一段 navigation を「広場」「タグ」「一角」「素材」「文書」「翻訳」「申請」「ユーザー」「抑止」の順で確定し、抑止は admin のみ、ニジラーと文字は第一段から除外、文字は管理者設定へ配置。`/help` の日本語名を「文書」とし Help のみを扱うこと、御意見板を申請 domain 内へ置くこと、第一段の利用者並べ替えを将来拡張候補に留めることを反映 | +| 2026-09-03 | 投稿一覧 sidebar / Random / mobile sidebar / page size・無限 load / 既定時系列 / thumbnail grid / parent-child marker の No.1〜10 を確定本文へ昇格。visible な Tag category 表示を colour のみに整理し、Random を server-side navigation route として確定。Gitea 残存課題を棚卸しして未捕捉の設計論点を 16.27 へ追加。日本語表記は《当用漢字への書きかえ》に原則従わず `編輯` / `関聯` を正本とする規則を追加し、本文表記を是正。未確定へ戻した Post `remarks` / generic publication の確定本文残骸を除去 | +| 2026-09-02 | V1 の具体的な能力・既定値・UI 挙動 54 項目を V2 採否監査として未確定事項へ追加。V1 / Danbooru は未確定事項で比較材料として参照できるが、確定本文では自己完結した完成仕様へ書き換える原則を明文化。Backend / schema / history も採否監査対象とし、未定義の `remarks`・Post Version の generic な公開状態を確定要件から外して再検討へ移動。event の操作種別・actor・差戻し元の命名を整理。 | +| 2026-09-02 | Frontend / 操作設計の棚卸しを実施。巨大な未確定事項を navigation、`/more`、quick search、locale routing、URL state、Post、Tag、申請、御意見板、問い合わせ、User / ニジラー、抑止、共通 component、sidebar、layout、theme 等へ細分化。Tag Name 専用 route と Post edit 専用 route を削除し、Tag category の日本語表示名、category colour 必須、共通 `TagLink`、TagLink の投稿絞込み遷移、長名 marquee、Tag 名称編輯、sidebar resize、第二段 quick search、御意見板の非 SNS 方針を本文へ反映。併せて「考え方・理由」を個別仕様の因果説明として記述する原則と、本書単体で完結する参照禁止原則を強化 | +| 2026-08-07 | 文書名を「システム仕様書」とし、`SYSTEM_SPEC.md` 一冊を正本とする構造へ再編 | +| 2026-08-07 | 資材一覧、本番件数、実装優先順位、具体的テスト手法、Codex 作業分解、現行実装メモを仕様本文から除外 | +| 2026-08-07 | 質問票を廃止し、未決定の設計だけを「未確定事項」へ整理 | +| 2026-08-07 | 上映会とグカネータを正式な個別章から外し、機能として採用すること自体を未確定とした | +| 2026-08-07 | 汎用 Wiki をタグ説明・ヘルプへ再定義 | +| 2026-08-07 | 投稿履歴、タグ物理削除、新規投稿フローその他の確定済み設計を統合 | +| 2026-08-12 | `audit_logs` を `events` へ統合し、event を利用者レベルの論理操作、各 immutable version と event の関係を自己完結スナップショットと操作を結ぶものとして定義。 | +| 2026-08-12 | event 内の変更を順序なしの集合とし、event 全体・一部の差戻しを新しい event として記録する規則を追加 | +| 2026-08-20 | Rails API + React/Vite の分離構成を SvelteKit + TypeScript + Drizzle + MySQL の full-stack 構成へ更新し、初回 SSR・以後 client navigation を基本とした | +| 2026-08-20 | Tag を language-independent な実体とし、language ごとの primary / alias、自動生成名、Reading を中心とする multilingual Tag Name 設計へ更新。 | +| 2026-08-20 | DB collation へ人間的な同定を委ねず、binary 保存、formatting、identity、allowed scripts、system-wide search rules を分離する原則を追加 | +| 2026-08-20 | Nico tag を内部 Tag category から分離して専用外部タグ台帳とする仕様を確定 | +| 2026-08-20 | Tag Description を Tag × language、Help を日本語 canonical source + revision-linked translations とする多言語仕様へ更新 | +| 2026-08-20 | URL display locale と編輯対象 language / pronunciation language を分離し、top-bar locale switcher、Tag 名称管理、category icon、custom context menu 等の UI 方針を追加 | +| 2026-08-20 | Post Version の Tag snapshot を、Tag identity・当時の Tag version・局所 section を中心とする軽量参照へ更新。 | +| 2026-08-22 | #1〜#23 の設計判断を反映し、presentation script 衝突を登録エラー化、`script_code` 必須化、text rule、Reading、localisation、動画 subtype、素材 guest 更新、暦、一角、外部 translation、上映会・グカネータ外部化を確定仕様へ昇格 | +| 2026-08-22 | Help / Tag Description の current + immutable version 構造、法的文書、共通 translation の採用方針を反映し、旧未確定事項を現時点の実残件へ整理 | +| 2026-08-28 | V2 の恒常認証を cookie-based session とし、recovery code を復旧専用 credential へ分離。`hub.nizika.monster` から `btr.moe` への旧 account 移行を、旧 origin での credential 検証と短寿命・一回限りの migration ticket による migration gate として定義 | +| 2026-08-28 | Active Storage を廃止する共通 Blob 台帳を正式仕様化。SHA-256 による system-wide deduplication、canonical object key、server-side MIME 判定、R2 を含む object storage への統合、content purge の4分類・human-only 操作・terminal state・再投入禁止を確定し、Blob 設計を未確定事項から除外 | +| 2026-08-30 | Tag category を一 Tag 一 category の粗い semantic class として明文化。旧 Wiki の Tag Group を `tag_groups` とする Tag 配下の第二級 structured resource へ昇格し、Tag / Tag Group membership と `tag_implications` を分離 | +| 2026-08-30 | Material を原則 `material` category Tag に所属させ、非-material Tag は implication descendant の Material を代表素材として参照する構造へ整理。包摂素材、媒体種別の metadata 判定、素材集相当の選択 download・folder export・AviUtl project generation を確定し、Material Tag の文脈・命名の残件を未確定事項へ縮約 | +| 2026-08-30 | `bot操作` を未確認の機械影響が残る状態として明文化。similarity の reference corpus から `bot操作` Post を除外し、Post similarity の external/workflow Tag 分離、Tag similarity の external ↔ internal 候補発見利用を確定。投稿自動分類・Tag 補完の詳細を未確定事項へ追加 | +| 2026-08-30 | 一角の resource、series / collection、public / private、編輯権限、Post membership と順序を正式確定し、locale sorting の既定を日本語 Reading と CLDR / ICU locale collation へ確定。 | +| 2026-08-30 | 日本の太陰太陽暦を「拡張天保暦」として正式化。DE440、国立天文台準拠の朔・二十四節気、日本標準時、天保暦優先 + 不定時の時憲暦 fallback、2033年問題の国立天文台案1、永続 conversion cache と将来約10年の background 補完を確定。 | +| 2026-08-30 | 投稿自動分類を Material detection 主軸へ更新。既知 Material の画像・音声・動画 fingerprint / feature 検出、material Tag 付与、`tag_implications` 展開、局所記載候補を正式方針とし、similarity を補助 retrieval signal へ位置付け | +| 2026-08-30 | Post の元作成日時における不詳日時について、source civil date と time zone / precision の扱いを独立した未確定事項として追加。 | +| 2026-08-31 | システムタグを通常 Tag のまま stable role で識別する mapping を正式確定。role は schema-defined な安定 identifier とし、表示名への system 依存を廃止。 | +| 2026-08-31 | 共通 Translation の resource model を正式確定。language 単位の current Note、Translation 全体の version、原文と全 Note の immutable snapshot、原 resource からの任意接続、source text 比較による stale 判定を採用し fingerprint 依存を廃止。 | +| 2026-08-31 | script 情報を language identity ではなく保存文字列の表記 script を示す補助情報として整理。中国語は `Hant` を preferred storage script とする方針を追加。 | +| 2026-08-31 | 画像・動画・音声の文字抽出 / transcription を共通 translation から分離し、媒体 anchor・OCR / ASR provenance その他を独立した未確定事項へ移動 | +| 2026-08-31 | Tag Group の resource model を正式確定。Tag / nested Tag Group membership を分離し、任意順序、cycle prohibition、共通 Translation 接続、member identity と対象 version を含む immutable snapshot、旧 Wiki からの best-effort + human review migration を確定して未確定事項から除外。 | +| 2026-08-31 | immutable history は原則として一つの `*_versions` レコードで当該 resource 自身の当時状態を理解・復元できる自己完結 snapshot とする原則を明文化し、他 resource version 参照を歴史的関係・provenance・navigation のための補助に限定 | +| 2026-09-01 | Frontend / routing / navigation の再検討を未確定事項へ追加。旧 system や Danbooru を知らなければ読めない『現行踏襲』表現を規範本文から除去し、route state、top navigation、Post 編輯導線、Tag 名称管理、申請・御意見板、User / ニジラー関聯、共通 UI pattern、layout customization 等を今後の確定対象として整理 | +| 2026-09-01 | Material domain を再整理。Material を無名の具体 resource、Material Tag / Tag Name を素材概念の名称とし、同一 Tag の複数 Material を許可する一方で旧包摂素材・Material 親子・同一 Tag 内 representative を廃止。file / URL-only の排他、物理削除、site-wide canonical export path、Material Source / Sync Source、Google Drive folder の日次同期、source 消失非追従、同期抑止、同期元 path と canonical path の分離を確定。不要な中間 resource / legacy source kind を初期 V2 から除外し、background job 管理を未確定事項へ独立。 | +| 2026-09-01 | 全章・主要 resource に「考え方・理由」を追加し、rationale を規範仕様の解釈を拘束する情報として定義。DB 命名規則として内部 `id` と外部 `*_code` の分離、surrogate ID の非自動付与、British English、boolean / audit column の非テンプレート化、JSON 型 suffix の禁止、typed relation 優先等を明文化 | +| 2026-09-02 | Frontend navigation を確定。Top navigation は二段を基本・必要時のみ第三段、第二段は domain navigation と contextual action を左右分離、`/more` は navigation index 兼 desktop overlay / overflow 入口、admin 時のみ「文字」を第一段表示、抑止は underlying policy を分離したまま横断 UI domain とする。 | diff --git a/docs/counts-exports.md b/docs/counts-exports.md index 3d00934..e02a8b7 100644 --- a/docs/counts-exports.md +++ b/docs/counts-exports.md @@ -158,7 +158,15 @@ private ID の正誤に応じて許可可否を変えないため、条件の一 非公開列を含む列順変更は API の column_values_editable でも不可と通知する。 公開列の値を使うソートは引き続き利用できる。 明細には元の認可を適用し、抽出条件自体や取得済みの全言語値を表示設定で改変しない。 -外部タグ履歴の linked_tags は名前しか記録していないため、非公開タグの記録名を含む -スナップショットを保守的に非公開とする。名前全体を空白区切りの境界で照合する。 -区切りが曖昧な旧データでは過剰な秘匿の可能性はあるが、参照 ID がない旧形式から -名前の公開可否を楽観的に推定しない。 +外部タグ履歴の linked_tag_ids は、連携する内部タグの ID を半角スペース区切りで +記録する。除票履歴のある内部タグの ID を含むスナップショットを非管理者へ公開しない。 +名前や UI ロカールを参照 ID の判定に使わない。 +外部タグ名は履歴に保存せず、external_tags の platform と name を参照する。 + +V1 Counts の公開フィールド名は linked_tags を維持し、DB の linked_tag_ids へ対応付ける。 +明細・フィルタ・集計軸・変更項目・ソート・CSV / JSON 出力で同じ対応付けを使う。 +既定列と既存の表示ラベルも linked_tags のままとし、値は ID のスペース区切りとする。 +linked_tag_ids は物理カラム名であり、Counts の公開フィールドとしては受け付けない。 +旧 URL や保存済み設定のフィールド指定は維持できるが、値にタグ名を指定した検索条件や +ドリル条件は ID 値への見直しが必要となる。旧履歴の名称は移行後の ID 列から復元できず、 +現在の名称からの推定変換も行わない。