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.
Files changed (49) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/CONTRIBUTING.md +144 -0
  3. package/LICENSE +21 -0
  4. package/README.md +231 -0
  5. package/SECURITY.md +93 -0
  6. package/docs/automatic-checkpoint.md +235 -0
  7. package/docs/compatibility.md +119 -0
  8. package/docs/effect-architecture.md +128 -0
  9. package/docs/fallow-coexistence.md +120 -0
  10. package/docs/overlay-interaction.md +347 -0
  11. package/docs/release.md +115 -0
  12. package/package.json +86 -0
  13. package/scripts/check-compatibility.mjs +103 -0
  14. package/skills/jscpd/SKILL.md +90 -0
  15. package/src/acknowledgements.ts +268 -0
  16. package/src/automatic.ts +396 -0
  17. package/src/baseline.ts +400 -0
  18. package/src/capability.ts +569 -0
  19. package/src/changed-files.ts +372 -0
  20. package/src/changed.ts +548 -0
  21. package/src/clone-identity.ts +373 -0
  22. package/src/config.ts +414 -0
  23. package/src/contract.ts +39 -0
  24. package/src/dispatch.ts +90 -0
  25. package/src/effect/clock.ts +10 -0
  26. package/src/effect/errors.ts +311 -0
  27. package/src/effect/filesystem.ts +240 -0
  28. package/src/effect/runtime-boundary.ts +25 -0
  29. package/src/effect/runtime-contract.ts +18 -0
  30. package/src/effect/services.ts +131 -0
  31. package/src/extension.ts +708 -0
  32. package/src/fallow.ts +479 -0
  33. package/src/finding-presentation.ts +73 -0
  34. package/src/index.ts +8 -0
  35. package/src/jscpd-report.ts +819 -0
  36. package/src/jscpd.ts +748 -0
  37. package/src/overlay.ts +1166 -0
  38. package/src/parser.ts +189 -0
  39. package/src/path-utils.ts +44 -0
  40. package/src/presentation.ts +232 -0
  41. package/src/process.ts +425 -0
  42. package/src/registry.ts +102 -0
  43. package/src/scan.ts +441 -0
  44. package/src/scheduler.ts +434 -0
  45. package/src/session-state.ts +229 -0
  46. package/src/status.ts +534 -0
  47. package/src/types.ts +334 -0
  48. package/src/value-utils.ts +14 -0
  49. 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.
@@ -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
+ }