@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-02
4
+
5
+ - Add Review Pack, Failure Evidence, and Context Pack tools and UI actions for Paperclip.
6
+ - Bundle and verify ChangeBucket, PatchBrief, CaseFile, and Scent component snapshots.
7
+ - Enforce company-scoped state, feature policy, stale-context rejection, workspace bounds, hostile Git configuration suppression, and process-tree cleanup.
8
+ - Add cross-platform compatibility, dependency review, CodeQL, SBOM, provenance, signed-tag, and exact-tarball release controls.
9
+
10
+ ## 0.1.0
11
+
12
+ - Initial Paperclip-native Review Pack, Failure Evidence, and Context Pack.
13
+ - Lifecycle-script-free Kujo runtime resolution and deterministic component bundle.
14
+ - Strict workspace, process, output, redaction, schema, provenance, and stale-state boundaries.
15
+ - Monochrome project badges and install-first ecosystem documentation.
16
+ - Root agent, contribution, security, and version contracts.
17
+ - Reproducible lockfile-based CI and npm release gates.
18
+ - Exact Kujo `1.2.2` runtime dependency with the restored Windows package build.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kujolang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Kujo for Paperclip
2
+
3
+ [![Version](https://img.shields.io/badge/version-0.1.0-black)](https://github.com/kujolang/paperclip/releases/tag/v0.1.0)
4
+ [![License](https://img.shields.io/badge/license-MIT-lightgrey)](LICENSE)
5
+ [![built with Kujo](https://img.shields.io/badge/built%20with-Kujo-white.svg)](https://github.com/kujolang/kujo)
6
+
7
+ Add scoped context, change review, and reproducible failure evidence to Paperclip.
8
+
9
+ ## Install
10
+
11
+ You need Paperclip `2026.824.1` or later and Node.js `24.11.0` or later.
12
+
13
+ ```bash
14
+ npx paperclipai plugin install @kujolang/paperclip
15
+ ```
16
+
17
+ Restart Paperclip if it is already running. Open a project, issue, or run and select the **Kujo** tab. The status panel should report that the runtime and all four bundled components are ready.
18
+
19
+ The npm package includes the correct Kujo runtime for macOS arm64/x64, Linux arm64/x64, and Windows x64. Installation does not run `preinstall`, `install`, or `postinstall` scripts.
20
+
21
+ See [Installation](docs/INSTALLATION.md) for upgrades, local development installs, and uninstall behavior.
22
+
23
+ ## Use
24
+
25
+ Open the **Kujo** tab on a project or issue to:
26
+
27
+ - generate a **Review Pack** for the working tree;
28
+ - create a task-specific **Context Pack**;
29
+ - capture bounded, redacted **Failure Evidence**.
30
+
31
+ Agents can call four tools:
32
+
33
+ | Tool | Use |
34
+ | --- | --- |
35
+ | `kujolang.paperclip:review-changes` | Measure change size and create a review handoff. |
36
+ | `kujolang.paperclip:capture-failure` | Save supplied command output as redacted evidence. It never reruns the command. |
37
+ | `kujolang.paperclip:get-context` | Select a bounded set of files for a task. |
38
+ | `kujolang.paperclip:get-context-content` | Read safe content from files selected by an existing Context Pack. |
39
+
40
+ Example agent requests:
41
+
42
+ ```text
43
+ Use kujolang.paperclip:get-context with task "trace the OAuth callback" and depth "focused".
44
+ Use kujolang.paperclip:review-changes after the edit and report the suggested tests as suggestions, not completed checks.
45
+ If a check fails, use kujolang.paperclip:capture-failure with the command, exit code, and bounded log.
46
+ ```
47
+
48
+ The content tool only reads files selected by the matching Context Pack. It rejects other paths, binary files, oversized files, traversal, and symlink escapes.
49
+
50
+ See [Usage](docs/USAGE.md) for tool inputs, UI actions, artifact fields, and common workflows.
51
+
52
+ ## What it adds
53
+
54
+ - **Review Pack** uses ChangeBucket to measure the change and PatchBrief to explain it.
55
+ - **Failure Evidence** uses CaseFile to turn supplied logs into a reviewable record.
56
+ - **Context Pack** uses Scent to select relevant files before an agent reads broadly.
57
+
58
+ All four component snapshots are pinned, checksummed, and shipped in the package. The plugin does not fetch component code at runtime.
59
+
60
+ ## Compatibility
61
+
62
+ | Package | Version |
63
+ | --- | --- |
64
+ | Paperclip host | `>=2026.824.1` |
65
+ | `@paperclipai/plugin-sdk` | `2026.824.1` |
66
+ | `@kujolang/kujo-runtime` | `1.2.2` |
67
+ | Node.js | `>=24.11.0` |
68
+
69
+ The npm package is `@kujolang/paperclip`. Its Paperclip manifest ID is `kujolang.paperclip` because manifest IDs do not accept npm scope syntax.
70
+
71
+ ## Configure
72
+
73
+ Paperclip builds the settings form from the plugin manifest. You can enable or disable each feature, set an absolute Kujo binary override, allow or deny `PATH` fallback, and change bounded process limits.
74
+
75
+ Defaults are safe for normal repositories: a 27-second timeout, 2 MB stdout limit, and 256 KB stderr limit. The timeout stays below Paperclip's bridge deadline so the worker can return a structured result. See [Configuration](docs/CONFIGURATION.md) for every setting and limit.
76
+
77
+ ## How it runs
78
+
79
+ The worker resolves Kujo in this order:
80
+
81
+ 1. configured absolute binary;
82
+ 2. bundled `@kujolang/kujo-runtime` binary;
83
+ 3. an absolute `kujo` found on `PATH`, when allowed;
84
+ 4. a structured error with a repair hint.
85
+
86
+ Each child process uses an argument array with `shell: false`, a small environment, a canonical Paperclip workspace, a timeout, and output caps. Review and context commands are read-only. Failure Evidence records supplied text and never exposes a command runner.
87
+
88
+ ## Develop
89
+
90
+ ```bash
91
+ npm ci --ignore-scripts
92
+ npm run verify
93
+ ```
94
+
95
+ `npm run verify` type-checks the source, runs contract, hardening, and real-component tests, verifies component checksums, audits dependency integrity and licenses, produces an SBOM, builds the worker/manifest/UI bundles, and inspects the npm tarball.
96
+
97
+ Read [AGENTS.md](AGENTS.md) before agent-assisted changes and [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
98
+
99
+ ## Documentation
100
+
101
+ - [Documentation index](docs/README.md)
102
+ - [Installation](docs/INSTALLATION.md)
103
+ - [Usage](docs/USAGE.md)
104
+ - [Configuration](docs/CONFIGURATION.md)
105
+ - [Compatibility](docs/COMPATIBILITY.md)
106
+ - [Architecture](docs/ARCHITECTURE.md)
107
+ - [Threat model](docs/THREAT_MODEL.md)
108
+ - [Security policy](SECURITY.md)
109
+ - [Troubleshooting](docs/TROUBLESHOOTING.md)
110
+ - [Release readiness](docs/RELEASE_READINESS.md)
111
+ - [Operations](docs/OPERATIONS.md)
112
+ - [Agent workflow example](examples/agent-workflow.md)
113
+
114
+ ## License
115
+
116
+ MIT. Bundled Kujo components keep their canonical MIT licenses and recorded provenance.
package/SECURITY.md ADDED
@@ -0,0 +1,35 @@
1
+ # Security policy
2
+
3
+ ## Report a vulnerability
4
+
5
+ Use GitHub Security Advisories for `kujolang/paperclip`. Do not open a public issue with exploit details, credentials, private source, logs, or workspace paths.
6
+
7
+ Include the affected version, impact, safe reproduction steps, and suggested mitigation when available.
8
+
9
+ ## Supported versions
10
+
11
+ Security fixes apply to the latest tagged `0.1.x` release and `main`. Before `1.0`, a fix may require upgrading to the latest minor version instead of receiving a backport.
12
+
13
+ ## Operating model
14
+
15
+ The plugin analyzes local Paperclip workspaces with bundled Kujo components. It requests no network, secret, issue-write, approval, budget, database, job, webhook, or arbitrary-folder capability.
16
+
17
+ The plugin does not provide a sandbox. Only install it in a trusted Paperclip host. Treat generated Review, Failure Evidence, and Context Packs as sensitive project data.
18
+
19
+ ## Security boundaries
20
+
21
+ - Paperclip supplies canonical workspaces; tools cannot choose raw workspace paths.
22
+ - Workspace scans reject individual files over 25 MB and repositories with more than 100,000 Git-visible files before a component starts.
23
+ - Child processes use fixed entrypoints, argument arrays, `shell: false`, a small environment, timeouts, output limits, and whole-process-tree termination.
24
+ - Git runs without system or global configuration, hooks, external diff drivers, credential prompts, or file-system monitors.
25
+ - Component checksums are verified before execution.
26
+ - Context content is opt-in, restricted to selected files, bounded, and checked against traversal and symlink escape.
27
+ - Stored artifacts are isolated by company, validated on read, checked against their entity and workspace, and cleared on request.
28
+ - Disabled features cannot be invoked through tools, UI actions, or stored-artifact reads.
29
+ - Generation and content-read events are written to the Paperclip activity log when the host accepts them.
30
+ - Failure Evidence records supplied text and never runs the recorded command.
31
+ - Redaction covers common credential shapes but cannot guarantee that every secret format is removed.
32
+
33
+ The release pipeline audits production dependencies, verifies registry integrity and accepted licenses, emits a CycloneDX SBOM, attests the npm tarball, and publishes that exact tarball with npm provenance. Dependabot, dependency review, CodeQL, and the cross-platform compatibility matrix run in GitHub Actions.
34
+
35
+ See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for assets, trust boundaries, abuse cases, and mitigations.
package/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kujolang <contact@kujolang.ai>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,12 @@
1
+ # ChangeBucket entrypoint.
2
+ #
3
+ # Thin wrapper: no logic here. Hand the user's arguments to the CLI module and
4
+ # translate its return value into a process exit code.
5
+ #
6
+ # Run via the bin/changebucket wrapper:
7
+ # ./bin/changebucket <command> [arguments]
8
+
9
+ from src.cli import main
10
+
11
+ code := main(args())
12
+ exit(code)
@@ -0,0 +1,240 @@
1
+ # ChangeBucket :: analyze
2
+ #
3
+ # The heart of the tool. It turns read-only git output into the structured
4
+ # analysis model (summary + categories + per-file detail + risk level).
5
+ # Nothing here mutates the repo. All multi-pass logic uses index/`while` loops
6
+ # or single-`for` helpers so the module compiles under the checker.
7
+
8
+ from src.classify import classify, category_order
9
+ from src.diffsrc import is_repo, repo_root, resolve_refs, validate_diff, numstat_text, namestatus_text, untracked_list, is_binary_path, count_added_lines
10
+
11
+ # Map a git name-status letter to a human status word.
12
+ func status_word(letter) {
13
+ if letter == "A" {
14
+ return "added"
15
+ }
16
+ if letter == "D" {
17
+ return "deleted"
18
+ }
19
+ if letter == "R" {
20
+ return "renamed"
21
+ }
22
+ if letter == "C" {
23
+ return "copied"
24
+ }
25
+ return "modified"
26
+ }
27
+
28
+ # Fresh categories dict with every bucket present and empty.
29
+ func init_cats() {
30
+ mut cats := {}
31
+ for name in category_order() {
32
+ cats[name] := []
33
+ }
34
+ return cats
35
+ }
36
+
37
+ # Record file `path` under each of its categories.
38
+ func add_to_cats(cats, categories, path) {
39
+ for c in categories {
40
+ cats[c] := push(cats[c], path)
41
+ }
42
+ return cats
43
+ }
44
+
45
+ # Ingest `git diff --name-status` lines: seed each path with its status.
46
+ func ingest_namestatus(order, info, text) {
47
+ mut ord := order
48
+ nul := parse_json("\"\\u0000\"")
49
+ fields := split(text, nul)
50
+ mut i := 0
51
+ while i + 1 < len(fields) {
52
+ status := fields[i]
53
+ path := fields[i + 1]
54
+ if status != "" && path != "" {
55
+ letter := substring(status, 0, 1)
56
+ if has_key(info, path) == 0 {
57
+ info[path] := {"path": path, "status": status_word(letter), "additions": 0, "deletions": 0, "binary": false}
58
+ ord = push(ord, path)
59
+ } else {
60
+ e := info[path]
61
+ e["status"] := status_word(letter)
62
+ info[path] := e
63
+ }
64
+ }
65
+ i = i + 2
66
+ }
67
+ return {"order": ord, "info": info}
68
+ }
69
+
70
+ # Ingest `git diff --numstat` lines: attach additions/deletions (or binary).
71
+ func ingest_numstat(order, info, text) {
72
+ mut ord := order
73
+ nul := parse_json("\"\\u0000\"")
74
+ records := split(text, nul)
75
+ for record in records {
76
+ if record != "" {
77
+ cols := split(record, "\t")
78
+ adds := cols[0]
79
+ dels := cols[1]
80
+ # Only the first two tabs are separators; a path may itself contain
81
+ # tabs, so reconstruct it from all remaining fields.
82
+ path := join(slice(cols, 2, len(cols)), "\t")
83
+ binary := adds == "-" || dels == "-"
84
+ mut a := 0
85
+ mut d := 0
86
+ if binary == false {
87
+ a = parse_int(adds)
88
+ d = parse_int(dels)
89
+ }
90
+ if has_key(info, path) == 0 {
91
+ info[path] := {"path": path, "status": "modified", "additions": a, "deletions": d, "binary": binary}
92
+ ord = push(ord, path)
93
+ } else {
94
+ e := info[path]
95
+ e["additions"] := a
96
+ e["deletions"] := d
97
+ e["binary"] := binary
98
+ info[path] := e
99
+ }
100
+ }
101
+ }
102
+ return {"order": ord, "info": info}
103
+ }
104
+
105
+ # Ingest untracked files as additions (worktree mode only).
106
+ func ingest_untracked(order, info, files, repo) {
107
+ mut ord := order
108
+ for path in files {
109
+ if path != "" && has_key(info, path) == 0 {
110
+ binary := is_binary_path(path)
111
+ mut a := 0
112
+ if binary == false {
113
+ a = count_added_lines(repo, path)
114
+ }
115
+ info[path] := {"path": path, "status": "added", "additions": a, "deletions": 0, "binary": binary}
116
+ ord = push(ord, path)
117
+ }
118
+ }
119
+ return {"order": ord, "info": info}
120
+ }
121
+
122
+ # A simple, documented blast-radius heuristic. This is "risk", not "quality".
123
+ # high : >20 files, or >1000 churn, or any deletes, or generated touched
124
+ # low : <=5 files, <=200 churn, no deletes, no dep/lock/ci/generated touched
125
+ # medium : everything in between
126
+ export func risk_level(files_changed, churn, deleted, deps, locks, generated, ci) {
127
+ if files_changed > 20 || churn > 1000 || deleted > 0 || generated > 0 {
128
+ return "high"
129
+ }
130
+ if files_changed <= 5 && churn <= 200 && deleted == 0 && deps == 0 && locks == 0 && ci == 0 {
131
+ return "low"
132
+ }
133
+ return "medium"
134
+ }
135
+
136
+ # Walk the ordered file list, classify each, and roll up summary + categories.
137
+ func build_model(repo, refs, order, info) {
138
+ mut cats := init_cats()
139
+ mut files := []
140
+ mut files_added := 0
141
+ mut files_modified := 0
142
+ mut files_deleted := 0
143
+ mut files_renamed := 0
144
+ mut binary_files := 0
145
+ mut lines_added := 0
146
+ mut lines_deleted := 0
147
+ mut i := 0
148
+ while i < len(order) {
149
+ p := order[i]
150
+ e := info[p]
151
+ categories := classify(p)
152
+ e["categories"] := categories
153
+ e["churn"] := e["additions"] + e["deletions"]
154
+ files = push(files, e)
155
+ cats = add_to_cats(cats, categories, p)
156
+
157
+ st := e["status"]
158
+ if st == "added" {
159
+ files_added = files_added + 1
160
+ }
161
+ if st == "modified" {
162
+ files_modified = files_modified + 1
163
+ }
164
+ if st == "deleted" {
165
+ files_deleted = files_deleted + 1
166
+ }
167
+ if st == "renamed" {
168
+ files_renamed = files_renamed + 1
169
+ }
170
+ if e["binary"] == true {
171
+ binary_files = binary_files + 1
172
+ }
173
+ lines_added = lines_added + e["additions"]
174
+ lines_deleted = lines_deleted + e["deletions"]
175
+ i = i + 1
176
+ }
177
+
178
+ churn := lines_added + lines_deleted
179
+ files_changed := len(order)
180
+ risk := risk_level(files_changed, churn, files_deleted, len(cats["dependency_manifests"]), len(cats["lockfiles"]), len(cats["generated"]), len(cats["ci"]))
181
+
182
+ summary := {
183
+ "files_changed": files_changed,
184
+ "files_added": files_added,
185
+ "files_modified": files_modified,
186
+ "files_deleted": files_deleted,
187
+ "files_renamed": files_renamed,
188
+ "binary_files": binary_files,
189
+ "lines_added": lines_added,
190
+ "lines_deleted": lines_deleted,
191
+ "total_churn": churn,
192
+ "risk_level": risk
193
+ }
194
+
195
+ return {
196
+ "base": refs["base_label"],
197
+ "head": refs["head_label"],
198
+ "generated_at": now_utc(),
199
+ "summary": summary,
200
+ "categories": cats,
201
+ "files": files,
202
+ "budget": {"checked": false, "passed": true, "failures": []}
203
+ }
204
+ }
205
+
206
+ # Top-level: produce the analysis model for `repo` over the requested refs, or
207
+ # {"error": "..."} when the directory is not a git repository.
208
+ export func analyze(repo, base_flag, head_flag) {
209
+ if is_repo(repo) == false {
210
+ return {"error": "not a git repository: " + repo}
211
+ }
212
+ root := repo_root(repo)
213
+ refs := resolve_refs(root, base_flag, head_flag)
214
+ if has_key(refs, "error") == 1 {
215
+ return {"error": refs["error"]}
216
+ }
217
+ diff_error := validate_diff(root, refs)
218
+ if diff_error != "" {
219
+ return {"error": diff_error}
220
+ }
221
+
222
+ mut order := []
223
+ mut info := {}
224
+
225
+ ns := ingest_namestatus(order, info, namestatus_text(root, refs))
226
+ order = ns["order"]
227
+ info = ns["info"]
228
+
229
+ num := ingest_numstat(order, info, numstat_text(root, refs))
230
+ order = num["order"]
231
+ info = num["info"]
232
+
233
+ if refs["include_untracked"] == true {
234
+ ut := ingest_untracked(order, info, untracked_list(root), root)
235
+ order = ut["order"]
236
+ info = ut["info"]
237
+ }
238
+
239
+ return build_model(root, refs, order, info)
240
+ }
@@ -0,0 +1,92 @@
1
+ # ChangeBucket :: budget
2
+ #
3
+ # Deterministic threshold checking against an analysis model. A budget config is
4
+ # a plain dict (numeric limits default to null = unlimited; boolean bans default
5
+ # to false). `evaluate` returns the model with its `budget` block filled in:
6
+ # {"checked": true, "passed": bool, "failures": [strings]}
7
+
8
+ from src.util import commas, display_path
9
+
10
+ # A budget config with everything off. The CLI overlays user flags onto this.
11
+ export func empty_config() {
12
+ return {
13
+ "max_files": null,
14
+ "max_churn": null,
15
+ "max_additions": null,
16
+ "max_deletions": null,
17
+ "no_deletes": false,
18
+ "no_dependency_changes": false,
19
+ "no_lockfile_changes": false,
20
+ "no_config_changes": false,
21
+ "no_generated_changes": false
22
+ }
23
+ }
24
+
25
+ # True if the user actually set any budget constraint. Used to decide whether to
26
+ # show the Budget section on the default (non-`check`) command.
27
+ export func has_constraints(cfg) {
28
+ if type(cfg["max_files"]) != "null" || type(cfg["max_churn"]) != "null" {
29
+ return true
30
+ }
31
+ if type(cfg["max_additions"]) != "null" || type(cfg["max_deletions"]) != "null" {
32
+ return true
33
+ }
34
+ if cfg["no_deletes"] == true || cfg["no_dependency_changes"] == true {
35
+ return true
36
+ }
37
+ if cfg["no_lockfile_changes"] == true || cfg["no_config_changes"] == true {
38
+ return true
39
+ }
40
+ if cfg["no_generated_changes"] == true {
41
+ return true
42
+ }
43
+ return false
44
+ }
45
+
46
+ # Append a "label: value > max limit" failure when a numeric ceiling is exceeded.
47
+ func check_max(failures, label, value, limit) {
48
+ if type(limit) == "null" {
49
+ return failures
50
+ }
51
+ if value > limit {
52
+ return push(failures, label + ": " + commas(value) + " > max " + commas(limit))
53
+ }
54
+ return failures
55
+ }
56
+
57
+ # Append one failure per file in a banned category.
58
+ func check_banned(failures, banned, paths, label) {
59
+ if banned != true {
60
+ return failures
61
+ }
62
+ mut out := failures
63
+ for p in paths {
64
+ out = push(out, label + ": " + display_path(p))
65
+ }
66
+ return out
67
+ }
68
+
69
+ # Evaluate `cfg` against `model`, mutating and returning the model's budget block.
70
+ export func evaluate(model, cfg) {
71
+ s := model["summary"]
72
+ cats := model["categories"]
73
+ mut failures := []
74
+
75
+ failures = check_max(failures, "Changed files", s["files_changed"], cfg["max_files"])
76
+ failures = check_max(failures, "Total churn", s["total_churn"], cfg["max_churn"])
77
+ failures = check_max(failures, "Lines added", s["lines_added"], cfg["max_additions"])
78
+ failures = check_max(failures, "Lines deleted", s["lines_deleted"], cfg["max_deletions"])
79
+
80
+ if cfg["no_deletes"] == true && s["files_deleted"] > 0 {
81
+ failures = push(failures, "Deletes not allowed: " + commas(s["files_deleted"]) + " file(s) deleted")
82
+ }
83
+
84
+ failures = check_banned(failures, cfg["no_dependency_changes"], cats["dependency_manifests"], "Dependency manifest changed")
85
+ failures = check_banned(failures, cfg["no_lockfile_changes"], cats["lockfiles"], "Lockfile changed")
86
+ failures = check_banned(failures, cfg["no_config_changes"], cats["config"], "Config file changed")
87
+ failures = check_banned(failures, cfg["no_generated_changes"], cats["generated"], "Generated file changed")
88
+
89
+ passed := len(failures) == 0
90
+ model["budget"] := {"checked": true, "passed": passed, "failures": failures}
91
+ return model
92
+ }