@kujolang/paperclip 0.1.0

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +21 -0
  3. package/README.md +116 -0
  4. package/SECURITY.md +35 -0
  5. package/VERSION +1 -0
  6. package/bundled/components/changebucket/LICENSE +9 -0
  7. package/bundled/components/changebucket/changebucket.kujo +12 -0
  8. package/bundled/components/changebucket/src/analyze.kujo +240 -0
  9. package/bundled/components/changebucket/src/budget.kujo +92 -0
  10. package/bundled/components/changebucket/src/classify.kujo +221 -0
  11. package/bundled/components/changebucket/src/cli.kujo +324 -0
  12. package/bundled/components/changebucket/src/diffsrc.kujo +252 -0
  13. package/bundled/components/changebucket/src/render.kujo +346 -0
  14. package/bundled/components/changebucket/src/util.kujo +62 -0
  15. package/bundled/components/context/LICENSE +9 -0
  16. package/bundled/components/context/scent.kujo +3250 -0
  17. package/bundled/components/failure-evidence/LICENSE +9 -0
  18. package/bundled/components/failure-evidence/casefile.kujo +2061 -0
  19. package/bundled/components/patchbrief/LICENSE +9 -0
  20. package/bundled/components/patchbrief/patchbrief.kujo +153 -0
  21. package/bundled/components/patchbrief/schemas/patchbrief-handoff.schema.json +38 -0
  22. package/bundled/components/patchbrief/schemas/patchbrief-summary.schema.json +46 -0
  23. package/bundled/components/patchbrief/src/common.kujo +212 -0
  24. package/bundled/components/patchbrief/src/git.kujo +250 -0
  25. package/bundled/components/patchbrief/src/handoff.kujo +145 -0
  26. package/bundled/components/patchbrief/src/suggest_tests.kujo +137 -0
  27. package/bundled/components/patchbrief/src/summarize.kujo +309 -0
  28. package/bundled/kujo-components.lock.json +134 -0
  29. package/dist/manifest.js +14696 -0
  30. package/dist/ui/index.js +179 -0
  31. package/dist/worker.js +30536 -0
  32. package/docs/ARCHITECTURE.md +28 -0
  33. package/docs/CATALOG_SUBMISSION.md +25 -0
  34. package/docs/COMPATIBILITY.md +21 -0
  35. package/docs/CONFIGURATION.md +33 -0
  36. package/docs/INSTALLATION.md +53 -0
  37. package/docs/OPERATIONS.md +60 -0
  38. package/docs/README.md +18 -0
  39. package/docs/RELEASE_READINESS.md +46 -0
  40. package/docs/THREAT_MODEL.md +26 -0
  41. package/docs/TROUBLESHOOTING.md +12 -0
  42. package/docs/USAGE.md +95 -0
  43. package/examples/agent-workflow.md +20 -0
  44. package/package.json +71 -0
  45. package/schemas/changebucket-analysis.schema.json +25 -0
  46. package/schemas/context-pack.schema.json +23 -0
  47. package/schemas/failure-evidence.schema.json +18 -0
  48. package/schemas/review-pack.schema.json +19 -0
  49. package/skills/scoped-repository-context/SKILL.md +9 -0
