Document the verified public tracking outputs explicitly

Add aligned CJ and 우체국 sample public-output blocks to both delivery-tracking docs and extend the docs regression so those examples stay synchronized with the normalized non-PII contract. The new examples are dated live smoke-test captures, giving reviewers and users a concrete expected result without relying on raw carrier fields.

Constraint: feature/#4 already matched dev, so the follow-up needed a small reviewable change to reopen PR work on the same branch
Constraint: Keep verification offline in CI and avoid new runtime dependencies
Rejected: Add live carrier calls to CI | external endpoints are flaky and would make the docs regression nondeterministic
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Refresh both dated sample-output blocks together if the smoke-test invoices ever change their public tracking history
Tested: node --test scripts/skill-docs.test.js
Tested: npm run ci
Tested: python3 /tmp/cj_verify.py
Tested: python3 /tmp/epost_verify.py
Tested: npx --yes skills add . --list
Tested: git diff --check
Not-tested: Automated live carrier verification in CI
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-03-27 02:44:48 +09:00
commit 21f036e942
3 changed files with 198 additions and 0 deletions

View file

@ -154,6 +154,42 @@ PY
rm -f "$tmp_body" "$tmp_cookie" "$tmp_json"
```
#### CJ 공개 출력 예시
아래 값은 2026-03-27 기준 live smoke test(`1234567890`)에서 확인한 정규화 결과다.
```json
{
"carrier": "cj",
"invoice": "1234567890",
"status_code": "91",
"status": "배달완료",
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"event_count": 3,
"recent_events": [
{
"timestamp": "2026-03-10 03:01:45",
"location": "청원HUB",
"status_code": "44",
"status": "상품이동중"
},
{
"timestamp": "2026-03-21 10:53:19",
"location": "경기광주오포",
"status_code": "82",
"status": "배송출발"
},
{
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"status_code": "91",
"status": "배달완료"
}
]
}
```
추가 smoke test 로는 `000000000000` 도 사용할 수 있다.
CJ 응답은 `parcelResultMap.resultList` 가 비어 있어도 `parcelDetailResultMap.resultList` 쪽에 이벤트가 들어올 수 있으므로, 상세 이벤트 배열을 우선 본다. published 예시는 공통 결과 스키마(`carrier`, `invoice`, `status`, `timestamp`, `location`, `event_count`, `recent_events`, 선택적 `status_code`)에 맞춰 비식별 필드만 남기고, 담당자 이름·연락처가 섞일 수 있는 `crgNm` 원문은 그대로 보여주지 않는다.
@ -259,6 +295,33 @@ PY
rm -f "$tmp_html"
```
#### 우체국 공개 출력 예시
아래 값은 2026-03-27 기준 live smoke test(`1234567890123`)에서 확인한 정규화 결과다.
```json
{
"carrier": "epost",
"invoice": "1234567890123",
"status": "배달완료",
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"event_count": 2,
"recent_events": [
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달준비"
},
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달완료"
}
]
}
```
우체국 기본정보 테이블은 `등기번호`, `보내는 분/접수일자`, `받는 분`, `수령인/배달일자`, `취급구분`, `배달결과` 순서를 사용하고, 상세 이벤트는 `processTable` 아래 `날짜 / 시간 / 발생국 / 처리현황` 행을 읽으면 된다. published 예시는 CJ와 같은 공통 결과 스키마(`carrier`, `invoice`, `status`, `timestamp`, `location`, `event_count`, `recent_events`)에 맞춰 배송 상태에 필요한 값만 남기고, 이벤트 location에 섞일 수 있는 `TEL` 번호 조각도 제거한 뒤 수령인/상세 메모 원문은 그대로 노출하지 않는다.
### 3. Normalize for humans

View file

@ -105,6 +105,42 @@ PY
rm -f "$tmp_body" "$tmp_cookie" "$tmp_json"
```
### CJ 공개 출력 예시
아래 값은 2026-03-27 기준 live smoke test(`1234567890`)에서 확인한 정규화 결과다.
```json
{
"carrier": "cj",
"invoice": "1234567890",
"status_code": "91",
"status": "배달완료",
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"event_count": 3,
"recent_events": [
{
"timestamp": "2026-03-10 03:01:45",
"location": "청원HUB",
"status_code": "44",
"status": "상품이동중"
},
{
"timestamp": "2026-03-21 10:53:19",
"location": "경기광주오포",
"status_code": "82",
"status": "배송출발"
},
{
"timestamp": "2026-03-21 12:22:13",
"location": "경기광주오포",
"status_code": "91",
"status": "배달완료"
}
]
}
```
CJ는 JSON 응답이므로 `parcelDetailResultMap.resultList` 를 기준으로 상태를 읽는 편이 가장 안정적이다. 문서 예시는 공통 결과 스키마(`carrier`, `invoice`, `status`, `timestamp`, `location`, `event_count`, `recent_events`, 선택적 `status_code`)만 남기고, 담당자 이름이나 휴대폰 번호가 포함될 수 있는 `crgNm` 원문은 그대로 출력하지 않는다.
## 우체국 예시
@ -204,6 +240,33 @@ PY
rm -f "$tmp_html"
```
### 우체국 공개 출력 예시
아래 값은 2026-03-27 기준 live smoke test(`1234567890123`)에서 확인한 정규화 결과다.
```json
{
"carrier": "epost",
"invoice": "1234567890123",
"status": "배달완료",
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"event_count": 2,
"recent_events": [
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달준비"
},
{
"timestamp": "2025.12.04 15:13",
"location": "제주우편집중국",
"status": "배달완료"
}
]
}
```
우체국은 HTML 응답이라 기본정보 `table_col` 과 상세 `processTable` 을 파싱해야 한다. 문서 예시는 CJ와 같은 공통 결과 스키마(`carrier`, `invoice`, `status`, `timestamp`, `location`, `event_count`, `recent_events`)만 남기고, 이벤트 location에 섞일 수 있는 `TEL` 번호 조각도 제거한 뒤 수령인/상세 메모 원문은 그대로 출력하지 않는다.
## 결과 정리 기준

