Skip to content

Commit a6a19e5

Browse files
author
SqlRush
committed
Normalize microcompact cache field names
1 parent 83cbfac commit a6a19e5

5 files changed

Lines changed: 103 additions & 72 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
602602

603603
本轮补充:microcompact disk cache loader 现在也会穿透 `edges`/`nodes`/`included` 等 collection wrapper 和数组元素,跳过无 summary 的非 cache resource,并恢复 GraphQL connection 或 JSON:API included 风格 cache entry。
604604

605+
本轮补充:microcompact disk cache loader 的字段查找现在接受大小写、snake_case 和 kebab-case 相邻形式归一,例如 `summary-text``cache-key``cache-version``created-at``ttl-seconds` 可恢复同一 cache entry。
606+
605607
本轮补充:microcompact disk cache loader 的 summary-like payload 现在接受 text content-block object、text content-block array 和 string array,会把可见 text block 合并为 summary,并会继续解包 text block 内嵌的 JSON/fenced summary payload,兼容官方/SDK 响应内容块形态的 cached microcompact 文件。
606608

607609
本轮补充:microcompact disk cache loader 的 summary array item 现在也复用 provider-style `parts`/`content.parts`/`output` 文本恢复路径,批量候选或 provider cache item 不再因数组元素不是标准 text block 而失效。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ test/parity/ # golden tests against TS/official behavior
319319
- 本轮补充:microcompact disk cache loader 继续接受相邻 timestamp/expiry aliases,包括 `cachedAt``cacheCreatedAt``storedAt``generatedAt``updatedAt``timestamp``expiry``expirationTime``validUntil``notAfter`,以及 `timeToLiveSeconds``validForMs` 等相对 TTL 字段。
320320
- 本轮补充:microcompact disk cache loader 的相对 TTL 现在也接受分钟、小时和天级别名,例如 `ttlMinutes``expiresInHours``validForDays` 及 snake/camel 相邻形式,恢复 cached microcompact 过期时间时不再局限于秒/毫秒。
321321
- 本轮补充:microcompact disk cache loader 的相对 TTL 字符串现在也接受固定单位 ISO-8601 duration,例如 `PT1H30M``P1D``P1DT2H`,并继续拒绝年/月这类不定长 duration。
322+
- 本轮补充:microcompact disk cache loader 的字段查找现在接受大小写、snake_case 和 kebab-case 相邻形式归一,例如 `summary-text``cache-key``cache-version``created-at``ttl-seconds` 可恢复同一 cache entry。
322323
- 本轮补充:microcompact disk cache loader 和 prune 现在接受 digest 缺失但文件名已 keyed 的 cache entry,会用 `<digest>.json` 文件名作为 digest fallback,同时仍保留显式 digest mismatch 的 invalid-cache guard。
323324
- 本轮补充:microcompact disk cache loader 的 `cached`/`fromCache`/`cacheHit`/`isCached` 布尔字段现在接受 JSON bool、`true`/`false``yes`/`no``on`/`off``1`/`0` 数字/字符串形态。
324325
- 本轮补充:microcompact disk cache loader 现在会从 `metadata`/`meta`/`cacheInfo`/`cacheDetails`/`cacheEntry`/`entry`/`record`/`cache` 等 sidecar object 中补缺失的 digest、version、cache-hit、timestamp、TTL 和 count aliases;主 summary payload 字段仍保持优先。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ M6 progress now includes:
128128
- `internal/compact`: microcompact disk cache loading now also accepts adjacent timestamp aliases such as `cachedAt`, `cacheCreatedAt`, `storedAt`, `generatedAt`, `updatedAt`, `timestamp`, `expiry`, `expirationTime`, `validUntil`, `notAfter`, and relative TTL aliases such as `timeToLiveSeconds` and `validForMs`.
129129
- `internal/compact`: microcompact disk cache relative TTL loading now also accepts minute, hour, and day aliases such as `ttlMinutes`, `expiresInHours`, and `validForDays`, including adjacent snake/camel forms.
130130
- `internal/compact`: microcompact disk cache relative TTL strings now also accept fixed-unit ISO-8601 durations such as `PT1H30M`, `P1D`, and `P1DT2H`, while rejecting ambiguous year/month durations.
131+
- `internal/compact`: microcompact disk cache field lookup now normalizes case, snake_case, and kebab-case spellings, so adjacent exports such as `summary-text`, `cache-key`, `cache-version`, `created-at`, and `ttl-seconds` recover the same cache entry.
131132
- `internal/compact`: microcompact disk cache loading now fills missing cache metadata from sidecar objects such as `metadata`, `meta`, `cacheInfo`, `cacheDetails`, `cacheEntry`, `entry`, `record`, and `cache`, preserving primary result fields while recovering digest/version/cache-hit/timestamp/TTL/count aliases stored outside the summary payload.
132133
- `internal/compact`: microcompact disk cache loading and pruning now use the `<digest>.json` filename as a digest fallback when a cache entry omits an internal digest, while preserving explicit digest mismatch pruning/errors.
133134
- `internal/compact`: microcompact disk cache loading now accepts non-strict cache-hit booleans for `cached`/`fromCache`/`cacheHit`/`isCached`, including `yes`/`no`, `on`/`off`, and `1`/`0` JSON number or string values.

