What is Hackers' Pub?

Hackers' Pub is a place for software engineers to share their knowledge and experience with each other. It's also an ActivityPub-enabled social network, so you can follow your favorite hackers in the fediverse and get their latest posts in your feed.

제법 큰 바위가 멀리서 둔탁하게 깨져 굴러내려가며 내는 소리는...

0
0
1
1
1

그리고, 상상하기 어렵지 않게, 그게 바로 내가 '나쁜' 리더였던 이유다. 문제를 지적하는 것과 blame은 메세지로써 분리되어야 했다. public한 곳에서는 전자에 철저해야 하고 후자는 반드시 1 on 1에서 분리된 이슈로써 커뮤니케이션했어야 했다. 나는 그 주니어에게 사과할 기회를 끝내 얻지 못하고 헤어졌다. 내 커리어 최대의 후회 중 하나다. 나의 지적은 legit issue였을지언정 지극히 부적절했던 것이다. 그런 자리 (다른 사람들 앞)에서 그런 메세징을 해서는 안 됐고, 그 문제는 추출되어 개인적으로 언급되어야 했다.

0

...온갖 상태를 글로벌 스코프에 선언해서 써먹는 - 코드의 컨텍스트 핸들링을 '접근이 편하고 싶어서' 슬그머니 쓰는 주니어를 휘하에 둔 적이 있고 그렇게 그로 인해 한번 어질러진 코드베이스가 깨진 창문 효과처럼 모두가 state를 내던져버리는 문제가 프로젝트에 만연한 문제가 되는 걸 겪은 적이 있다. 나는 이걸 코드 리뷰에서 지적하고 아침 스탠드업에서 이 문제를 레이즈했다. 리드로써 그 주니어의 코드를 딱 찝어서 "이러면 안 됩니다 제발 이러지들좀 마십시오 이딴 식으로 글로벌 스코프 쓰지 마세요" 라는 말과 함께.

0

이런 문제 때문에 leader-subordination (특히 authority에서 갭이 있는) 간의 커뮤니케이션에서는 신경써야 할 문제가 많다. 어떤 요소는 public에서 이야기되어야 하고 어떤 요소는 *반드시* private에서 이야기되어야 한다. 정책 현안에 되한 문제기 때문에 public이어야 하는 것을 이해하지만, 그럼 거기에 맞춰서 공개 가능한 메시징을 사용해야 한다 - '말이 기시네' 는 엄연히 부적절한 메세지 & 톤임. 그 갑갑함에도 공감은 하지만, 그게 진짜 짜증났어도 거기서는 그렇게 말하면 안 되었다.

0
0
0

1. 인생에서 가장 강력한 힘은 복리다. 서두르지 말고, 중간에 포기하지 마라. — 워런 버핏

2. 탁월한 성과는 지능이 아니라, 오래 버티는 꾸준함에서 나온다. — 찰리 멍거

3. 성공은 올바른 행동을 충분히 오래 반복한 결과다. — 레이 달리오

4. 투자는 인내심 있는 사람의 돈이 조급한 사람에게서 이동하는 과정이다. — 벤저민 그레이엄

5. 하루 1%의 개선은 눈에 보이지 않지만, 시간이 지나면 삶 전체를 바꾼다. — 제임스 클리어

6. 복리는 세상의 여덟 번째 불가사의다. 이해한 사람은 벌고, 이해하지 못한 사람은 지불한다. — 알버트 아인슈타인

7. 천천히 가는 것은 문제가 아니다. 멈추는 것이 문제다. — 공자

8. 작은 이득을 반복적으로 쌓을 수 있는 구조가 가장 강하다. — 나심 탈레브

May be a graphic of text that says '1% better every day 1% worse every day The Power of Tiny Gains 1.01365= 1.01365=37.18 31.18 0.99365= 0.99 0.03 Improvement or Decline 1 1 1Year lear JamesClear.com'
0

2020年に漢族としてウイグルに入り、強制収容所の実態を命がけで調査した中国人青年の「関恒( )」はウイグル人コミュニティ、漢族を中心とした民主派コミュニティからも勇気ある良心的な行動として尊敬を集めていたそうです。

そののち米国に逃れ、政治亡命を申請していたところICEに拘束され、現在は中国への強制送還の危機に直面しているとのこと。

米国の支援者らは、トランプ政権に対し、この青年(关恒)の即時釈放と、ICEによる亡命希望者を恣意的に拘留する慣行の停止を要求する署名を開始したそうです。
日本からもご協力をお願いします🤲
change.org/p/free-heng-guan-an

0
0
0
0
0
1
0
1
1
1
0
0
1

昨日しゅいろママとエアライダーやったけど、シティトライアル参加者全員のオレマシンが反映されるからトマタさんに乗るしゅいろママがワンチャン見れたって事なんだよな……

1
1
0
1
1
1

Song Yongseok shared the below article:

Claude Code가 모델이 하지도 않은 말을 했다고 하는 이유.

자손킴 @jasonkim@hackers.pub

Claude Code에서 첫 번째 요청을 입력하면 가장 먼저 다음과 같은 JSON을 API로 보낸다. 이 요청은 실제 작업에 앞서 대화 주제를 파악하고 제목을 생성하기 위한 보조 요청이다.

{
  "model": "claude-haiku-4-5-20251001",
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "Request Body의 구조를 분석하고 분류별로 묶어서 표현한다. ultrathink"
        }
      ]
    },
    {
      "role": "assistant",
      "content": [
        {
          "type": "text",
          "text": "{"
        }
      ]
    }
  ],
  "system": [
    {
      "type": "text",
      "text": "You are Claude Code, Anthropic's official CLI for Claude."
    },
    {
      "type": "text",
      "text": "Analyze if this message indicates a new conversation topic. If it does, extract a 2-3 word title that captures the new topic. Format your response as a JSON object with two fields: 'isNewTopic' (boolean) and 'title' (string, or null if isNewTopic is false). Only include these fields, no other text. ONLY generate the JSON object, no other text (eg. no markdown)."
    }
  ],
  "tools": [],
  "metadata": {
    "user_id": "user-id"
  },
  "max_tokens": 32000,
  "stream": true
}

시스템 프롬프트를 보면 이 요청이 신규 대화인지 판단하고, 신규 대화라면 2-3 단어의 제목을 추출하여 isNewTopictitle 필드로 구성된 JSON만 반환하라고 지시하고 있다.

여기서 내 눈에 띈 것은 첫 번째 요청임에도 불구하고 마치 멀티턴 대화가 진행된 것처럼 messages의 마지막 roleassistant라는 점이었다. 게다가 Claude가 { 한 글자만 응답한 것처럼 구성되어 있다.

이 요청에 대한 응답은 다음과 같다.

{
  "id": "msg_id",
  "type": "message",
  "role": "assistant",
  "model": "claude-haiku-4-5-20251001",
  "content": [
    {
      "type": "text",
      "text": "\n  \"isNewTopic\": true,\n  \"title\": \"Request Body Formatting\"\n}"
    }
  ],
  "stop_reason": "end_turn",
  "stop_sequence": null,
  "usage": {
    "input_tokens": 187,
    "output_tokens": 26,
    "cache_creation_input_tokens": 0,
    "cache_read_input_tokens": 0
  }
}

content.text를 보기좋게 정리해서 적으면 다음과 같다.

  "isNewTopic": true,
  "title": "Request Body Formatting"
}

완전한 JSON에서 맨 앞의 {가 빠진 형태다. 알고 보니 이것은 prefill 기법이라 불리는 것으로, 모델이 응답의 앞부분을 이미 출력한 것처럼 설정하여 이어지는 응답을 원하는 형식으로 유도하는 방법이다.

Claude Code는 이 기법을 활용해 모델이 JSON 형식으로 응답하도록 강제하고 있다. 단순히 "JSON으로 응답해줘"라고 요청하는 것보다 훨씬 확실한 방법이다. 모델 입장에서는 이미 {로 시작했으니 자연스럽게 JSON을 완성할 수밖에 없기 때문이다.

Prefill은 JSON 외에도 다양하게 활용할 수 있다. 예를 들어 ```python으로 시작하면 모델이 파이썬 코드 블록을 완성하게 되고, <analysis>로 시작하면 XML 형식의 응답을 유도할 수 있다.

Read more →
4
0
0
0
0
1
1
0
0

これまで無料で使ってた Evernote、今月27日から年間更新で強制的に有料のProアカウントに更新させられるらしい。しかも年間利用料金がU$200超え。最近ほとんど使っていなかったので、流石にこの金額は痛い。改めて機能とかチェックしてみたけど、どうしてもこのアプリでないとダメ、っていうのはないんだよなー。データをドライブにバックアップして、更新日前にアカウント閉鎖するか…😮‍💨

0

洪 民憙 (Hong Minhee) shared the below article:

Stop writing if statements for your CLI flags

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

If you've built CLI tools, you've written code like this:

if (opts.reporter === "junit" && !opts.outputFile) {
  throw new Error("--output-file is required for junit reporter");
}
if (opts.reporter === "html" && !opts.outputFile) {
  throw new Error("--output-file is required for html reporter");
}
if (opts.reporter === "console" && opts.outputFile) {
  console.warn("--output-file is ignored for console reporter");
}

A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options.

In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all.

We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one.

The state of TypeScript CLI parsers

The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader.

But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously:

// cmd-ts
const app = command({
  args: {
    reporter: option({ type: string, long: 'reporter' }),
    outputFile: option({ type: string, long: 'output-file' }),
  },
  handler: (args) => {
    // args.reporter: string
    // args.outputFile: string
  },
});
// Clipanion
class TestCommand extends Command {
  reporter = Option.String('--reporter');
  outputFile = Option.String('--output-file');
}

These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress.

But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system.

So you end up writing validation code anyway:

handler: (args) => {
  // Both cmd-ts and Clipanion need this
  if (args.reporter === "junit" && !args.outputFile) {
    throw new Error("--output-file required for junit");
  }
  // args.outputFile is still string | undefined
  // TypeScript doesn't know it's definitely string when reporter is "junit"
}

Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type.

If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type?

Modeling relationships with conditional()

Optique treats option relationships as a first-class concept. Here's the test reporter scenario:

import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = conditional(
  option("--reporter", choice(["console", "junit", "html"])),
  {
    console: object({}),
    junit: object({
      outputFile: option("--output-file", string()),
    }),
    html: object({
      outputFile: option("--output-file", string()),
      openBrowser: option("--open-browser"),
    }),
  }
);

const [reporter, config] = run(parser);

The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value.

TypeScript infers the result type automatically:

type Result =
  | ["console", {}]
  | ["junit", { outputFile: string }]
  | ["html", { outputFile: string; openBrowser: boolean }];

When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type.

Now your business logic gets real type safety:

const [reporter, config] = run(parser);

switch (reporter) {
  case "console":
    runWithConsoleOutput();
    break;
  case "junit":
    // TypeScript knows config.outputFile is string
    writeJUnitReport(config.outputFile);
    break;
  case "html":
    // TypeScript knows config.outputFile and config.openBrowser exist
    writeHtmlReport(config.outputFile);
    if (config.openBrowser) openInBrowser(config.outputFile);
    break;
}

No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you.

A more complex example: database connections

Test reporters are a nice example, but let's try something with more variation. Database connection strings:

myapp --db=sqlite --file=./data.db
myapp --db=postgres --host=localhost --port=5432 --user=admin
myapp --db=mysql --host=localhost --port=3306 --user=root --ssl

Each database type needs completely different options:

  • SQLite just needs a file path
  • PostgreSQL needs host, port, user, and optionally password
  • MySQL needs host, port, user, and has an SSL flag

Here's how you model this:

import { conditional, object } from "@optique/core/constructs";
import { withDefault, optional } from "@optique/core/modifiers";
import { option } from "@optique/core/primitives";
import { choice, string, integer } from "@optique/core/valueparser";

const dbParser = conditional(
  option("--db", choice(["sqlite", "postgres", "mysql"])),
  {
    sqlite: object({
      file: option("--file", string()),
    }),
    postgres: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 5432),
      user: option("--user", string()),
      password: optional(option("--password", string())),
    }),
    mysql: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 3306),
      user: option("--user", string()),
      ssl: option("--ssl"),
    }),
  }
);

The inferred type:

type DbConfig =
  | ["sqlite", { file: string }]
  | ["postgres", { host: string; port: number; user: string; password?: string }]
  | ["mysql", { host: string; port: number; user: string; ssl: boolean }];

Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less.

With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility.

Try expressing this with requires_if attributes. You can't. The relationships are too rich.

The pattern is everywhere

Once you see it, you find this pattern in many CLI tools:

Authentication modes:

const authParser = conditional(
  option("--auth", choice(["none", "basic", "token", "oauth"])),
  {
    none: object({}),
    basic: object({
      username: option("--username", string()),
      password: option("--password", string()),
    }),
    token: object({
      token: option("--token", string()),
    }),
    oauth: object({
      clientId: option("--client-id", string()),
      clientSecret: option("--client-secret", string()),
      tokenUrl: option("--token-url", url()),
    }),
  }
);

Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid.

Why conditional() exists

Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()?

The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely.

But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence.

// This won't work as intended
const parser = or(
  object({ reporter: option("--reporter", choice(["console"])) }),
  object({ 
    reporter: option("--reporter", choice(["junit", "html"])),
    outputFile: option("--output-file", string())
  }),
);

When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally.

conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions.

The structure is the constraint

Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint.

Traditional approach Optique approach
Parse → Validate → Use Parse (with constraints) → Use
Types and validation logic maintained separately Types reflect the constraints
Mismatches found at runtime Mismatches found at compile time

The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating.

Try it

If this resonates with a CLI you're building:

  • Documentation
  • Tutorial
  • conditional() reference
  • GitHub

Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead?

The structure of your parser is the constraint. You might not need that validation code at all.

Read more →
7
1
3

아하. 국제교류에서 한국 농인들과 일본 농인들이 소통이 잘된다고 생각한 건
청인들의 시선이었고,

실상은
"이거 알아? 어 너네도 그렇게 말해?" 하면서
즉석으로 바디랭귀지 혹은 만국공통어를 사용해서 소통하고자 한 걸
청인들이 소통이 잘된다고 착각한 것에 불과하답니다.

실제로 한국과 일본 농인들은
자신의 나라가 근대국가로 발전하면서
국가로부터 많은 압박을 받아왔고
이 경험은 동일하지만
결국 다른 나라이기 때문에
수어 역시 같은 계통이지만
통합될 수 없을 정도로 다릅니다.

0

꽤 오래 디지털 정원을 가꿔왔다. 그런데 기존 디지털 정원 도구들은 (1) 로컬 파일 시스템 기반이 아니거나, (2) 문서 모델과 애플리케이션이 분리되어 있지 않아서 나의 워크플로우와는 잘 맞지 않았다. 그래서 직접 문서 빌드 도구를 만들었다. 역시 야크 털 깎고 개밥먹는게 제일 재미있다. github.com/parksb/simpesys

2
0
0
1
1
0
0
0
1
1
0
0