← Files note Workspace|ROGNALIAARCHIVED FILE
skills/note-source-log/scripts/append_log.py
7.72 KB · Oct 2, 2026 · 00:33 UTC
#!/usr/bin/env python3
"""Append one exact source input to a user-owned workspace."""
from __future__ import annotations
import argparse
import json
import os
import sys
import uuid
from pathlib import Path
from typing import Optional
from source_common import (
INPUT_TYPES,
LOG_ID_PATTERN,
SCHEMA_VERSION,
VISIBILITIES,
SourceDataError,
append_jsonl,
canonical_hash,
exclusive_lock,
load_log_records,
parse_timestamp,
raw_log_payload,
read_text_input,
require_id,
resolve_workspace,
text_hash,
validate_log_records,
)
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Append an exact source entry to a note Workspace."
)
parser.add_argument("workspace", help="Absolute path to the selected workspace")
parser.add_argument(
"--input-file", required=True, help="UTF-8 input path, or - to read stdin"
)
parser.add_argument("--input-type", required=True, choices=sorted(INPUT_TYPES))
parser.add_argument("--request-id", required=True)
parser.add_argument("--visibility", choices=sorted(VISIBILITIES), default="confirm_before_use")
parser.add_argument("--parent-log-id")
parser.add_argument("--follow-up-question-file")
parser.add_argument("--conversation-id", help="Stable ID for a diary conversation")
parser.add_argument("--speaker", choices=("user", "assistant"))
parser.add_argument("--timestamp", help="ISO 8601 timestamp override")
parser.add_argument("--log-id", help="Deterministic log ID for tests or migrations")
return parser
def _ensure_month_file(workspace: Path, moment: object) -> Path:
year = workspace / "primary-log" / moment.strftime("%Y")
if year.exists() and (not year.is_dir() or year.is_symlink()):
raise SourceDataError("monthly log directory is unsafe")
year.mkdir(mode=0o700, exist_ok=True)
target = year / f"{moment.strftime('%Y-%m')}.jsonl"
if target.exists() and (not target.is_file() or target.is_symlink()):
raise SourceDataError("monthly log file is unsafe")
if not target.exists():
try:
descriptor = os.open(target, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
except FileExistsError:
pass
else:
os.close(descriptor)
return target
def main(argv: Optional[list[str]] = None) -> int:
args = _parser().parse_args(argv)
try:
if args.input_file == "-" and args.follow_up_question_file == "-":
raise SourceDataError("stdin cannot provide both input and follow-up question")
workspace = resolve_workspace(args.workspace)
if bool(args.conversation_id) != bool(args.speaker):
raise SourceDataError("--conversation-id and --speaker must be used together")
if args.conversation_id:
require_id(args.conversation_id, "conversation_id")
if args.follow_up_question_file:
raise SourceDataError("save questions as separate assistant records")
if args.speaker == "assistant" and args.input_type != "text":
raise SourceDataError("assistant records require --input-type text")
if args.speaker == "assistant" and args.visibility != "private":
raise SourceDataError("assistant records require --visibility private")
request_id = require_id(args.request_id, "request_id")
original_text = read_text_input(args.input_file, "original_text")
question = (
read_text_input(args.follow_up_question_file, "follow_up_question")
if args.follow_up_question_file
else None
)
parent_log_id = (
require_id(args.parent_log_id, "parent_log_id", LOG_ID_PATTERN)
if args.parent_log_id
else None
)
if args.input_type == "follow_up_answer" and parent_log_id is None:
raise SourceDataError("follow_up_answer requires --parent-log-id")
if not args.conversation_id and args.input_type != "follow_up_answer" and parent_log_id is not None:
raise SourceDataError("--parent-log-id is only valid for follow_up_answer")
recorded_at, moment = parse_timestamp(args.timestamp)
log_id = require_id(
args.log_id or f"log-{moment.strftime('%Y%m%d')}-{uuid.uuid4().hex[:12]}",
"log_id",
LOG_ID_PATTERN,
)
candidate = {
"schema_version": 2 if args.conversation_id else SCHEMA_VERSION,
"log_id": log_id,
"request_id": request_id,
"recorded_at": recorded_at,
"input_type": args.input_type,
"original_text": original_text,
"parent_log_id": parent_log_id,
"follow_up_question": question,
"visibility": args.visibility,
"status": "active",
"content_sha256": text_hash(original_text),
}
if args.conversation_id:
candidate.update({"conversation_id": args.conversation_id, "speaker": args.speaker})
candidate["payload_sha256"] = canonical_hash(raw_log_payload(candidate))
with exclusive_lock(workspace / "primary-log/.append.lock", recorded_at):
records = load_log_records(workspace)
errors = validate_log_records(records)
if errors:
raise SourceDataError("existing source logs are invalid: " + "; ".join(errors))
by_request = {record["request_id"]: record for record in records}
existing = by_request.get(request_id)
if existing is not None:
if existing.get("payload_sha256") != candidate["payload_sha256"]:
raise SourceDataError("request_id is already used for different input")
print(
json.dumps(
{
"status": "duplicate",
"log_id": existing["log_id"],
"path": existing["_path"],
"validation": "pass",
"external_actions": [],
},
ensure_ascii=False,
indent=2,
)
)
return 0
if any(record["log_id"] == log_id for record in records):
raise SourceDataError("log_id already exists")
if parent_log_id is not None and not any(
record["log_id"] == parent_log_id for record in records
):
raise SourceDataError("parent_log_id does not exist")
target = workspace / "primary-log" / moment.strftime("%Y") / f"{moment.strftime('%Y-%m')}.jsonl"
candidate_with_path = dict(candidate, _path=str(target.relative_to(workspace)))
prospective = sorted(records + [candidate_with_path], key=lambda record: record["_path"])
errors = validate_log_records(prospective)
if errors:
raise SourceDataError("invalid source entry: " + "; ".join(errors))
target = _ensure_month_file(workspace, moment)
append_jsonl(target, candidate)
print(
json.dumps(
{
"status": "created",
"log_id": log_id,
"path": str(target.relative_to(workspace)),
"validation": "pass",
"external_actions": [],
},
ensure_ascii=False,
indent=2,
)
)
return 0
except (OSError, ValueError, json.JSONDecodeError) as exc:
print(json.dumps({"status": "error", "error": str(exc)}, ensure_ascii=False), file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
SHA-256: 09380ee452b41207773b2ea96f268e265fff8a7574bf2fda153b3b393031bb09