Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`를 쓴다.
Expand Down
27 changes: 24 additions & 3 deletions apps/detect/src/t1/screens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -169,16 +181,25 @@ 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': {
const area = input.existing.id.split('.')[0] ?? '';
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':
Expand Down
18 changes: 18 additions & 0 deletions apps/detect/src/t1/t1.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Loading