pi-jscpd 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.
- package/CHANGELOG.md +99 -0
- package/CONTRIBUTING.md +144 -0
- package/LICENSE +21 -0
- package/README.md +231 -0
- package/SECURITY.md +93 -0
- package/docs/automatic-checkpoint.md +235 -0
- package/docs/compatibility.md +119 -0
- package/docs/effect-architecture.md +128 -0
- package/docs/fallow-coexistence.md +120 -0
- package/docs/overlay-interaction.md +347 -0
- package/docs/release.md +115 -0
- package/package.json +86 -0
- package/scripts/check-compatibility.mjs +103 -0
- package/skills/jscpd/SKILL.md +90 -0
- package/src/acknowledgements.ts +268 -0
- package/src/automatic.ts +396 -0
- package/src/baseline.ts +400 -0
- package/src/capability.ts +569 -0
- package/src/changed-files.ts +372 -0
- package/src/changed.ts +548 -0
- package/src/clone-identity.ts +373 -0
- package/src/config.ts +414 -0
- package/src/contract.ts +39 -0
- package/src/dispatch.ts +90 -0
- package/src/effect/clock.ts +10 -0
- package/src/effect/errors.ts +311 -0
- package/src/effect/filesystem.ts +240 -0
- package/src/effect/runtime-boundary.ts +25 -0
- package/src/effect/runtime-contract.ts +18 -0
- package/src/effect/services.ts +131 -0
- package/src/extension.ts +708 -0
- package/src/fallow.ts +479 -0
- package/src/finding-presentation.ts +73 -0
- package/src/index.ts +8 -0
- package/src/jscpd-report.ts +819 -0
- package/src/jscpd.ts +748 -0
- package/src/overlay.ts +1166 -0
- package/src/parser.ts +189 -0
- package/src/path-utils.ts +44 -0
- package/src/presentation.ts +232 -0
- package/src/process.ts +425 -0
- package/src/registry.ts +102 -0
- package/src/scan.ts +441 -0
- package/src/scheduler.ts +434 -0
- package/src/session-state.ts +229 -0
- package/src/status.ts +534 -0
- package/src/types.ts +334 -0
- package/src/value-utils.ts +14 -0
- package/src/verification.ts +220 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# `/jscpd` overlay interaction contract
|
|
2
|
+
|
|
3
|
+
Status: **implemented with managed-runtime action execution**
|
|
4
|
+
|
|
5
|
+
Applies to: bare `/jscpd` only
|
|
6
|
+
|
|
7
|
+
Does not change: `/jscpd scan`, `/jscpd changed`, `/jscpd status`, session controls, or `jscpd_run`
|
|
8
|
+
|
|
9
|
+
Overlay actions route through the extension's single managed runtime while Pi TUI
|
|
10
|
+
rendering and input remain the host adapter. The implementation preserves every
|
|
11
|
+
interaction below.
|
|
12
|
+
|
|
13
|
+
## Decision summary
|
|
14
|
+
|
|
15
|
+
In Pi's TUI mode, bare `/jscpd` opens one centered responsive overlay. Its
|
|
16
|
+
initial view is a **combined overview**, not an implicit scan or a command
|
|
17
|
+
palette. It shows compact status, the current ephemeral result when one exists,
|
|
18
|
+
and explicit actions. The user can request a changed-files check or full-project
|
|
19
|
+
scan, inspect bounded findings, toggle the session mode, refresh status, or open
|
|
20
|
+
help. The overlay never edits source, runs project tests, or changes jscpd
|
|
21
|
+
configuration.
|
|
22
|
+
|
|
23
|
+
The interface has three views:
|
|
24
|
+
|
|
25
|
+
1. **Overview** — status, last check, changed-file count, current-result summary,
|
|
26
|
+
and explicit actions.
|
|
27
|
+
2. **Findings navigator** — a bounded searchable list with inline expansion,
|
|
28
|
+
multi-selection, result counts, and a safe editor-prompt handoff.
|
|
29
|
+
3. **Help** — controls, state meanings, explicit command equivalents, and the
|
|
30
|
+
intentional-duplication caveat.
|
|
31
|
+
|
|
32
|
+
The visual and keyboard model follows Pi Fallow's established findings navigator:
|
|
33
|
+
a branded frame, compact status/count pills, selected-row background plus marker,
|
|
34
|
+
inline details, explicit earlier/later indicators, and visible controls. Navigation
|
|
35
|
+
uses one focused overlay and never creates stacked child overlays.
|
|
36
|
+
|
|
37
|
+
## Entry and initial state
|
|
38
|
+
|
|
39
|
+
Bare `/jscpd` must never mean “scan now.” In TUI mode the command calls:
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
ctx.ui.custom(factory, {
|
|
43
|
+
overlay: true,
|
|
44
|
+
overlayOptions: {
|
|
45
|
+
anchor: "center",
|
|
46
|
+
width: "90%",
|
|
47
|
+
minWidth: 50,
|
|
48
|
+
maxHeight: "95%",
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The overlay opens immediately with a bounded loading overview while it obtains
|
|
54
|
+
status through the existing service. A capability probe is allowed because the
|
|
55
|
+
user explicitly opened the integration; no duplication scan starts until the
|
|
56
|
+
user chooses one.
|
|
57
|
+
|
|
58
|
+
The overview then shows:
|
|
59
|
+
|
|
60
|
+
- session mode and whether it came from configuration or a session override;
|
|
61
|
+
- binary readiness and version, or a short setup/recovery state;
|
|
62
|
+
- effective extension configuration source;
|
|
63
|
+
- last-check summary;
|
|
64
|
+
- tracked session-change count and whether the current result is fresh;
|
|
65
|
+
- current finding/clean summary when an ephemeral result is available; and
|
|
66
|
+
- the available actions.
|
|
67
|
+
|
|
68
|
+
An ephemeral result cache may retain up to 100 normalized findings only for the
|
|
69
|
+
active extension runtime, project, branch scope, and mutation generation. This
|
|
70
|
+
TUI-only retention is independent of `maxFindings`: model/tool messages, explicit
|
|
71
|
+
subcommand output, acknowledgement writes, and persisted state remain capped by
|
|
72
|
+
the configured limit. The cache is cleared on session replacement/reload and
|
|
73
|
+
invalidated by a newer mutation or branch navigation. Findings and source
|
|
74
|
+
fragments are not added to persisted session state. After restoration, the
|
|
75
|
+
overview may show the persisted last-check summary, but opening details requires
|
|
76
|
+
a new current result.
|
|
77
|
+
|
|
78
|
+
## Views and actions
|
|
79
|
+
|
|
80
|
+
### Overview
|
|
81
|
+
|
|
82
|
+
The action list is ordered as follows:
|
|
83
|
+
|
|
84
|
+
1. **Check session changes** — equivalent to `/jscpd changed`; this is the
|
|
85
|
+
default action when tracked changes exist and scanning is enabled.
|
|
86
|
+
2. **Scan project** — equivalent to `/jscpd scan` with no target arguments.
|
|
87
|
+
3. **View findings** — available only for a fresh cached result containing
|
|
88
|
+
findings.
|
|
89
|
+
4. **Refresh status** — equivalent to the status service, not a duplication
|
|
90
|
+
scan.
|
|
91
|
+
5. **Disable for session** or **Enable for session** — routes through the
|
|
92
|
+
existing `off`/`on` control and changes no project file.
|
|
93
|
+
6. **Help**.
|
|
94
|
+
|
|
95
|
+
Arbitrary target entry is intentionally omitted from the first overlay. Users
|
|
96
|
+
retain `/jscpd scan <target ...>` for scoped scans; this avoids introducing a
|
|
97
|
+
second path parser or an ambiguous free-form field.
|
|
98
|
+
|
|
99
|
+
### Findings navigator
|
|
100
|
+
|
|
101
|
+
The findings view uses the ordering already selected by the presentation layer.
|
|
102
|
+
For changed checks, pairs whose two locations changed in the session come first,
|
|
103
|
+
then larger pairs, then deterministic location order. Each compact row identifies
|
|
104
|
+
both bounded paths and line spans plus line count, token count, and format.
|
|
105
|
+
`N`, `E`, and `C` markers mean `new in this session`, `existing match`, and
|
|
106
|
+
`current location`; inline expansion always spells those labels out.
|
|
107
|
+
|
|
108
|
+
The navigator initially reveals 10 retained findings. **Load next 10 / L**
|
|
109
|
+
reveals another page without rescanning, and moving down or paging past the last
|
|
110
|
+
revealed row automatically reveals the next page. Search and “all shown”
|
|
111
|
+
selection operate on revealed findings; loading another page extends that set.
|
|
112
|
+
|
|
113
|
+
The header and result context distinguish filtered, shown, retained, total,
|
|
114
|
+
overlay-cache omissions, and safely unclassified counts. The viewport shows
|
|
115
|
+
explicit earlier/later indicators. The selected row uses both a `❯` marker and
|
|
116
|
+
Pi's selected background. Below 64 columns, each finding becomes a three-line row
|
|
117
|
+
so both locations remain readable rather than disappearing behind truncation.
|
|
118
|
+
|
|
119
|
+
Enter, Space, Right, or `l` expands the current finding inline. Left or `h`
|
|
120
|
+
collapses it. Expanded content repeats both locations, relation labels, size,
|
|
121
|
+
format, verification state, omission/ambiguity context, and advisory next steps.
|
|
122
|
+
The list never renders source fragments or internal fingerprints and retains at
|
|
123
|
+
most 100 validated findings.
|
|
124
|
+
|
|
125
|
+
`/` enters search mode. Search is a case-insensitive literal substring match
|
|
126
|
+
against the two displayed paths and format. It is not a regular expression,
|
|
127
|
+
does not access the filesystem, and is capped at 256 Unicode code points. Enter
|
|
128
|
+
accepts the query, Esc restores the query that existed before editing, Ctrl+U
|
|
129
|
+
clears it, and `x` clears an applied query. An empty query restores deterministic
|
|
130
|
+
order. A no-match state keeps search and close controls visible.
|
|
131
|
+
|
|
132
|
+
`s` or Tab marks the current finding, `A` toggles all shown findings, and `c`
|
|
133
|
+
clears selection. `e` or `a` closes the overlay and returns a compact prompt for
|
|
134
|
+
the selected findings—or the current finding when none are marked. At most 20
|
|
135
|
+
findings and 12,000 Unicode code points enter that prompt. Only after
|
|
136
|
+
`ui.custom()` has returned does the launcher call `setEditorText()` and notify
|
|
137
|
+
the user. It never submits the editor, triggers a model turn, mutates source,
|
|
138
|
+
runs tests, or writes configuration. No “refactor now,” “delete,” or automatic
|
|
139
|
+
ignore/configure action belongs in the overlay.
|
|
140
|
+
|
|
141
|
+
### Help
|
|
142
|
+
|
|
143
|
+
Help lists the visible keyboard controls, command equivalents, missing-binary
|
|
144
|
+
setup direction, session-only nature of enable/disable, and the advisory rule.
|
|
145
|
+
It remains bounded and does not probe or scan.
|
|
146
|
+
|
|
147
|
+
## Asynchronous state model
|
|
148
|
+
|
|
149
|
+
The shell has exactly one of these states:
|
|
150
|
+
|
|
151
|
+
| State | Rendering and available behavior |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `loading-status` | Open immediately; show “Loading status…” and allow cancel/close. |
|
|
154
|
+
| `ready` | Show overview and enabled actions. |
|
|
155
|
+
| `running-changed` | Show “Checking session changes…” and allow cancellation. |
|
|
156
|
+
| `running-scan` | Show “Scanning project…” and allow cancellation. |
|
|
157
|
+
| `clean` | Show a short clean result; add no model message. |
|
|
158
|
+
| `findings` | Show count and allow Findings/detail navigation. |
|
|
159
|
+
| `empty` | Explain that no session-owned changed files are tracked and no scan ran. |
|
|
160
|
+
| `disabled` | Explain the session/config state and make Enable the primary action. |
|
|
161
|
+
| `unavailable` | Show missing/incompatible binary or baseline limitation and recovery action. |
|
|
162
|
+
| `timed-out` | Show the configured bound and allow retry. |
|
|
163
|
+
| `cancelled` | Confirm cancellation and return to usable overview state. |
|
|
164
|
+
| `failed` | Show a bounded safe reason and allow status refresh/retry. |
|
|
165
|
+
| `stale` | Do not present the result as current; require rescan or reopen. |
|
|
166
|
+
|
|
167
|
+
Messages reuse normalized execution results and must not expose subprocess
|
|
168
|
+
output, environment values, temporary paths, source fragments, or internal
|
|
169
|
+
fingerprints.
|
|
170
|
+
|
|
171
|
+
Only one overlay action may run at a time. While it runs, scan/toggle actions are
|
|
172
|
+
disabled. Status/help navigation may remain available only if it does not hide
|
|
173
|
+
the cancellation control.
|
|
174
|
+
|
|
175
|
+
## Cancellation, lifecycle, and ownership
|
|
176
|
+
|
|
177
|
+
Each overlay instance owns one action `AbortController`. The action still routes
|
|
178
|
+
through the existing scheduled executor and serialized `JscpdService`; the UI
|
|
179
|
+
never starts a child process directly.
|
|
180
|
+
|
|
181
|
+
- `Esc` or the configured select-cancel key during an idle view goes back one
|
|
182
|
+
view; from Overview it closes the overlay.
|
|
183
|
+
- During loading or a scan, the first cancel requests abort and keeps the overlay
|
|
184
|
+
open until the bounded operation settles, then shows `cancelled`.
|
|
185
|
+
- `Ctrl+C` has the same safe cancel behavior while work is active. When idle it
|
|
186
|
+
closes the overlay rather than exiting Pi.
|
|
187
|
+
- `q` closes from an idle view. If work is active it requests cancellation and
|
|
188
|
+
closes only after owned settlement.
|
|
189
|
+
- `dispose()` is idempotent, aborts owned work, drops late render callbacks, and
|
|
190
|
+
starts no cleanup process of its own.
|
|
191
|
+
- Session shutdown, reload, branch navigation, or command-context cancellation
|
|
192
|
+
aborts the action. Late completions cannot update another branch or project.
|
|
193
|
+
- A result is renderable only when its overlay instance token, project identity,
|
|
194
|
+
lifecycle scope, and mutation generation remain current. Otherwise render
|
|
195
|
+
`stale` or discard it if the overlay has closed.
|
|
196
|
+
|
|
197
|
+
Closing the overlay does not cancel unrelated explicit or automatic work. An
|
|
198
|
+
overlay scan may supersede scheduler-owned automatic work under the existing
|
|
199
|
+
explicit-work priority rule.
|
|
200
|
+
|
|
201
|
+
## Keyboard and accessibility contract
|
|
202
|
+
|
|
203
|
+
Use the injected `KeybindingsManager` for Pi select/navigation bindings and
|
|
204
|
+
`matchesKey()` only for overlay-specific letter shortcuts. Never import or
|
|
205
|
+
mutate global keybindings.
|
|
206
|
+
|
|
207
|
+
| Input | Behavior |
|
|
208
|
+
| --- | --- |
|
|
209
|
+
| configured up/down; `j`/`k` outside search mode | Move selection |
|
|
210
|
+
| configured page up/down; Home/End | Scroll one viewport or jump to a boundary |
|
|
211
|
+
| configured confirm; Enter, Space, Right, `l` | Activate an action or expand/collapse a finding |
|
|
212
|
+
| Left / `h` | Collapse the current finding |
|
|
213
|
+
| configured cancel; `Esc` | Cancel search/work, go back, or close |
|
|
214
|
+
| `Tab` | Open Findings from Overview; mark/unmark the current finding in Findings |
|
|
215
|
+
| `Shift+Tab` | Return to Overview from Findings or Help |
|
|
216
|
+
| `/` | Enter literal search mode in Findings |
|
|
217
|
+
| `Backspace`, arrows, Home/End, `Ctrl+U` | Edit or clear search while search mode is active |
|
|
218
|
+
| `x` | Clear the applied finding search |
|
|
219
|
+
| `L` | Reveal the next 10 retained findings without rescanning |
|
|
220
|
+
| `s` / Tab, `A`, `c` | Mark current, toggle all shown, or clear selected findings |
|
|
221
|
+
| `e` / `a` | Close and load a bounded finding prompt into Pi's editor |
|
|
222
|
+
| `r` | Rerun the current scan kind, or refresh status when no scan kind exists |
|
|
223
|
+
| `c` | Start changed-files check from Overview |
|
|
224
|
+
| `s` | Start full-project scan from Overview |
|
|
225
|
+
| `o` | Toggle session enable/disable from Overview |
|
|
226
|
+
| `?` | Open Help |
|
|
227
|
+
| `q` / `Ctrl+C` | Safe close/cancel as defined above |
|
|
228
|
+
|
|
229
|
+
Every action has a visible text label; color, glyphs, and punctuation are never
|
|
230
|
+
the only indication of selection, disabled state, error, freshness, or relation.
|
|
231
|
+
The selected row uses a textual `❯` marker plus Pi's selected background. The
|
|
232
|
+
component accepts focus through the normal `Focusable` contract, propagates it
|
|
233
|
+
to the search input, and requests a render after every state, selection, search,
|
|
234
|
+
or async-result change.
|
|
235
|
+
|
|
236
|
+
## Responsive and bounded rendering
|
|
237
|
+
|
|
238
|
+
Implementation must use the callback-provided theme and ANSI-aware Pi TUI
|
|
239
|
+
utilities (`visibleWidth`, `truncateToWidth`, and wrapping helpers). Every
|
|
240
|
+
rendered line must have visible width less than or equal to the supplied width.
|
|
241
|
+
|
|
242
|
+
- At 64 columns and wider, findings use one compact row with independently
|
|
243
|
+
middle-truncated locations and retained size/format metadata.
|
|
244
|
+
- Below 64 columns, each finding uses three rows so both locations remain
|
|
245
|
+
identifiable.
|
|
246
|
+
- Below 40 columns, use a compact single-column layout: short title, one action
|
|
247
|
+
or location field per line, no decorative side-by-side content, and a minimal
|
|
248
|
+
footer that always retains cancel/close guidance.
|
|
249
|
+
- Height is capped at 95% of the terminal. Header and footer remain visible;
|
|
250
|
+
content scrolls within the remaining viewport. Render no more rows than the
|
|
251
|
+
current overlay viewport instead of relying on compositor truncation.
|
|
252
|
+
- Paths use the existing middle-ellipsis bound and receive a second display-
|
|
253
|
+
width truncation at render time. Counts and omitted-state text remain visible.
|
|
254
|
+
- Search is capped at 256 code points, findings reveal in pages of 10, the
|
|
255
|
+
overlay cache is capped at 100, inline detail at one finding, and prompt
|
|
256
|
+
handoff at 20 findings/12,000 code points. No unbounded report, list, or source
|
|
257
|
+
content enters a component.
|
|
258
|
+
|
|
259
|
+
The overlay must remain closable on a 30x10 terminal. It may reduce content to a
|
|
260
|
+
status line, selected action, and footer, but it must not use responsive
|
|
261
|
+
`visible: false` because an invisible focused modal can strand input.
|
|
262
|
+
|
|
263
|
+
## Non-TUI fallback
|
|
264
|
+
|
|
265
|
+
The command must branch on `ctx.mode`, not `ctx.hasUI`: RPC reports UI capability
|
|
266
|
+
but cannot render terminal components.
|
|
267
|
+
|
|
268
|
+
- **RPC:** never call `ui.custom()`. Execute the existing status operation and
|
|
269
|
+
emit one non-blocking `ui.notify` request containing the bounded status plus:
|
|
270
|
+
`Use /jscpd changed, /jscpd scan, /jscpd off|on, or /jscpd help.`
|
|
271
|
+
- **JSON and print:** never call `ui.custom()` and never start a duplication
|
|
272
|
+
scan. Execute the same status operation and write the same bounded plain-text
|
|
273
|
+
fallback once to stderr, preserving stdout for JSON/print output. A closed
|
|
274
|
+
stderr fails open.
|
|
275
|
+
- **All non-TUI modes:** do not wait for input, create a component, alter model
|
|
276
|
+
context, or trigger a model turn. Explicit subcommands and `jscpd_run` remain
|
|
277
|
+
the machine-friendly interface.
|
|
278
|
+
|
|
279
|
+
Exact fallback prefix:
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
The /jscpd overlay requires Pi TUI mode.
|
|
283
|
+
<bounded /jscpd status output>
|
|
284
|
+
Use /jscpd changed, /jscpd scan, /jscpd off|on, or /jscpd help.
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Acceptance examples
|
|
288
|
+
|
|
289
|
+
### Ready overview
|
|
290
|
+
|
|
291
|
+
```text
|
|
292
|
+
╭ ✦ pi-jscpd · enabled ─────────────────────────────────────────╮
|
|
293
|
+
│ ✓ enabled (configuration) jscpd 5.1.2 bundled │
|
|
294
|
+
│ Configuration built-in defaults · 3 session-changed files │
|
|
295
|
+
│ Last check 2 duplicate blocks │
|
|
296
|
+
│ │
|
|
297
|
+
│ ❯ ◆ Check session changes · new blocks in tracked edits │
|
|
298
|
+
│ ◇ Scan project · all current duplicate blocks │
|
|
299
|
+
├──────────────────────────────────────────────────────────────┤
|
|
300
|
+
│ ↑↓ navigate · Enter select · c changes · s scan · ? help │
|
|
301
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Findings navigator
|
|
305
|
+
|
|
306
|
+
```text
|
|
307
|
+
╭ ✦ pi-jscpd · 10/48 findings ─────────────────────────────────╮
|
|
308
|
+
│ 10 shown 48 retained 48 total │
|
|
309
|
+
│ Load next 10 / L · 38 cached findings remain │
|
|
310
|
+
│ ❯ ☐ ▸ N src/new.ts:12-28 ↔ E src/old.ts:44-60 · 17L/91T ts │
|
|
311
|
+
│ ☑ ▸ C lib/a.py:3-10 ↔ C lib/b.py:20-27 · 8L/42T python │
|
|
312
|
+
├──────────────────────────────────────────────────────────────┤
|
|
313
|
+
│ 1 selected · ↑↓ navigate · L next 10 · Enter expand · e load│
|
|
314
|
+
╰──────────────────────────────────────────────────────────────╯
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Component and smoke-test matrix
|
|
318
|
+
|
|
319
|
+
Tests cover:
|
|
320
|
+
|
|
321
|
+
- bare command opens exactly one overlay only in `mode === "tui"`;
|
|
322
|
+
- RPC notification fallback and JSON/print stderr fallback start no scan;
|
|
323
|
+
- loading, clean, empty, findings, disabled, missing, incompatible, timeout,
|
|
324
|
+
cancellation, failure, and stale states;
|
|
325
|
+
- 100x30, 52x16, and 30x10 rendering with every line within visible width and
|
|
326
|
+
header/footer retained;
|
|
327
|
+
- configured navigation plus expand/collapse, search editing/cancellation,
|
|
328
|
+
filtering/no-match, scrolling, selection, and view back behavior;
|
|
329
|
+
- one active action, repeated-key suppression, cancellation propagation, late
|
|
330
|
+
completion discard, idempotent `dispose()`, and no timer/process leak;
|
|
331
|
+
- both finding locations, relation labels, size, format,
|
|
332
|
+
filtered/shown/retained/total counts, 10-item manual and automatic reveal,
|
|
333
|
+
cache omissions, ambiguity, and no source fragments/fingerprints;
|
|
334
|
+
- bounded prompt handoff only after overlay close, without submit or mutation;
|
|
335
|
+
- no source mutation or configuration write from any overlay action; and
|
|
336
|
+
- package/RPC smoke proving command discovery and non-TUI completion without a
|
|
337
|
+
hang.
|
|
338
|
+
|
|
339
|
+
## Deferred from the first overlay
|
|
340
|
+
|
|
341
|
+
- source preview or syntax-highlighted fragments;
|
|
342
|
+
- arbitrary scan-target input;
|
|
343
|
+
- mouse interaction;
|
|
344
|
+
- clone-family graphs;
|
|
345
|
+
- automatic refactoring, test execution, ignore-rule writes, or configuration
|
|
346
|
+
editing; and
|
|
347
|
+
- persisted finding-detail caches.
|
package/docs/release.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Release and publication policy
|
|
2
|
+
|
|
3
|
+
`pi-jscpd` releases are explicitly authorized by
|
|
4
|
+
[Revaz Zakalashvili](https://github.com/revazi) and published from reviewed tags
|
|
5
|
+
through `.github/workflows/release.yml`. CI, issue closure, package certification,
|
|
6
|
+
or CODEOWNERS review does not independently authorize a release.
|
|
7
|
+
|
|
8
|
+
The first public release is `0.1.0`. The required Effect migration and
|
|
9
|
+
recertification work is documented in
|
|
10
|
+
[Effect architecture and conformance](effect-architecture.md).
|
|
11
|
+
|
|
12
|
+
## Release gates
|
|
13
|
+
|
|
14
|
+
Run the complete gate from a clean supported checkout:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
npm ci --ignore-scripts
|
|
18
|
+
npm run release:check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`release:check` validates public Markdown links, repository hygiene, host
|
|
22
|
+
compatibility, strict types, Biome, the network-free test suite, the exact packed
|
|
23
|
+
and installed artifact, and the package dry run. CI repeats these checks on Node
|
|
24
|
+
22.19.0 and 24.12.0.
|
|
25
|
+
|
|
26
|
+
The manual **Release readiness (no publish)** workflow validates an exact
|
|
27
|
+
40-character commit that must resolve to `origin/main`. It has read-only
|
|
28
|
+
repository permission, receives no registry credential, retains no package
|
|
29
|
+
artifact, and cannot publish or create a GitHub release.
|
|
30
|
+
|
|
31
|
+
## Tagged publication workflow
|
|
32
|
+
|
|
33
|
+
`.github/workflows/release.yml` follows the `pi-fallow` release pattern. A pushed
|
|
34
|
+
`vMAJOR.MINOR.PATCH` tag starts one protected `npm` environment job that:
|
|
35
|
+
|
|
36
|
+
1. checks out the complete tag history without persisting credentials;
|
|
37
|
+
2. installs the pinned trusted-publishing npm version;
|
|
38
|
+
3. verifies tag/package/changelog and package-lock version agreement;
|
|
39
|
+
4. rejects a manifest that still has the npm `private` guard;
|
|
40
|
+
5. installs locked dependencies without lifecycle scripts;
|
|
41
|
+
6. reruns `npm run release:check`;
|
|
42
|
+
7. publishes publicly through npm trusted publishing with provenance and
|
|
43
|
+
lifecycle scripts disabled; and
|
|
44
|
+
8. creates the matching GitHub release only after npm publication succeeds.
|
|
45
|
+
|
|
46
|
+
Because npm cannot attach trusted-publisher settings before an unscoped package
|
|
47
|
+
exists, the `v0.1.0` publish step alone may receive a short-lived granular
|
|
48
|
+
`NPM_TOKEN` repository secret for first-publication bootstrap. Dependency
|
|
49
|
+
installation and release validation do not receive it, and `npm publish` runs
|
|
50
|
+
with lifecycle scripts disabled. Delete the secret and remove this fallback as
|
|
51
|
+
soon as npm confirms `0.1.0`; all later releases must use OIDC only. Default
|
|
52
|
+
GitHub permissions are empty; the release job receives only `contents: write`
|
|
53
|
+
for the GitHub release and `id-token: write` for trusted publishing.
|
|
54
|
+
|
|
55
|
+
Before later tags are pushed, the repository's protected `npm` environment and
|
|
56
|
+
npm trusted-publisher binding must match the exact repository and
|
|
57
|
+
`.github/workflows/release.yml`. Authentication details, token values, `.npmrc`
|
|
58
|
+
files, screenshots, and credential output must never be committed or copied into
|
|
59
|
+
issues or CI logs.
|
|
60
|
+
|
|
61
|
+
## Version and changelog procedure
|
|
62
|
+
|
|
63
|
+
Published versions follow Semantic Versioning. For each approved release:
|
|
64
|
+
|
|
65
|
+
1. Move relevant entries from `Unreleased` to
|
|
66
|
+
`## [MAJOR.MINOR.PATCH] - YYYY-MM-DD` and update comparison links.
|
|
67
|
+
2. Update `package.json` and `package-lock.json` together.
|
|
68
|
+
3. Confirm the package is publishable and `publishConfig` still requests public
|
|
69
|
+
access and provenance.
|
|
70
|
+
4. Merge the reviewed release commit to `main` and wait for both supported-Node
|
|
71
|
+
CI jobs to pass on that exact commit.
|
|
72
|
+
5. Run the manual non-publishing readiness workflow for the exact `main` SHA when
|
|
73
|
+
additional release evidence is required.
|
|
74
|
+
6. Create one annotated `vMAJOR.MINOR.PATCH` tag on that exact commit and push it.
|
|
75
|
+
7. Watch the release workflow through npm publication and GitHub Release
|
|
76
|
+
creation.
|
|
77
|
+
|
|
78
|
+
Do not move a tag, publish from an unreviewed checkout, bypass a failed gate, or
|
|
79
|
+
widen permissions to make a release pass.
|
|
80
|
+
|
|
81
|
+
## Failure and rollback policy
|
|
82
|
+
|
|
83
|
+
Before publication, any failed or ambiguous check means stop and discard or fix
|
|
84
|
+
the candidate. npm versions are immutable. After publication:
|
|
85
|
+
|
|
86
|
+
- never overwrite or silently replace a version;
|
|
87
|
+
- verify registry name, version, integrity, provenance, and source commit;
|
|
88
|
+
- deprecate a defective version with a concise migration message when needed;
|
|
89
|
+
- publish a corrected patch through the full process;
|
|
90
|
+
- use npm unpublish only when the maintainer determines it is necessary and
|
|
91
|
+
allowed by npm policy; and
|
|
92
|
+
- coordinate security defects through GitHub private vulnerability reporting and
|
|
93
|
+
[SECURITY.md](../SECURITY.md).
|
|
94
|
+
|
|
95
|
+
If GitHub Release creation fails after npm publication, verify npm first and
|
|
96
|
+
create the GitHub release for the same immutable tag. Do not republish merely to
|
|
97
|
+
repair release notes.
|
|
98
|
+
|
|
99
|
+
## Post-release verification
|
|
100
|
+
|
|
101
|
+
A release is complete only after the maintainer verifies:
|
|
102
|
+
|
|
103
|
+
1. npm metadata matches the approved name/version and reports integrity and
|
|
104
|
+
provenance;
|
|
105
|
+
2. a disposable project installs that exact version without lifecycle scripts;
|
|
106
|
+
3. installed files match the certified allowlist;
|
|
107
|
+
4. supported Pi discovers `/jscpd`, `jscpd_run`, and `/skill:jscpd` without
|
|
108
|
+
warnings in representative RPC, JSON, print, and TUI-compatible paths;
|
|
109
|
+
5. the package-owned jscpd route performs a controlled real scan;
|
|
110
|
+
6. damaged-analyzer behavior remains dormant and fail open;
|
|
111
|
+
7. Effect interruption closes active fibers and the root scope; and
|
|
112
|
+
8. shutdown leaves no child process or temporary report directory.
|
|
113
|
+
|
|
114
|
+
Record only bounded pass/fail evidence, public versions, and artifact digests.
|
|
115
|
+
Never retain source fragments, local paths, credentials, or environment dumps.
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-jscpd",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A Pi-native, polyglot duplication guardrail powered by jscpd.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Revaz Zakalashvili",
|
|
9
|
+
"url": "https://github.com/revazi"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/revazi/pi-jscpd.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/revazi/pi-jscpd#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/revazi/pi-jscpd/issues"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public",
|
|
21
|
+
"provenance": true
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"pi-package",
|
|
25
|
+
"pi-extension",
|
|
26
|
+
"jscpd",
|
|
27
|
+
"code-duplication",
|
|
28
|
+
"agentic-coding"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"skills",
|
|
33
|
+
"docs",
|
|
34
|
+
"scripts/check-compatibility.mjs",
|
|
35
|
+
"CHANGELOG.md",
|
|
36
|
+
"CONTRIBUTING.md",
|
|
37
|
+
"SECURITY.md",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"typecheck": "tsc --noEmit",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"format": "biome check --write src test scripts package.json tsconfig.json biome.json",
|
|
45
|
+
"lint": "biome check src test scripts package.json tsconfig.json biome.json",
|
|
46
|
+
"compatibility:check": "node scripts/check-compatibility.mjs",
|
|
47
|
+
"architecture:check": "node scripts/check-effect-boundaries.mjs",
|
|
48
|
+
"docs:check": "node scripts/check-markdown.mjs",
|
|
49
|
+
"repo:hygiene": "node scripts/check-repository-hygiene.mjs",
|
|
50
|
+
"check": "npm run compatibility:check && npm run architecture:check && npm run typecheck && npm run lint && npm test",
|
|
51
|
+
"pack:certify": "node scripts/package-certify.mjs",
|
|
52
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
53
|
+
"release:check": "npm run docs:check && npm run repo:hygiene && npm run check && npm run pack:certify && npm run pack:dry-run"
|
|
54
|
+
},
|
|
55
|
+
"pi": {
|
|
56
|
+
"extensions": [
|
|
57
|
+
"./src/index.ts"
|
|
58
|
+
],
|
|
59
|
+
"skills": [
|
|
60
|
+
"./skills/jscpd/SKILL.md"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@earendil-works/pi-ai": ">=0.84.4 <0.85.0",
|
|
65
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4 <0.85.0",
|
|
66
|
+
"@earendil-works/pi-tui": ">=0.84.4 <0.85.0",
|
|
67
|
+
"typebox": ">=1.3.7 <2"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"@biomejs/biome": "2.5.11",
|
|
71
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
73
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
74
|
+
"@types/node": "22.20.1",
|
|
75
|
+
"typebox": "1.3.7",
|
|
76
|
+
"typescript": "5.9.3",
|
|
77
|
+
"vitest": "4.1.11"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=22.19.0 <23 || >=24 <25"
|
|
81
|
+
},
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"effect": "3.22.1",
|
|
84
|
+
"jscpd": "5.1.2"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const manifest = await readJson(join(projectRoot, "package.json"));
|
|
7
|
+
const expectedNodeRange = ">=22.19.0 <23 || >=24 <25";
|
|
8
|
+
const expectedPiVersion = "0.84.4";
|
|
9
|
+
const expectedEffectVersion = "3.22.1";
|
|
10
|
+
const expectedEffectIntegrity =
|
|
11
|
+
"sha512-TNoXushmPOBAjJlthF5d2QwnX2xBPEtcNJr5XKNKbRLbDvBcOYkXlYDfvGfSA0zriwLFuCll5MDtNMAdZL17PQ==";
|
|
12
|
+
const expectedJscpdVersion = "5.1.2";
|
|
13
|
+
const expectedPeerRanges = Object.freeze({
|
|
14
|
+
"@earendil-works/pi-ai": ">=0.84.4 <0.85.0",
|
|
15
|
+
"@earendil-works/pi-coding-agent": ">=0.84.4 <0.85.0",
|
|
16
|
+
"@earendil-works/pi-tui": ">=0.84.4 <0.85.0",
|
|
17
|
+
typebox: ">=1.3.7 <2",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (manifest.engines?.node !== expectedNodeRange) {
|
|
21
|
+
fail(`The Node engine range must remain ${expectedNodeRange}.`);
|
|
22
|
+
}
|
|
23
|
+
if (!supportsNode(process.versions.node)) {
|
|
24
|
+
fail(`Node ${process.versions.node} is outside the supported Node 22.19+ and Node 24 ranges.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const [name, range] of Object.entries(expectedPeerRanges)) {
|
|
28
|
+
if (manifest.peerDependencies?.[name] !== range) {
|
|
29
|
+
fail(`The ${name} peer range must remain ${range}.`);
|
|
30
|
+
}
|
|
31
|
+
const installed = await readJson(
|
|
32
|
+
join(projectRoot, "node_modules", ...name.split("/"), "package.json"),
|
|
33
|
+
);
|
|
34
|
+
const pinned = manifest.devDependencies?.[name];
|
|
35
|
+
if (typeof pinned !== "string" || pinned === "" || /[<>=*^~| ]/.test(pinned)) {
|
|
36
|
+
fail(`The development fixture for ${name} must be an exact version.`);
|
|
37
|
+
}
|
|
38
|
+
if (installed.version !== pinned) {
|
|
39
|
+
fail(
|
|
40
|
+
`Installed ${name} ${installed.version} does not match the ${pinned} development fixture.`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const name of [
|
|
46
|
+
"@earendil-works/pi-ai",
|
|
47
|
+
"@earendil-works/pi-coding-agent",
|
|
48
|
+
"@earendil-works/pi-tui",
|
|
49
|
+
]) {
|
|
50
|
+
if (manifest.devDependencies?.[name] !== expectedPiVersion) {
|
|
51
|
+
fail(`The tested Pi package set must remain aligned at ${expectedPiVersion}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (manifest.dependencies?.effect !== expectedEffectVersion) {
|
|
56
|
+
fail(`The Effect runtime dependency must remain pinned at ${expectedEffectVersion}.`);
|
|
57
|
+
}
|
|
58
|
+
const installedEffect = await readJson(join(projectRoot, "node_modules", "effect", "package.json"));
|
|
59
|
+
if (installedEffect.version !== expectedEffectVersion || installedEffect.license !== "MIT") {
|
|
60
|
+
fail(
|
|
61
|
+
`Installed Effect ${installedEffect.version} (${installedEffect.license}) does not match the reviewed ${expectedEffectVersion} MIT runtime.`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
const lock = await readJson(join(projectRoot, "package-lock.json"));
|
|
65
|
+
const lockedEffect = lock.packages?.["node_modules/effect"];
|
|
66
|
+
if (
|
|
67
|
+
lock.packages?.[""]?.dependencies?.effect !== expectedEffectVersion ||
|
|
68
|
+
lockedEffect?.version !== expectedEffectVersion ||
|
|
69
|
+
lockedEffect?.integrity !== expectedEffectIntegrity
|
|
70
|
+
) {
|
|
71
|
+
fail(`package-lock.json does not preserve reviewed Effect ${expectedEffectVersion} integrity.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (manifest.dependencies?.jscpd !== expectedJscpdVersion) {
|
|
75
|
+
fail(`The bundled jscpd dependency must remain pinned at ${expectedJscpdVersion}.`);
|
|
76
|
+
}
|
|
77
|
+
const installedJscpd = await readJson(join(projectRoot, "node_modules", "jscpd", "package.json"));
|
|
78
|
+
if (installedJscpd.version !== expectedJscpdVersion) {
|
|
79
|
+
fail(
|
|
80
|
+
`Installed jscpd ${installedJscpd.version} does not match the ${expectedJscpdVersion} runtime dependency.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(
|
|
85
|
+
`Compatibility check passed: Node ${process.versions.node}, Pi ${expectedPiVersion}, TypeBox ${manifest.devDependencies.typebox}, Effect ${expectedEffectVersion}, jscpd ${expectedJscpdVersion}.`,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
function supportsNode(version) {
|
|
89
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-|$)/.exec(version);
|
|
90
|
+
if (!match) return false;
|
|
91
|
+
const major = Number(match[1]);
|
|
92
|
+
const minor = Number(match[2]);
|
|
93
|
+
return major === 24 || (major === 22 && minor >= 19);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readJson(path) {
|
|
97
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fail(message) {
|
|
101
|
+
console.error(`Compatibility check failed: ${message}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|