View file

@ -35,6 +35,14 @@ function findRecentEventsBlock(doc, carrier) {
return block;
}
function findJsonFenceAfterLabel(doc, label) {
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const match = doc.match(new RegExp(`${escaped}[\\s\\S]*?\\\`\\\`\\\`json\\n([\\s\\S]*?)\\n\\\`\\\`\\\``));
assert.ok(match, `expected JSON example after "${label}"`);
return JSON.parse(match[1]);
}
test("root npm test script includes the skill docs regression suite", () => {
const packageJson = JSON.parse(read("package.json"));
@ -329,3 +337,67 @@ test("delivery-tracking published examples lock a shared normalized non-PII sche
assert.doesNotMatch(skill, /"message":\s*latest\.get\("crgNm"\)/);
assert.doesNotMatch(featureDoc, /print\(\{\s*"tracking_no"/);
});
test("delivery-tracking docs publish aligned sample normalized outputs for both carriers", () => {
const skill = read(path.join("delivery-tracking", "SKILL.md"));
const featureDoc = read(path.join("docs", "features", "delivery-tracking.md"));
const cjSkillOutput = findJsonFenceAfterLabel(skill, "CJ 공개 출력 예시");
const cjFeatureOutput = findJsonFenceAfterLabel(featureDoc, "CJ 공개 출력 예시");
const epostSkillOutput = findJsonFenceAfterLabel(skill, "우체국 공개 출력 예시");
const epostFeatureOutput = findJsonFenceAfterLabel(featureDoc, "우체국 공개 출력 예시");
assert.deepEqual(cjSkillOutput, cjFeatureOutput, "CJ sample output must stay aligned across docs");
assert.deepEqual(epostSkillOutput, epostFeatureOutput, "ePost sample output must stay aligned across docs");
assert.deepEqual(Object.keys(cjSkillOutput), [
"carrier",
"invoice",
"status_code",
"status",
"timestamp",
"location",
"event_count",
"recent_events",
]);
assert.equal(cjSkillOutput.carrier, "cj");
assert.equal(cjSkillOutput.invoice, "1234567890");
assert.equal(typeof cjSkillOutput.status, "string");
assert.equal(typeof cjSkillOutput.timestamp, "string");
assert.equal(typeof cjSkillOutput.location, "string");
assert.equal(typeof cjSkillOutput.event_count, "number");
assert.ok(Array.isArray(cjSkillOutput.recent_events));
assert.ok(cjSkillOutput.recent_events.length > 0 && cjSkillOutput.recent_events.length <= 3);
for (const event of cjSkillOutput.recent_events) {
assert.deepEqual(Object.keys(event), ["timestamp", "location", "status_code", "status"]);
}
assert.deepEqual(Object.keys(epostSkillOutput), [
"carrier",
"invoice",
"status",
"timestamp",
"location",
"event_count",
"recent_events",
]);
assert.equal(epostSkillOutput.carrier, "epost");
assert.equal(epostSkillOutput.invoice, "1234567890123");
assert.equal(typeof epostSkillOutput.status, "string");
assert.equal(typeof epostSkillOutput.timestamp, "string");
assert.equal(typeof epostSkillOutput.location, "string");
assert.equal(typeof epostSkillOutput.event_count, "number");
assert.ok(Array.isArray(epostSkillOutput.recent_events));
assert.ok(epostSkillOutput.recent_events.length > 0 && epostSkillOutput.recent_events.length <= 3);
for (const event of epostSkillOutput.recent_events) {
assert.deepEqual(Object.keys(event), ["timestamp", "location", "status"]);
assert.ok(!JSON.stringify(event).includes("TEL"));
}
for (const output of [cjSkillOutput, epostSkillOutput]) {
const serialized = JSON.stringify(output);
assert.ok(!serialized.includes("crgNm"));
assert.ok(!serialized.includes("sender"));
assert.ok(!serialized.includes("receiver"));
assert.ok(!serialized.includes("delivered_to"));
}
});