feat(compliance): add content compliance detection across tenant, ops, and clients

Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web
content-safety pages, admin-web editor/publish gate integration, desktop publish
block surfacing, and supporting migrations / shared types / config plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 20:48:14 +08:00
parent 81577b6154
commit 745cdd79cf
73 changed files with 12747 additions and 1892 deletions
@@ -2,7 +2,9 @@ package repository
import (
"context"
"strings"
"github.com/geo-platform/tenant-api/internal/shared/contentstats"
"github.com/geo-platform/tenant-api/internal/tenant/repository/generated"
"github.com/jackc/pgx/v5"
)
@@ -69,9 +71,10 @@ func (r *articleRepository) CreateArticleVersion(ctx context.Context, input Crea
html_content,
markdown_content,
word_count,
source_label
source_label,
plaintext_snapshot
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
RETURNING id
`,
input.ArticleID,
@@ -81,6 +84,7 @@ func (r *articleRepository) CreateArticleVersion(ctx context.Context, input Crea
pgText(storedMarkdown),
input.WordCount,
pgText(&input.SourceLabel),
strings.TrimSpace(input.Title+"\n"+ExtractArticlePlaintext(input.MarkdownContent)),
).Scan(&versionID)
if err != nil {
return 0, err
@@ -88,6 +92,60 @@ func (r *articleRepository) CreateArticleVersion(ctx context.Context, input Crea
return versionID, nil
}
func ExtractArticlePlaintext(markdown string) string {
markdown = strings.TrimSpace(markdown)
if markdown == "" {
return ""
}
var builder strings.Builder
builder.Grow(len(markdown))
inFence := false
lines := strings.Split(markdown, "\n")
for _, line := range lines {
trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "```") || strings.HasPrefix(trimmed, "~~~") {
inFence = !inFence
continue
}
if inFence {
builder.WriteString(trimmed)
builder.WriteByte('\n')
continue
}
line = stripMarkdownLine(line)
if strings.TrimSpace(line) == "" {
continue
}
builder.WriteString(line)
builder.WriteByte('\n')
}
return strings.TrimSpace(builder.String())
}
func stripMarkdownLine(line string) string {
replacer := strings.NewReplacer(
"#", " ",
"*", " ",
"_", " ",
"`", " ",
">", " ",
"[", " ",
"]", " ",
"(", " ",
")", " ",
"!", " ",
"|", " ",
)
line = replacer.Replace(line)
line = strings.TrimLeft(line, "-+0123456789. \t")
return strings.Join(strings.Fields(line), " ")
}
func CountArticlePlaintextWords(markdown string) int {
return contentstats.CountWords(ExtractArticlePlaintext(markdown))
}
func (r *articleRepository) UpdateArticleCurrentVersion(ctx context.Context, articleID, tenantID, versionID int64) error {
tag, err := r.db.Exec(ctx, `
UPDATE articles