@aefree/pi-plastic 0.4.0 → 0.5.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 +17 -0
- package/README.md +8 -7
- package/extensions/bash-cm-diff-guard.ts +2 -1
- package/extensions/file-discovery-filter.ts +67 -0
- package/index.ts +12 -0
- package/package.json +7 -8
- package/skills/_shared/references/conventional-commits.md +198 -198
- package/skills/using-plastic/SKILL.md +2 -2
- package/skills/using-plastic/references/changeset-operations.md +2 -1
- package/skills/using-plastic/references/code-review-creation.md +30 -30
- package/skills/using-plastic/references/integration.md +2 -1
- package/skills/using-plastic/references/quick-reference.md +2 -1
- package/skills/using-plastic/references/resources.md +7 -7
- package/skills/using-plastic/references/reviewing-changes.md +4 -3
- package/skills/using-plastic/references/shelving.md +25 -25
- package/skills/using-plastic/references/troubleshooting.md +2 -1
- package/skills/using-plastic/references/workspaces.md +33 -33
- package/src/file-discovery-filter-rendezvous.ts +104 -0
- package/src/{repository-search-provider.ts → file-discovery-filter.ts} +21 -17
- package/src/plastic-core.ts +438 -152
- package/src/plastic-tool-loading.ts +2 -1
- package/evals/tool-loading/README.md +0 -38
- package/evals/tool-loading/RESULTS.md +0 -57
- package/evals/tool-loading/baseline.json +0 -34
- package/evals/tool-loading/cases.json +0 -70
- package/evals/tool-loading/config.json +0 -19
- package/evals/tool-loading/mutation-guard.ts +0 -30
- package/evals/tool-loading/provider-capture.ts +0 -22
- package/evals/tool-loading/results/.gitignore +0 -3
- package/evals/tool-loading/run-eval.ts +0 -372
- package/extensions/repository-search-provider.ts +0 -66
- package/tests/bash-cm-diff-guard-validation.ts +0 -55
- package/tests/fixtures/plastic.selector +0 -3
- package/tests/fixtures/status-compact.txt +0 -1
- package/tests/fixtures/status-normal.txt +0 -1
- package/tests/packed-copy-owner-conflict.test.ts +0 -28
- package/tests/pi-tool-harness.ts +0 -116
- package/tests/plastic-branch-create-live.test.ts +0 -78
- package/tests/plastic-branch-query.test.ts +0 -26
- package/tests/plastic-branch-status.test.ts +0 -241
- package/tests/plastic-checkin-path-resolution.ts +0 -219
- package/tests/plastic-constrained-sampling.test.ts +0 -77
- package/tests/plastic-diff-workflow.test.ts +0 -86
- package/tests/plastic-dynamic-tool-loading.test.ts +0 -224
- package/tests/plastic-extension-registration.test.ts +0 -34
- package/tests/plastic-live-smoke.test.ts +0 -37
- package/tests/plastic-mutation-execution.test.ts +0 -61
- package/tests/plastic-patch-command.test.ts +0 -118
- package/tests/plastic-process-lifecycle.test.ts +0 -148
- package/tests/plastic-tool-rendering.test.ts +0 -16
- package/tests/plastic-tool-validation.ts +0 -153
- package/tests/plastic-workspace.test.ts +0 -62
- package/tests/repository-search-provider-registration.test.ts +0 -37
- package/tests/repository-search-provider.test.ts +0 -27
|
@@ -16,9 +16,9 @@ Treat `plastic_merge` success as provisional until `plastic_status` confirms the
|
|
|
16
16
|
|
|
17
17
|
For the common closeout flow of merging a finished source branch into its parent branch, prefer `plastic_mergeToBranch` when available. When `target` is omitted, it resolves the source branch's Plastic parent branch instead of assuming `/dev`. It switches to the target, optionally updates, runs the safe merge, checks merge state, and checks in the merge result. Pass `target` only when the user explicitly wants a different integration branch.
|
|
18
18
|
|
|
19
|
-
`plastic_diff` is intentionally disabled.
|
|
19
|
+
`plastic_diff` is intentionally disabled. Route changed-path listing to `plastic_status()`, one workspace file to `plastic_diffFile(path="<workspace-path>")`, pending review to `plastic_workspaceDiff()`, and an explicit historical pair to `plastic_diffRevisions`. `plastic_diffFile` uses machine-readable status: added and explicitly selected private/new files compare against an empty base; changed, moved, and deleted items use their reported base revision when available. A `--nodata` base failure means Plastic cannot provide historical bytes; refresh/update the workspace or use two known file-qualified revisions. Explicit `revision` behavior remains a separate historical request. Binary content is reported rather than decoded as text.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
`plastic_workspaceDiff` runs status once, has intentional file/per-file/combined-output bounds, and returns per-file outcomes without aborting when one file is unavailable. It excludes private files by default; select their paths explicitly or pass `includePrivate=true` only when intentional. Use `plastic_patch` for review patch generation with `clean` and `integration` filters; it is generation-only and does not apply patches. It uses `PI_PLASTIC_DIFF_EXECUTABLE` when set, otherwise GNU/POSIX `diff` on PATH; paths containing spaces are supported, but Pi does not discover Git Bash paths automatically. Pass `toolPath` only for an intentional one-call override. For a large patch, pass an intentional new `output` path because Plastic will not overwrite it.
|
|
22
22
|
|
|
23
23
|
Prefer runtime `plastic_*` tools first. Keep `cm` shell commands as manual fallback.
|
|
24
24
|
|
|
@@ -77,7 +77,8 @@ Use one of these safe alternatives:
|
|
|
77
77
|
|
|
78
78
|
```text
|
|
79
79
|
plastic_status(machineReadable=true) # list pending paths/statuses
|
|
80
|
-
plastic_diffFile(path="<workspace-path>") #
|
|
80
|
+
plastic_diffFile(path="<workspace-path>") # one workspace file
|
|
81
|
+
plastic_workspaceDiff() # bounded pending review; excludes private unless selected/opted in
|
|
81
82
|
plastic_diffFile(path="<workspace-path>", revision="cs:<number>")
|
|
82
83
|
plastic_diffRevisions(leftRevision="<left-file-qualified-revspec>", rightRevision="<right-file-qualified-revspec>")
|
|
83
84
|
```
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
# Code Review Creation
|
|
2
|
-
|
|
3
|
-
## CLI (Preferred)
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
branch=$(cm status | head -1 | cut -d@ -f1 | xargs)
|
|
7
|
-
review_id=$(cm codereview "br:${branch}" "Feature: Brief description" --format="{id}")
|
|
8
|
-
|
|
9
|
-
# Assign reviewer immediately after creating the review
|
|
10
|
-
cm codereview -e "$review_id" --reviewer="reviewer-name"
|
|
11
|
-
|
|
12
|
-
# List reviews for this branch
|
|
13
|
-
cm find review "where target='br:${branch}'" --format="{id} {status} {title}"
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Agent preference: use runtime `plastic_*` methods first for status/branch lookup; keep shell commands as manual fallback.
|
|
17
|
-
|
|
18
|
-
Reviewer assignment is required in the workflow. If your server uses different reviewer flags, run `cm codereview --help` and use the server-specific reviewer option right after review creation.
|
|
19
|
-
|
|
20
|
-
Tool-first flow (preferred):
|
|
21
|
-
|
|
22
|
-
```text
|
|
23
|
-
plastic_status()
|
|
24
|
-
# Then use cm codereview ... as fallback for review create/assign
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## CLI Not Available
|
|
28
|
-
|
|
29
|
-
- Document branch and changeset in plan/issue tracker.
|
|
30
|
-
- Or create review via Plastic GUI and assign a reviewer before sharing the review link.
|
|
1
|
+
# Code Review Creation
|
|
2
|
+
|
|
3
|
+
## CLI (Preferred)
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
branch=$(cm status | head -1 | cut -d@ -f1 | xargs)
|
|
7
|
+
review_id=$(cm codereview "br:${branch}" "Feature: Brief description" --format="{id}")
|
|
8
|
+
|
|
9
|
+
# Assign reviewer immediately after creating the review
|
|
10
|
+
cm codereview -e "$review_id" --reviewer="reviewer-name"
|
|
11
|
+
|
|
12
|
+
# List reviews for this branch
|
|
13
|
+
cm find review "where target='br:${branch}'" --format="{id} {status} {title}"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Agent preference: use runtime `plastic_*` methods first for status/branch lookup; keep shell commands as manual fallback.
|
|
17
|
+
|
|
18
|
+
Reviewer assignment is required in the workflow. If your server uses different reviewer flags, run `cm codereview --help` and use the server-specific reviewer option right after review creation.
|
|
19
|
+
|
|
20
|
+
Tool-first flow (preferred):
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
plastic_status()
|
|
24
|
+
# Then use cm codereview ... as fallback for review create/assign
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## CLI Not Available
|
|
28
|
+
|
|
29
|
+
- Document branch and changeset in plan/issue tracker.
|
|
30
|
+
- Or create review via Plastic GUI and assign a reviewer before sharing the review link.
|
|
@@ -37,7 +37,8 @@ plastic_patch(source="<branch-spec>", clean=true, integration=true, output="<pat
|
|
|
37
37
|
- Inspect specific file content changes with the common workspace-base path first:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
|
-
plastic_diffFile(path="<workspace-path>")
|
|
40
|
+
plastic_diffFile(path="<workspace-path>") # one workspace file
|
|
41
|
+
plastic_workspaceDiff() # bounded pending review
|
|
41
42
|
plastic_diffFile(path="<workspace-path>", revision="cs:<number>")
|
|
42
43
|
plastic_diffRevisions(leftRevision="<left-file-qualified-revspec>", rightRevision="<right-file-qualified-revspec>")
|
|
43
44
|
```
|
|
@@ -18,7 +18,8 @@ Note: command examples target current `cm` 11.x CLI syntax; legacy aliases may d
|
|
|
18
18
|
| Shelve | `plastic_shelvesetCreate(comment="description")` | `cm shelveset create -c="description"` | Save work temporarily |
|
|
19
19
|
| Patch for review | `plastic_patch(source="<branch-spec>", clean=true, integration=true, output="<patch-file>")` | `cm patch <branch-spec> --clean --integration --output=<patch-file>` | Focused branch-review patch; inspect before sharing |
|
|
20
20
|
| Changed-file listing | `plastic_status(machineReadable=true)` | `cm status --all` | Lists changed, added, moved, deleted, and private items without GUI diff |
|
|
21
|
-
| Diff (workspace
|
|
21
|
+
| Diff (one workspace file) | `plastic_diffFile(path="<workspace-path>")` | None recommended | Uses status base materialization; added/private files use an empty base |
|
|
22
|
+
| Diff (pending review) | `plastic_workspaceDiff()` | None recommended | Runs status once; bounded per-file outcomes; private files excluded unless selected/opted in |
|
|
22
23
|
| Diff (workspace vs supported revision) | `plastic_diffFile(path="<workspace-path>", revision="cs:<number>")` | None recommended | Also accepts a branch, label, file-qualified, or global revision spec |
|
|
23
24
|
| Diff (revision vs revision) | `plastic_diffRevisions(leftRevision="<left-revspec>", rightRevision="<right-revspec>")` | None recommended | Requires two file-qualified revisions; avoids GUI and package temp-file recipes |
|
|
24
25
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
# Resources
|
|
2
|
-
|
|
3
|
-
- CLI Reference: https://docs.unity.com/en-us/unity-version-control/uvcs-cli
|
|
4
|
-
- Branch Management: https://docs.unity.com/en-us/unity-version-control/branches
|
|
5
|
-
- Code Reviews: https://docs.unity.com/en-us/unity-version-control/code-reviews
|
|
6
|
-
|
|
7
|
-
Minimum version: PlasticSCM 11.0+
|
|
1
|
+
# Resources
|
|
2
|
+
|
|
3
|
+
- CLI Reference: https://docs.unity.com/en-us/unity-version-control/uvcs-cli
|
|
4
|
+
- Branch Management: https://docs.unity.com/en-us/unity-version-control/branches
|
|
5
|
+
- Code Reviews: https://docs.unity.com/en-us/unity-version-control/code-reviews
|
|
6
|
+
|
|
7
|
+
Minimum version: PlasticSCM 11.0+
|
|
@@ -25,12 +25,13 @@ Prefer tool-first:
|
|
|
25
25
|
|
|
26
26
|
```text
|
|
27
27
|
plastic_status(machineReadable=true) # “what changed?” / changed-path listing
|
|
28
|
-
plastic_diffFile(path="<workspace-path>") #
|
|
29
|
-
|
|
28
|
+
plastic_diffFile(path="<workspace-path>") # one workspace file
|
|
29
|
+
plastic_workspaceDiff() # bounded pending review; private files excluded by default
|
|
30
|
+
plastic_diffFile(path="<workspace-path>", revision="cs:<number>") # explicit historical comparison
|
|
30
31
|
plastic_diffRevisions(leftRevision="<left-file-qualified-revspec>", rightRevision="<right-file-qualified-revspec>")
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
The diff tools materialize historical bytes and remove their temporary files internally.
|
|
34
|
+
The diff tools materialize historical bytes and remove their temporary files internally. Added and explicitly selected private/new files use an empty base; changed, moved, and deleted records use their status revision ID when available. A `--nodata` item cannot supply historical/base bytes: refresh the workspace or use two known file-qualified revisions. `plastic_workspaceDiff` returns per-file unavailable outcomes instead of aborting the batch. All tools use bounded GNU/POSIX text diff, treat valid Unity YAML as text, and report genuine binary content explicitly. Do not construct `cm cat` temporary-file recipes for ordinary review.
|
|
34
35
|
|
|
35
36
|
## Metadata Listing (No GUI Diff)
|
|
36
37
|
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# Shelving
|
|
2
|
-
|
|
3
|
-
## Save Work
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
cm shelveset create -c="Working on feature X"
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
## List Shelves
|
|
10
|
-
|
|
11
|
-
```bash
|
|
12
|
-
cm find shelve "where owner='me'" --format="{shelveid} {date} {comment}"
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## Restore Work
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
cm shelveset apply sh:5
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Delete Shelve
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
cm shelveset delete sh:5
|
|
25
|
-
```
|
|
1
|
+
# Shelving
|
|
2
|
+
|
|
3
|
+
## Save Work
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
cm shelveset create -c="Working on feature X"
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## List Shelves
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
cm find shelve "where owner='me'" --format="{shelveid} {date} {comment}"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Restore Work
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cm shelveset apply sh:5
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Delete Shelve
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cm shelveset delete sh:5
|
|
25
|
+
```
|
|
@@ -41,7 +41,8 @@ cm codereview -e <review-id> --reviewer="reviewer-name"
|
|
|
41
41
|
- `cm diff` is blocked in Pi because it can launch GUI windows and hang the CLI.
|
|
42
42
|
- Use text-only alternatives:
|
|
43
43
|
- `plastic_status(machineReadable=true)` for changed paths/statuses.
|
|
44
|
-
- `plastic_diffFile(path="<workspace-path>")` for
|
|
44
|
+
- `plastic_diffFile(path="<workspace-path>")` for one workspace file.
|
|
45
|
+
- `plastic_workspaceDiff()` for bounded pending review (private files are excluded unless selected/opted in).
|
|
45
46
|
- `plastic_diffFile(path="<workspace-path>", revision="cs:<number>")` for an explicit supported revision.
|
|
46
47
|
- `plastic_diffRevisions(leftRevision="<left-file-qualified-revspec>", rightRevision="<right-file-qualified-revspec>")` for two historical files.
|
|
47
48
|
|
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
# Review Workspaces
|
|
2
|
-
|
|
3
|
-
## Strategy
|
|
4
|
-
|
|
5
|
-
- Use a separate review workspace for isolation.
|
|
6
|
-
- Reuse `-review-1` by default (Unity workspaces are large).
|
|
7
|
-
|
|
8
|
-
## Create or Reuse
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
main_workspace=$(basename "$PWD")
|
|
12
|
-
review_workspace_name="${main_workspace}-review-1"
|
|
13
|
-
review_workspace="../${review_workspace_name}"
|
|
14
|
-
|
|
15
|
-
if [ -d "$review_workspace" ]; then
|
|
16
|
-
cd "$review_workspace"
|
|
17
|
-
cm switch --silent --noinput "$review_branch" || {
|
|
18
|
-
cm shelveset create --all -c="Auto-shelve before review switch"
|
|
19
|
-
cm switch --silent --noinput "$review_branch"
|
|
20
|
-
}
|
|
21
|
-
else
|
|
22
|
-
repo_spec=$(cm status --wkconfig | grep "^Repository:" | cut -d: -f2- | xargs)
|
|
23
|
-
cm workspace create "$review_workspace_name" "$review_workspace" "$repo_spec"
|
|
24
|
-
cd "$review_workspace"
|
|
25
|
-
cm switch --silent --noinput "$review_branch"
|
|
26
|
-
fi
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Agent preference: use runtime `plastic_*` methods first for branch/status operations; keep shell commands as manual fallback.
|
|
30
|
-
|
|
31
|
-
## Cleanup
|
|
32
|
-
|
|
33
|
-
Only remove review workspaces when disk space is tight or requested.
|
|
1
|
+
# Review Workspaces
|
|
2
|
+
|
|
3
|
+
## Strategy
|
|
4
|
+
|
|
5
|
+
- Use a separate review workspace for isolation.
|
|
6
|
+
- Reuse `-review-1` by default (Unity workspaces are large).
|
|
7
|
+
|
|
8
|
+
## Create or Reuse
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
main_workspace=$(basename "$PWD")
|
|
12
|
+
review_workspace_name="${main_workspace}-review-1"
|
|
13
|
+
review_workspace="../${review_workspace_name}"
|
|
14
|
+
|
|
15
|
+
if [ -d "$review_workspace" ]; then
|
|
16
|
+
cd "$review_workspace"
|
|
17
|
+
cm switch --silent --noinput "$review_branch" || {
|
|
18
|
+
cm shelveset create --all -c="Auto-shelve before review switch"
|
|
19
|
+
cm switch --silent --noinput "$review_branch"
|
|
20
|
+
}
|
|
21
|
+
else
|
|
22
|
+
repo_spec=$(cm status --wkconfig | grep "^Repository:" | cut -d: -f2- | xargs)
|
|
23
|
+
cm workspace create "$review_workspace_name" "$review_workspace" "$repo_spec"
|
|
24
|
+
cd "$review_workspace"
|
|
25
|
+
cm switch --silent --noinput "$review_branch"
|
|
26
|
+
fi
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Agent preference: use runtime `plastic_*` methods first for branch/status operations; keep shell commands as manual fallback.
|
|
30
|
+
|
|
31
|
+
## Cleanup
|
|
32
|
+
|
|
33
|
+
Only remove review workspaces when disk space is tight or requested.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow local adapter for the published capability-registry global protocol.
|
|
3
|
+
*
|
|
4
|
+
* Pi loads independently installed packages from separate module roots. Plastic
|
|
5
|
+
* therefore registers its optional file-discovery filter through the stable
|
|
6
|
+
* global capability key instead of importing a sibling package contract.
|
|
7
|
+
*/
|
|
8
|
+
const ROOT_PROTOCOL = "@aefree/pi-capability-registry/root";
|
|
9
|
+
const ROOT_PROTOCOL_VERSION = 1;
|
|
10
|
+
export const PLASTIC_FILE_DISCOVERY_FILTER_REGISTRY_KEY_V1 = "@aefree/pi-file-discovery/filters/v1" as const;
|
|
11
|
+
|
|
12
|
+
type FilterOwner = Readonly<{ packageName: string; packageRoot: string }>;
|
|
13
|
+
type FilterRecord = Readonly<{ contractVersion: 1; id: string; kind: "file-discovery-filter"; owner: FilterOwner; evaluate: (...args: never[]) => unknown }>;
|
|
14
|
+
export type PlasticFileDiscoveryRegistrationToken = Readonly<{ registryKey: string; contractVersion: 1; scope: object; ownerKey: string; id: string; nonce: number }>;
|
|
15
|
+
type StoredRecord = Readonly<{ nonce: number; record: FilterRecord }>;
|
|
16
|
+
type ScopedState = { sequence: number; records: Map<string, StoredRecord> };
|
|
17
|
+
type VersionState = { version: 1; scopes: WeakMap<object, ScopedState> };
|
|
18
|
+
type RegistryRoot = { protocol: string; protocolVersion: number; registryKey: string; versions: Map<unknown, unknown> };
|
|
19
|
+
|
|
20
|
+
export type PlasticFileDiscoveryFilterRegistryV1 = Readonly<{
|
|
21
|
+
register(scope: object, filter: FilterRecord): PlasticFileDiscoveryRegistrationToken;
|
|
22
|
+
unregister(token: PlasticFileDiscoveryRegistrationToken | undefined): boolean;
|
|
23
|
+
}>;
|
|
24
|
+
|
|
25
|
+
/** Create Plastic's view of the shared file-discovery v1 capability registry. */
|
|
26
|
+
export function createPlasticFileDiscoveryFilterRegistryV1(): PlasticFileDiscoveryFilterRegistryV1 {
|
|
27
|
+
const registryKey = PLASTIC_FILE_DISCOVERY_FILTER_REGISTRY_KEY_V1;
|
|
28
|
+
const globalRecord = globalThis as typeof globalThis & Record<symbol, unknown>;
|
|
29
|
+
const symbol = Symbol.for(registryKey);
|
|
30
|
+
let candidate = globalRecord[symbol];
|
|
31
|
+
if (candidate === undefined) {
|
|
32
|
+
candidate = { protocol: ROOT_PROTOCOL, protocolVersion: ROOT_PROTOCOL_VERSION, registryKey, versions: new Map() } satisfies RegistryRoot;
|
|
33
|
+
globalRecord[symbol] = candidate;
|
|
34
|
+
}
|
|
35
|
+
const root = assertRoot(candidate, registryKey);
|
|
36
|
+
let versionCandidate = root.versions.get(1);
|
|
37
|
+
if (versionCandidate === undefined) {
|
|
38
|
+
versionCandidate = { version: 1, scopes: new WeakMap<object, ScopedState>() } satisfies VersionState;
|
|
39
|
+
root.versions.set(1, versionCandidate);
|
|
40
|
+
}
|
|
41
|
+
const version = assertVersionState(versionCandidate, registryKey);
|
|
42
|
+
|
|
43
|
+
return Object.freeze({
|
|
44
|
+
register(scope, filter) {
|
|
45
|
+
assertScope(scope);
|
|
46
|
+
assertFilter(filter);
|
|
47
|
+
let scoped = version.scopes.get(scope);
|
|
48
|
+
if (scoped === undefined) {
|
|
49
|
+
scoped = { sequence: 0, records: new Map() };
|
|
50
|
+
version.scopes.set(scope, scoped);
|
|
51
|
+
} else {
|
|
52
|
+
assertScopedState(scoped, registryKey);
|
|
53
|
+
}
|
|
54
|
+
const ownerKey = `${filter.owner.packageName}\0${filter.owner.packageRoot}\0${filter.id}`;
|
|
55
|
+
for (const current of scoped.records.values()) {
|
|
56
|
+
if (current.record.id === filter.id && ownerKeyFor(current.record) !== ownerKey) {
|
|
57
|
+
throw new TypeError(`Provider id '${filter.id}' conflicts in file-discovery registry '${registryKey}'.`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const nonce = scoped.sequence + 1;
|
|
61
|
+
scoped.records.set(ownerKey, Object.freeze({ nonce, record: filter }));
|
|
62
|
+
scoped.sequence = nonce;
|
|
63
|
+
return Object.freeze({ registryKey, contractVersion: 1, scope, ownerKey, id: filter.id, nonce });
|
|
64
|
+
},
|
|
65
|
+
unregister(token) {
|
|
66
|
+
if (!token || token.registryKey !== registryKey || token.contractVersion !== 1) return false;
|
|
67
|
+
const scoped = version.scopes.get(token.scope);
|
|
68
|
+
if (scoped === undefined) return false;
|
|
69
|
+
assertScopedState(scoped, registryKey);
|
|
70
|
+
const current = scoped.records.get(token.ownerKey);
|
|
71
|
+
if (current === undefined || current.nonce !== token.nonce || current.record.id !== token.id) return false;
|
|
72
|
+
scoped.records.delete(token.ownerKey);
|
|
73
|
+
return true;
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function assertRoot(value: unknown, registryKey: string): RegistryRoot {
|
|
79
|
+
if (value === null || typeof value !== "object") throw incompatible(registryKey);
|
|
80
|
+
const root = value as Partial<RegistryRoot>;
|
|
81
|
+
if (root.protocol !== ROOT_PROTOCOL || root.protocolVersion !== ROOT_PROTOCOL_VERSION || root.registryKey !== registryKey || !(root.versions instanceof Map)) throw incompatible(registryKey);
|
|
82
|
+
return root as RegistryRoot;
|
|
83
|
+
}
|
|
84
|
+
function assertVersionState(value: unknown, registryKey: string): VersionState {
|
|
85
|
+
if (value === null || typeof value !== "object") throw incompatible(registryKey);
|
|
86
|
+
const state = value as Partial<VersionState>;
|
|
87
|
+
if (state.version !== 1 || !(state.scopes instanceof WeakMap)) throw incompatible(registryKey);
|
|
88
|
+
return state as VersionState;
|
|
89
|
+
}
|
|
90
|
+
function assertScopedState(value: unknown, registryKey: string): asserts value is ScopedState {
|
|
91
|
+
if (value === null || typeof value !== "object") throw incompatible(registryKey);
|
|
92
|
+
const state = value as Partial<ScopedState>;
|
|
93
|
+
if (!Number.isSafeInteger(state.sequence) || (state.sequence ?? -1) < 0 || !(state.records instanceof Map)) throw incompatible(registryKey);
|
|
94
|
+
}
|
|
95
|
+
function assertScope(scope: unknown): asserts scope is object {
|
|
96
|
+
if ((typeof scope !== "object" && typeof scope !== "function") || scope === null) throw new TypeError("File-discovery registration requires a session scope object.");
|
|
97
|
+
}
|
|
98
|
+
function assertFilter(value: unknown): asserts value is FilterRecord {
|
|
99
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Plastic attempted an invalid file-discovery filter registration.");
|
|
100
|
+
const filter = value as Partial<FilterRecord>;
|
|
101
|
+
if (filter.contractVersion !== 1 || filter.kind !== "file-discovery-filter" || typeof filter.id !== "string" || !filter.id || typeof filter.evaluate !== "function" || filter.owner === null || typeof filter.owner !== "object" || typeof filter.owner.packageName !== "string" || !filter.owner.packageName || typeof filter.owner.packageRoot !== "string" || !filter.owner.packageRoot) throw new TypeError("Plastic attempted an invalid file-discovery filter registration.");
|
|
102
|
+
}
|
|
103
|
+
function ownerKeyFor(record: FilterRecord): string { return `${record.owner.packageName}\0${record.owner.packageRoot}\0${record.id}`; }
|
|
104
|
+
function incompatible(registryKey: string): TypeError { return new TypeError(`@aefree/pi-file-discovery advertises an incompatible capability-registry contract for '${registryKey}'.`); }
|
|
@@ -2,23 +2,26 @@ import { access, readFile, realpath, stat } from "node:fs/promises";
|
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
// Structural copies of the narrow public v1 contract keep the
|
|
5
|
+
// Structural copies of the narrow public v1 contract keep the filter module loadable
|
|
6
6
|
// when the optional registry package is absent. The installed registry validates the
|
|
7
|
-
// record when registration is available
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
type
|
|
11
|
-
type
|
|
7
|
+
// record when registration is available; unavailable or malformed filter data
|
|
8
|
+
// degrades to generic discovery rather than interrupting file discovery.
|
|
9
|
+
type FileDiscoveryOwnerV1 = Readonly<{ packageName: string; packageVersion: string; packageRoot: string; registeredBy: string }>;
|
|
10
|
+
type FileDiscoveryExecutionContextV1 = Readonly<{ cwd: string; requestId?: string; signal: AbortSignal }>;
|
|
11
|
+
type FileDiscoveryFilterRequestV1 = Readonly<{ workspaceRoot: string; roots: readonly string[]; includeHidden: boolean; signal: AbortSignal }>;
|
|
12
|
+
type FileDiscoveryAppliedRootV1 = Readonly<{ root: string; filterDecision: "applied" | "bypassed"; decisionCode: string; filterBoundary?: string; excludeGlobs?: readonly string[]; ignoreFiles?: readonly string[]; disclosures: readonly string[] }>;
|
|
13
|
+
type FileDiscoveryFilterResultV1 =
|
|
12
14
|
| Readonly<{ outcome: "not_applicable" }>
|
|
13
|
-
| Readonly<{ outcome: "applied"; roots: readonly
|
|
15
|
+
| Readonly<{ outcome: "applied"; roots: readonly FileDiscoveryAppliedRootV1[] }>
|
|
14
16
|
| Readonly<{ outcome: "unavailable" | "error"; code: string; retryable: boolean }>;
|
|
15
|
-
type
|
|
17
|
+
type FileDiscoveryFilterV1 = Readonly<{ contractVersion: 1; id: string; kind: "file-discovery-filter"; owner: FileDiscoveryOwnerV1; evaluate(context: FileDiscoveryExecutionContextV1, request: FileDiscoveryFilterRequestV1): Promise<FileDiscoveryFilterResultV1> }>;
|
|
16
18
|
import { discoverPlasticWorkspace } from "./plastic-workspace.ts";
|
|
17
19
|
|
|
18
|
-
export const
|
|
20
|
+
export const PLASTIC_FILE_DISCOVERY_FILTER_ID = "plastic.ignore-files" as const;
|
|
21
|
+
export const PLASTIC_IGNORE_FILES_APPLIED_CODE = "plastic_ignore_files_applied" as const;
|
|
19
22
|
|
|
20
|
-
/** Resolve this physical package copy's identity for
|
|
21
|
-
export async function loadPlasticOwnerV1(moduleUrl: string = import.meta.url): Promise<
|
|
23
|
+
/** Resolve this physical package copy's identity for file-discovery registration. */
|
|
24
|
+
export async function loadPlasticOwnerV1(moduleUrl: string = import.meta.url): Promise<FileDiscoveryOwnerV1> {
|
|
22
25
|
const registeredBy = fileURLToPath(moduleUrl);
|
|
23
26
|
const packageRoot = await realpath(fileURLToPath(new URL("../", moduleUrl)));
|
|
24
27
|
const manifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")) as { name?: unknown; version?: unknown };
|
|
@@ -26,13 +29,13 @@ export async function loadPlasticOwnerV1(moduleUrl: string = import.meta.url): P
|
|
|
26
29
|
return Object.freeze({ packageName: "@aefree/pi-plastic", packageVersion: manifest.version, packageRoot, registeredBy });
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
/**
|
|
30
|
-
export function
|
|
32
|
+
/** Advisory filter that supplies bounded Plastic ignore/cloaking data to file discovery. */
|
|
33
|
+
export function createPlasticFileDiscoveryFilterV1(owner: FileDiscoveryOwnerV1): FileDiscoveryFilterV1 {
|
|
31
34
|
return Object.freeze({
|
|
32
|
-
contractVersion: 1, id:
|
|
33
|
-
async evaluate(context:
|
|
35
|
+
contractVersion: 1, id: PLASTIC_FILE_DISCOVERY_FILTER_ID, kind: "file-discovery-filter", owner,
|
|
36
|
+
async evaluate(context: FileDiscoveryExecutionContextV1, request: FileDiscoveryFilterRequestV1): Promise<FileDiscoveryFilterResultV1> {
|
|
34
37
|
throwIfAborted(context.signal);
|
|
35
|
-
const roots
|
|
38
|
+
const roots: FileDiscoveryAppliedRootV1[] = [];
|
|
36
39
|
for (const root of request.roots) {
|
|
37
40
|
throwIfAborted(context.signal);
|
|
38
41
|
const workspace = await discoverPlasticWorkspace(root);
|
|
@@ -42,7 +45,8 @@ export function createPlasticRepositorySearchPolicyV1(owner: RepoSearchOwnerV1):
|
|
|
42
45
|
const canonicalWorkspace = await realpath(workspace.value.root).catch(() => undefined);
|
|
43
46
|
if (!canonicalRoot || !canonicalWorkspace || !isWithin(canonicalWorkspace, canonicalRoot)) return { outcome: "error", code: "plastic_workspace_boundary_invalid", retryable: false };
|
|
44
47
|
const ignoreFiles = await discoverIgnoreFiles(canonicalWorkspace, canonicalRoot, context.signal);
|
|
45
|
-
|
|
48
|
+
if (ignoreFiles.length === 0) continue;
|
|
49
|
+
roots.push({ root: canonicalRoot, filterDecision: "applied", decisionCode: PLASTIC_IGNORE_FILES_APPLIED_CODE, filterBoundary: canonicalWorkspace, ignoreFiles, disclosures: [`Plastic ignore filter applied (${ignoreFiles.length} readable file${ignoreFiles.length === 1 ? "" : "s"}).`] });
|
|
46
50
|
}
|
|
47
51
|
return roots.length ? { outcome: "applied", roots } : { outcome: "not_applicable" };
|
|
48
52
|
},
|