@rubytech/create-maxy-code 0.1.174 → 0.1.177
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 +13 -4
- package/package.json +1 -1
- package/payload/platform/neo4j/schema.cypher +49 -10
- package/payload/platform/plugins/docs/references/voice-mirror-guide.md +18 -16
- package/payload/platform/plugins/email/skills/email-composition/SKILL.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts +11 -2
- package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +95 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +98 -15
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
- package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +3 -2
- package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -0
- package/payload/platform/scripts/setup-account.sh +8 -0
- package/payload/platform/scripts/verify-skill-tool-surface.sh +27 -8
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +1 -1
- package/payload/premium-plugins/writer-craft/PLUGIN.md +11 -7
- package/payload/premium-plugins/writer-craft/hooks/__tests__/voice-session-end-text.test.sh +284 -0
- package/payload/premium-plugins/writer-craft/hooks/voice-session-end-text.sh +75 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts +3 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js +58 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
- package/payload/premium-plugins/writer-craft/mcp/dist/index.js +95 -18
- 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 +47 -6
- 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 +69 -6
- 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 +8 -2
- 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 +82 -53
- 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 +23 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js +191 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js.map +1 -0
- package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +6 -0
- 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 -6
- 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 +12 -1
- 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 +23 -12
- 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 +3 -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 +33 -5
- 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 +289 -28
- package/payload/premium-plugins/writer-craft/mcp/src/cli/ingest-session-text.ts +65 -0
- package/payload/premium-plugins/writer-craft/mcp/src/index.ts +108 -18
- package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +78 -6
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +115 -56
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-ingest-session-text.ts +229 -0
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +20 -6
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +38 -13
- package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +42 -5
- package/payload/premium-plugins/writer-craft/skills/voice-mirror/SKILL.md +65 -31
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
* voice-retrieve-conditioning — return the operator's style card plus K
|
|
3
3
|
* voice-matched exemplars for a drafting brief.
|
|
4
4
|
*
|
|
5
|
+
* Task 462 changes:
|
|
6
|
+
* - `brief.format` (K-selector: 'short'|'long') renamed to `brief.length`
|
|
7
|
+
* to free the name for the new `brief.format: VoiceFormat` axis.
|
|
8
|
+
* - `brief.format: VoiceFormat` (required) — corpus exemplars are filtered
|
|
9
|
+
* to this writing format; the `:VoiceProfile` is the per-format profile.
|
|
10
|
+
* - All callers updated in the same commit.
|
|
11
|
+
*
|
|
5
12
|
* Retrieval order (best to worst):
|
|
6
|
-
* 1. If brief.topic is provided AND keyword search on the
|
|
13
|
+
* 1. If brief.topic is provided AND keyword search on the per-format corpus
|
|
7
14
|
* returns ≥K matches → use those (ordered by recency).
|
|
8
|
-
* 2. Otherwise, return the K most-recent
|
|
15
|
+
* 2. Otherwise, return the K most-recent per-format corpus nodes.
|
|
9
16
|
*
|
|
10
17
|
* Embeddings are not used in v1: the corpus is small (single operator's
|
|
11
18
|
* personal writing — typically <500 docs), and recency-with-keyword-filter
|
|
@@ -18,7 +25,7 @@
|
|
|
18
25
|
* calling drafting skill falls back to its existing default register.
|
|
19
26
|
*/
|
|
20
27
|
import { getSession } from "../lib/neo4j.js";
|
|
21
|
-
import {
|
|
28
|
+
import { voiceCorpusWhereWithFormat, } from "../lib/voice-corpus.js";
|
|
22
29
|
const DEFAULT_TOKEN_BUDGET_SHORT = 16_000;
|
|
23
30
|
const DEFAULT_TOKEN_BUDGET_LONG = 96_000;
|
|
24
31
|
const CHARS_PER_TOKEN = 4;
|
|
@@ -27,23 +34,26 @@ export async function voiceRetrieveConditioning(params) {
|
|
|
27
34
|
const { accountId, userId, brief } = params;
|
|
28
35
|
if (!accountId || !userId)
|
|
29
36
|
return EMPTY;
|
|
30
|
-
const
|
|
37
|
+
const length = brief.length ?? "short";
|
|
38
|
+
const k = length === "long" ? 15 : 5;
|
|
31
39
|
const tokenBudget = params.tokenBudget ??
|
|
32
|
-
(
|
|
40
|
+
(length === "long" ? DEFAULT_TOKEN_BUDGET_LONG : DEFAULT_TOKEN_BUDGET_SHORT);
|
|
33
41
|
const charBudget = tokenBudget * CHARS_PER_TOKEN;
|
|
42
|
+
const format = brief.format;
|
|
43
|
+
const corpusWhere = voiceCorpusWhereWithFormat(format);
|
|
34
44
|
const session = getSession();
|
|
35
45
|
try {
|
|
36
|
-
// 1. Style card.
|
|
46
|
+
// 1. Style card — per-format profile.
|
|
37
47
|
const profile = await session.run(`MATCH (a:AdminUser {accountId: $accountId, userId: $userId})
|
|
38
|
-
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile)
|
|
39
|
-
RETURN p.styleCard AS styleCard`, { accountId, userId });
|
|
48
|
+
OPTIONAL MATCH (a)-[:HAS_VOICE_PROFILE]->(p:VoiceProfile {accountId: $accountId, userId: $userId, format: $format})
|
|
49
|
+
RETURN p.styleCard AS styleCard`, { accountId, userId, format });
|
|
40
50
|
const styleCard = profile.records[0]?.get("styleCard") ?? null;
|
|
41
|
-
// 2. Exemplars. Keyword-aware
|
|
51
|
+
// 2. Exemplars — filtered to brief.format. Keyword-aware when topic is set.
|
|
42
52
|
const topic = (brief.topic ?? "").trim();
|
|
43
53
|
const usesTopic = topic.length > 0;
|
|
44
54
|
const cypher = usesTopic
|
|
45
55
|
? `MATCH (n)
|
|
46
|
-
WHERE ${
|
|
56
|
+
WHERE ${corpusWhere}
|
|
47
57
|
AND toLower(coalesce(n.body, n.abstract, n.subject, n.title, n.summary, '')) CONTAINS toLower($topic)
|
|
48
58
|
WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
|
|
49
59
|
RETURN elementId(n) AS id,
|
|
@@ -54,7 +64,7 @@ export async function voiceRetrieveConditioning(params) {
|
|
|
54
64
|
ORDER BY ts IS NULL, ts DESC
|
|
55
65
|
LIMIT $k`
|
|
56
66
|
: `MATCH (n)
|
|
57
|
-
WHERE ${
|
|
67
|
+
WHERE ${corpusWhere}
|
|
58
68
|
WITH n, coalesce(n.dateSent, n.datePublished, n.firstMessageAt, n.dateCreated, n.createdAt) AS ts
|
|
59
69
|
RETURN elementId(n) AS id,
|
|
60
70
|
labels(n) AS labels,
|
|
@@ -65,6 +75,7 @@ export async function voiceRetrieveConditioning(params) {
|
|
|
65
75
|
LIMIT $k`;
|
|
66
76
|
const result = await session.run(cypher, {
|
|
67
77
|
accountId,
|
|
78
|
+
format,
|
|
68
79
|
topic,
|
|
69
80
|
k,
|
|
70
81
|
});
|
|
@@ -88,7 +99,7 @@ export async function voiceRetrieveConditioning(params) {
|
|
|
88
99
|
});
|
|
89
100
|
charsUsed += truncated.length;
|
|
90
101
|
}
|
|
91
|
-
process.stderr.write(`[voice-retrieve] task=${brief.topic ??
|
|
102
|
+
process.stderr.write(`[voice-retrieve] task=${brief.topic ?? length} format=${format} styleCardBytes=${styleCard?.length ?? 0} exemplarCount=${exemplars.length} tokenBudget=${tokenBudget}\n`);
|
|
92
103
|
return { styleCard, exemplars };
|
|
93
104
|
}
|
|
94
105
|
catch (err) {
|
package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-retrieve-conditioning.js","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"voice-retrieve-conditioning.js","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { type VoiceFormat } from "../lib/voice-corpus.js";
|
|
1
2
|
export type AuthorshipMode = "human-only" | "human-led-agent-assisted" | "agent-led-human-reviewed" | "agent-only" | "unknown";
|
|
2
3
|
export declare const AUTHORSHIP_MODES: ReadonlySet<AuthorshipMode>;
|
|
3
4
|
export interface VoiceTagContentParams {
|
|
4
5
|
nodeIds: string[];
|
|
5
6
|
mode: AuthorshipMode;
|
|
6
7
|
accountId: string;
|
|
8
|
+
/** Optional format. When omitted, derived from each node's labels. */
|
|
9
|
+
format?: VoiceFormat;
|
|
7
10
|
}
|
|
8
11
|
export interface VoiceTagContentResult {
|
|
9
12
|
updated: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-tag-content.d.ts","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"voice-tag-content.d.ts","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"AAiBA,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,0BAA0B,GAC1B,0BAA0B,GAC1B,YAAY,GACZ,SAAS,CAAC;AAEd,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,cAAc,CAMvD,CAAC;AAEH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAcD,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CAuFhC"}
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* voice-tag-content — set `authorshipMode`
|
|
2
|
+
* voice-tag-content — set `authorshipMode` (and optionally `format`) on one
|
|
3
|
+
* or many content nodes.
|
|
3
4
|
*
|
|
4
5
|
* The tool routes through neo4j directly with an account-scoped MERGE so
|
|
5
6
|
* the operator can only mutate their own nodes. We do not go through
|
|
6
7
|
* `memory-update` because that tool re-embeds and re-validates on every
|
|
7
8
|
* call — overkill for a single-property authorship stamp.
|
|
9
|
+
*
|
|
10
|
+
* Format stamping (Task 462):
|
|
11
|
+
* - When `format` is provided: validated against FORMAT_VALUES and written
|
|
12
|
+
* to `n.format` alongside `authorshipMode`.
|
|
13
|
+
* - When `format` is omitted: a Cypher CASE expression derives format from
|
|
14
|
+
* each node's labels and `source` property, matching the `deriveFormat()`
|
|
15
|
+
* rule table in voice-corpus.ts. This keeps tag and ingest in lockstep.
|
|
8
16
|
*/
|
|
9
17
|
import { getSession } from "../lib/neo4j.js";
|
|
10
|
-
import { TAGGABLE_PRIMARY_LABELS } from "../lib/voice-corpus.js";
|
|
18
|
+
import { TAGGABLE_PRIMARY_LABELS, FORMAT_VALUES, } from "../lib/voice-corpus.js";
|
|
11
19
|
export const AUTHORSHIP_MODES = new Set([
|
|
12
20
|
"human-only",
|
|
13
21
|
"human-led-agent-assisted",
|
|
@@ -15,8 +23,19 @@ export const AUTHORSHIP_MODES = new Set([
|
|
|
15
23
|
"agent-only",
|
|
16
24
|
"unknown",
|
|
17
25
|
]);
|
|
26
|
+
// Cypher CASE expression that mirrors deriveFormat() for in-query derivation.
|
|
27
|
+
// Used when the caller does not supply an explicit format.
|
|
28
|
+
const FORMAT_DERIVE_CASE = `CASE
|
|
29
|
+
WHEN 'Message' IN labels(n) THEN 'text'
|
|
30
|
+
WHEN 'SocialPost' IN labels(n) THEN 'social-post'
|
|
31
|
+
WHEN ('KnowledgeDocument' IN labels(n) OR 'ConversationArchive' IN labels(n))
|
|
32
|
+
AND n.source = 'email' THEN 'email'
|
|
33
|
+
WHEN 'KnowledgeDocument' IN labels(n) AND size(coalesce(n.body, '')) > 1500 THEN 'article'
|
|
34
|
+
WHEN 'KnowledgeDocument' IN labels(n) THEN 'note'
|
|
35
|
+
ELSE 'text'
|
|
36
|
+
END`;
|
|
18
37
|
export async function voiceTagContent(params) {
|
|
19
|
-
const { nodeIds, mode, accountId } = params;
|
|
38
|
+
const { nodeIds, mode, accountId, format } = params;
|
|
20
39
|
if (!AUTHORSHIP_MODES.has(mode)) {
|
|
21
40
|
throw new Error(`voice-tag-content: invalid authorshipMode '${mode}'. Valid values: ${[
|
|
22
41
|
...AUTHORSHIP_MODES,
|
|
@@ -25,6 +44,9 @@ export async function voiceTagContent(params) {
|
|
|
25
44
|
if (!accountId) {
|
|
26
45
|
throw new Error("voice-tag-content: accountId is required");
|
|
27
46
|
}
|
|
47
|
+
if (format !== undefined && !FORMAT_VALUES.includes(format)) {
|
|
48
|
+
throw new Error(`voice-tag-content: invalid format '${format}'. Valid values: ${FORMAT_VALUES.join(", ")}`);
|
|
49
|
+
}
|
|
28
50
|
if (!Array.isArray(nodeIds) || nodeIds.length === 0) {
|
|
29
51
|
return { updated: 0, skipped: 0, skippedReasons: {} };
|
|
30
52
|
}
|
|
@@ -44,6 +66,10 @@ export async function voiceTagContent(params) {
|
|
|
44
66
|
// accidentally tagging document-path :Section rows AND parent
|
|
45
67
|
// `:Conversation` / `:AdminConversation` nodes (which are not
|
|
46
68
|
// body-bearing content).
|
|
69
|
+
//
|
|
70
|
+
// Format is either the caller-supplied constant ($format is non-null)
|
|
71
|
+
// or derived per-node via the CASE expression that mirrors
|
|
72
|
+
// deriveFormat() in voice-corpus.ts.
|
|
47
73
|
`UNWIND $ids AS id
|
|
48
74
|
MATCH (n) WHERE elementId(n) = id AND n.accountId = $accountId
|
|
49
75
|
WITH n
|
|
@@ -53,12 +79,14 @@ export async function voiceTagContent(params) {
|
|
|
53
79
|
WHERE parent.conversationIdentity IS NOT NULL
|
|
54
80
|
})
|
|
55
81
|
SET n.authorshipMode = $mode,
|
|
56
|
-
n.authorshipTaggedAt = $now
|
|
82
|
+
n.authorshipTaggedAt = $now,
|
|
83
|
+
n.format = CASE WHEN $format IS NOT NULL THEN $format ELSE ${FORMAT_DERIVE_CASE} END
|
|
57
84
|
RETURN elementId(n) AS id`, {
|
|
58
85
|
ids: batch,
|
|
59
86
|
accountId,
|
|
60
87
|
mode,
|
|
61
88
|
now,
|
|
89
|
+
format: format ?? null,
|
|
62
90
|
primaryLabels: [...TAGGABLE_PRIMARY_LABELS],
|
|
63
91
|
});
|
|
64
92
|
const matched = new Set(result.records.map((r) => r.get("id")));
|
|
@@ -74,7 +102,7 @@ export async function voiceTagContent(params) {
|
|
|
74
102
|
finally {
|
|
75
103
|
await session.close();
|
|
76
104
|
}
|
|
77
|
-
process.stderr.write(`[voice-tag] mode=${mode} accountId=${accountId} updated=${updated} skipped=${nodeIds.length - updated}\n`);
|
|
105
|
+
process.stderr.write(`[voice-tag] mode=${mode} format=${format ?? "derived"} accountId=${accountId} updated=${updated} skipped=${nodeIds.length - updated}\n`);
|
|
78
106
|
return {
|
|
79
107
|
updated,
|
|
80
108
|
skipped: nodeIds.length - updated,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"voice-tag-content.js","sourceRoot":"","sources":["../../src/tools/voice-tag-content.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,26 +2,35 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Boot + shape smoke test for the writer-craft MCP server.
|
|
4
4
|
*
|
|
5
|
-
* Verifies:
|
|
6
|
-
* 1.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
5
|
+
* Verifies without a live Neo4j connection:
|
|
6
|
+
* 1. Cypher forbidden-pattern gate (Task 458 fold-in): no NULLS LAST /
|
|
7
|
+
* NULLS FIRST / RETURNING patterns in the Cypher emitted by tool sources.
|
|
8
|
+
* 2. Every compiled tool module loads without throwing.
|
|
9
|
+
* 3. Every exported handler is a function with the expected name.
|
|
10
|
+
* 4. AUTHORSHIP_MODES enum covers the five spec-required values.
|
|
11
|
+
* 5. TAGGABLE_PRIMARY_LABELS covers the three content labels.
|
|
12
|
+
* 6. FORMAT_VALUES covers the six writing-format values (Task 462).
|
|
13
|
+
* 7. voiceCorpusWhereWithFormat() appends the format predicate.
|
|
14
|
+
* 8. deriveFormat() maps all node shapes to the correct format value.
|
|
15
|
+
* 9. voice-retrieve-conditioning returns the graceful-degradation shape
|
|
16
|
+
* {styleCard: null, exemplars: []} on empty accountId/userId.
|
|
17
|
+
* 10. voice-tag-content validates mode and format enums; returns zero counts
|
|
18
|
+
* on empty nodeIds.
|
|
19
|
+
* 11. voice-ingest-session-text: extractOperatorTurns() extracts real turns
|
|
20
|
+
* and excludes slash commands, paste blocks, and system reminders.
|
|
13
21
|
*
|
|
14
|
-
*
|
|
15
|
-
* graceful-degradation case never opens a session. The four other tools
|
|
16
|
-
* are not exercised against the database here; their unit test coverage
|
|
17
|
-
* is filed as a follow-up. This smoke harness catches import errors,
|
|
18
|
-
* accidental contract changes on the empty-corpus return shape, and
|
|
19
|
-
* structural drift on the exported enum.
|
|
20
|
-
*
|
|
21
|
-
* Exit code: 0 on pass, 1 on any assertion failure. Designed for CI
|
|
22
|
-
* gating and `npm run smoke` in the build pipeline.
|
|
22
|
+
* Exit code: 0 on pass, 1 on any assertion failure.
|
|
23
23
|
*/
|
|
24
24
|
import { strict as assert } from "node:assert";
|
|
25
|
+
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join, dirname } from "node:path";
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
|
+
import { readdirSync } from "node:fs";
|
|
30
|
+
|
|
31
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
32
|
+
const TOOLS_SRC = join(__dirname, "../src/tools");
|
|
33
|
+
const LIB_SRC = join(__dirname, "../src/lib");
|
|
25
34
|
|
|
26
35
|
const errors = [];
|
|
27
36
|
function check(name, fn) {
|
|
@@ -45,11 +54,57 @@ function check(name, fn) {
|
|
|
45
54
|
|
|
46
55
|
console.log("[writer-craft smoke] starting");
|
|
47
56
|
|
|
48
|
-
//
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// 1. Cypher forbidden-pattern gate (Task 458).
|
|
59
|
+
// No NULLS LAST / NULLS FIRST / RETURNING in tool source files.
|
|
60
|
+
// These patterns are Postgres-isms; neo4j-driver silently ignores or
|
|
61
|
+
// errors on them. The gate runs on .ts source files so it catches issues
|
|
62
|
+
// before compilation.
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
console.log("\n-- Cypher gate --");
|
|
65
|
+
const FORBIDDEN_PATTERNS = [/NULLS\s+LAST/i, /NULLS\s+FIRST/i, /\bRETURNING\b/];
|
|
66
|
+
|
|
67
|
+
function scanFile(filePath) {
|
|
68
|
+
const content = readFileSync(filePath, "utf-8");
|
|
69
|
+
const hits = [];
|
|
70
|
+
for (const pat of FORBIDDEN_PATTERNS) {
|
|
71
|
+
if (pat.test(content)) hits.push(pat.toString());
|
|
72
|
+
}
|
|
73
|
+
return hits;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function scanDir(dirPath) {
|
|
77
|
+
const allHits = {};
|
|
78
|
+
for (const entry of readdirSync(dirPath)) {
|
|
79
|
+
if (!entry.endsWith(".ts")) continue;
|
|
80
|
+
const filePath = join(dirPath, entry);
|
|
81
|
+
const hits = scanFile(filePath);
|
|
82
|
+
if (hits.length > 0) allHits[filePath] = hits;
|
|
83
|
+
}
|
|
84
|
+
return allHits;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await check("no forbidden Cypher patterns in tool sources", () => {
|
|
88
|
+
const toolHits = scanDir(TOOLS_SRC);
|
|
89
|
+
const libHits = scanDir(LIB_SRC);
|
|
90
|
+
const allHits = { ...toolHits, ...libHits };
|
|
91
|
+
const fileList = Object.keys(allHits);
|
|
92
|
+
assert.equal(
|
|
93
|
+
fileList.length,
|
|
94
|
+
0,
|
|
95
|
+
`Forbidden Cypher patterns found:\n${fileList.map((f) => ` ${f}: ${allHits[f].join(", ")}`).join("\n")}`,
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// 2. Module load + export shape checks.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
console.log("\n-- Module shapes --");
|
|
49
103
|
const tagMod = await import("../dist/tools/voice-tag-content.js");
|
|
50
104
|
const distilMod = await import("../dist/tools/voice-distil-profile.js");
|
|
51
105
|
const retrieveMod = await import("../dist/tools/voice-retrieve-conditioning.js");
|
|
52
106
|
const feedbackMod = await import("../dist/tools/voice-record-feedback.js");
|
|
107
|
+
const ingestMod = await import("../dist/tools/voice-ingest-session-text.js");
|
|
53
108
|
const corpusMod = await import("../dist/lib/voice-corpus.js");
|
|
54
109
|
|
|
55
110
|
await check("voice-tag-content exports voiceTagContent function", () => {
|
|
@@ -75,45 +130,111 @@ await check("voice-retrieve-conditioning exports voiceRetrieveConditioning funct
|
|
|
75
130
|
await check("voice-record-feedback exports voiceRecordFeedback function", () => {
|
|
76
131
|
assert.equal(typeof feedbackMod.voiceRecordFeedback, "function");
|
|
77
132
|
});
|
|
133
|
+
await check("voice-ingest-session-text exports required functions", () => {
|
|
134
|
+
assert.equal(typeof ingestMod.voiceIngestSessionText, "function");
|
|
135
|
+
assert.equal(typeof ingestMod.extractOperatorTurns, "function");
|
|
136
|
+
assert.equal(typeof ingestMod.extractOperatorText, "function");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// 3. voice-corpus exports — format axis (Task 462).
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
console.log("\n-- voice-corpus format exports --");
|
|
78
143
|
await check("VOICE_CORPUS_WHERE is non-empty string with both predicates", () => {
|
|
79
144
|
assert.equal(typeof corpusMod.VOICE_CORPUS_WHERE, "string");
|
|
80
145
|
assert.ok(corpusMod.VOICE_CORPUS_WHERE.includes("authorshipMode IN"));
|
|
81
146
|
assert.ok(corpusMod.VOICE_CORPUS_WHERE.includes("human-only"));
|
|
82
|
-
assert.ok(corpusMod.VOICE_CORPUS_WHERE.includes("human-led-agent-assisted"));
|
|
83
147
|
assert.ok(corpusMod.VOICE_CORPUS_WHERE.includes("Section"));
|
|
84
148
|
});
|
|
85
149
|
await check("TAGGABLE_PRIMARY_LABELS covers the three content labels", () => {
|
|
86
150
|
const labels = [...corpusMod.TAGGABLE_PRIMARY_LABELS];
|
|
87
151
|
assert.deepEqual(labels.sort(), ["KnowledgeDocument", "Message", "SocialPost"]);
|
|
88
152
|
});
|
|
153
|
+
await check("FORMAT_VALUES covers all six writing formats", () => {
|
|
154
|
+
const expected = ["text", "email", "social-post", "article", "marketing-copy", "note"];
|
|
155
|
+
const actual = [...corpusMod.FORMAT_VALUES].sort();
|
|
156
|
+
assert.deepEqual(actual.sort(), expected.sort());
|
|
157
|
+
});
|
|
158
|
+
await check("voiceCorpusWhereWithFormat appends format predicate", () => {
|
|
159
|
+
const result = corpusMod.voiceCorpusWhereWithFormat("email");
|
|
160
|
+
assert.ok(typeof result === "string");
|
|
161
|
+
assert.ok(result.includes(corpusMod.VOICE_CORPUS_WHERE));
|
|
162
|
+
assert.ok(result.includes("n.format = $format"));
|
|
163
|
+
assert.ok(result.includes("email") || result.includes("$format"));
|
|
164
|
+
});
|
|
89
165
|
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// 4. deriveFormat() unit tests — all derivation rules.
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
console.log("\n-- deriveFormat() rules --");
|
|
170
|
+
const { deriveFormat } = corpusMod;
|
|
171
|
+
|
|
172
|
+
await check("deriveFormat: :Message → text", () => {
|
|
173
|
+
assert.equal(deriveFormat(["Message"]), "text");
|
|
174
|
+
assert.equal(deriveFormat(["Message", "SomeOther"]), "text");
|
|
175
|
+
});
|
|
176
|
+
await check("deriveFormat: :SocialPost → social-post", () => {
|
|
177
|
+
assert.equal(deriveFormat(["SocialPost"]), "social-post");
|
|
178
|
+
});
|
|
179
|
+
await check("deriveFormat: :KnowledgeDocument {source:'email'} → email", () => {
|
|
180
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], "email"), "email");
|
|
181
|
+
});
|
|
182
|
+
await check("deriveFormat: :ConversationArchive {source:'email'} → email", () => {
|
|
183
|
+
assert.equal(deriveFormat(["ConversationArchive"], "email"), "email");
|
|
184
|
+
});
|
|
185
|
+
await check("deriveFormat: :KnowledgeDocument body > 1500 → article", () => {
|
|
186
|
+
const longBody = "x".repeat(1501);
|
|
187
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], null, longBody), "article");
|
|
188
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], "other", longBody), "article");
|
|
189
|
+
});
|
|
190
|
+
await check("deriveFormat: :KnowledgeDocument body = 1500 → note", () => {
|
|
191
|
+
const borderBody = "x".repeat(1500);
|
|
192
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], null, borderBody), "note");
|
|
193
|
+
});
|
|
194
|
+
await check("deriveFormat: :KnowledgeDocument body ≤ 1500 → note", () => {
|
|
195
|
+
const shortBody = "x".repeat(100);
|
|
196
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], null, shortBody), "note");
|
|
197
|
+
assert.equal(deriveFormat(["KnowledgeDocument"], undefined, undefined), "note");
|
|
198
|
+
});
|
|
199
|
+
await check("deriveFormat: :Section (conversation chunk) → text", () => {
|
|
200
|
+
assert.equal(deriveFormat(["Section"]), "text");
|
|
201
|
+
});
|
|
202
|
+
await check("deriveFormat: unknown label → text (fallback)", () => {
|
|
203
|
+
assert.equal(deriveFormat(["Unknown"]), "text");
|
|
204
|
+
assert.equal(deriveFormat([]), "text");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// 5. Graceful-degradation contract for voice-retrieve-conditioning.
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
console.log("\n-- graceful-degradation contracts --");
|
|
93
211
|
await check(
|
|
94
212
|
"voice-retrieve-conditioning returns empty shape on missing accountId",
|
|
95
213
|
async () => {
|
|
96
214
|
const result = await retrieveMod.voiceRetrieveConditioning({
|
|
97
215
|
accountId: "",
|
|
98
|
-
|
|
99
|
-
brief: { format: "short" },
|
|
216
|
+
userId: "u",
|
|
217
|
+
brief: { format: "text", length: "short" },
|
|
100
218
|
});
|
|
101
219
|
assert.deepEqual(result, { styleCard: null, exemplars: [] });
|
|
102
220
|
},
|
|
103
221
|
);
|
|
104
222
|
await check(
|
|
105
|
-
"voice-retrieve-conditioning returns empty shape on missing
|
|
223
|
+
"voice-retrieve-conditioning returns empty shape on missing userId",
|
|
106
224
|
async () => {
|
|
107
225
|
const result = await retrieveMod.voiceRetrieveConditioning({
|
|
108
226
|
accountId: "acc",
|
|
109
|
-
|
|
110
|
-
brief: { format: "long" },
|
|
227
|
+
userId: "",
|
|
228
|
+
brief: { format: "email", length: "long" },
|
|
111
229
|
});
|
|
112
230
|
assert.deepEqual(result, { styleCard: null, exemplars: [] });
|
|
113
231
|
},
|
|
114
232
|
);
|
|
115
233
|
|
|
116
|
-
//
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// 6. voice-tag-content enum validation.
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
console.log("\n-- voice-tag-content validation --");
|
|
117
238
|
await check("voice-tag-content rejects an invalid mode", async () => {
|
|
118
239
|
await assert.rejects(
|
|
119
240
|
() =>
|
|
@@ -125,6 +246,18 @@ await check("voice-tag-content rejects an invalid mode", async () => {
|
|
|
125
246
|
/invalid authorshipMode/,
|
|
126
247
|
);
|
|
127
248
|
});
|
|
249
|
+
await check("voice-tag-content rejects an invalid format", async () => {
|
|
250
|
+
await assert.rejects(
|
|
251
|
+
() =>
|
|
252
|
+
tagMod.voiceTagContent({
|
|
253
|
+
nodeIds: ["x"],
|
|
254
|
+
mode: "human-only",
|
|
255
|
+
accountId: "acc",
|
|
256
|
+
format: "bad-format",
|
|
257
|
+
}),
|
|
258
|
+
/invalid format/,
|
|
259
|
+
);
|
|
260
|
+
});
|
|
128
261
|
await check("voice-tag-content rejects empty accountId", async () => {
|
|
129
262
|
await assert.rejects(
|
|
130
263
|
() =>
|
|
@@ -145,6 +278,134 @@ await check("voice-tag-content returns zero counts on empty nodeIds (no DB hit)"
|
|
|
145
278
|
assert.deepEqual(result, { updated: 0, skipped: 0, skippedReasons: {} });
|
|
146
279
|
});
|
|
147
280
|
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// 7. voice-ingest-session-text: JSONL extraction (no DB required).
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
console.log("\n-- JSONL extraction --");
|
|
285
|
+
|
|
286
|
+
function makeJsonlLine(type, role, content) {
|
|
287
|
+
return JSON.stringify({ type, message: { role, content } });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
await check("extractOperatorText: real user turn is extracted", () => {
|
|
291
|
+
const record = { type: "user", message: { role: "user", content: "Hello world" } };
|
|
292
|
+
const result = ingestMod.extractOperatorText(record);
|
|
293
|
+
assert.equal(result, "Hello world");
|
|
294
|
+
});
|
|
295
|
+
await check("extractOperatorText: slash command is excluded", () => {
|
|
296
|
+
const record = { type: "user", message: { role: "user", content: "/end" } };
|
|
297
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
298
|
+
});
|
|
299
|
+
await check("extractOperatorText: system-reminder is excluded", () => {
|
|
300
|
+
const record = {
|
|
301
|
+
type: "user",
|
|
302
|
+
message: { role: "user", content: "<system-reminder>some injected text</system-reminder>" },
|
|
303
|
+
};
|
|
304
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
305
|
+
});
|
|
306
|
+
await check("extractOperatorText: paste block > 4096 chars is excluded", () => {
|
|
307
|
+
const record = {
|
|
308
|
+
type: "user",
|
|
309
|
+
message: { role: "user", content: "x".repeat(4097) },
|
|
310
|
+
};
|
|
311
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
312
|
+
});
|
|
313
|
+
await check("extractOperatorText: paste block = 4096 chars is excluded", () => {
|
|
314
|
+
const record = {
|
|
315
|
+
type: "user",
|
|
316
|
+
message: { role: "user", content: "x".repeat(4096) },
|
|
317
|
+
};
|
|
318
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
319
|
+
});
|
|
320
|
+
await check("extractOperatorText: paste block = 4095 chars is included", () => {
|
|
321
|
+
const body = "x".repeat(4095);
|
|
322
|
+
const record = { type: "user", message: { role: "user", content: body } };
|
|
323
|
+
assert.equal(ingestMod.extractOperatorText(record), body);
|
|
324
|
+
});
|
|
325
|
+
await check("extractOperatorText: assistant turn is excluded", () => {
|
|
326
|
+
const record = { type: "assistant", message: { role: "assistant", content: "Reply" } };
|
|
327
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
328
|
+
});
|
|
329
|
+
await check("extractOperatorText: tool_result-only content is excluded", () => {
|
|
330
|
+
const record = {
|
|
331
|
+
type: "user",
|
|
332
|
+
message: {
|
|
333
|
+
role: "user",
|
|
334
|
+
content: [{ type: "tool_result", tool_use_id: "x", content: "ok" }],
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
assert.equal(ingestMod.extractOperatorText(record), null);
|
|
338
|
+
});
|
|
339
|
+
await check("extractOperatorText: content array with text block is extracted", () => {
|
|
340
|
+
const record = {
|
|
341
|
+
type: "user",
|
|
342
|
+
message: {
|
|
343
|
+
role: "user",
|
|
344
|
+
content: [{ type: "text", text: "Hello from array" }],
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
assert.equal(ingestMod.extractOperatorText(record), "Hello from array");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Full JSONL fixture: 3 real turns + 2 assistant + 1 slash + 1 system-reminder.
|
|
351
|
+
// Expected: extractOperatorTurns returns 3 strings.
|
|
352
|
+
await check(
|
|
353
|
+
"extractOperatorTurns: fixture with 3 real turns + 4 excluded → 3 results",
|
|
354
|
+
async () => {
|
|
355
|
+
const tmp = mkdtempSync(join(tmpdir(), "smoke-ingest-"));
|
|
356
|
+
const jsonlPath = join(tmp, "session.jsonl");
|
|
357
|
+
const lines = [
|
|
358
|
+
// 3 real operator turns
|
|
359
|
+
makeJsonlLine("user", "user", "First real turn"),
|
|
360
|
+
makeJsonlLine("user", "user", "Second real turn"),
|
|
361
|
+
makeJsonlLine("user", "user", "Third real turn"),
|
|
362
|
+
// 2 assistant turns
|
|
363
|
+
makeJsonlLine("assistant", "assistant", "Agent reply 1"),
|
|
364
|
+
makeJsonlLine("assistant", "assistant", "Agent reply 2"),
|
|
365
|
+
// 1 slash command
|
|
366
|
+
makeJsonlLine("user", "user", "/end"),
|
|
367
|
+
// 1 system reminder
|
|
368
|
+
makeJsonlLine("user", "user", "<system-reminder>injected</system-reminder>"),
|
|
369
|
+
];
|
|
370
|
+
writeFileSync(jsonlPath, lines.join("\n") + "\n");
|
|
371
|
+
try {
|
|
372
|
+
const turns = await ingestMod.extractOperatorTurns(jsonlPath);
|
|
373
|
+
assert.equal(turns.length, 3, `expected 3 turns, got ${turns.length}`);
|
|
374
|
+
assert.equal(turns[0], "First real turn");
|
|
375
|
+
assert.equal(turns[1], "Second real turn");
|
|
376
|
+
assert.equal(turns[2], "Third real turn");
|
|
377
|
+
} finally {
|
|
378
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
// Second-run fixture: same lines written to same path → same 3 turns extracted.
|
|
384
|
+
// (Actual dedup happens in the DB via contentHash MERGE; extraction is idempotent.)
|
|
385
|
+
await check(
|
|
386
|
+
"extractOperatorTurns: re-running same JSONL extracts same turns (dedup is in DB)",
|
|
387
|
+
async () => {
|
|
388
|
+
const tmp = mkdtempSync(join(tmpdir(), "smoke-ingest-dedup-"));
|
|
389
|
+
const jsonlPath = join(tmp, "session.jsonl");
|
|
390
|
+
const lines = [
|
|
391
|
+
makeJsonlLine("user", "user", "Dedupe turn"),
|
|
392
|
+
makeJsonlLine("user", "user", "/skip-cmd"),
|
|
393
|
+
];
|
|
394
|
+
writeFileSync(jsonlPath, lines.join("\n") + "\n");
|
|
395
|
+
try {
|
|
396
|
+
const turns1 = await ingestMod.extractOperatorTurns(jsonlPath);
|
|
397
|
+
const turns2 = await ingestMod.extractOperatorTurns(jsonlPath);
|
|
398
|
+
assert.deepEqual(turns1, turns2, "idempotent: same JSONL gives same turns");
|
|
399
|
+
assert.equal(turns1.length, 1);
|
|
400
|
+
} finally {
|
|
401
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Final summary.
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
148
409
|
if (errors.length > 0) {
|
|
149
410
|
console.error(`\n[writer-craft smoke] FAILED — ${errors.length} assertion(s)`);
|
|
150
411
|
process.exit(1);
|