@rubytech/create-maxy-code 0.1.265 → 0.1.267
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/dist/index.js +16 -0
- package/package.json +1 -1
- package/payload/platform/lib/models/dist/index.d.ts +1 -1
- package/payload/platform/lib/models/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/models/dist/index.js +5 -2
- package/payload/platform/lib/models/dist/index.js.map +1 -1
- package/payload/platform/lib/models/src/index.ts +5 -2
- package/payload/platform/neo4j/schema.cypher +13 -0
- package/payload/platform/package-lock.json +16 -0
- package/payload/platform/package.json +3 -2
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js +9 -9
- package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-search.test.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +11 -3
- package/payload/platform/plugins/admin/skills/public-agent-manager/SKILL.md +2 -2
- package/payload/platform/plugins/business-assistant/PLUGIN.md +1 -5
- package/payload/platform/plugins/docs/references/admin-ui.md +1 -1
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +9 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +36 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts +10 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js +59 -0
- package/payload/platform/services/claude-session-manager/dist/rc-daemon.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts +19 -0
- package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js +31 -0
- package/payload/platform/services/claude-session-manager/dist/wa-channel-mcp.js.map +1 -0
- package/payload/platform/services/whatsapp-channel/package.json +20 -0
- package/payload/platform/templates/account.json +1 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
- package/payload/platform/templates/specialists/agents/librarian.md +1 -1
- package/payload/platform/templates/specialists/agents/research-assistant.md +1 -1
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +1 -1
- package/payload/premium-plugins/writer-craft/PLUGIN.md +4 -4
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js +44 -9
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +31 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +28 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +7 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +93 -44
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +7 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +14 -3
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +22 -8
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +93 -84
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +18 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +32 -3
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +35 -2
- package/payload/premium-plugins/writer-craft/mcp/src/index.ts +52 -10
- package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +39 -0
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +108 -44
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-ingest-session-text.ts +1 -0
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +24 -4
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +136 -102
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +45 -3
- package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +34 -23
- package/payload/server/{chunk-SOLVVUST.js → chunk-W4EM7RK4.js} +2 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/server.js +345 -14
- package/payload/platform/plugins/business-assistant/references/quote-engine.md +0 -122
- package/payload/platform/plugins/business-assistant/references/quote-generation.md +0 -94
- package/payload/platform/plugins/business-assistant/references/quoting.md +0 -85
- package/payload/platform/plugins/business-assistant/skills/pricing-method/SKILL.md +0 -78
- package/payload/platform/plugins/business-assistant/skills/pricing-method/references/learning-from-history.md +0 -51
- package/payload/platform/plugins/business-assistant/skills/pricing-method/references/maintenance.md +0 -32
- package/payload/platform/plugins/business-assistant/skills/pricing-method/references/manual-definition.md +0 -42
- package/payload/platform/plugins/business-assistant/skills/pricing-method/references/verification.md +0 -37
|
@@ -29,8 +29,11 @@ import { notTrashed } from "../lib/voice-corpus.js";
|
|
|
29
29
|
import {
|
|
30
30
|
VOICE_CORPUS_WHERE,
|
|
31
31
|
voiceCorpusWhereWithFormat,
|
|
32
|
+
voiceCorpusWhereWithFormatAndAuthor,
|
|
33
|
+
profileUserIdForScope,
|
|
32
34
|
FORMAT_VALUES,
|
|
33
35
|
type VoiceFormat,
|
|
36
|
+
type VoiceScope,
|
|
34
37
|
} from "../lib/voice-corpus.js";
|
|
35
38
|
|
|
36
39
|
/**
|
|
@@ -60,6 +63,12 @@ const RE_RUN_AGE_DAYS = 30;
|
|
|
60
63
|
export interface VoiceDistilProfileParams {
|
|
61
64
|
accountId: string;
|
|
62
65
|
userId: string;
|
|
66
|
+
/**
|
|
67
|
+
* Profile scope (Task 676). `'personal'` (default) distils the operator's
|
|
68
|
+
* own author-filtered corpus onto `:AdminUser`; `'org'` distils the whole
|
|
69
|
+
* account onto `:LocalBusiness` under the reserved sentinel userId.
|
|
70
|
+
*/
|
|
71
|
+
scope?: VoiceScope;
|
|
63
72
|
force?: boolean;
|
|
64
73
|
/**
|
|
65
74
|
* Writing format to distil. When omitted on `sample`, enumerates distinct
|
|
@@ -152,42 +161,54 @@ export interface VoiceDistilProfileResult {
|
|
|
152
161
|
*/
|
|
153
162
|
async function distilForFormat(
|
|
154
163
|
params: Required<Pick<VoiceDistilProfileParams, "accountId" | "userId">> &
|
|
155
|
-
Pick<VoiceDistilProfileParams, "force" | "mode" | "styleCardYaml"> & {
|
|
164
|
+
Pick<VoiceDistilProfileParams, "force" | "mode" | "styleCardYaml" | "scope"> & {
|
|
156
165
|
format: VoiceFormat;
|
|
157
166
|
},
|
|
158
167
|
): Promise<VoiceDistilProfileResult> {
|
|
159
168
|
const { accountId, userId, format, force = false } = params;
|
|
169
|
+
const scope: VoiceScope = params.scope ?? "personal";
|
|
170
|
+
const profileUserId = profileUserIdForScope(scope, userId);
|
|
171
|
+
const anchorLabel = scope === "org" ? "LocalBusiness" : "AdminUser";
|
|
172
|
+
// Org walks the whole account; personal narrows to one author's content.
|
|
173
|
+
const corpusWhere =
|
|
174
|
+
scope === "org"
|
|
175
|
+
? voiceCorpusWhereWithFormat(format)
|
|
176
|
+
: voiceCorpusWhereWithFormatAndAuthor(format);
|
|
177
|
+
const corpusParams =
|
|
178
|
+
scope === "org"
|
|
179
|
+
? { accountId, format }
|
|
180
|
+
: { accountId, format, voiceAuthor: userId };
|
|
160
181
|
const session = getSession();
|
|
161
182
|
const now = new Date();
|
|
162
183
|
const nowIso = now.toISOString();
|
|
163
184
|
|
|
164
185
|
try {
|
|
165
|
-
// 1. Read existing
|
|
186
|
+
// 1. Read existing scoped profile (if any) for cadence-guard comparison.
|
|
187
|
+
// Key-only match — the (accountId, profileUserId, format) triple is the
|
|
188
|
+
// unique constraint key, so no anchor traversal is needed on the read.
|
|
166
189
|
const existing = await session.run(
|
|
167
|
-
`MATCH (
|
|
168
|
-
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, userId: $userId, format: $format})
|
|
190
|
+
`OPTIONAL MATCH (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
|
|
169
191
|
RETURN elementId(p) AS profileId, p.corpusSize AS corpusSize,
|
|
170
192
|
p.generatedAt AS generatedAt`,
|
|
171
|
-
{ accountId,
|
|
193
|
+
{ accountId, profileUserId, format },
|
|
172
194
|
);
|
|
173
195
|
const existingRow = existing.records[0];
|
|
174
196
|
const prevProfileId = (existingRow?.get("profileId") as string | null) ?? null;
|
|
175
197
|
const prevCorpusSize = toJsNumber(existingRow?.get("corpusSize"));
|
|
176
198
|
const prevGeneratedAt = (existingRow?.get("generatedAt") as string | null) ?? null;
|
|
177
199
|
|
|
178
|
-
// 2. Count corpus for this
|
|
179
|
-
const corpusWhere = voiceCorpusWhereWithFormat(format);
|
|
200
|
+
// 2. Count corpus for this scope + format.
|
|
180
201
|
const countResult = await session.run(
|
|
181
202
|
`MATCH (n)
|
|
182
203
|
WHERE ${corpusWhere}
|
|
183
204
|
RETURN count(n) AS c`,
|
|
184
|
-
|
|
205
|
+
corpusParams,
|
|
185
206
|
);
|
|
186
207
|
const corpusSize = toJsNumber(countResult.records[0]?.get("c")) ?? 0;
|
|
187
208
|
|
|
188
209
|
if (corpusSize === 0) {
|
|
189
210
|
process.stderr.write(
|
|
190
|
-
`[voice-distil]
|
|
211
|
+
`[voice-distil] scope=${scope} userId=${profileUserId} format=${format} skip reason=empty-corpus\n`,
|
|
191
212
|
);
|
|
192
213
|
return {
|
|
193
214
|
profileId: prevProfileId,
|
|
@@ -212,7 +233,7 @@ async function distilForFormat(
|
|
|
212
233
|
if (!ageTrigger && !growthTrigger) {
|
|
213
234
|
const reason = ageDays < 1 ? "recent" : "below-threshold";
|
|
214
235
|
process.stderr.write(
|
|
215
|
-
`[voice-distil]
|
|
236
|
+
`[voice-distil] scope=${scope} userId=${profileUserId} format=${format} skip reason=${reason} corpusSize=${corpusSize} prevCorpus=${prevCorpusSize} ageDays=${ageDays.toFixed(1)}\n`,
|
|
216
237
|
);
|
|
217
238
|
return {
|
|
218
239
|
profileId: prevProfileId,
|
|
@@ -265,7 +286,7 @@ async function distilForFormat(
|
|
|
265
286
|
ts
|
|
266
287
|
ORDER BY ts IS NULL, ts DESC
|
|
267
288
|
LIMIT 500`,
|
|
268
|
-
|
|
289
|
+
corpusParams,
|
|
269
290
|
);
|
|
270
291
|
const samples: { id: string; label: string; body: string }[] = [];
|
|
271
292
|
let sectionWalkCount = 0;
|
|
@@ -290,11 +311,14 @@ async function distilForFormat(
|
|
|
290
311
|
if (charBudget <= 0) break;
|
|
291
312
|
}
|
|
292
313
|
|
|
293
|
-
// 5. Pull feedback intents from existing :VoiceEdit nodes for this
|
|
314
|
+
// 5. Pull feedback intents from existing :VoiceEdit nodes for this scope +
|
|
315
|
+
// format. Keyed by profileUserId: org edits live under the sentinel, so
|
|
316
|
+
// org distil reads org feedback and personal distil reads only this
|
|
317
|
+
// operator's edits — no extra scope predicate needed.
|
|
294
318
|
const feedback = await session.run(
|
|
295
|
-
`MATCH (e:VoiceEdit {userId: $
|
|
319
|
+
`MATCH (e:VoiceEdit {userId: $profileUserId, accountId: $accountId, format: $format})
|
|
296
320
|
RETURN e.intent AS intent ORDER BY e.occurredAt DESC LIMIT 50`,
|
|
297
|
-
{ accountId,
|
|
321
|
+
{ accountId, profileUserId, format },
|
|
298
322
|
);
|
|
299
323
|
const feedbackIntents = feedback.records
|
|
300
324
|
.map((r) => r.get("intent") as string | null)
|
|
@@ -304,7 +328,7 @@ async function distilForFormat(
|
|
|
304
328
|
const mode = params.mode ?? "sample";
|
|
305
329
|
if (mode === "sample") {
|
|
306
330
|
process.stderr.write(
|
|
307
|
-
`[voice-distil]
|
|
331
|
+
`[voice-distil] scope=${scope} userId=${profileUserId} format=${format} mode=sample corpusSize=${corpusSize} ` +
|
|
308
332
|
`exemplars=${samples.length} feedbackEntries=${feedbackIntents.length} ` +
|
|
309
333
|
`sample-body-source=section-walk:${sectionWalkCount} n.body:${nBodyCount}\n`,
|
|
310
334
|
);
|
|
@@ -327,13 +351,27 @@ async function distilForFormat(
|
|
|
327
351
|
);
|
|
328
352
|
}
|
|
329
353
|
|
|
330
|
-
// 7. Write the :VoiceProfile node — keyed on (accountId,
|
|
331
|
-
//
|
|
354
|
+
// 7. Write the :VoiceProfile node — keyed on (accountId, profileUserId,
|
|
355
|
+
// format) — anchored on :AdminUser (personal) or :LocalBusiness (org),
|
|
356
|
+
// plus LEARNED_FROM edges to every sampled node. `scope` is SET after the
|
|
357
|
+
// MERGE (never inside the pattern) so a legacy scope-less personal
|
|
358
|
+
// profile is matched and backfilled rather than duplicated.
|
|
359
|
+
const anchorMatch =
|
|
360
|
+
scope === "org"
|
|
361
|
+
? `MATCH (anchor:LocalBusiness {accountId: $accountId})`
|
|
362
|
+
: `MATCH (anchor:AdminUser {accountId: $accountId, userId: $profileUserId})`;
|
|
363
|
+
// Two-step MERGE: the profile node is MERGEd by its unique key alone, then
|
|
364
|
+
// the anchor edge is MERGEd separately. MERGEing the node through the anchor
|
|
365
|
+
// pattern would create a duplicate (constraint error) if the node existed
|
|
366
|
+
// but its anchor edge did not. Keying the node first mirrors the key-only
|
|
367
|
+
// reads and keeps the legacy scope-less backfill matching by key.
|
|
332
368
|
const writeResult = await session.run(
|
|
333
|
-
|
|
334
|
-
MERGE (
|
|
369
|
+
`${anchorMatch}
|
|
370
|
+
MERGE (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
|
|
335
371
|
ON CREATE SET p.createdAt = $now
|
|
336
|
-
|
|
372
|
+
MERGE (anchor)-[:HAS_VOICE_PROFILE]->(p)
|
|
373
|
+
SET p.scope = $scope,
|
|
374
|
+
p.styleCard = $styleCard,
|
|
337
375
|
p.generatedAt = $now,
|
|
338
376
|
p.corpusSize = $corpusSize,
|
|
339
377
|
p.feedbackEntries = $feedbackEntries,
|
|
@@ -345,8 +383,9 @@ async function distilForFormat(
|
|
|
345
383
|
RETURN elementId(p) AS profileId`,
|
|
346
384
|
{
|
|
347
385
|
accountId,
|
|
348
|
-
|
|
386
|
+
profileUserId,
|
|
349
387
|
format,
|
|
388
|
+
scope,
|
|
350
389
|
styleCard: styleCardYaml,
|
|
351
390
|
now: nowIso,
|
|
352
391
|
corpusSize,
|
|
@@ -356,13 +395,15 @@ async function distilForFormat(
|
|
|
356
395
|
);
|
|
357
396
|
if (writeResult.records.length === 0) {
|
|
358
397
|
throw new Error(
|
|
359
|
-
|
|
398
|
+
scope === "org"
|
|
399
|
+
? `voice-mirror: :LocalBusiness {accountId='${accountId}'} not found. The account org node must exist before an org distil (created at onboarding alongside :AdminUser).`
|
|
400
|
+
: `voice-mirror: :AdminUser {accountId='${accountId}', userId='${userId}'} not found. Onboarding must promote the operator to :AdminUser before distillation.`,
|
|
360
401
|
);
|
|
361
402
|
}
|
|
362
403
|
const profileId = writeResult.records[0].get("profileId") as string;
|
|
363
404
|
|
|
364
405
|
process.stderr.write(
|
|
365
|
-
`[voice-distil]
|
|
406
|
+
`[voice-distil] scope=${scope} userId=${profileUserId} anchor=${anchorLabel} format=${format} corpusSize=${corpusSize} generatedAt=${nowIso} feedbackEntries=${feedbackIntents.length}${scope === "org" ? " sentinel=__org__" : ""}\n`,
|
|
366
407
|
);
|
|
367
408
|
|
|
368
409
|
return {
|
|
@@ -393,20 +434,23 @@ async function amendForFormat(params: {
|
|
|
393
434
|
userId: string;
|
|
394
435
|
format: VoiceFormat;
|
|
395
436
|
nodeIds: string[];
|
|
437
|
+
scope?: VoiceScope;
|
|
396
438
|
}): Promise<VoiceDistilProfileResult> {
|
|
397
439
|
const { accountId, userId, format, nodeIds } = params;
|
|
440
|
+
const scope: VoiceScope = params.scope ?? "personal";
|
|
441
|
+
const profileUserId = profileUserIdForScope(scope, userId);
|
|
398
442
|
const session = getSession();
|
|
399
443
|
|
|
400
444
|
try {
|
|
401
|
-
// 1. Existing profile (may be absent — first-time amend
|
|
445
|
+
// 1. Existing scoped profile (may be absent — first-time amend, no prior
|
|
446
|
+
// card). Key-only match on the unique (accountId, profileUserId, format).
|
|
402
447
|
const profileRow = await session.run(
|
|
403
|
-
`MATCH (
|
|
404
|
-
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, userId: $userId, format: $format})
|
|
448
|
+
`OPTIONAL MATCH (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
|
|
405
449
|
RETURN elementId(p) AS profileId,
|
|
406
450
|
p.styleCard AS styleCard,
|
|
407
451
|
p.corpusSize AS corpusSize,
|
|
408
452
|
p.generatedAt AS generatedAt`,
|
|
409
|
-
{ accountId,
|
|
453
|
+
{ accountId, profileUserId, format },
|
|
410
454
|
);
|
|
411
455
|
const prow = profileRow.records[0];
|
|
412
456
|
const prevProfileId = (prow?.get("profileId") as string | null) ?? null;
|
|
@@ -453,11 +497,12 @@ async function amendForFormat(params: {
|
|
|
453
497
|
}
|
|
454
498
|
|
|
455
499
|
// 3. Feedback intents (same as sample) — agent benefits even on a no-op
|
|
456
|
-
// amend ("you've already corrected this voice tic ten times").
|
|
500
|
+
// amend ("you've already corrected this voice tic ten times"). Keyed by
|
|
501
|
+
// profileUserId so org amends read org feedback.
|
|
457
502
|
const feedback = await session.run(
|
|
458
|
-
`MATCH (e:VoiceEdit {userId: $
|
|
503
|
+
`MATCH (e:VoiceEdit {userId: $profileUserId, accountId: $accountId, format: $format})
|
|
459
504
|
RETURN e.intent AS intent ORDER BY e.occurredAt DESC LIMIT 50`,
|
|
460
|
-
{ accountId,
|
|
505
|
+
{ accountId, profileUserId, format },
|
|
461
506
|
);
|
|
462
507
|
const feedbackIntents = feedback.records
|
|
463
508
|
.map((r) => r.get("intent") as string | null)
|
|
@@ -465,7 +510,7 @@ async function amendForFormat(params: {
|
|
|
465
510
|
|
|
466
511
|
if (eligibleIds.length === 0) {
|
|
467
512
|
process.stderr.write(
|
|
468
|
-
`[voice-distil] mode=amend userId=${
|
|
513
|
+
`[voice-distil] scope=${scope} mode=amend userId=${profileUserId} format=${format} nodeIds=${nodeIds.length} ` +
|
|
469
514
|
`existingProfile=${prevProfileId ?? "none"} eligible=0 ineligible=${ineligible.length}\n`,
|
|
470
515
|
);
|
|
471
516
|
return {
|
|
@@ -537,7 +582,7 @@ async function amendForFormat(params: {
|
|
|
537
582
|
}
|
|
538
583
|
|
|
539
584
|
process.stderr.write(
|
|
540
|
-
`[voice-distil] mode=amend userId=${
|
|
585
|
+
`[voice-distil] scope=${scope} mode=amend userId=${profileUserId} format=${format} nodeIds=${nodeIds.length} ` +
|
|
541
586
|
`existingProfile=${prevProfileId ?? "none"} eligible=${eligibleIds.length} ineligible=${ineligible.length} ` +
|
|
542
587
|
`exemplars=${exemplars.length} sample-body-source=section-walk:${sectionWalkCount} n.body:${nBodyCount}\n`,
|
|
543
588
|
);
|
|
@@ -578,8 +623,12 @@ async function amendWriteForFormat(params: {
|
|
|
578
623
|
format: VoiceFormat;
|
|
579
624
|
styleCardYaml: string;
|
|
580
625
|
amendedFromNodeIds: string[];
|
|
626
|
+
scope?: VoiceScope;
|
|
581
627
|
}): Promise<VoiceDistilProfileResult> {
|
|
582
628
|
const { accountId, userId, format, styleCardYaml, amendedFromNodeIds } = params;
|
|
629
|
+
const scope: VoiceScope = params.scope ?? "personal";
|
|
630
|
+
const profileUserId = profileUserIdForScope(scope, userId);
|
|
631
|
+
const anchorLabel = scope === "org" ? "LocalBusiness" : "AdminUser";
|
|
583
632
|
const session = getSession();
|
|
584
633
|
const nowIso = new Date().toISOString();
|
|
585
634
|
try {
|
|
@@ -607,17 +656,24 @@ async function amendWriteForFormat(params: {
|
|
|
607
656
|
}
|
|
608
657
|
|
|
609
658
|
const feedback = await session.run(
|
|
610
|
-
`MATCH (e:VoiceEdit {userId: $
|
|
659
|
+
`MATCH (e:VoiceEdit {userId: $profileUserId, accountId: $accountId, format: $format})
|
|
611
660
|
RETURN count(e) AS c`,
|
|
612
|
-
{ accountId,
|
|
661
|
+
{ accountId, profileUserId, format },
|
|
613
662
|
);
|
|
614
663
|
const feedbackEntries = toJsNumber(feedback.records[0]?.get("c")) ?? 0;
|
|
615
664
|
|
|
665
|
+
const anchorMatch =
|
|
666
|
+
scope === "org"
|
|
667
|
+
? `MATCH (anchor:LocalBusiness {accountId: $accountId})`
|
|
668
|
+
: `MATCH (anchor:AdminUser {accountId: $accountId, userId: $profileUserId})`;
|
|
669
|
+
// Two-step MERGE (see distilForFormat): node by unique key, then anchor edge.
|
|
616
670
|
const result = await session.run(
|
|
617
|
-
|
|
618
|
-
MERGE (
|
|
671
|
+
`${anchorMatch}
|
|
672
|
+
MERGE (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
|
|
619
673
|
ON CREATE SET p.createdAt = $now, p.corpusSize = 0
|
|
620
|
-
|
|
674
|
+
MERGE (anchor)-[:HAS_VOICE_PROFILE]->(p)
|
|
675
|
+
SET p.scope = $scope,
|
|
676
|
+
p.styleCard = $styleCard,
|
|
621
677
|
p.generatedAt = $now,
|
|
622
678
|
p.feedbackEntries = $feedbackEntries,
|
|
623
679
|
p.updatedAt = $now
|
|
@@ -629,8 +685,9 @@ async function amendWriteForFormat(params: {
|
|
|
629
685
|
RETURN elementId(p) AS profileId, p.corpusSize AS corpusSize`,
|
|
630
686
|
{
|
|
631
687
|
accountId,
|
|
632
|
-
|
|
688
|
+
profileUserId,
|
|
633
689
|
format,
|
|
690
|
+
scope,
|
|
634
691
|
styleCard: styleCardYaml,
|
|
635
692
|
now: nowIso,
|
|
636
693
|
feedbackEntries,
|
|
@@ -639,15 +696,17 @@ async function amendWriteForFormat(params: {
|
|
|
639
696
|
);
|
|
640
697
|
if (result.records.length === 0) {
|
|
641
698
|
throw new Error(
|
|
642
|
-
|
|
643
|
-
`
|
|
699
|
+
scope === "org"
|
|
700
|
+
? `voice-mirror: :LocalBusiness {accountId='${accountId}'} not found. The account org node must exist before an org amend-write.`
|
|
701
|
+
: `voice-mirror: :AdminUser {accountId='${accountId}', userId='${userId}'} not found. ` +
|
|
702
|
+
`Onboarding must promote the operator to :AdminUser before distillation.`,
|
|
644
703
|
);
|
|
645
704
|
}
|
|
646
705
|
const profileId = result.records[0].get("profileId") as string;
|
|
647
706
|
const corpusSize = toJsNumber(result.records[0].get("corpusSize")) ?? 0;
|
|
648
707
|
|
|
649
708
|
process.stderr.write(
|
|
650
|
-
`[voice-distil] mode=write userId=${
|
|
709
|
+
`[voice-distil] scope=${scope} mode=write userId=${profileUserId} anchor=${anchorLabel} format=${format} amended=true ` +
|
|
651
710
|
`amendedFromNodeIds=${amendedFromNodeIds.length} corpusSize=${corpusSize} ` +
|
|
652
711
|
`generatedAt=${nowIso} feedbackEntries=${feedbackEntries}\n`,
|
|
653
712
|
);
|
|
@@ -668,6 +727,7 @@ export async function voiceDistilProfile(
|
|
|
668
727
|
params: VoiceDistilProfileParams,
|
|
669
728
|
): Promise<VoiceDistilProfileResult | VoiceDistilProfileResult[]> {
|
|
670
729
|
const { accountId, userId, force = false } = params;
|
|
730
|
+
const scope: VoiceScope = params.scope ?? "personal";
|
|
671
731
|
if (!accountId || !userId) {
|
|
672
732
|
throw new Error("voice-distil-profile: accountId and userId required");
|
|
673
733
|
}
|
|
@@ -688,6 +748,7 @@ export async function voiceDistilProfile(
|
|
|
688
748
|
userId,
|
|
689
749
|
format: params.format,
|
|
690
750
|
nodeIds,
|
|
751
|
+
scope,
|
|
691
752
|
});
|
|
692
753
|
}
|
|
693
754
|
|
|
@@ -716,6 +777,7 @@ export async function voiceDistilProfile(
|
|
|
716
777
|
format: params.format,
|
|
717
778
|
styleCardYaml,
|
|
718
779
|
amendedFromNodeIds: params.amendedFromNodeIds,
|
|
780
|
+
scope,
|
|
719
781
|
});
|
|
720
782
|
}
|
|
721
783
|
|
|
@@ -725,14 +787,16 @@ export async function voiceDistilProfile(
|
|
|
725
787
|
}
|
|
726
788
|
|
|
727
789
|
// Multi-format path: enumerate distinct format values present in the corpus.
|
|
790
|
+
// Scope-aware: personal enumerates only the operator's authored content, org
|
|
791
|
+
// enumerates the whole account.
|
|
728
792
|
const session = getSession();
|
|
729
793
|
let formats: VoiceFormat[] = [];
|
|
730
794
|
try {
|
|
731
795
|
const enumResult = await session.run(
|
|
732
796
|
`MATCH (n)
|
|
733
|
-
WHERE ${VOICE_CORPUS_WHERE} AND n.format IS NOT NULL
|
|
797
|
+
WHERE ${VOICE_CORPUS_WHERE} AND n.format IS NOT NULL${scope === "personal" ? " AND n.voiceAuthor = $voiceAuthor" : ""}
|
|
734
798
|
RETURN DISTINCT n.format AS fmt`,
|
|
735
|
-
{ accountId },
|
|
799
|
+
scope === "personal" ? { accountId, voiceAuthor: userId } : { accountId },
|
|
736
800
|
);
|
|
737
801
|
formats = enumResult.records
|
|
738
802
|
.map((r) => r.get("fmt") as string)
|
|
@@ -745,7 +809,7 @@ export async function voiceDistilProfile(
|
|
|
745
809
|
|
|
746
810
|
if (formats.length === 0) {
|
|
747
811
|
process.stderr.write(
|
|
748
|
-
`[voice-distil]
|
|
812
|
+
`[voice-distil] scope=${scope} userId=${profileUserIdForScope(scope, userId)} skip reason=no-format-tagged-content\n`,
|
|
749
813
|
);
|
|
750
814
|
return {
|
|
751
815
|
profileId: null,
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { randomUUID } from "node:crypto";
|
|
19
19
|
import { getSession } from "../lib/neo4j.js";
|
|
20
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
profileUserIdForScope,
|
|
22
|
+
type VoiceFormat,
|
|
23
|
+
type VoiceScope,
|
|
24
|
+
} from "../lib/voice-corpus.js";
|
|
21
25
|
|
|
22
26
|
export interface VoiceRecordFeedbackParams {
|
|
23
27
|
accountId: string;
|
|
@@ -29,6 +33,12 @@ export interface VoiceRecordFeedbackParams {
|
|
|
29
33
|
* matching per-format :VoiceProfile.
|
|
30
34
|
*/
|
|
31
35
|
format: VoiceFormat;
|
|
36
|
+
/**
|
|
37
|
+
* Scope of the edited draft (Task 676). `'org'` routes the edit to the
|
|
38
|
+
* account/house profile (`userId='__org__'`); `'personal'` (default) routes
|
|
39
|
+
* to the operator's own profile.
|
|
40
|
+
*/
|
|
41
|
+
scope?: VoiceScope;
|
|
32
42
|
/**
|
|
33
43
|
* One sentence describing what changed and what voice preference it reveals.
|
|
34
44
|
* The calling agent summarises this from `originalText` vs `editedText` in
|
|
@@ -54,6 +64,8 @@ export async function voiceRecordFeedback(
|
|
|
54
64
|
params: VoiceRecordFeedbackParams,
|
|
55
65
|
): Promise<VoiceRecordFeedbackResult> {
|
|
56
66
|
const { accountId, userId, originalText, editedText, format, intent: rawIntent, context } = params;
|
|
67
|
+
const scope: VoiceScope = params.scope ?? "personal";
|
|
68
|
+
const profileUserId = profileUserIdForScope(scope, userId);
|
|
57
69
|
if (!accountId || !userId) {
|
|
58
70
|
throw new Error("voice-record-feedback: accountId and userId required");
|
|
59
71
|
}
|
|
@@ -76,12 +88,18 @@ export async function voiceRecordFeedback(
|
|
|
76
88
|
|
|
77
89
|
const session = getSession();
|
|
78
90
|
try {
|
|
91
|
+
// The :VoiceEdit carries the profile-key userId ($profileUserId — the
|
|
92
|
+
// sentinel for org) plus `scope`, so distillation's feedback query (keyed by
|
|
93
|
+
// profileUserId) reads org edits for the org profile and personal edits for
|
|
94
|
+
// the operator. The real editor is preserved via the AUTHORED edge from
|
|
95
|
+
// their :AdminUser. FEEDBACK_FOR targets the scoped profile (Task 676).
|
|
79
96
|
const result = await session.run(
|
|
80
97
|
`MATCH (a:AdminUser {accountId: $accountId, userId: $userId})
|
|
81
98
|
CREATE (e:VoiceEdit {
|
|
82
99
|
editId: $editId,
|
|
83
100
|
accountId: $accountId,
|
|
84
|
-
userId: $
|
|
101
|
+
userId: $profileUserId,
|
|
102
|
+
scope: $scope,
|
|
85
103
|
format: $format,
|
|
86
104
|
originalText: $originalText,
|
|
87
105
|
editedText: $editedText,
|
|
@@ -92,7 +110,7 @@ export async function voiceRecordFeedback(
|
|
|
92
110
|
})
|
|
93
111
|
CREATE (a)-[:AUTHORED]->(e)
|
|
94
112
|
WITH e
|
|
95
|
-
OPTIONAL MATCH (
|
|
113
|
+
OPTIONAL MATCH (p:VoiceProfile {accountId: $accountId, userId: $profileUserId, format: $format})
|
|
96
114
|
FOREACH (_ IN CASE WHEN p IS NULL THEN [] ELSE [1] END |
|
|
97
115
|
MERGE (e)-[:FEEDBACK_FOR]->(p)
|
|
98
116
|
)
|
|
@@ -100,6 +118,8 @@ export async function voiceRecordFeedback(
|
|
|
100
118
|
{
|
|
101
119
|
accountId,
|
|
102
120
|
userId,
|
|
121
|
+
profileUserId,
|
|
122
|
+
scope,
|
|
103
123
|
format,
|
|
104
124
|
editId,
|
|
105
125
|
originalText,
|
|
@@ -118,7 +138,7 @@ export async function voiceRecordFeedback(
|
|
|
118
138
|
const hadProfile = result.records[0].get("hadProfile") === true;
|
|
119
139
|
|
|
120
140
|
process.stderr.write(
|
|
121
|
-
`[voice-record-feedback]
|
|
141
|
+
`[voice-record-feedback] scope=${scope} userId=${profileUserId} format=${format} intent="${cappedIntent.replace(/"/g, "'")}" diffBytes=${editedText.length - originalText.length}\n`,
|
|
122
142
|
);
|
|
123
143
|
|
|
124
144
|
return { editId, intent: cappedIntent, occurredAt, hadProfile };
|