fix(docs-i18n): protect split product links

This commit is contained in:
Peter Steinberger
2026-07-15 13:50:06 -04:00
parent 506bb14d22
commit 2d7be9e84f
4 changed files with 73 additions and 7 deletions
+6 -4
View File
@@ -115,10 +115,12 @@ Add one `operator_approvals` table to the shared state database.
Required indexes:
- unique `(resolution_ref)`; inserts also reject cross-column `approval_id`/`resolution_ref` ambiguity
- `(status, expires_at_ms)`
- `(source_session_key, created_at_ms DESC)`
- `(resolved_at_ms)` for retention pruning
| Index | Purpose |
| ------------------------------------------ | --------------------------------------------------------------------------- |
| unique `(resolution_ref)` | Reject cross-column `approval_id`/`resolution_ref` ambiguity during insert. |
| `(status, expires_at_ms)` | Find pending approvals and reconcile authoritative deadlines. |
| `(source_session_key, created_at_ms DESC)` | Replay recent approvals for one source session. |
| `(resolved_at_ms)` | Prune retained terminal approvals according to the fixed retention policy. |
Audience arrays are small and bounded. Session-filtered replay first selects visible pending rows through Kysely, then decodes and filters the bounded audience arrays in application code; it does not use string matching or raw SQL JSON queries.
+36 -2
View File
@@ -783,6 +783,40 @@ func TestMaskMarkdownDocSyntaxPreservesCanonicalNestedBackticks(t *testing.T) {
}
}
func TestMaskMarkdownDocSyntaxProtectsProductLinkLabelsInsideRawHTML(t *testing.T) {
t.Parallel()
source := strings.Join([]string{
`<div className="maturity-category-docs">`,
"",
"Use `channel links`: [Discord](/channels/discord), [Render](https://render.com/docs), [Groups](/channels/groups).",
"[Render](/guides/pre-render) the page first.",
"",
`</div>`,
"",
}, "\n")
state := NewPlaceholderState(source)
placeholders := []string{}
mapping := map[string]string{}
masked := maskMarkdownDocSyntax(source, state.Next, &placeholders, mapping)
if strings.Contains(masked, "[Discord]") {
t.Fatalf("expected protected link label %q to be masked:\n%s", "Discord", masked)
}
if strings.Contains(masked, "[Render](https://render.com/docs)") {
t.Fatalf("expected contextual product link label %q to be masked:\n%s", "Render", masked)
}
if !strings.Contains(masked, "[Groups]") {
t.Fatalf("expected ordinary link label to remain translatable:\n%s", masked)
}
if !strings.Contains(masked, "[Render](/guides/pre-render)") {
t.Fatalf("expected contextual ordinary-word label to remain translatable:\n%s", masked)
}
if restored := unmaskMarkdown(masked, placeholders, mapping); restored != source {
t.Fatalf("protected link-label round trip changed source:\n%s\nwant:\n%s", restored, source)
}
}
func TestValidateDocBodyRejectsTranslatedInlineCode(t *testing.T) {
t.Parallel()
@@ -2384,8 +2418,8 @@ func TestProcessFileDocUsesFieldLevelFrontmatterTranslation(t *testing.T) {
if !strings.Contains(text, "在 Fly.io 上部署 OpenClaw") {
t.Fatalf("expected translated read_when entry in output:\n%s", text)
}
if !strings.Contains(text, "prompt_version: 29") {
t.Fatalf("expected prompt version 29 in output metadata:\n%s", text)
if !strings.Contains(text, "prompt_version: 30") {
t.Fatalf("expected prompt version 30 in output metadata:\n%s", text)
}
}
+30
View File
@@ -11,6 +11,7 @@ var (
inlineCodeRe = regexp.MustCompile("`[^`]+`")
angleLinkRe = regexp.MustCompile(`<https?://[^>]+>`)
linkURLRe = regexp.MustCompile(`\[[^\]]*\]\(([^)]+)\)`)
linkLabelRe = regexp.MustCompile(`!?\[([^\]\r\n]+)\]\(([^)\r\n]+)\)`)
placeholderRe = regexp.MustCompile(`__OC_I18N_\d+__`)
listMarkerRe = regexp.MustCompile(`^([ \t]*(?:>[ \t]*)*)([-+*]|[0-9]+[.)])([ \t]+)`)
// Hard validation stays limited to low-ambiguity composite literals. Plain numbers remain
@@ -83,6 +84,7 @@ func maskMarkdownDocSyntax(text string, nextPlaceholder func() string, placehold
inlineRanges = append(inlineRanges, span)
}
}
inlineRanges = append(inlineRanges, protectedMarkdownLinkLabelRanges(text)...)
masked := maskByteRanges(text, inlineRanges, nextPlaceholder, placeholders, mapping)
listRanges := make([][2]int, 0)
@@ -116,6 +118,34 @@ func maskMarkdownDocSyntax(text string, nextPlaceholder func() string, placehold
return maskByteRanges(masked, listRanges, nextPlaceholder, placeholders, mapping)
}
func protectedMarkdownLinkLabelRanges(text string) [][2]int {
ranges := make([][2]int, 0)
for _, match := range linkLabelRe.FindAllStringSubmatchIndex(text, -1) {
if len(match) < 6 {
continue
}
label := text[match[2]:match[3]]
destination := markdownInlineLinkDestination(text[match[4]:match[5]])
if isProtectedProductLinkLabel(label, destination) {
ranges = append(ranges, [2]int{match[2], match[3]})
}
}
return ranges
}
func markdownInlineLinkDestination(value string) string {
value = strings.TrimSpace(value)
if strings.HasPrefix(value, "<") {
if end := strings.IndexByte(value, '>'); end > 0 {
return value[1:end]
}
}
if fields := strings.Fields(value); len(fields) > 0 {
return fields[0]
}
return value
}
func extractNumericValues(text string) []string {
protocolRanges := make([][2]int, 0)
for _, span := range placeholderRe.FindAllStringIndex(text, -1) {
+1 -1
View File
@@ -12,7 +12,7 @@ import (
const (
workflowVersion = 16
promptVersion = 29
promptVersion = 30
docsI18nEngineName = "codex"
envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER"
envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"