feat(schema): Phase 1 schema templates + Nexus DevOps runbook
## AI Schema Assist Phase 1 (steps 1-2)
Approach A core (templates only, no LLM) — admin начинает создание нового
справочника с curated skeleton вместо пустого Monaco. Approach C (per-field
LLM suggest) — Phase 2 на этом фундаменте.
### Backend
- `templates/*.template.json` в ordinis-cuod-bundle resources — 6 шаблонов
(blank/spacecraft/ground-station/antenna/frequency-band/operator)
- `SchemaTemplateRegistry` (rest-api) — classpath scan at startup, strip
x-template-* metadata, expose canonical JSON Schema
- `SchemaTemplatesController` — GET /api/v1/dictionaries/templates (list)
+ /{id} (detail с schemaJson)
- Cross-bundle support: scanner matches classpath*:templates/*.template.json
— другие bundles могут shippит свои templates через те же conventions
### Frontend
- types: SchemaTemplateSummary + SchemaTemplateDetail
- queries: schemaTemplatesQuery + lazy detail query
- TemplatePicker.tsx — grid of cards (icon + name + description + tags),
click → fetch detail → fire onPick(detail)
- Integrated в DictionaryEditorDialog (create mode, properties.length===0
visible — после первой property picker исчезает чтобы не перезатереть)
## Nexus DevOps handoff
`docs-internal/ops/nexus-alpine-mirror.md` — runbook про APKINDEX vs file
inventory drift на v3.23 mirror. Includes:
- Verification commands
- 3 fix options (invalidate cache / scheduled refresh / self-host)
- Prometheus alert proposal
- Current workaround (node:22-alpine3.20 pin) reference
Active issue paired с CI saga !227/!229/!230 — нужен DevOps action.
This commit is contained in:
+143
@@ -0,0 +1,143 @@
|
||||
package cloud.nstart.terravault.ordinis.restapi.service.template;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* AI Schema Assist — Phase 1 step 1: curated schema templates registry.
|
||||
*
|
||||
* <p>Scans classpath {@code templates/*.template.json} at startup. Каждый файл —
|
||||
* полная JSON Schema + дополнительные {@code x-template-*} metadata header
|
||||
* (id/name/description/icon/tags) для каталога UI.
|
||||
*
|
||||
* <p>Approach A (templates only) core. Pure offline — no LLM dep. Templates
|
||||
* ship as part of {@code ordinis-cuod-bundle}; другие bundles могут добавить
|
||||
* свои templates в собственные {@code resources/templates/} — scanner picks
|
||||
* them all up (classpath wildcard).
|
||||
*
|
||||
* <p>Approach C upgrade path: AI suggest-field будет принимать template'у как
|
||||
* starting context и предлагать additional fields — но это Phase 1 step 4-6,
|
||||
* не сейчас.
|
||||
*
|
||||
* <p>Templates immutable per deploy — reload требует restart. Acceptable
|
||||
* trade-off: templates меняются редко, hot-reload добавляет complexity без
|
||||
* value.
|
||||
*/
|
||||
@Service
|
||||
public class SchemaTemplateRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SchemaTemplateRegistry.class);
|
||||
private static final String TEMPLATES_PATTERN = "classpath*:templates/*.template.json";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
private final ResourcePatternResolver resourceResolver;
|
||||
private final Map<String, SchemaTemplate> templatesById = new LinkedHashMap<>();
|
||||
|
||||
public SchemaTemplateRegistry(ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
this.resourceResolver = new PathMatchingResourcePatternResolver();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
void loadTemplates() {
|
||||
try {
|
||||
Resource[] resources = resourceResolver.getResources(TEMPLATES_PATTERN);
|
||||
for (Resource resource : resources) {
|
||||
try (var is = resource.getInputStream()) {
|
||||
JsonNode root = mapper.readTree(is);
|
||||
SchemaTemplate tpl = parseTemplate(root, resource.getFilename());
|
||||
if (tpl != null) {
|
||||
templatesById.put(tpl.id(), tpl);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Skipping malformed template {}: {}", resource.getFilename(), e.getMessage());
|
||||
}
|
||||
}
|
||||
log.info("Loaded {} schema templates: {}", templatesById.size(), templatesById.keySet());
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to scan templates classpath", e);
|
||||
}
|
||||
}
|
||||
|
||||
private SchemaTemplate parseTemplate(JsonNode root, String fileName) {
|
||||
String id = textOrNull(root, "x-template-id");
|
||||
if (id == null || id.isBlank()) {
|
||||
log.warn("Template {} missing x-template-id — skipping", fileName);
|
||||
return null;
|
||||
}
|
||||
String name = Optional.ofNullable(textOrNull(root, "x-template-name")).orElse(id);
|
||||
String description = Optional.ofNullable(textOrNull(root, "x-template-description")).orElse("");
|
||||
String icon = textOrNull(root, "x-template-icon");
|
||||
|
||||
List<String> tags = new ArrayList<>();
|
||||
JsonNode tagsNode = root.get("x-template-tags");
|
||||
if (tagsNode != null && tagsNode.isArray()) {
|
||||
tagsNode.forEach(n -> { if (n.isTextual()) tags.add(n.asText()); });
|
||||
}
|
||||
|
||||
// Stripped schema = template без x-template-* metadata (clean JSON Schema
|
||||
// готова к загрузке в Monaco / dictionary creation). Keep $schema, type,
|
||||
// required, properties, x-id-source — это canonical schema content.
|
||||
var stripped = root.deepCopy();
|
||||
if (stripped.isObject()) {
|
||||
var obj = (com.fasterxml.jackson.databind.node.ObjectNode) stripped;
|
||||
obj.remove("x-template-id");
|
||||
obj.remove("x-template-name");
|
||||
obj.remove("x-template-description");
|
||||
obj.remove("x-template-icon");
|
||||
obj.remove("x-template-tags");
|
||||
obj.remove("$comment");
|
||||
}
|
||||
|
||||
return new SchemaTemplate(
|
||||
id, name, description, icon,
|
||||
Collections.unmodifiableList(tags),
|
||||
stripped);
|
||||
}
|
||||
|
||||
private static String textOrNull(JsonNode root, String field) {
|
||||
JsonNode n = root.get(field);
|
||||
return (n != null && n.isTextual()) ? n.asText() : null;
|
||||
}
|
||||
|
||||
public List<SchemaTemplate> all() {
|
||||
return new ArrayList<>(templatesById.values());
|
||||
}
|
||||
|
||||
public Optional<SchemaTemplate> byId(String id) {
|
||||
return Optional.ofNullable(templatesById.get(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema template metadata + cleaned schema JSON.
|
||||
*
|
||||
* @param id unique template identifier (matches filename prefix usually)
|
||||
* @param name human-readable name (RU/EN agnostic — admin sees it)
|
||||
* @param description short blurb что внутри + что admin должен дополнить
|
||||
* @param icon optional phosphor-icons icon name (e.g. "SatelliteIcon")
|
||||
* @param tags tags для filtering в UI (e.g. ["space", "cuod"])
|
||||
* @param schemaJson canonical JSON Schema (без x-template-* metadata)
|
||||
*/
|
||||
public record SchemaTemplate(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
List<String> tags,
|
||||
JsonNode schemaJson) {}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package cloud.nstart.terravault.ordinis.restapi.web;
|
||||
|
||||
import cloud.nstart.terravault.ordinis.restapi.service.template.SchemaTemplateRegistry;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI Schema Assist Phase 1 — templates listing endpoint.
|
||||
*
|
||||
* <p>{@code GET /api/v1/dictionaries/templates} — каталог доступных шаблонов
|
||||
* без полной schema (lightweight для grid rendering).
|
||||
*
|
||||
* <p>{@code GET /api/v1/dictionaries/templates/{id}} — полная JSON Schema
|
||||
* для выбранного шаблона (lazy load когда admin кликнул).
|
||||
*
|
||||
* <p>Public endpoint (auth optional) — templates сами по себе не sensitive,
|
||||
* это shared catalog для admins. RBAC будет gate'нуть actual dict creation
|
||||
* (downstream), а здесь только preview.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dictionaries/templates")
|
||||
public class SchemaTemplatesController {
|
||||
|
||||
private final SchemaTemplateRegistry registry;
|
||||
|
||||
public SchemaTemplatesController(SchemaTemplateRegistry registry) {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
/** Catalog — без schemaJson (экономит ~3KB на шаблон в list view). */
|
||||
@GetMapping
|
||||
public List<TemplateSummary> list() {
|
||||
return registry.all().stream()
|
||||
.map(t -> new TemplateSummary(t.id(), t.name(), t.description(), t.icon(), t.tags()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
/** Detail — full JSON Schema готовая к загрузке в Monaco. */
|
||||
@GetMapping("/{id}")
|
||||
public TemplateDetail get(@PathVariable String id) {
|
||||
return registry.byId(id)
|
||||
.map(t -> new TemplateDetail(
|
||||
t.id(), t.name(), t.description(), t.icon(), t.tags(), t.schemaJson()))
|
||||
.orElseThrow(() -> new ResponseStatusException(
|
||||
HttpStatus.NOT_FOUND, "Template not found: " + id));
|
||||
}
|
||||
|
||||
public record TemplateSummary(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
List<String> tags) {}
|
||||
|
||||
public record TemplateDetail(
|
||||
String id,
|
||||
String name,
|
||||
String description,
|
||||
String icon,
|
||||
List<String> tags,
|
||||
JsonNode schemaJson) {}
|
||||
}
|
||||
Reference in New Issue
Block a user