@aefree/pi-plastic 0.4.0 → 0.5.1
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 +24 -0
- package/README.md +9 -7
- package/extensions/bash-cm-diff-guard.ts +2 -1
- package/extensions/file-discovery-filter.ts +67 -0
- package/index.ts +18 -2
- package/package.json +7 -8
- package/skills/_shared/references/conventional-commits.md +198 -198
- package/skills/using-plastic/SKILL.md +3 -3
- package/skills/using-plastic/references/changeset-operations.md +5 -5
- package/skills/using-plastic/references/code-review-creation.md +30 -30
- package/skills/using-plastic/references/integration.md +5 -3
- 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 +9 -7
- package/skills/using-plastic/references/shelving.md +25 -25
- package/skills/using-plastic/references/troubleshooting.md +5 -3
- 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 +579 -170
- package/src/plastic-tool-loading.ts +3 -3
- 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
|
@@ -41,7 +41,9 @@ 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>")`
|
|
44
|
+
- `plastic_diffFile(path="<workspace-path>", maxChars=4000)` when one intentional file comparison is needed.
|
|
45
|
+
- `plastic_workspaceDiff(paths=["<workspace-path>"], maxChars=3000)` for an explicitly scoped pending review, or `allPending=true` for an intentional small whole-workspace review.
|
|
46
|
+
- Use `plastic_status` instead when only pending paths are needed; do not diff routinely.
|
|
45
47
|
- `plastic_diffFile(path="<workspace-path>", revision="cs:<number>")` for an explicit supported revision.
|
|
46
48
|
- `plastic_diffRevisions(leftRevision="<left-file-qualified-revspec>", rightRevision="<right-file-qualified-revspec>")` for two historical files.
|
|
47
49
|
|
|
@@ -95,12 +97,12 @@ plastic_resolveDeleteChangeConflict(paths=["<workspace-path>"], keepOnDisk=true)
|
|
|
95
97
|
|
|
96
98
|
- Do not use `cm diff --summary`; this form is invalid.
|
|
97
99
|
- Also avoid all other `cm diff` forms in Pi.
|
|
98
|
-
- Use the
|
|
100
|
+
- Use `plastic_status` unless change-boundary evidence is actually needed; then use the focused text-only alternatives above.
|
|
99
101
|
|
|
100
102
|
## Deleted Files Missing From Checkin
|
|
101
103
|
|
|
102
104
|
- Run `cm status --all` and confirm deleted items are listed before checkin.
|
|
103
|
-
-
|
|
105
|
+
- For a path-scoped checkin containing moved/deleted items, use `plastic_checkin(preflight=true, paths=[...])`; the tool can preview rewriting unstable file scopes to parent directories. Do not add preflight to unrelated routine checkins.
|
|
104
106
|
- If a path-scoped checkin is rejected as "not changed", rerun with `applyChanged=true` and a parent-directory scope.
|
|
105
107
|
- Re-run `cm status --all` just before checkin to validate scope.
|
|
106
108
|
|
|
@@ -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
|
},
|