Harden repo identity speech names

This commit is contained in:
Meta Vens 2026-06-11 09:06:04 +01:00
commit 4e091a6f45
3 changed files with 84 additions and 4 deletions

View file

@ -1300,7 +1300,11 @@ async function postRepoIdentityIntent(job: JobRecord, intent: RepoIdentityPostIn
if (isNonPublicRepoIdentitySpeech(rawContent)) {
throw new Error(`Repo identity ${identity.id} SAY content is not public speech.`);
}
const content = normalizePublicRepoIdentitySpeech(rawContent);
const content = normalizePublicRepoIdentitySpeech(rawContent, {
identityId: identity.id,
displayName: identity.displayName,
repoName: identity.repoName,
});
if (isNonPublicRepoIdentitySpeech(content)) {
throw new Error(`Repo identity ${identity.id} SAY content is not public speech.`);
}

View file

@ -7,6 +7,12 @@ export interface RepoIdentityPostIntent {
replyToMessageId?: string;
}
export interface PublicRepoIdentitySpeechNormalizationOptions {
identityId?: string;
displayName?: string;
repoName?: string;
}
type RepoFaceActionBlock = {
kind: "say" | "state_note" | "article" | "reddit_thread" | "bifrost_topic" | "update_request";
fields: Record<string, string>;
@ -91,11 +97,17 @@ export function isNonPublicRepoIdentitySpeech(value: string): boolean {
].includes(normalized);
}
export function normalizePublicRepoIdentitySpeech(value: string): string {
return value
export function normalizePublicRepoIdentitySpeech(
value: string,
options: PublicRepoIdentitySpeechNormalizationOptions = {},
): string {
return stripLeadingSelfLabel(
value
.replace(/```[A-Za-z0-9_-]*\s*\r?\n([\s\S]*?)\r?\n```/g, (_match, body: string) => body.trim())
.replace(/`([^`\r\n]+)`/g, (_match, body: string) => `"${body.trim()}"`)
.trim();
.trim(),
options,
);
}
function stripSingleMarkdownFence(value: string): string {
@ -204,3 +216,55 @@ function optionalDslString(value: string | undefined): string | undefined {
function requiredDslString(value: string | undefined): string | undefined {
return optionalDslString(value);
}
function stripLeadingSelfLabel(
value: string,
options: PublicRepoIdentitySpeechNormalizationOptions,
): string {
const aliases = [
options.displayName,
options.identityId,
options.repoName,
]
.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
.map((entry) => normalizeSpeakerKey(entry));
if (aliases.length === 0) {
return value;
}
const match = value.match(/^([^\r\n::-]{1,80})(\s*[::]\s*|\s+-\s+)([\s\S]*)$/);
if (!match) {
return value;
}
const candidate = normalizeSpeakerKey(
match[1].replace(/^["'“”‘’`]+|["'“”‘’`]+$/g, "").trim(),
);
if (!candidate || !aliases.includes(candidate)) {
return value;
}
return match[3].trimStart();
}
function normalizeSpeakerKey(value: string): string {
return foldKana(
value
.normalize("NFKC")
.toLowerCase()
.replace(/\s+/g, "")
.replace(/["'“”‘’`・.]+/g, ""),
);
}
function foldKana(value: string): string {
let output = "";
for (const char of value) {
const code = char.charCodeAt(0);
output += code >= 0x30a1 && code <= 0x30f6
? String.fromCharCode(code - 0x60)
: char;
}
return output;
}

View file

@ -72,6 +72,18 @@ const cases: Case[] = [
"show \"rear pair drifting 38 ms\", \"geometry confidence low\", \"world model provisional\".",
detail: "Repo Faces can mention technical labels without speaking in code spans.",
},
{
name: "leading self-label is stripped with kana-folding",
run: () => normalizePublicRepoIdentitySpeech(
"メタめ: We need one real owner for the public name.",
{
identityId: "metame",
displayName: "メタメ",
repoName: "メタメ",
},
) === "We need one real owner for the public name.",
detail: "The public speech boundary owns the speaker label, so mixed-script self-renames in SAY content must be stripped before posting.",
},
];
const failures = cases.filter((entry) => !entry.run());