@zalkera/client 0.22.1 → 0.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * AEO 보장 표면 검사기 (memo119 §1.3 · T5).
3
+ * AEO 보장 표면 검사기.
4
4
  *
5
5
  * **개시된 사이트를 크롤해 보장 그래프가 실제로 나오는지 잰다.** 소스를 읽지 않는 것이 요점이다
6
6
  * (오너 전제 C): "이 섹션을 써라"는 디자인 자유를 깎지만 "개시된 페이지에서 이 그래프가 나오는가"는
@@ -9,7 +9,7 @@
9
9
  * 잣대는 백엔드 정본 `doc/contracts/aeo-surface-guarantees.json` 이고 이 파일은 그 정본의 **집행자**다.
10
10
  * 규범을 여기서 새로 만들지 않는다 — 표에 없는 것은 검사하지 않고, 표에 있는 것은 봐주지 않는다.
11
11
  *
12
- * ── 이 파일이 왜 client 패키지에 사는가(memo123 §6.1) ────────────────────────
12
+ * ── 이 파일이 왜 client 패키지에 사는가 ────────────────────────
13
13
  * 원래 이 검사기는 `storefront-template/scripts/` 에 살았다. 그런데 이제 **사람이 CLI 로 치는 것 말고도**
14
14
  * 두 자리에서 돌아야 한다: ⑴ 고객 zip(원래 자리) ⑵ serving-orchestrator 가 발행 직후 자동으로 재는 자리.
15
15
  * ⑵의 실행체는 "외부 의존 0 단일 `server.mjs`"라는 속성을 지켜야 해서 검사기를 자기 안에 넣을 수 없고,
@@ -17,7 +17,7 @@
17
17
  * 사본을 하나 더 두면 두 검사기가 조용히 갈라지므로(`lib/site-crawl.mjs` 의 KDoc 과 같은 논거) 정본을
18
18
  * 여기로 옮기고 template 의 `scripts/check-aeo-surfaces.mjs` 는 이 bin 을 부르는 **얇은 wrapper** 다.
19
19
  *
20
- * ── 잣대를 어디서 찾는가(memo122 §1.3-2) ────────────────────────────────────
20
+ * ── 잣대를 어디서 찾는가 ────────────────────────────────────
21
21
  * 이 검사기는 고객 zip 에 실려 나간다. 그런데 **잣대는 실려 나가지 않아서** 고객 기계에서는 기본 경로
22
22
  * (형제 `../backend`)가 없어 exit 2 로 죽어 있었다(2026-07-29 실측). 그래서 해석을 3단으로 둔다:
23
23
  *
@@ -42,7 +42,7 @@
42
42
  * (스토어프론트 체크아웃·고객 zip 안에서는 `npm run check:aeo -- …` 가 같은 bin 을 부른다)
43
43
  *
44
44
  * npx zalkera-aeo-check <사이트URL> --site-wide-only
45
- * **보장 주장이 없는 사이트**를 재는 모드(memo123 §6.3 B-1). `--category` 를 요구하지 않고
45
+ * **보장 주장이 없는 사이트**를 재는 모드. `--category` 를 요구하지 않고
46
46
  * siteWide 버킷(robots·sitemap·JSON-LD 절대 URL)만 잰다. 카테고리 required 는 아예 판정하지
47
47
  * 않는다 — 어휘 준수를 주장한 적 없는 사이트에 그 잣대를 들이대는 것은 오너 전제 A(디자인 자유)를
48
48
  * 어기는 일이다. 발행 직후 자동 검사(orchestrator)가 전 사이트에 쓰는 모드가 이것이다.
@@ -75,7 +75,7 @@ import {FUNCTIONAL_SEGMENTS, crawlPages, makeClassifier} from "../lib/site-crawl
75
75
  /**
76
76
  * 판정 형식이 바뀌면 올린다 — 스냅샷에 박혀서 "어느 잣대로 잰 판정인가"가 사후에도 남는다.
77
77
  *
78
- * 2 = `--site-wide-only` 모드와 스냅샷의 `mode` 필드 추가(memo123 §6.3). 기존 카테고리 판정의 잣대는
78
+ * 2 = `--site-wide-only` 모드와 스냅샷의 `mode` 필드 추가. 기존 카테고리 판정의 잣대는
79
79
  * 하나도 안 바뀌었지만, 스냅샷 형식에 필드가 늘었고 그 필드를 읽는 소비자(promote 게이트·백엔드 ingest)가
80
80
  * "구판 스냅샷인가"를 이 수로 가른다.
81
81
  */
