Add full shell, replaceable Omics adapter and bilingual interaction

Implement approved specification #32 and tickets #33-#37. Keep host authentication server-verified and pin session interaction language. Compile scoped base selectors for browser compatibility and retain full gutters during CSS pruning.
This commit is contained in:
Codex
2026-09-13 14:26:39 +02:00
parent 2d1b714ebe
commit d8a29bfbdd
207 changed files with 8570 additions and 2164 deletions
+11 -1
View File
@@ -40,6 +40,7 @@ type descriptor struct {
WorkspaceRepository workspaceRepositoryDescriptor `yaml:"workspaceRepository"`
Authentication authenticationDescriptor `yaml:"authentication"`
ModelCatalog ModelCatalog `yaml:"modelCatalog"`
Shell Shell `yaml:"shell"`
Overrides []string `yaml:"overrides"`
}
@@ -91,6 +92,7 @@ type Installation struct {
WorkspaceRepository WorkspaceRepository
Authentication Authentication
ModelCatalog ModelCatalog
Shell Shell
Overrides []string
}
@@ -128,6 +130,9 @@ func Load(path string) (Installation, error) {
if err := raw.ModelCatalog.NormalizeDefaults(); err != nil {
return Installation{}, err
}
if err := raw.Shell.NormalizeDefaults(); err != nil {
return Installation{}, err
}
if raw.SchemaVersion != 2 {
return Installation{}, errors.New("migration_required: installation schemaVersion must be 2")
@@ -176,6 +181,7 @@ func Load(path string) (Installation, error) {
},
Authentication: authentication,
ModelCatalog: raw.ModelCatalog,
Shell: raw.Shell,
Overrides: make([]string, 0, len(raw.Overrides)),
}
values, err := installation.environmentValues()
@@ -435,7 +441,7 @@ func (i Installation) ComposeFiles() []string {
return files
}
// GeneratedDirectory contains disposable runtime adapters derived from modelCatalog.
// GeneratedDirectory contains disposable model and public frontend runtime projections.
func (i Installation) GeneratedDirectory() string {
return filepath.Join(filepath.Dir(i.Path), "generated")
}
@@ -452,6 +458,10 @@ func (i Installation) GeneratedPiSettingsPath() string {
return filepath.Join(i.GeneratedDirectory(), "pi", "settings.json")
}
func (i Installation) GeneratedFrontendConfigPath() string {
return filepath.Join(i.GeneratedDirectory(), "frontend", "config.js")
}
func (i Installation) ModelProjectionComposePath() string {
return filepath.Join(i.GeneratedDirectory(), "compose.models.yaml")
}
@@ -0,0 +1,113 @@
package config
import (
"os"
"strings"
"testing"
)
func TestLoadDefaultsLegacyInstallationToEmbeddedShell(t *testing.T) {
for _, profile := range []string{"local", "server"} {
t.Run(profile, func(t *testing.T) {
path, _, _, _ := writeInstallation(t, profile)
installation, err := Load(path)
if err != nil {
t.Fatal(err)
}
if got, want := installation.Shell, (Shell{Mode: "embedded", DefaultLocale: "en", Adapter: "omics-portal"}); got != want {
t.Fatalf("Shell = %#v, want %#v", got, want)
}
})
}
}
func TestLoadResolvesExplicitShell(t *testing.T) {
for _, test := range []struct {
name, yaml string
want Shell
}{
{"empty mapping", "shell: {}\n", Shell{Mode: "embedded", DefaultLocale: "en", Adapter: "omics-portal"}},
{"embedded Italian", "shell:\n mode: embedded\n defaultLocale: it\n", Shell{Mode: "embedded", DefaultLocale: "it", Adapter: "omics-portal"}},
{"full defaults", "shell:\n mode: full\n", Shell{Mode: "full", DefaultLocale: "en"}},
{"full ignores known adapter", "shell:\n mode: full\n defaultLocale: it\n adapter: omics-portal\n", Shell{Mode: "full", DefaultLocale: "it"}},
} {
t.Run(test.name, func(t *testing.T) {
path, _, _, _ := writeInstallation(t, "local")
appendShellDescriptor(t, path, test.yaml)
installation, err := Load(path)
if err != nil {
t.Fatal(err)
}
if installation.Shell != test.want {
t.Fatalf("Shell = %#v, want %#v", installation.Shell, test.want)
}
})
}
}
func TestLoadRejectsInvalidShell(t *testing.T) {
for _, test := range []struct{ name, yaml, errorPart string }{
{"unknown mode", "shell:\n mode: fullscreen\n", "shell.mode"},
{"mode whitespace", "shell:\n mode: ' full '\n", "shell.mode"},
{"malformed locale separator", "shell:\n defaultLocale: en_US\n", "shell.defaultLocale"},
{"malformed locale", "shell:\n defaultLocale: 'en;alert(1)'\n", "shell.defaultLocale"},
{"unknown adapter", "shell:\n adapter: other-portal\n", "shell.adapter"},
{"unknown adapter in full", "shell:\n mode: full\n adapter: other-portal\n", "shell.adapter"},
{"wrong shape", "shell: []\n", "cannot unmarshal"},
{"wrong field type", "shell:\n mode: [full]\n", "cannot unmarshal"},
{"unknown field", "shell:\n theme: light\n", "field theme not found"},
} {
t.Run(test.name, func(t *testing.T) {
path, _, _, _ := writeInstallation(t, "local")
appendShellDescriptor(t, path, test.yaml)
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), test.errorPart) {
t.Fatalf("Load() error = %v, want %q", err, test.errorPart)
}
})
}
}
func TestLoadPreservesWellFormedShellLocalesWithoutCatalogLookup(t *testing.T) {
for _, locale := range []string{
"fr", "de-DE", "en-US", "sr-Latn-RS", "es-419", "zh-cmn-Hans-CN",
"sl-rozaj-biske", "de-CH-1901", "en-US-u-ca-gregory", "en-a-foo-b-bar-x-private",
"x-clinical", "en-x-a-a", "qaa-Qaaa-QM", "zzzz", "abcde", "EN-us",
"en-GB-oed", "i-klingon", "sgn-BE-FR", "zh-min-nan",
} {
t.Run(locale, func(t *testing.T) {
path, _, _, _ := writeInstallation(t, "local")
appendShellDescriptor(t, path, "shell:\n mode: full\n defaultLocale: "+locale+"\n")
installation, err := Load(path)
if err != nil || installation.Shell.DefaultLocale != locale {
t.Fatalf("Load() locale = %q, %v; want preserved %q", installation.Shell.DefaultLocale, err, locale)
}
})
}
}
func TestLoadRejectsMalformedShellLocales(t *testing.T) {
for _, locale := range []string{
"en_US", "en--US", "-en", "en-", "e", "123", "en-1234a5678", "en-12",
"en-US-Latn", "en-u", "en-x", "x", "a-foo", "i-unknown", "en-äbc", "en-Kaaa",
"en US", " en", "en;alert(1)", "sl-rozaj-ROZAJ", "en-a-foo-A-bar",
} {
t.Run(locale, func(t *testing.T) {
path, _, _, _ := writeInstallation(t, "local")
appendShellDescriptor(t, path, "shell:\n defaultLocale: '"+locale+"'\n")
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "shell.defaultLocale") {
t.Fatalf("Load() error = %v; want invalid locale %q rejected", err, locale)
}
})
}
}
func appendShellDescriptor(t *testing.T, path, shell string) {
t.Helper()
contents, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(contents, []byte(shell)...), 0o600); err != nil {
t.Fatal(err)
}
}
+90
View File
@@ -0,0 +1,90 @@
package config
import (
"errors"
"regexp"
"strings"
)
// Shell is the public presentation configuration, independent of deployment topology and auth.
type Shell struct {
Mode string `yaml:"mode" json:"mode"`
DefaultLocale string `yaml:"defaultLocale" json:"defaultLocale"`
Adapter string `yaml:"adapter,omitempty" json:"adapter,omitempty"`
}
// NormalizeDefaults resolves omitted installation settings before projecting them to the browser.
func (s *Shell) NormalizeDefaults() error {
if s.Mode == "" {
s.Mode = "embedded"
}
if s.DefaultLocale == "" {
s.DefaultLocale = "en"
}
if s.Mode != "embedded" && s.Mode != "full" {
return errors.New("shell.mode must be full or embedded")
}
if !wellFormedLocale(s.DefaultLocale) {
return errors.New("shell.defaultLocale must be a well-formed BCP47 language tag")
}
if s.Adapter != "" && s.Adapter != "omics-portal" {
return errors.New("shell.adapter must be omics-portal")
}
if s.Mode == "full" {
s.Adapter = ""
return nil
}
if s.Adapter == "" {
s.Adapter = "omics-portal"
}
return nil
}
// RFC 5646 section 2.1 grammar. Capture variants and extensions to reject duplicates.
// Validate syntax without consulting a language registry or the frontend's available catalogs.
var localePattern = regexp.MustCompile(`^(?:` +
`(?:[a-z]{2,3}(?:-[a-z]{3}){0,3}|[a-z]{4}|[a-z]{5,8})` + // language / extlang
`(?:-[a-z]{4})?(?:-(?:[a-z]{2}|[0-9]{3}))?` + // script / region
`((?:-(?:[a-z0-9]{5,8}|[0-9][a-z0-9]{3}))*)` + // variants
`((?:-[0-9a-wy-z](?:-[a-z0-9]{2,8})+)*)` + // extensions
`(?:-x(?:-[a-z0-9]{1,8})+)?` + // private use after a language
`|x(?:-[a-z0-9]{1,8})+)$`) // private use alone
func wellFormedLocale(locale string) bool {
// BCP47 is ASCII. Check before case folding, which could otherwise map Unicode to ASCII.
for _, character := range locale {
if character > 127 {
return false
}
}
tag := strings.ToLower(locale)
// These grandfathered irregular tags do not match the normal language-tag grammar.
switch tag {
case "en-gb-oed", "i-ami", "i-bnn", "i-default", "i-enochian", "i-hak", "i-klingon",
"i-lux", "i-mingo", "i-navajo", "i-pwn", "i-tao", "i-tay", "i-tsu",
"sgn-be-fr", "sgn-be-nl", "sgn-ch-de":
return true
}
parts := localePattern.FindStringSubmatch(tag)
if parts == nil {
return false
}
variants := make(map[string]bool)
for _, variant := range strings.Split(strings.TrimPrefix(parts[1], "-"), "-") {
if variants[variant] {
return false
}
variants[variant] = true
}
singletons := make(map[string]bool)
for _, subtag := range strings.Split(parts[2], "-") {
if len(subtag) != 1 {
continue
}
if singletons[subtag] {
return false
}
singletons[subtag] = true
}
return true
}