fix(slugs): CJK slug support in SlugRegistry and dream-cycle summary slug (takeover of #782, #738) (#3083)

Master already widened slugifySegment (sync.ts) and validatePageSlug
(operations.ts) to CJK in v0.32.7, but the other two validators #782
targeted stayed ASCII-only: SlugRegistry's SLUG_RE rejected any CJK
desiredSlug from BrainWriter, and synthesize.ts's SUMMARY_SLUG_RE (whose
comment claimed it was kept in sync with validatePageSlug) rejected CJK
output roots.

Hoist the segment grammar into cjk.ts as PAGE_SLUG_SEG and compose all
three regex sites from it, so the four slug validators share one grammar.
Each site keeps its own shape (SlugRegistry's >=2-segment dir/name form,
validatePageSlug's case-insensitive flag).

Scope stays CJK (matching v0.32.7), not full \p{L} Unicode as #782
proposed — all-scripts slugs (lookalike/RTL spoofing) is a maintainer
policy call.

Co-authored-by: Sinabina <sinabina@Sinabinas-MacBook-Pro-4.local>
Co-authored-by: tamagodo-fu <tamagodo-fu@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Time Attakc
2026-07-23 12:13:50 -07:00
committed by GitHub
co-authored by Sinabina tamagodo-fu Claude Fable 5
parent 7421efc41e
commit b139602119
6 changed files with 33 additions and 5 deletions
+8
View File
@@ -20,6 +20,14 @@ export const CJK_SLUG_CHARS = '一-鿿぀-ゟ゠-ヿ가-힯';
export const CJK_RANGES_REGEX = new RegExp(`[${CJK_SLUG_CHARS}]`);
/**
* Page-slug segment grammar (no anchors): alnum-or-CJK lead char, then
* alnum/CJK/hyphen continuation. Single source for validatePageSlug
* (operations.ts), SlugRegistry's SLUG_RE, and the dream-cycle
* SUMMARY_SLUG_RE so every slug validator shares one grammar (#738).
*/
export const PAGE_SLUG_SEG = `[a-z0-9${CJK_SLUG_CHARS}][a-z0-9${CJK_SLUG_CHARS}\\-]*`;
export const CJK_SENTENCE_DELIMITERS = ['。', '', '']; // 。!?
export const CJK_CLAUSE_DELIMITERS = ['', '', '', '、']; // ;:,、
+3 -2
View File
@@ -43,10 +43,11 @@ import { serializeMarkdown, serializePageToMarkdown } from '../markdown.ts';
import type { Page, PageType } from '../types.ts';
import { validateSourceId } from '../utils.ts';
import { safeSplitIndex } from '../text-safe.ts';
import { PAGE_SLUG_SEG } from '../cjk.ts';
// Slug regex from validatePageSlug — kept in sync.
// Slug grammar from validatePageSlug — shared via PAGE_SLUG_SEG (#738).
// Used for the orchestrator-written summary index slug.
const SUMMARY_SLUG_RE = /^[a-z0-9][a-z0-9\-]*(\/[a-z0-9][a-z0-9\-]*)*$/;
const SUMMARY_SLUG_RE = new RegExp(`^${PAGE_SLUG_SEG}(\\/${PAGE_SLUG_SEG})*$`);
// ── Model context budget (D1, D5, D7, D9) ─────────────────────────────
+1 -2
View File
@@ -25,7 +25,7 @@ import { bumpLastRetrievedAt } from './last-retrieved.ts';
import { isSearchMode } from './search/mode.ts';
import { stampEvidence } from './search/evidence.ts';
import type { SearchResult } from './types.ts';
import { CJK_SLUG_CHARS } from './cjk.ts';
import { CJK_SLUG_CHARS, PAGE_SLUG_SEG } from './cjk.ts';
import * as db from './db.ts';
import { VERSION } from '../version.ts';
import {
@@ -162,7 +162,6 @@ export function validatePageSlug(slug: string): void {
}
// v0.32.7: CJK ranges (Han / Hiragana / Katakana / Hangul Syllables) allowed
// in segments. ASCII shape rules (lead char, hyphen continuation) preserved.
const PAGE_SLUG_SEG = `[a-z0-9${CJK_SLUG_CHARS}][a-z0-9${CJK_SLUG_CHARS}\\-]*`;
if (!new RegExp(`^${PAGE_SLUG_SEG}(\\/${PAGE_SLUG_SEG})*$`, 'i').test(slug)) {
throw new OperationError('invalid_params', `Invalid page_slug: ${slug} (allowed: alphanumeric, CJK, hyphens, forward-slash separated segments)`);
}
+4 -1
View File
@@ -17,6 +17,7 @@
import type { BrainEngine } from '../engine.ts';
import type { PageType } from '../types.ts';
import { PAGE_SLUG_SEG } from '../cjk.ts';
export interface CreateSlugInput {
/**
@@ -71,7 +72,9 @@ export class SlugRegistryError extends Error {
// SlugRegistry
// ---------------------------------------------------------------------------
const SLUG_RE = /^[a-z0-9][a-z0-9\-]*(\/[a-z0-9][a-z0-9\-]*)+$/;
// Shares the page-slug segment grammar (incl. CJK ranges, #738) with
// validatePageSlug; keeps this site's dir/name shape (>= 2 segments).
const SLUG_RE = new RegExp(`^${PAGE_SLUG_SEG}(\\/${PAGE_SLUG_SEG})+$`);
export class SlugRegistry {
constructor(private engine: BrainEngine) {}
+6
View File
@@ -86,6 +86,12 @@ describe('#2415: loadOutputRoot validation + patterns gather scope', () => {
expect(await loadOutputRoot(engine)).toBe('wiki');
});
test('CJK root passes the slug grammar (#738)', async () => {
await engine.setConfig('dream.synthesize.output_root', '知识/笔记');
expect(await loadOutputRoot(engine)).toBe('知识/笔记');
await engine.setConfig('dream.synthesize.output_root', '');
});
test('patterns phase gathers reflections under the configured root', async () => {
await engine.setConfig('dream.synthesize.output_root', 'notes');
for (let i = 0; i < 3; i++) {
+11
View File
@@ -203,6 +203,17 @@ describe('SlugRegistry', () => {
})).rejects.toThrow(SlugRegistryError);
});
test('create accepts CJK slugs (#738)', async () => {
const reg = new SlugRegistry(engine);
const r = await reg.create({
desiredSlug: '知识/品牌圣经',
displayName: '品牌圣经',
type: 'note',
});
expect(r.slug).toBe('知识/品牌圣经');
expect(r.exact).toBe(true);
});
test('create throws on invalid slug', async () => {
const reg = new SlugRegistry(engine);
await expect(reg.create({