mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
refactor(daily): replace slug with session_id for daily note identification (#266)
- Rename slug parameter to session_id across daily note operations - Update validation function from validate_slug to validate_session_id - Change data structure keys from slug to session_id in note objects - Modify file paths to use session_id instead of slug in daily folder - Update documentation and comments to reflect session_id terminology - Adjust test cases to use session_id parameter instead of slug - Change default frontmatter to include empty description field - Update configuration files to use session_id parameter name - Modify scan_notes function to return session_id instead of slug
This commit is contained in:
parent
ef22bfb071
commit
9ee2f0f7ab
7 changed files with 99 additions and 571 deletions
|
|
@ -109,19 +109,19 @@ jobs:
|
|||
|
||||
daily_create:
|
||||
backend: base
|
||||
description: "Provision a note slug under a daily folder: daily/<date>/<slug>.md"
|
||||
description: "Provision a session note under a daily folder: daily/<date>/<session_id>.md"
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
slug:
|
||||
session_id:
|
||||
type: string
|
||||
description: "the file stem: event name or topic name"
|
||||
description: "the session identifier (also the file stem)"
|
||||
date:
|
||||
type: string
|
||||
description: "YYYY-MM-DD; empty = today"
|
||||
default: ""
|
||||
required:
|
||||
- slug
|
||||
- session_id
|
||||
steps:
|
||||
- backend: daily_create_step
|
||||
|
||||
|
|
|
|||
|
|
@ -1,474 +0,0 @@
|
|||
service:
|
||||
backend: http
|
||||
|
||||
daily_dir: daily
|
||||
digest_dir: digest
|
||||
resource_dir: ""
|
||||
|
||||
jobs:
|
||||
version:
|
||||
backend: base
|
||||
description: "return reme package version"
|
||||
parameters:
|
||||
type: object
|
||||
properties: {}
|
||||
steps:
|
||||
- backend: version_step
|
||||
|
||||
health_check:
|
||||
backend: base
|
||||
description: "return a concise health-check snapshot of reme components"
|
||||
parameters:
|
||||
type: object
|
||||
properties: {}
|
||||
steps:
|
||||
- backend: health_check_step
|
||||
|
||||
help:
|
||||
backend: base
|
||||
description: "list all registered jobs with their metadata"
|
||||
parameters:
|
||||
type: object
|
||||
properties: {}
|
||||
steps:
|
||||
- backend: help_step
|
||||
|
||||
traverse:
|
||||
backend: base
|
||||
description: "Walk the wikilink graph from a path."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "path"
|
||||
depth:
|
||||
type: integer
|
||||
description: "hop limit"
|
||||
default: 1
|
||||
direction:
|
||||
type: string
|
||||
enum:
|
||||
- forward
|
||||
- backward
|
||||
- both
|
||||
default: both
|
||||
required:
|
||||
- path
|
||||
steps:
|
||||
- backend: traverse_step
|
||||
|
||||
reindex:
|
||||
backend: base
|
||||
description: "wipe the file store and rebuild it from the watcher's tracked files"
|
||||
parameters:
|
||||
type: object
|
||||
properties: {}
|
||||
steps:
|
||||
- backend: clear_and_scan_step
|
||||
- backend: update_index_step
|
||||
persist: true
|
||||
|
||||
index_changes:
|
||||
backend: base
|
||||
description: "apply a batch of file changes (added/modified/deleted) into file_store"
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
changes:
|
||||
type: array
|
||||
description: "list of change items"
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
change:
|
||||
type: string
|
||||
enum: [added, modified, deleted]
|
||||
description: "type of file change"
|
||||
path:
|
||||
type: string
|
||||
description: "absolute file path"
|
||||
required:
|
||||
- change
|
||||
- path
|
||||
required:
|
||||
- changes
|
||||
steps:
|
||||
- backend: index_changes_step
|
||||
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
# ATOMIC TOOLS — same surface plugins/reme-{service,expert} expose
|
||||
# ════════════════════════════════════════════════════════════════════
|
||||
|
||||
# ── Retrieve ───────────────────────────────────────────────────────
|
||||
search:
|
||||
backend: base
|
||||
description: "Hybrid vault search (vector + BM25, RRF-fused)."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: "search query"
|
||||
limit:
|
||||
type: integer
|
||||
description: "max results"
|
||||
default: 5
|
||||
min_score:
|
||||
type: number
|
||||
description: "min fused score"
|
||||
default: 0.0
|
||||
required:
|
||||
- query
|
||||
steps:
|
||||
- backend: search_step
|
||||
vector_weight: 0.7
|
||||
candidate_multiplier: 3.0
|
||||
expand_links: true
|
||||
max_links_per_direction: 10
|
||||
|
||||
|
||||
|
||||
# ── Read Operations ───────────────────────────────────────────────────────────
|
||||
list:
|
||||
backend: base
|
||||
description: "List files under a vault path."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative dir; empty = root"
|
||||
default: ""
|
||||
recursive:
|
||||
type: boolean
|
||||
description: "recurse"
|
||||
default: false
|
||||
limit:
|
||||
type: integer
|
||||
description: "max results"
|
||||
default: 100
|
||||
steps:
|
||||
- backend: list_step
|
||||
|
||||
read:
|
||||
backend: base
|
||||
description: "Read a markdown file under the vault."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path; markdown only"
|
||||
start_line:
|
||||
type: integer
|
||||
description: "first line (1-based, inclusive)"
|
||||
end_line:
|
||||
type: integer
|
||||
description: "last line (1-based, inclusive)"
|
||||
required:
|
||||
- path
|
||||
steps:
|
||||
- backend: read_step
|
||||
|
||||
stat:
|
||||
backend: base
|
||||
description: "Stat a vault file (size, mtime, exists, is_dir, is_file)."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
required:
|
||||
- path
|
||||
steps:
|
||||
- backend: stat_step
|
||||
|
||||
frontmatter_read:
|
||||
backend: base
|
||||
description: "Read a file's YAML frontmatter as a dict."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
required:
|
||||
- path
|
||||
steps:
|
||||
- backend: frontmatter_read_step
|
||||
|
||||
# ── Write Operations──────────────────────────────────────────────────────────
|
||||
write:
|
||||
backend: base
|
||||
description: "Write a markdown file (create or overwrite) with name/description frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path; markdown only"
|
||||
name:
|
||||
type: string
|
||||
description: "frontmatter name"
|
||||
description:
|
||||
type: string
|
||||
description: "frontmatter description"
|
||||
content:
|
||||
type: string
|
||||
description: "body"
|
||||
required:
|
||||
- path
|
||||
- name
|
||||
- description
|
||||
- content
|
||||
steps:
|
||||
- backend: write_step
|
||||
|
||||
edit:
|
||||
backend: base
|
||||
description: "Find-and-replace in a markdown file (all occurrences)."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
old:
|
||||
type: string
|
||||
description: "text to find"
|
||||
new:
|
||||
type: string
|
||||
description: "replacement"
|
||||
default: ""
|
||||
required:
|
||||
- path
|
||||
- old
|
||||
- new
|
||||
steps:
|
||||
- backend: edit_step
|
||||
|
||||
append:
|
||||
backend: base
|
||||
description: "Append content to a markdown file."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
content:
|
||||
type: string
|
||||
description: "content to append"
|
||||
required:
|
||||
- path
|
||||
- content
|
||||
steps:
|
||||
- backend: append_step
|
||||
|
||||
frontmatter_update:
|
||||
backend: base
|
||||
description: "Merge keys into a file's YAML frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
metadata:
|
||||
type: object
|
||||
description: "keys to merge"
|
||||
additionalProperties: true
|
||||
required:
|
||||
- path
|
||||
- metadata
|
||||
steps:
|
||||
- backend: frontmatter_update_step
|
||||
|
||||
frontmatter_delete:
|
||||
backend: base
|
||||
description: "Drop keys from a file's YAML frontmatter."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
keys:
|
||||
type: array
|
||||
description: "keys to remove"
|
||||
items:
|
||||
type: string
|
||||
required:
|
||||
- path
|
||||
- keys
|
||||
steps:
|
||||
- backend: frontmatter_delete_step
|
||||
|
||||
delete:
|
||||
backend: base
|
||||
description: "Delete a vault file or folder; returns surviving inbound wikilinks."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "vault-relative path"
|
||||
required:
|
||||
- path
|
||||
steps:
|
||||
- backend: delete_step
|
||||
|
||||
upload_resource:
|
||||
backend: base
|
||||
description: "Ingest an external-channel asset into resource/<today>/ with provenance."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
path:
|
||||
type: string
|
||||
description: "host source path"
|
||||
channel:
|
||||
type: string
|
||||
description: "channel id (wechat / email / browser / api / ...)"
|
||||
description:
|
||||
type: string
|
||||
description: "what the asset is and how to interpret it"
|
||||
metadata:
|
||||
type: object
|
||||
description: "extra provenance keys (e.g. source)"
|
||||
default: {}
|
||||
required:
|
||||
- path
|
||||
- channel
|
||||
- description
|
||||
steps:
|
||||
- backend: upload_resource_step
|
||||
|
||||
download:
|
||||
backend: base
|
||||
description: "Copy a vault file out to the host filesystem."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
src_path:
|
||||
type: string
|
||||
description: "vault-relative source"
|
||||
dst_path:
|
||||
type: string
|
||||
description: "host absolute dest; empty = temp file"
|
||||
default: ""
|
||||
overwrite:
|
||||
type: boolean
|
||||
description: "overwrite if dst exists"
|
||||
default: false
|
||||
required:
|
||||
- src_path
|
||||
steps:
|
||||
- backend: download_step
|
||||
|
||||
# ── Daily Operations (slug provisioning + day-index rollup) ──────────
|
||||
daily_create:
|
||||
backend: base
|
||||
description: "Provision daily/<date>/<slug>.md (empty body, frontmatter {name: slug}); idempotent; refreshes the day index."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
slug:
|
||||
type: string
|
||||
description: "note slug"
|
||||
date:
|
||||
type: string
|
||||
description: "ISO date; empty = today"
|
||||
default: ""
|
||||
required:
|
||||
- slug
|
||||
steps:
|
||||
- backend: daily_create_step
|
||||
|
||||
daily_list:
|
||||
backend: base
|
||||
description: "List notes under a single day."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
description: "ISO date; empty = today"
|
||||
default: ""
|
||||
steps:
|
||||
- backend: daily_list_step
|
||||
|
||||
daily_reindex:
|
||||
backend: base
|
||||
description: "Rebuild the day-index page daily/<date>.md."
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
date:
|
||||
type: string
|
||||
description: "ISO date; empty = today"
|
||||
default: ""
|
||||
steps:
|
||||
- backend: daily_reindex_step
|
||||
|
||||
watch_file:
|
||||
backend: background
|
||||
watch_paths:
|
||||
- MEMORY.md
|
||||
- memory
|
||||
suffix_filters:
|
||||
- md
|
||||
steps:
|
||||
- backend: scan_changes_step
|
||||
- backend: update_index_step
|
||||
persist: true
|
||||
- backend: watch_changes_step
|
||||
dispatch_step: update_index_step
|
||||
|
||||
components:
|
||||
tokenizer:
|
||||
default:
|
||||
backend: regex
|
||||
|
||||
embedding_model:
|
||||
default:
|
||||
backend: ${EMBEDDING_BACKEND:-openai}
|
||||
api_key: ${EMBEDDING_API_KEY}
|
||||
base_url: ${EMBEDDING_BASE_URL:-https://api.openai.com/v1}
|
||||
model_name: ${EMBEDDING_MODEL_NAME:-text-embedding-v4}
|
||||
dimensions: 1024
|
||||
|
||||
file_graph:
|
||||
default:
|
||||
backend: local
|
||||
|
||||
file_parser:
|
||||
linked:
|
||||
backend: linked
|
||||
supported_extensions:
|
||||
- md
|
||||
chunked:
|
||||
backend: chunked
|
||||
supported_extensions:
|
||||
- txt
|
||||
- html
|
||||
- json
|
||||
- yaml
|
||||
- py
|
||||
default:
|
||||
backend: default
|
||||
|
||||
keyword_index:
|
||||
default:
|
||||
backend: bm25
|
||||
tokenizer: default
|
||||
|
||||
file_store:
|
||||
default:
|
||||
backend: local
|
||||
store_name: local
|
||||
embedding_model: default
|
||||
keyword_index: default
|
||||
file_graph: default
|
||||
|
|
@ -4,10 +4,10 @@ Two related concerns, both private to the ``crud`` package:
|
|||
|
||||
1. **Generic file IO** — path gating, encoding-aware read/write, output
|
||||
truncation (used by every CRUD step that touches the filesystem).
|
||||
2. **Daily-note helpers** — slug validation + ``daily/<date>.md`` index
|
||||
rebuild (used by the ``daily_*`` steps). The day index is a derived
|
||||
rollup page auto-managed in marker-delimited sections; user-edited
|
||||
manual sections are preserved verbatim across refreshes.
|
||||
2. **Daily-note helpers** — session_id validation + ``daily/<date>.md``
|
||||
index rebuild (used by the ``daily_*`` steps). The day index is a
|
||||
derived rollup page auto-managed in marker-delimited sections;
|
||||
user-edited manual sections are preserved verbatim across refreshes.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
|
@ -140,7 +140,7 @@ def validate_filename_component(name: str, *, kind: str = "filename") -> str | N
|
|||
for callers that validate per component)
|
||||
|
||||
``kind`` is the human-readable label inserted into error messages
|
||||
(e.g. ``"slug"``, ``"path component"``).
|
||||
(e.g. ``"session_id"``, ``"path component"``).
|
||||
"""
|
||||
if not name:
|
||||
return f"{kind} is required"
|
||||
|
|
@ -382,16 +382,16 @@ def truncate_text_output(
|
|||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Daily-note helpers: slug validation + day-index rebuild
|
||||
# Daily-note helpers: session_id validation + day-index rebuild
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Slug validation
|
||||
# ---------------
|
||||
# Session ID validation
|
||||
# ---------------------
|
||||
|
||||
|
||||
def validate_slug(slug: str) -> str | None:
|
||||
"""Validate a daily-note slug. Thin wrapper over :func:`validate_filename_component`."""
|
||||
return validate_filename_component(slug, kind="slug")
|
||||
def validate_session_id(session_id: str) -> str | None:
|
||||
"""Validate a daily-note session_id. Thin wrapper over :func:`validate_filename_component`."""
|
||||
return validate_filename_component(session_id, kind="session_id")
|
||||
|
||||
|
||||
# Day-index rebuild
|
||||
|
|
@ -447,26 +447,26 @@ def scan_notes(vault_dir: Path, date: str, daily_dir: str) -> list[dict]:
|
|||
|
||||
Returns one dict per note::
|
||||
|
||||
{"slug": str, "path": str, "metadata": dict}
|
||||
{"session_id": str, "path": str, "metadata": dict}
|
||||
|
||||
``metadata`` is the raw frontmatter dict (insertion-ordered);
|
||||
consumers decide which keys to surface. Each ``.md`` directly
|
||||
under the day folder is a note; the file's stem is the slug.
|
||||
under the day folder is a note; the file's stem is the session_id.
|
||||
"""
|
||||
date_dir = vault_dir / daily_dir / date
|
||||
if not date_dir.is_dir():
|
||||
return []
|
||||
out: list[dict] = []
|
||||
for md_path in sorted(p for p in date_dir.iterdir() if p.is_file() and p.suffix == ".md"):
|
||||
slug = md_path.stem
|
||||
session_id = md_path.stem
|
||||
try:
|
||||
post = frontmatter.loads(md_path.read_text(encoding="utf-8"))
|
||||
except Exception: # pylint: disable=broad-except
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"slug": slug,
|
||||
"path": f"{daily_dir}/{date}/{slug}.md",
|
||||
"session_id": session_id,
|
||||
"path": f"{daily_dir}/{date}/{session_id}.md",
|
||||
"metadata": dict(post.metadata or {}),
|
||||
},
|
||||
)
|
||||
|
|
@ -492,7 +492,7 @@ async def refresh_day_index(file_store, date: str, daily_dir: str) -> dict:
|
|||
index_abs = vault_dir / index_rel
|
||||
notes = scan_notes(vault_dir, date, daily_dir)
|
||||
|
||||
notes_payload = [{"path": n["path"], "slug": n["slug"], "metadata": n["metadata"]} for n in notes]
|
||||
notes_payload = [{"path": n["path"], "session_id": n["session_id"], "metadata": n["metadata"]} for n in notes]
|
||||
|
||||
if not notes and not index_abs.is_file():
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
"""``daily_create`` — provision a note slug under a daily folder: ``daily/<date>/<slug>.md``.
|
||||
"""``daily_create`` — provision a session note under a daily folder: ``daily/<date>/<session_id>.md``.
|
||||
|
||||
Minimal slug provisioner. Validates the slug, mkdirs the day folder,
|
||||
writes an empty-body note with frontmatter ``{name: slug}`` if (and
|
||||
only if) the file does not already exist, refreshes the day index,
|
||||
and returns the vault-relative path.
|
||||
Validates the session_id, mkdirs the day folder, writes an empty-body
|
||||
note with frontmatter ``{name: session_id}`` if (and only if) the file
|
||||
does not already exist, refreshes the day index, and returns the
|
||||
vault-relative path.
|
||||
|
||||
Idempotent: when the note already exists this is a no-op write (the
|
||||
day index still refreshes — siblings may have changed; cheap
|
||||
|
|
@ -12,12 +12,12 @@ self-healing). The caller fills the body via ``file_write`` /
|
|||
deliberately does not accept a body.
|
||||
|
||||
Inputs:
|
||||
slug (required, validated) — the note's name (also the file stem)
|
||||
date (optional, ``YYYY-MM-DD``; empty = today)
|
||||
session_id (required, validated) — the note's session identifier (also the file stem)
|
||||
date (optional, ``YYYY-MM-DD``; empty = today)
|
||||
|
||||
Outputs:
|
||||
answer = one-line human-readable status
|
||||
metadata = {date, slug, path, created, index?}
|
||||
metadata = {date, session_id, path, created, index?}
|
||||
"""
|
||||
|
||||
from datetime import date as _date
|
||||
|
|
@ -25,14 +25,14 @@ from pathlib import Path
|
|||
|
||||
import frontmatter
|
||||
|
||||
from ._file_io import refresh_day_index, validate_slug, write_file_safe
|
||||
from ._file_io import refresh_day_index, validate_session_id, write_file_safe
|
||||
from ..base_step import BaseStep
|
||||
from ...components import R
|
||||
|
||||
|
||||
@R.register("daily_create_step")
|
||||
class DailyCreateStep(BaseStep):
|
||||
"""Provision ``daily/<date>/<slug>.md`` (idempotent); refresh day index."""
|
||||
"""Provision ``daily/<date>/<session_id>.md`` (idempotent); refresh day index."""
|
||||
|
||||
def _fail(self, message: str, **meta) -> None:
|
||||
"""Mark response failed; copy ``meta`` into ``response.metadata``."""
|
||||
|
|
@ -43,24 +43,24 @@ class DailyCreateStep(BaseStep):
|
|||
self.context.response.metadata.update(meta)
|
||||
|
||||
def _collect_params(self) -> tuple[str, str, str]:
|
||||
"""Read ``slug`` + ``date`` from context; default ``date`` today, ``daily_dir`` from app config."""
|
||||
"""Read ``session_id`` + ``date`` from context; default ``date`` today, ``daily_dir`` from app config."""
|
||||
assert self.context is not None
|
||||
slug = self.context.get("slug", "")
|
||||
session_id = self.context.get("session_id", "")
|
||||
day = self.context.get("date", "") or _date.today().strftime("%Y-%m-%d")
|
||||
daily_dir = self.app_context.app_config.daily_dir if self.app_context is not None else "daily"
|
||||
return slug, day, daily_dir
|
||||
return session_id, day, daily_dir
|
||||
|
||||
@staticmethod
|
||||
def _empty_note_text(slug: str) -> str:
|
||||
"""Serialize an empty-body markdown note with frontmatter ``{name: slug}``; trailing newline guaranteed."""
|
||||
text = frontmatter.dumps(frontmatter.Post("", name=slug))
|
||||
def _empty_note_text(session_id: str) -> str:
|
||||
"""Serialize an empty-body markdown note with frontmatter ``{name, description}``; trailing newline."""
|
||||
text = frontmatter.dumps(frontmatter.Post("", name=session_id, description=""))
|
||||
return text if text.endswith("\n") else text + "\n"
|
||||
|
||||
async def _create_if_missing(self, path_abs: Path, slug: str) -> bool:
|
||||
async def _create_if_missing(self, path_abs: Path, session_id: str) -> bool:
|
||||
"""Write the empty note only when the file is absent. Returns ``True`` iff a new file was created."""
|
||||
if path_abs.is_file():
|
||||
return False
|
||||
await write_file_safe(path_abs, self._empty_note_text(slug), encoding="utf-8")
|
||||
await write_file_safe(path_abs, self._empty_note_text(session_id), encoding="utf-8")
|
||||
return True
|
||||
|
||||
def _set_success(self, payload: dict, created: bool) -> None:
|
||||
|
|
@ -71,24 +71,27 @@ class DailyCreateStep(BaseStep):
|
|||
self.context.response.metadata.update(payload)
|
||||
|
||||
async def execute(self):
|
||||
"""Validate the slug, provision the note file, refresh the day index, stamp the response."""
|
||||
"""Validate the session_id, provision the note file, refresh the day index, stamp the response."""
|
||||
assert self.context is not None
|
||||
slug, day, daily_dir = self._collect_params()
|
||||
session_id, day, daily_dir = self._collect_params()
|
||||
|
||||
err = validate_slug(slug)
|
||||
err = validate_session_id(session_id)
|
||||
if err:
|
||||
self._fail(err)
|
||||
return None
|
||||
|
||||
path_rel = f"{daily_dir}/{day}/{slug}.md"
|
||||
path_rel = f"{daily_dir}/{day}/{session_id}.md"
|
||||
path_abs = (self.vault_path / path_rel).resolve()
|
||||
try:
|
||||
created = await self._create_if_missing(path_abs, slug)
|
||||
created = await self._create_if_missing(path_abs, session_id)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
self._fail(f"create failed: {e}", date=day, slug=slug, path=path_rel)
|
||||
self._fail(f"create failed: {e}", date=day, session_id=session_id, path=path_rel)
|
||||
return None
|
||||
|
||||
index = await refresh_day_index(self.file_store, day, daily_dir)
|
||||
self._set_success({"date": day, "slug": slug, "path": path_rel, "created": created, "index": index}, created)
|
||||
self._set_success(
|
||||
{"date": day, "session_id": session_id, "path": path_rel, "created": created, "index": index},
|
||||
created,
|
||||
)
|
||||
self.logger.info(f"[{self.name}] {'created' if created else 'reused'} path={path_rel}")
|
||||
return self.context.response
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""``daily_list`` — list the notes under a single day (pure read, no side effects).
|
||||
|
||||
Returns one row per ``daily/<date>/<slug>.md`` note file with its
|
||||
vault-relative ``path``, ``slug``, and the raw ``metadata`` dict
|
||||
(full frontmatter). Sorted by slug for stable output.
|
||||
Returns one row per ``daily/<date>/<session_id>.md`` note file with its
|
||||
vault-relative ``path``, ``session_id``, and the raw ``metadata`` dict
|
||||
(full frontmatter). Sorted by session_id for stable output.
|
||||
|
||||
**Does NOT refresh** ``daily/<date>.md`` — call ``daily_reindex``
|
||||
explicitly when the index page needs to be rebuilt. Decoupling
|
||||
|
|
@ -35,7 +35,7 @@ class DailyListStep(BaseStep):
|
|||
@staticmethod
|
||||
def _project(note: dict) -> dict:
|
||||
"""Keep only the user-facing keys (drop internal scan_notes fields, if any)."""
|
||||
return {"path": note["path"], "slug": note["slug"], "metadata": note["metadata"]}
|
||||
return {"path": note["path"], "session_id": note["session_id"], "metadata": note["metadata"]}
|
||||
|
||||
@staticmethod
|
||||
def _format_note_line(note: dict) -> str:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""``daily_reindex_step`` — rebuild ``daily/<date>.md`` from its notes.
|
||||
"""``daily_reindex_step`` — rebuild ``daily/<date>.md`` from its session notes.
|
||||
|
||||
The day index ``daily/<date>.md`` is a derived artifact whose job is to
|
||||
list and describe every note file under ``daily/<date>/``. It is auto-
|
||||
refreshed by ``daily_create``. Generic ops like ``file_write`` /
|
||||
list and describe every session note file under ``daily/<date>/``. It is
|
||||
auto-refreshed by ``daily_create``. Generic ops like ``file_write`` /
|
||||
``file_append`` / ``frontmatter_update`` leave it stale — this step
|
||||
is the standalone writer to call after batch flows (historical
|
||||
backfill, drift recovery, end-of-batch consolidation, or a
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ provision / listing / index-rebuild operations. Body authoring and
|
|||
frontmatter mutations are generic CRUD (covered in test_crud_steps
|
||||
and test_property_steps).
|
||||
|
||||
A daily note is the single file ``daily/<YYYY-MM-DD>/<slug>.md`` (no
|
||||
folder, no sibling materials). ``daily_create`` is a minimal slug
|
||||
provisioner: it validates the slug, writes an empty-body note with
|
||||
default ``{name: slug}`` frontmatter when the file is absent, and
|
||||
A daily note is the single file ``daily/<YYYY-MM-DD>/<session_id>.md``
|
||||
(no folder, no sibling materials). ``daily_create`` validates the
|
||||
session_id, writes an empty-body note with default
|
||||
``{name: session_id}`` frontmatter when the file is absent, and
|
||||
refreshes the day index. When the file already exists it is a no-op
|
||||
write (``created=False``) — the body is filled in afterwards via
|
||||
``file_write`` / ``file_edit`` / ``frontmatter_update`` or a native
|
||||
|
|
@ -69,17 +69,17 @@ def _today() -> str:
|
|||
async def _make_store_with_dailies(entries: list[tuple[str, str, str]]) -> LocalFileStore:
|
||||
"""Seed the vault with daily notes.
|
||||
|
||||
entries: list of (date, slug, body). Each tuple creates
|
||||
``daily/<date>/<slug>.md`` with a minimal ``name``-only
|
||||
entries: list of (date, session_id, body). Each tuple creates
|
||||
``daily/<date>/<session_id>.md`` with a minimal ``name``-only
|
||||
frontmatter — no opinionated status / lifecycle axes.
|
||||
"""
|
||||
store = LocalFileStore(name="t", embedding_model="")
|
||||
await store.start()
|
||||
for day, slug, body in entries:
|
||||
for day, session_id, body in entries:
|
||||
day_dir = Path.cwd() / "daily" / day
|
||||
day_dir.mkdir(parents=True, exist_ok=True)
|
||||
text = f"---\nname: {slug}\n---\n{body}\n"
|
||||
(day_dir / f"{slug}.md").write_text(text, encoding="utf-8")
|
||||
text = f"---\nname: {session_id}\n---\n{body}\n"
|
||||
(day_dir / f"{session_id}.md").write_text(text, encoding="utf-8")
|
||||
return store
|
||||
|
||||
|
||||
|
|
@ -87,15 +87,15 @@ def _metadata(step) -> dict:
|
|||
return step.context.response.metadata
|
||||
|
||||
|
||||
async def _seed_note(date: str, slug: str, name: str = "", description: str = "") -> None:
|
||||
"""Write ``daily/<date>/<slug>.md`` with optional frontmatter."""
|
||||
async def _seed_note(date: str, session_id: str, name: str = "", description: str = "") -> None:
|
||||
"""Write ``daily/<date>/<session_id>.md`` with optional frontmatter."""
|
||||
day_dir = Path.cwd() / "daily" / date
|
||||
day_dir.mkdir(parents=True, exist_ok=True)
|
||||
fm_lines = [f"name: {name or slug}"]
|
||||
fm_lines = [f"name: {name or session_id}"]
|
||||
if description:
|
||||
fm_lines.append(f"description: {description}")
|
||||
text = "---\n" + "\n".join(fm_lines) + "\n---\nbody\n"
|
||||
(day_dir / f"{slug}.md").write_text(text, encoding="utf-8")
|
||||
(day_dir / f"{session_id}.md").write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
# -- daily_list_step ----------------------------------------------------------
|
||||
|
|
@ -151,8 +151,8 @@ def test_daily_list_filters_by_date():
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_daily_list_returns_path_slug_metadata():
|
||||
"""Each note row exposes path / slug / metadata (full frontmatter dict)."""
|
||||
def test_daily_list_returns_path_session_id_metadata():
|
||||
"""Each note row exposes path / session_id / metadata (full frontmatter dict)."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
|
|
@ -173,7 +173,7 @@ def test_daily_list_returns_path_slug_metadata():
|
|||
assert "Alpha Project" in answer
|
||||
assert "JWT auth migration" in answer
|
||||
await store.close()
|
||||
print("✓ test_daily_list_returns_path_slug_metadata passed")
|
||||
print("✓ test_daily_list_returns_path_session_id_metadata passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
|
@ -272,19 +272,19 @@ def test_daily_list_response_shape():
|
|||
|
||||
|
||||
def test_daily_create_provisions_note_and_refreshes_index():
|
||||
"""Fresh slug ⇒ empty-body note with ``{name: slug}`` + day index refreshed."""
|
||||
"""Fresh session_id ⇒ empty-body note with ``{name: session_id}`` + day index refreshed."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
await step(slug="kickoff", date="2026-05-18")
|
||||
await step(session_id="kickoff", date="2026-05-18")
|
||||
payload = _metadata(step)
|
||||
|
||||
assert step.context.response.success is True
|
||||
assert payload["created"] is True
|
||||
assert payload["date"] == "2026-05-18"
|
||||
assert payload["slug"] == "kickoff"
|
||||
assert payload["session_id"] == "kickoff"
|
||||
assert payload["path"] == "daily/2026-05-18/kickoff.md"
|
||||
|
||||
note = Path(tmp) / "daily" / "2026-05-18" / "kickoff.md"
|
||||
|
|
@ -314,7 +314,7 @@ def test_daily_create_is_idempotent_on_existing():
|
|||
before = file_path.read_text(encoding="utf-8")
|
||||
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
await step(slug="ongoing", date="2026-05-18")
|
||||
await step(session_id="ongoing", date="2026-05-18")
|
||||
payload = _metadata(step)
|
||||
|
||||
assert step.context.response.success is True
|
||||
|
|
@ -335,7 +335,7 @@ def test_daily_create_default_date_is_today():
|
|||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
await step(slug="today-task")
|
||||
await step(session_id="today-task")
|
||||
payload = _metadata(step)
|
||||
assert payload["date"] == _today()
|
||||
assert payload["path"] == f"daily/{_today()}/today-task.md"
|
||||
|
|
@ -346,55 +346,54 @@ def test_daily_create_default_date_is_today():
|
|||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_daily_create_default_frontmatter_uses_slug_as_name():
|
||||
"""The provisioned note's frontmatter is ``{name: slug}`` (no body)."""
|
||||
def test_daily_create_default_frontmatter_uses_session_id_as_name():
|
||||
"""The provisioned note's frontmatter is ``{name: session_id, description: ''}`` (no body)."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
await step(slug="auth-refactor", date="2026-05-18")
|
||||
await step(session_id="auth-refactor", date="2026-05-18")
|
||||
|
||||
note = Path(tmp) / "daily" / "2026-05-18" / "auth-refactor.md"
|
||||
text = note.read_text(encoding="utf-8")
|
||||
assert "name: auth-refactor" in text
|
||||
# No description in default frontmatter.
|
||||
assert "description:" not in text
|
||||
assert "description:" in text
|
||||
await store.close()
|
||||
print("✓ test_daily_create_default_frontmatter_uses_slug_as_name passed")
|
||||
print("✓ test_daily_create_default_frontmatter_uses_session_id_as_name passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_daily_create_rejects_invalid_slug():
|
||||
"""Slug validation runs before any IO; no day folder is created on reject."""
|
||||
def test_daily_create_rejects_invalid_session_id():
|
||||
"""session_id validation runs before any IO; no day folder is created on reject."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
for bad in ("foo/bar", "foo:bar", "CON", "lpt9", "foo.", " bar"):
|
||||
await step(slug=bad, date="2026-05-18")
|
||||
await step(session_id=bad, date="2026-05-18")
|
||||
assert step.context.response.success is False, f"expected reject for {bad!r}"
|
||||
assert not (Path(tmp) / "daily" / "2026-05-18").exists()
|
||||
await store.close()
|
||||
print("✓ test_daily_create_rejects_invalid_slug passed")
|
||||
print("✓ test_daily_create_rejects_invalid_session_id passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
def test_daily_create_rejects_empty_slug():
|
||||
"""Empty / missing slug is rejected with a clear message."""
|
||||
def test_daily_create_rejects_empty_session_id():
|
||||
"""Empty / missing session_id is rejected with a clear message."""
|
||||
|
||||
async def run():
|
||||
with tempfile.TemporaryDirectory() as tmp, temp_chdir(tmp):
|
||||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
await step(slug="", date="2026-05-18")
|
||||
await step(session_id="", date="2026-05-18")
|
||||
assert step.context.response.success is False
|
||||
assert "slug" in (step.context.response.answer or "").lower()
|
||||
assert "session_id" in (step.context.response.answer or "").lower()
|
||||
await store.close()
|
||||
print("✓ test_daily_create_rejects_empty_slug passed")
|
||||
print("✓ test_daily_create_rejects_empty_session_id passed")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
|
@ -407,14 +406,14 @@ def test_daily_create_then_skip_round_trip():
|
|||
store = await _make_store_with_dailies([])
|
||||
step = daily_create_step.DailyCreateStep(file_store=store)
|
||||
|
||||
await step(slug="probe", date="2026-05-18")
|
||||
await step(session_id="probe", date="2026-05-18")
|
||||
first = _metadata(step)
|
||||
assert first["created"] is True
|
||||
|
||||
note = Path(tmp) / "daily" / "2026-05-18" / "probe.md"
|
||||
before = note.read_text(encoding="utf-8")
|
||||
|
||||
await step(slug="probe", date="2026-05-18")
|
||||
await step(session_id="probe", date="2026-05-18")
|
||||
second = _metadata(step)
|
||||
assert second["created"] is False
|
||||
assert note.read_text(encoding="utf-8") == before
|
||||
|
|
@ -465,8 +464,8 @@ def test_day_index_includes_note_descriptions():
|
|||
("beta", "beta", "调研增值税新政对 SaaS 的影响"),
|
||||
("gamma", "Gamma", ""),
|
||||
]
|
||||
for slug, name, description in cases:
|
||||
await _seed_note("2026-05-18", slug, name=name, description=description)
|
||||
for sid, name, description in cases:
|
||||
await _seed_note("2026-05-18", sid, name=name, description=description)
|
||||
|
||||
await daily_reindex_step.DailyReindexStep(file_store=store)(date="2026-05-18")
|
||||
text = _day_index_text(tmp, "2026-05-18")
|
||||
|
|
@ -601,7 +600,7 @@ if __name__ == "__main__":
|
|||
print("\n=== Daily step tests ===")
|
||||
test_daily_list_default_date_is_today()
|
||||
test_daily_list_filters_by_date()
|
||||
test_daily_list_returns_path_slug_metadata()
|
||||
test_daily_list_returns_path_session_id_metadata()
|
||||
test_daily_list_ignores_subdirectories()
|
||||
test_daily_list_empty_when_no_daily_dir()
|
||||
test_daily_list_does_not_refresh_index()
|
||||
|
|
@ -609,9 +608,9 @@ if __name__ == "__main__":
|
|||
test_daily_create_provisions_note_and_refreshes_index()
|
||||
test_daily_create_is_idempotent_on_existing()
|
||||
test_daily_create_default_date_is_today()
|
||||
test_daily_create_default_frontmatter_uses_slug_as_name()
|
||||
test_daily_create_rejects_invalid_slug()
|
||||
test_daily_create_rejects_empty_slug()
|
||||
test_daily_create_default_frontmatter_uses_session_id_as_name()
|
||||
test_daily_create_rejects_invalid_session_id()
|
||||
test_daily_create_rejects_empty_session_id()
|
||||
test_daily_create_then_skip_round_trip()
|
||||
test_day_index_lists_each_note()
|
||||
test_day_index_includes_note_descriptions()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue