Commit graph

360 commits

Author SHA1 Message Date
uppinote
9c8bafa42f 1.0.2 2026-06-21 00:53:27 +09:00
uppinote
8796c7fa46
Merge pull request #113 from uppinote20/refactor/assert-never-helper
refactor: unify exhaustiveness guards behind assertNever helper
2026-06-20 20:07:31 +09:00
uppinote
9c4c7b4f13 refactor: route exhaustiveness guards through assertNever
- sync mode/scope, conflict mode, setup-requirement kind, credential type 5개 throw 가드 → assertNever(x, label) (에러 메시지 보존)
- migration.ts: 가짜 `type as never` → 진짜 `credType satisfies never` 가드로 업그레이드 (컴파일타임 exhaustiveness 확보, return undefined 동작 유지)
- settings-tab filterMode는 non-throw fallback이라 satisfies never 유지
2026-06-20 20:03:34 +09:00
uppinote
2181997e27 refactor(utils): add shared assertNever exhaustiveness helper
- assertNever(value: never, label?): never — never 파라미터가 컴파일타임 exhaustiveness를 강제, 런타임 throw는 방어망
- src/utils/index.ts 배럴에서 export
- 단위 테스트 4개 (default/custom label, non-string 값 stringify, typed default 분기 컴파일 계약)
2026-06-20 20:03:34 +09:00
uppinote
0ba8cea21a
Merge pull request #112 from uppinote20/feat/cache-recent-setup-verification
feat: cache last-verified-at to skip redundant verifySetup probe
2026-06-20 17:57:10 +09:00
uppinote
831c69cba3 refactor(settings): throw on unknown credential type in fingerprint guard
- getCredentialVerificationFingerprint의 default 분기를 [_exhaustive] 반환 → throw
- 코드베이스 exhaustiveness 관례(§6.3) 일치, [undefined].map(trim) footgun 제거
2026-06-20 17:51:55 +09:00
uppinote20
7aa0934908 Avoid redundant setup probes after recent credential verification
Cache a short-lived credential verification fingerprint in the settings form so Test/Verify followed by Save does not immediately repeat the same setup probe.

Constraint: #93 targets the common Verify/Test-then-Save flow while preserving fail-closed save validation.
Rejected: skipping save-time verification unconditionally | stale or edited credentials would be persisted without setup validation.
Confidence: high
Scope-risk: moderate
Directive: Invalidate the cached verification whenever credential inputs change or a setup probe fails/needs setup.
Tested: npm run test:run; npm run typecheck; npm run lint; npm run build
Not-tested: Manual browser/Obsidian network panel probe count against a live Supabase project
2026-06-20 17:40:37 +09:00
uppinote
00e617c860
Merge pull request #111 from uppinote20/test/field-mapper-parity
test: add cross-provider FieldTypeMapper parity contract
2026-06-20 17:39:40 +09:00
uppinote
2f4f86a2c8 test(field-mapper): wire @tested markers to parity contract
- mapper-parity.test.ts의 @covers 4개에 대응하는 @tested 역링크 추가
- airtable/seatable/supabase field-mapper + provider-registry → @tested mapper-parity.test.ts
- 양방향 테스트 커버리지 링크 무결성 완성
2026-06-20 17:31:07 +09:00
uppinote20
b07341701b test(field-mapper): add cross-provider parity contract
provider별 테스트는 각자의 불변식만 검증할 뿐, 새 provider가 공통 계약을
어겨도 잡지 못하는 갭이 있었다. provider-registry에 등록된 모든 매퍼를
동적으로 탐색해 공유 계약을 한곳에서 검증한다.

- filename-safe ⊆ subfolder-safe, subfolder-safe 타입이 안전한 표준 타입으로
  매핑되는지, 프로토타입 체인 이름(toString 등)의 fail-closed,
  열거 메서드의 sorted+unique 검증
- airtable/seatable/supabase 존재를 floor guard로 고정해 등록 회귀 시
  it.each([])가 빈 통과(vacuous pass)하는 것을 방지
2026-06-20 17:17:30 +09:00
uppinote20
ec8a33f4f6 refactor(field-mapper): sort filename-safe types for deterministic output
airtable/seatable의 FILENAME_SAFE_TYPES 배열만 정의 순서가 정렬되지 않은
상태로 남아 있었다. settings-tab은 이 배열을 join(', ')으로 "Filtered to: …"
안내 문구에 그대로 노출하므로, 정렬되지 않은 순서가 그대로 사용자에게 보이는
비결정적 출력이 된다.

- 두 배열을 알파벳순으로 정렬 — 이미 모든 SUBFOLDER_SAFE_TYPES(3개 provider)와
  supabase의 FILENAME_SAFE_TYPES/READ_ONLY_TYPES가 따르던 컨벤션과 일치
- FieldTypeMapper 인터페이스 JSDoc에 getFilenameSafeTypes/getSubfolderSafeTypes의
  sorted+deduplicated 계약 명시 — 테스트로만 암묵 보장되던 불변식을 문서화
- 두 매퍼 단위 테스트의 기대 순서 동기화
2026-06-20 17:17:30 +09:00
uppinote
c21869d131
Merge pull request #109 from uppinote20/chore/typescript-5-upgrade
chore: bump TypeScript 4.7.4 to 5.9.3
2026-06-20 16:02:09 +09:00
uppinote20
ec222e056c Unblock modern TypeScript syntax in the toolchain
Upgrade the project to TypeScript 5.9 and the matching @typescript-eslint 8 line, then replace the local exhaustiveness workaround with satisfies never.

Constraint: #102 asks for the TypeScript 4.7.4 pin to move to the latest stable 5.x while keeping Obsidian type resolution working.
Rejected: upgrading ESLint itself | @typescript-eslint 8.61.0 supports the existing ESLint 8.57.1 range and avoids unnecessary config churn.
Confidence: high
Scope-risk: moderate
Directive: Keep TypeScript pinned to the verified 5.x version unless a later migration reruns the full validation suite.
Tested: npm ls typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint --depth=0; npm run test:run; npm run typecheck; npm run lint; npm run build
Not-tested: e2e suite requiring a live Obsidian remote-debugging session
2026-06-20 15:08:42 +09:00
uppinote
eebdc9d588
Merge pull request #108 from uppinote20/fix/object-shaped-field-push-guard
fix: prevent frontmatter edits from overwriting structured remote fields
2026-06-20 12:18:20 +09:00
uppinote
28f61634f4 docs: clarify push-safety comments after isPushable migration
- supabase-field-mapper: 제거된 "" → null coercion을 설명하던 stale 주석을
  isPushable/OBJECT_SHAPED_TYPES 기반 제외 메커니즘으로 갱신
- airtable-field-mapper: button/aiText/externalSyncSource/lookup이
  READ_ONLY_TYPES와 의도적으로 중복되는 이유(belt-and-suspenders) 명시
- frontmatter-parser: cachedFields가 undefined면 isPushable 게이트가 우회됨을
  extractSyncableFields JSDoc에 문서화
- PR #108 리뷰 반영
2026-06-20 11:43:58 +09:00
uppinote
7aded2cda4 fix: base Supabase view-guard on writability, not pushability
- loadWritableColumns가 isPushable로 맵을 구성하면서, object-shaped(jsonb 등)
  writable 컬럼만 가진 테이블이 "non-updatable view"로 오판돼 batch 전체가
  실패하던 회귀를 수정
- view-guard는 writable(!isReadOnly) 컬럼 수로 판정하도록 되돌리고, isPushable은
  payload 조립 시점에만 적용해 object-shaped 컬럼을 upsert body에서 omit
  (원격 구조값 보존)
- object-shaped 컬럼만 있는 테이블이 view 오판 없이 성공하고 해당 필드를
  omit함을 검증하는 회귀 테스트 추가
- PR #108 Codex 리뷰(P2) 반영
2026-06-20 11:43:58 +09:00
uppinote20
b4bd61854c Prevent frontmatter edits from overwriting structured remote fields
Introduce pushability as a stricter mapper contract than read-only checks so provider metadata can exclude object-shaped API values before composing push payloads.

Constraint: #101 reports object-shaped non-attachment provider values cannot be reconstructed from YAML frontmatter.
Rejected: using isReadOnly as the push guard | object-shaped writable types pass that check.
Confidence: high
Scope-risk: moderate
Directive: Keep isPushable stricter than isReadOnly when adding provider types with object or array runtime values.
Tested: npm run test:run; npm run typecheck; npm run lint; npm run build
Not-tested: Live provider API push against Airtable, SeaTable, or Supabase
2026-06-12 10:07:23 +09:00
uppinote
856a1bc7a6
Merge pull request #106 from uppinote20/chore/ci-node24-actions
chore: bump CI artifact and attest actions to Node 24 runtimes
2026-05-29 21:35:35 +09:00
uppinote
a9561d0325 ci(release): bump artifact and attest actions to Node 24 runtimes
GitHub은 2026-06-02부터 Node.js 20 기반 액션을 Node.js 24로 강제 실행하고,
Node 20 런타임은 2026-09-16에 러너에서 제거된다. release.yml의 SHA-핀 액션
3종이 아직 Node 20에서 동작해 deprecation 경고가 발생했다.

- upload-artifact v4.6.2 → v7.0.1 (node24, 기본 archive 동작 유지)
- download-artifact v4.3.0 → v8.0.1 (node24, name 기반 다운로드 경로 불변,
  해시 불일치 시 기본 error로 강화 — supply-chain 하드닝과 부합)
- attest-build-provenance v2.4.0 → v4.1.0 (node24 서브액션, subject-path 입력 유지)

각 핀이 해당 버전 태그의 커밋 SHA와 일치함을 교차검증했고, 우리 사용 패턴
(name 기반 업로드/다운로드, subject-path attest)은 메이저 점프의 breaking
change에 영향받지 않는다. 러너는 ubuntu-latest로 최소 요구 버전(2.327.1)을
항상 충족한다.
2026-05-29 21:22:06 +09:00
uppinote
0966d3586d 1.0.1 2026-05-29 21:09:30 +09:00
uppinote
e60a463704
Merge pull request #105 from uppinote20/chore/post-merge-cleanup
fix(seatable): accept full 2xx status range in client
2026-05-29 20:14:36 +09:00
uppinote
297d8f4ca1 style(main): trim cross-file reference from command-registry comment
- private command 레지스트리 narrowing 주석에서 다른 파일명
  (rate-limiter / supabase cache) 참조 제거 — comment-rot 위험 감소
