chore: initialize EverOS 1.0.0
md-first memory extraction framework for AI agents. Markdown is the single source of truth; SQLite holds state and LanceDB provides the rebuildable vector + BM25 + scalar index. The codebase follows a single-direction DDD layering (entrypoints -> service -> memory -> infra, with component / core / config cross-cutting) enforced by import-linter. Engineering surface: - Coding conventions in .claude/rules/ (path-scoped) and workflows in .claude/skills/ (/commit, /new-branch, /pr). - GitHub Actions CI runs make lint + test + integration; pre-commit mirrors the gates locally (ruff, hygiene hooks, gitlint commit-msg). - Commit messages follow Conventional Commits, enforced by gitlint. - make lint also enforces datetime two-zone discipline and OpenAPI drift.
This commit is contained in:
138
scripts/check_datetime_discipline.py
Normal file
138
scripts/check_datetime_discipline.py
Normal file
@ -0,0 +1,138 @@
|
||||
"""CI gate: enforce the two-zone discipline at the source-code level.
|
||||
|
||||
Scans `src/` for code patterns that bypass
|
||||
:mod:`everos.component.utils.datetime` and would silently introduce
|
||||
naive or local-tz datetimes. Exits non-zero on any hit.
|
||||
|
||||
Forbidden patterns (with a small allowlist):
|
||||
|
||||
1. ``datetime.now()`` / ``datetime.utcnow()`` / ``datetime.today()`` —
|
||||
naive constructors / deprecated. Use ``get_utc_now()`` (storage) or
|
||||
``get_now_with_timezone()`` (display).
|
||||
2. ``time.time()`` / ``time.time_ns()`` — bypasses the helper module.
|
||||
Use ``to_timestamp_ms(get_utc_now())`` if you really need ms epoch.
|
||||
3. Direct ``datetime(YYYY, ...)`` constructor without ``tzinfo=`` —
|
||||
produces naive datetimes; use ``ensure_utc(datetime(...))`` instead.
|
||||
4. ``.astimezone(`` / ``.replace(tzinfo=`` outside the helper module —
|
||||
should go through ``to_display_tz`` / ``ensure_utc``.
|
||||
|
||||
Allowlist (legitimate uses):
|
||||
|
||||
* ``src/everos/component/utils/datetime.py`` — the helper module itself.
|
||||
* ``src/everos/core/persistence/sqlite/base.py`` — the SQLAlchemy ``load``
|
||||
event listener that re-attaches UTC on hydrate.
|
||||
|
||||
Run::
|
||||
|
||||
python scripts/check_datetime_discipline.py
|
||||
|
||||
Wired into ``make ci``; any violation fails the build.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
_SRC = _ROOT / "src"
|
||||
|
||||
_ALLOWLIST: set[Path] = {
|
||||
_ROOT / "src/everos/component/utils/datetime.py",
|
||||
_ROOT / "src/everos/core/persistence/sqlite/base.py",
|
||||
}
|
||||
|
||||
# (regex, message) pairs. Each regex must match on a single line.
|
||||
_HELPER_HINT = "use get_utc_now() / get_now_with_timezone()"
|
||||
_PATTERNS: list[tuple[re.Pattern[str], str]] = [
|
||||
(
|
||||
re.compile(r"\bdatetime\.now\s*\(\s*\)"),
|
||||
f"datetime.now() returns naive — {_HELPER_HINT}",
|
||||
),
|
||||
(
|
||||
re.compile(r"\bdatetime\.utcnow\s*\("),
|
||||
"datetime.utcnow() is deprecated and naive — use get_utc_now()",
|
||||
),
|
||||
(
|
||||
re.compile(r"\bdatetime\.today\s*\("),
|
||||
"datetime.today() returns naive — use today_with_timezone()",
|
||||
),
|
||||
(
|
||||
re.compile(r"\bdt\.datetime\.now\s*\(\s*\)"),
|
||||
f"dt.datetime.now() returns naive — {_HELPER_HINT}",
|
||||
),
|
||||
(
|
||||
re.compile(r"\bdt\.datetime\.utcnow\s*\("),
|
||||
"dt.datetime.utcnow() is deprecated and naive — use get_utc_now()",
|
||||
),
|
||||
(
|
||||
re.compile(r"\b_dt\.datetime\.now\s*\(\s*\)"),
|
||||
f"_dt.datetime.now() returns naive — {_HELPER_HINT}",
|
||||
),
|
||||
(
|
||||
re.compile(r"\btime\.time(?:_ns)?\s*\("),
|
||||
"time.time() bypasses the helper — use to_timestamp_ms(get_utc_now())",
|
||||
),
|
||||
(
|
||||
re.compile(r"\.astimezone\s*\("),
|
||||
".astimezone(...) outside helper — use to_display_tz() / ensure_utc()",
|
||||
),
|
||||
(
|
||||
re.compile(r"\.replace\s*\(\s*tzinfo\s*="),
|
||||
".replace(tzinfo=...) outside helper — use ensure_utc() / to_display_tz()",
|
||||
),
|
||||
]
|
||||
|
||||
# Skip lines that match these (comments, docstrings, `# tz-noqa`).
|
||||
_COMMENT_RE = re.compile(r"^\s*#")
|
||||
_DOCSTRING_TRIPLE = '"""'
|
||||
|
||||
|
||||
def _scan_file(path: Path) -> list[tuple[int, str, str]]:
|
||||
"""Return list of (line_no, line, message) violations in *path*."""
|
||||
if path in _ALLOWLIST:
|
||||
return []
|
||||
hits: list[tuple[int, str, str]] = []
|
||||
try:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
return []
|
||||
|
||||
# Strip out triple-quoted blocks (docstrings + multi-line literals).
|
||||
text_no_docstrings = re.sub(r'""".*?"""', "", text, flags=re.DOTALL)
|
||||
text_no_docstrings = re.sub(r"'''.*?'''", "", text_no_docstrings, flags=re.DOTALL)
|
||||
|
||||
for lineno, line in enumerate(text_no_docstrings.splitlines(), start=1):
|
||||
if _COMMENT_RE.match(line):
|
||||
continue
|
||||
if "# tz-noqa" in line:
|
||||
continue
|
||||
# Strip inline trailing comment to avoid false positives in
|
||||
# comment text like ``# replace(tzinfo=...) — explanation``.
|
||||
code_part = line.split("#", 1)[0]
|
||||
for pat, msg in _PATTERNS:
|
||||
if pat.search(code_part):
|
||||
hits.append((lineno, line.rstrip(), msg))
|
||||
break
|
||||
return hits
|
||||
|
||||
|
||||
def main() -> int:
|
||||
rc = 0
|
||||
for py in sorted(_SRC.rglob("*.py")):
|
||||
violations = _scan_file(py)
|
||||
if not violations:
|
||||
continue
|
||||
rel = py.relative_to(_ROOT)
|
||||
for lineno, line, msg in violations:
|
||||
print(f"{rel}:{lineno}: {msg}")
|
||||
print(f" {line}")
|
||||
rc = 1
|
||||
if rc == 0:
|
||||
print("OK — datetime discipline clean.")
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user