📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# Audit report template
|
||||
|
||||
`audit` writes its output to `.lore/audit/audit-YYYY-MM-DD.md`. This file is read-only with respect to `.lore/*.md` (see main `SKILL.md` Conflict resolution — audit never mutates and never ALERTs).
|
||||
|
||||
## Template
|
||||
|
||||
```markdown
|
||||
# Memory Audit Report
|
||||
|
||||
> Date: 2026-07-09
|
||||
> Total entries audited: <N>
|
||||
> Findings: <X> CONFLICT, <Y> STALE, <Z> UNVERIFIED
|
||||
|
||||
## Global (`_global/`)
|
||||
|
||||
### CONFLICT
|
||||
- [CONV-2026-01-20-b1e8] claims "all packages TypeScript strict mode"
|
||||
Evidence: `packages/legacy/tsconfig.json` has `"strict": false`
|
||||
|
||||
### STALE
|
||||
- [ARCH-2026-01-15-d7a3] references `nx.json`
|
||||
Evidence: file no longer exists at repo root
|
||||
|
||||
### UNVERIFIED
|
||||
- [DEC-2026-02-03-7c19] last verified 2025-09-12 (>90 days)
|
||||
|
||||
## Scope: frontend
|
||||
|
||||
### CONFLICT
|
||||
- ...
|
||||
|
||||
### STALE
|
||||
- ...
|
||||
|
||||
### UNVERIFIED
|
||||
- ...
|
||||
|
||||
## Summary
|
||||
|
||||
Recommended action: run `lore sync` to address these findings.
|
||||
Audit itself does not modify any entry.
|
||||
```
|
||||
|
||||
## Severity definitions
|
||||
|
||||
| Severity | Meaning |
|
||||
|---|---|
|
||||
| `CONFLICT` | Code/config directly contradicts the entry content (e.g. memory says `react@18`, `package.json` says `16`) |
|
||||
| `STALE` | Entry references a resource (file, API, version) that no longer exists |
|
||||
| `UNVERIFIED` | Entry's `#verified` date is >90 days; needs re-confirmation |
|
||||
|
||||
## Required rules
|
||||
|
||||
- The audit report **never** modifies any `.lore/*.md` file.
|
||||
- The audit report **never** emits ALERT blocks (ALERT noise is contained to `sync` and `query`).
|
||||
- Audit is a pure read-and-report operation. To act on findings, the user runs `sync`.
|
||||
|
||||
## Evidence format
|
||||
|
||||
Each finding includes a one-line `Evidence:` reference pointing to the file path and (when possible) line number that triggered the finding. The agent must verify the evidence exists before writing the report.
|
||||
@@ -0,0 +1,228 @@
|
||||
# Compatibility policy
|
||||
|
||||
This document defines how `lore` evolves without breaking existing user projects. It is the contract between current users and future maintainers. Any change to `.lore/` structure, file formats, Python scripts, mirror templates, or this skill's reference docs must conform to these rules.
|
||||
|
||||
## Three principles
|
||||
|
||||
1. **Add, never subtract.** New fields, scripts, sections, and reference docs always use new names. Removal happens only after the deprecation cycle (below).
|
||||
2. **Readers are forward-compatible.** An older skill reading a newer `.lore/` ignores unknown fields, unknown files, and unknown tags. It never errors on unfamiliar content.
|
||||
3. **Writers are backward-compatible (during transition).** A newer skill detecting an older `.lore/` runs a migration step before writing. It never overwrites old data with new defaults.
|
||||
|
||||
## Layer-specific rules
|
||||
|
||||
### Layer 1: `.lore/.config.json` schema
|
||||
|
||||
- `schema_version` is **required** (integer). See `references/config.md` for handling missing/newer/older values.
|
||||
- Adding a new field = bump `schema_version` to N+1; old fields stay; the field is added with its default value at first read.
|
||||
- Removing a field requires the deprecation cycle (one schema version's worth of warnings before hard removal).
|
||||
- Renaming a field: write the new field, copy the value, mark the old one with `_deprecated: "reason"`. The migration tool handles this in `migrate.py`.
|
||||
|
||||
### Layer 2: Entry format
|
||||
|
||||
```
|
||||
- [ARCH-2026-07-10-a3f2] Entry text; reason. #added:2026-07-10 #verified:2026-07-15
|
||||
```
|
||||
|
||||
- IDs (`LAYER-DATE-HASH`) are stable as long as the entry text is unchanged. Editing an entry produces a new ID; old ID stays in history (via git) for `history` queries.
|
||||
- Tag set is a closed set today: `#added`, `#verified`, `#stale`, `#archived`. Adding a new tag is allowed; old skills' tag parsers (which match `(added|verified|stale|archived)`) silently ignore unknown tags.
|
||||
- **Never make a tag required.** Required tags break every old entry in every old `.lore/`.
|
||||
|
||||
### Layer 3: `.lore/` directory structure
|
||||
|
||||
Current canonical layout:
|
||||
|
||||
```
|
||||
.lore/
|
||||
├── SUMMARY.md
|
||||
├── _global/
|
||||
├── scopes/
|
||||
├── draft/ (init only — temporary)
|
||||
├── audit/ (audit only)
|
||||
└── archive/ (referenced in spec; reserved for future)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Adding a new top-level directory (e.g., `rejected/` for rejected entries) is non-breaking.
|
||||
- Renaming an existing directory is breaking — every reference in `references/*.md`, every script, and every user's project breaks.
|
||||
- Removing a directory is breaking unless that directory was never actually written (e.g., removing `archive/` today is non-breaking because nothing writes there yet).
|
||||
|
||||
### Layer 4: Python scripts
|
||||
|
||||
Current scripts: `id_hash.py`, `list_entries.py`, `find_stale.py`, `find_duplicates.py`, `history.py`, plus planned `migrate.py`.
|
||||
|
||||
Rules:
|
||||
|
||||
- **Renaming is breaking.** All names are part of the public surface; they're referenced from `SKILL.md`, `references/*.md`, and downstream tooling. Don't rename; deprecate and add a new one if needed.
|
||||
- **Removing is breaking.** A deprecated script stays on disk with a clear "Deprecated: use `migrate.py` instead" header for one schema version.
|
||||
- **Adding a new script is non-breaking.** Reference it from `SKILL.md` reference index on introduction.
|
||||
- **Changing output format is breaking for `--json` consumers.** Add `--v2-output` or a new flag; old flag keeps old behavior forever.
|
||||
|
||||
### Layer 5: Platform mirror files
|
||||
|
||||
Mirror files (`CLAUDE.md`, `.cursorrules`, `AGENTS.md`, etc.) follow this contract:
|
||||
|
||||
```markdown
|
||||
## Lore (auto-managed)
|
||||
|
||||
... lore content ...
|
||||
|
||||
---
|
||||
## My notes (free edit)
|
||||
|
||||
... user content (preserved verbatim) ...
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `## Lore (auto-managed)` is a **contract string**. Never rename; never remove. Mirror detection regexes depend on it.
|
||||
- `## My notes (free edit)` is a **contract string**. Never rename; never remove. User-written content depends on it.
|
||||
- The content between `## Lore` and `---` is lore's domain; content after `---` is the user's. Respect the boundary on every regeneration.
|
||||
- Adding a new auto-managed section (e.g., `## Sync history (auto-managed)`) is allowed; insert before `---`. Old skills ignore it.
|
||||
- Changing the index template body (e.g., adding a "Last mirror:" line) is non-breaking: content-based dedup means unchanged mirrors are not rewritten, so old mirrors stay valid.
|
||||
- **Backward-write safety**: if the existing mirror has no `## My notes (free edit)` section (e.g., a legacy single-section mirror from a pre-v1 project), the first `lore mirror` run must **append** an empty My notes section rather than overwriting the file.
|
||||
|
||||
### Layer 6: reference docs
|
||||
|
||||
Current docs: `entry-format.md`, `summary-template.md`, `audit-template.md`, `monorepo-detection.md`, `stale-new-markers.md`, `platform-mirrors.md`, `config.md`, `history-command.md`, `compatibility.md` (this file).
|
||||
|
||||
Rules:
|
||||
|
||||
- **Renaming a reference doc is breaking.** Every external link (issue trackers, blog posts, README badges) breaks. Add a redirect stub instead.
|
||||
- **Splitting a doc** (e.g., `platform-mirrors.md` → `mirror-index.md` + `mirror-takeover.md`) requires a stub at the old path that points to the new location. Update `SKILL.md` reference index on the same commit.
|
||||
- **Removing a doc** is breaking. Mark it `<!-- DEPRECATED: see new-location.md -->` for one schema version, then move to `archive/` (in `references/`, not in `.lore/`).
|
||||
- **Adding a doc** is non-breaking. Add to `SKILL.md` reference index on introduction.
|
||||
|
||||
## Migration tool
|
||||
|
||||
`scripts/migrate.py` does not exist in v1. It will be added on the first `schema_version` bump.
|
||||
|
||||
**Triggers that will ship the first migration:**
|
||||
|
||||
- A new required field is added to `.lore/.config.json`.
|
||||
- A field is renamed or its accepted values are tightened.
|
||||
- The entry ID algorithm changes.
|
||||
|
||||
Until any of these happen, `schema_version: 1` is the only version and no migration is possible or needed.
|
||||
|
||||
Until the script ships, `list_entries.py` emits a one-time warning to stderr if `.lore/.config.json` is missing the `schema_version` field. This nudges users to add the field manually before the first breaking change ships, so future upgrades can detect the version mismatch correctly.
|
||||
|
||||
**Template — to be implemented when the first migration is needed:**
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""Migrate .lore/.config.json from version N to N+1.
|
||||
|
||||
Idempotent: running twice produces no further changes.
|
||||
Refuses to run if schema_version > EXPECTED_FROM.
|
||||
"""
|
||||
import json, sys
|
||||
from pathlib import Path
|
||||
|
||||
EXPECTED_FROM = 1 # versions this script can read
|
||||
TARGET = 2 # current schema version after migration
|
||||
|
||||
|
||||
def migrate(config: dict) -> tuple[dict, list[str]]:
|
||||
msgs = []
|
||||
# v1 → v2 transformations go here. Example:
|
||||
# if config.get("mirror_mode") == "summary":
|
||||
# config.pop("mirror_mode", None)
|
||||
# msgs.append('removed deprecated "mirror_mode": "summary"')
|
||||
config["schema_version"] = TARGET
|
||||
return config, msgs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
cfg_path = Path(".lore/.config.json")
|
||||
if not cfg_path.exists():
|
||||
print(f"error: {cfg_path} not found", file=sys.stderr)
|
||||
return 1
|
||||
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
|
||||
current = cfg.get("schema_version", 1)
|
||||
if current > EXPECTED_FROM:
|
||||
print(
|
||||
f"error: schema_version={current} is newer than this skill "
|
||||
f"can read (max: {EXPECTED_FROM}). Pull latest lore.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
if current < TARGET:
|
||||
print(f"migrating v{current} → v{TARGET}...")
|
||||
cfg, msgs = migrate(cfg)
|
||||
for m in msgs:
|
||||
print(f" - {m}")
|
||||
cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
|
||||
print("done.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
```
|
||||
|
||||
## Deprecation workflow
|
||||
|
||||
Any capability slated for removal follows a three-stage cycle. The minimum cycle is **two schema versions** (typically 6–12 months).
|
||||
|
||||
| Stage | Schema version | Behavior |
|
||||
|---|---|---|
|
||||
| **Announce** | N | Add an entry to `references/deprecations.md` with the feature name, replacement, and removal target version. The skill prints a one-line notice when the feature is used. |
|
||||
| **Warn** | N+1 | The skill prints a louder warning (with remediation steps) and writes a `_deprecation_warnings_shown` array to `.lore/.config.json` so the warning doesn't repeat. |
|
||||
| **Remove** | N+2 | Hard delete. Old `.lore/` data is migrated by `migrate.py` to the new format. Users who skipped migrations will see explicit errors pointing at `migrate.py`. |
|
||||
|
||||
Skipping a stage is allowed only for security fixes or unreleased features that were never shipped.
|
||||
|
||||
## CI enforcement
|
||||
|
||||
A compatibility CI job (planned for `.github/workflows/compat.yml`) verifies:
|
||||
|
||||
1. **New skill reads old `.lore/`**: checkout a fixture project from `fixtures/v0-project/`, run `list_entries.py`, `history.py`, `find_stale.py` on it. Pass = no exceptions, correct counts.
|
||||
2. **Old skill reads new `.lore/`**: build a fixture with the latest schema, check out the previous release's scripts, run them. Pass = no exceptions on known fields; unknown fields silently ignored.
|
||||
3. **No rename or delete in `scripts/` or `references/`**: PR diff against `scripts/` and `references/` filenames; any removed file = failure.
|
||||
|
||||
At least one fixture project must be checked in at `fixtures/v0-project/` and re-pinned to a known old schema version after each major release.
|
||||
|
||||
## Examples
|
||||
|
||||
### Compatible change (additive)
|
||||
|
||||
Adding a new `compress_thresholds.max_entries_per_scope` field:
|
||||
|
||||
- Bump `schema_version` to 2.
|
||||
- `migrate.py` v1 → v2: add the new field with default `100` if absent.
|
||||
- Old skill reads v2 config: sees only the fields it knows; ignores `max_entries_per_scope`.
|
||||
- New skill reads v1 config: detects `schema_version` mismatch, refuses to write until migration runs.
|
||||
|
||||
### Incompatible change (avoid)
|
||||
|
||||
Renaming `mirror_mode` to `render_mode`:
|
||||
|
||||
- Every existing `.lore/.config.json` would silently lose its `mirror_mode: "index"` setting on next migration (old field dropped, new field absent → defaults kick in).
|
||||
- Bad. Instead: add `render_mode` as the new canonical field; mark `mirror_mode` as `_deprecated`; keep both for one schema version; eventually remove `mirror_mode` via the deprecation cycle.
|
||||
|
||||
### Breaking change (deprecation cycle required)
|
||||
|
||||
Removing support for `mirror_mode: "full"`:
|
||||
|
||||
- v1: ship `mirror_mode: "full"` as deprecated; full mode still works.
|
||||
- v2: print warning when `"full"` is set; suggest migrating to `"index"`.
|
||||
- v3: hard reject `"full"`; `migrate.py` auto-converts to `"index"`.
|
||||
- Each version's release notes link to the deprecation entry in `references/deprecations.md`.
|
||||
|
||||
## Decision checklist
|
||||
|
||||
Before merging any change to lore, answer these questions:
|
||||
|
||||
1. Does this change add, modify, or remove anything in `.lore/`?
|
||||
2. Does this change add, modify, or remove any script in `scripts/`?
|
||||
3. Does this change add, modify, or remove any contract string (`## Lore (auto-managed)`, `## My notes (free edit)`, etc.)?
|
||||
4. Does this change add, modify, or remove any reference doc filename?
|
||||
5. Does this change add, modify, or remove any entry tag?
|
||||
|
||||
If any answer is "modify" or "remove", the change requires either:
|
||||
- A migration step in `migrate.py` (for schema changes)
|
||||
- A deprecation cycle (for removals)
|
||||
- An entry in `references/deprecations.md`
|
||||
|
||||
If all answers are "add" or "no", the change is non-breaking and can ship as a minor or patch release.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Configuration reference
|
||||
|
||||
`.lore/.config.json` holds user-tunable settings. The file is optional; without it, the skill uses sensible defaults.
|
||||
|
||||
## Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": 1,
|
||||
"auto_mirror": true | false,
|
||||
"sync_updates_mirror": true | false,
|
||||
"sync_trust": "high" | "medium" | "low",
|
||||
"mirror_targets": ["CLAUDE.md"], // optional — auto-detected if absent
|
||||
"mirror_mode": "index",
|
||||
"compress_thresholds": {
|
||||
"max_entries": 500,
|
||||
"max_days_since_compress": 30
|
||||
},
|
||||
"sync_thresholds": {
|
||||
"min_lines_changed": 50,
|
||||
"min_directories_changed": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Schema version (`schema_version`)
|
||||
|
||||
**Required for new configs** (set automatically by `lore init`). Tracks the schema version of `.lore/.config.json` so future releases can detect old configs before writing.
|
||||
|
||||
- **Missing** → treated as `schema_version: 1`. A `[WARN]` notice is printed to stderr by `list_entries.py`; add the field manually to silence it.
|
||||
- **Equal to skill's expected version** → use as-is.
|
||||
- **Lower than expected** → refuse to write; ask the user to run the migration script shipped with that future release.
|
||||
- **Higher than expected** → refuse to read with an error; the user's skill is older than their `.lore/`. They need to upgrade lore (pull latest from upstream) before continuing.
|
||||
|
||||
For the full compatibility policy (migration tools, deprecation cycle, reader/writer contracts), see `references/compatibility.md`.
|
||||
|
||||
## Field semantics
|
||||
|
||||
### `auto_mirror`
|
||||
|
||||
Default: `false`.
|
||||
|
||||
Controls whether `compress` and `lore mirror` regenerate platform mirrors automatically after the canonical change is accepted.
|
||||
|
||||
- `true` — regenerate mirrors automatically
|
||||
- `false` — ask per target before writing
|
||||
|
||||
Note: this flag does **not** affect `sync`. By default `sync` does not touch mirrors at all (see `sync_updates_mirror`).
|
||||
|
||||
### `sync_updates_mirror`
|
||||
|
||||
Default: `false`.
|
||||
|
||||
Controls whether `sync` regenerates platform mirrors as a side effect.
|
||||
|
||||
- `false` — `sync` only writes `.lore/*.md`. Mirrors are updated by `compress` or explicit `lore mirror`. This is the recommended setting to avoid cluttering `git log` of mirror files.
|
||||
- `true` — `sync` regenerates mirrors (with content-based dedup) after the canonical change is accepted. Restore this setting if the old "update everything on every sync" behavior is preferred.
|
||||
|
||||
### `sync_trust`
|
||||
|
||||
Default: `"medium"`.
|
||||
|
||||
Controls how much confirmation `sync` requires for individual change types.
|
||||
|
||||
- `"high"` — auto-apply everything, including `NEW` and `STALE`. Only `ALERT` blocks interrupt.
|
||||
- `"medium"` — auto-apply low-risk changes (de-duplicate hits, equivalent REFINEDs). `NEW`, `STALE`, and `ALERT` require confirmation.
|
||||
- `"low"` — every change requires confirmation, including de-duplicate hits and equivalent REFINEDs.
|
||||
|
||||
### `mirror_targets`
|
||||
|
||||
Default: auto-detected at runtime (see "If absent" below).
|
||||
|
||||
Array of file paths (relative to project root) that should be kept in sync with `.lore/*`. Path must match one of the platform entries in `references/platform-mirrors.md`. Unsupported paths trigger a warning at config-load time.
|
||||
|
||||
If absent: mirror targets are auto-detected at runtime by scanning the project root for existing platform files. If no platform files exist, the user is asked via a multi-select question during `init`, the first `mirror` call, or `compress` (when `auto_mirror: true`). See `references/platform-mirrors.md` for the resolution algorithm.
|
||||
|
||||
If present: used verbatim. Empty array `[]` is valid and disables mirror generation.
|
||||
|
||||
When auto-detection is in effect, `lore init` populates this field with the user's selections so subsequent runs are silent.
|
||||
|
||||
### `mirror_mode`
|
||||
|
||||
Default: `"index"`.
|
||||
|
||||
Only `"index"` is accepted. The mirror renders a small index structure pointing into `.lore/` (see `references/platform-mirrors.md` for the template and adaptive rendering rules). Per-session token cost stays flat (~500 B) regardless of project size.
|
||||
|
||||
Any other value (e.g., the historical `"summary"` or `"full"`) is rejected at config-load time with an error. Remove the field, or set it to `"index"`.
|
||||
|
||||
### `compress_thresholds`
|
||||
|
||||
Defaults: `{"max_entries": 500, "max_days_since_compress": 30}`.
|
||||
|
||||
`sync` checks these silently and emits a `[COMPRESS NOTICE]` when tripped. See `SKILL.md` sync procedure.
|
||||
|
||||
### `sync_thresholds`
|
||||
|
||||
Defaults: `{"min_lines_changed": 50, "min_directories_changed": 2}`.
|
||||
|
||||
`sync` only proposes an update when at least one trigger threshold is met (see `SKILL.md` sync trigger threshold). Lowering these values means `sync` proposes updates more often.
|
||||
|
||||
## Editing the config
|
||||
|
||||
Edit `.lore/.config.json` directly. After editing:
|
||||
|
||||
- `sync` and `compress` re-read the config on every run; no restart needed.
|
||||
- Invalid JSON → fall back to defaults + warn the user.
|
||||
- After editing, verify with `python scripts/list_entries.py --config-check` (added in a future migration).
|
||||
- For schema version changes, see `references/compatibility.md`.
|
||||
|
||||
## Upgrade path
|
||||
|
||||
When a future lore release introduces the first `schema_version` bump:
|
||||
|
||||
1. That release ships `scripts/migrate.py` for the specific version bump.
|
||||
2. The skill prompts: "Your `.lore/.config.json` is v1; lore now expects v2. Run `python scripts/migrate.py` to upgrade."
|
||||
3. `migrate.py` is idempotent — running it twice is a no-op.
|
||||
4. After migration, the file is updated in place; no manual editing needed.
|
||||
|
||||
In v1, no migration has shipped and `scripts/migrate.py` does not exist. Add `"schema_version": 1` manually to old configs to silence the warning.
|
||||
@@ -0,0 +1,70 @@
|
||||
# Entry format reference
|
||||
|
||||
Detailed specification for `.lore/` entries. The main `SKILL.md` covers entry structure briefly; this file is the full spec.
|
||||
|
||||
## Bullet structure
|
||||
|
||||
Each entry is a Markdown bullet (≤ 2 lines), containing:
|
||||
|
||||
- **Layer prefix**: `ARCH`, `DEC`, or `CONV`
|
||||
- **ID**: `LAYER-YYYY-MM-DD-xxxx` where `xxxx` is a 4-char content hash
|
||||
- **Inline status tags** (at the end of the entry)
|
||||
|
||||
```markdown
|
||||
- [ARCH-2026-07-09-a3f2] Use Next.js App Router; reason: streaming + RSC. #added:2026-07-09
|
||||
- [DEC-2026-02-03-7c19] Chose Zustand over Redux; reason: 60% less boilerplate. Alternatives: Redux Toolkit, Jotai. #added:2026-02-03
|
||||
- [CONV-2026-01-20-b1e8] Never commit secrets; use `dotenv` + `.env.local` (gitignored). #added:2026-01-20
|
||||
- [ARCH-2026-03-10-a1b2] Use TanStack Query for all server state. #added:2026-03-10 #verified:2026-06-15
|
||||
```
|
||||
|
||||
## ID generation
|
||||
|
||||
The 4-char `xxxx` is the first 4 hex chars of `sha256(entry text)`. This makes IDs:
|
||||
|
||||
- **Deterministic**: rewriting the same fact produces the same ID
|
||||
- **Conflict-free** under concurrent writes by multiple agents
|
||||
- **Reverse-lookup-able** by audit tools
|
||||
|
||||
If two entries have identical content (hash collision, statistically rare), add a distinguishing word to one and recompute.
|
||||
|
||||
## Tag specification
|
||||
|
||||
| Tag | Meaning |
|
||||
|---|---|
|
||||
| `#added:YYYY-MM-DD` | When the entry was created |
|
||||
| `#verified:YYYY-MM-DD` | Last time a human or audit confirmed the entry is still true |
|
||||
| `#stale:YYYY-MM-DD` | Flagged by `sync` as superseded or contradicted; user decides keep/archive |
|
||||
| `#archived:YYYY-MM-DD` | Moved to `archive/` |
|
||||
|
||||
Multiple tags can co-exist on one entry (e.g. `#added:2026-01-15 #verified:2026-06-01`).
|
||||
|
||||
## Cross-file references
|
||||
|
||||
When `SUMMARY.md` or another file references an entry, qualify it with the file path to avoid ID collisions across scopes:
|
||||
|
||||
```
|
||||
[scopes/frontend/DECISIONS.md#DEC-2026-02-03-7c19]
|
||||
[_global/CONVENTIONS.md#CONV-2026-01-20-b1e8]
|
||||
```
|
||||
|
||||
The path is relative to `.lore/`.
|
||||
|
||||
## Splitting vs. single entries
|
||||
|
||||
If a fact can't fit in ≤ 2 lines, split into multiple entries and cross-reference them by ID:
|
||||
|
||||
```markdown
|
||||
- [ARCH-2026-07-09-a3f2] Use Next.js App Router. #added:2026-07-09
|
||||
- [DEC-2026-07-09-b1e8] Reason: streaming + RSC, see [ARCH-2026-07-09-a3f2]. #added:2026-07-09
|
||||
```
|
||||
|
||||
Instead of stuffing them into a single overly long bullet.
|
||||
|
||||
## What counts as "atomic"
|
||||
|
||||
A fact is atomic if it answers exactly one question:
|
||||
|
||||
- "What is the frontend framework?" → `ARCH` entry about Next.js
|
||||
- "Why Next.js not Remix?" → `DEC` entry referencing the `ARCH` entry
|
||||
|
||||
If your entry answers two questions, split it.
|
||||
@@ -0,0 +1,106 @@
|
||||
# `lore history` — full specification
|
||||
|
||||
Read-only command. Lists git commits related to a memory entry, a file,
|
||||
or a scope, since the entry's `#added` date. Output to stdout only;
|
||||
never writes to `.lore/`.
|
||||
|
||||
## Synopsis
|
||||
|
||||
```
|
||||
lore history <entry-id>
|
||||
lore history <file-path>
|
||||
lore history --scope=<name>
|
||||
lore history --since=<YYYY-MM-DD>
|
||||
lore history --json
|
||||
```
|
||||
|
||||
## Forms
|
||||
|
||||
| Form | Argument shape | Example | Behavior |
|
||||
|---|---|---|---|
|
||||
| Entry | `[A-Z]+-\d{4}-\d{2}-\d{2}-[a-f0-9]{4}` | `lore history DEC-2026-02-03-7c19` | Locate entry in `.lore/`, derive its `#added` date and code file, then `git log` since that date. |
|
||||
| File | contains `/` or starts with `.` | `lore history frontend/src/store/index.ts` | Run `git log --since=1970-01-01` on the given path. |
|
||||
| Scope | `--scope=<name>` only | `lore history --scope=frontend` | For each `*.md` in `.lore/scopes/<name>/`, run file form on the lore file path itself. |
|
||||
|
||||
## Code-file resolution (entry form)
|
||||
|
||||
Priority:
|
||||
|
||||
1. First backtick-quoted path in entry.text that looks like a file
|
||||
(e.g. `src/store/index.ts`).
|
||||
2. Scope directory at the project root (e.g. entry scope `frontend` →
|
||||
`frontend/`).
|
||||
3. Project root `.` for entries in `_global/`.
|
||||
|
||||
If the regex finds no path, falls back to the scope directory.
|
||||
|
||||
## Data source
|
||||
|
||||
`git` CLI only. No network calls. Requires:
|
||||
|
||||
- A git repository at or above the current working directory.
|
||||
- The `git` executable on `PATH`.
|
||||
|
||||
## Output
|
||||
|
||||
### Markdown (default)
|
||||
|
||||
Header block: `Entry`, `Since`, `File`, `Commits`. One section per
|
||||
commit with `## <short-hash> (<date>, <author>)`, subject, optional
|
||||
`Body:` line, optional `Refs:` line. A "Suggested next step" footer
|
||||
appears only when at least one commit is found.
|
||||
|
||||
### JSON (`--json`)
|
||||
|
||||
```json
|
||||
{
|
||||
"entry_id": "DEC-2026-02-03-7c19",
|
||||
"lore_file": "scopes/frontend/DECISIONS.md",
|
||||
"code_file": "frontend/src/store/index.ts",
|
||||
"since": "2026-02-03",
|
||||
"since_source": "entry_added",
|
||||
"commits": [
|
||||
{
|
||||
"hash": "...",
|
||||
"short": "abc1234",
|
||||
"author": "alice",
|
||||
"date": "2026-04-12",
|
||||
"subject": "Use Zustand v4",
|
||||
"body": "Migrate notes here.",
|
||||
"refs": ["#234"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
| Condition | Exit code | Message |
|
||||
|---|---|---|
|
||||
| No argument | 2 | `error: missing argument` |
|
||||
| Unrecognized argument | 2 | `error: unrecognized argument: <arg>` |
|
||||
| `.lore/` not found | 2 | `error: .lore/ not found. Run 'lore init' first.` |
|
||||
| Entry not in index | 3 | `error: Entry <id> not found. Available: ...` |
|
||||
| Not a git repo | 4 | `error: Not a git repository. ...` |
|
||||
| `git` missing | 5 | `error: git executable not found on PATH.` |
|
||||
| Bad scope name | 6 | `error: Scope '<name>' not found. Available: ...` |
|
||||
| `git log` failure | 7 | `error: git log failed: <stderr>` |
|
||||
| Entry missing `#added` | 0 (warning) | `warning: entry has no #added tag; using full history` |
|
||||
|
||||
## Exit codes summary
|
||||
|
||||
- `0` — success (including "0 commits found" case)
|
||||
- `2` — usage / configuration error
|
||||
- `3` — entry lookup failure
|
||||
- `4` — not a git repo
|
||||
- `5` — git CLI missing
|
||||
- `6` — invalid scope
|
||||
- `7` — git command failed
|
||||
|
||||
## Why this exists
|
||||
|
||||
`lore sync` reads `git diff` (working-tree deltas). It never reads
|
||||
commit history. `lore history` fills that gap: given a memory entry,
|
||||
it shows the commits that introduced or modified the underlying code,
|
||||
letting the agent answer "why does this decision exist?" with a pointer
|
||||
to the original commit instead of an LLM-generated guess.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Monorepo detection rules
|
||||
|
||||
`init` needs to identify whether the project is a monorepo and how to split scopes. This file lists the detection rules per tool.
|
||||
|
||||
## Detection order
|
||||
|
||||
Check markers in this order; the first match determines scope layout:
|
||||
|
||||
1. pnpm workspaces
|
||||
2. Yarn workspaces
|
||||
3. npm workspaces
|
||||
4. Lerna
|
||||
5. Nx
|
||||
6. Rush
|
||||
7. Cargo workspaces
|
||||
8. Go workspaces
|
||||
9. Bazel
|
||||
|
||||
No monorepo marker → fall back to `_global/` only (single-scope project).
|
||||
|
||||
## Per-tool rules
|
||||
|
||||
### pnpm workspaces
|
||||
- Marker: `pnpm-workspace.yaml` at repo root
|
||||
- Read: `packages:` field, e.g. `packages: [frontend, backend, shared/*]`
|
||||
- One scope per listed package directory
|
||||
|
||||
### Yarn workspaces (classic / berry)
|
||||
- Marker: `package.json` top-level `workspaces` field
|
||||
- Example: `"workspaces": ["packages/*"]`
|
||||
- One scope per glob-resolved directory
|
||||
|
||||
### npm workspaces
|
||||
- Same as Yarn (npm 7+ uses the same `package.json#workspaces` field)
|
||||
|
||||
### Lerna
|
||||
- Marker: `lerna.json`
|
||||
- Read: `packages` field (array of paths)
|
||||
- One scope per path
|
||||
|
||||
### Nx
|
||||
- Marker: `nx.json` or `workspace.json`
|
||||
- Nx typically delegates package discovery to npm/yarn workspaces — read both
|
||||
- One scope per resolved package
|
||||
|
||||
### Rush
|
||||
- Marker: `rush.json`
|
||||
- Read: `projects` array (each entry has a `packageName` and directory)
|
||||
|
||||
### Cargo workspaces
|
||||
- Marker: `Cargo.toml` top-level `[workspace]` table
|
||||
- Read: `members` array
|
||||
- One scope per member crate
|
||||
|
||||
### Go workspaces
|
||||
- Marker: `go.work`
|
||||
- Read: `use` directives (one per module)
|
||||
- One scope per module
|
||||
|
||||
### Bazel
|
||||
- Marker: `MODULE.bazel` or `WORKSPACE`
|
||||
- Bazel repos are deeply nested; precise extraction is fragile. Fallback: collapse to one scope per top-level directory and let the user override.
|
||||
|
||||
## Scope naming
|
||||
|
||||
- Default: directory name (`frontend/` → scope `frontend`)
|
||||
- If multiple directories belong to one logical scope (e.g. `packages/web` and `packages/mobile` are both "frontend"), agent should ask the user whether to merge
|
||||
- Nested monorepos (`packages/web/components/`) are **not** supported as nested scopes. Flatten to `web`.
|
||||
|
||||
## When detection fails
|
||||
|
||||
If detection succeeds but the resulting scopes don't match the user's mental model, agent should:
|
||||
|
||||
1. Show the proposed scope list
|
||||
2. Let the user rename / merge / split scopes
|
||||
3. Proceed with the corrected list
|
||||
|
||||
This is part of the init confirmation step (see main `SKILL.md` init step 2).
|
||||
@@ -0,0 +1,346 @@
|
||||
# Platform mirrors reference
|
||||
|
||||
How `.lore/*` content gets mirrored to platform-specific config files. The main `SKILL.md` covers the high-level rules; this file holds the per-platform mapping, the two-section file structure, and the algorithm that resolves which files to generate (auto-detect by default, explicit override available).
|
||||
|
||||
## Platform → file mapping
|
||||
|
||||
| Platform | File (default) | Also accepted |
|
||||
|---|---|---|
|
||||
| Claude Code | `CLAUDE.md` (root) | `.claude/CLAUDE.md` |
|
||||
| Cursor | `.cursorrules` (root) | `.cursor/rules/*.mdc` |
|
||||
| Cline | `.clinerules` (root) | — |
|
||||
| Aider | `AGENTS.md` (root) | `CONVENTIONS.md` |
|
||||
| OpenAI Codex | `AGENTS.md` (root) | — |
|
||||
| OpenCode | `AGENTS.md` (root) | — |
|
||||
| Windsurf | `.windsurfrules` (root) | — |
|
||||
| GitHub Copilot | `.github/copilot-instructions.md` | — |
|
||||
| Continue.dev | `.continue/rules/lore.md` | — |
|
||||
| LangGraph / DeepAgents | (no file — inject at runtime) | — |
|
||||
|
||||
For LangGraph and DeepAgents, the skill does not produce a mirror file. Read `.lore/*.md` directly or ingest into the system prompt at runtime — that ingestion is the user's responsibility.
|
||||
|
||||
## Resolution: how `mirror_targets` is computed
|
||||
|
||||
When the skill needs to know which platform files to generate (during `init`, `mirror`, and `compress` when `auto_mirror: true`), it runs the following procedure:
|
||||
|
||||
```
|
||||
resolve_mirror_targets(config, repo_root):
|
||||
|
||||
# 1. If config has mirror_targets set, use it verbatim (auto-detect skipped)
|
||||
if "mirror_targets" in config:
|
||||
return list(config["mirror_targets"])
|
||||
|
||||
# 2. Scan repo root for existing platform files (see Scan candidates)
|
||||
detected = scan_existing_platform_files(repo_root)
|
||||
if detected:
|
||||
return detected
|
||||
|
||||
# 3. Nothing detected → ask user via multi-select, persist to config, return
|
||||
selected = ask_user_multi_select(AGENT_CHOICES)
|
||||
write_mirror_targets_to_config(selected)
|
||||
return selected
|
||||
```
|
||||
|
||||
This is the core resolution used by all three commands. `init` extends it with classification and per-file takeover steps — see "Init-time behavior (full procedure)" below.
|
||||
|
||||
### Scan candidates
|
||||
|
||||
The auto-detect step checks for the following paths at `repo_root`:
|
||||
|
||||
```
|
||||
CLAUDE.md
|
||||
.claude/CLAUDE.md
|
||||
.cursorrules
|
||||
.clinerules
|
||||
AGENTS.md
|
||||
CONVENTIONS.md
|
||||
.windsurfrules
|
||||
.github/copilot-instructions.md
|
||||
.continue/rules/lore.md
|
||||
.cursor/rules/*.mdc # glob: any .mdc file under .cursor/rules/
|
||||
```
|
||||
|
||||
These match the platform table above (default + "Also accepted" filenames). The `.cursor/rules/*.mdc` entry is a glob — it's a hit if `.cursor/rules/` exists and contains at least one `.mdc` file.
|
||||
|
||||
### Multi-select agent choices
|
||||
|
||||
When Step 3 fires, present this question to the user:
|
||||
|
||||
| Choice | Primary file written |
|
||||
|---|---|
|
||||
| Claude Code | `CLAUDE.md` |
|
||||
| Cursor | `.cursorrules` |
|
||||
| Cline | `.clinerules` |
|
||||
| Aider | `AGENTS.md` |
|
||||
| Codex | `AGENTS.md` |
|
||||
| OpenCode | `AGENTS.md` |
|
||||
| Windsurf | `.windsurfrules` |
|
||||
| GitHub Copilot | `.github/copilot-instructions.md` |
|
||||
| Continue.dev | `.continue/rules/lore.md` |
|
||||
|
||||
Aider, Codex, and OpenCode all map to `AGENTS.md`. Selecting any combination produces one entry. Selecting nothing is valid — writes `mirror_targets: []` (no mirrors generated).
|
||||
|
||||
### When this runs
|
||||
|
||||
- `lore init` — always interactive.
|
||||
- `lore mirror` when `mirror_targets` is absent — also interactive (skill is invoked through chat).
|
||||
- `lore compress` (when `auto_mirror: true`) — also goes through this resolution if `mirror_targets` is absent.
|
||||
|
||||
Both paths use the same function. Once `init` has run, `mirror_targets` is set, so subsequent `mirror` calls hit Step 1 and are silent.
|
||||
|
||||
## Two-section file structure
|
||||
|
||||
Every mirror file is split into two sections by a `---` separator. The top section is Skill-managed and rewritten on mirror regeneration. The bottom section is user-editable and preserved verbatim.
|
||||
|
||||
```markdown
|
||||
## Lore (auto-managed)
|
||||
|
||||
# .lore SUMMARY (synced 2026-07-09)
|
||||
|
||||
> Last compressed: 2026-07-09
|
||||
> Total entries: 247 across 3 scopes
|
||||
|
||||
## Global
|
||||
- Monorepo with pnpm workspaces + Turborepo — [_global/ARCHITECTURE.md#ARCH-2026-01-15-d7a3]
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
## My notes (free edit)
|
||||
|
||||
- Keep answers concise
|
||||
- Currently refactoring the user auth module
|
||||
- Prefer English
|
||||
```
|
||||
|
||||
The `---` separator is a literal Markdown horizontal rule. Both sections are plain Markdown so any agent or editor can render them normally.
|
||||
|
||||
### Section detection rules
|
||||
|
||||
When syncing a mirror file:
|
||||
|
||||
1. If the file contains `---` on its own line, that line is the boundary. Everything above is the Lore section, everything below is My notes.
|
||||
2. If the file contains a `## My notes` header, the My notes section starts at that header and goes to EOF.
|
||||
3. If neither marker is present, the entire file is treated as the Lore section (i.e. no My notes section). Subsequent sync appends a separator + empty My notes section.
|
||||
4. If the file is missing the `## Lore` header but has `## My notes`, the entire file is treated as user notes. Skill does not write to it. User is asked to confirm before sync restructures the file.
|
||||
|
||||
## Sync-time behavior
|
||||
|
||||
**`sync` does not regenerate platform mirrors.** This is intentional — see the "Mirror update triggers" section in `SKILL.md`. The skill only writes `.lore/*.md` during `sync`. To update mirrors after `sync`, the user runs `lore mirror` (or `compress`, which calls mirror generation as a side effect).
|
||||
|
||||
If a project needs the old behavior (mirror updates on every `sync`), set `sync_updates_mirror: true` in `.lore/.config.json`.
|
||||
|
||||
## Mirror-time behavior (`lore mirror`)
|
||||
|
||||
This is the actual write step for platform mirrors.
|
||||
|
||||
1. Read the current state of `.lore/SUMMARY.md` and the scope-tagged index.
|
||||
2. For each configured mirror target, read the existing file and detect the section boundary.
|
||||
3. Compute the new Lore section content.
|
||||
4. **Content-based dedup**: if the new Lore section content is byte-identical to the existing one, skip writing. Report "No changes needed: `<file>`".
|
||||
5. If different, replace the Lore section (full rewrite, no merge with previous content). Preserve the My notes section verbatim.
|
||||
6. Write the file back. Report "Mirror updated: `<file>`".
|
||||
|
||||
The content-based dedup step (4) is the key reason `mirror` can be run frequently without polluting `git log` — most invocations will be no-ops once the mirror is in sync.
|
||||
|
||||
## Init-time behavior (full procedure)
|
||||
|
||||
The `init` command extends the resolution algorithm above with classification and per-file takeover steps. The full procedure:
|
||||
|
||||
1. **Check whether `.lore/` exists.**
|
||||
- Absent → create `.lore/` and write an initial empty config.
|
||||
- Present → load existing `.lore/.config.json` (use defaults if missing).
|
||||
|
||||
2. **Scan existing platform files** in repo root using the same candidate list as the resolution algorithm. Result: list of paths that exist.
|
||||
|
||||
3. **Classify each detected file** into one of three classes:
|
||||
- **Class (a)** — already a lore mirror: contains `## Lore` section.
|
||||
- **Class (b)** — user-written: contains `## My notes` but no `## Lore`.
|
||||
- **Class (c)** — unmarked: neither header present.
|
||||
|
||||
For class (b) and (c) files, present a per-file choice:
|
||||
- **Take over**: file becomes a two-section mirror; existing content is preserved as My notes.
|
||||
- **Preserve as-is**: file is left alone; NOT added to `mirror_targets`.
|
||||
- **Abort**: exit init entirely. `.lore/` may exist (from Step 1) but no `mirror_targets` is written.
|
||||
|
||||
Class (a) files are auto-included in `mirror_targets`.
|
||||
|
||||
4. **Multi-select question.** "Which agents do you use in this project?" Default pre-selection: every agent corresponding to a class (a) file. Empty selection is allowed — but class (a) files still get included via Step 5.
|
||||
|
||||
5. **Compute final `mirror_targets`** by combining three sources and deduplicating:
|
||||
- All class (a) files from Step 3 (always included, regardless of Step 4 selection).
|
||||
- Files chosen via "take over" in Step 3.
|
||||
- Primary files for additional agents the user selected in Step 4 that aren't already covered.
|
||||
|
||||
Dedup: Aider and Codex both map to `AGENTS.md` and collapse to one entry.
|
||||
|
||||
6. **Write `.lore/.config.json`** with `mirror_targets` populated.
|
||||
|
||||
7. **Generate initial mirror files** for each target:
|
||||
- File absent → full template (`## Lore` + `---` + empty `## My notes`).
|
||||
- File present with `## Lore` → refresh Lore section, preserve My notes verbatim.
|
||||
- File present and "take over" chosen → old content becomes My notes, new `## Lore` above.
|
||||
- File present and "preserve" chosen → no write.
|
||||
|
||||
For each generated mirror file, the section template is:
|
||||
|
||||
```
|
||||
## Lore (auto-managed)
|
||||
|
||||
<initial or refreshed Lore content>
|
||||
|
||||
---
|
||||
|
||||
## My notes (free edit)
|
||||
|
||||
<preserved or empty>
|
||||
```
|
||||
|
||||
## What gets mirrored
|
||||
|
||||
The mirror's Lore section is an **index** into `.lore/` — not a copy of its content. This keeps per-session token cost flat (~500 B regardless of project size) and aligns with how platform instruction files (`CLAUDE.md`, `.cursorrules`, etc.) are designed to be used: as small pointers that tell the agent where to find detail on demand.
|
||||
|
||||
The agent generating the mirror walks `.lore/` and emits the structure below. Sections appear only when their content exists (adaptive rendering).
|
||||
|
||||
### Index template
|
||||
|
||||
```
|
||||
## Lore (auto-managed)
|
||||
|
||||
Project memory. Read deeper on demand.
|
||||
|
||||
**Structure**:
|
||||
- Digest: `.lore/SUMMARY.md` (top-level overview)
|
||||
- Global: `.lore/_global/` (architecture, decisions, conventions)
|
||||
- Scopes: `.lore/scopes/`
|
||||
- `.lore/scopes/<scope_name>/` (<description>)
|
||||
- `.lore/scopes/<scope_name>/`
|
||||
...
|
||||
|
||||
**Query**: `lore query <term>` or `lore query <scope>:<term>`
|
||||
**Update**: see the `lore` skill (init / sync / query / audit / compress / mirror)
|
||||
|
||||
---
|
||||
## My notes (free edit)
|
||||
```
|
||||
|
||||
The `## Lore (auto-managed)` opener, `---` separator, and `## My notes (free edit)` closer are **always present** — only the `**Structure**:` body varies with adaptive rendering. Agent preserves the My notes section verbatim across regenerations.
|
||||
|
||||
### Field sources
|
||||
|
||||
- `<scope_name>` — directory name under `.lore/scopes/`. Each scope's full path is `.lore/scopes/<scope_name>/`.
|
||||
- `<description>` — extracted from `.lore/scopes/<scope_name>/ARCHITECTURE.md` via the HTML comment `<!-- description: ... -->`. See "Scope description extraction" below. If absent, the description is omitted (scope row still appears, just without parenthetical).
|
||||
|
||||
The index does **not** track the project's source-directory mapping for each scope (e.g. `packages/frontend/` for the `frontend` scope). Source paths are detected by `references/monorepo-detection.md` at init time but not persisted in `.lore/`. If a user needs source paths surfaced in the mirror, that mapping belongs in the project's own docs.
|
||||
|
||||
### Section visibility rules
|
||||
|
||||
| Section | Visible when |
|
||||
|---|---|
|
||||
| `Digest:` line | always |
|
||||
| `Global:` line | `.lore/_global/` exists and has any entry |
|
||||
| `Scopes:` block | at least one scope directory exists under `.lore/` |
|
||||
| `Query:` line | always |
|
||||
| `Update:` line | always |
|
||||
|
||||
### Adaptive renderings
|
||||
|
||||
Only the `**Structure**:` body varies. The `## Lore (auto-managed)` opener, `---` separator, and `## My notes (free edit)` closer are always present and unchanged.
|
||||
|
||||
**Empty project** (just initialized, no entries yet):
|
||||
|
||||
```
|
||||
## Lore (auto-managed)
|
||||
|
||||
Project memory. Read deeper on demand.
|
||||
|
||||
**Structure**:
|
||||
- Digest: `.lore/SUMMARY.md` (top-level overview)
|
||||
|
||||
**Query**: `lore query <term>`
|
||||
**Update**: see the `lore` skill
|
||||
|
||||
---
|
||||
## My notes (free edit)
|
||||
```
|
||||
|
||||
`Global:` and `Scopes:` blocks omitted.
|
||||
|
||||
**Single-scope project**:
|
||||
|
||||
```
|
||||
**Structure**:
|
||||
- Digest: `.lore/SUMMARY.md`
|
||||
- Global: `.lore/_global/`
|
||||
- Scopes: `.lore/scopes/`
|
||||
- `.lore/scopes/frontend/` (React 18 + TypeScript)
|
||||
```
|
||||
|
||||
`Scopes:` block has one entry.
|
||||
|
||||
**Monorepo with multiple scopes**:
|
||||
|
||||
```
|
||||
**Structure**:
|
||||
- Digest: `.lore/SUMMARY.md`
|
||||
- Global: `.lore/_global/`
|
||||
- Scopes: `.lore/scopes/`
|
||||
- `.lore/scopes/frontend/` (React 18 + TypeScript)
|
||||
- `.lore/scopes/backend/` (PostgreSQL + Prisma)
|
||||
- `.lore/scopes/shared/`
|
||||
```
|
||||
|
||||
### Scope description extraction
|
||||
|
||||
The agent scans `.lore/scopes/<scope_name>/ARCHITECTURE.md` for the **first line matching** `<!-- description: <text> -->` (anchored to start of line; `description:` literal). Rules:
|
||||
|
||||
- **First match wins.** If multiple `<!-- description: ... -->` lines exist, only the first is used.
|
||||
- **`<text>` is single-line.** A comment must not contain a newline before `-->`. Multi-line comments are ignored.
|
||||
- **Whitespace trimmed.** Leading and trailing whitespace inside `<text>` is stripped.
|
||||
- **No match → no description.** The scope row appears without parenthetical; the row is not removed.
|
||||
|
||||
Example `ARCHITECTURE.md` with description:
|
||||
|
||||
```
|
||||
<!-- description: React 18 + TypeScript frontend -->
|
||||
# Frontend Architecture
|
||||
|
||||
All UI code lives here. ...
|
||||
```
|
||||
|
||||
### Scope ordering
|
||||
|
||||
Scope rows in the `Scopes:` block are emitted in **alphabetical order** by `<scope_name>`. Pinning order is important: the content-based dedup step compares byte-for-byte, so any order change between runs causes spurious "Mirror updated" reports.
|
||||
|
||||
### What does NOT trigger mirror regeneration
|
||||
|
||||
Index content does not change when:
|
||||
- Individual entries are edited
|
||||
- `SUMMARY.md` content is updated (the index only points to its path)
|
||||
- Entry counts change
|
||||
- A scope's `ARCHITECTURE.md` content changes (only the `<!-- description: -->` comment affects the index)
|
||||
|
||||
Index content changes require regeneration when:
|
||||
- A new scope directory is added under `.lore/`
|
||||
- A scope is removed
|
||||
- A scope's `ARCHITECTURE.md` `<!-- description: -->` line changes
|
||||
- `.lore/_global/` gains or loses its first entry (Global section visibility flips)
|
||||
|
||||
## Manual operations
|
||||
|
||||
| Command | Effect |
|
||||
|---|---|
|
||||
| `lore mirror` | Force-regenerate all configured platform mirrors from current `.lore/*` state. Content-based dedup: skips targets whose new Lore section matches the existing one. |
|
||||
| `lore mirror reset <file>` | Archive current My notes content to `.lore/.archive/<file>-<date>.md`, then write a clean mirror with only the Lore section. User must confirm. |
|
||||
| `lore mirror show <file>` | Print the file with the two sections clearly delimited in the output. Pure read. |
|
||||
| `lore mirror check` | For each configured target, verify it has a `---` separator and a `## My notes` section. Report any structural problems. Read-only. |
|
||||
|
||||
## Trigger rules
|
||||
|
||||
| Trigger | Behavior |
|
||||
|---|---|
|
||||
| `init` confirms draft | Auto-generate mirrors for all configured targets using the init-time rules above. |
|
||||
| `sync` proposal accepted | Writes to `.lore/*.md` only. Does **not** touch mirrors. User runs `lore mirror` separately to publish. (Override: set `sync_updates_mirror: true` in config to restore old behavior.) |
|
||||
| `compress` completes | If `auto_mirror: true`, regenerate mirrors (with content-based dedup). Otherwise ask per target. |
|
||||
| `lore mirror` | Force-regenerate all configured targets with content-based dedup. |
|
||||
| `query` / `audit` | Never touches mirrors. |
|
||||
@@ -0,0 +1,62 @@
|
||||
# Stale / New marking convention
|
||||
|
||||
When `sync` proposes a change, it never silently mutates files. Instead it emits one or more of these markers. The user reads the proposal and accepts/rejects per marker type.
|
||||
|
||||
## Marker types
|
||||
|
||||
| Marker | Purpose |
|
||||
|---|---|
|
||||
| `[NEW]` | Propose adding a new entry |
|
||||
| `[STALE]` | Propose marking an existing entry as superseded/contradicted |
|
||||
| `[REFINED]` | Propose updating an existing entry's text in place |
|
||||
| `[ALERT]` | Conflicting signal detected during sync that needs human resolution |
|
||||
| `[COMPRESS NOTICE]` | Threshold tripped; suggest running `compress` after this sync |
|
||||
|
||||
## Full example
|
||||
|
||||
```markdown
|
||||
## [NEW] Proposed additions
|
||||
- [scopes/frontend/ARCHITECTURE.md] [ARCH-2026-07-09-b4d2] Use `react-hook-form` for all forms. #added:2026-07-09
|
||||
- [scopes/frontend/CONVENTIONS.md] [CONV-2026-07-09-c5e1] Never use `any` in TypeScript; prefer `unknown` + narrowing. #added:2026-07-09
|
||||
|
||||
## [STALE] Candidates for archive
|
||||
- [scopes/frontend/ARCHITECTURE.md] [ARCH-2026-01-15-d7a3] Use Pages Router (Next.js). #stale:2026-07-09
|
||||
Evidence: `frontend/package.json` shows `"next": "^14.0.0"` with `app/` directory present.
|
||||
|
||||
## [REFINED] Existing entries updated
|
||||
- [scopes/frontend/DECISIONS.md] [DEC-2026-02-03-7c19] (was: "use Zustand") → "use Zustand v4+ with slices pattern" #verified:2026-07-09
|
||||
|
||||
## [ALERT] Conflicting signals detected during sync
|
||||
- Sync proposes `[CONV-2026-07-09-c5e1]` (no `any`), but `[CONV-2026-06-01-f0a1]` already says "use `any` sparingly in test mocks". Resolution: refined entry above clarifies the exception.
|
||||
|
||||
## [COMPRESS NOTICE]
|
||||
- Memory bank has 612 entries; last compression 47 days ago. Consider running `lore compress` after this sync.
|
||||
```
|
||||
|
||||
## User reply semantics
|
||||
|
||||
The user can reply with:
|
||||
|
||||
- `"accept all"` — apply every `[NEW]`, `[STALE]`, and `[REFINED]` in the proposal
|
||||
- `"accept only NEW"` — add new entries, leave existing untouched
|
||||
- `"accept NEW + REFINE"` — add new and refine, do not mark anything stale
|
||||
- `"drop STALE #d7a3"` — skip one specific stale entry
|
||||
- `"reject all"` — discard the entire proposal
|
||||
|
||||
For partial acceptance, the user should explicitly list which items to apply.
|
||||
|
||||
## Marker → file operation mapping
|
||||
|
||||
| Marker | File action |
|
||||
|---|---|
|
||||
| `[NEW]` | Append a new bullet to the named file, with `#added:<today>` |
|
||||
| `[STALE]` | Append `#stale:<today>` tag to the existing entry; entry stays in the file |
|
||||
| `[REFINED]` | Replace the entry text in place, keep the ID, update `#verified:<today>` |
|
||||
| `[ALERT]` | No direct file change; only marks the conflict for user resolution |
|
||||
| `[COMPRESS NOTICE]` | No file change; advisory only |
|
||||
|
||||
Note: `[STALE]` does not delete or move anything. The entry remains in its file with a `#stale` tag until the user (or a later sync) explicitly moves it to `archive/`. This keeps the rollback path clean.
|
||||
|
||||
## When audit uses these markers
|
||||
|
||||
`audit` does **not** use these markers. It writes its own severity tags (`[CONFLICT]`, `[STALE]`, `[UNVERIFIED]`) into the audit report file under `.lore/audit/`. The naming overlap (`[STALE]` in sync vs `[STALE]` severity in audit) is intentional — both refer to the same concept (entry no longer accurate) but operate in different files with different downstream actions.
|
||||
@@ -0,0 +1,98 @@
|
||||
# SUMMARY.md template
|
||||
|
||||
`compress` generates/refreshes `SUMMARY.md` from existing entries. This file holds the schema and worked example.
|
||||
|
||||
## Skeleton
|
||||
|
||||
```markdown
|
||||
# .lore SUMMARY
|
||||
|
||||
> Last compressed: <YYYY-MM-DD>
|
||||
> Total entries: <N> across <M> scopes
|
||||
|
||||
## Global (`_global/`)
|
||||
|
||||
### Architecture
|
||||
- <bullet> — [_global/ARCHITECTURE.md#<ID>]
|
||||
- ...
|
||||
|
||||
### Decisions
|
||||
- ...
|
||||
|
||||
### Conventions
|
||||
- ...
|
||||
|
||||
## Scope: <name>
|
||||
|
||||
### Architecture
|
||||
- ...
|
||||
|
||||
### Decisions
|
||||
- ...
|
||||
|
||||
### Conventions
|
||||
- ...
|
||||
|
||||
## Scope: <name2>
|
||||
...
|
||||
```
|
||||
|
||||
## Selection rule (3–5 entries per scope per layer)
|
||||
|
||||
For each (scope, layer) tuple, pick entries by this priority:
|
||||
|
||||
1. Most recent `#verified` date wins
|
||||
2. Tiebreaker: most recent `#added` date
|
||||
3. Tiebreaker: entries that contain "primary" / "main" / "core" / "use <X>" — these are typically the anchor facts
|
||||
|
||||
If a (scope, layer) has fewer than 3 entries, include all of them.
|
||||
|
||||
If a (scope, layer) is empty, omit the subsection entirely.
|
||||
|
||||
## Worked example
|
||||
|
||||
```markdown
|
||||
# .lore SUMMARY
|
||||
|
||||
> Last compressed: 2026-07-09
|
||||
> Total entries: 247 across 3 scopes
|
||||
|
||||
## Global (`_global/`)
|
||||
|
||||
### Architecture
|
||||
- Monorepo with pnpm workspaces + Turborepo — [_global/ARCHITECTURE.md#ARCH-2026-01-15-d7a3]
|
||||
- Node.js 20 baseline — [_global/ARCHITECTURE.md#ARCH-2026-02-01-9b1c]
|
||||
|
||||
### Decisions
|
||||
- Rejected Nx → chose Turborepo (faster builds, simpler config) — [_global/DECISIONS.md#DEC-2026-02-03-7c19]
|
||||
|
||||
### Conventions
|
||||
- All packages use TypeScript strict mode — [_global/CONVENTIONS.md#CONV-2026-01-20-b1e8]
|
||||
|
||||
## Scope: frontend
|
||||
|
||||
### Architecture
|
||||
- Next.js 14 App Router — [scopes/frontend/ARCHITECTURE.md#ARCH-2026-03-10-a1b2]
|
||||
- TanStack Query for server state — [scopes/frontend/ARCHITECTURE.md#ARCH-2026-03-15-e5f6]
|
||||
|
||||
### Decisions
|
||||
- Zustand over Redux (60% less boilerplate) — [scopes/frontend/DECISIONS.md#DEC-2026-02-03-7c19]
|
||||
|
||||
### Conventions
|
||||
- No default exports — [scopes/frontend/CONVENTIONS.md#CONV-2026-04-12-c3d4]
|
||||
|
||||
## Scope: backend
|
||||
|
||||
### Architecture
|
||||
- Node.js + Fastify + PostgreSQL — [scopes/backend/ARCHITECTURE.md#ARCH-2026-01-15-e5f6]
|
||||
|
||||
### Decisions
|
||||
- Fastify over Express (3x throughput in our benchmarks) — [scopes/backend/DECISIONS.md#DEC-2026-02-10-a8c9]
|
||||
|
||||
### Conventions
|
||||
- All DB queries go through repository pattern — [scopes/backend/CONVENTIONS.md#CONV-2026-03-01-b1d2]
|
||||
```
|
||||
|
||||
## Idempotency
|
||||
|
||||
Running `compress` twice without intervening `sync`s produces identical content (modulo the `Last compressed:` date). This is intentional — compress is a pure projection of the underlying entries.
|
||||
Reference in New Issue
Block a user