@blxzer/cursor-trellis 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/README.md +51 -10
- package/README.zh-CN.md +21 -7
- package/bin/cstl.js +1 -1
- package/bin/smart-search.js +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +17 -15
- package/dist/cli/index.js.map +1 -1
- package/dist/commands/capability-smoke.js +2 -2
- package/dist/commands/capability-smoke.js.map +1 -1
- package/dist/commands/channel/agent-loader.d.ts +1 -1
- package/dist/commands/channel/agent-loader.js +4 -4
- package/dist/commands/channel/agent-loader.js.map +1 -1
- package/dist/commands/channel/guard.d.ts +2 -2
- package/dist/commands/channel/guard.js +3 -3
- package/dist/commands/channel/guard.js.map +1 -1
- package/dist/commands/channel/index.js +3 -3
- package/dist/commands/channel/index.js.map +1 -1
- package/dist/commands/channel/list.d.ts +1 -1
- package/dist/commands/channel/list.js +1 -1
- package/dist/commands/channel/rm.d.ts +1 -1
- package/dist/commands/channel/rm.js +1 -1
- package/dist/commands/channel/store/lock.d.ts +1 -1
- package/dist/commands/channel/store/lock.js +1 -1
- package/dist/commands/channel/store/paths.js +1 -1
- package/dist/commands/channel/store/paths.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +105 -59
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/uninstall.d.ts +3 -3
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +69 -15
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/commands/update.d.ts +6 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +83 -28
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/workflow.d.ts +4 -4
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +10 -9
- package/dist/commands/workflow.js.map +1 -1
- package/dist/configurators/index.d.ts.map +1 -1
- package/dist/configurators/index.js +1 -1
- package/dist/configurators/index.js.map +1 -1
- package/dist/configurators/shared.js +6 -6
- package/dist/configurators/shared.js.map +1 -1
- package/dist/configurators/workflow.d.ts +4 -4
- package/dist/configurators/workflow.js +1 -1
- package/dist/constants/paths.d.ts +24 -24
- package/dist/constants/paths.d.ts.map +1 -1
- package/dist/constants/paths.js +16 -16
- package/dist/constants/paths.js.map +1 -1
- package/dist/migrations/manifests/0.3.1.json +32 -0
- package/dist/templates/common/bundled-skills/cstl-cursor2plus-setup/SKILL.md +11 -11
- package/dist/templates/common/bundled-skills/cstl-meta/SKILL.md +15 -15
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/add-project-local-conventions.md +12 -12
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-agents.md +2 -2
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-context-loading.md +21 -21
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-hooks.md +9 -9
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-skills-or-commands.md +7 -7
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-spec-structure.md +15 -15
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-task-lifecycle.md +27 -27
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/change-workflow.md +7 -7
- package/dist/templates/common/bundled-skills/cstl-meta/references/customize-local/overview.md +11 -11
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/context-injection.md +12 -12
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/generated-files.md +21 -21
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/overview.md +14 -14
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/spec-system.md +12 -12
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/task-system.md +23 -23
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/workflow.md +3 -3
- package/dist/templates/common/bundled-skills/cstl-meta/references/local-architecture/workspace-memory.md +16 -16
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/agents.md +2 -2
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/hooks-and-settings.md +6 -6
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/overview.md +5 -5
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/platform-map.md +3 -3
- package/dist/templates/common/bundled-skills/cstl-meta/references/platform-files/skills-and-commands.md +5 -5
- package/dist/templates/common/bundled-skills/cstl-skill-creator/SKILL.md +2 -2
- package/dist/templates/common/bundled-skills/cstl-skill-creator/references/review-checklist.md +1 -1
- package/dist/templates/common/bundled-skills/cstl-spec-bootstrap/SKILL.md +5 -5
- package/dist/templates/common/bundled-skills/cstl-spec-bootstrap/references/repository-analysis.md +1 -1
- package/dist/templates/common/bundled-skills/cstl-spec-bootstrap/references/spec-task-planning.md +3 -3
- package/dist/templates/common/bundled-skills/cstl-spec-bootstrap/references/spec-writing.md +1 -1
- package/dist/templates/common/commands/continue.md +6 -6
- package/dist/templates/common/commands/finish-work.md +5 -5
- package/dist/templates/common/commands/start.md +8 -8
- package/dist/templates/common/skills/before-dev.md +4 -4
- package/dist/templates/common/skills/brainstorm.md +5 -5
- package/dist/templates/common/skills/break-loop.md +3 -3
- package/dist/templates/common/skills/check.md +5 -5
- package/dist/templates/common/skills/update-spec.md +4 -4
- package/dist/templates/cursor/agents/cstl-check.md +6 -6
- package/dist/templates/cursor/agents/cstl-implement.md +7 -7
- package/dist/templates/cursor/agents/cstl-research.md +9 -9
- package/dist/templates/cursor/commands/cursor2plus-setup.md +10 -10
- package/dist/templates/cursor/rules/cstl-subagent-dispatch.mdc +2 -2
- package/dist/templates/cursor/rules/cstl-triage.mdc +2 -2
- package/dist/templates/cursor/rules/retrieval-routing.mdc +4 -4
- package/dist/templates/extract.d.ts +1 -1
- package/dist/templates/extract.js +1 -1
- package/dist/templates/markdown/agents.md +11 -11
- package/dist/templates/markdown/prompts/run-semantic-slice-12.md.txt +1 -1
- package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +5 -5
- package/dist/templates/markdown/spec/guides/cursor-context-injection-guide.md.txt +3 -3
- package/dist/templates/markdown/spec/guides/cursor-semantic-compliance.md.txt +1 -1
- package/dist/templates/markdown/spec/guides/cursor-subagent-policy.md.txt +11 -11
- package/dist/templates/markdown/spec/guides/cursor-trellis-release-coexistence-guide.md.txt +166 -0
- package/dist/templates/markdown/spec/guides/durable-learning-decision-guide.md.txt +8 -8
- package/dist/templates/markdown/spec/guides/execution-strategy.md.txt +3 -3
- package/dist/templates/markdown/spec/guides/index.md.txt +1 -0
- package/dist/templates/markdown/spec/guides/retrieval-daily-guide.md.txt +10 -10
- package/dist/templates/markdown/workspace-index.md +3 -3
- package/dist/templates/markdown/worktree.yaml.txt +1 -1
- package/dist/templates/shared-hooks/index.d.ts +1 -1
- package/dist/templates/shared-hooks/index.js +1 -1
- package/dist/templates/shared-hooks/inject-retrieval-plan.py +3 -3
- package/dist/templates/shared-hooks/inject-shell-session-context.py +7 -7
- package/dist/templates/shared-hooks/inject-subagent-context.py +1 -1
- package/dist/templates/shared-hooks/inject-workflow-state.py +11 -11
- package/dist/templates/shared-hooks/research-end-retrieval-pack.py +1 -1
- package/dist/templates/shared-hooks/session-start.py +13 -13
- package/dist/templates/trellis/config.yaml +2 -22
- package/dist/templates/trellis/index.d.ts +2 -2
- package/dist/templates/trellis/index.js +2 -2
- package/dist/templates/trellis/local/README.md +3 -3
- package/dist/templates/trellis/local/patch_wpelc8.py +4 -4
- package/dist/templates/trellis/scripts/add_session.py +20 -20
- package/dist/templates/trellis/scripts/common/active_task.py +2 -2
- package/dist/templates/trellis/scripts/common/artifact_search.py +587 -587
- package/dist/templates/trellis/scripts/common/codebase_retrieval_router.py +6 -6
- package/dist/templates/trellis/scripts/common/config.py +3 -3
- package/dist/templates/trellis/scripts/common/developer.py +2 -2
- package/dist/templates/trellis/scripts/common/packages_context.py +23 -23
- package/dist/templates/trellis/scripts/common/parent_orchestration.py +16 -16
- package/dist/templates/trellis/scripts/common/paths.py +107 -107
- package/dist/templates/trellis/scripts/common/project_file_stats.py +1 -1
- package/dist/templates/trellis/scripts/common/retrieval_agent_instructions.py +1 -1
- package/dist/templates/trellis/scripts/common/safe_commit.py +31 -31
- package/dist/templates/trellis/scripts/common/session_memory.py +360 -360
- package/dist/templates/trellis/scripts/common/smart_search_evidence.py +514 -514
- package/dist/templates/trellis/scripts/common/smart_search_resolve.py +1 -1
- package/dist/templates/trellis/scripts/common/subagent_dispatch.py +2 -2
- package/dist/templates/trellis/scripts/common/task_context.py +1 -1
- package/dist/templates/trellis/scripts/common/task_dashboard.py +140 -140
- package/dist/templates/trellis/scripts/common/task_gates.py +23 -23
- package/dist/templates/trellis/scripts/common/task_store.py +5 -5
- package/dist/templates/trellis/scripts/common/task_utils.py +3 -3
- package/dist/templates/trellis/scripts/common/tasks.py +1 -1
- package/dist/templates/trellis/scripts/common/trellis_config.py +3 -3
- package/dist/templates/trellis/scripts/common/workflow_phase.py +1 -1
- package/dist/templates/trellis/scripts/cursor_retrieval_probe.py +3 -3
- package/dist/templates/trellis/scripts/cursor_retrieval_probe_prompt.md +11 -11
- package/dist/templates/trellis/scripts/hooks/linear_sync.py +8 -8
- package/dist/templates/trellis/scripts/init_developer.py +2 -2
- package/dist/templates/trellis/scripts/retrieval_probe_matrix_template.json +4 -4
- package/dist/templates/trellis/scripts/route_codebase_retrieval.py +1 -1
- package/dist/templates/trellis/scripts/task.py +918 -918
- package/dist/templates/trellis/tasks/templates/release-readiness/implement.md +2 -2
- package/dist/templates/trellis/workflow.md +64 -64
- package/dist/types/migration.d.ts +1 -1
- package/dist/types/migration.d.ts.map +1 -1
- package/dist/utils/agents-md.d.ts +47 -0
- package/dist/utils/agents-md.d.ts.map +1 -0
- package/dist/utils/agents-md.js +107 -0
- package/dist/utils/agents-md.js.map +1 -0
- package/dist/utils/codebase-retrieval-router.js +5 -5
- package/dist/utils/codebase-retrieval-router.js.map +1 -1
- package/dist/utils/manifest-prune.d.ts +2 -2
- package/dist/utils/manifest-prune.js +4 -4
- package/dist/utils/manifest-prune.js.map +1 -1
- package/dist/utils/mirror-check.d.ts +3 -1
- package/dist/utils/mirror-check.d.ts.map +1 -1
- package/dist/utils/mirror-check.js +22 -11
- package/dist/utils/mirror-check.js.map +1 -1
- package/dist/utils/project-capabilities.js +5 -5
- package/dist/utils/project-capabilities.js.map +1 -1
- package/dist/utils/retrieval-agent-instructions.js +1 -1
- package/dist/utils/retrieval-agent-instructions.js.map +1 -1
- package/dist/utils/retrieval-execution-telemetry.js +1 -1
- package/dist/utils/retrieval-execution-telemetry.js.map +1 -1
- package/dist/utils/template-fetcher.js +1 -1
- package/dist/utils/template-fetcher.js.map +1 -1
- package/dist/utils/template-hash.d.ts +2 -2
- package/dist/utils/template-hash.d.ts.map +1 -1
- package/dist/utils/template-hash.js +51 -18
- package/dist/utils/template-hash.js.map +1 -1
- package/dist/utils/workflow-dir.d.ts +13 -0
- package/dist/utils/workflow-dir.d.ts.map +1 -0
- package/dist/utils/workflow-dir.js +35 -0
- package/dist/utils/workflow-dir.js.map +1 -0
- package/dist/utils/workflow-ownership.d.ts +36 -0
- package/dist/utils/workflow-ownership.d.ts.map +1 -0
- package/dist/utils/workflow-ownership.js +137 -0
- package/dist/utils/workflow-ownership.js.map +1 -0
- package/dist/utils/workflow-resolver.d.ts +1 -1
- package/dist/utils/workflow-resolver.js +1 -1
- package/package.json +2 -2
|
@@ -1,360 +1,360 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Search Trellis workspace session memory.
|
|
4
|
-
|
|
5
|
-
This module parses `.
|
|
6
|
-
by add_session.py and returns compact, explainable memory results.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import argparse
|
|
12
|
-
import json
|
|
13
|
-
import re
|
|
14
|
-
import sys
|
|
15
|
-
from dataclasses import dataclass, field
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
from typing import Iterable
|
|
18
|
-
|
|
19
|
-
from .paths import DIR_WORKFLOW, DIR_WORKSPACE, FILE_JOURNAL_PREFIX, get_developer, get_repo_root
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
SECTION_NAMES = {"summary", "main changes", "git commits", "testing", "status", "next steps"}
|
|
23
|
-
FIELD_WEIGHTS = {
|
|
24
|
-
"title": 8,
|
|
25
|
-
"task": 8,
|
|
26
|
-
"summary": 6,
|
|
27
|
-
"next steps": 6,
|
|
28
|
-
"main changes": 4,
|
|
29
|
-
"commits": 3,
|
|
30
|
-
"package": 3,
|
|
31
|
-
"branch": 3,
|
|
32
|
-
"path": 3,
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@dataclass
|
|
37
|
-
class SessionEntry:
|
|
38
|
-
developer: str
|
|
39
|
-
session_number: int
|
|
40
|
-
title: str
|
|
41
|
-
date: str
|
|
42
|
-
task: str
|
|
43
|
-
package: str
|
|
44
|
-
branch: str
|
|
45
|
-
commits: list[str]
|
|
46
|
-
sections: dict[str, str]
|
|
47
|
-
path: Path
|
|
48
|
-
rel_path: str
|
|
49
|
-
line: int
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
@dataclass
|
|
53
|
-
class MemoryResult:
|
|
54
|
-
entry: SessionEntry
|
|
55
|
-
score: int
|
|
56
|
-
matched_sections: set[str] = field(default_factory=set)
|
|
57
|
-
matched_fields: set[str] = field(default_factory=set)
|
|
58
|
-
reason_parts: list[str] = field(default_factory=list)
|
|
59
|
-
|
|
60
|
-
def to_json(self) -> dict[str, object]:
|
|
61
|
-
return {
|
|
62
|
-
"version": 1,
|
|
63
|
-
"source": "session-memory",
|
|
64
|
-
"developer": self.entry.developer,
|
|
65
|
-
"session": self.entry.session_number,
|
|
66
|
-
"title": self.entry.title,
|
|
67
|
-
"date": self.entry.date,
|
|
68
|
-
"task": self.entry.task,
|
|
69
|
-
"package": self.entry.package,
|
|
70
|
-
"branch": self.entry.branch,
|
|
71
|
-
"commits": self.entry.commits,
|
|
72
|
-
"summary": normalize_space(self.entry.sections.get("summary", "")),
|
|
73
|
-
"matchedSections": sorted(self.matched_sections),
|
|
74
|
-
"matchedFields": sorted(self.matched_fields),
|
|
75
|
-
"path": self.entry.rel_path,
|
|
76
|
-
"line": self.entry.line,
|
|
77
|
-
"score": self.score,
|
|
78
|
-
"reason": "; ".join(self.reason_parts),
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
83
|
-
parser = argparse.ArgumentParser(description="Search Trellis session memory journals.")
|
|
84
|
-
parser.add_argument("--query", "-q", default="", help="Whitespace-token OR query.")
|
|
85
|
-
parser.add_argument(
|
|
86
|
-
"--developer",
|
|
87
|
-
action="append",
|
|
88
|
-
default=[],
|
|
89
|
-
help="Developer workspace to search. Defaults to current developer, then all.",
|
|
90
|
-
)
|
|
91
|
-
parser.add_argument("--task", help="Filter by task/title substring.")
|
|
92
|
-
parser.add_argument("--package", help="Filter by package substring.")
|
|
93
|
-
parser.add_argument("--branch", help="Filter by branch substring.")
|
|
94
|
-
parser.add_argument("--since", help="Filter sessions on or after YYYY-MM-DD.")
|
|
95
|
-
parser.add_argument("--until", help="Filter sessions on or before YYYY-MM-DD.")
|
|
96
|
-
parser.add_argument("--limit", type=int, default=10, help="Maximum results. Use 0 for no limit.")
|
|
97
|
-
parser.add_argument("--json", action="store_true", help="Emit stable JSON.")
|
|
98
|
-
parser.add_argument("--root", help="Repository root. Defaults to nearest parent with .
|
|
99
|
-
return parser.parse_args(argv)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def main(argv: list[str] | None = None) -> int:
|
|
103
|
-
args = parse_args(argv)
|
|
104
|
-
if args.limit < 0:
|
|
105
|
-
print("Error: --limit must be >= 0", file=sys.stderr)
|
|
106
|
-
return 2
|
|
107
|
-
|
|
108
|
-
repo_root = Path(args.root).resolve() if args.root else get_repo_root()
|
|
109
|
-
results = search_memory(
|
|
110
|
-
repo_root=repo_root,
|
|
111
|
-
query=args.query,
|
|
112
|
-
developers=args.developer,
|
|
113
|
-
task_filter=args.task or "",
|
|
114
|
-
package_filter=args.package or "",
|
|
115
|
-
branch_filter=args.branch or "",
|
|
116
|
-
since=args.since or "",
|
|
117
|
-
until=args.until or "",
|
|
118
|
-
limit=args.limit,
|
|
119
|
-
)
|
|
120
|
-
payload = {
|
|
121
|
-
"query": args.query,
|
|
122
|
-
"developers": args.developer,
|
|
123
|
-
"filters": {
|
|
124
|
-
"task": args.task or "",
|
|
125
|
-
"package": args.package or "",
|
|
126
|
-
"branch": args.branch or "",
|
|
127
|
-
"since": args.since or "",
|
|
128
|
-
"until": args.until or "",
|
|
129
|
-
},
|
|
130
|
-
"total": len(results),
|
|
131
|
-
"results": [result.to_json() for result in results],
|
|
132
|
-
}
|
|
133
|
-
if args.json:
|
|
134
|
-
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
135
|
-
else:
|
|
136
|
-
print_human(payload)
|
|
137
|
-
return 0
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
def search_memory(
|
|
141
|
-
repo_root: Path,
|
|
142
|
-
query: str,
|
|
143
|
-
developers: list[str],
|
|
144
|
-
task_filter: str,
|
|
145
|
-
package_filter: str,
|
|
146
|
-
branch_filter: str,
|
|
147
|
-
since: str,
|
|
148
|
-
until: str,
|
|
149
|
-
limit: int,
|
|
150
|
-
) -> list[MemoryResult]:
|
|
151
|
-
tokens = tokenize(query)
|
|
152
|
-
results: list[MemoryResult] = []
|
|
153
|
-
for entry in iter_session_entries(repo_root, developers):
|
|
154
|
-
if task_filter and task_filter.lower() not in entry.task.lower() and task_filter.lower() not in entry.title.lower():
|
|
155
|
-
continue
|
|
156
|
-
if package_filter and package_filter.lower() not in entry.package.lower():
|
|
157
|
-
continue
|
|
158
|
-
if branch_filter and branch_filter.lower() not in entry.branch.lower():
|
|
159
|
-
continue
|
|
160
|
-
if since and entry.date and entry.date < since:
|
|
161
|
-
continue
|
|
162
|
-
if until and entry.date and entry.date > until:
|
|
163
|
-
continue
|
|
164
|
-
result = score_entry(entry, tokens)
|
|
165
|
-
if result is not None:
|
|
166
|
-
results.append(result)
|
|
167
|
-
|
|
168
|
-
results.sort(key=lambda item: (-item.score, -item.entry.session_number, item.entry.rel_path, item.entry.line))
|
|
169
|
-
if limit:
|
|
170
|
-
return results[:limit]
|
|
171
|
-
return results
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def iter_session_entries(repo_root: Path, developers: list[str]) -> Iterable[SessionEntry]:
|
|
175
|
-
workspace_root = repo_root / DIR_WORKFLOW / DIR_WORKSPACE
|
|
176
|
-
if not workspace_root.is_dir():
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
selected_developers = developers or ([get_developer(repo_root)] if get_developer(repo_root) else [])
|
|
180
|
-
developer_dirs: list[Path]
|
|
181
|
-
if selected_developers:
|
|
182
|
-
developer_dirs = [workspace_root / developer for developer in selected_developers if developer]
|
|
183
|
-
else:
|
|
184
|
-
developer_dirs = [path for path in sorted(workspace_root.iterdir()) if path.is_dir()]
|
|
185
|
-
|
|
186
|
-
for developer_dir in developer_dirs:
|
|
187
|
-
if not developer_dir.is_dir():
|
|
188
|
-
continue
|
|
189
|
-
developer = developer_dir.name
|
|
190
|
-
for journal in sorted(developer_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"), key=journal_sort_key):
|
|
191
|
-
yield from parse_journal(journal, repo_root, developer)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def journal_sort_key(path: Path) -> tuple[int, str]:
|
|
195
|
-
match = re.search(r"(\d+)$", path.stem)
|
|
196
|
-
return (int(match.group(1)) if match else 0, path.name)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def parse_journal(path: Path, repo_root: Path, developer: str) -> list[SessionEntry]:
|
|
200
|
-
if not path.is_file():
|
|
201
|
-
return []
|
|
202
|
-
lines = path.read_text(encoding="utf-8").splitlines()
|
|
203
|
-
entries: list[SessionEntry] = []
|
|
204
|
-
current_start = 0
|
|
205
|
-
current_header = ""
|
|
206
|
-
current_lines: list[str] = []
|
|
207
|
-
for index, line in enumerate(lines, start=1):
|
|
208
|
-
if re.match(r"^## Session \d+:", line):
|
|
209
|
-
if current_header:
|
|
210
|
-
entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
|
|
211
|
-
current_start = index
|
|
212
|
-
current_header = line
|
|
213
|
-
current_lines = []
|
|
214
|
-
elif current_header:
|
|
215
|
-
current_lines.append(line)
|
|
216
|
-
if current_header:
|
|
217
|
-
entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
|
|
218
|
-
return entries
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
def build_entry(
|
|
222
|
-
path: Path,
|
|
223
|
-
repo_root: Path,
|
|
224
|
-
developer: str,
|
|
225
|
-
line: int,
|
|
226
|
-
header: str,
|
|
227
|
-
lines: list[str],
|
|
228
|
-
) -> SessionEntry:
|
|
229
|
-
match = re.match(r"^## Session (\d+):\s*(.+?)\s*$", header)
|
|
230
|
-
session_number = int(match.group(1)) if match else 0
|
|
231
|
-
title = match.group(2).strip() if match else header.strip("# ")
|
|
232
|
-
fields = extract_fields(lines)
|
|
233
|
-
sections = extract_sections(lines)
|
|
234
|
-
commits = extract_commits(sections.get("git commits", ""))
|
|
235
|
-
return SessionEntry(
|
|
236
|
-
developer=developer,
|
|
237
|
-
session_number=session_number,
|
|
238
|
-
title=title,
|
|
239
|
-
date=fields.get("date", ""),
|
|
240
|
-
task=fields.get("task", title),
|
|
241
|
-
package=fields.get("package", ""),
|
|
242
|
-
branch=fields.get("branch", ""),
|
|
243
|
-
commits=commits,
|
|
244
|
-
sections=sections,
|
|
245
|
-
path=path,
|
|
246
|
-
rel_path=to_repo_path(path, repo_root),
|
|
247
|
-
line=line,
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def extract_fields(lines: list[str]) -> dict[str, str]:
|
|
252
|
-
fields: dict[str, str] = {}
|
|
253
|
-
for line in lines:
|
|
254
|
-
match = re.match(r"^\*\*(Date|Task|Package|Branch)\*\*:\s*(.+?)\s*$", line)
|
|
255
|
-
if not match:
|
|
256
|
-
continue
|
|
257
|
-
key = match.group(1).lower()
|
|
258
|
-
value = match.group(2).strip()
|
|
259
|
-
if key == "branch":
|
|
260
|
-
value = value.strip("`")
|
|
261
|
-
fields[key] = value
|
|
262
|
-
return fields
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def extract_sections(lines: list[str]) -> dict[str, str]:
|
|
266
|
-
sections: dict[str, list[str]] = {}
|
|
267
|
-
current: str | None = None
|
|
268
|
-
for line in lines:
|
|
269
|
-
heading = re.match(r"^###\s+(.+?)\s*$", line)
|
|
270
|
-
if heading:
|
|
271
|
-
name = heading.group(1).strip().lower()
|
|
272
|
-
current = name if name in SECTION_NAMES else name
|
|
273
|
-
sections.setdefault(current, [])
|
|
274
|
-
continue
|
|
275
|
-
if current:
|
|
276
|
-
sections[current].append(line)
|
|
277
|
-
return {key: "\n".join(value).strip() for key, value in sections.items()}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def extract_commits(content: str) -> list[str]:
|
|
281
|
-
return sorted(set(re.findall(r"`?([a-f0-9]{7,40})`?", content, flags=re.IGNORECASE)))
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
def score_entry(entry: SessionEntry, tokens: list[str]) -> MemoryResult | None:
|
|
285
|
-
result = MemoryResult(entry=entry, score=min(entry.session_number, 5))
|
|
286
|
-
fields = searchable_fields(entry)
|
|
287
|
-
if not tokens:
|
|
288
|
-
result.reason_parts.append("recent session memory")
|
|
289
|
-
return result
|
|
290
|
-
|
|
291
|
-
matched_any = False
|
|
292
|
-
for token in tokens:
|
|
293
|
-
token_matched = False
|
|
294
|
-
for field, value in fields:
|
|
295
|
-
if token not in value.lower():
|
|
296
|
-
continue
|
|
297
|
-
token_matched = True
|
|
298
|
-
matched_any = True
|
|
299
|
-
result.score += FIELD_WEIGHTS.get(field, 2)
|
|
300
|
-
if field in SECTION_NAMES:
|
|
301
|
-
result.matched_sections.add(display_section_name(field))
|
|
302
|
-
else:
|
|
303
|
-
result.matched_fields.add(field)
|
|
304
|
-
if token_matched:
|
|
305
|
-
result.reason_parts.append(f"matched '{token}'")
|
|
306
|
-
|
|
307
|
-
if not matched_any:
|
|
308
|
-
return None
|
|
309
|
-
if not result.reason_parts:
|
|
310
|
-
result.reason_parts.append("matched session memory")
|
|
311
|
-
return result
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def searchable_fields(entry: SessionEntry) -> list[tuple[str, str]]:
|
|
315
|
-
fields = [
|
|
316
|
-
("title", entry.title),
|
|
317
|
-
("task", entry.task),
|
|
318
|
-
("package", entry.package),
|
|
319
|
-
("branch", entry.branch),
|
|
320
|
-
("commits", " ".join(entry.commits)),
|
|
321
|
-
("path", entry.rel_path),
|
|
322
|
-
]
|
|
323
|
-
for section, content in entry.sections.items():
|
|
324
|
-
fields.append((section, content))
|
|
325
|
-
return fields
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def display_section_name(name: str) -> str:
|
|
329
|
-
return " ".join(part.capitalize() for part in name.split())
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def tokenize(query: str) -> list[str]:
|
|
333
|
-
return [token.lower() for token in re.findall(r"\S+", query) if token.strip()]
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
def normalize_space(value: str) -> str:
|
|
337
|
-
return re.sub(r"\s+", " ", value).strip()
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
def to_repo_path(path: Path, repo_root: Path) -> str:
|
|
341
|
-
try:
|
|
342
|
-
return path.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
343
|
-
except ValueError:
|
|
344
|
-
return str(path)
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
def print_human(payload: dict[str, object]) -> None:
|
|
348
|
-
results = payload["results"]
|
|
349
|
-
if not isinstance(results, list) or not results:
|
|
350
|
-
print("No session memory results.")
|
|
351
|
-
return
|
|
352
|
-
for index, raw in enumerate(results, start=1):
|
|
353
|
-
if not isinstance(raw, dict):
|
|
354
|
-
continue
|
|
355
|
-
print(f"{index}. {raw.get('title')} ({raw.get('date') or 'unknown date'})")
|
|
356
|
-
print(f" Path: {raw.get('path')}:{raw.get('line')}")
|
|
357
|
-
print(f" Score: {raw.get('score')} - {raw.get('reason')}")
|
|
358
|
-
summary = raw.get("summary")
|
|
359
|
-
if summary:
|
|
360
|
-
print(f" Summary: {summary}")
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Search Trellis workspace session memory.
|
|
4
|
+
|
|
5
|
+
This module parses `.cstl/workspace/<developer>/journal-*.md` files written
|
|
6
|
+
by add_session.py and returns compact, explainable memory results.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Iterable
|
|
18
|
+
|
|
19
|
+
from .paths import DIR_WORKFLOW, DIR_WORKSPACE, FILE_JOURNAL_PREFIX, get_developer, get_repo_root
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
SECTION_NAMES = {"summary", "main changes", "git commits", "testing", "status", "next steps"}
|
|
23
|
+
FIELD_WEIGHTS = {
|
|
24
|
+
"title": 8,
|
|
25
|
+
"task": 8,
|
|
26
|
+
"summary": 6,
|
|
27
|
+
"next steps": 6,
|
|
28
|
+
"main changes": 4,
|
|
29
|
+
"commits": 3,
|
|
30
|
+
"package": 3,
|
|
31
|
+
"branch": 3,
|
|
32
|
+
"path": 3,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class SessionEntry:
|
|
38
|
+
developer: str
|
|
39
|
+
session_number: int
|
|
40
|
+
title: str
|
|
41
|
+
date: str
|
|
42
|
+
task: str
|
|
43
|
+
package: str
|
|
44
|
+
branch: str
|
|
45
|
+
commits: list[str]
|
|
46
|
+
sections: dict[str, str]
|
|
47
|
+
path: Path
|
|
48
|
+
rel_path: str
|
|
49
|
+
line: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MemoryResult:
|
|
54
|
+
entry: SessionEntry
|
|
55
|
+
score: int
|
|
56
|
+
matched_sections: set[str] = field(default_factory=set)
|
|
57
|
+
matched_fields: set[str] = field(default_factory=set)
|
|
58
|
+
reason_parts: list[str] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
def to_json(self) -> dict[str, object]:
|
|
61
|
+
return {
|
|
62
|
+
"version": 1,
|
|
63
|
+
"source": "session-memory",
|
|
64
|
+
"developer": self.entry.developer,
|
|
65
|
+
"session": self.entry.session_number,
|
|
66
|
+
"title": self.entry.title,
|
|
67
|
+
"date": self.entry.date,
|
|
68
|
+
"task": self.entry.task,
|
|
69
|
+
"package": self.entry.package,
|
|
70
|
+
"branch": self.entry.branch,
|
|
71
|
+
"commits": self.entry.commits,
|
|
72
|
+
"summary": normalize_space(self.entry.sections.get("summary", "")),
|
|
73
|
+
"matchedSections": sorted(self.matched_sections),
|
|
74
|
+
"matchedFields": sorted(self.matched_fields),
|
|
75
|
+
"path": self.entry.rel_path,
|
|
76
|
+
"line": self.entry.line,
|
|
77
|
+
"score": self.score,
|
|
78
|
+
"reason": "; ".join(self.reason_parts),
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
83
|
+
parser = argparse.ArgumentParser(description="Search Trellis session memory journals.")
|
|
84
|
+
parser.add_argument("--query", "-q", default="", help="Whitespace-token OR query.")
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"--developer",
|
|
87
|
+
action="append",
|
|
88
|
+
default=[],
|
|
89
|
+
help="Developer workspace to search. Defaults to current developer, then all.",
|
|
90
|
+
)
|
|
91
|
+
parser.add_argument("--task", help="Filter by task/title substring.")
|
|
92
|
+
parser.add_argument("--package", help="Filter by package substring.")
|
|
93
|
+
parser.add_argument("--branch", help="Filter by branch substring.")
|
|
94
|
+
parser.add_argument("--since", help="Filter sessions on or after YYYY-MM-DD.")
|
|
95
|
+
parser.add_argument("--until", help="Filter sessions on or before YYYY-MM-DD.")
|
|
96
|
+
parser.add_argument("--limit", type=int, default=10, help="Maximum results. Use 0 for no limit.")
|
|
97
|
+
parser.add_argument("--json", action="store_true", help="Emit stable JSON.")
|
|
98
|
+
parser.add_argument("--root", help="Repository root. Defaults to nearest parent with .cstl/.")
|
|
99
|
+
return parser.parse_args(argv)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main(argv: list[str] | None = None) -> int:
|
|
103
|
+
args = parse_args(argv)
|
|
104
|
+
if args.limit < 0:
|
|
105
|
+
print("Error: --limit must be >= 0", file=sys.stderr)
|
|
106
|
+
return 2
|
|
107
|
+
|
|
108
|
+
repo_root = Path(args.root).resolve() if args.root else get_repo_root()
|
|
109
|
+
results = search_memory(
|
|
110
|
+
repo_root=repo_root,
|
|
111
|
+
query=args.query,
|
|
112
|
+
developers=args.developer,
|
|
113
|
+
task_filter=args.task or "",
|
|
114
|
+
package_filter=args.package or "",
|
|
115
|
+
branch_filter=args.branch or "",
|
|
116
|
+
since=args.since or "",
|
|
117
|
+
until=args.until or "",
|
|
118
|
+
limit=args.limit,
|
|
119
|
+
)
|
|
120
|
+
payload = {
|
|
121
|
+
"query": args.query,
|
|
122
|
+
"developers": args.developer,
|
|
123
|
+
"filters": {
|
|
124
|
+
"task": args.task or "",
|
|
125
|
+
"package": args.package or "",
|
|
126
|
+
"branch": args.branch or "",
|
|
127
|
+
"since": args.since or "",
|
|
128
|
+
"until": args.until or "",
|
|
129
|
+
},
|
|
130
|
+
"total": len(results),
|
|
131
|
+
"results": [result.to_json() for result in results],
|
|
132
|
+
}
|
|
133
|
+
if args.json:
|
|
134
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
135
|
+
else:
|
|
136
|
+
print_human(payload)
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def search_memory(
|
|
141
|
+
repo_root: Path,
|
|
142
|
+
query: str,
|
|
143
|
+
developers: list[str],
|
|
144
|
+
task_filter: str,
|
|
145
|
+
package_filter: str,
|
|
146
|
+
branch_filter: str,
|
|
147
|
+
since: str,
|
|
148
|
+
until: str,
|
|
149
|
+
limit: int,
|
|
150
|
+
) -> list[MemoryResult]:
|
|
151
|
+
tokens = tokenize(query)
|
|
152
|
+
results: list[MemoryResult] = []
|
|
153
|
+
for entry in iter_session_entries(repo_root, developers):
|
|
154
|
+
if task_filter and task_filter.lower() not in entry.task.lower() and task_filter.lower() not in entry.title.lower():
|
|
155
|
+
continue
|
|
156
|
+
if package_filter and package_filter.lower() not in entry.package.lower():
|
|
157
|
+
continue
|
|
158
|
+
if branch_filter and branch_filter.lower() not in entry.branch.lower():
|
|
159
|
+
continue
|
|
160
|
+
if since and entry.date and entry.date < since:
|
|
161
|
+
continue
|
|
162
|
+
if until and entry.date and entry.date > until:
|
|
163
|
+
continue
|
|
164
|
+
result = score_entry(entry, tokens)
|
|
165
|
+
if result is not None:
|
|
166
|
+
results.append(result)
|
|
167
|
+
|
|
168
|
+
results.sort(key=lambda item: (-item.score, -item.entry.session_number, item.entry.rel_path, item.entry.line))
|
|
169
|
+
if limit:
|
|
170
|
+
return results[:limit]
|
|
171
|
+
return results
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def iter_session_entries(repo_root: Path, developers: list[str]) -> Iterable[SessionEntry]:
|
|
175
|
+
workspace_root = repo_root / DIR_WORKFLOW / DIR_WORKSPACE
|
|
176
|
+
if not workspace_root.is_dir():
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
selected_developers = developers or ([get_developer(repo_root)] if get_developer(repo_root) else [])
|
|
180
|
+
developer_dirs: list[Path]
|
|
181
|
+
if selected_developers:
|
|
182
|
+
developer_dirs = [workspace_root / developer for developer in selected_developers if developer]
|
|
183
|
+
else:
|
|
184
|
+
developer_dirs = [path for path in sorted(workspace_root.iterdir()) if path.is_dir()]
|
|
185
|
+
|
|
186
|
+
for developer_dir in developer_dirs:
|
|
187
|
+
if not developer_dir.is_dir():
|
|
188
|
+
continue
|
|
189
|
+
developer = developer_dir.name
|
|
190
|
+
for journal in sorted(developer_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"), key=journal_sort_key):
|
|
191
|
+
yield from parse_journal(journal, repo_root, developer)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def journal_sort_key(path: Path) -> tuple[int, str]:
|
|
195
|
+
match = re.search(r"(\d+)$", path.stem)
|
|
196
|
+
return (int(match.group(1)) if match else 0, path.name)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def parse_journal(path: Path, repo_root: Path, developer: str) -> list[SessionEntry]:
|
|
200
|
+
if not path.is_file():
|
|
201
|
+
return []
|
|
202
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
203
|
+
entries: list[SessionEntry] = []
|
|
204
|
+
current_start = 0
|
|
205
|
+
current_header = ""
|
|
206
|
+
current_lines: list[str] = []
|
|
207
|
+
for index, line in enumerate(lines, start=1):
|
|
208
|
+
if re.match(r"^## Session \d+:", line):
|
|
209
|
+
if current_header:
|
|
210
|
+
entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
|
|
211
|
+
current_start = index
|
|
212
|
+
current_header = line
|
|
213
|
+
current_lines = []
|
|
214
|
+
elif current_header:
|
|
215
|
+
current_lines.append(line)
|
|
216
|
+
if current_header:
|
|
217
|
+
entries.append(build_entry(path, repo_root, developer, current_start, current_header, current_lines))
|
|
218
|
+
return entries
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def build_entry(
|
|
222
|
+
path: Path,
|
|
223
|
+
repo_root: Path,
|
|
224
|
+
developer: str,
|
|
225
|
+
line: int,
|
|
226
|
+
header: str,
|
|
227
|
+
lines: list[str],
|
|
228
|
+
) -> SessionEntry:
|
|
229
|
+
match = re.match(r"^## Session (\d+):\s*(.+?)\s*$", header)
|
|
230
|
+
session_number = int(match.group(1)) if match else 0
|
|
231
|
+
title = match.group(2).strip() if match else header.strip("# ")
|
|
232
|
+
fields = extract_fields(lines)
|
|
233
|
+
sections = extract_sections(lines)
|
|
234
|
+
commits = extract_commits(sections.get("git commits", ""))
|
|
235
|
+
return SessionEntry(
|
|
236
|
+
developer=developer,
|
|
237
|
+
session_number=session_number,
|
|
238
|
+
title=title,
|
|
239
|
+
date=fields.get("date", ""),
|
|
240
|
+
task=fields.get("task", title),
|
|
241
|
+
package=fields.get("package", ""),
|
|
242
|
+
branch=fields.get("branch", ""),
|
|
243
|
+
commits=commits,
|
|
244
|
+
sections=sections,
|
|
245
|
+
path=path,
|
|
246
|
+
rel_path=to_repo_path(path, repo_root),
|
|
247
|
+
line=line,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def extract_fields(lines: list[str]) -> dict[str, str]:
|
|
252
|
+
fields: dict[str, str] = {}
|
|
253
|
+
for line in lines:
|
|
254
|
+
match = re.match(r"^\*\*(Date|Task|Package|Branch)\*\*:\s*(.+?)\s*$", line)
|
|
255
|
+
if not match:
|
|
256
|
+
continue
|
|
257
|
+
key = match.group(1).lower()
|
|
258
|
+
value = match.group(2).strip()
|
|
259
|
+
if key == "branch":
|
|
260
|
+
value = value.strip("`")
|
|
261
|
+
fields[key] = value
|
|
262
|
+
return fields
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def extract_sections(lines: list[str]) -> dict[str, str]:
|
|
266
|
+
sections: dict[str, list[str]] = {}
|
|
267
|
+
current: str | None = None
|
|
268
|
+
for line in lines:
|
|
269
|
+
heading = re.match(r"^###\s+(.+?)\s*$", line)
|
|
270
|
+
if heading:
|
|
271
|
+
name = heading.group(1).strip().lower()
|
|
272
|
+
current = name if name in SECTION_NAMES else name
|
|
273
|
+
sections.setdefault(current, [])
|
|
274
|
+
continue
|
|
275
|
+
if current:
|
|
276
|
+
sections[current].append(line)
|
|
277
|
+
return {key: "\n".join(value).strip() for key, value in sections.items()}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def extract_commits(content: str) -> list[str]:
|
|
281
|
+
return sorted(set(re.findall(r"`?([a-f0-9]{7,40})`?", content, flags=re.IGNORECASE)))
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def score_entry(entry: SessionEntry, tokens: list[str]) -> MemoryResult | None:
|
|
285
|
+
result = MemoryResult(entry=entry, score=min(entry.session_number, 5))
|
|
286
|
+
fields = searchable_fields(entry)
|
|
287
|
+
if not tokens:
|
|
288
|
+
result.reason_parts.append("recent session memory")
|
|
289
|
+
return result
|
|
290
|
+
|
|
291
|
+
matched_any = False
|
|
292
|
+
for token in tokens:
|
|
293
|
+
token_matched = False
|
|
294
|
+
for field, value in fields:
|
|
295
|
+
if token not in value.lower():
|
|
296
|
+
continue
|
|
297
|
+
token_matched = True
|
|
298
|
+
matched_any = True
|
|
299
|
+
result.score += FIELD_WEIGHTS.get(field, 2)
|
|
300
|
+
if field in SECTION_NAMES:
|
|
301
|
+
result.matched_sections.add(display_section_name(field))
|
|
302
|
+
else:
|
|
303
|
+
result.matched_fields.add(field)
|
|
304
|
+
if token_matched:
|
|
305
|
+
result.reason_parts.append(f"matched '{token}'")
|
|
306
|
+
|
|
307
|
+
if not matched_any:
|
|
308
|
+
return None
|
|
309
|
+
if not result.reason_parts:
|
|
310
|
+
result.reason_parts.append("matched session memory")
|
|
311
|
+
return result
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def searchable_fields(entry: SessionEntry) -> list[tuple[str, str]]:
|
|
315
|
+
fields = [
|
|
316
|
+
("title", entry.title),
|
|
317
|
+
("task", entry.task),
|
|
318
|
+
("package", entry.package),
|
|
319
|
+
("branch", entry.branch),
|
|
320
|
+
("commits", " ".join(entry.commits)),
|
|
321
|
+
("path", entry.rel_path),
|
|
322
|
+
]
|
|
323
|
+
for section, content in entry.sections.items():
|
|
324
|
+
fields.append((section, content))
|
|
325
|
+
return fields
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def display_section_name(name: str) -> str:
|
|
329
|
+
return " ".join(part.capitalize() for part in name.split())
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def tokenize(query: str) -> list[str]:
|
|
333
|
+
return [token.lower() for token in re.findall(r"\S+", query) if token.strip()]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def normalize_space(value: str) -> str:
|
|
337
|
+
return re.sub(r"\s+", " ", value).strip()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def to_repo_path(path: Path, repo_root: Path) -> str:
|
|
341
|
+
try:
|
|
342
|
+
return path.resolve().relative_to(repo_root.resolve()).as_posix()
|
|
343
|
+
except ValueError:
|
|
344
|
+
return str(path)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def print_human(payload: dict[str, object]) -> None:
|
|
348
|
+
results = payload["results"]
|
|
349
|
+
if not isinstance(results, list) or not results:
|
|
350
|
+
print("No session memory results.")
|
|
351
|
+
return
|
|
352
|
+
for index, raw in enumerate(results, start=1):
|
|
353
|
+
if not isinstance(raw, dict):
|
|
354
|
+
continue
|
|
355
|
+
print(f"{index}. {raw.get('title')} ({raw.get('date') or 'unknown date'})")
|
|
356
|
+
print(f" Path: {raw.get('path')}:{raw.get('line')}")
|
|
357
|
+
print(f" Score: {raw.get('score')} - {raw.get('reason')}")
|
|
358
|
+
summary = raw.get("summary")
|
|
359
|
+
if summary:
|
|
360
|
+
print(f" Summary: {summary}")
|