This is an automated email from the ASF dual-hosted git repository.

hanahmily pushed a commit to branch feat/segmeta
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git


The following commit(s) were added to refs/heads/feat/segmeta by this push:
     new d20527794 chore
d20527794 is described below

commit d20527794d55032d49610a5a3d835d94289f61cc
Author: Hongtao Gao <[email protected]>
AuthorDate: Wed Apr 8 06:56:33 2026 +0000

    chore
---
 .gitignore                                         |   5 +-
 CHANGES.md                                         |   1 +
 .../plans/2026-04-07-stable-segment-end-time.md    | 545 ---------------------
 .../2026-04-07-stable-segment-end-time-design.md   |  75 ---
 4 files changed, 2 insertions(+), 624 deletions(-)

diff --git a/.gitignore b/.gitignore
index e433fb61c..6f4a80ab7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,12 +75,9 @@ gomock_reflect*
 .cursor/
 
 # Claude
-Claude.md
 .claude/
-CLAUDE.md
+.omc/
 
 # eBPF generated files and binaries
 fodc/agent/internal/ktm/iomonitor/ebpf/generated/vmlinux.h
 
-# Codex
-AGENTS.md
diff --git a/CHANGES.md b/CHANGES.md
index f24db4091..f8ccdc934 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -11,6 +11,7 @@ Release Notes.
 - Add log query e2e test.
 - Sync lifecycle e2e test from SkyWalking stages test.
 - Add periodic health check for property schema connection.
+- Persist segment end time in per-segment metadata so boundaries don't shift 
across restarts or config changes
 
 ### Bug Fixes
 