internal/compact/compact_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,17 @@ func TestLoadMicroResultAcceptsSummaryTextAliases(t *testing.T) {
392392
}`,
393393
want: "detail block alias summary",
394394
},
395+
{
396+
digest: "summary-hyphenated-fields",
397+
payload: `{
398+
"summary-text": "hyphenated alias summary",
399+
"cache-key": "summary-hyphenated-fields",
400+
"cache-version": "microcompact.v1",
401+
"created-at": 100,
402+
"ttl-seconds": 3600
403+
}`,
404+
want: "hyphenated alias summary",
405+
},
395406
} {
396407
if err := os.WriteFile(microResultPath(cacheDir, tc.digest), []byte(tc.payload), 0o600); err != nil {
397408
t.Fatal(err)
@@ -406,6 +417,9 @@ func TestLoadMicroResultAcceptsSummaryTextAliases(t *testing.T) {
406417
if result.Summary != tc.want || result.Digest != tc.digest || result.Version != DefaultMicroCacheVersion {
407418
t.Fatalf("%s result = %#v", tc.digest, result)
408419
}
420+
if tc.digest == "summary-hyphenated-fields" && (!result.CreatedAt.Equal(time.Unix(100, 0).UTC()) || !result.ExpiresAt.Equal(time.Unix(3700, 0).UTC())) {
421+
t.Fatalf("%s times = %#v", tc.digest, result)
422+
}
409423
}
410424
}
411425

internal/compact/micro.go

Lines changed: 85 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"os"
1010
"path/filepath"
11+
"sort"
1112
"strconv"
1213
"strings"
1314
"sync"
@@ -565,33 +566,28 @@ func PruneMicroCache(dir string, options MicroPruneOptions) (int, error) {
565566
}
566567

567568
func microStringJSONField(fields map[string]json.RawMessage, names ...string) (string, bool, error) {
568-
for _, name := range names {
569-
raw, ok := fields[name]
570-
if !ok || string(raw) == "null" {
571-
continue
572-
}
573-
var value string
574-
if err := json.Unmarshal(raw, &value); err != nil {
575-
return "", false, err
576-
}
577-
return value, true, nil
569+
raw, _, ok := microRawJSONField(fields, names...)
570+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
571+
return "", false, nil
578572
}
579-
return "", false, nil
573+
var value string
574+
if err := json.Unmarshal(raw, &value); err != nil {
575+
return "", false, err
576+
}
577+
return value, true, nil
580578
}
581579

582580
func microSummaryJSONField(fields map[string]json.RawMessage, names ...string) (string, bool, error) {
583-
for _, name := range names {
584-
raw, ok := fields[name]
585-
if !ok || string(raw) == "null" {
586-
continue
587-
}
588-
value, ok, err := microSummaryFromRaw(raw, name)
589-
if err != nil {
590-
return "", false, err
591-
}
592-
if ok {
593-
return value, true, nil
594-
}
581+
raw, name, ok := microRawJSONField(fields, names...)
582+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
583+
return "", false, nil
584+
}
585+
value, ok, err := microSummaryFromRaw(raw, name)
586+
if err != nil {
587+
return "", false, err
588+
}
589+
if ok {
590+
return value, true, nil
595591
}
596592
return "", false, nil
597593
}
@@ -813,18 +809,15 @@ func microSummaryMessageText(raw json.RawMessage) (string, bool, error) {
813809
}
814810

815811
func microBoolJSONField(fields map[string]json.RawMessage, names ...string) (bool, bool, error) {
816-
for _, name := range names {
817-
raw, ok := fields[name]
818-
if !ok || string(raw) == "null" {
819-
continue
820-
}
821-
value, err := microParseJSONBool(raw, name)
822-
if err != nil {
823-
return false, false, err
824-
}
825-
return value, true, nil
812+
raw, name, ok := microRawJSONField(fields, names...)
813+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
814+
return false, false, nil
815+
}
816+
value, err := microParseJSONBool(raw, name)
817+
if err != nil {
818+
return false, false, err
826819
}
827-
return false, false, nil
820+
return value, true, nil
828821
}
829822

830823
func microParseJSONBool(raw json.RawMessage, field string) (bool, error) {
@@ -864,56 +857,76 @@ func microParseJSONBool(raw json.RawMessage, field string) (bool, error) {
864857
}
865858

866859
func microIntJSONField(fields map[string]json.RawMessage, names ...string) (int, bool, error) {
867-
for _, name := range names {
868-
raw, ok := fields[name]
869-
if !ok || string(raw) == "null" {
870-
continue
871-
}
872-
var value int
873-
if err := json.Unmarshal(raw, &value); err == nil {
874-
return value, true, nil
875-
}
876-
var text string
877-
if err := json.Unmarshal(raw, &text); err != nil {
878-
return 0, false, err
879-
}
880-
parsed, err := strconv.Atoi(text)
881-
if err != nil {
882-
return 0, false, err
883-
}
884-
return parsed, true, nil
860+
raw, _, ok := microRawJSONField(fields, names...)
861+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
862+
return 0, false, nil
863+
}
864+
var value int
865+
if err := json.Unmarshal(raw, &value); err == nil {
866+
return value, true, nil
885867
}
886-
return 0, false, nil
868+
var text string
869+
if err := json.Unmarshal(raw, &text); err != nil {
870+
return 0, false, err
871+
}
872+
parsed, err := strconv.Atoi(text)
873+
if err != nil {
874+
return 0, false, err
875+
}
876+
return parsed, true, nil
887877
}
888878

889879
func microTimeJSONField(fields map[string]json.RawMessage, names ...string) (time.Time, bool, error) {
890-
for _, name := range names {
891-
raw, ok := fields[name]
892-
if !ok || string(raw) == "null" {
893-
continue
894-
}
895-
value, err := microParseJSONTime(raw, name)
896-
if err != nil {
897-
return time.Time{}, false, err
898-
}
899-
return value, true, nil
880+
raw, name, ok := microRawJSONField(fields, names...)
881+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
882+
return time.Time{}, false, nil
900883
}
901-
return time.Time{}, false, nil
884+
value, err := microParseJSONTime(raw, name)
885+
if err != nil {
886+
return time.Time{}, false, err
887+
}
888+
return value, true, nil
902889
}
903890

904891
func microDurationJSONField(fields map[string]json.RawMessage, names ...string) (time.Duration, bool, error) {
892+
raw, name, ok := microRawJSONField(fields, names...)
893+
if !ok || bytes.Equal(bytes.TrimSpace(raw), []byte("null")) {
894+
return 0, false, nil
895+
}
896+
value, err := microParseJSONDuration(raw, name)
897+
if err != nil {
898+
return 0, false, err
899+
}
900+
return value, true, nil
901+
}
902+
903+
func microRawJSONField(fields map[string]json.RawMessage, names ...string) (json.RawMessage, string, bool) {
905904
for _, name := range names {
906-
raw, ok := fields[name]
907-
if !ok || string(raw) == "null" {
908-
continue
905+
if raw, ok := fields[name]; ok {
906+
return raw, name, true
909907
}
910-
value, err := microParseJSONDuration(raw, name)
911-
if err != nil {
912-
return 0, false, err
908+
}
909+
keys := make([]string, 0, len(fields))
910+
for key := range fields {
911+
keys = append(keys, key)
912+
}
913+
sort.Strings(keys)
914+
for _, name := range names {
915+
normalized := microNormalizedJSONFieldName(name)
916+
for _, key := range keys {
917+
if microNormalizedJSONFieldName(key) == normalized {
918+
return fields[key], key, true
919+
}
913920
}
914-
return value, true, nil
915921
}
916-
return 0, false, nil
922+
return nil, "", false
923+
}
924+
925+
func microNormalizedJSONFieldName(name string) string {
926+
name = strings.ToLower(strings.TrimSpace(name))
927+
name = strings.ReplaceAll(name, "_", "")
928+
name = strings.ReplaceAll(name, "-", "")
929+
return name
917930
}
918931

919932
func microParseJSONDuration(raw json.RawMessage, field string) (time.Duration, error) {

0 commit comments

Comments
 (0)