@@ -106,7 +106,7 @@ const siteUrl = argv.find((a) => !a.startsWith("--") && /^https?:\/\//.test(a));
106
106
  const categories = flagAll("category");
107
107
  const printOnly = argv.includes("--print-guarantees");
108
108
  /**
109
- * 무주장 모드(memo123 §6.3 B-1). **카테고리 판정을 아예 하지 않는다** — 끄는 것이 아니라 안 하는 것이다.
109
+ * 무주장 모드. **카테고리 판정을 아예 하지 않는다** — 끄는 것이 아니라 안 하는 것이다.
110
110
  * 보장 어휘를 주장한 적 없는 사이트에 그 잣대를 대면, 그 사이트가 어떤 스택으로 짰든 red 가 쏟아지고
111
111
  * 그 red 는 사실이 아니라 **잣대를 잘못 고른 결과**다(오너 전제 A). 그래서 카테고리 축은 판정에서 빠지고
112
112
  * 사이트 축(robots·sitemap·절대 URL)만 남는다 — 그 셋은 어떤 디자인 선택과도 무관한 기계 가독 최소치다.
@@ -638,7 +638,7 @@ const results = [];
638
638
 
639
639
  if (siteWideOnly) {
640
640
  /**
641
- * 무주장 판정 — **사이트 축만**(memo123 §6.3). 카테고리 required·conditional·planned 는 물론
641
+ * 무주장 판정 — **사이트 축만**. 카테고리 required·conditional·planned 는 물론
642
642
  * `negative`(부정 보장)도 여기서는 안 잰다: 부정 보장은 "우리 어휘를 쓰는 팩이 자사 별점을 달지
643
643
  * 않는다"는 우리 팩의 규율이고, 남의 사이트가 자기 후기 마크업을 어떻게 다는지는 우리가 판정할
644
644
  * 자리가 아니다(전제 A). B-2 에서 주장 앵커가 생기면 그 사이트에는 잰다.
@@ -17,7 +17,7 @@
17
17
  * ⚠ I1 은 T4 에 오류로 **승격 예정**이다. I2 는 그 승격에 끌려가지 않도록 **싱크가
18
18
  * 분리돼 있다**(`clientIpDeclarationSink` — 승격 대상 아님). 합치지 마라.
19
19
  * W1 클라이언트 싱글턴 파일이 하나도 없음 → 서버 사이드 호출 패턴 미구현 의심.
20
- * C1 ISR-우선 게이트(memo31 §0-12) — SEO 라우트 page 가 per-page SSR(동적 렌더)을 강제하면 실패.
20
+ * C1 ISR-우선 게이트 — SEO 라우트 page 가 per-page SSR(동적 렌더)을 강제하면 실패.
21
21
  * codegen 산출물이 홈·목록·상세·콘텐츠 페이지를 동적SSR 로 만들면 CI 를 red 로 만들어 미배포.
22
22
  * 정당화된 예외는 파일에 `// zalkera-allow-dynamic: <이유>` 마커를 두면 경고로 강등된다.
23
23
  * C1b layout 폭발반경 게이트 — layout/template 이 **import 로 도달하는 서버 모듈**에서 동적 API 를
@@ -43,12 +43,12 @@
43
43
  * 심장을 한 번도 안 봤다(memo107 §4.2 가 '거짓 양성의 잔여'로 인정하고 미뤄 둔 자리).
44
44
  * S6 error — 남의 토큰 어휘(shadcn 기본 변수명) 클래스 사용. shadcn 소스를 발췌해 올 때 재작성 표를
45
45
  * 적용하지 않으면 `bg-card`·`text-muted-foreground` 같은 **정의되지 않은 토큰**을 참조해 색이 조용히
46
- * 빠진다. 우리 @theme 이 토큰 정본이고 shadcn 변수층은 반입하지 않는다(memo102 §4.1).
46
+ * 빠진다. 우리 @theme 이 토큰 정본이고 shadcn 변수층은 반입하지 않는다.
47
47
  * N1~N5 — **콘텐츠 파일 계약**(어휘 계약 rev 4 `contentFile` · 선언 `zalkera.content`). 사이트의
48
48
  * 얼굴(페이지·섹션·문구·이미지 선택·내비)이 `content/` 아래 json 으로 살 때, 그 파일이 조용히
49
49
  * 안 그려지는 형상을 잡는다. 아래 '콘텐츠 조건화' 참고 — **선언한 레포에서만 error** 다.
50
50
  *
51
- * ── 스택 조건화(memo75) ──────────────────────────────────────────
51
+ * ── 스택 조건화 ──────────────────────────────────────────
52
52
  * E1/E2/W1(헤드리스 계약·스택무관)·C1/C1b(Next App Router 전제)는 **상시** 적용한다.
53
53
  * S1~S5(Tailwind+토큰 전제)는 레포의 스택 선언에 따라 3모드로 게이팅한다(선두에서 1회 판정):
54
54
  *
@@ -57,7 +57,7 @@
57
57
  * inferred 선언 부재 + deps/devDeps 에 tailwindcss 있음 W W W W W (위생 모드)
58
58
  * none 그 외(다른 선언값·tailwindcss 도 없음) – – – – – (스킵)
59
59
  *
60
- * ── 콘텐츠 조건화(memo129 §1.3) ─────────────────────────────────
60
+ * ── 콘텐츠 조건화 ─────────────────────────────────
61
61
  * 같은 3모드를 `content` 축에 그대로 적용한다. **계약을 안 지킨 레포도 돌아야 한다**(오너 정본 전제 1
62
62
  * "강제할 수 없다") — 문구를 tsx 에 직접 든 레포도 개시·발행·codegen 이 전부 정상이고, 검사는 레포가
63
63
  * **스스로 선언했을 때만** 격상된다.
@@ -113,7 +113,7 @@
113
113
  * 서빙 이미지에 사본을 굽는 길은 **정본을 둘로 만든다**. 그 병은 이 코드베이스가 하루에도 몇 번씩
114
114
  * 겪는 것이다(계약 rev 가 올랐는데 검사기가 옛 키를 보던 일, 같은 관례를 두 곳이 다르게 구현하던 일).
115
115
  * 그래서 **`@zalkera/client` 가 bin 으로 배송한다** — `zalkera-aeo-check` 가 이미 그 자리에 있고
116
- * (memo123 §6.1), 소스 검사기도 같은 자리에 두면 정본이 하나로 남는다.
116
+ *, 소스 검사기도 같은 자리에 두면 정본이 하나로 남는다.
117
117
  *
118
118
  * 부수 효과가 하나 더 있다: **고객이 자기 손으로 같은 검사기를 돌릴 수 있다.** 종전에는 우리 예제
119
119
  * 레포에만 있어서, 자기 소스를 받아 고치는 사람이 쓸 방법이 없었다.
@@ -468,13 +468,13 @@ const cssFiles = [];
468
468
  let singletonFound = false;
469
469
 
470
470
  /**
471
- * 스타일 규약 모드 판정(memo75 §5) — validator 선두에서 1회. srcDir 상위 최근접 package.json 의
471
+ * 스타일 규약 모드 판정 — validator 선두에서 1회. srcDir 상위 최근접 package.json 의
472
472
  * `zalkera.styling` 선언을 읽는다(구 `oneque.styling` 도 수용 — 리네임 이행기):
473
473
  * declared : zalkera.styling === "tailwind-tokens" → 토큰 계약 모드(S2/S4 error 격상)
474
474
  * inferred : 선언 부재 + deps/devDeps 에 tailwindcss 있음 → 위생 모드(S 전부 warning)
475
475
  * none : 그 외(다른 선언값·tailwindcss 도 없음) → S 전부 스킵
476
476
  * package.json 을 못 찾거나 파싱 실패하면 none(안전 — S 안 들이댄다).
477
- * 백엔드는 스택을 모른다 — 선언은 레포 안에 살고 validator 가 현장에서 읽는다(memo75 §2).
477
+ * 백엔드는 스택을 모른다 — 선언은 레포 안에 살고 validator 가 현장에서 읽는다.
478
478
  */
479
479
  /** 모르는 `zalkera.styling` 값. 모드는 `none` 이지만 조용히 넘어가지 않는다. */
480
480
  let styleDeclarationNote = null;
@@ -574,7 +574,7 @@ function servingSink() {
574
574
  * 없고 상용 실측도 0건이라, 관문으로 켜도 아무도 안 막힌다. 그래서 그 둘만 `servingSink` 를 쓴다.
575
575
  */
576
576
  /**
577
- * **X 축(X1·X1p·X3) 전용 목적지 — 승격은 영구 금지다**(memo140 §6.5).
577
+ * **X 축(X1·X1p·X3) 전용 목적지 — 승격은 영구 금지다**.
578
578
  *
579
579
  * [clientIpSink] 와 지금은 같은 배열로 가지만 **함수가 다르다.** 이유는 [clientIpDeclarationSink] 와 같다:
580
580
  * 남이 I1 을 `servingSink()` 로 승격시키는 날, 싱크를 공유하면 X 축이 **딸려 올라간다.**
@@ -639,7 +639,7 @@ function clientIpDeclarationSink() {
639
639
  /**
640
640
  * **서빙 산출물 계약 축(O)의 목적지 — 관문 모드에서도 경고다. 승격은 영구 금지다.**
641
641
  *
642
- * 계약 자체는 사실이다(memo145 §0): *"잘커라가 서빙하는 소스는 빌드가 `.next/standalone` 자기완결
642
+ * 계약 자체는 사실이다: *"잘커라가 서빙하는 소스는 빌드가 `.next/standalone` 자기완결
643
643
  * 산출물을 내야 한다."* 우리 박스는 `next start` 가 아니라 그 산출물을 `node server.js` 로 띄운다.
644
644
  *
645
645
  * **그런데 이 검사기는 그 사실을 잴 수 없다.** 여기서 읽을 수 있는 것은 `next.config` 라는 **설정 문자열**
@@ -1321,7 +1321,7 @@ function missingClientIp(rawWithBom, jsx = false) {
1321
1321
  const STYLE_MODE = detectStyleMode(root);
1322
1322
 
1323
1323
  /**
1324
- * 콘텐츠 규약 모드 판정(memo129 §1.3) — [detectStyleMode] 와 **같은 모양**이다.
1324
+ * 콘텐츠 규약 모드 판정 — [detectStyleMode] 와 **같은 모양**이다.
1325
1325
  *
1326
1326
  * 다른 점 하나: 추론의 근거가 의존성이 아니라 **형상**이다(`content/pages/*.json` 실재). 콘텐츠는
1327
1327
  * 패키지로 안 오므로 deps 로는 알 수 없고, 그 형상을 갖췄다는 것 자체가 "이 계약을 쓰는 중"의 신호다.
@@ -2086,12 +2086,12 @@ function checkLayoutBlastRadius() {
2086
2086
 
2087
2087
  /**
2088
2088
  * C2 — 섹션 렌더러 커버리지. vendored `@zalkera/client` 의 SECTION_CONTRACT 와 SectionRenderer 의 case 를
2089
- * 대조한다. 어휘 사본이 넷이라 사람 주석 규약으로는 갈라짐을 못 막는다는 게 실측된 교훈이라(memo102 §6),
2089
+ * 대조한다. 어휘 사본이 넷이라 사람 주석 규약으로는 갈라짐을 못 막는다는 게 실측된 교훈이라,
2090
2090
  * 레포 안에서 확인 가능한 짝은 기계가 센다. 계약을 못 읽으면(구 client·미설치) **검사를 건너뛴다** —
2091
2091
  * BYO 레포에서 이 검사가 빌드를 막으면 안 되기 때문이다.
2092
2092
  */
2093
2093
  /**
2094
- * S8 — 표현 계약(L1) 배선 검사(memo109). **declared 전용**이다: S8 은 위생이 아니라 **선언의 이행 검사**라,
2094
+ * S8 — 표현 계약(L1) 배선 검사. **declared 전용**이다: S8 은 위생이 아니라 **선언의 이행 검사**라,
2095
2095
  * 계약을 자처하지 않은 레포에는 검사할 약속 자체가 없다(inferred·none 스킵 — S1~S5 와 게이팅이 다른 이유).
2096
2096
  *
2097
2097
  * 두 조각을 센다:
@@ -2437,7 +2437,7 @@ function checkContentContract() {
2437
2437
  // 필수 참조를 `requiredRefs`(이제 빈 배열)에서 이 키로 옮겼는데 이 검사기는 옛 키만 읽고 있었다 —
2438
2438
  // **참조가 하나도 없는 섹션이 통과했다**(실측). N5 가 존재 이유로 삼는 바로 그 결함이 무검출이었다.
2439
2439
  // 팩 게이트(`pack-preset.mjs`)는 anyOf 를 집행하고 있어 **어휘 사본 둘이 갈라진 상태**였다.
2440
- // ⚠ rev 6 에서 그 두 타입이 어휘에서 삭제돼 **오늘 이 축을 쓰는 타입은 0 이다**(memo142).
2440
+ // ⚠ rev 6 에서 그 두 타입이 어휘에서 삭제돼 **오늘 이 축을 쓰는 타입은 0 이다**.
2441
2441
  // 코드를 남기는 것은 계약 기계를 유지하기 위해서다 — 여기를 지우면 참조 필수 타입이 다시
2442
2442
  // 생기는 날 같은 무검출이 재발한다(그것이 이 주석이 기록하는 사고다).
2443
2443
  for (const group of spec?.requiredRefsAnyOf ?? []) {
@@ -2524,7 +2524,7 @@ function check(file) {
2524
2524
  // 문자열까지 지운 사본에서 찾으면 className 안의 값이 소거돼 영원히 못 잡는다).
2525
2525
  const text = stripComments(src);
2526
2526
 
2527
- // S6: 남의 토큰 어휘(shadcn 기본 변수명). 재작성 표(memo102 §4.1)의 좌변이 그대로 남아 있으면 잡는다.
2527
+ // S6: 남의 토큰 어휘(shadcn 기본 변수명). 재작성 표의 좌변이 그대로 남아 있으면 잡는다.
2528
2528
  // 우리에겐 정의가 없는 토큰이라 Tailwind 가 클래스를 만들지 않고 → 색이 조용히 빠진 채 배포된다.
2529
2529
  const s6 = styleSink("S6");
2530
2530
  if (s6) {
@@ -2941,7 +2941,7 @@ function judgeGuardPlacement(rawBody) {
2941
2941
  );
2942
2942
  }
2943
2943
 
2944
- // ── X1: 교차사이트 위조 가드 (memo118) ─────────────────────────────
2944
+ // ── X1: 교차사이트 위조 가드 ─────────────────────────────
2945
2945
  //
2946
2946
  // 변이 메서드(POST·PUT·PATCH·DELETE)를 export 하는 라우트 핸들러는 **자기 본문의 첫 구문으로**
2947
2947
  // `assertSameOrigin` 을 불러야 한다. 경로 목록이 아니라 **메서드**로 판정하는 이유는,
@@ -3378,7 +3378,7 @@ if (styleDeclarationNote !== null) {
3378
3378
  );
3379
3379
  }
3380
3380
 
3381
- checkCrossOriginGuards(); // X1·X2·X3 — 교차사이트 위조 가드(memo118).
3381
+ checkCrossOriginGuards(); // X1·X2·X3 — 교차사이트 위조 가드.
3382
3382
  checkPagesRouter(); // C1p·C1pa·X1p — Pages Router 좌표(App Router 전용이던 사각).
3383
3383
  checkEnvFiles(); // E3 — `.env*` 의 NEXT_PUBLIC_ 시크릿.
3384
3384
  checkServingOutputContract(); // O1 — 서빙 산출물 계약(경고 전용·관문 승격 영구 금지).
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 개시된 사이트를 훑는 최소 크롤러 — `snapshot-preview.mjs`(memo116 §3)와
2
+ * 개시된 사이트를 훑는 최소 크롤러 — `snapshot-preview.mjs`와
3
3
  * `check-aeo-surfaces.mjs`(memo119 T5)가 **같은 사본**을 쓴다.
4
4
  *
5
5
  * 원래 이 코드는 snapshot-preview 안에 살았다. 보장 검사기가 "산출물을 크롤해 판정한다"(memo119 오너
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zalkera/client",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "zalkera 헤드리스 CMS 공개 API 클라이언트 (테넌트 사이트용)",
5
5
  "license": "MIT",
6
6
  "author": "Credium Co., Ltd.",