@@ -0,0 +1,309 @@
1
+ # Summarize module — analyzes git diff and generates structured summaries.
2
+
3
+ from src.git import is_git_repo, current_branch, repo_name, diff_stat, changed_files_result, full_diff_analysis, recent_commits
4
+ from src.common import str_trim, str_contains, str_lower, str_join, str_starts_with, str_ends_with, markdown_text, markdown_code, print_lines, print_prefixed_lines, print_prefixed_or_fallback, print_markdown_section, print_markdown_paragraph, print_fenced, print_not_git_repo_error
5
+
6
+ # Language/extension to type mapping for summary descriptions.
7
+ LANG_MAP := {
8
+ ".kujo": "Kujo",
9
+ ".rs": "Rust",
10
+ ".py": "Python",
11
+ ".js": "JavaScript",
12
+ ".ts": "TypeScript",
13
+ ".jsx": "React JSX",
14
+ ".tsx": "React TSX",
15
+ ".php": "PHP",
16
+ ".rb": "Ruby",
17
+ ".go": "Go",
18
+ ".java": "Java",
19
+ ".kt": "Kotlin",
20
+ ".swift": "Swift",
21
+ ".c": "C",
22
+ ".cpp": "C++",
23
+ ".h": "C/C++ Header",
24
+ ".css": "CSS",
25
+ ".html": "HTML",
26
+ ".md": "Markdown",
27
+ ".json": "JSON",
28
+ ".yaml": "YAML",
29
+ ".yml": "YAML",
30
+ ".toml": "TOML",
31
+ ".sql": "SQL",
32
+ ".sh": "Shell",
33
+ ".bash": "Bash",
34
+ ".dockerfile": "Docker",
35
+ ".tf": "Terraform",
36
+ ".xml": "XML"
37
+ }
38
+
39
+ # Get the language label for a file extension.
40
+ export func lang_for_file(file_path) {
41
+ lower := str_lower(file_path)
42
+ if lower == "dockerfile" || str_ends_with(lower, "/dockerfile") || str_ends_with(lower, ".dockerfile") {
43
+ return "Docker"
44
+ }
45
+ # Check each extension
46
+ extensions := [".kujo", ".rs", ".py", ".js", ".tsx", ".ts", ".jsx", ".php", ".rb", ".go", ".java", ".kt", ".swift", ".cpp", ".c", ".h", ".css", ".html", ".md", ".json", ".yaml", ".yml", ".toml", ".sql", ".sh", ".bash", ".tf", ".xml"]
47
+ ei := 0
48
+ while ei < len(extensions) {
49
+ ext := to_string(extensions[ei])
50
+ if str_ends_with(lower, ext) {
51
+ return LANG_MAP[ext]
52
+ }
53
+ ei := ei + 1
54
+ }
55
+ return "Unknown"
56
+ }
57
+
58
+ func file_detail_lines(files_list) {
59
+ lines := []
60
+ fi_idx := 0
61
+ while fi_idx < len(files_list) {
62
+ f := to_string(files_list[fi_idx])
63
+ lang := lang_for_file(f)
64
+ lines := push(lines, "- " + markdown_code(f) + " (" + lang + ")")
65
+ fi_idx := fi_idx + 1
66
+ }
67
+ return lines
68
+ }
69
+
70
+ func push_unique_text(items, value) {
71
+ text := to_string(value)
72
+ idx := 0
73
+ while idx < len(items) {
74
+ if to_string(items[idx]) == text {
75
+ return items
76
+ }
77
+ idx := idx + 1
78
+ }
79
+ return push(items, text)
80
+ }
81
+
82
+ # Analyze changed files and infer likely purpose.
83
+ export func infer_purpose(files_list, diff_text) {
84
+ if len(files_list) == 0 {
85
+ return "No changes detected"
86
+ }
87
+ lower_diff := str_lower(diff_text)
88
+ purposes := []
89
+ # Check for common change patterns
90
+ if str_contains(lower_diff, "test") || str_contains(lower_diff, "spec") {
91
+ purposes := push(purposes, "Testing")
92
+ }
93
+ if str_contains(lower_diff, "fix") || str_contains(lower_diff, "bug") {
94
+ purposes := push(purposes, "Bug fix")
95
+ }
96
+ if str_contains(lower_diff, "feature") || str_contains(lower_diff, "add") {
97
+ purposes := push(purposes, "Feature addition")
98
+ }
99
+ if str_contains(lower_diff, "refactor") || str_contains(lower_diff, "cleanup") {
100
+ purposes := push(purposes, "Refactoring")
101
+ }
102
+ if str_contains(lower_diff, "doc") || str_contains(lower_diff, "readme") {
103
+ purposes := push(purposes, "Documentation")
104
+ }
105
+ if str_contains(lower_diff, "security") || str_contains(lower_diff, "auth") || str_contains(lower_diff, "vuln") {
106
+ purposes := push(purposes, "Security")
107
+ }
108
+ if str_contains(lower_diff, "perf") || str_contains(lower_diff, "optimiz") || str_contains(lower_diff, "speed") {
109
+ purposes := push(purposes, "Performance")
110
+ }
111
+ if str_contains(lower_diff, "config") || str_contains(lower_diff, "setting") {
112
+ purposes := push(purposes, "Configuration")
113
+ }
114
+ if str_contains(lower_diff, "deprecat") || str_contains(lower_diff, "remov") {
115
+ purposes := push(purposes, "Deprecation/Removal")
116
+ }
117
+ if len(purposes) == 0 {
118
+ purposes := push(purposes, "General changes")
119
+ }
120
+ return str_join(purposes, ", ")
121
+ }
122
+
123
+ # Identify risk areas based on changed files.
124
+ export func identify_risks(files_list, diff_text) {
125
+ risks := []
126
+ added_lines := []
127
+ diff_lines := split(to_string(diff_text), "\n")
128
+ di := 0
129
+ while di < len(diff_lines) {
130
+ line := to_string(diff_lines[di])
131
+ if str_starts_with(line, "+") && str_starts_with(line, "+++") == false {
132
+ added_lines := push(added_lines, substring(line, 1, len(line)))
133
+ }
134
+ di := di + 1
135
+ }
136
+ lower_diff := str_lower(str_join(added_lines, "\n"))
137
+ # Check for risky patterns
138
+ fi := 0
139
+ while fi < len(files_list) {
140
+ f := str_lower(to_string(files_list[fi]))
141
+ if str_contains(f, "auth") || str_contains(f, "login") || str_contains(f, "password") || str_contains(f, "token") || str_contains(f, "secret") {
142
+ risks := push_unique_text(risks, "Authentication/authorization changes — review for security implications")
143
+ }
144
+ if str_contains(f, "database") || str_contains(f, "migration") || str_contains(f, "schema") {
145
+ risks := push_unique_text(risks, "Database schema or query changes — verify migration safety")
146
+ }
147
+ if str_contains(f, "api") || str_contains(f, "route") || str_contains(f, "endpoint") || str_contains(f, "handler") {
148
+ risks := push_unique_text(risks, "API/route changes — verify backward compatibility")
149
+ }
150
+ if str_contains(f, "config") || str_contains(f, ".env") || str_contains(f, "setting") {
151
+ risks := push_unique_text(risks, "Configuration changes — verify environment-specific values")
152
+ }
153
+ if str_contains(f, "test") || str_contains(f, "spec") {
154
+ risks := push_unique_text(risks, "Test changes — ensure tests actually validate the intended behavior")
155
+ }
156
+ fi := fi + 1
157
+ }
158
+ if str_contains(lower_diff, "eval(") || str_contains(lower_diff, "exec(") || str_contains(lower_diff, "system(") {
159
+ risks := push(risks, "Dangerous function calls detected (eval/exec/system) — security review required")
160
+ }
161
+ if str_contains(lower_diff, "todo") || str_contains(lower_diff, "fixme") || str_contains(lower_diff, "hack") {
162
+ risks := push(risks, "Unresolved TODO/FIXME/HACK comments — review before merge")
163
+ }
164
+ if len(risks) == 0 {
165
+ risks := push(risks, "No high-risk patterns detected (heuristic check only)")
166
+ }
167
+ return risks
168
+ }
169
+
170
+ # Generate a Markdown summary.
171
+ export func summarize_markdown() {
172
+ if is_git_repo() == false {
173
+ print_not_git_repo_error()
174
+ exit(1)
175
+ }
176
+ branch := current_branch()
177
+ repo := repo_name()
178
+ stat := diff_stat()
179
+ files_result := changed_files_result()
180
+ if files_result["ok"] == false {
181
+ print_lines(["Error: Failed to inspect git working tree."])
182
+ exit(1)
183
+ }
184
+ files := files_result["files"]
185
+ diff_analysis := full_diff_analysis()
186
+ diff_text := diff_analysis["text"]
187
+ if len(files) == 0 && stat["files"] == 0 {
188
+ print_lines([
189
+ "# PatchBrief Summary",
190
+ "",
191
+ "**Repo:** " + markdown_text(repo),
192
+ "**Branch:** " + markdown_text(branch),
193
+ "",
194
+ "No uncommitted changes detected."
195
+ ])
196
+ return
197
+ }
198
+ purpose := infer_purpose(files, diff_text)
199
+ risks := identify_risks(files, diff_text)
200
+ if diff_analysis["error"] == true {
201
+ risks := push_unique_text(risks, "Diff content analysis failed — purpose and content-based risk hints may be incomplete")
202
+ } else if diff_analysis["truncated"] == true {
203
+ risks := push_unique_text(risks, "Diff analysis was truncated at " + to_string(diff_analysis["max_bytes"]) + " bytes — purpose and content-based risk hints may be incomplete")
204
+ }
205
+ print_lines([
206
+ "# PatchBrief Summary",
207
+ "",
208
+ "**Repo:** " + markdown_text(repo),
209
+ "**Branch:** " + markdown_text(branch),
210
+ "**Changed Files:** " + to_string(len(files)),
211
+ "**Lines Added:** +" + to_string(stat["added"]),
212
+ "**Lines Removed:** -" + to_string(stat["removed"])
213
+ ])
214
+ print_markdown_section("Likely Purpose")
215
+ print_markdown_paragraph(purpose)
216
+ print_markdown_section("Changed Files")
217
+ # Show the git diff --stat output
218
+ if str_trim(stat["output"]) == "" {
219
+ print_fenced("No tracked diff stat available. Untracked files are listed below.")
220
+ } else {
221
+ print_fenced(str_trim(stat["output"]))
222
+ }
223
+ print_markdown_section("File Details")
224
+ print_lines(file_detail_lines(files))
225
+ print_markdown_section("Risk Areas")
226
+ print_prefixed_lines(risks, "- ")
227
+ print_markdown_section("Recent Commits")
228
+ commits_raw := recent_commits(5)
229
+ commits := []
230
+ ci := 0
231
+ while ci < len(commits_raw) {
232
+ commits := push(commits, markdown_text(commits_raw[ci]))
233
+ ci := ci + 1
234
+ }
235
+ print_prefixed_or_fallback(commits, "- ", "No recent commits found.")
236
+ print_lines([
237
+ "",
238
+ "---",
239
+ "*Generated by PatchBrief v1.0.1*"
240
+ ])
241
+ }
242
+
243
+
244
+ # Generate a JSON summary.
245
+ export func summarize_json(pretty_json) {
246
+ if is_git_repo() == false {
247
+ error_obj := {
248
+ "error": true,
249
+ "message": "Not a git repository. Run patchbrief inside a git repo."
250
+ }
251
+ if pretty_json == true { print(to_json_pretty(error_obj)) } else { print(to_json(error_obj)) }
252
+ exit(1)
253
+ }
254
+ branch := current_branch()
255
+ repo := repo_name()
256
+ stat := diff_stat()
257
+ files_result := changed_files_result()
258
+ if files_result["ok"] == false {
259
+ error_obj := {"error": true, "message": "Failed to inspect git working tree."}
260
+ if pretty_json == true { print(to_json_pretty(error_obj)) } else { print(to_json(error_obj)) }
261
+ exit(1)
262
+ }
263
+ files := files_result["files"]
264
+ diff_analysis := full_diff_analysis()
265
+ diff_text := diff_analysis["text"]
266
+ purpose := infer_purpose(files, diff_text)
267
+ risks := identify_risks(files, diff_text)
268
+ if diff_analysis["error"] == true {
269
+ risks := push_unique_text(risks, "Diff content analysis failed — purpose and content-based risk hints may be incomplete")
270
+ } else if diff_analysis["truncated"] == true {
271
+ risks := push_unique_text(risks, "Diff analysis was truncated at " + to_string(diff_analysis["max_bytes"]) + " bytes — purpose and content-based risk hints may be incomplete")
272
+ }
273
+ commits := recent_commits(5)
274
+ # Build file details array
275
+ file_details := []
276
+ fi_idx := 0
277
+ while fi_idx < len(files) {
278
+ f := to_string(files[fi_idx])
279
+ lang := lang_for_file(f)
280
+ entry := {
281
+ "path": f,
282
+ "language": lang
283
+ }
284
+ file_details := push(file_details, entry)
285
+ fi_idx := fi_idx + 1
286
+ }
287
+ output := {
288
+ "tool": "patchbrief",
289
+ "version": "1.0.1",
290
+ "repo": repo,
291
+ "branch": branch,
292
+ "summary": {
293
+ "files_changed": len(files),
294
+ "lines_added": stat["added"],
295
+ "lines_removed": stat["removed"]
296
+ },
297
+ "analysis": {
298
+ "diff_bytes_analyzed": diff_analysis["bytes_analyzed"],
299
+ "diff_error": diff_analysis["error"],
300
+ "diff_max_bytes": diff_analysis["max_bytes"],
301
+ "diff_truncated": diff_analysis["truncated"]
302
+ },
303
+ "likely_purpose": purpose,
304
+ "files": file_details,
305
+ "risk_areas": risks,
306
+ "recent_commits": commits
307
+ }
308
+ if pretty_json == true { print(to_json_pretty(output)) } else { print(to_json(output)) }
309
+ }
@@ -0,0 +1,134 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "bundleVersion": "0.1.0",
4
+ "runtime": {
5
+ "minimumVersion": "1.2.0"
6
+ },
7
+ "components": [
8
+ {
9
+ "id": "changebucket",
10
+ "repository": "https://github.com/kujolang/changebucket",
11
+ "gitCommit": "6fcaf41e7ca6c317ebb3b3408b4d185e76fb1640",
12
+ "version": "1.0.0",
13
+ "license": "MIT",
14
+ "entrypoint": "changebucket.kujo",
15
+ "files": [
16
+ "LICENSE",
17
+ "changebucket.kujo",
18
+ "src/cli.kujo",
19
+ "src/analyze.kujo",
20
+ "src/budget.kujo",
21
+ "src/classify.kujo",
22
+ "src/diffsrc.kujo",
23
+ "src/render.kujo",
24
+ "src/util.kujo"
25
+ ],
26
+ "checksums": {
27
+ "LICENSE": "4b6449939f8dce26dc5fb87756740a5977da9085517af127d9e634e6a6492506",
28
+ "changebucket.kujo": "74613364f008c52238aaeb89db99076229bca83a1e9ace50ee2b7037f179a9fd",
29
+ "src/cli.kujo": "221b482606be0620021ffe1ce7d0bf7109feb7425aff3c0548d0d09f21779b48",
30
+ "src/analyze.kujo": "17924d0133926622ac5b2a7f0d34971fd8e758e590ab067be444816864d1eafc",
31
+ "src/budget.kujo": "7e253ca9ee4a36e42aebf79f8af9b3af5bd3b251e8d0a44d674b5058ce347bde",
32
+ "src/classify.kujo": "4a79df8bc7db71bcc0805b76a37a6f77935c6db40795382db852dc0622ba7ed5",
33
+ "src/diffsrc.kujo": "9eb254c59ebeb6283bb88cd9b69a2059563a7f9ef0f7fe6bf22cbbc1bba94cd8",
34
+ "src/render.kujo": "60b084134fca588640a8bd02b9e2397d3a586b94ba5a2cb48bc4d7d1bc9850ac",
35
+ "src/util.kujo": "6bffc05e1dd0918e3934d6dd43681f326dae85ab9cb4f06adb24c08822e050ad"
36
+ },
37
+ "outputContracts": [
38
+ {
39
+ "name": "analysis",
40
+ "format": "json",
41
+ "schema": "schemas/changebucket-analysis.schema.json"
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "id": "patchbrief",
47
+ "repository": "https://github.com/kujolang/patchbrief",
48
+ "gitCommit": "d88397da0d98825191a76fdbce7a68fb9acb9a55",
49
+ "version": "1.0.1",
50
+ "license": "MIT",
51
+ "entrypoint": "patchbrief.kujo",
52
+ "files": [
53
+ "LICENSE",
54
+ "patchbrief.kujo",
55
+ "src/common.kujo",
56
+ "src/git.kujo",
57
+ "src/summarize.kujo",
58
+ "src/suggest_tests.kujo",
59
+ "src/handoff.kujo",
60
+ "schemas/patchbrief-summary.schema.json",
61
+ "schemas/patchbrief-handoff.schema.json"
62
+ ],
63
+ "checksums": {
64
+ "LICENSE": "4b6449939f8dce26dc5fb87756740a5977da9085517af127d9e634e6a6492506",
65
+ "patchbrief.kujo": "510e7880c9002fa0d630f0377440803b2b1dfb9f818633eb47d1daf058521645",
66
+ "src/common.kujo": "8fbd7f7853ae47f5f726cae3131cc27ffc21c3052b7740c85d057e382811f133",
67
+ "src/git.kujo": "4611fb50e827ec74abd26b607a91d5ec8a48a7bbb99900af9496b1e972582622",
68
+ "src/summarize.kujo": "55e62d1aa58fd724cbd7c4686f7da15a33b83116d01cb2810d1509b98dfe2c1e",
69
+ "src/suggest_tests.kujo": "667be110291da1d0c86015a51298cf2c93e3f7402997b1094803b6d80114286c",
70
+ "src/handoff.kujo": "74ad797e6c554f62a7c0cc1a7633f5d4097fea0d8c73ab412c2b44e3ab462810",
71
+ "schemas/patchbrief-summary.schema.json": "0fb7277cae93f93285bc69481c58edf3fe8590f6bd5960241b3ca3d6fa1f19bf",
72
+ "schemas/patchbrief-handoff.schema.json": "314416c21471023087309b46d783997399bb552ace95f80796a955256a58d0ed"
73
+ },
74
+ "outputContracts": [
75
+ {
76
+ "name": "summary",
77
+ "format": "json",
78
+ "schema": "schemas/patchbrief-summary.schema.json"
79
+ },
80
+ {
81
+ "name": "handoff",
82
+ "format": "json",
83
+ "schema": "schemas/patchbrief-handoff.schema.json"
84
+ }
85
+ ]
86
+ },
87
+ {
88
+ "id": "failure-evidence",
89
+ "repository": "https://github.com/kujolang/casefile",
90
+ "gitCommit": "6e8a6ded379a9882b2e48b0a8f0bad57e284c3bf",
91
+ "version": "1.0.0",
92
+ "license": "MIT",
93
+ "entrypoint": "casefile.kujo",
94
+ "files": [
95
+ "LICENSE",
96
+ "casefile.kujo"
97
+ ],
98
+ "checksums": {
99
+ "LICENSE": "4b6449939f8dce26dc5fb87756740a5977da9085517af127d9e634e6a6492506",
100
+ "casefile.kujo": "28ffc48f61b9abc959f590ef33ad0e75c060fd825801e46f943139050bbf9a2d"
101
+ },
102
+ "outputContracts": [
103
+ {
104
+ "name": "case",
105
+ "format": "json",
106
+ "schema": "schemas/failure-evidence.schema.json"
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "id": "context",
112
+ "repository": "https://github.com/kujolang/scent",
113
+ "gitCommit": "6e72cd06bf422a29e93148cf4edaddd98d937392",
114
+ "version": "1.0.0",
115
+ "license": "MIT",
116
+ "entrypoint": "scent.kujo",
117
+ "files": [
118
+ "LICENSE",
119
+ "scent.kujo"
120
+ ],
121
+ "checksums": {
122
+ "LICENSE": "4b6449939f8dce26dc5fb87756740a5977da9085517af127d9e634e6a6492506",
123
+ "scent.kujo": "01c16d6dfe3db280c5fc9a0ab689dc6de288f749daac17c9acadbb898f174f98"
124
+ },
125
+ "outputContracts": [
126
+ {
127
+ "name": "context",
128
+ "format": "json",
129
+ "schema": "schemas/context-pack.schema.json"
130
+ }
131
+ ]
132
+ }
133
+ ]
134
+ }