pi-harness-runtime 1.1.413 → 1.1.415
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/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/index.d.ts +1 -0
- package/packages/skills/dist/index.d.ts.map +1 -1
- package/packages/skills/dist/index.js +1 -0
- package/packages/skills/dist/index.js.map +1 -1
- package/packages/skills/dist/loader.d.ts +19 -7
- package/packages/skills/dist/loader.d.ts.map +1 -1
- package/packages/skills/dist/loader.js +39 -22
- package/packages/skills/dist/loader.js.map +1 -1
- package/packages/skills/dist/sanitize.d.ts +27 -0
- package/packages/skills/dist/sanitize.d.ts.map +1 -0
- package/packages/skills/dist/sanitize.js +75 -0
- package/packages/skills/dist/sanitize.js.map +1 -0
- package/packages/skills/dist/vector-schema.d.ts +93 -4
- package/packages/skills/dist/vector-schema.d.ts.map +1 -1
- package/packages/skills/dist/vector-schema.js +71 -4
- package/packages/skills/dist/vector-schema.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/index.ts +1 -0
- package/packages/skills/src/loader.ts +53 -27
- package/packages/skills/src/sanitize.ts +86 -0
- package/packages/skills/src/vector-schema.ts +127 -4
- package/packages/skills/test/remote-skills.test.ts +41 -0
- package/packages/skills/test/sanitize.test.ts +88 -0
- package/packages/skills/test/vector-schema.test.ts +167 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/sync-to-qdrant.ts +29 -69
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* of the vector store.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { sanitizeText, skillPointName } from "./sanitize.js";
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* The collection the runtime indexes into and searches.
|
|
21
23
|
*
|
|
@@ -51,12 +53,133 @@ export interface SkillVectorSchema {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
+
* The payload of a skill point.
|
|
57
|
+
*
|
|
58
|
+
* Both writers of the collection - the runtime registry and
|
|
59
|
+
* `scripts/sync-to-qdrant.ts` - store the SAME payload under the SAME
|
|
60
|
+
* point id. A point is addressed by its id, so a writer that used a
|
|
61
|
+
* different id for the same skill would leave two points behind, and a
|
|
62
|
+
* writer whose id happened to collide with a foreign one would overwrite
|
|
63
|
+
* an unrelated skill.
|
|
64
|
+
*/
|
|
65
|
+
export type SkillPointPayload = {
|
|
66
|
+
/** The name of the skill. The point id is derived from it. */
|
|
67
|
+
name: string;
|
|
68
|
+
/** What the skill does. */
|
|
69
|
+
description: string;
|
|
70
|
+
/** The body of the skill. */
|
|
71
|
+
body: string;
|
|
72
|
+
/** The text the vector was built from. */
|
|
73
|
+
text: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The body of a payload is truncated.
|
|
78
|
+
*
|
|
79
|
+
* A payload is an index, not a file store - the complete skill comes from
|
|
80
|
+
* the skill service.
|
|
81
|
+
*/
|
|
82
|
+
export const SKILL_PAYLOAD_BODY_LIMIT = 2000;
|
|
83
|
+
|
|
84
|
+
/** The text of a payload is truncated as well. */
|
|
85
|
+
export const SKILL_PAYLOAD_TEXT_LIMIT = 1500;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A stable hash of a string.
|
|
89
|
+
*
|
|
90
|
+
* The same string always produces the same number, in every process and
|
|
91
|
+
* on every machine - a point id has to survive a restart.
|
|
92
|
+
*
|
|
93
|
+
* Two uses:
|
|
94
|
+
* - the id of a point is the hash of the name its payload carries, so the
|
|
95
|
+
* point id and the payload name always belong together. Two writers
|
|
96
|
+
* that store the same name address the same point.
|
|
97
|
+
* - the hash of the text of a skill says whether its content changed.
|
|
98
|
+
*/
|
|
99
|
+
export function stableHash(value: string): number {
|
|
100
|
+
let hash = 0;
|
|
101
|
+
for (let i = 0; i < value.length; i++) {
|
|
102
|
+
hash = (Math.imul(31, hash) + value.charCodeAt(i)) | 0;
|
|
103
|
+
}
|
|
104
|
+
return Math.abs(hash);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The text a skill vector is built from.
|
|
56
109
|
*
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
110
|
+
* The title is added when it differs from the name: a skill whose
|
|
111
|
+
* directory is named differently from its title carries both.
|
|
59
112
|
*/
|
|
113
|
+
export function skillPointText(input: {
|
|
114
|
+
/** The name the payload carries - the identity of the skill. */
|
|
115
|
+
name: string;
|
|
116
|
+
/** The title of the skill, when it differs from the name. */
|
|
117
|
+
title?: string;
|
|
118
|
+
/** What the skill does. */
|
|
119
|
+
description: string;
|
|
120
|
+
/** The body of the skill. */
|
|
121
|
+
body: string;
|
|
122
|
+
}): string {
|
|
123
|
+
const parts = [input.name];
|
|
124
|
+
if (input.title && input.title !== input.name) parts.push(input.title);
|
|
125
|
+
parts.push(input.description);
|
|
126
|
+
parts.push(input.body.slice(0, SKILL_PAYLOAD_TEXT_LIMIT));
|
|
127
|
+
return parts.join(" ");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* A point of the skill collection.
|
|
132
|
+
*
|
|
133
|
+
* The id is the hash of the name the payload carries, so the point id and
|
|
134
|
+
* the payload name always belong together: two writers that store the
|
|
135
|
+
* same name address the same point.
|
|
136
|
+
*/
|
|
137
|
+
export interface SkillQdrantPoint {
|
|
138
|
+
id: number;
|
|
139
|
+
payload: SkillPointPayload;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The point of a skill.
|
|
144
|
+
*
|
|
145
|
+
* The runtime registry and `scripts/sync-to-qdrant.ts` both build their
|
|
146
|
+
* points here, so they cannot drift apart. A writer that used its own id
|
|
147
|
+
* would leave a second point for every skill the other one indexes, and
|
|
148
|
+
* an id that happens to collide with a foreign one would overwrite an
|
|
149
|
+
* unrelated skill.
|
|
150
|
+
*/
|
|
151
|
+
export function skillQdrantPoint(payload: SkillPointPayload): SkillQdrantPoint {
|
|
152
|
+
return { id: stableHash(payload.name), payload };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The payload of a skill point.
|
|
157
|
+
*
|
|
158
|
+
* The payload is sanitized: the collection is shared, so the client names
|
|
159
|
+
* of a user must not travel into it. Both writers use this function, so
|
|
160
|
+
* both write the same sanitized payload for the same skill.
|
|
161
|
+
*/
|
|
162
|
+
export function skillPointPayload(input: {
|
|
163
|
+
/** The name the payload carries - the identity of the skill. */
|
|
164
|
+
name: string;
|
|
165
|
+
/** The title of the skill, when it differs from the name. */
|
|
166
|
+
title?: string;
|
|
167
|
+
/** What the skill does. */
|
|
168
|
+
description: string;
|
|
169
|
+
/** The body of the skill. */
|
|
170
|
+
body: string;
|
|
171
|
+
}): SkillPointPayload {
|
|
172
|
+
const name = skillPointName(input.name);
|
|
173
|
+
const title = input.title ? skillPointName(input.title) : undefined;
|
|
174
|
+
const description = sanitizeText(input.description);
|
|
175
|
+
const body = sanitizeText(input.body).slice(0, SKILL_PAYLOAD_BODY_LIMIT);
|
|
176
|
+
return {
|
|
177
|
+
name,
|
|
178
|
+
description,
|
|
179
|
+
body,
|
|
180
|
+
text: skillPointText({ name, title, description, body }),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
60
183
|
export function describeSkillSchemaMismatch(
|
|
61
184
|
actual: { dimensions?: number; distance?: string },
|
|
62
185
|
expected: SkillVectorSchema,
|
|
@@ -169,6 +169,47 @@ describe("a skill that only exists in the vector store", () => {
|
|
|
169
169
|
});
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
+
describe("a hit that carries the shared name of a local skill", () => {
|
|
173
|
+
it("resolves to the local skill", async () => {
|
|
174
|
+
// The collection is shared, so the point carries the SANITIZED name.
|
|
175
|
+
const client = fakeClient([
|
|
176
|
+
{
|
|
177
|
+
id: 1,
|
|
178
|
+
score: 0.9,
|
|
179
|
+
payload: {
|
|
180
|
+
name: "client-a-pe-approval-workflow",
|
|
181
|
+
description: "the workflow",
|
|
182
|
+
body: "the body",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
]);
|
|
186
|
+
const registry = createSkillRegistry({
|
|
187
|
+
qdrantClient: client,
|
|
188
|
+
embedder: fakeEmbedder,
|
|
189
|
+
collection: "pi-harness-skills-test",
|
|
190
|
+
});
|
|
191
|
+
registry.register(localSkill("inpac-pe-approval-workflow"));
|
|
192
|
+
|
|
193
|
+
const results = await registry.findVector("workflow");
|
|
194
|
+
|
|
195
|
+
expect(results).toHaveLength(1);
|
|
196
|
+
// The skill of this machine stays a local skill - with its own
|
|
197
|
+
// name and its own body, not the sanitized copy.
|
|
198
|
+
expect(results[0].source).toBe("local");
|
|
199
|
+
expect(results[0].skill.id).toBe("inpac-pe-approval-workflow");
|
|
200
|
+
expect(results[0].skill.body).toBe("the local body of inpac-pe-approval-workflow");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("resolves the shared name of a local skill in a lookup", () => {
|
|
204
|
+
const registry = createSkillRegistry({ indexer: null });
|
|
205
|
+
registry.register(localSkill("inpac-pe-approval-workflow"));
|
|
206
|
+
|
|
207
|
+
expect(registry.get("client-a-pe-approval-workflow")?.id).toBe(
|
|
208
|
+
"inpac-pe-approval-workflow",
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
172
213
|
describe("the state of the vector store", () => {
|
|
173
214
|
it("reports the number of skills", async () => {
|
|
174
215
|
const client = fakeClient([], {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the sanitization of a skill for the shared collection.
|
|
3
|
+
*
|
|
4
|
+
* The collection is shared, so the client names of a user must not travel
|
|
5
|
+
* into it. The rules used to live in `scripts/sync-to-qdrant.ts` - the
|
|
6
|
+
* only writer that could reach them. The runtime registry writes into the
|
|
7
|
+
* same collection, so it needs them too.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "bun:test";
|
|
11
|
+
|
|
12
|
+
import { sanitizeText, skillPointName } from "../src/sanitize.js";
|
|
13
|
+
import { skillPointPayload } from "../src/vector-schema.js";
|
|
14
|
+
|
|
15
|
+
describe("the sanitization of a text", () => {
|
|
16
|
+
test("the name of a client is replaced", () => {
|
|
17
|
+
expect(sanitizeText("the inpac workflow")).toBe("the [Client A] workflow");
|
|
18
|
+
expect(sanitizeText("for digisoft")).toBe("for [Client B]");
|
|
19
|
+
expect(sanitizeText("gse-insurance step")).toBe("[Insurance Project] step");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("the name of a client is replaced in any case", () => {
|
|
23
|
+
expect(sanitizeText("INPAC and Inpac")).toBe("[Client A] and [Client A]");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("a text without a client name stays the same", () => {
|
|
27
|
+
expect(sanitizeText("a plain skill")).toBe("a plain skill");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("the generic phrases are replaced", () => {
|
|
31
|
+
expect(sanitizeText("Company Name")).toBe("[Client Name]");
|
|
32
|
+
expect(sanitizeText("your company")).toBe("[Client]");
|
|
33
|
+
expect(sanitizeText("our client")).toBe("[Client]");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("the name of a skill in the shared collection", () => {
|
|
38
|
+
test("a name of the map is renamed", () => {
|
|
39
|
+
expect(skillPointName("inpac-pe-approval-workflow")).toBe(
|
|
40
|
+
"client-a-pe-approval-workflow",
|
|
41
|
+
);
|
|
42
|
+
expect(skillPointName("tbs-import-clearance-lcv")).toBe(
|
|
43
|
+
"import-project-clearance",
|
|
44
|
+
);
|
|
45
|
+
expect(skillPointName("whispertool")).toBe("whisper-tool");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("a name without a client name stays the same", () => {
|
|
49
|
+
expect(skillPointName("code-review")).toBe("code-review");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("the name never carries a client name", () => {
|
|
53
|
+
for (const name of [
|
|
54
|
+
"inpac-pe-approval-workflow",
|
|
55
|
+
"gse-insurance-step5-slot-filter",
|
|
56
|
+
"tbs-import-clearance-lcv",
|
|
57
|
+
"whispertool",
|
|
58
|
+
]) {
|
|
59
|
+
expect(skillPointName(name)).not.toMatch(/inpac|gse-insurance|tbs-import/i);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("the payload of a point is sanitized", () => {
|
|
65
|
+
test("the client names of the body do not travel into the collection", () => {
|
|
66
|
+
const payload = skillPointPayload({
|
|
67
|
+
name: "inpac-pe-approval-workflow",
|
|
68
|
+
description: "the workflow of inpac",
|
|
69
|
+
body: "the approval of inpac for the client",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(payload.name).toBe("client-a-pe-approval-workflow");
|
|
73
|
+
expect(payload.description).not.toMatch(/inpac/i);
|
|
74
|
+
expect(payload.body).not.toMatch(/inpac/i);
|
|
75
|
+
expect(payload.text).not.toMatch(/inpac/i);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("the text is built from the sanitized parts", () => {
|
|
79
|
+
const payload = skillPointPayload({
|
|
80
|
+
name: "inpac-pe-approval-workflow",
|
|
81
|
+
description: "the workflow",
|
|
82
|
+
body: "the body",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(payload.text).toContain("client-a-pe-approval-workflow");
|
|
86
|
+
expect(payload.text).not.toContain("inpac-pe-approval-workflow");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the point identity of the skill collection.
|
|
3
|
+
*
|
|
4
|
+
* Two writers fill the same collection: the runtime registry and
|
|
5
|
+
* `scripts/sync-to-qdrant.ts`. They used to disagree about the identity of
|
|
6
|
+
* a point - the runtime hashed the name of the skill, the script used a
|
|
7
|
+
* running number. The same skill then existed twice, and a running number
|
|
8
|
+
* could overwrite a point of an unrelated skill.
|
|
9
|
+
*
|
|
10
|
+
* The identity now comes from one shared place, and these tests hold it
|
|
11
|
+
* there.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, test } from "bun:test";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createSkillRegistry,
|
|
18
|
+
type SkillVectorIndexer,
|
|
19
|
+
type SkillVectorPoint,
|
|
20
|
+
} from "../src/loader.js";
|
|
21
|
+
import {
|
|
22
|
+
SKILL_PAYLOAD_BODY_LIMIT,
|
|
23
|
+
SKILL_PAYLOAD_TEXT_LIMIT,
|
|
24
|
+
skillPointPayload,
|
|
25
|
+
skillQdrantPoint,
|
|
26
|
+
stableHash,
|
|
27
|
+
} from "../src/vector-schema.js";
|
|
28
|
+
import type { Skill } from "../src/types.js";
|
|
29
|
+
|
|
30
|
+
/** Wait until the registry has no index work left. */
|
|
31
|
+
async function settle(registry: { isIndexing(): boolean }): Promise<void> {
|
|
32
|
+
for (let attempt = 0; attempt < 200 && registry.isIndexing(); attempt++) {
|
|
33
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** A skill with the given id and body. */
|
|
38
|
+
function makeSkill(id: string, body = `body of ${id}`): Skill {
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
path: `/tmp/skills/${id}`,
|
|
42
|
+
frontmatter: {
|
|
43
|
+
name: id,
|
|
44
|
+
description: `the ${id} skill`,
|
|
45
|
+
},
|
|
46
|
+
body,
|
|
47
|
+
metadata: {} as Skill["metadata"],
|
|
48
|
+
} as Skill;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** An indexer that keeps the points it receives. */
|
|
52
|
+
class RecordingIndexer implements SkillVectorIndexer {
|
|
53
|
+
readonly upsertCalls: SkillVectorPoint[][] = [];
|
|
54
|
+
|
|
55
|
+
async embed(texts: string[]): Promise<number[][]> {
|
|
56
|
+
return texts.map((text) => [text.length, 1, 2]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async upsert(points: SkillVectorPoint[]): Promise<void> {
|
|
60
|
+
this.upsertCalls.push(points);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("the identity of a skill point", () => {
|
|
65
|
+
test("the id of the point is the hash of the name in the payload", () => {
|
|
66
|
+
const payload = skillPointPayload({
|
|
67
|
+
name: "a-skill",
|
|
68
|
+
description: "does something",
|
|
69
|
+
body: "the body",
|
|
70
|
+
});
|
|
71
|
+
const point = skillQdrantPoint(payload);
|
|
72
|
+
|
|
73
|
+
expect(point.id).toBe(stableHash(payload.name));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the same name always addresses the same point", () => {
|
|
77
|
+
const first = skillQdrantPoint(
|
|
78
|
+
skillPointPayload({ name: "a-skill", description: "one", body: "one" }),
|
|
79
|
+
);
|
|
80
|
+
const second = skillQdrantPoint(
|
|
81
|
+
skillPointPayload({ name: "a-skill", description: "two", body: "two" }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// The content may change, the identity of the point may not.
|
|
85
|
+
expect(second.id).toBe(first.id);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("different names address different points", () => {
|
|
89
|
+
const first = skillQdrantPoint(
|
|
90
|
+
skillPointPayload({ name: "a-skill", description: "d", body: "b" }),
|
|
91
|
+
);
|
|
92
|
+
const second = skillQdrantPoint(
|
|
93
|
+
skillPointPayload({ name: "b-skill", description: "d", body: "b" }),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(second.id).not.toBe(first.id);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the payload carries the shared fields and nothing else", () => {
|
|
100
|
+
const payload = skillPointPayload({
|
|
101
|
+
name: "a-skill",
|
|
102
|
+
title: "A Skill",
|
|
103
|
+
description: "does something",
|
|
104
|
+
body: "the body",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(Object.keys(payload).sort()).toEqual([
|
|
108
|
+
"body",
|
|
109
|
+
"description",
|
|
110
|
+
"name",
|
|
111
|
+
"text",
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("the payload and the text are truncated", () => {
|
|
116
|
+
const payload = skillPointPayload({
|
|
117
|
+
name: "a-skill",
|
|
118
|
+
description: "d",
|
|
119
|
+
body: "x".repeat(SKILL_PAYLOAD_BODY_LIMIT + 500),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(payload.body.length).toBe(SKILL_PAYLOAD_BODY_LIMIT);
|
|
123
|
+
expect(payload.text.length).toBeLessThanOrEqual(
|
|
124
|
+
SKILL_PAYLOAD_TEXT_LIMIT + payload.name.length + payload.description.length + 10,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("the title is part of the text when it differs from the name", () => {
|
|
129
|
+
const withTitle = skillPointPayload({
|
|
130
|
+
name: "a-skill",
|
|
131
|
+
title: "A Human Title",
|
|
132
|
+
description: "d",
|
|
133
|
+
body: "b",
|
|
134
|
+
});
|
|
135
|
+
const withoutTitle = skillPointPayload({
|
|
136
|
+
name: "a-skill",
|
|
137
|
+
description: "d",
|
|
138
|
+
body: "b",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(withTitle.text).toContain("A Human Title");
|
|
142
|
+
expect(withoutTitle.text).not.toContain("A Human Title");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe("the runtime writes the points of the shared schema", () => {
|
|
147
|
+
test("the indexed point carries the shared payload and id", async () => {
|
|
148
|
+
const indexer = new RecordingIndexer();
|
|
149
|
+
const registry = createSkillRegistry({ indexer });
|
|
150
|
+
|
|
151
|
+
registry.register(makeSkill("alpha"));
|
|
152
|
+
await settle(registry);
|
|
153
|
+
|
|
154
|
+
const points = indexer.upsertCalls.flat();
|
|
155
|
+
expect(points.length).toBe(1);
|
|
156
|
+
|
|
157
|
+
const point = points[0];
|
|
158
|
+
expect(point.id).toBe(stableHash(point.payload.name));
|
|
159
|
+
expect(point.payload.name).toBe("alpha");
|
|
160
|
+
expect(Object.keys(point.payload).sort()).toEqual([
|
|
161
|
+
"body",
|
|
162
|
+
"description",
|
|
163
|
+
"name",
|
|
164
|
+
"text",
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-harness/two-phase-review",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.415",
|
|
4
4
|
"description": "pi-harness-runtime — two-phase implementation and review workflow (queue review signals, gate the phase, scope the diff)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|