mirror of
https://github.com/NomaDamas/k-skill.git
synced 2026-06-24 02:04:11 +00:00
Clarify donation verification links
Reject misleading 1365 URL contracts and keep item search categories aligned with the candidate that is being recommended. Constraint: PR #214 round-3 review required TDD fixes for multi-category candidate links, clean install docs, and evidence-safe 1365 wording. Rejected: Keep broad first-request category on every item URL | It mislabels later-category candidates in multi-category requests. Rejected: Preserve public baseUrl override | It conflicts with the official 1365 helper contract. Confidence: high Scope-risk: narrow Directive: Keep 1365 URLs framed as best-effort verification assists unless browser-observed 1365 search parameters are documented. Tested: npm test --workspace donation-place-search; node --test --test-name-pattern 'donation-place-search' scripts/skill-docs.test.js; npm run lint --workspace donation-place-search; npm run typecheck; npm run ci; node smoke for multi-category URLs, malformed limits, baseUrl rejection, and empty category. Not-tested: Live 1365 parameter behavior; headless HTTP remains documented as unreliable. Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
parent
be46d689d1
commit
c6e428d923
8 changed files with 82 additions and 30 deletions
|
|
@ -31,4 +31,4 @@ console.log(formatDonationRecommendationReport(result));
|
|||
|
||||
## 검증 표면
|
||||
|
||||
`nanumkorea.go.kr`는 1365 자원봉사/기부 통합 안내를 반환하므로, 스킬은 `www.1365.go.kr/dntn/main.do`를 최신 공식 확인 링크의 기준으로 사용한다. 1365 페이지가 headless HTTP에서 느리거나 빈 응답을 줄 수 있어 화면 스크래핑 대신 공식 검색 링크와 후보 공식 홈페이지를 함께 제시한다.
|
||||
`nanumkorea.go.kr`는 1365 자원봉사/기부 통합 안내를 반환하므로, 스킬은 `www.1365.go.kr/dntn/main.do`를 최신 공식 확인 진입점의 기준으로 사용한다. 1365 페이지가 headless HTTP에서 느리거나 빈 응답을 줄 수 있어 화면 스크래핑 대신 best-effort 확인 보조 링크와 후보 공식 홈페이지를 함께 제시하며, 후보별 등록 검증이 이미 완료됐다고 표현하지 않는다.
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ npx --yes skills add <owner/repo> \
|
|||
--skill k-schoollunch-menu \
|
||||
--skill korean-character-count \
|
||||
--skill court-auction-notice-search \
|
||||
--skill donation-place-search \
|
||||
--skill k-skill-cleaner
|
||||
```
|
||||
|
||||
|
|
@ -277,7 +278,7 @@ npm run ci
|
|||
### Node 패키지
|
||||
|
||||
```bash
|
||||
npm install -g kordoc pdfjs-dist kbo-game kbl-results kleague-results lck-analytics toss-securities hipass-receipt k-lotto coupang-product-search used-car-price-search cheap-gas-nearby public-restroom-nearby korean-law-mcp market-kurly-search daiso bunjang-cli court-auction-notice-search gongsijiga-search
|
||||
npm install -g kordoc pdfjs-dist kbo-game kbl-results kleague-results lck-analytics toss-securities hipass-receipt k-lotto coupang-product-search used-car-price-search cheap-gas-nearby public-restroom-nearby korean-law-mcp market-kurly-search daiso bunjang-cli court-auction-notice-search gongsijiga-search donation-place-search
|
||||
export NODE_PATH="$(npm root -g)"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: donation-place-search
|
||||
description: Use when the user asks where to donate, 기부처 조회, or donation place recommendations by Korean location and category. Recommend recipients with official 1365 verification links and never execute donations.
|
||||
description: Use when the user asks where to donate, 기부처 조회, or donation place recommendations by Korean location and category. Recommend recipients with best-effort 1365 verification-assist links and never execute donations.
|
||||
license: MIT
|
||||
metadata:
|
||||
category: utility
|
||||
|
|
@ -33,7 +33,7 @@ metadata:
|
|||
- `category`: 선택. 예: `아동`, `동물보호`, `환경`, `재난 구호`, `장애`, `노인`, `생계`, `의료`, `해외구호`
|
||||
- `limit`: 선택. 기본 5개
|
||||
|
||||
위치나 카테고리가 없으면 보수적으로 `전국`·`일반/종합` 후보와 공식 1365 검색 링크를 제공한다. 비대화형 자동화에서는 임의로 좁히지 말고 “입력 없음”을 명시한다.
|
||||
위치나 카테고리가 없으면 보수적으로 `전국`·`일반/종합` 후보와 1365 공식 확인 보조 링크를 제공한다. 비대화형 자동화에서는 임의로 좁히지 말고 “입력 없음”을 명시한다.
|
||||
|
||||
## Public access path discovered
|
||||
|
||||
|
|
@ -45,7 +45,7 @@ metadata:
|
|||
|
||||
### Search-link strategy
|
||||
|
||||
1365 pages can be slow or unavailable to headless HTTP clients, so the package does not depend on brittle screen scraping. It builds an official search link with the user’s location/category keywords, then ranks a curated fallback list locally:
|
||||
1365 pages can be slow or unavailable to headless HTTP clients, so the package does not depend on brittle screen scraping. It builds a best-effort official-entry/search-assist link with the user’s location/category keywords, then ranks a curated fallback list locally. The package does not assert that 1365 has already verified each returned candidate:
|
||||
|
||||
```js
|
||||
const { recommendDonationPlaces } = require("donation-place-search");
|
||||
|
|
@ -60,7 +60,7 @@ console.log(result.items);
|
|||
console.log(result.officialSearchUrl);
|
||||
```
|
||||
|
||||
The returned `officialSearchUrl` is the first place to verify current registration and campaign status before giving the final answer.
|
||||
The returned `officialSearchUrl` is a best-effort verification assist: open it as an official 1365 entry point, then confirm current registration and campaign status before giving the final answer.
|
||||
|
||||
## Workflow
|
||||
|
||||
|
|
@ -84,14 +84,14 @@ console.log(formatDonationRecommendationReport(result));
|
|||
NODE
|
||||
```
|
||||
|
||||
3. Open or cite the returned 1365 official search URL for latest verification when fresh browsing is available.
|
||||
3. Open or cite the returned best-effort 1365 verification-assist URL for latest verification when fresh browsing is available.
|
||||
4. Summarize 3–5 candidates, including:
|
||||
- 기부처명
|
||||
- 분야/카테고리
|
||||
- 지역 일치 여부 또는 전국 단위 여부
|
||||
- 왜 맞는지 한 줄
|
||||
- 공식 홈페이지
|
||||
- 1365 확인 링크
|
||||
- 1365 확인 보조 링크
|
||||
5. Add a caution that campaign status, donation receipt eligibility, and designated-use options must be checked on official pages before donating.
|
||||
|
||||
## Output fields
|
||||
|
|
@ -113,20 +113,20 @@ The npm helper returns:
|
|||
}
|
||||
],
|
||||
"officialSearchUrl": "https://www.1365.go.kr/dntn/main.do?...",
|
||||
"meta": { "source": "curated-fallback-plus-1365-official-search" }
|
||||
"meta": { "source": "curated-fallback-plus-1365-search-assist" }
|
||||
}
|
||||
```
|
||||
|
||||
## Done when
|
||||
|
||||
- 장소/카테고리 조건을 반영해 후보를 3–5개 이내로 정리했다.
|
||||
- 각 후보마다 공식 홈페이지 또는 1365 공식 확인 링크를 제공했다.
|
||||
- 각 후보마다 공식 홈페이지 또는 1365 확인 보조 링크를 제공했다.
|
||||
- 최종 기부 전 등록 상태, 모금 기간, 기부금영수증 가능 여부를 확인하라고 안내했다.
|
||||
- 자동 결제/후원 신청을 시도하지 않았다.
|
||||
|
||||
## Failure modes
|
||||
|
||||
- 1365 사이트가 느리거나 headless HTTP에서 timeout/empty page를 반환할 수 있다. 이 경우 공식 검색 URL과 후보 홈페이지를 제공하고 “최신 상태는 직접 확인 필요”라고 명시한다.
|
||||
- 1365 사이트가 느리거나 headless HTTP에서 timeout/empty page를 반환할 수 있다. 이 경우 확인 보조 URL과 후보 홈페이지를 제공하고 “최신 상태는 직접 확인 필요”라고 명시한다.
|
||||
- 위치 문자열이 행정구역으로 파싱되지 않으면 전국 후보 위주로 제안한다.
|
||||
- 지역·카테고리 모두 정확히 맞는 후보가 없으면 전국 단위 후보를 fallback으로 보여준다.
|
||||
- 특정 단체의 모금 캠페인, 지정기부 가능 여부, 기부금영수증 처리는 수시로 바뀌므로 package 내 curated 설명만으로 확정하지 않는다.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Recommend Korean donation recipients by location and donation category.
|
|||
|
||||
The package combines:
|
||||
|
||||
- a public 1365 Give Korea (`www.1365.go.kr`) search-link builder for latest official verification;
|
||||
- a public 1365 Give Korea (`www.1365.go.kr`) best-effort search-assist link builder for latest official verification;
|
||||
- deterministic category/location ranking over a small curated fallback set of well-known donation recipients;
|
||||
- Korean report formatting with cautions to verify current registration, campaign period, and donation receipt handling before donating.
|
||||
|
||||
|
|
@ -37,4 +37,4 @@ console.log(formatDonationRecommendationReport(result));
|
|||
|
||||
## Notes
|
||||
|
||||
Donation campaigns and registration status change frequently. Always open the returned official 1365 search link and the recipient's official homepage before recommending a final donation decision.
|
||||
Donation campaigns and registration status change frequently. Always treat returned 1365 URLs as best-effort verification assists: open the 1365 official entry/search page and the recipient's official homepage before recommending a final donation decision.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "donation-place-search",
|
||||
"version": "0.1.0",
|
||||
"description": "Recommend Korean donation recipients by location and category with official 1365 Give Korea search links and curated fallback data",
|
||||
"description": "Recommend Korean donation recipients by location and category with best-effort 1365 Give Korea search-assist links and curated fallback data",
|
||||
"license": "MIT",
|
||||
"main": "src/index.js",
|
||||
"files": [
|
||||
|
|
|
|||
|
|
@ -268,7 +268,11 @@ function normalizeCategoriesForSearch(input) {
|
|||
}
|
||||
|
||||
function build1365DonationSearchUrl(options = {}) {
|
||||
const url = new URL(options.baseUrl || OFFICIAL_1365_DONATION_URL);
|
||||
if (Object.prototype.hasOwnProperty.call(options, "baseUrl")) {
|
||||
throw new Error("baseUrl is not supported for 1365 donation search-assist links.");
|
||||
}
|
||||
|
||||
const url = new URL(OFFICIAL_1365_DONATION_URL);
|
||||
const [category] = normalizeCategoriesForSearch(options.category);
|
||||
const parts = [options.keyword, options.location].map((value) => String(value || "").trim()).filter(Boolean);
|
||||
url.searchParams.set("query", parts.join(" ") || CATEGORIES[category].label);
|
||||
|
|
@ -284,6 +288,10 @@ function buildCandidateSearchKeyword(place, keyword) {
|
|||
return `${place.name} ${baseKeyword}`;
|
||||
}
|
||||
|
||||
function selectCandidateSearchCategory(place, categories) {
|
||||
return categories.find((category) => place.categories.includes(category)) || categories[0];
|
||||
}
|
||||
|
||||
function scoreDonationPlace(place, categories, location) {
|
||||
const categoryMatch = categories.some((category) => place.categories.includes(category));
|
||||
const provinceMatch = !!location.province && place.locations.includes(location.province);
|
||||
|
|
@ -322,7 +330,7 @@ function recommendDonationPlaces(options = {}) {
|
|||
match,
|
||||
officialSearchUrl: build1365DonationSearchUrl({
|
||||
location: location.raw,
|
||||
category: categories[0],
|
||||
category: selectCandidateSearchCategory(place, categories),
|
||||
keyword: buildCandidateSearchKeyword(place, keyword)
|
||||
})
|
||||
}));
|
||||
|
|
@ -334,7 +342,7 @@ function recommendDonationPlaces(options = {}) {
|
|||
notes.push("정확한 지역 일치 기부처를 찾지 못해 전국 단위 기부처를 우선 제안했습니다.");
|
||||
}
|
||||
if (items.length === 0) {
|
||||
notes.push("조건에 맞는 기본 후보가 없어 1365 기부포털 공식 검색 링크로 최신 등록 기부처를 확인해야 합니다.");
|
||||
notes.push("조건에 맞는 기본 후보가 없어 1365 기부포털 확인 보조 링크로 최신 등록 기부처를 직접 확인해야 합니다.");
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -345,7 +353,7 @@ function recommendDonationPlaces(options = {}) {
|
|||
meta: {
|
||||
totalCandidates: ranked.length,
|
||||
limit,
|
||||
source: "curated-fallback-plus-1365-official-search",
|
||||
source: "curated-fallback-plus-1365-search-assist",
|
||||
notes
|
||||
}
|
||||
};
|
||||
|
|
@ -355,8 +363,11 @@ function normalizeLimit(value) {
|
|||
if (value === undefined || value === null || value === "") {
|
||||
return 5;
|
||||
}
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 20) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed)) {
|
||||
throw new Error("limit must be an integer between 1 and 20.");
|
||||
}
|
||||
if (parsed < 1 || parsed > 20) {
|
||||
throw new Error("limit must be between 1 and 20.");
|
||||
}
|
||||
return parsed;
|
||||
|
|
@ -377,7 +388,7 @@ function formatDonationRecommendationReport(result) {
|
|||
lines.push(`${index + 1}. ${item.name} — ${item.description}`);
|
||||
lines.push(` - 분야: ${item.categories.map((category) => CATEGORIES[category]?.label || category).join(", ")} / 범위: ${locality}`);
|
||||
lines.push(` - 공식 페이지: ${item.homepageUrl}`);
|
||||
lines.push(` - 1365 확인: ${item.officialSearchUrl}`);
|
||||
lines.push(` - 1365 확인 보조 링크: ${item.officialSearchUrl}`);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -386,7 +397,7 @@ function formatDonationRecommendationReport(result) {
|
|||
for (const note of result.meta.notes) {
|
||||
lines.push(`- ${note}`);
|
||||
}
|
||||
lines.push(`- 최신 모금 상태는 1365 공식 검색에서 다시 확인하세요: ${result.officialSearchUrl}`);
|
||||
lines.push(`- 1365 링크는 검색 보조용입니다. 최신 모금 상태는 1365 공식 페이지에서 직접 다시 확인하세요: ${result.officialSearchUrl}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ test("parseLocationQuery extracts Korean province and district hints conservativ
|
|||
});
|
||||
});
|
||||
|
||||
test("build1365DonationSearchUrl creates a public official search link without proxy auth", () => {
|
||||
test("build1365DonationSearchUrl creates a public 1365 search-assist link without proxy auth", () => {
|
||||
const url = new URL(build1365DonationSearchUrl({
|
||||
location: "서울 마포구",
|
||||
category: "animals",
|
||||
|
|
@ -86,7 +86,7 @@ test("recommendDonationPlaces ranks local category matches before broad national
|
|||
assert.ok(result.items.every((item) => item.categories.includes("animals")));
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces emits candidate-specific official 1365 search links", () => {
|
||||
test("recommendDonationPlaces emits candidate-specific 1365 search-assist links", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "서울 마포구",
|
||||
category: "동물",
|
||||
|
|
@ -130,6 +130,33 @@ test("recommendDonationPlaces supports multiple category filters and explains no
|
|||
assert.ok(result.meta.notes.some((note) => note.includes("정확한 지역 일치")));
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces uses each matched candidate category in multi-category item links", () => {
|
||||
const result = recommendDonationPlaces({
|
||||
location: "제주 서귀포시",
|
||||
category: ["장애", "노인"],
|
||||
limit: 4
|
||||
});
|
||||
|
||||
assert.deepEqual(result.category, ["disability", "elderly"]);
|
||||
result.items.forEach((item) => {
|
||||
const url = new URL(item.officialSearchUrl);
|
||||
const urlCategory = url.searchParams.get("category");
|
||||
assert.ok(item.categories.includes(urlCategory), `${item.name} URL category ${urlCategory} must match candidate categories`);
|
||||
});
|
||||
});
|
||||
|
||||
test("build1365DonationSearchUrl does not allow overriding the official 1365 endpoint", () => {
|
||||
assert.throws(
|
||||
() => build1365DonationSearchUrl({ baseUrl: "https://example.com/dntn/main.do" }),
|
||||
/baseUrl is not supported/
|
||||
);
|
||||
});
|
||||
|
||||
test("recommendDonationPlaces rejects malformed non-integer limits", () => {
|
||||
assert.throws(() => recommendDonationPlaces({ limit: "2abc" }), /limit must be an integer/);
|
||||
assert.throws(() => recommendDonationPlaces({ limit: "1.9" }), /limit must be an integer/);
|
||||
});
|
||||
|
||||
test("formatDonationRecommendationReport creates a concise Korean report with verification cautions", () => {
|
||||
const result = recommendDonationPlaces({ location: "서울", category: "아동", limit: 2 });
|
||||
const report = formatDonationRecommendationReport(result);
|
||||
|
|
|
|||
|
|
@ -671,8 +671,6 @@ test("ktx-booking helper python regression tests pass", () => {
|
|||
);
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("repository docs advertise the geeknews-search skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -1320,6 +1318,25 @@ test("README main capability table advertises the donation-place-search skill",
|
|||
assert.match(tableSection, /docs\/features\/donation-place-search\.md/);
|
||||
});
|
||||
|
||||
test("donation-place-search install docs include the skill and npm helper", () => {
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
||||
assert.match(install, /--skill donation-place-search/);
|
||||
assert.match(install, /npm install -g .*donation-place-search/);
|
||||
});
|
||||
|
||||
test("donation-place-search docs describe 1365 links as best-effort verification assists", () => {
|
||||
const skill = read(path.join("donation-place-search", "SKILL.md"));
|
||||
const packageReadme = read(path.join("packages", "donation-place-search", "README.md"));
|
||||
const featureDoc = read(path.join("docs", "features", "donation-place-search.md"));
|
||||
const packageJson = readJson(path.join("packages", "donation-place-search", "package.json"));
|
||||
|
||||
for (const doc of [skill, packageReadme, featureDoc, packageJson.description]) {
|
||||
assert.match(doc, /best-effort|보조|assist/i);
|
||||
assert.doesNotMatch(doc, /candidate-verified|후보별 검증 완료/);
|
||||
}
|
||||
});
|
||||
|
||||
test("repository docs advertise the kbl-results skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -1487,8 +1504,6 @@ test("blue-ribbon-nearby package README stays aligned with the location-first an
|
|||
assert.match(packageReadme, /searchNearbyByLocationQuery/);
|
||||
});
|
||||
|
||||
|
||||
|
||||
test("repository docs advertise the kakao-bar-nearby skill across the documented surfaces", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -2569,7 +2584,6 @@ test("repository docs advertise the han-river-water-level skill and rollout-pend
|
|||
assert.match(roadmap, /한강 수위 정보 조회 스킬 출시/);
|
||||
});
|
||||
|
||||
|
||||
test("repository docs advertise the MFDS drug and food safety skills", () => {
|
||||
const readme = read("README.md");
|
||||
const install = read(path.join("docs", "install.md"));
|
||||
|
|
@ -3567,7 +3581,6 @@ test("corporate-registration-consulting skill covers court registry workflow, ta
|
|||
assert.match(sources, /law\.go\.kr/);
|
||||
});
|
||||
|
||||
|
||||
test("iros-registry-automation skill documents safe IROS registry certificate automation and upstream credit", () => {
|
||||
const skillPath = path.join(repoRoot, "iros-registry-automation", "SKILL.md");
|
||||
const featureDocPath = path.join(repoRoot, "docs", "features", "iros-registry-automation.md");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue