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
provider별 테스트는 각자의 불변식만 검증할 뿐, 새 provider가 공통 계약을
어겨도 잡지 못하는 갭이 있었다. provider-registry에 등록된 모든 매퍼를
동적으로 탐색해 공유 계약을 한곳에서 검증한다.
- filename-safe ⊆ subfolder-safe, subfolder-safe 타입이 안전한 표준 타입으로
매핑되는지, 프로토타입 체인 이름(toString 등)의 fail-closed,
열거 메서드의 sorted+unique 검증
- airtable/seatable/supabase 존재를 floor guard로 고정해 등록 회귀 시
it.each([])가 빈 통과(vacuous pass)하는 것을 방지
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 계약 명시 — 테스트로만 암묵 보장되던 불변식을 문서화
- 두 매퍼 단위 테스트의 기대 순서 동기화
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
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
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)을
항상 충족한다.
- fetchBases / fetchTables / fetchTableMetadata의 HTTP 성공 판정을
`!== 200`에서 `< 200 || >= 300`으로 확장
- SeaTableClient와 동일한 2xx 처리로 통일 — Airtable Meta API가 프록시
뒤에서 200을 201/202로 재작성하는 환경 대응 (PR #105 리뷰 follow-up)
- fetchBases·fetchTables·fetchTableMetadata 2xx 회귀 테스트 3건 추가
- getBaseToken / fetchNotes / fetchRecord / batchUpdate의 HTTP 성공 판정을
`!== 200`에서 `< 200 || >= 300`으로 확장
- SeaTableMetadataCache와 동일한 2xx 처리로 일치 — 일부 기업 프록시가
200 응답을 201/202로 재작성하는 환경에서 동기화 실패하던 문제 대응
- fetchNotes(토큰+rows 2xx)·batchUpdate(2xx) 회귀 테스트 2건 추가
- 기존 '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.
이전 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" 토글의 동작 표면을 명시
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 안에서
렌더링되므로 런타임 매칭 안전.
코드 리뷰에서 발견된 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 누수 차단)
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.
브라우저 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 헬퍼 갱신.
- 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)로 대체
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 참조 추가.
코드 변경 없음. 주석만.
다중 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 패턴 일치
마지막 두 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으로 그레이스풌하게 처리.
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로 교체.