@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +8 -7
  3. package/extensions/bash-cm-diff-guard.ts +2 -1
  4. package/extensions/file-discovery-filter.ts +67 -0
  5. package/index.ts +12 -0
  6. package/package.json +7 -8
  7. package/skills/_shared/references/conventional-commits.md +198 -198
  8. package/skills/using-plastic/SKILL.md +2 -2
  9. package/skills/using-plastic/references/changeset-operations.md +2 -1
  10. package/skills/using-plastic/references/code-review-creation.md +30 -30
  11. package/skills/using-plastic/references/integration.md +2 -1
  12. package/skills/using-plastic/references/quick-reference.md +2 -1
  13. package/skills/using-plastic/references/resources.md +7 -7
  14. package/skills/using-plastic/references/reviewing-changes.md +4 -3
  15. package/skills/using-plastic/references/shelving.md +25 -25
  16. package/skills/using-plastic/references/troubleshooting.md +2 -1
  17. package/skills/using-plastic/references/workspaces.md +33 -33
  18. package/src/file-discovery-filter-rendezvous.ts +104 -0
  19. package/src/{repository-search-provider.ts → file-discovery-filter.ts} +21 -17
  20. package/src/plastic-core.ts +438 -152
  21. package/src/plastic-tool-loading.ts +2 -1
  22. package/evals/tool-loading/README.md +0 -38
  23. package/evals/tool-loading/RESULTS.md +0 -57
  24. package/evals/tool-loading/baseline.json +0 -34
  25. package/evals/tool-loading/cases.json +0 -70
  26. package/evals/tool-loading/config.json +0 -19
  27. package/evals/tool-loading/mutation-guard.ts +0 -30
  28. package/evals/tool-loading/provider-capture.ts +0 -22
  29. package/evals/tool-loading/results/.gitignore +0 -3
  30. package/evals/tool-loading/run-eval.ts +0 -372
  31. package/extensions/repository-search-provider.ts +0 -66
  32. package/tests/bash-cm-diff-guard-validation.ts +0 -55
  33. package/tests/fixtures/plastic.selector +0 -3
  34. package/tests/fixtures/status-compact.txt +0 -1
  35. package/tests/fixtures/status-normal.txt +0 -1
  36. package/tests/packed-copy-owner-conflict.test.ts +0 -28
  37. package/tests/pi-tool-harness.ts +0 -116
  38. package/tests/plastic-branch-create-live.test.ts +0 -78
  39. package/tests/plastic-branch-query.test.ts +0 -26
  40. package/tests/plastic-branch-status.test.ts +0 -241
  41. package/tests/plastic-checkin-path-resolution.ts +0 -219
  42. package/tests/plastic-constrained-sampling.test.ts +0 -77
  43. package/tests/plastic-diff-workflow.test.ts +0 -86
  44. package/tests/plastic-dynamic-tool-loading.test.ts +0 -224
  45. package/tests/plastic-extension-registration.test.ts +0 -34
  46. package/tests/plastic-live-smoke.test.ts +0 -37
  47. package/tests/plastic-mutation-execution.test.ts +0 -61
  48. package/tests/plastic-patch-command.test.ts +0 -118
  49. package/tests/plastic-process-lifecycle.test.ts +0 -148
  50. package/tests/plastic-tool-rendering.test.ts +0 -16
  51. package/tests/plastic-tool-validation.ts +0 -153
  52. package/tests/plastic-workspace.test.ts +0 -62
  53. package/tests/repository-search-provider-registration.test.ts +0 -37
  54. 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. For the common workspace change, use `plastic_diffFile(path="<workspace-path>")` with no revision; it materializes the Plastic base internally and returns a bounded portable text diff. Use a revision only when needed: a changeset number/`cs:<number>`, `br:/<branch>`, `lb:<label>`, file-qualified spec, or global `revid:`/`rev:` spec. `base`, `head`, and `cs:head` are intentionally rejected rather than guessed. Use `plastic_diffRevisions` only for two explicit file-qualified revisions. Binary content is reported rather than decoded as text.
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
- Use `plastic_status()` for “what changed?” and changed-path/status listing; do not use `cm diff` formatting for that purpose. 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 portable `diff` on PATH; 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.
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>") # common workspace-base diff
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 vs Plastic base) | `plastic_diffFile(path="<workspace-path>")` | None recommended | Bounded portable text diff; no guessed revision needed |
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>") # common workspace vs Plastic-base comparison
29
- plastic_diffFile(path="<workspace-path>", revision="cs:<number>") # explicit advanced comparison
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. They use a bounded portable 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
+ 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 the common workspace-base comparison.
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 policy module loadable
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
- type RepoSearchOwnerV1 = Readonly<{ packageName: string; packageVersion: string; packageRoot: string; registeredBy: string }>;
9
- type RepoSearchExecutionContextV1 = Readonly<{ cwd: string; requestId?: string; signal: AbortSignal }>;
10
- type RepositoryPolicyRequestV1 = Readonly<{ workspaceRoot: string; roots: readonly string[]; includeHidden: boolean; options?: Readonly<Record<string, unknown>>; signal: AbortSignal }>;
11
- type RepositoryPolicyResultV1 =
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 Readonly<{ root: string; policyOwnedRoot?: string; excludeGlobs?: readonly string[]; ignoreFiles?: readonly string[]; disclosures: readonly string[] }>[] }>
15
+ | Readonly<{ outcome: "applied"; roots: readonly FileDiscoveryAppliedRootV1[] }>
14
16
  | Readonly<{ outcome: "unavailable" | "error"; code: string; retryable: boolean }>;
15
- type RepositoryPolicyV1 = Readonly<{ contractVersion: 1; id: string; kind: "repository-search-policy"; owner: RepoSearchOwnerV1; evaluate(context: RepoSearchExecutionContextV1, request: RepositoryPolicyRequestV1): Promise<RepositoryPolicyResultV1> }>;
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 PLASTIC_REPOSITORY_SEARCH_POLICY_ID = "plastic.ignore-files" as 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 repository-search registration. */
21
- export async function loadPlasticOwnerV1(moduleUrl: string = import.meta.url): Promise<RepoSearchOwnerV1> {
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
- /** Independent executable policy for bounded Plastic ignore/cloaking discovery. */
30
- export function createPlasticRepositorySearchPolicyV1(owner: RepoSearchOwnerV1): RepositoryPolicyV1 {
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: PLASTIC_REPOSITORY_SEARCH_POLICY_ID, kind: "repository-search-policy", owner,
33
- async evaluate(context: RepoSearchExecutionContextV1, request: RepositoryPolicyRequestV1): Promise<RepositoryPolicyResultV1> {
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 = [] as { root: string; policyOwnedRoot: string; ignoreFiles: string[]; disclosures: string[] }[];
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
- roots.push({ root: canonicalRoot, policyOwnedRoot: canonicalWorkspace, ignoreFiles, disclosures: [ignoreFiles.length ? `Plastic ignore policy applied (${ignoreFiles.length} file${ignoreFiles.length === 1 ? "" : "s"}).` : "Plastic workspace detected; no readable ignore.conf or cloaked.conf found."] });
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
  },