inscript 0.5.0__tar.gz → 0.7.0__tar.gz
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.
- {inscript-0.5.0 → inscript-0.7.0}/PKG-INFO +1 -1
- {inscript-0.5.0 → inscript-0.7.0}/inscript.egg-info/PKG-INFO +1 -1
- {inscript-0.5.0 → inscript-0.7.0}/inscript.egg-info/SOURCES.txt +2 -1
- {inscript-0.5.0 → inscript-0.7.0}/inscript_pkg/__init__.py +1 -1
- inscript-0.7.0/inscript_pkg/reflect.py +377 -0
- {inscript-0.5.0 → inscript-0.7.0}/pyproject.toml +1 -1
- {inscript-0.5.0 → inscript-0.7.0}/LICENSE +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/README.md +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/inscript.egg-info/dependency_links.txt +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/inscript.egg-info/entry_points.txt +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/inscript.egg-info/top_level.txt +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/inscript_pkg/hook.py +0 -0
- {inscript-0.5.0 → inscript-0.7.0}/setup.cfg +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"""inscript reflect — identify what your codebase is becoming.
|
|
2
|
+
|
|
3
|
+
Reads inscript session history (diffs, prompts, touches) and optionally
|
|
4
|
+
tamachi structural analysis, feeds them to Claude, and returns:
|
|
5
|
+
1. What archetype the codebase is converging toward
|
|
6
|
+
2. The trajectory from recent diffs
|
|
7
|
+
3. Predicted pain points based on archetype + trajectory
|
|
8
|
+
4. What the codebase could become
|
|
9
|
+
|
|
10
|
+
Based on research from IntentDiff v4:
|
|
11
|
+
- Structure carries intent (architecture IS the domain)
|
|
12
|
+
- Invariants accumulate and converge toward known archetypes
|
|
13
|
+
- Bugfixes reveal hidden invariants
|
|
14
|
+
- Early commits predict the end state
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
inscript reflect # reflect on current project
|
|
18
|
+
inscript reflect --sessions 5 # use last 5 sessions
|
|
19
|
+
inscript reflect --deep # include tamachi structural analysis
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from . import (
|
|
29
|
+
INSCRIPT_DIR,
|
|
30
|
+
SESSIONS_DIR,
|
|
31
|
+
active_project,
|
|
32
|
+
active_session,
|
|
33
|
+
list_sessions,
|
|
34
|
+
session_dir,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# The 10 function archetypes (from IntentDiff v4 research)
|
|
39
|
+
ARCHETYPES = {
|
|
40
|
+
"coordinator": "Orchestrates other calls, manages control flow, dispatches work",
|
|
41
|
+
"handler": "Handles events, requests, callbacks — reacts to external input",
|
|
42
|
+
"factory": "Creates instances, builds objects, spawns processes",
|
|
43
|
+
"accessor": "Returns state, reads fields, property getters",
|
|
44
|
+
"transformer": "Pure function, transforms input to output",
|
|
45
|
+
"validator": "Returns bool, checks conditions, enforces constraints",
|
|
46
|
+
"state_mutator": "Modifies self state, writes fields",
|
|
47
|
+
"serializer": "Format conversion (JSON, dict, etc.)",
|
|
48
|
+
"initializer": "__init__ and setup methods",
|
|
49
|
+
"test": "Test functions",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Known archetype convergence patterns (from invariant_convergence.md)
|
|
53
|
+
ARCHETYPE_PAIN_POINTS = {
|
|
54
|
+
"coordinator": [
|
|
55
|
+
"No graceful degradation — when one subsystem fails, the coordinator has no fallback",
|
|
56
|
+
"State management — coordinating multiple actors creates implicit shared state",
|
|
57
|
+
"The coordinator becomes a god object — too many responsibilities in one place",
|
|
58
|
+
"Missing health checks — no way to know if delegated work is actually progressing",
|
|
59
|
+
"Backpressure — no mechanism to slow down when workers are overloaded",
|
|
60
|
+
],
|
|
61
|
+
"handler": [
|
|
62
|
+
"Handler explosion — too many handlers with overlapping concerns",
|
|
63
|
+
"Missing middleware — cross-cutting logic (auth, logging) duplicated across handlers",
|
|
64
|
+
"No error propagation strategy — handlers swallow errors silently",
|
|
65
|
+
"Missing validation at boundaries — handlers trust input too much",
|
|
66
|
+
],
|
|
67
|
+
"factory": [
|
|
68
|
+
"Factory sprawl — too many factory methods creating slight variations",
|
|
69
|
+
"Missing cleanup — factories create but nothing tracks lifecycle or disposal",
|
|
70
|
+
"Configuration explosion — factories need more and more parameters",
|
|
71
|
+
],
|
|
72
|
+
"state_mutator": [
|
|
73
|
+
"Mutation without notification — state changes don't trigger dependent updates",
|
|
74
|
+
"Missing invariant enforcement — state can reach invalid combinations",
|
|
75
|
+
"Concurrency hazards — multiple writers with no synchronization",
|
|
76
|
+
"No undo/rollback capability",
|
|
77
|
+
],
|
|
78
|
+
"event_driven": [
|
|
79
|
+
"Event ordering — no guarantee events arrive in the right order",
|
|
80
|
+
"Missing dead letter queue — failed events are lost",
|
|
81
|
+
"Event schema evolution — changing event shapes breaks consumers",
|
|
82
|
+
"Debugging is hard — tracing causality through async event chains",
|
|
83
|
+
],
|
|
84
|
+
"pipeline": [
|
|
85
|
+
"Pipeline stage coupling — stages know too much about each other",
|
|
86
|
+
"No partial failure handling — one stage fails, entire pipeline stalls",
|
|
87
|
+
"Observability gaps — hard to know where data is in the pipeline",
|
|
88
|
+
"Backpressure propagation — slow stages cause memory buildup",
|
|
89
|
+
],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _gather_session_data(project_path: str | None, max_sessions: int = 10) -> dict:
|
|
94
|
+
"""Gather recent session data for a project."""
|
|
95
|
+
all_sessions = list_sessions()
|
|
96
|
+
|
|
97
|
+
# Filter to project if specified
|
|
98
|
+
if project_path:
|
|
99
|
+
all_sessions = [s for s in all_sessions if s.get("project") == project_path]
|
|
100
|
+
|
|
101
|
+
# Take most recent N
|
|
102
|
+
sessions = all_sessions[:max_sessions]
|
|
103
|
+
|
|
104
|
+
result = {
|
|
105
|
+
"session_count": len(sessions),
|
|
106
|
+
"prompts": [],
|
|
107
|
+
"diffs": [],
|
|
108
|
+
"files_touched": {},
|
|
109
|
+
"total_edits": 0,
|
|
110
|
+
"total_tokens": 0,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for s in sessions:
|
|
114
|
+
sid = s.get("session_id", "")
|
|
115
|
+
sdir = session_dir(sid)
|
|
116
|
+
|
|
117
|
+
# Load prompts
|
|
118
|
+
prompts_file = sdir / "prompts.jsonl"
|
|
119
|
+
if prompts_file.exists():
|
|
120
|
+
for line in prompts_file.open():
|
|
121
|
+
try:
|
|
122
|
+
p = json.loads(line)
|
|
123
|
+
p["session"] = sid[:8]
|
|
124
|
+
result["prompts"].append(p)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
# Load diffs (just file + type, not full content for context window)
|
|
129
|
+
diffs_file = sdir / "diffs.jsonl"
|
|
130
|
+
if diffs_file.exists():
|
|
131
|
+
for line in diffs_file.open():
|
|
132
|
+
try:
|
|
133
|
+
d = json.loads(line)
|
|
134
|
+
result["diffs"].append({
|
|
135
|
+
"file": d.get("file", ""),
|
|
136
|
+
"tool": d.get("tool", ""),
|
|
137
|
+
"session": sid[:8],
|
|
138
|
+
"has_old_new": "old_string" in d,
|
|
139
|
+
"is_new_file": d.get("is_new", False),
|
|
140
|
+
})
|
|
141
|
+
except json.JSONDecodeError:
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
# Load touches for file frequency
|
|
145
|
+
touches_file = sdir / "touches.jsonl"
|
|
146
|
+
if touches_file.exists():
|
|
147
|
+
for line in touches_file.open():
|
|
148
|
+
try:
|
|
149
|
+
t = json.loads(line)
|
|
150
|
+
f = t.get("file", "")
|
|
151
|
+
action = t.get("action", "")
|
|
152
|
+
if f:
|
|
153
|
+
if f not in result["files_touched"]:
|
|
154
|
+
result["files_touched"][f] = {"reads": 0, "edits": 0}
|
|
155
|
+
if action == "read":
|
|
156
|
+
result["files_touched"][f]["reads"] += 1
|
|
157
|
+
elif action in ("edit", "write"):
|
|
158
|
+
result["files_touched"][f]["edits"] += 1
|
|
159
|
+
result["total_edits"] += 1
|
|
160
|
+
except json.JSONDecodeError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
# Token usage from summary
|
|
164
|
+
summary_file = sdir / "summary.json"
|
|
165
|
+
if summary_file.exists():
|
|
166
|
+
try:
|
|
167
|
+
summary = json.loads(summary_file.read_text())
|
|
168
|
+
tokens = summary.get("tokens", {})
|
|
169
|
+
result["total_tokens"] += tokens.get("total_tokens", 0)
|
|
170
|
+
except (json.JSONDecodeError, OSError):
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _load_tamachi_analysis(project_path: str) -> dict | None:
|
|
177
|
+
"""Try to load tamachi structural analysis for the project."""
|
|
178
|
+
project = Path(project_path)
|
|
179
|
+
|
|
180
|
+
# Check .tamachi/ directory
|
|
181
|
+
for candidate in [
|
|
182
|
+
project / ".tamachi" / "tamachi.json",
|
|
183
|
+
project / ".tamachi",
|
|
184
|
+
]:
|
|
185
|
+
if candidate.is_file():
|
|
186
|
+
try:
|
|
187
|
+
data = json.loads(candidate.read_text())
|
|
188
|
+
tami = data.get("tami", data)
|
|
189
|
+
return {
|
|
190
|
+
"classes": len(tami.get("classes", [])),
|
|
191
|
+
"mutations": len(tami.get("mutation_sites", [])),
|
|
192
|
+
"reads": len(tami.get("method_read_sites", [])),
|
|
193
|
+
"state_machines": [
|
|
194
|
+
{"class": sm.get("class_name"), "field": sm.get("field_name"),
|
|
195
|
+
"states": sm.get("states", [])}
|
|
196
|
+
for sm in tami.get("state_machines", [])
|
|
197
|
+
],
|
|
198
|
+
"boolean_lifecycles": len(tami.get("boolean_lifecycle_invariants", [])),
|
|
199
|
+
"writer_set_groups": len(tami.get("writer_set_groups", [])),
|
|
200
|
+
"behavioral_clusters": len(tami.get("behavioral_clusters", [])),
|
|
201
|
+
}
|
|
202
|
+
except (json.JSONDecodeError, OSError):
|
|
203
|
+
pass
|
|
204
|
+
elif candidate.is_dir():
|
|
205
|
+
for f in candidate.glob("*_analysis.json"):
|
|
206
|
+
try:
|
|
207
|
+
data = json.loads(f.read_text())
|
|
208
|
+
tami = data.get("tami", data)
|
|
209
|
+
return {
|
|
210
|
+
"classes": len(tami.get("classes", [])),
|
|
211
|
+
"mutations": len(tami.get("mutation_sites", [])),
|
|
212
|
+
"reads": len(tami.get("method_read_sites", [])),
|
|
213
|
+
"state_machines": [
|
|
214
|
+
{"class": sm.get("class_name"), "field": sm.get("field_name"),
|
|
215
|
+
"states": sm.get("states", [])}
|
|
216
|
+
for sm in tami.get("state_machines", [])
|
|
217
|
+
],
|
|
218
|
+
"boolean_lifecycles": len(tami.get("boolean_lifecycle_invariants", [])),
|
|
219
|
+
"writer_set_groups": len(tami.get("writer_set_groups", [])),
|
|
220
|
+
"behavioral_clusters": len(tami.get("behavioral_clusters", [])),
|
|
221
|
+
}
|
|
222
|
+
except (json.JSONDecodeError, OSError):
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _build_reflect_prompt(session_data: dict, tamachi_data: dict | None, project_name: str) -> str:
|
|
229
|
+
"""Build the LLM prompt for reflection."""
|
|
230
|
+
|
|
231
|
+
# Top edited files
|
|
232
|
+
top_files = sorted(
|
|
233
|
+
session_data["files_touched"].items(),
|
|
234
|
+
key=lambda x: x[1]["edits"],
|
|
235
|
+
reverse=True,
|
|
236
|
+
)[:15]
|
|
237
|
+
|
|
238
|
+
# Recent prompts (last 20)
|
|
239
|
+
recent_prompts = session_data["prompts"][-20:]
|
|
240
|
+
|
|
241
|
+
# Diff summary
|
|
242
|
+
new_files = [d for d in session_data["diffs"] if d.get("is_new_file")]
|
|
243
|
+
edited_files = [d for d in session_data["diffs"] if d.get("has_old_new")]
|
|
244
|
+
|
|
245
|
+
prompt = f"""You are analyzing a codebase's development trajectory to identify what it's becoming.
|
|
246
|
+
|
|
247
|
+
## Project: {project_name}
|
|
248
|
+
|
|
249
|
+
## Recent Activity ({session_data['session_count']} sessions, {session_data['total_edits']} edits)
|
|
250
|
+
|
|
251
|
+
### Most Edited Files
|
|
252
|
+
"""
|
|
253
|
+
for f, counts in top_files:
|
|
254
|
+
prompt += f"- `{f}` — {counts['edits']} edits, {counts['reads']} reads\n"
|
|
255
|
+
|
|
256
|
+
prompt += f"\n### New Files Created ({len(new_files)})\n"
|
|
257
|
+
for d in new_files[:10]:
|
|
258
|
+
prompt += f"- `{d['file']}`\n"
|
|
259
|
+
|
|
260
|
+
prompt += f"\n### Recent Prompts (what the developer asked for)\n"
|
|
261
|
+
for p in recent_prompts:
|
|
262
|
+
tag = f" [{p['tag']}]" if p.get("tag") else ""
|
|
263
|
+
prompt += f"- \"{p.get('prompt', '')}\"{tag}\n"
|
|
264
|
+
|
|
265
|
+
if tamachi_data:
|
|
266
|
+
prompt += f"""
|
|
267
|
+
### Structural Analysis (from tamachi)
|
|
268
|
+
- {tamachi_data['classes']} classes, {tamachi_data['mutations']} mutation sites, {tamachi_data['reads']} read sites
|
|
269
|
+
- {len(tamachi_data['state_machines'])} state machines: {', '.join(f"{sm['class']}.{sm['field']}" for sm in tamachi_data['state_machines'][:5])}
|
|
270
|
+
- {tamachi_data['boolean_lifecycles']} boolean lifecycle invariants
|
|
271
|
+
- {tamachi_data['writer_set_groups']} writer set groups
|
|
272
|
+
- {tamachi_data['behavioral_clusters']} behavioral clusters
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
prompt += f"""
|
|
276
|
+
## Known Archetypes
|
|
277
|
+
These are the known system archetypes that codebases converge toward:
|
|
278
|
+
- **Coordinator/Supervisor**: Orchestrates workers, manages lifecycle, fault tolerance (Erlang OTP pattern)
|
|
279
|
+
- **Event-Driven Pipeline**: Events flow through handlers, async processing, pub/sub
|
|
280
|
+
- **Request-Response Server**: Handlers process requests, middleware chains, routing
|
|
281
|
+
- **Data Pipeline**: ETL, transformation stages, batch processing
|
|
282
|
+
- **State Machine Engine**: Manages entities through lifecycle states
|
|
283
|
+
- **Analysis Engine**: Reads data, computes insights, produces reports
|
|
284
|
+
- **CLI/Tool**: Command dispatch, argument parsing, output formatting
|
|
285
|
+
|
|
286
|
+
## Your Task
|
|
287
|
+
|
|
288
|
+
Based on the development activity, structural analysis, and file patterns:
|
|
289
|
+
|
|
290
|
+
1. **What it is**: Identify the primary archetype this codebase is converging toward. Name it precisely. Explain what structural patterns reveal this.
|
|
291
|
+
|
|
292
|
+
2. **Trajectory**: What direction is the project moving based on recent prompts and edits? What's being built right now vs what was built before?
|
|
293
|
+
|
|
294
|
+
3. **Predicted pain points**: Based on the archetype, what infrastructure problems will this project hit next? Be specific — name the files or patterns that will break. Reference the archetype's known failure modes.
|
|
295
|
+
|
|
296
|
+
4. **What it could become**: Project the trajectory forward. If the current development direction continues, what will this system look like in 1 month? What architectural decisions are being implicitly made right now?
|
|
297
|
+
|
|
298
|
+
5. **Hidden invariants**: What invariants exist in this codebase that aren't explicitly enforced? What "rules" does the code follow that a new contributor wouldn't know?
|
|
299
|
+
|
|
300
|
+
Respond in this format:
|
|
301
|
+
|
|
302
|
+
## Archetype
|
|
303
|
+
[Name and explanation]
|
|
304
|
+
|
|
305
|
+
## Trajectory
|
|
306
|
+
[Direction from recent activity]
|
|
307
|
+
|
|
308
|
+
## Predicted Pain Points
|
|
309
|
+
1. [Specific prediction with file/pattern reference]
|
|
310
|
+
2. [...]
|
|
311
|
+
3. [...]
|
|
312
|
+
|
|
313
|
+
## What It Could Become
|
|
314
|
+
[1-month projection]
|
|
315
|
+
|
|
316
|
+
## Hidden Invariants
|
|
317
|
+
[Rules the code follows implicitly]
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
return prompt
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def reflect(max_sessions: int = 10, deep: bool = False) -> str:
|
|
324
|
+
"""Run the reflect analysis. Returns the LLM's analysis as a string."""
|
|
325
|
+
import anthropic
|
|
326
|
+
|
|
327
|
+
project = active_project()
|
|
328
|
+
project_path = str(project) if project else None
|
|
329
|
+
project_name = project.name if project else "unknown"
|
|
330
|
+
|
|
331
|
+
# Gather data
|
|
332
|
+
session_data = _gather_session_data(project_path, max_sessions)
|
|
333
|
+
|
|
334
|
+
if session_data["session_count"] == 0:
|
|
335
|
+
return "No session data found. Use inscript for a few sessions first."
|
|
336
|
+
|
|
337
|
+
# Load tamachi if --deep or if it exists
|
|
338
|
+
tamachi_data = None
|
|
339
|
+
if project_path:
|
|
340
|
+
tamachi_data = _load_tamachi_analysis(project_path)
|
|
341
|
+
|
|
342
|
+
# Build prompt
|
|
343
|
+
prompt = _build_reflect_prompt(session_data, tamachi_data, project_name)
|
|
344
|
+
|
|
345
|
+
# Call Claude
|
|
346
|
+
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
|
347
|
+
if not api_key:
|
|
348
|
+
return "ANTHROPIC_API_KEY not set. inscript reflect needs Claude API access."
|
|
349
|
+
|
|
350
|
+
client = anthropic.Anthropic(api_key=api_key)
|
|
351
|
+
|
|
352
|
+
print(f"Reflecting on {project_name} ({session_data['session_count']} sessions, {session_data['total_edits']} edits)...\n", file=sys.stderr)
|
|
353
|
+
|
|
354
|
+
message = client.messages.create(
|
|
355
|
+
model="claude-sonnet-4-20250514",
|
|
356
|
+
max_tokens=4096,
|
|
357
|
+
messages=[{"role": "user", "content": prompt}],
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return message.content[0].text
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def main():
|
|
364
|
+
"""CLI entry point for inscript reflect."""
|
|
365
|
+
import argparse
|
|
366
|
+
|
|
367
|
+
parser = argparse.ArgumentParser(description="Reflect on your codebase's trajectory")
|
|
368
|
+
parser.add_argument("--sessions", type=int, default=10, help="Number of recent sessions to analyze")
|
|
369
|
+
parser.add_argument("--deep", action="store_true", help="Include tamachi structural analysis")
|
|
370
|
+
args = parser.parse_args()
|
|
371
|
+
|
|
372
|
+
result = reflect(max_sessions=args.sessions, deep=args.deep)
|
|
373
|
+
print(result)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
if __name__ == "__main__":
|
|
377
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|