- "코드베이스 컨벤션 따름"은 일반화해 WHY는 유지 (PR #105 리뷰 nit)
2026-05-29 19:51:03 +09:00
uppinote
2fbedbd24f test(seatable): cover fetchRecord 2xx path
- fetchRecord도 `< 200 || >= 300` 확장을 받았으나 회귀 테스트가 없던 갭 보강
  (PR #105 리뷰 지적 — fetchNotes/batchUpdate만 커버되어 있었음)
- 단일 행 조회가 201 응답을 정상 처리하는지 검증
2026-05-29 19:50:54 +09:00
uppinote
60719579b4 fix(field-cache): accept full 2xx status range
- fetchBases / fetchTables / fetchTableMetadata의 HTTP 성공 판정을
  `!== 200`에서 `< 200 || >= 300`으로 확장
- SeaTableClient와 동일한 2xx 처리로 통일 — Airtable Meta API가 프록시
  뒤에서 200을 201/202로 재작성하는 환경 대응 (PR #105 리뷰 follow-up)
- fetchBases·fetchTables·fetchTableMetadata 2xx 회귀 테스트 3건 추가
2026-05-29 19:50:45 +09:00
uppinote
5d5a719713 refactor: resolve eslint warnings in main and field-cache
- main.ts: Obsidian 비공개 command 레지스트리 접근을 `as any` 대신
  `as unknown as { … }`로 좁힘 (rate-limiter / supabase-metadata-cache의
  기존 컨벤션과 일치, 런타임 동작 불변)
- field-cache.ts: fetchTableMetadata가 `{ fields, views }`를 반환하도록
  변경해 fetchFields/fetchViews의 non-null 단언(`!`) 제거 — set 직후 get을
  되짚는 대신 방금 채운 값을 직접 반환
- ESLint warning 3건 해소 (no-explicit-any 1, no-non-null-assertion 2)
2026-05-29 19:22:51 +09:00
uppinote
2a073b0936 fix(seatable): accept full 2xx status range in client
- getBaseToken / fetchNotes / fetchRecord / batchUpdate의 HTTP 성공 판정을
  `!== 200`에서 `< 200 || >= 300`으로 확장
- SeaTableMetadataCache와 동일한 2xx 처리로 일치 — 일부 기업 프록시가
  200 응답을 201/202로 재작성하는 환경에서 동기화 실패하던 문제 대응
- fetchNotes(토큰+rows 2xx)·batchUpdate(2xx) 회귀 테스트 2건 추가
2026-05-29 19:22:40 +09:00
uppinote
bba5e1d55b
Merge pull request #104 from uppinote20/chore/scorecard-cleanup
fix: community scorecard cleanup + hardening from review
2026-05-29 17:36:03 +09:00
uppinote
73dc1140ea fix(release): SHA-pin checkout and setup-node actions
PR #104 review (Claude, 2nd round — last remaining item):

upload-artifact / download-artifact / attest-build-provenance는 이미
SHA pin됐으나 checkout / setup-node가 floating @v6 tag로 남아
supply-chain 강화의 일관성 빈틈이었음.

- actions/checkout@v6 → de0fac2 (v6.0.2), build + release job 양쪽
- actions/setup-node@v6 → 48b55a0 (v6.4.0)

이제 워크플로우의 모든 action이 SHA pin. 입력 단계(checkout/node)가
오염되면 그 뒤 attestation이 변조된 빌드를 정상으로 서명하게 되므로
입력 단계 pin도 artifact 단계와 동등하게 중요.
2026-05-29 16:25:11 +09:00
uppinote
6a18091d9a chore(seatable): address PR review nits
PR #104 review (Claude Low / Nit):

- parseJson이 null 반환(200 + non-JSON body)할 때 console.warn 추가.
  silent empty-list path가 진단 가능한 신호를 남기게 됨
  (프록시 interstitial 케이스 식별 용이).
- race test의 microtask drain count(20)에 근거 주석 추가:
  현재 체인(fetchTables → fetchTablesUncached → getBaseToken → request
  → requestUrl + json + cache set) 5-8턴, 20은 향후 async 레이어 추가
  여유분.
- non-JSON body test의 verbose 타입 캐스트
  (`Awaited<ReturnType<typeof import('obsidian').requestUrl>>`)를
  `RequestUrlResponse`로 단순화 + 해당 타입 import 추가.
2026-05-28 23:10:44 +09:00
uppinote
fcab899f1f fix(release): attest manifest.json + SHA-pin artifact actions
PR #104 review (Claude Medium + Low):

- build job upload-artifact path에 manifest.json 추가.
  release job의 attestation이 동일한 isolated build artifact를 통과한
  자산을 가리키게 됨 (SLSA traceability 일관성).
- actions/upload-artifact를 v4 floating tag → SHA pin (v4.6.2,
  ea165f8d65b6e75b540449e92b4886f43607fa02).
- actions/download-artifact도 동일하게 SHA pin (v4.3.0,
  d3f86a106a0bac45b974a628896c90dbdf5c8093). attest 액션과 동일한
  supply-chain 강화 수준 유지.
2026-05-28 23:10:26 +09:00
uppinote
a7ad79df7d fix(api-errors): guard extractApiErrorMessage against lazy json getter
PR #104 review (Codex P2): extractApiErrorDetails(r)가 RequestUrlResponse를
바로 받을 때 lazy r.json 게터가 non-JSON body(HTML 502 등)에서
SyntaxError를 던지면 'Failed: HTTP …' 메시지가 SyntaxError로 대체됨.

이전 커밋에서 seatable-metadata-cache의 success 경로엔 parseJson을
적용했지만 error 경로의 extractApiErrorDetails(r) 호출은 보호 안 됨.
util 자체에 try/catch를 추가해서 모든 provider(airtable/seatable/supabase)
호출자가 한꺼번에 보호되도록 함.

테스트 2개 추가:
- 200/4xx/5xx에 lazy json getter가 throw하면 response.text로 fallback
- text도 비어 있으면 'HTTP {status}'로 fallback
2026-05-28 23:10:10 +09:00
uppinote
0f191b0afa test(seatable): cover Accept header and clearForCred race fix
- 기존 'exchanges Base-Token then GETs /metadata/' 테스트에
  Accept: application/json 헤더 검증 2줄 추가 (양쪽 요청 모두)
- 신규 'clearForCred during in-flight fetch' describe 블록 추가:
  identity-checked finally가 SWEEP#2 race에서 후속 in-flight 엔트리를
  보존하는지 deterministic하게 검증.

Race 테스트 설계:
- A의 metadata를 500으로 실패시켜 cachedTables가 비어있게 유지
  (성공 시 cache hit으로 dedup 경로를 우회해버려 race를 관찰 불가)
- A의 finally가 B의 in-flight 엔트리를 잘못 삭제하는지 D의 dedup
  여부로 판정. identity check 동작 시 4 mock calls, 미동작 시 6.
2026-05-28 22:34:46 +09:00
uppinote
7245a549ef docs(readme): correct vault API disclosure list
이전 disclosure는 실제 호출하지 않는 API를 나열하고
실제로 사용하는 API는 빠뜨려서 grep으로 검증 가능한 부정확함이 있었음.

- 제거: vault.cachedRead, vault.getFiles, vault.getMarkdownFiles
  (src/ 어디서도 호출하지 않음)
- 추가: vault.getAbstractFileByPath, vault.createFolder,
  vault.adapter.exists
- 신규 섹션: Vault change events (vault.on / vault.offref)
  · "Watch for file changes" 토글의 동작 표면을 명시
2026-05-28 07:47:08 +09:00
uppinote
61eeb7e7fc fix(css): bump ani-field-error specificity to (0,3,0)
Minimal·Things·AnuPpuccin 등 인기 테마가
.setting-item .setting-item-description를 (0,2,0)으로 정의해서
기존 selector (0,2,0)와 동률이 되고, 스타일시트 로드 순서에 따라
plugin의 에러 색상이 무시될 수 있었음.

.vertical-tab-content 선조 추가로 (0,3,0)이 되어 테마 규칙을
확정적으로 이김. 플러그인 설정은 항상 .vertical-tab-content 안에서
렌더링되므로 런타임 매칭 안전.
2026-05-28 07:46:56 +09:00
uppinote
5c87c3e0d0 fix(seatable): harden metadata cache for proxies and older Obsidian
코드 리뷰에서 발견된 7건의 robustness 이슈를 함께 해결:

- request() 헬퍼 추가: throw:false를 무시하는 구버전 Obsidian이
  rejection을 던질 때 응답 shape을 복구 (SupabaseMetadataCache 패턴)
- parseJson() 헬퍼 추가: 리버스 프록시가 200으로 HTML 유지보수
  페이지를 돌려줄 때 r.json 게터의 SyntaxError를 삼키고 null 반환
- dtable_server fallback을 SeaTableClient.getBaseToken과 동일하게
  ${serverUrl}/api-gateway/ 로 통일. self-hosted SeaTable이
  dtable_server를 생략할 때 settings UI가 404로 깨지는 버그 해결
- 두 요청에 Accept: application/json 헤더 추가
  (SeaTableClient와 정합)
- status check를 !== 200 → !2xx 범위로 확대.
  201/202/204 등을 전달하는 프록시 환경에서 동작
- body 객체에 null-safe 체크 (?. 옵셔널 체이닝)
- fetchTables의 finally에서 identity-check 후 delete.
  clearForCred 중간 호출로 인한 in-flight Map 오삭제 race 차단

테스트 4개 추가:
- dtable_server 누락 시 /api-gateway/ fallback 확인
- 2xx (201) status 허용
- 200 + non-JSON body 처리
- 구버전 Obsidian의 throw:false 무시 시 wrapped error 복구

beforeEach: clearAllMocks → resetAllMocks
(mockImplementation 누수 차단)
2026-05-28 07:46:43 +09:00
uppinote
bf5feec8a4 fix(release): harden workflow against supply chain attacks
코드 리뷰에서 발견된 4건의 워크플로우 보안 이슈를 함께 해결:

- 빌드와 attest+release를 별도 job으로 분리. id-token: write가
  npm install이 실행되는 job과 분리돼 postinstall 스크립트가
  Sigstore OIDC token을 발급받을 수 없음
- npm install → npm ci --ignore-scripts
  · ci: lockfile 외 패키지 변동 차단 (provenance와 lock 정합성 유지)
  · --ignore-scripts: 모든 postinstall 훅 차단
- checkout step에 persist-credentials: false 추가.
  npm 단계가 .git/config의 GITHUB_TOKEN을 사용 못 함
- actions/attest-build-provenance를 mutable @v2 → SHA pin
  (e8998f9, v2.4.0). 업스트림 tag 재포인팅 공격 차단
- subject-path에 manifest.json 추가. 다운로드 사용자가
  gh attestation verify로 manifest까지 검증 가능
2026-05-28 07:46:20 +09:00
uppinote
9e99cf043d docs: disclose permissions and network surfaces
README에 'Permissions & Disclosures' 섹션 추가.
- Vault enumeration / read / write 사용 이유 + scope 명시
- Clipboard write는 Supabase RPC 'Copy SQL' 버튼에서만 사용,
  read는 하지 않음을 명시
- Network requests: 사용자가 등록한 DB credential 외 destination 없음,
  no telemetry / no analytics 명시

커뮤니티 store 리뷰의 Behavior Recommendation 2건
(Vault Enumeration, Clipboard Access)에 대한 disclosure.
2026-05-27 14:54:14 +09:00
uppinote
3d5132356d ci(release): attest build provenance for main.js and styles.css
actions/attest-build-provenance@v2 step 추가.
- Sigstore에서 단기 서명 인증서를 발급받아 GitHub attestation store에
  자산의 빌드 출처(commit SHA + workflow run)를 게시
- id-token: write + attestations: write permissions 추가
- 다음 release(1.0.1+)부터 자동 적용. 1.0.0은 retroactive 서명 불가
2026-05-27 14:52:52 +09:00
uppinote
df0590e8e7 refactor(seatable): use Obsidian requestUrl for metadata cache
브라우저 fetch에서 obsidian의 requestUrl로 마이그레이션:
- 플러그인 가이드라인 권장 준수 (network call 단일 surface)
- 모바일에서 더 안정적 (Obsidian Mobile의 fetch CORS 제약 회피)
- SeaTableClient와 동일한 네트워크 경로 사용 → disclosure 카운트 감소

JSDoc 상단의 '직접 fetch 사용 의도' 주석도 새 방침에 맞게 재작성.
테스트는 vi.stubGlobal('fetch') 대신 vi.mocked(requestUrl) 패턴으로,
response shape(json sync 속성, ok 없음)에 맞게 mock 헬퍼 갱신.
2026-05-27 14:51:43 +09:00
uppinote
e363253371 chore: address community scorecard warnings (manifest, build, css)
- manifest.json: authorUrl을 repo URL → author profile URL로 변경
  (커뮤니티 가이드라인은 개인/조직 프로필을 기대함)
- esbuild.config.mjs + package.json: 외부 의존성 builtin-modules 제거,
  Node 내장 module.builtinModules로 대체
- styles.css: text-decoration / text-decoration-color 분리 선언을
  border-bottom으로 대체 (Obsidian 1.4.5 부분 지원 회피)
- styles.css: .ani-field-error의 !important를 selector specificity bump
  (.setting-item-description.ani-field-error)로 대체
2026-05-27 14:50:11 +09:00
uppinote
0e7f15aedb 1.0.0 2026-05-27 11:08:20 +09:00
uppinote
55228085a0
Merge pull request #100 from uppinote20/feature/issue-98-subfolder-mapper-policy
feat: FieldTypeMapper.isSubfolderSafe + UI parity across providers (#98)
2026-05-27 10:52:04 +09:00
uppinote
1e0dbd87ab docs(supabase): clarify json/jsonb comment vs OBJECT_SHAPED_TYPES (#98)
PR #100 4th review (Claude #2): TYPE_TO_STANDARD의 json/jsonb 주석이
'serialized as JSON string in OpenAPI shape'라고만 했고, OBJECT_SHAPED_TYPES
주석은 'runtime is JS object'라고 표현해서 같은 type을 두 가지로 보는
미래 reader가 혼동 가능. 두 사실 모두 명시 + OBJECT_SHAPED_TYPES 참조 추가.

코드 변경 없음. 주석만.
2026-05-27 09:29:20 +09:00
uppinote
7e47f445f7 fix: supabase isReadOnly prototype-leak + exhaustive-guard TS comment (#98)
PR #100 3rd review (Claude #1): Airtable + SeaTable의 isReadOnly는
hasOwnProperty.call로 잡았는데 Supabase 누락. 비대칭. 'toString'이
prototype chain으로 통과하여 fail-closed 우회.

수정:
- supabase-field-mapper.ts: in TYPE_TO_STANDARD → hasOwnProperty.call
  (base type strip 후). Airtable/SeaTable 패턴 일치
- 회귀 테스트: prototype-chain 이름 모두 isReadOnly=true 확인
  (base + :readonly variant 양쪽)

추가:
- settings-tab.ts exhaustive guard: 'void _exhaustive' 패턴을 idiomatic
  'satisfies never'로 바꾸려 했으나 TS 4.7.4 (repo pin)가 satisfies
  미지원 (4.9+ 필요). 주석으로 toolchain 제약 명시.
2026-05-27 03:17:36 +09:00
uppinote
832b5884eb fix(ui): filename stale guard, fail-closed switch, double-count, gen guard (#98)
다중 review finding 처리 (review chain의 SHOULD/INFO/NICE 묶음):

1) SeaTable + Supabase FILENAME 드롭다운 stale-value guard:
   - 이전 PR이 subfolder만 처리. 이번에 filename도 동일 패턴 적용
   - '<value> (unsupported / hidden)' synthetic option

2) Cold-load flicker 방지:
   - selectedTable/columns가 비어있을 때는 stale 판정 skip
   - 초기 metadata fetch 동안 false-positive 'unsupported' 표시 회피

3) Switch default fail-closed:
   - 기존: supportedFields = fields (모든 column, attachment 포함)
   - 수정: supportedFields = []. 미래 filterMode union 확장 시 silent
     attachment 노출 방지

4) unsupportedCount double-count fix:
   - stale-value가 currently-present (renamed/changed)이면 visible inline
     AND 'N hidden' banner 둘 다 카운트 → contradictory
   - stale && fieldInCurrentBatch이면 카운트에서 1 차감

5) "unrecognized" → "excluded" wording:
   - subfolder mode가 known but policy-excluded (attachment/link) 포함
   - 'unrecognized'는 plugin이 모른다는 함의 → user가 잘못 진단할 수
     있음. 'excluded'로 두 가지 reason 모두 포괄

6) renderGeneration guard:
   - async fetchFields 도중 settings re-render 시 stale callback이
     detached DOM에 쓰는 race. SeaTable/Supabase 패턴 일치
2026-05-27 02:24:07 +09:00
uppinote
26d1c50768 fix: determineFolderPath runtime guard against object-shaped values (#98)
UI dropdown filter는 새 선택을 막지만, 기존(pre-#98) config의
subfolderFieldName이 attachment/collaborator/jsonb column을 가리키면
sync 시 String(value) = '[object Object]' → 모든 record가 한 폴더에 떨어짐.

수정:
- determineFolderPath: rawString.startsWith('[object ') 검출 시
  settings.folderPath로 fallback. debugMode일 때 Notice.

회귀 가드:
- subfolder value가 plain object일 때 fallback
- subfolder value가 array of objects일 때 fallback
2026-05-27 02:23:56 +09:00
uppinote
6372c74f6b fix: deeper isSubfolderSafe / isReadOnly hardening (#98)
이전 commit이 attachment/link/unknown standard type만 제외했는데, runtime
shape이 객체인 다른 types도 같은 footgun 노출. Codex P1 + 다중 angle review.

Object-shaped exclusion (per-mapper allowlist):
- Airtable: singleCollaborator, multipleCollaborators, barcode, button,
  aiText, externalSyncSource, lookup
- SeaTable: collaborator, geolocation, button
- Supabase: object, array:object, string:json, string:jsonb

isReadOnly prototype leak (pre-existing, Claude review #5):
- Airtable + SeaTable의 `if (!(t in TYPE_TO_STANDARD))` 가 `'toString'` 등
  prototype-chain 이름에 false → fail-closed contract 위반
- `Object.prototype.hasOwnProperty.call`로 교체 + 회귀 테스트

NOOP_MAPPER fail-closed mirror:
- tests/__mocks__/database-client.mock.ts: isReadOnly→true, isFilenameSafe
  → false, isSubfolderSafe → false. 'permissive mock' antipattern 제거.

Cardinality drift guards (3 mapper):
- expect(getSubfolderSafeTypes().length).toBe(N) — Airtable 24, SeaTable 19,
  Supabase 30. TYPE_TO_STANDARD에 신규 attachment/object-shaped 타입 추가
  시 OBJECT_SHAPED_TYPES도 갱신하도록 강제.

Tests:
- isSubfolderSafe object-shaped false (3 provider 각각)
- isReadOnly prototype-chain false (Airtable + SeaTable)
- 기존 'stringifiable known types' 목록에서 새 제외 type들 제거
2026-05-27 02:23:51 +09:00
uppinote
9d3911240f fix(ui): supabase hasFieldTypeMapper guard + mode-specific hint text (#98)
마지막 두 review finding 처리:

#10 unsupportedCount placeholder text mode-specific:
- 기존: '--- N unsupported fields hidden ---'  (양 모드 동일)
- 수정: filename mode → 'unsupported' (타입이 OS 규칙 불합),
        subfolder mode → 'unrecognized' (mapper가 모르는 타입).
  사용자가 'unrecognized' 에서 plugin update 가능성을 움직일 수 있음.

#12 hasFieldTypeMapper guard 대칭을 위해 renderSupabaseConnection
에도 가드 추가 (renderSeaTableConnection 패턴 일치):
- 원래: Supabase는 그대로 getFieldTypeMapper 호출 → 이론적
  로 provider factory 있으나 mapper 안 등록되면 throw
- 수정: 상위에서 hasFieldTypeMapper 체크 후 'Field type mapper
  missing' Setting으로 그레이스풌하게 처리.
2026-05-27 01:42:33 +09:00
uppinote
851e01f189 fix(ui): stale subfolder value guard + exhaustive filterMode (#98)
PR 리뷰 사이의 그다음 두 항목:

1) Stale value UX divergence (3 provider 공통):
   - 문제: 아이쓰 필터링이 좀 더 엄격해져서, 기존
     subfolderFieldName이 필터링 후 옵션 목록에 없어질 수 있음.
     dropdown.setValue(누락된값)은 silent no-op — visible
     selection은 빈칸이면서 config는 그대로 유지되어 UI/state
     는 갈리며 사용자가 다른 값을 고르면 조용히 덮어쓰임.
   - 조치: 저장된 값이 supported list에 없으면
     \`\`\`
     dropdown.addOption(currentValue, '<value> (unsupported / hidden)')
     \`\`\`
     으로 synthetic 옵션 추가 — 사용자에게 노출.
   - Airtable renderFieldDropdown + SeaTable renderSeaTableDropdowns +
     Supabase renderSupabaseDropdowns 세 곳 모두 적용.

2) Exhaustive filterMode switch (Airtable renderFieldDropdown):
   - 기존 ternary \`filterMode === 'filename' ? ... : ...\` 은 union이
     세 번째 값으로 확장되면 silently subfolder branch로
     fallthrough.
   - switch + \`const _exhaustive: never = filterMode\` guard로 교체.
2026-05-27 01:32:10 +09:00
uppinote
fdfb5b56ac fix: harden isSubfolderSafe — exclude attachment/link, no prototype leak (#98)
PR 수정 사항 (code review fix):

1) Prototype-chain leak (Airtable + SeaTable):
   - 동작: 기존 구현 \`providerType in TYPE_TO_STANDARD\`는
     \`in\` 연산자가 prototype chain을 타고 건너 \`'toString'\` /
     \`'constructor'\` / \`'hasOwnProperty'\` / \`'__proto__'\` 같은 inherited
     name에서 true 반환
   - JSDoc 해제적 계약 ('fail-closed on unknown types') 위반
   - Supabase는 \`(SUBFOLDER_SAFE_TYPES as readonly string[]).includes()\`
     패턴으로 안전 — 다른 두 mapper도 동일 패턴으로 일괄
   - 회귀 가드: 8개 prototype name이 false 반환하는지 테스트

2) Attachment / link / unknown types 제외:
   - 동작: 이전 구현은 \`multipleAttachments\` / \`multipleRecordLinks\`
     도 수용 → \`String([{id:'att...'}])\` = \`'[object Object]'\` 로
     사용자가 해당 컬럼을 고르면 전수 레코드가 한 개 의미없는
     폴더에 더미됨
   - SUBFOLDER_SAFE_TYPES를 TYPE_TO_STANDARD entries에서
     \`std 제외: attachment / link / unknown\`으로 필터링
   - Supabase도 \`std === 'unknown'\` (\`string:byte\` 등) 제외
   - sorted로 압축 — 3 provider 드롭다운 순서 일관

3) Mock drift:
   - tests/services/provider-registry.test.ts fake mapper literal에
     isSubfolderSafe + getSubfolderSafeTypes 추가
   - NOOP_MAPPER와 parity

신규 테스트: prototype-name false (Airtable+SeaTable), attachment/link
false (전 provider), string:byte false (Supabase), sorted enumeration (전 provider).
2026-05-27 01:31:47 +09:00