mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Protect structural spans during style conversion
Keep arbitrary text links, email addresses, and code spans usable while preserving the deterministic middle-korean-style-v1 prose transform.\n\nConstraint: PR #281 round 3 requested URL/email/code-like span protection after broad global replacement probes rewrote structural tokens.\nRejected: Narrowing all lexicon and particle rules with word-boundary heuristics | would change established v1 creative broad-replacement behavior beyond the reviewed issue.\nConfidence: high\nScope-risk: narrow\nDirective: Protect new structural span classes before broad replacements and add regression tests before extending the protected surface.\nTested: node --test scripts/test_korean_middle_korean.js; node --check korean-middle-korean/scripts/korean_middle_korean.js; node --check scripts/korean_middle_korean.js; CLI URL/email/code/Markdown probes; installed-skill smoke via ~/.agents/skills/korean-middle-korean/scripts/korean_middle_korean.js; npm run lint; npm run typecheck; root/workspace test chain without pip bootstrap; npm run pack:dry-run; post-deslop npm run lint && npm run typecheck && node --test scripts/test_korean_middle_korean.js && npm run pack:dry-run\nNot-tested: direct npm run test remains blocked before repo tests by local Homebrew Python 3.14 pyexpat/libexpat import error.
This commit is contained in:
parent
ab04911f2f
commit
76995658e8
4 changed files with 59 additions and 2 deletions
|
|
@ -7,6 +7,7 @@
|
|||
- `했다`, `하는`, `말하는` 같은 일부 어미를 `ᄒᆞ엿다〮`, `ᄒᆞᄂᆞᆫ`, `ᄆᆞᆯᄒᆞᄂᆞᆫ`처럼 변환
|
||||
- 날짜 단위를 `年`, `月`, `日`로 변환
|
||||
- 일부 한자어를 `熱愛說`, `俳優`, `學校`처럼 Hanja 힌트로 변환
|
||||
- URL, 이메일, Markdown 링크, inline/fenced code span은 구조 토큰으로 보고 변환하지 않음
|
||||
- 인명·숫자·고유명사는 완전 보존이 아니라, 규칙이 맞지 않을 때 원문을 남기는 best-effort 방식으로 처리
|
||||
|
||||
## 왜 별도 스킬이 필요한가
|
||||
|
|
@ -25,6 +26,7 @@ LLM에게 "중세 국어처럼"이라고만 요청하면 변환 강도와 표기
|
|||
- 그다음 결정론적 lexicon 치환을 적용한다.
|
||||
- 일부 현대 조사를 중세국어풍 조사로 바꾼다.
|
||||
- 일부 현대 어미를 `ᄒᆞ-` 계열 중세국어풍 어미로 바꾼다.
|
||||
- URL, 이메일, Markdown 링크, inline/fenced code span은 먼저 보호한 뒤 마지막에 원문 그대로 복원한다.
|
||||
- 한자어 힌트는 넓은 전역 치환으로 적용되므로 합성어·고유명사처럼 보이는 문자열 안에서도 바뀔 수 있다.
|
||||
- 변환하지 못한 내용은 원문 의미 보존을 위해 그대로 둔다.
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ metadata:
|
|||
- `했다`, `하는`, `말하는` 같은 현대 어미를 `ᄒᆞ엿다〮`, `ᄒᆞᄂᆞᆫ`, `ᄆᆞᆯᄒᆞᄂᆞᆫ`처럼 바꾼다.
|
||||
- 날짜 단위는 `年`, `月`, `日`로 바꾼다.
|
||||
- 일부 한자어/밈 예시는 `熱愛說`, `俳優`, `學校`처럼 Hanja 힌트를 섞는다.
|
||||
- URL, 이메일, Markdown 링크, inline/fenced code span은 구조 토큰으로 보고 변환하지 않는다.
|
||||
- 인명·숫자·고유명사는 완전 보존이 아니라, 규칙이 맞지 않을 때 원문을 남기는 best-effort 방식으로 처리한다.
|
||||
- 한자어 힌트는 넓은 전역 치환으로 적용되므로 합성어·고유명사처럼 보이는 문자열 안에서도 바뀔 수 있다.
|
||||
|
||||
|
|
@ -46,6 +47,7 @@ metadata:
|
|||
|
||||
1. 변환할 한국어 텍스트를 받는다.
|
||||
2. 설치된 `korean-middle-korean` skill 디렉터리를 기준으로 `node scripts/korean_middle_korean.js` 를 실행한다.
|
||||
- URL, 이메일, Markdown 링크, inline/fenced code span은 보호되어 원문 그대로 복원된다.
|
||||
3. 기본 JSON 출력에서 `output`을 사용자에게 반환한다.
|
||||
4. 사용자가 근거를 원하면 `replacements` 배열의 규칙 적용 내역을 요약한다.
|
||||
5. 학술적 정확성이 필요하다고 보이면 이 스킬은 창작용 스타일 변환임을 먼저 밝힌다.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ const fs = require("node:fs");
|
|||
|
||||
const PROFILE = "middle-korean-style-v1";
|
||||
const CONTRACT =
|
||||
"Deterministic Korean Middle Korean-style rewrite: public-domain orthographic flavor rules, fixed broad lexicon replacements, archaic particles/endings, Sino-Korean Hanja hints, and best-effort preservation for names/numbers when no rule matches.";
|
||||
"Deterministic Korean Middle Korean-style rewrite: public-domain orthographic flavor rules, fixed broad lexicon replacements, archaic particles/endings, Sino-Korean Hanja hints, protected URL/email/Markdown-code spans, and best-effort preservation for names/numbers when no rule matches.";
|
||||
|
||||
const LEXICON = [
|
||||
["야 이", "이"],
|
||||
|
|
@ -53,6 +53,35 @@ function replaceRegex(text, pattern, to, replacements, kind, label) {
|
|||
return next;
|
||||
}
|
||||
|
||||
function protectSpans(input) {
|
||||
const protectedSpans = [];
|
||||
let text = input;
|
||||
|
||||
function protect(pattern) {
|
||||
text = text.replace(pattern, (match) => {
|
||||
const token = `\uE000${protectedSpans.length}\uE001`;
|
||||
protectedSpans.push(match);
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
protect(/```[\s\S]*?```/g);
|
||||
protect(/`[^`\n]*`/g);
|
||||
protect(/\[[^\]\n]*\]\([^\s)]+(?:\s+"[^"]*")?\)/g);
|
||||
protect(/\bhttps?:\/\/[A-Za-z0-9._~:/?#[\]@!$&'()*+,;=%-]+/g);
|
||||
protect(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g);
|
||||
|
||||
return { text, protectedSpans };
|
||||
}
|
||||
|
||||
function restoreSpans(text, protectedSpans) {
|
||||
let output = text;
|
||||
protectedSpans.forEach((span, index) => {
|
||||
output = output.replaceAll(`\uE000${index}\uE001`, span);
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
function convertToMiddleKoreanStyle(input) {
|
||||
return createReport(input).output;
|
||||
}
|
||||
|
|
@ -63,7 +92,8 @@ function createReport(input) {
|
|||
}
|
||||
|
||||
const replacements = [];
|
||||
let output = input;
|
||||
const protectedInput = protectSpans(input);
|
||||
let output = protectedInput.text;
|
||||
|
||||
output = replaceRegex(output, /(\d+)년/g, "$1年", replacements, "date", "년→年");
|
||||
output = replaceRegex(output, /(\d+)월/g, "$1月", replacements, "date", "월→月");
|
||||
|
|
@ -86,6 +116,7 @@ function createReport(input) {
|
|||
output = replaceRegex(output, /([가-힣ᄀ-ᇿA-Za-z0-9一-龥]+)와(?=\s|[",.?!]|$)/g, "$1와", replacements, "particle", "와 보존");
|
||||
|
||||
output = output.replace(/\s+([,.;:?!])/g, "$1");
|
||||
output = restoreSpans(output, protectedInput.protectedSpans);
|
||||
|
||||
return {
|
||||
profile: PROFILE,
|
||||
|
|
|
|||
|
|
@ -44,12 +44,34 @@ test("documentation and skill describe proper-noun preservation as best effort",
|
|||
|
||||
assert.match(docs, /인명·숫자·고유명사는 완전 보존이 아니라/i);
|
||||
assert.match(docs, /넓은 전역 치환/i);
|
||||
assert.match(docs, /URL, 이메일, Markdown 링크, inline\/fenced code span은 구조 토큰/i);
|
||||
assert.match(skill, /인명·숫자·고유명사는 완전 보존이 아니라/i);
|
||||
assert.match(skill, /넓은 전역 치환/i);
|
||||
assert.match(skill, /URL, 이메일, Markdown 링크, inline\/fenced code span은 구조 토큰/i);
|
||||
|
||||
assert.match(convertToMiddleKoreanStyle("배우자는 학교에서 일했다."), /俳優자/);
|
||||
});
|
||||
|
||||
|
||||
test("converter preserves URLs, emails, Markdown links, and code spans unchanged", () => {
|
||||
const input = [
|
||||
"https://example.com에서 확인했다.",
|
||||
"contact@example.com은 말했다.",
|
||||
"[학교에서 보기](https://example.com/학교에서)은 유지했다.",
|
||||
"`학교에서` 테스트했다.",
|
||||
"```\n학교에서 공부했다.\n```\n밖에서 공부했다.",
|
||||
].join("\n");
|
||||
|
||||
const output = convertToMiddleKoreanStyle(input);
|
||||
|
||||
assert.match(output, /https:\/\/example[.]com에서 확인ᄒᆞ엿다〮[.]/);
|
||||
assert.match(output, /contact@example[.]com은 말ᄒᆞ엿다〮[.]/);
|
||||
assert.match(output, /\[학교에서 보기\]\(https:\/\/example[.]com\/학교에서\)은 유지ᄒᆞ엿다〮[.]/);
|
||||
assert.match(output, /`학교에서` 테스트ᄒᆞ엿다〮[.]/);
|
||||
assert.match(output, /```\n학교에서 공부했다[.]\n```/);
|
||||
assert.match(output, /밖애 공부ᄒᆞ엿다〮[.]/);
|
||||
});
|
||||
|
||||
test("createReport exposes deterministic metadata and replacement evidence", () => {
|
||||
const report = createReport("열애설을 인정했다.");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue