k-skill/korean-slang-writing
Jeffrey (Dongkyu) Kim 4fc01391ce
Release: Merge dev into main (#163)
* Add a guided Hola Poke Yeoksam skill without widening repo scope

Issue #120 only needs a repository skill payload, discoverability docs,
and regression coverage. This change adds the new skill, wires it into
existing docs surfaces, and locks the remote-MCP-only contract in tests
so future edits keep the phone-only event flow and verbatim message
relay behavior.

Constraint: The upstream Hola Poke flow lives on a remote MCP server, so this repo should not add proxy/runtime code
Constraint: Tests must be written before refining the new docs/skill wording
Rejected: Add local package or proxy support for Hola Poke | would over-scope a docs-only skill addition
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep this skill limited to 올라포케 역삼점 and treat the MCP response message as the event source of truth
Tested: node --test scripts/skill-docs.test.js --test-name-pattern='hola-poke-yeoksam'
Tested: npm run ci
Tested: Live MCP initialize/tools/list/get_menu/get_shop_info/enter_event(phone_format) smoke checks against https://hola-poke-yeoksam-skill.onrender.com/mcp
Not-tested: Successful live event entry with a real phone number

* Help users find nearby public restrooms from Korean location queries

This adds a new public-restroom-nearby skill and reusable package that resolves a user-provided location, narrows the official 공중화장실정보 dataset by region when possible, and ranks nearby restroom results with opening-time hints and map links.

Constraint: Must use free official/open surfaces without introducing new dependencies
Constraint: Must follow TDD and keep release/docs metadata aligned in the same change
Rejected: Add a proxy route first | direct official CSV access already works and keeps scope narrower
Rejected: Use nationwide-only ranking without regional narrowing | too much noisy data for dense urban anchors
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: If Kakao place-panel or localdata CSV schema changes, update parser fixtures before broad logic changes
Tested: npm run ci; live smoke via searchNearbyPublicRestroomsByLocationQuery('광화문', { limit: 3 }); architect review APPROVED
Not-tested: Non-Seoul live smoke across every regional orgCode

* Pin the Hola Poke MCP contract in repo-owned regression fixtures

The earlier issue #120 regression only matched prose, so this follow-up records the verified remote MCP tool/result snapshot in a checked-in fixture and makes both docs surfaces byte-align to it. That keeps the discoverability docs honest while turning the review claim into a real contract lock for tools/list, get_menu, get_shop_info, and the invalid-phone event flow.

Constraint: The upstream remote MCP server can change independently of this repo
Rejected: Keep prose-only regex checks | would not catch contract drift
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Refresh the fixture, both JSON fences, and the live-smoke evidence together whenever the upstream contract changes
Tested: node --test scripts/skill-docs.test.js --test-name-pattern='hola-poke-yeoksam'; npm run ci; live MCP smoke check against https://hola-poke-yeoksam-skill.onrender.com/mcp (initialize, tools/list, get_menu, get_shop_info, invalid enter_event)
Not-tested: Successful enter_event with a real phone number (intentionally avoided to prevent live event participation)

* Keep nearby restroom lookups resilient to flaky Kakao place panels

The review caught two regressions in the new public-restroom-nearby package: a single broken Kakao panel aborted anchor resolution, and coordinate search dropped maxDistanceMeters before normalization. This change adds targeted regression coverage first, keeps per-candidate HTTP failures recoverable, and hardens request errors with explicit status/url metadata so fallback logic no longer depends on parsing error strings.

Constraint: Must preserve the published package surface and keep the fix scoped to PR #123 follow-up
Rejected: Swallow all panel errors | would hide non-HTTP failures like network faults
Rejected: Parse request error messages for status codes | brittle coupling to string formatting
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep recoverable Kakao panel handling aligned with request() error annotations if request() changes again
Tested: npm test --workspace public-restroom-nearby
Tested: npm run ci
Tested: live smoke searchNearbyPublicRestroomsByLocationQuery('광화문', { limit: 3 })
Tested: LSP diagnostics on packages/public-restroom-nearby/src/index.js and test/index.test.js
Not-tested: Live Kakao fallback against a real upstream 5xx place-panel response

* Keep the Hola Poke contract claims aligned with verified coverage

The reviewed fixture-based regression already locks the documented remote
snapshot, but the docs still implied the enter_event success path had
live proof. Narrow the docs and the regression so they explicitly say the
success fields are pinned by the recorded snapshot while the live smoke
only verifies the invalid-phone retry path.

Constraint: Live success-path verification would trigger a real event entry and is intentionally avoided
Rejected: Leave the broader wording in place | review feedback showed it overstated the live evidence
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: If a safe non-mutating success-path probe becomes available, update the docs and fixture wording together
Tested: node --test scripts/skill-docs.test.js --test-name-pattern='hola-poke-yeoksam'; npm run ci; live MCP smoke against https://hola-poke-yeoksam-skill.onrender.com/mcp (initialize, tools/list, get_menu subset, get_shop_info subset, invalid enter_event)
Not-tested: Real enter_event success-path invocation

* Document the restroom distance-cap contract with regression coverage

The approved issue-117 code fix already restored maxDistanceMeters behavior, but the published docs did not lock or explain that contract. This follow-up adds a failing-first doc regression, then updates the feature guide and package README with the verified 100m example so users and future reviewers see the same behavior the package now ships.

Constraint: Must stay scoped to the existing PR #123 follow-up without reopening the implementation surface
Rejected: Leave the behavior implicit in code/tests only | published docs would lag the verified contract
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep the public-restroom-nearby docs and skill-docs regression aligned with live maxDistanceMeters smoke evidence if the sample query changes
Tested: node --test scripts/skill-docs.test.js (red then green)
Tested: npm test --workspace public-restroom-nearby
Tested: npm run ci
Tested: live smoke searchNearbyPublicRestroomsByLocationQuery('광화문', { limit: 3 })
Tested: live smoke searchNearbyPublicRestroomsByLocationQuery('광화문', { limit: 3, maxDistanceMeters: 100 })
Tested: architect review APPROVED
Not-tested: Alternative landmark queries with a non-zero maxDistanceMeters hit set

* Expose KRX partial failures instead of misreporting stock lookups

The Korean stock proxy used to silently drop failed market snapshots during
search and could turn an empty holiday trade snapshot into a 502 by falling
back into base-info lookup.

This change surfaces degraded market metadata on partial search success,
short-circuits empty trade snapshots to not_found, and refreshes the user
docs to use a real trading day in examples.

Constraint: KOSPI base-info approval is granted separately from other KRX routes
Constraint: Healthy markets should still return usable search results during a partial outage
Rejected: Return 502 on every partial search failure | hides still-usable markets and breaks current clients unnecessarily
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep degraded search metadata when any market snapshot fetch fails so partial outages stay visible
Tested: npm test --workspace k-skill-proxy
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Not-tested: Live KOSPI base-info behavior after the new KRX permission is approved

* Adopt kordoc for the hwp skill workflow

Issue #119 replaces the previous HWP guidance with kordoc so the skill matches the newer agent-native document flow. The docs and regression tests now center the HWP skill on kordoc parsing, JSON extraction, diffing, form filling, and Markdown-to-HWPX round-tripping, while the install/source references stay in sync.

Constraint: The repository treats skill behavior as documentation contracts backed by regression tests
Constraint: The requested branch/PR flow must target dev with TDD and verified execution evidence
Rejected: Keep @ohah/hwpjs or hwp-mcp as fallback guidance | issue #119 explicitly approves replacing the prior stack with kordoc
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep future hwp skill/docs/tests aligned to a single kordoc-first contract unless a new issue explicitly reintroduces multi-backend routing
Tested: node --test scripts/skill-docs.test.js; npm run ci; temp-dir kordoc roundtrip via markdownToHwpx -> sample.hwpx -> kordoc CLI markdown output; architect review APPROVED
Not-tested: Live parsing of user-provided proprietary HWP/HWPX samples outside the generated roundtrip fixture

* Prevent degraded stock search outages from sticking in cache

Reviewer feedback showed that partial KRX market failures could be cached as full search answers, masking recovery on the next identical request. This change adds a regression that fails first, skips route-level caching for degraded search payloads, and keeps the trade-info empty-snapshot contract documented alongside the partial-failure response semantics.

Constraint: Existing PR #124 already targets dev and must remain the follow-up lane for issue #99
Constraint: Proxy behavior must stay read-only and dependency-free
Rejected: Cache degraded search payloads for a short TTL | still risks transient false negatives during the TTL window
Rejected: Broaden trade-info fallback behavior | empty snapshots should stay explicit not_found results
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep degraded search responses out of the long-lived route cache unless a future design adds explicit revalidation semantics
Tested: npm test --workspace k-skill-proxy; node --test scripts/skill-docs.test.js; npm run ci; explicit buildServer degraded-search recovery repro
Not-tested: Live KRX production endpoints from this branch

* Align HWP docs with the published kordoc surface

The issue #119 follow-up needs the repository contract to match what the
currently published kordoc package actually supports. This narrows the
HWP skill/docs/tests to the verified install requirement and supported
CLI/Node API surfaces, and removes unsupported fill/mcp claims.

Constraint: Published kordoc CLI fails at startup without pdfjs-dist
Constraint: Docs/tests must reflect the current npm package behavior, not intended future features
Rejected: Keep fill/mcp examples with caveats | still documents unsupported entrypoints
Confidence: high
Scope-risk: narrow
Directive: Reintroduce fill/mcp docs only after verifying the published package exposes them in both CLI and Node API
Tested: node --test scripts/skill-docs.test.js; npm run ci; temp-dir clean install smoke; temp-dir kordoc+pdfjs-dist watch/parse/extractFormFields/compare/markdownToHwpx/roundtrip smoke; Claude architect review
Not-tested: Real-world HWPX template that produces non-empty extractFormFields output

* Keep HWP docs runnable against the published kordoc package

The follow-up closes the last runnable-contract gaps from review by documenting the working one-shot npx form and separating Node API examples into a local project install path. The regression suite now locks both install notes so future edits do not drift back to broken command shapes.

Constraint: Published kordoc CLI still requires pdfjs-dist at startup
Constraint: Global NODE_PATH does not make ESM imports from kordoc resolvable in the documented examples
Rejected: Keep bare `npx kordoc` examples | fails in a clean environment
Rejected: Keep global-install Node API guidance | ESM import remains unresolved
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep HWP docs aligned to verified published kordoc surfaces until the package contract changes upstream
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Tested: temp-dir local npm install kordoc pdfjs-dist plus markdownToHwpx -> sample.hwpx -> one-shot kordoc roundtrip smoke
Not-tested: upstream unpublished kordoc features beyond the verified CLI and Node API surfaces

* Add Korean scholarship search skill and reporting workflow (#116)

* Add nationwide scholarship search skill workflow

* Rename scholarship skill to 장학금 주세요 쮜에발

* Fix scholarship skill validation in CI

* Trigger GitHub PR diff refresh after dev rebase on main

* Fix scholarship helper status handling and test coverage

* Use KST as scholarship helper default date basis

* Rename scholarship skill display name

---------

Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>

* Feature/#121 (#127)

* Recover KakaoTalk mac skill auth when upstream user_id detection fails

Issue #121 reproduces on a real MacBook because `kakaocli auth` can fail even when the encrypted hex-named DB exists. This change adds a thin repo-owned helper that recovers the active user_id from plist revision hashes, caches the validated DB/key tuple, and reuses it for read-only `kakaocli` commands. The skill and feature docs now steer users to the helper when upstream auto-detection stops at candidate key mismatch, and regression tests lock the recovery flow before the implementation.

Constraint: Must stay a thin adapter around upstream kakaocli rather than forking the CLI
Constraint: Must verify on a real local macOS KakaoTalk install where issue #121 reproduces
Rejected: Full kakaocli reimplementation inside k-skill | too broad for the user_id/key-derivation failure scope
Rejected: Docs-only workaround | does not actually fix the broken auth path for users
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep this helper limited to auth/key recovery and read-only passthrough unless upstream gaps widen materially
Tested: python3 -m unittest scripts.test_kakaotalk_mac
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Tested: python3 scripts/kakaotalk_mac.py auth --refresh --max-user-id 800000000 --workers 8 --chunk-size 2000000
Tested: python3 scripts/kakaotalk_mac.py chats --limit 1 --json
Not-tested: Other kakaocli subcommands beyond auth/chats/messages/search/query/schema

* Protect the KakaoTalk helper's safe recovery path

Address the PR follow-up by treating malformed auth cache files as cache misses,
removing write-capable passthrough from the wrapper surface, and redacting
human-readable auth output so the cached SQLCipher key is not echoed back into
terminal history. The docs and regression suite now describe and enforce the
read-only contract that the helper is meant to preserve.

Constraint: Helper must remain a read-only recovery wrapper around local kakaocli access
Rejected: Keep query support with SQL validation | still leaves a risky write-capable escape hatch
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Do not re-expose arbitrary SQL passthrough or print the SQLCipher key in default text output
Tested: python3 -m unittest scripts.test_kakaotalk_mac; node --test scripts/skill-docs.test.js; npm run ci; python3 scripts/kakaotalk_mac.py auth --refresh --max-user-id 800000000 --workers 8 --chunk-size 2000000; python3 scripts/kakaotalk_mac.py chats --limit 1 --json; python3 scripts/kakaotalk_mac.py auth --cache-path <bad-json>; python3 scripts/kakaotalk_mac.py query --help
Not-tested: External automation consumers that depend on shell/json auth output beyond the documented helper flows

* Lock the helper CLI surface against accidental regressions

The approved issue #121 fixes already hardened the KakaoTalk Mac helper, but the test suite still only exercised the passthrough validator directly. Add an explicit parser-level regression so the public CLI contract stays read-only and `query` cannot quietly reappear in future edits.

Constraint: Follow-up is on the existing feature/#121 PR branch and must stay minimal
Rejected: Re-open helper implementation changes | current code already satisfies the approved review findings
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep parser exposure tests aligned with READ_ONLY_COMMANDS whenever helper subcommands change
Tested: python3 -m unittest scripts.test_kakaotalk_mac; node --test scripts/skill-docs.test.js; npm run ci; python3 scripts/kakaotalk_mac.py auth --refresh --max-user-id 800000000 --workers 8 --chunk-size 2000000; python3 scripts/kakaotalk_mac.py chats --limit 1 --json; python3 scripts/kakaotalk_mac.py auth --cache-path <bad-json>
Not-tested: No new production code paths changed in this follow-up

* Honor explicit Kakao auth recovery overrides

The helper now treats manual auth overrides as a cache-bypassing recovery request and rejects invalid brute-force tuning flags at the CLI boundary so users get deterministic behavior instead of stale cached tuples or Python tracebacks. Regression coverage locks both paths before the PR follow-up lands.

Constraint: The helper must remain a thin read-only wrapper around kakaocli auth recovery
Rejected: Require --refresh whenever --user-id/--uuid is passed | worse UX than honoring overrides directly
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep explicit auth overrides ahead of cache reuse unless the CLI contract is redesigned and documented
Tested: python3 -m unittest scripts.test_kakaotalk_mac; node --test scripts/skill-docs.test.js; npm run ci; python3 scripts/kakaotalk_mac.py auth --refresh --max-user-id 800000000 --workers 8 --chunk-size 2000000; python3 scripts/kakaotalk_mac.py chats --limit 1 --json; python3 scripts/kakaotalk_mac.py auth --cache-path <bad-json>; python3 scripts/kakaotalk_mac.py auth --refresh --max-user-id -1; python3 scripts/kakaotalk_mac.py auth --refresh --workers 2 --chunk-size 0 --max-user-id 10; python3 scripts/kakaotalk_mac.py auth --cache-path <temp-cache> --user-id 999; python3 scripts/kakaotalk_mac.py auth --cache-path <temp-cache> --uuid <live-uuid>
Not-tested: Manual override success with a truly alternate valid user_id/uuid pair on a multi-account local install

* Feature/#129 (#131)

* Add official KBL results support so basketball queries use live league data

Issue #129 needs a read-only skill and reusable package for KBL schedules, results, and standings. The implementation follows the existing sports package pattern and uses the league's live JSON APIs after verifying they respond successfully in real requests.

Constraint: Must use official KBL JSON surfaces before considering scraping
Constraint: Packaging changes must pass npm run ci and include docs plus Changesets updates
Rejected: Browser scraping first | official api.kbl.or.kr endpoints are live and simpler to maintain
Rejected: Reuse KBO/K League package shapes verbatim | KBL payload and team/status fields differ materially
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep seasonGrade=1 as the default KBL path unless future docs/tests explicitly widen to D-League flows
Tested: npm run ci; npm run lint --workspace kbl-results; npm test --workspace kbl-results; live getKBLSummary("2026-04-01", { team: "KCC", includeStandings: true })
Not-tested: Historical standings snapshots for past seasons via alternative KBL endpoints

* Prevent optional standings lookups from over-fetching the KBL API

The new kbl-results summary helper exposes includeStandings=false, so the
regression suite now proves that path stays schedule-only and never calls
the standings endpoint when the caller opts out.

Constraint: The KBL package should preserve the caller's no-standings contract
Rejected: Rely on manual inspection of the helper options | a targeted test is cheaper and safer
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep includeStandings=false side-effect free unless the public API contract changes explicitly
Tested: npm test --workspace kbl-results; npm run lint --workspace kbl-results
Not-tested: Full-repo CI before stacking this commit onto the rebased branch

* Add Naver Shopping price comparison skill

* Use Naver Shopping BFF fallback

* Fix naver shopping BFF page and sort fallback

* Clarify Naver OpenAPI review sort fallback

* Add library book search skill

* Add Data4Library route regression coverage

* Fix Data4Library book-exists ISBN-10 handling

* Refactor Coupang skill to retention MCP layer

* Add Coupang MCP wrapper follow-up coverage

* Clarify Coupang wrapper init guidance

* Document Coupang MCP init examples

* Add parking lot search skill

* Add korean-privacy-terms skill regression tests

* Add korean-privacy-terms thin-wrapper skill

* Document korean-privacy-terms skill across repo docs

* Bundle Apache-2.0 LICENSE with korean-privacy-terms wrapper

Addresses PR #149 review SHOULD FIX: ship the Apache-2.0 LICENSE text
alongside the thin wrapper so Apache License 2.0 §4(a) ('give any other
recipients of the Work or Derivative Works a copy of this License') is
satisfied even before `install.sh` fetches the upstream payload.

- Copy upstream LICENSE verbatim to `korean-privacy-terms/LICENSE.upstream`
  (byte-for-byte identical to upstream at pinned SHA
  e390f7b9feb825e368c26726363ea5ce11a34083; SHA256
  35ef947614c2f14df01c5fc553f987f644f0c9f6b011adda397bd788a87f1510).
- Update SKILL.md Notes to link LICENSE.upstream, clarify that repo-root
  LICENSE (MIT) is k-skill's own license not this skill's, and document
  that nested upstream SKILL.md is not discovered by agent platforms.
- Document the home-path `bash ~/.claude/skills/.../install.sh` variant in
  SKILL.md so users who pulled the wrapper via `npx skills add --skill` can
  install without a repo checkout (installer already resolves
  ${BASH_SOURCE[0]} absolutely).
- Update docs/features/korean-privacy-terms.md to document LICENSE.upstream
  and the §4(a) rationale.
- Strengthen skill-docs regression tests (NICE TO HAVE items from review):
  * Reject placeholder pins (all-zero / all-f 40-char strings).
  * Assert the literal upstream clone URL
    (https://github.com/kimlawtech/korean-privacy-terms.git).
  * Assert `git clone --filter=blob:none` is used for blobless fetches.
  * Add new regression test that verifies LICENSE.upstream exists, matches
    the Apache-2.0 preamble / §4 / APPENDIX structure, and is referenced
    from both SKILL.md and the feature doc.

* Assert APPENDIX anchor in korean-privacy-terms LICENSE.upstream

Close Round 3 NICE TO HAVE from PR #149. The LICENSE.upstream
regression block asserted preamble, Version 2.0, Redistribution,
END OF TERMS, and Copyright 2026 kimlawtech but not the APPENDIX
anchor at LICENSE.upstream:179, even though the Round 1 follow-up
and Round 2 review collectively described 'APPENDIX structure
verification'. Adding this one assertion closes that claim/test
parity gap and acts as tamper-detection if upstream reformats
LICENSE later.

Verified with TDD: temporarily stripped APPENDIX line from
LICENSE.upstream, confirmed test 108 FAILS with the expected
regex mismatch, then restored and re-confirmed 109/109 GREEN.
Byte-for-byte identity with upstream LICENSE still holds
(SHA256 35ef947614c2f14df01c5fc553f987f644f0c9f6b011adda397bd788a87f1510).

npm run ci exit 0 with 357 ok subtests (unchanged baseline,
additive assertion within existing test block).

* Fix extractDataGoItems to handle current data.go.kr JSON shapes

The MFDS data.go.kr drug and food endpoints now return body.items as a
flat array (DrbEasyDrugInfoService, SafeStadDrugService) or an array of
{item: {...}} wrappers (PrsecImproptFoodInfoService03), instead of the
legacy {items: {item: [...]}} XML→JSON auto-convert shape.

Our extractDataGoItems was still looking for body.items.item, so it
returned [] for every entry, silently breaking:
  - /v1/mfds/drug-safety/lookup
  - /v1/mfds/food-safety/search (improperFood portion)

Update extractDataGoItems to accept all three shapes and refresh the
mock fixtures in server.test.js to match what upstream actually returns,
while adding a backward-compat test for the legacy shape.

Note: this does not resolve the remaining FOODSAFETYKOREA_API_KEY being
rejected by upstream (issue #148 core symptom) - that is a separate
operational key rotation on the proxy server.

* Make proxy cache failure-aware and require route-prefixed cache keys

Two related issues surfaced while investigating issue #148:

1. Transient upstream failures were being cached for the full 5-minute
   TTL because every route handler called cache.set() unconditionally
   with whatever payload came back - including empty items + warnings
   from a flaky upstream like openapi.foodsafetykorea.go.kr. The user
   would then see "empty + warning" for 5 minutes even after upstream
   recovered.

2. makeCacheKey(payload) hashes the whole payload, but fine-dust/report
   was the only route calling it without a "route" prefix
   (makeCacheKey(normalized) instead of
   makeCacheKey({ route: "fine-dust-report", ...normalized })).
   Different routes with the same normalized shape could collide.

Fix both globally in the cache layer so every current and future route
benefits without per-route edits:

- createMemoryCache.set rejects any payload that isFailureResponse
  considers a failure (explicit error field, upstream.degraded flag,
  or empty items alongside warnings). Returns false on reject, true
  on accept, so callers can observe the decision if needed.
- makeCacheKey now throws if payload.route is missing or empty. This
  catches the fine-dust inconsistency and prevents new routes from
  reintroducing it.
- fine-dust/report now passes `route: "fine-dust-report"` like every
  other route.

New tests:
- makeCacheKey asserts distinct routes produce distinct keys and throws
  without a route.
- isFailureResponse covers all failure signatures plus graceful-
  fallback cases (items present alongside warnings) that must stay
  cacheable.
- createMemoryCache.set refuses each failure shape and still stores
  healthy payloads.
- End-to-end: food-safety/search with a flaky recall upstream serves
  the upstream failure, retries live when upstream recovers, and only
  caches once the payload is healthy.

TTL itself is unchanged - the value still protects upstream rate
limits; it just no longer amplifies transient errors.

* Document Coupang hosted fallback contract and affiliate disclosure

retention-corp/coupang_partners#1 is merged, so upstream now transparently falls back to the Retention Corp hosted backend at https://a.retn.kr/v1/public/assist when Coupang Partners API credentials are missing. The k-skill wrapper already passes environment variables through unchanged, so this commit lines up the documented contract with the actual two-path behavior without changing runtime logic.

- SKILL.md and docs/features/coupang-product-search.md describe both execution paths (operator local HMAC vs credentialless hosted fallback), the honored OPENCLAW_SHOPPING_* env vars, the allowlist client-id convention including the k-skill-specific coupang-mcp-fallback value, and the mandatory affiliate disclosure when a.retn.kr/s/ shortlinks or lptag=AF deeplinks appear in responses.
- docs/sources.md adds the hosted assist endpoint and the merged upstream PR so the source surface stays truthful.
- README.md reflects the 선택사항 semantics for the 쿠팡 상품 검색 row and extends the column legend so 선택사항 is distinct from 불필요.
- coupang_partners_mcp.py expands its --help epilog so operators discover the honored upstream env vars without reading the wrapper source; no runtime behavior change.
- scripts/test_coupang_partners_mcp_wrapper.py locks env pass-through as a regression, asserts the new --help contract, and adds an opt-in K_SKILL_COUPANG_SMOKE=1 live smoke test that verifies the credentialless hosted path returns a Coupang deeplink.
- scripts/skill-docs.test.js extends the docs regression to require the hosted assist URL, OPENCLAW_SHOPPING_* env prefix, affiliate disclosure wording, and hosted fallback concept while keeping the yuju777 HF Space negative assertion.

Verified: npm run ci exits 0, live smoke test (K_SKILL_COUPANG_SMOKE=1) returns a.retn.kr/s/ shortlinks via credentialless wrapper, and manual env -u COUPANG_ACCESS_KEY -u COUPANG_SECRET_KEY call returns isRocket+lptag=AF3727577 responses through the hosted fallback.

Refs: #134

* Drop non-allowlisted coupang-mcp-fallback recommendation from hosted fallback docs

Direct probes against https://a.retn.kr/v1/public/assist confirmed that
X-OpenClaw-Client-Id: coupang-mcp-fallback returns HTTP 403 Client is not
allowlisted, while the upstream default openclaw-skill returns HTTP 200.
The default wrapper path already works because upstream falls back to
openclaw-skill, but the explicit recommendation in SKILL.md and the
feature doc was luring users to a 403 path.

Remove the dead recommendation and lock in the working configuration:

- Docs describe openclaw-skill as the upstream-allowlisted default and
  note that k-skill does not override OPENCLAW_SHOPPING_CLIENT_ID.
- Wrapper --help epilog drops the Suggested k-skill value line and
  documents openclaw-skill as the allowlist value in play.
- New skill-docs regression asserts coupang-mcp-fallback is absent from
  SKILL.md, the feature doc, the wrapper, and docs/sources.md while
  openclaw-skill is documented across all three narrative surfaces.
- New Python wrapper regression asserts --help drops the dead value and
  surfaces openclaw-skill so the constraint stays locked.
- Existing env-forwarding test uses openclaw-skill as the pass-through
  sentinel so the repo no longer ships the non-allowlisted string at all.

* Add lh-notice-search skill and /v1/lh-notice/{search,detail} proxy routes

Wraps the official data.go.kr LH (Korea Land & Housing Corporation) 청약
공고 Open API (B552555/lhLeaseNoticeInfo1/*) so agents can look up LH
임대/분양/주거복지/토지/상가 공고 by region, status, category, keyword,
and notice ID without asking users for a ServiceKey. Reuses the shared
DATA_GO_KR_API_KEY the proxy already manages; users see '불필요'.

Adapter handles both the LH-specific [CMN, dsList] JSON envelope and the
standard data.go.kr <OpenAPI_ServiceResponse> XML error envelope; refuses
to cache failure responses so transient upstream errors self-heal.

Closes #145.

* Document LH extractNoticeEnvelope success-code accept-list as deliberate

Per review note #4 on PR #158, extractNoticeEnvelope accepts four upstream
CMN.CODE values ("SUCCESS", "0", "00", "000") and three header.resultCode
values ("0", "00", "000") as success. This is deliberate: the data.go.kr
platform has surfaced different forms across catalog eras, and a future
normalization that flips SUCCESS to a numeric form must not regress into
502'ing otherwise-valid responses.

- Add an inline comment above the array-envelope success-code check in
  src/lh-notice.js explaining why the accept-list is NOT redundant.
- Add regression tests in test/lh-notice.test.js that explicitly exercise
  each accepted success code (SUCCESS/0/00/000 for array envelope; 0/00/000
  for object envelope) so a future refactor cannot silently collapse the
  accept-list.
- Add a paired rejection test that numeric-looking non-success codes like
  "22" and "10" still raise as upstream_error, disambiguating the
  accept-list from a blanket 'any numeric string passes' rule.

Test count: lh-notice.test.js 30 -> 38 (all pass); npm run ci exits 0.

* Pin LH /v1/lh-notice/detail failure-not-cached contract with regression test

Round 2 review noted that /v1/lh-notice/detail failure-not-cached
behavior was only verified via manual QA, while /search had an
explicit automated regression test.

This adds an equivalent automated test for /detail that:
- fails upstream once (XML SERVICE_KEY error, upstream_code=30)
- confirms first call returns 502 with cache.hit=false
- switches upstream to success and retries the same URL
- confirms second call returns 200 with cache.hit=false (failure was
  NOT cached, retry hit upstream again)
- sabotages upstream back to failing and verifies the third call
  serves the previously-cached success (cache.hit=true, no new fetch)

Verified the test genuinely catches regressions by temporarily
monkey-patching the detail route to cache error payloads — the test
correctly fails in that sabotaged state and passes when the route is
correct. Full server.test.js suite goes from 95 to 96 tests, all pass.

* Document LH /detail test pins both cache-protection layers

Adds a 12-line header comment to the 'lh-notice detail does not cache
upstream XML auth errors so retries self-heal' test in server.test.js
naming the two cache-protection layers it pins:

  (a) the early-return catch block in the route handler (no cache.set
      on upstream failure), and
  (b) the isFailureResponse() guard inside cache.set (refuses any
      payload with .error set).

Points future maintainers to the independent sabotage audit in PR #158
Round 3 review that proved bypassing either layer alone makes the
State 2 self-heal assertion fail, and cross-links the sibling /search
failure-not-cached test for symmetric coverage.

Addresses the Round 3 non-blocking observation #2 nice-to-have.
Test-only, comment-only: +12 lines, 0 source changes, 0 behavior
changes, 0 doc changes, 0 changeset changes. server.test.js remains
96/96, lh-notice.test.js remains 38/38, full proxy workspace 184/184.

* Add naver-news-search skill and /v1/naver-news/search proxy route

Closes #143. Proxies the official Naver Search Open API news endpoint
(openapi.naver.com/v1/search/news.json) through k-skill-proxy so users do
not need to issue their own Naver Client ID/Secret. Reuses the existing
NAVER_SEARCH_CLIENT_ID/NAVER_SEARCH_CLIENT_SECRET that naver-shopping already
consumes, since the Naver Developer application enables the 'Search' scope
covering both news and shopping.

Implementation details:
- src/naver-news.js normalizes q/display/start/sort, builds the official URL,
  calls upstream with X-Naver-Client-Id/Secret headers, and parses the JSON
  response into rank/title/description/link/original_link/pub_date items.
- Strips <b> highlight tags and decodes HTML entities in title/description
  using zero-width replacement so compound Korean words like '주식형' are
  preserved (not split into '주식 형').
- Parses RFC822 pubDate into pub_date_iso (ISO-8601 UTC) for clients.
- Deduplicates items by normalized link; drops entries missing title/link.
- Returns 503 upstream_not_configured when proxy keys are absent (no public
  BFF fallback exists for news like it does for shopping, so keys are
  required).
- Failure responses are not cached (failure-aware cache layer).
- Exposes naverNewsApiConfigured on /health.

14 new tests in test/naver-news.test.js cover query validation, URL
building, payload normalization (HTML stripping, entity decoding,
deduplication, missing-field tolerance), plus Fastify integration tests
for 200/400/401/429/500/503 paths, cache hit/miss, header wiring, and
the health flag.

* Add rhwp-edit and rhwp-advanced skills with k-skill-rhwp CLI

Splits HWP handling into three focused skills per issue #155:

- hwp (kept): kordoc-based read/convert (Markdown, JSON, diffing, form
  fields, Markdown->HWPX). Description narrowed to 'read-only' to make
  the routing policy explicit.
- rhwp-edit (new): HWP binary editing via new k-skill-rhwp npm package
  that wraps the @rhwp/core WASM bindings as CLI subcommands: info,
  list-paragraphs, search, insert-text, delete-text, replace-all,
  create-table, set-cell-text, create-blank, and render.
- rhwp-advanced (new): guidance for the upstream Rust rhwp CLI
  (export-svg --debug-overlay, dump, dump-pages, ir-diff, thumbnail,
  convert) for layout debugging, IR inspection, version comparison,
  and read-only-document unlocking.

The new k-skill-rhwp package under packages/ ships a Node.js 18+ CLI
and library that round-trips HWP 5.x documents entirely in-process; no
Rust toolchain is required. It auto-installs the WASM-required
globalThis.measureTextWidth shim for headless Node, and all editing
subcommands always write to a distinct output path so the source file
is never mutated. HWPX save remains disabled per the upstream rhwp
#196 data-safety gate; HWPX input is accepted but output is written as
HWP 5.x.

Includes 24 node:test cases covering init, round-trip insertText,
replaceAll, createTable + setCellText, deleteText, searchText,
listParagraphs, renderPage (SVG/HTML), and full CLI arg-parse +
end-to-end round-trip through the CLI layer.

Wires README feature table (3 rows for hwp / rhwp-edit / rhwp-advanced),
docs/install.md optional-install list, docs/roadmap.md (marks HWP
advanced editing as shipped while keeping Windows/security-module
automation out of scope), docs/sources.md (adds rhwp upstream, CLI
source, @rhwp/core, @rhwp/editor, and rhwp #196 references), and the
root pack:dry-run script. Adds a Changesets entry for k-skill-rhwp
minor.

Closes #155.

*  feat: add k-dart skill for DART OpenAPI financial disclosures (#147)

*  feat: add k-dart skill for DART OpenAPI financial disclosures

금감원 전자공시시스템(DART) 14개 endpoint 조회 스킬 추가.
공시검색, 기업개황, 재무제표, 배당, 증자/감자, 전환사채, 소송 등.
API_K_DART 환경변수로 직접 호출하며 프록시 불필요.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs(k-dart): remove redundant korean-stock-search dependency

corpCode.xml 자체에 회사명·종목코드·고유번호가 모두 포함되어 있으므로
korean-stock-search 스킬 연계 절차 제거

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: add k-dart to README feature table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 📝 docs: add k-dart feature guide and fix README link format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* 🐛 fix(k-dart): correct status code 013, remove invalid corp_name filter, update daily limit

3개 critical 정확성 오류 수정:

1. 상태코드 013은 "조회된 데이터 없음"이며 "접근 권한 없음"이 아님 (012=접근 불가 IP).
   상태코드 표를 공식 명세 기준으로 재정리하고 누락된 014/021 코드 추가.
2. list.json은 corp_name 파라미터를 검색 필터로 지원하지 않음. SKILL.md의
   잘못된 진술과 corp_name을 사용한 misleading example을 제거하고, corp_code
   확보 절차를 거치도록 명시.
3. DART 일일 한도는 키당 10,000건이 아닌 20,000건이며 분당 약 1,000회
   throttle도 별도로 존재함. SKILL.md 및 docs/features/k-dart.md 모두 정정.

추가로 status: "013" 발생 시 사용자 안내 정책을 Response policy에 추가하고,
오픈API 이용현황 페이지 링크를 Notes에 추가함.

* 🐛 fix(k-dart): correct pifricDecsn endpoint, list.json corp_code optional, add empSttus, soften throttle claim

Codex adversarial review에서 식별된 4건의 추가 정확성 이슈 수정:

1. endpoint #8 유무상증자 결정이 잘못된 API에 연결됨. piicDecsn.json은
   유상증자 결정 (apiId=2020023)이며, 유무상증자 결정은 pifricDecsn.json
   (apiId=2020025)이 맞음. endpoint를 정정하고 piicDecsn (유상증자) 및
   fricDecsn (무상증자)와의 차이를 주의문으로 추가.

2. list.json의 corp_code 는 사실 선택사항이며, 미지정 시 검색 기간이
   3개월 이내로 제한될 뿐임. 이전 commit의 "corp_code 필수" 표현을
   정정하고, 두 가지 호출 패턴(corp_code 지정/미지정)을 Example
   requests에 모두 추가.

3. "분당 약 1,000회 throttle"은 공식 공개 가이드에 근거 없음
   (apiUsageStatusView.do 는 로그인 게이트). 공식 가이드가 명시한
   "일반적으로 20,000건 이상 요청 시 020 발생"만 유지하고 분당
   throttle 주장을 제거. 상태코드 표·Response policy도 일관되게 정리.

4. docs/features/k-dart.md가 "직원 현황" 기능을 광고하지만 SKILL.md
   에는 endpoint가 누락됨. empSttus.json (apiGrpCd=DS002,
   apiId=2019011)을 endpoint #8로 추가하고 example도 함께 등록.
   기존 endpoint 9~14는 10~15로 재번호.

* 🐛 fix(k-dart): align list.json signature and 020 caveat with official spec

Codex 2nd-round review에서 식별된 정확성 이슈 2건 수정:

1) list.json 요청 인자 signature가 공식 가이드(DS001/2019001)와 정확히
   일치하도록 재작성. crtfc_key 외 모든 파라미터가 선택사항임을 분명히
   하고, 각 파라미터의 default 동작과 pblntf_ty 값(A/B/C/D/E)도 명시.
   "corp_code 지정 시 기간 제한 없음" 표현은 공식 가이드가 보장하지
   않으므로 제거. corp_name이 공식 파라미터에 "존재하지 않는다"는
   사실로 수정 (이전: "지원하지 않는다").
   "corp_code 미지정 시 3개월 제한"은 외부 사용 사례에서 관찰된
   동작으로 약화 (공식 가이드에 별도 명시 없음).

2) 020 (요청 제한 초과) 안내가 일일 20,000건 cap 으로 너무 단정적
   해석되던 표현을 공식 메시지 그대로 보존: "일반적으로 20,000건
   이상 요청 시 발생하며, 키별로 별도 한도가 설정된 경우 다른
   임계치에서도 동일 코드가 반환될 수 있음". 상태코드 표·Response
   policy·Notes·docs/features/k-dart.md 모두 일관되게 정정.

* 🐛 fix(k-dart): mirror official Korean DS001/2019001 list.json spec exactly

Codex 3rd-round review에서 식별된 잔존 정확성 이슈 수정.

영어 가이드(DE001/AE00001)와 한국어 가이드(DS001/2019001)가 list.json
필수여부에서 다르게 표기되어 있어 이전 commit이 영어 가이드를 따랐으나,
한국어 공식 가이드를 직접 확인한 결과(opendart.fss.or.kr/guide/detail.do
?apiGrpCd=DS001&apiId=2019001) 다음이 한국어 공식 spec임을 확인:

- bgn_de, end_de는 Y(필수) (기본값은 명시되어 있으나 표기상 필수)
- corp_code 미지정 시 검색기간 3개월 제한은 공식 spec에 명시된 룰
  (외부 사용 사례 관찰이 아님)
- pblntf_ty는 A~J 전체 enum (정기공시/주요사항보고/발행공시/지분공시/
  기타공시/외부감사관련/펀드공시/자산유동화/거래소공시/공정위공시)
- page_count 기본값 10, 최대값 100
- corp_cls 복수 조건 불가
- last_reprt_at, sort, sort_mth 각 default 동작 명시

list.json 섹션을 공식 가이드 표와 1:1 일치하는 마크다운 표로 재작성.
3개월 제한 표현을 "외부 사례"에서 "공식 spec"으로 정정. Response policy
에 잔존하던 corp_name "지원하지 않는다" 표현도 "공식 파라미터에 존재하지
않는다"로 통일하여 #1 endpoint 섹션과 일관성 확보. docs/features/k-dart.md
도 동일하게 정정.

* 🐛 fix(k-dart): make list.json table 1:1 mirror of DS001/2019001 + unify corp_name wording

Codex 4th-round review가 식별한 잔존 이슈 2건 마무리.

1) list.json 파라미터 표를 공식 가이드 행 순서 그대로(crtfc_key,
   corp_code, bgn_de, end_de, last_reprt_at, pblntf_ty,
   pblntf_detail_ty, corp_cls, sort, sort_mth, page_no, page_count)
   재정리하고 공식 표의 모든 컬럼(요청키/명칭/타입/필수여부/값설명)을
   포함. page_no(1~n) / page_count(1~100, 기본10, 최대100) 범위
   값을 공식 표 그대로 표기. pblntf_detail_ty 값설명도 공식 표
   그대로 "(※ 상세 유형 참조: pblntf_detail_ty)"로 두고, 자주 쓰는
   코드 예시(A001/B001/F001/D001)는 표 아래 별도 단락으로 분리해
   표의 1:1 mirror 성격을 유지.

2) corp_name 관련 canonical 문장 "공식 요청 파라미터 표에
   corp_name 은 존재하지 않는다" 를 다음 3곳 모두 verbatim 일치
   시킴 (이전 commit에서 SKILL.md는 '않는다', docs/features는
   '않음' 으로 어미 차이가 잔존했음):
   - k-dart/SKILL.md #1 endpoint 섹션 주의문
   - k-dart/SKILL.md Response policy
   - docs/features/k-dart.md 에러/제약 섹션

* 🐛 fix(k-dart): unify corp_name canonical sentence verbatim + soften list.json table claim

Codex 5th-round review가 식별한 fine-grained 이슈 마무리.

1) corp_name canonical 문장을 self-contained 형태로 재작성하여
   3곳 모두 byte-for-byte 동일하게 통일:
   "DART OpenAPI list.json 의 공식 요청 파라미터 표에 corp_name 은
   존재하지 않는다."
   - SKILL.md #1 endpoint 섹션 주의문
   - SKILL.md Response policy
   - docs/features/k-dart.md 에러/제약 섹션
   이전에는 SKILL.md는 "위 공식 요청 파라미터 표에"로 docs/features는
   "list.json 공식 요청 파라미터 표에" 로 prefix가 달라 verbatim
   일치하지 않았음.

2) list.json 표 헤더 문구를 "공식 가이드 표를 그대로 옮긴 것"에서
   "공식 가이드 요청 인자 정리 (필수여부·기본값·허용값은 공식 표
   기준, 식별자는 코드 폰트로 표기)"로 약화. 마크다운 backtick 등
   포매팅 차이가 "1:1 mirror" 약속과 모순되지 않게 정확히 표현.

---------

Co-authored-by: hon2be <hon2be>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>

* WIP korean-slang-writing (#133): scaffold slang_search.py

* WIP korean-slang-writing (#133): add http + lookup scripts

* WIP korean-slang-writing (#133): add seed index of 30 curated trending slang

* WIP korean-slang-writing (#133): add test suite

* korean-slang-writing (#133): fix module-loader sys.modules registration

* korean-slang-writing (#133): add SKILL.md

* korean-slang-writing (#133): add feature doc

* korean-slang-writing (#133): register skill in README and root lint/test pipeline

* Revert out-of-scope HWP README edits to unblock CI

The prior commit 4c7877a on this branch renamed the HWP feature row to
'HWP 문서 조회/변환' and added two new rows ('HWP 문서 편집',
'HWP 레이아웃·IR 디버깅') pointing at docs/features/rhwp-edit.md and
docs/features/rhwp-advanced.md. Those docs do not exist on any branch
in this repo, and the rename violates scripts/skill-docs.test.js
assertions at lines 210, 223, 224, which caused the CI 'validate' job
to fail.

Those changes belong to a separate rhwp-edit/rhwp-advanced feature
effort (tracked elsewhere), not to issue #143 'naver-news-search'.
Revert README.md in both the feature table and the list section so the
only additions in this PR relative to origin/dev are the two
in-scope naver-news-search entries.

Verified by running 'npm run ci' locally (EXIT=0). skill-docs.test.js
now passes 110/110 (previously failed 2/110) and the full
k-skill-proxy suite remains 198/198 including the 14 naver-news tests.

* Update skill-docs tests to cover rhwp-edit, rhwp-advanced, and the k-skill-rhwp package

Pins the HWP table row rename to 'HWP 문서 조회/변환', asserts the new
'HWP 문서 편집' and 'HWP 레이아웃·IR 디버깅' README rows and their linked
feature docs, pins the new SKILL.md routing policy for rhwp-edit and
rhwp-advanced (k-skill-rhwp CLI + @rhwp/core for editing vs upstream
Rust CLI for layout/IR debugging), and asserts the k-skill-rhwp
package.json wiring (bin mapping, @rhwp/core dependency, Node 18+
engines, wasm-init shim + CLI bin files).

Per AGENTS.md rule, no assertion is added on the presence of any
.changeset/*.md file so the changeset release flow can consume the
rhwp-edit-skill.md entry without breaking CI at version-bump time.

Also captures the package-lock.json delta introduced by adding the
k-skill-rhwp workspace (pulls @rhwp/core@0.7.3 and its WASM binary).

Refs #155.

* Polish naver-news: preflight, link canonicalization, /health docs (#143)

Address the three non-blocking items flagged in the round 1/2 reviews. All
were explicitly deferred by the reviewer as "follow-up if the maintainer
wants" — picking them up now so the feature lands with a tighter surface.

1) Preflight 400 for start + display - 1 > 1000
   Naver's official news endpoint only exposes the first 1000 items
   (start 1..1000, display 1..100). Asking for start=1000 & display=100
   would send a request that silently returns no usable items, wasting
   an upstream quota call. Reject the combination before calling upstream
   with a 400 bad_request and a message that tells the caller which item
   the request would have needed and what the cap is. Boundary values
   (start + display - 1 === 1000) are still accepted.

2) Canonical link dedup
   The previous dedup key was link.toLowerCase(), which failed to merge
   the same article when Naver's redirect URLs differed only by query-param
   order, trailing slash, host-name casing, or fragment. Added
   canonicalizeLinkForDedup() which parses the URL, sorts search params by
   key, strips a single trailing pathname slash, drops the fragment, and
   lowercases the result — conservative on purpose so different paths or
   different query values stay as distinct articles. The visible
   items[].link value is still the original URL returned by Naver; only
   the dedup key is canonicalized.

3) Clarify the naverSearchApiConfigured vs naverNewsApiConfigured split
   The two flags currently evaluate the same boolean, but their semantic
   contracts differ: naverSearchApiConfigured reports "are the Naver
   Open API keys configured" (which is advisory for the shopping route
   since shopping has a BFF fallback), while naverNewsApiConfigured
   reports "is the news route operational end-to-end" (no fallback — 503
   when false). Hoist the shared expression into a local, and add a
   `/health 업스트림 플래그 의미` section to packages/k-skill-proxy/README.md
   documenting the split. Also update naver-news-search SKILL.md and
   docs/features/naver-news-search.md to mention the new preflight and
   the canonical-link dedup behavior.

TDD verification: added 4 new node:test cases exercising the boundary,
overflow, and URL-dedup paths; ran the full k-skill-proxy workspace
suite (202/202 pass) plus the root `npm run ci` (exit 0). Manual QA on
a proxy started from this commit reproduces every round-1 case plus the
new preflight: start=1000 & display=100 → 400 bad_request before
upstream; start=1000 & display=1 and start=901 & display=100 → 503 (or
200/401 depending on keys), confirming the boundary passes preflight.

* korean-slang-writing (#133): fix broken seed namuwiki URLs + add encoding invariant test

Reviewer flagged 4/30 seed namuwiki_url values returning HTTP 404 on live
Namu Wiki. These URLs are part of the documented response contract and get
surfaced directly to agents, so broken links are a functional bug, not a
cosmetic one.

Root causes per entry:
- 중꺾마: wrong 꺾 codepoint (U+AFFA 꿺 instead of U+AEBE 꺾).
- 아아: typo in aliased title (아이스 아메리칸노 instead of 아메리카노).
- 어쩔티비: missing 받침 (어쩌티비 instead of 어쩔티비).
- 당모치: encoding correct but no live Namu Wiki article exists; dropped.

Also fixes two separately-broken 중꺾마 example URLs in SKILL.md
(U+AFBE 꾾 instead of U+AEBE 꺾) — these were discovered while auditing
the seed and would have surfaced as 404 to agents following the example
snippets.

Adds two regression tests:
- test_each_seed_url_decodes_to_term_or_alias: decodes every seed URL's
  path segment and asserts it equals the term or one of its aliases.
  Catches Hangul-codepoint typos offline (no network dependency) and
  would have caught all 3 encoding bugs in this PR.
- test_no_seed_entry_points_at_known_missing_namuwiki_page: locks the
  당모치 drop so nobody re-adds an entry pointing at a page that does
  not exist on Namu Wiki.

Fixes the existing LookupNetworkTest assertion that was hard-coding the
broken URL — it now derives the expected URL via build_namuwiki_url()
so the test cannot drift out of sync with the helper again.

Verification:
- PYTHONPATH=.:scripts python3 -m unittest scripts.test_korean_slang_writing -> 40/40 pass
- Live GET with browser headers against all 29 remaining seed URLs -> 29/29 return 200
- npm run ci -> exit 0
- Manual QA: slang_search on 중꺾마, 어쩔티비, 아이스 아메리카노 returns
  correct URLs; slang_lookup live-fetches 중꺾마 and extracts the
  canonical title '중요한 것은 꺾이지 않는 마음'.

* korean-slang-writing (#133): extract summaries via h2 section anchor + og:description fallback

Namu Wiki's current HTML layout uses build-time-obfuscated CSS class
names (e.g. _36R8DWTn, OZVChh+l) and has no <article>/<main>/<section>
tags, so all six MAIN_CONTENT_CLASSES anchors fail to match and
extract_summary() returned empty with a 'Main content region not
detected' warning on every live page.

Replace the single class-based strategy with a three-tier fallback
chain that pins to progressively weaker but more structurally stable
anchors:

  1. First h2 section boundary. Namu Wiki articles consistently open
     with '<h2>1. 개요[편집]</h2>' and mark subsequent sections with
     numbered h2 headings. Extracting text between the first and
     second h2 reliably captures the overview section on every page
     sampled (중꺾마, 갓생, 럭키비키, 어쩔티비).
  2. MAIN_CONTENT_CLASSES / <article> - kept as a legacy fallback
     for older Namu Wiki layouts and for third-party fixtures.
  3. og:description meta tag - final safety net before returning
     empty, gives the agent at least a ~64-char preview when the
     article has unusual structure.

Strip '[편집]' edit-affordance markers and numbered section prefixes
(e.g. '1.2.') from the extracted text so headings don't leak through
as noise.

Live verification (text format):
  slang_lookup.py 중꺾마   -> Title + 286-char summary
  slang_lookup.py 갓생     -> Title + 96-char summary
  slang_lookup.py 럭키비키 -> Title + 59-char summary
  slang_lookup.py 어쩔티비 -> Title + 20-char summary

All previously-empty. Not-found / blocked / upstream-error paths and
exit codes are unchanged.

* korean-slang-writing (#133): harden extractor with numbered-h2 gate + category-nav strip

Implements the three non-blocking observations from PR #161 round-3 review:

1. Numbered-h2 gate (reviewer-flagged fragility):
   Refactored _extract_first_section_between_h2 to extract h2 inner text
   (stripping nested tags) and filter by '^\\s*\\d+(?:\\.\\d+)*\\.\\s+\\S'.
   Sidebar widgets like <h2>관련 문서</h2> or <h2>외부 링크</h2> can no longer
   anchor the extractor - only numbered section headers (1., 1.2., 2.3.4.) do.
   Handles live Namu Wiki structure where the number sits inside an <a> tag
   (<a>1.</a> <span>개요</span>), which the round-3 suggested regex-only gate
   missed. All 29 seed pages continue to produce valid summaries on live
   fetches.

2. Category-nav template strip (reviewer-flagged long-page noise):
   a. CATEGORY_NAV_RE strips the inline '[펼치기 · 접기]' marker plus its
      same-line aftermath (the category list items on the same line).
   b. DETAILS_PELCHIGI_RE strips the entire <details> block whose <summary>
      contains 펼치기. Namu Wiki today wraps category nav in exactly this
      structure, so the strip removes the full noise block (not just the
      marker line).
   꿀잼 summary drops from 3482 chars of category dump to 562 chars
   starting with the real definition '무언가가 매우 재미있다는 의미의 인터넷
   유행어'. Non-category <details> blocks (spoilers, footnotes) are
   preserved.

3. TDD + mutation coverage:
   6 new tests total: 2 numbered-h2 gate tests, 2 inline category-nav tests,
   1 <details>-block strip test, 1 <details>-keep test (negative case).
   All 6 were written first and confirmed RED against the round-2 baseline,
   then made GREEN after the implementation landed. Each fix path was also
   mutation-tested (revert regex, remove .sub line) to confirm the tests
   genuinely catch the target bug class.

Suite grows from 45 to 51 tests. All pass. npm run ci exits 0.

* rhwp-edit (#155): fix replace-all silent no-op and document body-only scope

Upstream @rhwp/core HwpDocument.replaceAll returns {ok:true, count:N} but
does not persist the mutation into exportHwp() serialization, so the output
bytes are byte-identical to the input. This is confirmed against
@rhwp/core@0.7.3 with SHA diffing and round-trip searchText.

Rewrite the Node wrapper replaceAll to compose engine primitives that do
persist: for each body paragraph, read the full text via getTextRange,
compute all non-overlapping match offsets in JS, then apply replaceText
right-to-left so earlier offsets are unaffected by length changes. This
restores the documented '2025 → 2026 일괄 치환' headline workflow.

Guard rails in the new replaceAll:
- Reject replacements containing newline or paragraph-break characters
  (\n, \r, U+2028, U+2029) with a descriptive error. Splitting a paragraph
  via replaceText would invalidate subsequent offsets.
- Non-overlapping semantics against the original text, so
  --query a --replacement aa against 'aaa' yields 'aaaaaa' (3 replacements)
  instead of looping on the freshly inserted 'a' characters.

Tighten the regression tests to assert content, not just length:
- Same-length replacement: output SHA must differ from input, searchText
  must find the replacement and must NOT find the original query.
- Longer-length replacement: paragraph length must grow by the correct
  amount and output SHA must differ.
- Shorter-length replacement: paragraph length must shrink by the correct
  amount and output SHA must differ.
- Empty replacement: deletes every match and output no longer contains
  the query.
- Replacement contains query (a→aa on aaa): expects count 3 and length 6.
- Zero matches: count 0, output still written.
- Case-sensitive flag skips mismatched case.
- Newline replacement is rejected synchronously.

Document the body-only scope of search and replace-all in the SKILL.md
routing policy, failure-modes, CLI USAGE text, feature doc, and package
README so users know to use set-cell-text for cell content. This matches
the upstream searchText contract, which does not descend into table cells,
headers, footers, or footnotes.

Add a matching regression assertion to scripts/skill-docs.test.js so the
body-only scope note cannot be silently removed from SKILL.md or the
feature doc.

Closes review round 1 for PR #162.

* rhwp-edit (#155): guard replace-all case-insensitive path against UTF-16 length-drift

Round 2 review flagged a latent Unicode safety bug: when replaceAll's
caseSensitive=false branch encounters characters whose toLowerCase()
changes UTF-16 length (e.g. Turkish İ U+0130 → i + U+0307 combining dot
above), offsets taken in the lowercased haystack drift by the expansion
delta for every subsequent match and silently corrupt the document.
Reviewer repro: 'ABCİABCİXYZ' + case-insensitive İ→Z reported
{ok:true,count:2} but rendered 'ABCZABCİZYZ' instead of 'ABCZABCZXYZ'
(the X at index 8 was corrupted while the second İ survived).

Surface a descriptive error rather than silently drift:
- findAllMatchOffsets: in the case-insensitive branch, verify that the
  paragraph text and the query each preserve UTF-16 length under
  toLowerCase; otherwise throw with an actionable message pointing the
  user to --case-sensitive or input normalization.
- This is strictly a safety guard: the 2025→2026 headline workflow,
  ASCII, Hangul, and every existing test are unaffected.

Tests (TDD red → green, net +4 in packages/k-skill-rhwp):
- 'replaceAll refuses case-insensitive matching when source text
  contains case-folding length-changing chars (e.g. Turkish İ U+0130)'
  reproduces the exact reviewer input and asserts rejection + no output
  file
- 'replaceAll refuses case-insensitive matching when the query itself
  contains case-folding length-changing chars' covers the query-side path
- 'replaceAll with --case-sensitive succeeds on inputs containing İ'
  confirms the guard only fires in the case-insensitive path and that
  case-sensitive produces ABCZABCZXYZ with no X corruption
- 'replaceAll case-insensitive still works for normal ASCII/Hangul'
  regression-guards against the fix over-rejecting the common case

Doc disclosure in all 4 surfaces called out by the reviewer:
- rhwp-edit/SKILL.md: new failure-mode bullet naming U+0130 specifically
- docs/features/rhwp-edit.md: Unicode 대소문자 무시 주의 paragraph
  under scenario 3 (replace-all)
- packages/k-skill-rhwp/README.md: extended Scope section
- packages/k-skill-rhwp/src/cli.js: USAGE 'Scope note' appended
- scripts/skill-docs.test.js: 2 new assertions locking the SKILL.md and
  feature-doc disclosure so they can't be silently removed
- .changeset: note the guard in the pending v0.1.0 release notes

Manual QA (end-to-end via the published CLI):
  $ k-skill-rhwp replace-all … --query İ --replacement Z
  → exit 1 + 'case-insensitive matching is unsafe because case folding
    changes the UTF-16 length …'
  → no output file written
  $ k-skill-rhwp replace-all … --query İ --replacement Z --case-sensitive
  → {ok:true,count:2}, render shows 'ABCZABCZXYZ', search İ ⇒ found:false
  $ replace-all '2025'→'2026' on '2025 2025 2025' ⇒ {ok:true,count:3}
  $ replace-all 'hello'→'hi' (case-insens.) on 'hello WORLD 안녕 HELLO'
    ⇒ {ok:true,count:2}

Verification:
- npm test --workspace k-skill-rhwp: 35 pass / 0 fail (+4 vs Round 2)
- node --test scripts/skill-docs.test.js: 114 pass / 0 fail
- npm run ci: exit 0 (lint + typecheck + all workspace tests +
  pack:dry-run + validate-skills.sh all green)

Refs PR #162 Round 2 review 'Non-blocking residual risk — Unicode
case-insensitive offset drift'.

* Document preflight 400 and full canonical dedup contract in naver-news feature doc

Round-3 review flagged two non-blocking doc-completeness nits in docs/features/naver-news-search.md:

- 실패 모드의 `400 bad_request` 항목이 preflight 케이스(`start + display - 1 > 1000`)를 누락하고 있었음. SKILL.md line 94 와 본문 line 128 의 '운영 팁' 과 대칭이 되도록 업데이트.
- 운영 팁의 canonical dedup 설명이 쿼리 파라미터 순서와 trailing slash 만 언급해서, 실제 구현(`canonicalizeLinkForDedup`)이 같이 정규화하는 host 대소문자와 URL fragment 를 빠뜨리고 있었음. test/naver-news.test.js line 273 이 네 가지 모두 검증하고 있으므로 공개 문서를 구현과 테스트에 맞춰 정정.

* feat: add catchtable-sniper skill (#146)

* feat: add catchtable-sniper skill

* Make the Catchtable skill loadable and discoverable

The submitted skill landed under skills/ without YAML frontmatter, which broke the repo's auto-discovery contract and Codex skill loading. Move it to the root-level skill layout, add the required metadata block, and document the feature in the main README plus a dedicated guide so the PR ships in a usable state.

Constraint: This repository auto-discovers skills from root-level directories only
Constraint: Skill manifests must start with YAML frontmatter for Codex to load them
Rejected: Keep the nested skills/catchtable-sniper layout | validate-skills and the repo's documented convention reject it
Rejected: Add only README links without a feature guide | would create a broken documentation target
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Future skill PRs should follow docs/adding-a-skill.md and place each skill in its own root directory
Tested: node --test scripts/skill-docs.test.js
Tested: ./scripts/validate-skills.sh
Tested: git diff --check
Not-tested: End-to-end Catchtable reservation completion on a logged-in account

---------

Co-authored-by: Jeffrey (Dongkyu) Kim <vkehfdl1@gmail.com>

---------

Co-authored-by: minsing-jin <ironman0722@naver.com>
Co-authored-by: hon2be <saysun34@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: choihyun-1110 <74152226+choihyun-1110@users.noreply.github.com>
2026-04-24 10:41:21 +09:00
..
data Release: Merge dev into main (#163) 2026-04-24 10:41:21 +09:00
scripts Release: Merge dev into main (#163) 2026-04-24 10:41:21 +09:00
SKILL.md Release: Merge dev into main (#163) 2026-04-24 10:41:21 +09:00