Compare commits

...

3 Commits

Author SHA1 Message Date
Benjamin Admin a064933c1f docs(master-controls): list all 4 seeded mapping tables + sentinel caveat
CI / detect-changes (push) Successful in 18s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 7s
CI / validate-canonical-controls (push) Successful in 15s
CI / loc-budget (push) Successful in 17s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m27s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
The guard probes mc_use_case_mappings as the existence sentinel, but the route
also queries mc_verification, mc_regulations and mc_use_case_sync_state. Document
that they are seeded together and that a half-seeded DB (sentinel present, a
sibling missing) still 500s on the sibling's queries.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 16:10:34 +02:00
Benjamin Admin 3e2bd91209 fix(ci): unblock deploy on main — test-go vet, loc-budget, build-sha
CI / detect-changes (push) Successful in 15s
CI / branch-name (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / build-sha-integrity (push) Successful in 8s
CI / validate-canonical-controls (push) Successful in 13s
CI / loc-budget (push) Successful in 20s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Has been skipped
CI / test-go (push) Successful in 58s
CI / iace-gt-coverage (push) Successful in 26s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-backend (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
test-go (go vet runs as part of go test) failed on two pre-existing iace spots:
- cmd/iace-audit/main.go: 6x fmt.Println with redundant trailing \n
- internal/iace/document_export_sources.go: duplicate `r == ';'` clause

build-sha-integrity failed because the alpine job installs python3 but not
pyyaml, so `import yaml` raised ModuleNotFoundError. Add py3-yaml to apk.

loc-budget flagged iace_handler_init_helpers.go (530 lines, committed state).
The other session already split it to 455 in the working tree (uncommitted);
grandfather it until that split lands, then remove the exception.

Verified locally: go test ./... all ok, go vet clean, check-loc.sh exit 0.

[guardrail-change]

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-10 14:17:27 +02:00
Benjamin_Boenisch bb6139df3e MC mapping: defensive route + MinIO overridable + iace migration 151 (#27)
CI / detect-changes (push) Successful in 18s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 8s
CI / validate-canonical-controls (push) Successful in 15s
CI / loc-budget (push) Failing after 18s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m25s
CI / test-go (push) Failing after 41s
CI / iace-gt-coverage (push) Successful in 26s
CI / test-python-backend (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 21s
MC mapping deploy: defensive route + MinIO overridable + Migration 151 + loc-exception [migration-approved] [guardrail-change]
2026-06-10 11:54:48 +00:00
7 changed files with 143 additions and 74 deletions
+19
View File
@@ -222,3 +222,22 @@ consent-tester/services/banner_text_checker.py
# Demo-Daten und Export. Split nach React-Sub-Components (_components/ # Demo-Daten und Export. Split nach React-Sub-Components (_components/
# RiskClassifier, _components/MitigationForm) ist React-Refactor-Sprint. # RiskClassifier, _components/MitigationForm) ist React-Refactor-Sprint.
admin-compliance/app/sdk/ai-act/page.tsx admin-compliance/app/sdk/ai-act/page.tsx
# --- 2026-06-10 CI-Unblocker: agent doc-check extras ---
# agent_doc_check_extras.py (~535 im CI-Stand): supplementaere Endpoints/Helfer
# der Agent-Dokumentenpruefung, ueber den 500-Cap gewachsen — blockiert seit
# #657 die loc-budget-Pruefung (scannt das ganze Repo, nicht nur Diffs).
# Pre-existing Tech-Debt (nicht aus IACE-Arbeit). Phase-2-Split nach
# Endpoint-/Helfer-Gruppen geplant; bis dahin Exception mit Rationale.
# [guardrail-change]
backend-compliance/compliance/api/agent_doc_check_extras.py
# --- 2026-06-10 CI-Unblocker: IACE handler init helpers ---
# iace_handler_init_helpers.go (530 im CI-Stand): Init-/Wiring-Helfer der
# IACE-Handler, ueber den 500-Cap gewachsen. Die andere Session hat die Datei
# im Working-Tree bereits auf 455 Zeilen gesplittet (uncommittet) — sobald
# dieser Split committet ist, MUSS diese Exception wieder entfernt werden.
# Bis dahin Exception mit Rationale, damit der Deploy nicht an pre-existing
# IACE-Refactor-Zwischenstand scheitert.
# [guardrail-change]
ai-compliance-sdk/internal/api/handlers/iace_handler_init_helpers.go
+1 -1
View File
@@ -422,7 +422,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
run: | run: |
apk add --no-cache git python3 apk add --no-cache git python3 py3-yaml
git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git . git clone --depth 1 --branch ${GITHUB_REF_NAME} ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git .
- name: Validate every Dockerfile + compose block declares BUILD_SHA - name: Validate every Dockerfile + compose block declares BUILD_SHA
run: | run: |
@@ -15,6 +15,28 @@ const pool = new Pool({ connectionString: dbUrl })
let metaCache: { at: number; data: unknown } | null = null let metaCache: { at: number; data: unknown } | null = null
const META_TTL_MS = 120_000 const META_TTL_MS = 120_000
// The use-case mapping tables (mc_use_case_mappings, mc_verification,
// mc_regulations, mc_use_case_sync_state) are seeded together per-environment
// and may not exist yet on a fresh/unseeded DB. We probe mc_use_case_mappings as
// the existence sentinel and guard every mapping query so the route degrades to
// empty filters instead of a 500. Short TTL so it picks up the tables once seeded.
// NB: the sentinel assumes the siblings are seeded together — a half-seeded DB
// (mappings present but e.g. mc_regulations missing) would still 500 on those.
let mappingTablesCache: { at: number; present: boolean } | null = null
async function hasMappingTables(): Promise<boolean> {
if (mappingTablesCache && Date.now() - mappingTablesCache.at < 300_000) {
return mappingTablesCache.present
}
let present = false
try {
const r = await pool.query(
"SELECT to_regclass('compliance.mc_use_case_mappings') IS NOT NULL AS present")
present = !!r.rows[0]?.present
} catch { present = false }
mappingTablesCache = { at: Date.now(), present }
return present
}
type MCListRow = { type MCListRow = {
id: string; control_id: string; title: string; objective: string id: string; control_id: string; title: string; objective: string
severity: string; category: string; total_controls: number severity: string; category: string; total_controls: number
@@ -58,7 +80,7 @@ export async function GET(request: NextRequest) {
// Shared WHERE builder so list + count stay in lock-step (incl. the // Shared WHERE builder so list + count stay in lock-step (incl. the
// use_case / verification_method / source_regulation mapping filters). // use_case / verification_method / source_regulation mapping filters).
function buildControlsWhere(params: URLSearchParams): { where: string; args: unknown[]; idx: number } { function buildControlsWhere(params: URLSearchParams, hasMapping: boolean): { where: string; args: unknown[]; idx: number } {
let where = "WHERE 1=1" let where = "WHERE 1=1"
const args: unknown[] = [] const args: unknown[] = []
let idx = 1 let idx = 1
@@ -82,41 +104,44 @@ function buildControlsWhere(params: URLSearchParams): { where: string; args: unk
idx++ idx++
} }
const useCase = params.get('use_case') || '' // Mapping-based filters only apply when the mapping tables exist (seeded DB).
const primaryOnly = params.get('primary') === '1' if (hasMapping) {
if (useCase) { const useCase = params.get('use_case') || ''
where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m const primaryOnly = params.get('primary') === '1'
WHERE m.master_control_uuid = mc.id AND m.use_case = $${idx}${primaryOnly ? ' AND m.is_primary' : ''})` if (useCase) {
args.push(useCase) where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m
idx++ WHERE m.master_control_uuid = mc.id AND m.use_case = $${idx}${primaryOnly ? ' AND m.is_primary' : ''})`
} args.push(useCase)
idx++
}
const verification = params.get('verification_method') || '' const verification = params.get('verification_method') || ''
if (verification === '__none__') { if (verification === '__none__') {
where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_verification v where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_verification v
WHERE v.master_control_uuid = mc.id)` WHERE v.master_control_uuid = mc.id)`
} else if (verification) { } else if (verification) {
where += ` AND EXISTS (SELECT 1 FROM compliance.mc_verification v where += ` AND EXISTS (SELECT 1 FROM compliance.mc_verification v
WHERE v.master_control_uuid = mc.id AND v.verification_method = $${idx})` WHERE v.master_control_uuid = mc.id AND v.verification_method = $${idx})`
args.push(verification) args.push(verification)
idx++ idx++
} }
const regulation = params.get('source_regulation') || '' const regulation = params.get('source_regulation') || ''
if (regulation) { if (regulation) {
where += ` AND EXISTS (SELECT 1 FROM compliance.mc_regulations r where += ` AND EXISTS (SELECT 1 FROM compliance.mc_regulations r
WHERE r.master_control_uuid = mc.id AND r.source_regulation = $${idx})` WHERE r.master_control_uuid = mc.id AND r.source_regulation = $${idx})`
args.push(regulation) args.push(regulation)
idx++ idx++
} }
const mapped = params.get('mapped') || '' const mapped = params.get('mapped') || ''
if (mapped === 'mapped') { if (mapped === 'mapped') {
where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m where += ` AND EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m
WHERE m.master_control_uuid = mc.id)` WHERE m.master_control_uuid = mc.id)`
} else if (mapped === 'unmapped') { } else if (mapped === 'unmapped') {
where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m where += ` AND NOT EXISTS (SELECT 1 FROM compliance.mc_use_case_mappings m
WHERE m.master_control_uuid = mc.id)` WHERE m.master_control_uuid = mc.id)`
}
} }
// Member-based filter: an MC matches if ANY of its atomic members has the // Member-based filter: an MC matches if ANY of its atomic members has the
@@ -141,12 +166,23 @@ async function handleControls(params: URLSearchParams) {
const sort = params.get('sort') || 'control_id' const sort = params.get('sort') || 'control_id'
const order = params.get('order') === 'desc' ? 'DESC' : 'ASC' const order = params.get('order') === 'desc' ? 'DESC' : 'ASC'
const { where, args, idx } = buildControlsWhere(params) const hasMapping = await hasMappingTables()
const { where, args, idx } = buildControlsWhere(params, hasMapping)
const sortCol = sort === 'control_id' ? 'mc.master_control_id' : const sortCol = sort === 'control_id' ? 'mc.master_control_id' :
sort === 'created_at' ? 'mc.created_at' : sort === 'created_at' ? 'mc.created_at' :
sort === 'source' ? 'mc.canonical_name' : 'mc.master_control_id' sort === 'source' ? 'mc.canonical_name' : 'mc.master_control_id'
const mapCols = hasMapping ? `,
(SELECT v.verification_method FROM compliance.mc_verification v
WHERE v.master_control_uuid = mc.id) as verification_method,
(SELECT array_agg(m.use_case ORDER BY m.is_primary DESC, m.use_case)
FROM compliance.mc_use_case_mappings m
WHERE m.master_control_uuid = mc.id) as use_cases,
(SELECT r.source_regulation FROM compliance.mc_regulations r
WHERE r.master_control_uuid = mc.id AND r.is_primary LIMIT 1) as primary_regulation`
: `, NULL as verification_method, NULL::text[] as use_cases, NULL as primary_regulation`
args.push(limit, offset) args.push(limit, offset)
const res = await pool.query(` const res = await pool.query(`
SELECT mc.master_control_id as control_id, SELECT mc.master_control_id as control_id,
@@ -159,14 +195,7 @@ async function handleControls(params: URLSearchParams) {
mc.total_controls, mc.total_controls,
mc.phases_covered, mc.phases_covered,
mc.id, mc.id,
mc.created_at, mc.created_at${mapCols}
(SELECT v.verification_method FROM compliance.mc_verification v
WHERE v.master_control_uuid = mc.id) as verification_method,
(SELECT array_agg(m.use_case ORDER BY m.is_primary DESC, m.use_case)
FROM compliance.mc_use_case_mappings m
WHERE m.master_control_uuid = mc.id) as use_cases,
(SELECT r.source_regulation FROM compliance.mc_regulations r
WHERE r.master_control_uuid = mc.id AND r.is_primary LIMIT 1) as primary_regulation
FROM compliance.master_controls mc FROM compliance.master_controls mc
${where} ${where}
ORDER BY ${sortCol} ${order} ORDER BY ${sortCol} ${order}
@@ -203,7 +232,8 @@ async function handleControls(params: URLSearchParams) {
} }
async function handleCount(params: URLSearchParams) { async function handleCount(params: URLSearchParams) {
const { where, args } = buildControlsWhere(params) const hasMapping = await hasMappingTables()
const { where, args } = buildControlsWhere(params, hasMapping)
const res = await pool.query( const res = await pool.query(
`SELECT count(*) FROM compliance.master_controls mc ${where}`, args `SELECT count(*) FROM compliance.master_controls mc ${where}`, args
) )
@@ -230,23 +260,26 @@ async function handleMeta(_params: URLSearchParams) {
GROUP BY 1 ORDER BY 2 DESC LIMIT 30 GROUP BY 1 ORDER BY 2 DESC LIMIT 30
`) `)
// Mapping distribution + coverage + member-derived category facet. Only // category facet is member-based (those tables always exist); the mapping
// category is populated on the deduplicated members (evidence_type / // facets only when the mapping tables are present (seeded DB).
// target_audience are NULL there), so it is the one canonical facet we surface. const hasMapping = await hasMappingTables()
const [ucRes, vRes, regRes, mappedRes, catRes] = await Promise.all([ const catRes = await pool.query(`SELECT cc.category v, count(DISTINCT mcm.master_control_uuid) c
pool.query(`SELECT use_case, count(DISTINCT master_control_uuid) c
FROM compliance.mc_use_case_mappings GROUP BY 1 ORDER BY 2 DESC`),
pool.query(`SELECT verification_method, count(*) c
FROM compliance.mc_verification GROUP BY 1 ORDER BY 2 DESC`),
pool.query(`SELECT source_regulation, count(DISTINCT master_control_uuid) c
FROM compliance.mc_regulations GROUP BY 1 ORDER BY 2 DESC LIMIT 200`),
pool.query(`SELECT count(DISTINCT master_control_uuid) c
FROM compliance.mc_use_case_mappings`),
pool.query(`SELECT cc.category v, count(DISTINCT mcm.master_control_uuid) c
FROM compliance.master_control_members mcm FROM compliance.master_control_members mcm
JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid JOIN compliance.canonical_controls cc ON cc.id = mcm.control_uuid
WHERE cc.category IS NOT NULL GROUP BY 1 ORDER BY 2 DESC`), WHERE cc.category IS NOT NULL GROUP BY 1 ORDER BY 2 DESC`)
]) const emptyRows = { rows: [] as Array<Record<string, string>> }
const [ucRes, vRes, regRes, mappedRes] = hasMapping
? await Promise.all([
pool.query(`SELECT use_case, count(DISTINCT master_control_uuid) c
FROM compliance.mc_use_case_mappings GROUP BY 1 ORDER BY 2 DESC`),
pool.query(`SELECT verification_method, count(*) c
FROM compliance.mc_verification GROUP BY 1 ORDER BY 2 DESC`),
pool.query(`SELECT source_regulation, count(DISTINCT master_control_uuid) c
FROM compliance.mc_regulations GROUP BY 1 ORDER BY 2 DESC LIMIT 200`),
pool.query(`SELECT count(DISTINCT master_control_uuid) c
FROM compliance.mc_use_case_mappings`),
])
: [emptyRows, emptyRows, emptyRows, { rows: [{ c: '0' }] }]
const facet = (rows: Array<{ v: string; c: string }>) => const facet = (rows: Array<{ v: string; c: string }>) =>
Object.fromEntries(rows.filter(x => x.v).map(x => [x.v, parseInt(x.c)])) Object.fromEntries(rows.filter(x => x.v).map(x => [x.v, parseInt(x.c)]))
@@ -311,8 +344,9 @@ async function handleDetail(params: URLSearchParams) {
LIMIT 100 LIMIT 100
`, [mc.id]) `, [mc.id])
// Use-case / verification / regulation mapping (the "regulation→code" lineage) // Use-case / verification / regulation mapping (only when the tables exist).
const mapRes = await pool.query(` const mapping: Record<string, any> = (await hasMappingTables())
? ((await pool.query(`
SELECT SELECT
(SELECT json_agg(json_build_object('use_case', m.use_case, 'is_primary', m.is_primary) (SELECT json_agg(json_build_object('use_case', m.use_case, 'is_primary', m.is_primary)
ORDER BY m.is_primary DESC, m.use_case) ORDER BY m.is_primary DESC, m.use_case)
@@ -323,8 +357,8 @@ async function handleDetail(params: URLSearchParams) {
'is_primary', r.is_primary, 'member_count', r.member_count) 'is_primary', r.is_primary, 'member_count', r.member_count)
ORDER BY r.is_primary DESC, r.member_count DESC) ORDER BY r.is_primary DESC, r.member_count DESC)
FROM compliance.mc_regulations r WHERE r.master_control_uuid = $1) as regulations FROM compliance.mc_regulations r WHERE r.master_control_uuid = $1) as regulations
`, [mc.id]) `, [mc.id])).rows[0] || {})
const mapping = mapRes.rows[0] || {} : {}
const regs = mapping.regulations || [] const regs = mapping.regulations || []
const primaryReg = regs.find((x: { is_primary: boolean }) => x.is_primary) || regs[0] const primaryReg = regs.find((x: { is_primary: boolean }) => x.is_primary) || regs[0]
+6 -6
View File
@@ -54,11 +54,11 @@ func cmdReachability(_ []string) {
"universe_tags": len(r.UniverseTags), "universe_tags": len(r.UniverseTags),
}) })
if len(r.UnreachablePatterns) > 0 { if len(r.UnreachablePatterns) > 0 {
fmt.Println("\n## Unreachable patterns (top 30 by priority):\n") fmt.Println("\n## Unreachable patterns (top 30 by priority):")
printPatternRows(r.UnreachablePatterns, 30) printPatternRows(r.UnreachablePatterns, 30)
} }
if len(r.WeakPatterns) > 0 { if len(r.WeakPatterns) > 0 {
fmt.Println("\n## Weakly reachable (top 20 by priority):\n") fmt.Println("\n## Weakly reachable (top 20 by priority):")
printPatternRows(r.WeakPatterns, 20) printPatternRows(r.WeakPatterns, 20)
} }
writeJSON("audit-reports/reachability.json", r) writeJSON("audit-reports/reachability.json", r)
@@ -72,7 +72,7 @@ func cmdConsistency(_ []string) {
"incomplete": r.Incomplete, "incomplete": r.Incomplete,
}) })
if len(r.IncompleteComponents) > 0 { if len(r.IncompleteComponents) > 0 {
fmt.Println("\n## Components missing tags for declared hazard categories:\n") fmt.Println("\n## Components missing tags for declared hazard categories:")
for _, c := range r.IncompleteComponents { for _, c := range r.IncompleteComponents {
fmt.Printf("- %s (%s)\n", c.ComponentID, c.NameDE) fmt.Printf("- %s (%s)\n", c.ComponentID, c.NameDE)
for _, miss := range c.MissingForCategories { for _, miss := range c.MissingForCategories {
@@ -99,7 +99,7 @@ func cmdVocabulary(args []string) {
"unknown_with_pattern_hit": len(r.SuggestedDictionaryEntries), "unknown_with_pattern_hit": len(r.SuggestedDictionaryEntries),
}) })
if len(r.SuggestedDictionaryEntries) > 0 { if len(r.SuggestedDictionaryEntries) > 0 {
fmt.Println("\n## Suggested dictionary additions (token appears in pattern scenarios but not in dict):\n") fmt.Println("\n## Suggested dictionary additions (token appears in pattern scenarios but not in dict):")
for _, s := range r.SuggestedDictionaryEntries { for _, s := range r.SuggestedDictionaryEntries {
fmt.Printf("- '%s' → seen in %d patterns. Examples: %s\n", s.Token, len(s.PatternIDs), joinFirst(s.PatternIDs, 5)) fmt.Printf("- '%s' → seen in %d patterns. Examples: %s\n", s.Token, len(s.PatternIDs), joinFirst(s.PatternIDs, 5))
} }
@@ -129,7 +129,7 @@ func cmdEcho(args []string) {
"orphaned": r.Orphaned, "orphaned": r.Orphaned,
}) })
if len(r.OrphanedPhrases) > 0 { if len(r.OrphanedPhrases) > 0 {
fmt.Println("\n## Orphaned phrases (no hazard echoes them):\n") fmt.Println("\n## Orphaned phrases (no hazard echoes them):")
for _, o := range r.OrphanedPhrases { for _, o := range r.OrphanedPhrases {
fmt.Printf("- [%s] %s\n", o.Field, truncate(o.Phrase, 120)) fmt.Printf("- [%s] %s\n", o.Field, truncate(o.Phrase, 120))
} }
@@ -163,7 +163,7 @@ func cmdHierarchy(args []string) {
"missing_info": r.MissingInfo, "missing_info": r.MissingInfo,
}) })
if len(r.IncompleteHazards) > 0 { if len(r.IncompleteHazards) > 0 {
fmt.Println("\n## Hazards with incomplete hierarchy:\n") fmt.Println("\n## Hazards with incomplete hierarchy:")
for _, h := range r.IncompleteHazards { for _, h := range r.IncompleteHazards {
fmt.Printf("- [%s] %s — missing: %s\n", h.Category, truncate(h.Name, 70), joinFirst(h.MissingLevels, 3)) fmt.Printf("- [%s] %s — missing: %s\n", h.Category, truncate(h.Name, 70), joinFirst(h.MissingLevels, 3))
} }
@@ -94,7 +94,7 @@ func extractCitedNorms(hz []Hazard, mt []Mitigation) []string {
seen := make(map[string]bool) seen := make(map[string]bool)
consider := func(s string) { consider := func(s string) {
fields := strings.FieldsFunc(s, func(r rune) bool { fields := strings.FieldsFunc(s, func(r rune) bool {
return r == ' ' || r == ',' || r == ';' || r == '\n' || r == ';' || r == '(' return r == ' ' || r == ',' || r == ';' || r == '\n' || r == '('
}) })
for i := 0; i < len(fields)-1; i++ { for i := 0; i < len(fields)-1; i++ {
head := strings.ToUpper(strings.TrimSpace(fields[i])) head := strings.ToUpper(strings.TrimSpace(fields[i]))
@@ -0,0 +1,16 @@
-- Migration 151: iace_projects.parent_project_id (Variantenmanagement)
-- Die Spalte wurde urspruenglich ad-hoc auf die DBs gebracht (Commit 8682522,
-- 2026-05-09) OHNE Repo-Migration -- dadurch ging sie bei DB-Wechseln/Resets
-- verloren. Diese Migration macht sie reproduzierbar. Code erwartet
-- *uuid.UUID (nullable). Strikt add-only. [migration-approved]
SET search_path TO compliance, public;
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema='compliance' AND table_name='iace_projects') THEN
ALTER TABLE iace_projects
ADD COLUMN IF NOT EXISTS parent_project_id UUID;
END IF;
END $$;
+4 -4
View File
@@ -204,10 +204,10 @@ services:
expose: expose:
- "8095" - "8095"
environment: environment:
MINIO_ENDPOINT: nbg1.your-objectstorage.com MINIO_ENDPOINT: ${MINIO_ENDPOINT:-nbg1.your-objectstorage.com}
MINIO_ACCESS_KEY: T18RGFVXXG2ZHQ5404TP MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-T18RGFVXXG2ZHQ5404TP}
MINIO_SECRET_KEY: KOUU4WO6wh07cQjNgh0IZHkeKQrVfBz6hnIGpNss MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-KOUU4WO6wh07cQjNgh0IZHkeKQrVfBz6hnIGpNss}
MINIO_SECURE: "true" MINIO_SECURE: ${MINIO_SECURE:-true}
PIPER_MODEL_PATH: /app/models/de_DE-thorsten-high.onnx PIPER_MODEL_PATH: /app/models/de_DE-thorsten-high.onnx
depends_on: depends_on:
core-health-check: core-health-check: