From 9c2f29a0abf9d5b2c603c5c5ec939a656be6e6f4 Mon Sep 17 00:00:00 2001 From: k0nghaa Date: Sun, 18 Oct 2026 22:14:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(detect):=20T1=20=EC=98=88=EC=99=B8=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=EC=9D=98=20=EA=B8=B4=20=EA=B2=B0=EC=A0=95=20?= =?UTF-8?q?=EC=82=AC=ED=95=AD=20=EC=A4=84=EC=9D=84=2076=EC=B9=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=82=98=EB=88=A0=20=EB=84=98=EC=96=B4=EA=B0=84=20?= =?UTF-8?q?=EC=A4=84=EB=8F=84=20=EA=B8=B0=EB=B3=B8=20=EA=B8=80=EC=9E=90?= =?UTF-8?q?=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=B4=EC=97=AC=20=EC=A4=8C=20?= =?UTF-8?q?(SIL-107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/detect/src/t1/screens.ts | 27 ++++++++++++++++++++++++--- apps/detect/src/t1/t1.test.ts | 18 ++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/detect/src/t1/screens.ts b/apps/detect/src/t1/screens.ts index 3d125fa..6817bf7 100644 --- a/apps/detect/src/t1/screens.ts +++ b/apps/detect/src/t1/screens.ts @@ -152,6 +152,18 @@ export type T1NoticeInput = | { kind: 'similarDeclined' } | { kind: 'saveRace' }; +/** + * 굵은 이름 뒤에 내용을 이어 COLS칸에서 줄을 나눠요. 넘어간 줄은 들여쓰고 기본 글자색으로 다시 칠해요. + * 터미널이 알아서 넘긴 줄은 꾸밈이 풀려 회색이 되어 다른 문장처럼 보여요 (10/16) + */ +function labeledLines(label: string, text: string): string[] { + return wrap(`${label}${text}`, COLS - 2).map((line, i) => + i === 0 && line.startsWith(label) + ? ` ${strong(label, true)}${strong(line.slice(label.length))}` + : ` ${strong(line, i === 0)}`, + ); +} + /** 예외 안내: 색 막대 한 줄과 설명 한 줄 (프로토타입 failFlow·sameFlow·race) */ export function noticeScreen(input: T1NoticeInput): string { const { notices } = T1_COPY; @@ -169,7 +181,10 @@ export function noticeScreen(input: T1NoticeInput): string { ? colorBar('red', notices.same) : colorBar('yellow', notices.similar), // 이미 있는 결정 사항: 굵은 [영역] 제목 · 값 (프로토타입 existingLine) - ` ${strong(areaTitle(existing.id, existing.title), true)}${strong(` · ${existing.value}`)}`, + ...labeledLines( + areaTitle(existing.id, existing.title), + ` · ${existing.value}`, + ), ].join('\n'); } case 'revertSame': { @@ -177,8 +192,14 @@ export function noticeScreen(input: T1NoticeInput): string { return [ colorBar('red', notices.revertSame.head), // 되돌려 달라고 한 버전: 굵은 [영역] vN: 값 - ` ${strong(`[${area}] v${input.version}`, true)}${strong(`: ${input.versionValue}`)}`, - ` ${strong(notices.revertSame.current, true)}${strong(`: ${input.existing.value}`)}`, + ...labeledLines( + `[${area}] v${input.version}`, + `: ${input.versionValue}`, + ), + ...labeledLines( + notices.revertSame.current, + `: ${input.existing.value}`, + ), ].join('\n'); } case 'similarDeclined': diff --git a/apps/detect/src/t1/t1.test.ts b/apps/detect/src/t1/t1.test.ts index aa3160c..8f6c515 100644 --- a/apps/detect/src/t1/t1.test.ts +++ b/apps/detect/src/t1/t1.test.ts @@ -998,6 +998,24 @@ describe('noticeScreen (예외 안내, 10/14 확정)', () => { ).toBe(true); }); + it('기존 결정 사항 줄이 길면 76칸에서 나누고 넘어간 줄도 기본 글자색으로 칠해요', () => { + const long = { + id: 'copy.explain.plain-words', + title: '변경 설명에는 개발 용어 대신 쉬운 말을 쓴다', + value: + 'AI가 쓰는 변경 설명은 개발 용어를 꼭 필요할 때만 쓰고, 처음 나오면 괄호로 풀어 쓴다', + }; + const screen = noticeScreen({ kind: 'same', existing: long }); + const [, ...rest] = lines({ kind: 'same', existing: long }); + expect(rest.length).toBeGreaterThan(1); + expect(rest.every((line) => textWidth(line) <= COLS)).toBe(true); + expect(rest.join(' ').replace(/\s+/g, ' ').trim()).toBe( + `[copy] ${long.title} · ${long.value}`, + ); + // 넘어간 줄도 기본 글자색(39)으로 시작해요 + expect(screen.split('\n')[2]?.startsWith(' \u001b[0;39m')).toBe(true); + }); + it('되돌릴 버전이 지금 값과 같으면 빨간 막대와 그 버전 값·지금 값을 보여 줘요 (SIL-104)', () => { const input: T1NoticeInput = { kind: 'revertSame', From b2a995a9e6123b44adcab951af7a304db2b55e85 Mon Sep 17 00:00:00 2001 From: k0nghaa Date: Tue, 20 Oct 2026 11:48:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20T1=20=EB=8B=A4=EB=A5=B8=20=EA=B2=B0?= =?UTF-8?q?=EC=A0=95=20=EC=82=AC=ED=95=AD=20=ED=99=94=EB=A9=B4=EC=9D=98=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=20=EC=A4=84=EC=9D=84=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=86=A0=ED=83=80=EC=9E=85=EC=B2=98=EB=9F=BC=20=EB=B0=94?= =?UTF-8?q?=EB=80=8C=EB=8A=94=20=EA=B0=92=EB=A7=8C=20=EC=A7=A7=EA=B2=8C=20?= =?UTF-8?q?=EC=93=B0=EB=8F=84=EB=A1=9D=20=EC=A7=80=EC=8B=9C=20(SIL-107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 027faee..1c0a7c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,7 +15,7 @@ 문구는 `apps/detect/src/t1/copy.ts`의 `T1_COPY`를 글자 그대로 쓴다(바꿔 말하지 않는다). 선택지는 거기 있는 것만 쓰고 새로 만들지 않는다. 1. haribo `get_standard`로 지금 영역·제목·값·`alwaysOn`·`currentVersion`과 적용 범위(`versions[0].scope`)를 읽는다. - 1-1. 되돌림 확인 (SIL-104, `detect.change.revert-mark`): `get_standard`는 `historyLimit: 50`으로 읽고, 바꿀 값을 지금 버전이 아닌 예전 버전과 비교한다. 띄어쓰기·끝 마침표·전각 글자만 다르거나, 글자는 달라도 뜻이 같으면(숫자·범위·조건·대상이 모두 같을 때만) 되돌림이다. 되돌림이면 바꿀 값을 그 버전의 값으로 글자 그대로 바꾸고(저장은 그대로 `save_standard`, DB가 되돌림으로 표시한다), 2의 화면에 그 버전을 `revert`로 더한다(되돌림 화면이 된다. 어긋나는 결정 사항이 없으면 보라 막대, 있으면 노란 막대). 2·5의 선택 창 질문은 되돌림이 아닐 때와 같다. - 2. 다른 결정 사항 화면: `trigger`(작업 중에 알아챘으면 `"agent"`, 8의 직접 요청이면 `"direct"`), 부탁받은 작업이나 요청을 한 줄로 정리한 `request`, 바꾸려는 결정 사항(`kind: "change"`, 맨 앞)·바꾸면 함께 어긋나는 다른 결정 사항(`kind: "mismatch"`)을 JSON으로 `node apps/detect/src/t1/conflicts.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다). 항목마다 `id`·`title`·`kind`·`why`(왜 그렇게 판단했는지 한 문장)를 넣고, 낱말만 겹치는 결정 사항은 넣지 않는다. 1-1에서 되돌림이면 `kind: "change"` 항목에 지금 버전 `version`(`currentVersion`)을 넣고, `get_standard`로 읽은 되돌아갈 버전의 `version`·`title`·`reason`·`changedBy`·`createdAt`을 `revert`로 글자 그대로 더한다. 화면을 요약하거나 "화면을 띄웠어요"처럼 설명하는 글을 쓰지 않고 바로 다음으로 간다(③도 같다). 그다음 `ask`를 AskUserQuestion으로 묻는다(질문은 `ask.question`, header는 `ask.header`, 선택지는 `yes`·`no`의 label·description 그대로, 질문에 화면 내용을 다시 적지 않는다). 선택지 대신 글로 답하면 `ask.typedReply`를 쓰고 같은 질문을 다시 묻는다. + 2. 다른 결정 사항 화면: `trigger`(작업 중에 알아챘으면 `"agent"`, 8의 직접 요청이면 `"direct"`), 부탁받은 작업이나 요청을 한 줄로 정리한 `request`, 바꾸려는 결정 사항(`kind: "change"`, 맨 앞)·바꾸면 함께 어긋나는 다른 결정 사항(`kind: "mismatch"`)을 JSON으로 `node apps/detect/src/t1/conflicts.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다). 항목마다 `id`·`title`·`kind`·`why`를 넣고(`why`는 어떻게 바뀌는지만 짧게 쓴 한 문장. `change`는 지금 값과 바꿀 값만 써서 "40자 이하로 정한 길이를 32자 이하로 다시 정해요"처럼, `mismatch`는 무엇이 어긋나는지만 써서 "결정 사항 제목(최대 40자)을 그대로 쓰면 32자에서 잘려요"처럼 쓴다. "지금 결정 사항은 …인데, 부탁하신 작업은 …"처럼 작업을 다시 설명하지 않는다. SIL-107), 낱말만 겹치는 결정 사항은 넣지 않는다. 1-1에서 되돌림이면 `kind: "change"` 항목에 지금 버전 `version`(`currentVersion`)을 넣고, `get_standard`로 읽은 되돌아갈 버전의 `version`·`title`·`reason`·`changedBy`·`createdAt`을 `revert`로 글자 그대로 더한다. 화면을 요약하거나 "화면을 띄웠어요"처럼 설명하는 글을 쓰지 않고 바로 다음으로 간다(③도 같다). 그다음 `ask`를 AskUserQuestion으로 묻는다(질문은 `ask.question`, header는 `ask.header`, 선택지는 `yes`·`no`의 label·description 그대로, 질문에 화면 내용을 다시 적지 않는다). 선택지 대신 글로 답하면 `ask.typedReply`를 쓰고 같은 질문을 다시 묻는다. 3. 아니요면 저장하지 않고 `notChanged`를 보여 준 뒤 작업을 멈추고 기다린다. 지금 값대로도 코드를 쓰지 않는다. 사람이 "40자로 작업해 줘"처럼 어떻게 할지 다시 말할 때만 그대로 진행한다. 아니요는 그 프롬프트로 시작한 작업 하나에만 적용된다. 새 프롬프트로 다시 결정 사항과 다른 값을 쓰게 되면 새 작업이라 T1을 1부터 다시 한다(예외 11). 4. 예면 이유를 정한다. 사람이 대화에서 한 말로 이유를 알 수 있으면 그 말을 한 문장으로 정리한다. 알 수 없으면 `reasonPrompt`를 글로 묻고(AskUserQuestion이 아님) 답을 한 문장으로 정리한다. 근거 없이 지어내지 않는다. 5. ③ 등록될 모습: 등록할 값(`id`·`title`·`value`·`reason`·`alwaysOn`, 그리고 바꾸는 사람 이름 `changedBy` = 이 세션 `list_pending`의 `recipient`, 모르면 뺀다)을 JSON으로 `node apps/detect/src/t1/preview.ts <<'EOF' … EOF`에 넘겨 실행한다(화면은 hook이 띄운다). 그다음 `preview.confirm`을 AskUserQuestion으로 묻는다(header는 `preview.header`, 선택지는 `register`·`edit`·`cancel`의 label·description 그대로, 질문과 설명에 ③ 내용을 다시 적지 않는다. 명령 출력의 "알림 받는 이슈"가 1개 이상이면 `register` 대신 `registerWithIssues`를 쓴다). 수정하기면 `editPrompt`를 글로 묻고 고친 뒤 5를 다시 한다. 취소하기면 저장하지 않고 `cancelled`를 보여 준 뒤 하던 작업을 멈추고 사람의 말을 기다린다. `cancelled`는 다른 작업 중에 T1이 떴으면 `withTask`, 결정 사항을 바꿔 달라는 것 자체가 요청이었으면 `noTask`를 쓴다.