diff --git a/docs/superpowers/plans/2026-04-07-stable-segment-end-time.md 
b/docs/superpowers/plans/2026-04-07-stable-segment-end-time.md
deleted file mode 100644
index 495886235..000000000
--- a/docs/superpowers/plans/2026-04-07-stable-segment-end-time.md
+++ /dev/null
@@ -1,545 +0,0 @@
-# Stable Segment End Time Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use 
superpowers:subagent-driven-development (recommended) or 
superpowers:executing-plans to implement this plan task-by-task. Steps use 
checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Persist segment end time in per-segment metadata so boundaries don't 
shift across restarts or config changes.
-
-**Architecture:** Extend the existing `metadata` file from a plain version 
string to JSON containing both version and end time. New `readSegmentMeta()` 
function handles both old and new formats. The write path serializes JSON; the 
read path reads it with fallback to current computation for old segments.
-
-**Tech Stack:** Go standard library (`encoding/json`, `time`), existing test 
framework (`testify`).
-
----
-
-### Task 1: Add `segmentMeta` and `readSegmentMeta()` to version.go
-
-**Files:**
-- Modify: `banyand/internal/storage/version.go`
-- Test: `banyand/internal/storage/version_test.go` (new file)
-
-- [ ] **Step 1: Write tests for `readSegmentMeta`**
-
-Create `banyand/internal/storage/version_test.go`:
-
-```go
-package storage
-
-import (
-       "testing"
-
-       "github.com/stretchr/testify/assert"
-       "github.com/stretchr/testify/require"
-)
-
-func TestReadSegmentMeta_NewFormat(t *testing.T) {
-       data := 
[]byte(`{"version":"1.5.0","endTime":"2026-04-07T00:00:00+08:00"}`)
-       meta, err := readSegmentMeta(data)
-       require.NoError(t, err)
-       assert.Equal(t, "1.5.0", meta.Version)
-       assert.Equal(t, "2026-04-07T00:00:00+08:00", meta.EndTime)
-}
-
-func TestReadSegmentMeta_OldFormat(t *testing.T) {
-       data := []byte("1.4.0")
-       meta, err := readSegmentMeta(data)
-       require.NoError(t, err)
-       assert.Equal(t, "1.4.0", meta.Version)
-       assert.Equal(t, "", meta.EndTime)
-}
-
-func TestReadSegmentMeta_OldFormatWithNewline(t *testing.T) {
-       data := []byte("1.4.0\n")
-       meta, err := readSegmentMeta(data)
-       require.NoError(t, err)
-       assert.Equal(t, "1.4.0", meta.Version)
-       assert.Equal(t, "", meta.EndTime)
-}
-
-func TestReadSegmentMeta_IncompatibleVersion(t *testing.T) {
-       data := 
[]byte(`{"version":"0.1.0","endTime":"2026-04-07T00:00:00+08:00"}`)
-       _, err := readSegmentMeta(data)
-       assert.Error(t, err)
-}
-
-func TestReadSegmentMeta_NewFormatNoEndTime(t *testing.T) {
-       data := []byte(`{"version":"1.5.0"}`)
-       meta, err := readSegmentMeta(data)
-       require.NoError(t, err)
-       assert.Equal(t, "1.5.0", meta.Version)
-       assert.Equal(t, "", meta.EndTime)
-}
-```
-
-- [ ] **Step 2: Run tests to verify they fail**
-
-Run: `go test ./banyand/internal/storage/ -run TestReadSegmentMeta -v`
-Expected: FAIL — `readSegmentMeta` undefined.
-
-- [ ] **Step 3: Add `segmentMeta` struct and `readSegmentMeta()` to 
`version.go`**
-
-Add after the `checkVersion` function in `banyand/internal/storage/version.go`:
-
-```go
-type segmentMeta struct {
-       Version string `json:"version"`
-       EndTime string `json:"endTime,omitempty"`
-}
-
-func readSegmentMeta(data []byte) (segmentMeta, error) {
-       var meta segmentMeta
-       trimmed := strings.TrimSpace(string(data))
-       if len(trimmed) > 0 && trimmed[0] == '{' {
-               if unmarshalErr := json.Unmarshal(data, &meta); unmarshalErr != 
nil {
-                       return segmentMeta{}, unmarshalErr
-               }
-       } else {
-               meta.Version = trimmed
-       }
-       if checkErr := checkVersion(meta.Version); checkErr != nil {
-               return segmentMeta{}, checkErr
-       }
-       return meta, nil
-}
-```
-
-Note: `encoding/json` and `strings` are already imported in `version.go`.
-
-- [ ] **Step 4: Run tests to verify they pass**
-
-Run: `go test ./banyand/internal/storage/ -run TestReadSegmentMeta -v`
-Expected: PASS — all 5 tests green.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add banyand/internal/storage/version.go 
banyand/internal/storage/version_test.go
-git commit -m "feat(storage): add segmentMeta struct and readSegmentMeta 
helper"
-```
-
----
-
-### Task 2: Bump version to 1.5.0
-
-**Files:**
-- Modify: `banyand/internal/storage/version.go:31`
-- Modify: `banyand/internal/storage/versions.yml`
-
-- [ ] **Step 1: Update `currentVersion` in `version.go`**
-
-Change line 31 from:
-```go
-currentVersion             = "1.4.0"
-```
-to:
-```go
-currentVersion             = "1.5.0"
-```
-
-- [ ] **Step 2: Add `1.5.0` to `versions.yml`**
-
-Read the current `versions.yml` and append `"1.5.0"` to the versions list.
-
-- [ ] **Step 3: Verify existing tests still pass**
-
-Run: `go test ./banyand/internal/storage/ -run TestReadSegmentMeta -v`
-Expected: PASS — old-format test still passes because `"1.4.0"` remains in the 
compatible list.
-
-- [ ] **Step 4: Commit**
-
-```bash
-git add banyand/internal/storage/version.go 
banyand/internal/storage/versions.yml
-git commit -m "feat(storage): bump metadata version to 1.5.0"
-```
-
----
-
-### Task 3: Update `create()` to write JSON metadata
-
-**Files:**
-- Modify: `banyand/internal/storage/segment.go:540-544`
-
-- [ ] **Step 1: Write a test that creates a segment and verifies metadata 
format**
-
-Add to `banyand/internal/storage/segment_test.go`:
-
-```go
-func TestCreateSegmentWritesJSONMetadata(t *testing.T) {
-       tempDir, cleanup := setupTestEnvironment(t)
-       defer cleanup()
-
-       ctx := context.Background()
-       l := logger.GetLogger("test-segment-metadata")
-       ctx = context.WithValue(ctx, logger.ContextKey, l)
-       ctx = common.SetPosition(ctx, func(_ common.Position) common.Position {
-               return common.Position{
-                       Database: "test-db",
-                       Stage:    "test-stage",
-               }
-       })
-
-       group := "test-group"
-       opts := TSDBOpts[mockTSTable, mockTSTableOpener]{
-               TSTableCreator: func(_ fs.FileSystem, _ string, _ 
common.Position, _ *logger.Logger,
-                       _ timestamp.TimeRange, _ mockTSTableOpener, _ any,
-               ) (mockTSTable, error) {
-                       return mockTSTable{ID: common.ShardID(0)}, nil
-               },
-               ShardNum:                       1,
-               SegmentInterval:                IntervalRule{Unit: DAY, Num: 1},
-               TTL:                            IntervalRule{Unit: DAY, Num: 7},
-               SeriesIndexFlushTimeoutSeconds: 10,
-               SeriesIndexCacheMaxBytes:       1024 * 1024,
-       }
-
-       serviceCache := NewServiceCache().(*serviceCache)
-       sc := newSegmentController[mockTSTable, mockTSTableOpener](
-               ctx,
-               tempDir,
-               l,
-               opts,
-               nil,
-               nil,
-               5*time.Minute,
-               
fs.NewLocalFileSystemWithLoggerAndLimit(logger.GetLogger("storage"), 
opts.MemoryLimit),
-               serviceCache,
-               group,
-       )
-
-       now := time.Now().UTC()
-       startTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, 
time.UTC)
-
-       seg, err := sc.create(startTime)
-       require.NoError(t, err)
-       require.NotNil(t, seg)
-
-       // Read metadata from disk and verify it's JSON with endTime
-       suffix := startTime.Format(dayFormat)
-       metadataPath := filepath.Join(tempDir, fmt.Sprintf("seg-%s", suffix), 
metadataFilename)
-       rawMeta, readErr := os.ReadFile(metadataPath)
-       require.NoError(t, readErr)
-
-       meta, parseErr := readSegmentMeta(rawMeta)
-       require.NoError(t, parseErr)
-       assert.Equal(t, currentVersion, meta.Version)
-       assert.NotEmpty(t, meta.EndTime, "endTime should be persisted in 
metadata")
-
-       // Verify the endTime matches the segment's End
-       expectedEnd := startTime.Add(24 * time.Hour)
-       assert.Equal(t, expectedEnd.Format(time.RFC3339Nano), meta.EndTime)
-
-       seg.DecRef()
-}
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-Run: `go test ./banyand/internal/storage/ -run 
TestCreateSegmentWritesJSONMetadata -v`
-Expected: FAIL — metadata is still plain text, `readSegmentMeta` parses it as 
old format so `EndTime` is empty.
-
-- [ ] **Step 3: Update `create()` to write JSON metadata**
-
-In `banyand/internal/storage/segment.go`, replace lines 540-544:
-
-```go
-       segPath := path.Join(sc.location, fmt.Sprintf(segTemplate, 
sc.format(start)))
-       sc.lfs.MkdirPanicIfExist(segPath, DirPerm)
-       data := []byte(currentVersion)
-       metadataPath := filepath.Join(segPath, metadataFilename)
-       lf, err := sc.lfs.CreateLockFile(metadataPath, FilePerm)
-```
-
-with:
-
-```go
-       segPath := path.Join(sc.location, fmt.Sprintf(segTemplate, 
sc.format(start)))
-       sc.lfs.MkdirPanicIfExist(segPath, DirPerm)
-       meta := segmentMeta{
-               Version: currentVersion,
-               EndTime: end.Format(time.RFC3339Nano),
-       }
-       data, marshalErr := json.Marshal(meta)
-       if marshalErr != nil {
-               logger.Panicf("cannot marshal segment metadata: %s", marshalErr)
-       }
-       metadataPath := filepath.Join(segPath, metadataFilename)
-       lf, err := sc.lfs.CreateLockFile(metadataPath, FilePerm)
-```
-
-Add `"encoding/json"` to the imports in `segment.go` if not already present.
-
-- [ ] **Step 4: Run test to verify it passes**
-
-Run: `go test ./banyand/internal/storage/ -run 
TestCreateSegmentWritesJSONMetadata -v`
-Expected: PASS.
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add banyand/internal/storage/segment.go 
banyand/internal/storage/segment_test.go
-git commit -m "feat(storage): write JSON metadata with endTime on segment 
creation"
-```
-
----
-
-### Task 4: Update `open()` to read `endTime` from metadata
-
-**Files:**
-- Modify: `banyand/internal/storage/segment.go:477-510`
-
-- [ ] **Step 1: Write a test that loads segments with persisted endTime**
-
-Add to `banyand/internal/storage/segment_test.go`:
-
-```go
-func TestOpenReadsPersistedEndTime(t *testing.T) {
-       tempDir, cleanup := setupTestEnvironment(t)
-       defer cleanup()
-
-       ctx := context.Background()
-       l := logger.GetLogger("test-open-endtime")
-       ctx = context.WithValue(ctx, logger.ContextKey, l)
-       ctx = common.SetPosition(ctx, func(_ common.Position) common.Position {
-               return common.Position{
-                       Database: "test-db",
-                       Stage:    "test-stage",
-               }
-       })
-
-       group := "test-group"
-       opts := TSDBOpts[mockTSTable, mockTSTableOpener]{
-               TSTableCreator: func(_ fs.FileSystem, _ string, _ 
common.Position, _ *logger.Logger,
-                       _ timestamp.TimeRange, _ mockTSTableOpener, _ any,
-               ) (mockTSTable, error) {
-                       return mockTSTable{ID: common.ShardID(0)}, nil
-               },
-               ShardNum:                       1,
-               SegmentInterval:                IntervalRule{Unit: DAY, Num: 1},
-               TTL:                            IntervalRule{Unit: DAY, Num: 
30},
-               SeriesIndexFlushTimeoutSeconds: 10,
-               SeriesIndexCacheMaxBytes:       1024 * 1024,
-       }
-
-       serviceCache := NewServiceCache().(*serviceCache)
-       sc := newSegmentController[mockTSTable, mockTSTableOpener](
-               ctx,
-               tempDir,
-               l,
-               opts,
-               nil,
-               nil,
-               5*time.Minute,
-               
fs.NewLocalFileSystemWithLoggerAndLimit(logger.GetLogger("storage"), 
opts.MemoryLimit),
-               serviceCache,
-               group,
-       )
-
-       now := time.Now().UTC()
-       day1 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, 
time.UTC)
-       day2 := day1.Add(24 * time.Hour)
-
-       // Manually create segment directories with JSON metadata
-       // day1 with a specific endTime that differs from the default NextTime
-       customEnd := day1.Add(12 * time.Hour) // 12 hours instead of 24
-       segPath1 := filepath.Join(tempDir, "seg-"+day1.Format(dayFormat))
-       require.NoError(t, os.MkdirAll(segPath1, DirPerm))
-       meta1 := segmentMeta{Version: currentVersion, EndTime: 
customEnd.Format(time.RFC3339Nano)}
-       meta1Data, _ := json.Marshal(meta1)
-       require.NoError(t, os.WriteFile(filepath.Join(segPath1, 
metadataFilename), meta1Data, FilePerm))
-
-       // day2 with standard endTime
-       segPath2 := filepath.Join(tempDir, "seg-"+day2.Format(dayFormat))
-       require.NoError(t, os.MkdirAll(segPath2, DirPerm))
-       meta2 := segmentMeta{Version: currentVersion, EndTime: day2.Add(24 * 
time.Hour).Format(time.RFC3339Nano)}
-       meta2Data, _ := json.Marshal(meta2)
-       require.NoError(t, os.WriteFile(filepath.Join(segPath2, 
metadataFilename), meta2Data, FilePerm))
-
-       // Open the controller (loads segments from disk)
-       openErr := sc.open()
-       require.NoError(t, openErr)
-
-       // Verify segments loaded correctly
-       require.Len(t, sc.lst, 2)
-
-       // First segment should have the custom endTime from metadata
-       assert.Equal(t, customEnd, sc.lst[0].End, "segment 0 should use 
persisted endTime")
-       // Second segment should have its endTime from metadata
-       assert.Equal(t, day2.Add(24*time.Hour), sc.lst[1].End, "segment 1 
should use persisted endTime")
-
-       for _, seg := range sc.lst {
-               seg.DecRef()
-       }
-}
-
-func TestOpenFallbackOldFormatMetadata(t *testing.T) {
-       tempDir, cleanup := setupTestEnvironment(t)
-       defer cleanup()
-
-       ctx := context.Background()
-       l := logger.GetLogger("test-open-fallback")
-       ctx = context.WithValue(ctx, logger.ContextKey, l)
-       ctx = common.SetPosition(ctx, func(_ common.Position) common.Position {
-               return common.Position{
-                       Database: "test-db",
-                       Stage:    "test-stage",
-               }
-       })
-
-       group := "test-group"
-       opts := TSDBOpts[mockTSTable, mockTSTableOpener]{
-               TSTableCreator: func(_ fs.FileSystem, _ string, _ 
common.Position, _ *logger.Logger,
-                       _ timestamp.TimeRange, _ mockTSTableOpener, _ any,
-               ) (mockTSTable, error) {
-                       return mockTSTable{ID: common.ShardID(0)}, nil
-               },
-               ShardNum:                       1,
-               SegmentInterval:                IntervalRule{Unit: DAY, Num: 1},
-               TTL:                            IntervalRule{Unit: DAY, Num: 
30},
-               SeriesIndexFlushTimeoutSeconds: 10,
-               SeriesIndexCacheMaxBytes:       1024 * 1024,
-       }
-
-       serviceCache := NewServiceCache().(*serviceCache)
-       sc := newSegmentController[mockTSTable, mockTSTableOpener](
-               ctx,
-               tempDir,
-               l,
-               opts,
-               nil,
-               nil,
-               5*time.Minute,
-               
fs.NewLocalFileSystemWithLoggerAndLimit(logger.GetLogger("storage"), 
opts.MemoryLimit),
-               serviceCache,
-               group,
-       )
-
-       now := time.Now().UTC()
-       day1 := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, 
time.UTC)
-       day2 := day1.Add(24 * time.Hour)
-
-       // Create segment with OLD format metadata (plain version string)
-       segPath1 := filepath.Join(tempDir, "seg-"+day1.Format(dayFormat))
-       require.NoError(t, os.MkdirAll(segPath1, DirPerm))
-       require.NoError(t, os.WriteFile(filepath.Join(segPath1, 
metadataFilename), []byte("1.4.0"), FilePerm))
-
-       segPath2 := filepath.Join(tempDir, "seg-"+day2.Format(dayFormat))
-       require.NoError(t, os.MkdirAll(segPath2, DirPerm))
-       require.NoError(t, os.WriteFile(filepath.Join(segPath2, 
metadataFilename), []byte("1.4.0"), FilePerm))
-
-       // Open should succeed with fallback end time computation
-       openErr := sc.open()
-       require.NoError(t, openErr)
-
-       require.Len(t, sc.lst, 2)
-
-       // First segment's end should be day2's start (fallback: end = next 
segment's start)
-       assert.Equal(t, day2, sc.lst[0].End, "old format should use fallback 
end time")
-       // Second segment's end should be day2 + 24h (fallback: end = 
NextTime(start))
-       assert.Equal(t, day2.Add(24*time.Hour), sc.lst[1].End, "last segment 
should use NextTime fallback")
-
-       for _, seg := range sc.lst {
-               seg.DecRef()
-       }
-}
-```
-
-- [ ] **Step 2: Run tests to verify they fail**
-
-Run: `go test ./banyand/internal/storage/ -run 
"TestOpenReadsPersistedEndTime|TestOpenFallbackOldFormatMetadata" -v`
-Expected: FAIL — `open()` still computes end times from adjacent segments, 
ignoring metadata.
-
-- [ ] **Step 3: Update `open()` to read `endTime` from metadata**
-
-In `banyand/internal/storage/segment.go`, replace the callback body inside 
`open()` (lines 482-501):
-
-```go
-               suffix := sc.format(start)
-               segmentPath := path.Join(sc.location, fmt.Sprintf(segTemplate, 
suffix))
-               metadataPath := path.Join(segmentPath, metadataFilename)
-               version, err := sc.lfs.Read(metadataPath)
-               if err != nil {
-                       if errors.Is(err, fs.ErrNotExist) {
-                               emptySegments = append(emptySegments, 
segmentPath)
-                               return nil
-                       }
-                       return err
-               }
-               if len(version) == 0 {
-                       emptySegments = append(emptySegments, segmentPath)
-                       return nil
-               }
-               if err = checkVersion(convert.BytesToString(version)); err != 
nil {
-                       return err
-               }
-               _, err = sc.load(start, end, sc.location)
-               return err
-```
-
-with:
-
-```go
-               suffix := sc.format(start)
-               segmentPath := path.Join(sc.location, fmt.Sprintf(segTemplate, 
suffix))
-               metadataPath := path.Join(segmentPath, metadataFilename)
-               rawMeta, readErr := sc.lfs.Read(metadataPath)
-               if readErr != nil {
-                       if errors.Is(readErr, fs.ErrNotExist) {
-                               emptySegments = append(emptySegments, 
segmentPath)
-                               return nil
-                       }
-                       return readErr
-               }
-               if len(rawMeta) == 0 {
-                       emptySegments = append(emptySegments, segmentPath)
-                       return nil
-               }
-               meta, parseErr := readSegmentMeta(rawMeta)
-               if parseErr != nil {
-                       return parseErr
-               }
-               segmentEnd := end
-               if meta.EndTime != "" {
-                       parsedEnd, timeErr := time.Parse(time.RFC3339Nano, 
meta.EndTime)
-                       if timeErr != nil {
-                               return timeErr
-                       }
-                       segmentEnd = parsedEnd
-               }
-               _, err = sc.load(start, segmentEnd, sc.location)
-               return err
-```
-
-- [ ] **Step 4: Run the new tests to verify they pass**
-
-Run: `go test ./banyand/internal/storage/ -run 
"TestOpenReadsPersistedEndTime|TestOpenFallbackOldFormatMetadata" -v`
-Expected: PASS.
-
-- [ ] **Step 5: Run all existing segment tests to verify no regression**
-
-Run: `go test ./banyand/internal/storage/ -run TestSegment -v`
-Expected: PASS — existing tests use `openSegment()` directly, not `open()`, so 
they're unaffected.
-
-- [ ] **Step 6: Commit**
-
-```bash
-git add banyand/internal/storage/segment.go 
banyand/internal/storage/segment_test.go
-git commit -m "feat(storage): read persisted endTime from segment metadata on 
load"
-```
-
----
-
-### Task 5: Run full test suite and verify
-
-**Files:** None changed — verification only.
-
-- [ ] **Step 1: Run the full storage package test suite**
-
-Run: `go test ./banyand/internal/storage/ -v -count=1`
-Expected: All tests PASS.
-
-- [ ] **Step 2: Run the linter**
-
-Run: `make lint`
-Expected: No new lint errors.
-
-- [ ] **Step 3: Final commit (if any fixes needed)**
-
-Only if lint or test fixes were needed.
diff --git 
a/docs/superpowers/specs/2026-04-07-stable-segment-end-time-design.md 
b/docs/superpowers/specs/2026-04-07-stable-segment-end-time-design.md
deleted file mode 100644
index 9cfd0b180..000000000
--- a/docs/superpowers/specs/2026-04-07-stable-segment-end-time-design.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# Stable Segment End Time Design
-
-## Problem
-
-Segment end times are computed dynamically at load time. The last segment's 
end is always `start + interval` (a moving target), and non-last segments 
derive their end from the next segment's start. This causes:
-
-1. **Query correctness risk**: end boundaries can shift across restarts or 
interval config changes, causing data to be missed or double-counted.
-2. **External coordination difficulty**: replication, backup, and compaction 
systems need predictable segment boundaries.
-
-## Solution
-
-Persist the end time in each segment's `metadata` file alongside the existing 
version string. The end time is written once at segment creation and read back 
on load.
-
-## Metadata File Format
-
-**Current** (plain text):
-```
-1.4.0
-```
-
-**New** (JSON):
-```json
-{"version":"1.5.0","endTime":"2026-04-07T00:00:00+08:00"}
-```
-
-- `endTime` uses RFC3339Nano format with timezone.
-- Version bumped to `1.5.0`.
-- Old plain-text format remains readable via fallback.
-
-## Write Path
-
-In `segmentController.create()` (`segment.go:512-556`):
-
-1. Compute `end` exactly as today (`NextTime(start)` or clamped to next 
segment's start).
-2. Serialize `segmentMeta{Version, EndTime}` as JSON and write to `metadata` 
file.
-
-No signature changes needed — `openSegment` already accepts `startTime` and 
`endTime` parameters.
-
-## Read Path
-
-In `segmentController.open()` → `loadSegments()` (`segment.go:477-510`, 
`segment.go:718-746`):
-
-1. Read the metadata file and try to parse as JSON.
-2. If `endTime` field is present → use it directly.
-3. If absent (old format, plain version string) → fall back to current 
computation (next segment's start or `NextTime(start)`).
-
-Detection heuristic: if file content starts with `{`, parse as JSON; otherwise 
treat as plain version string.
-
-`checkVersion` validation is integrated into the new `readSegmentMeta` 
function.
-
-## Backward Compatibility
-
-- `currentVersion` bumped to `"1.5.0"`.
-- `"1.5.0"` added to `versions.yml` compatible versions list.
-- `"1.4.0"` remains in the list — old segments are fully readable.
-- No migration tool required. Old segments compute end time dynamically via 
fallback. As segments expire via TTL, they are naturally replaced by new-format 
segments.
-
-## Snapshots
-
-`TakeFileSnapshot` (`tsdb.go:265-318`) hardlinks the `metadata` file as-is. No 
change needed — the JSON format is copied identically. Restored segments read 
their own `endTime` from their metadata.
-
-## Files Modified
-
-| File | Change |
-|------|--------|
-| `banyand/internal/storage/version.go` | Bump `currentVersion` to `"1.5.0"`, 
add `segmentMeta` struct, add `readSegmentMeta()` |
-| `banyand/internal/storage/versions.yml` | Add `"1.5.0"` to compatible 
versions |
-| `banyand/internal/storage/segment.go` | `create()`: write JSON metadata; 
`open()`: read `endTime` from metadata with fallback |
-| `banyand/internal/storage/segment_test.go` | Update test helpers to write 
JSON metadata |
-
-## Files Not Changed
-
-- `segment.go` method signatures — `openSegment` already accepts `startTime, 
endTime`
-- `tsdb.go` snapshot logic — hardlinks work as-is
-- `storage.go` — no changes to interval or time range types

Reply via email to