@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.
Files changed (66) hide show
  1. package/dist/index.js +13 -4
  2. package/package.json +1 -1
  3. package/payload/platform/neo4j/schema.cypher +49 -10
  4. package/payload/platform/plugins/docs/references/voice-mirror-guide.md +18 -16
  5. package/payload/platform/plugins/email/skills/email-composition/SKILL.md +1 -1
  6. package/payload/platform/plugins/memory/mcp/dist/index.js +1 -1
  7. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  8. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts +11 -2
  9. package/payload/platform/plugins/memory/mcp/dist/lib/section-types.d.ts.map +1 -1
  10. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts +2 -0
  11. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.d.ts.map +1 -0
  12. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js +95 -0
  13. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-ingest-body-server-sliced.test.js.map +1 -0
  14. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts +1 -0
  15. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.d.ts.map +1 -1
  16. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js +98 -15
  17. package/payload/platform/plugins/memory/mcp/dist/tools/memory-ingest.js.map +1 -1
  18. package/payload/platform/plugins/memory/skills/document-ingest/SKILL.md +3 -2
  19. package/payload/platform/plugins/substack-import/skills/substack-import/SKILL.md +1 -0
  20. package/payload/platform/scripts/setup-account.sh +8 -0
  21. package/payload/platform/scripts/verify-skill-tool-surface.sh +27 -8
  22. package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +1 -1
  23. package/payload/premium-plugins/writer-craft/PLUGIN.md +11 -7
  24. package/payload/premium-plugins/writer-craft/hooks/__tests__/voice-session-end-text.test.sh +284 -0
  25. package/payload/premium-plugins/writer-craft/hooks/voice-session-end-text.sh +75 -0
  26. package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts +3 -0
  27. package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.d.ts.map +1 -0
  28. package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js +58 -0
  29. package/payload/premium-plugins/writer-craft/mcp/dist/cli/ingest-session-text.js.map +1 -0
  30. package/payload/premium-plugins/writer-craft/mcp/dist/index.d.ts.map +1 -1
  31. package/payload/premium-plugins/writer-craft/mcp/dist/index.js +95 -18
  32. package/payload/premium-plugins/writer-craft/mcp/dist/index.js.map +1 -1
  33. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts +47 -6
  34. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.d.ts.map +1 -1
  35. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js +69 -6
  36. package/payload/premium-plugins/writer-craft/mcp/dist/lib/voice-corpus.js.map +1 -1
  37. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts +8 -2
  38. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.d.ts.map +1 -1
  39. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js +82 -53
  40. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-distil-profile.js.map +1 -1
  41. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts +23 -0
  42. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.d.ts.map +1 -0
  43. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js +191 -0
  44. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-ingest-session-text.js.map +1 -0
  45. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts +6 -0
  46. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.d.ts.map +1 -1
  47. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js +14 -6
  48. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-record-feedback.js.map +1 -1
  49. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts +12 -1
  50. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.d.ts.map +1 -1
  51. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js +23 -12
  52. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-retrieve-conditioning.js.map +1 -1
  53. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts +3 -0
  54. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.d.ts.map +1 -1
  55. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js +33 -5
  56. package/payload/premium-plugins/writer-craft/mcp/dist/tools/voice-tag-content.js.map +1 -1
  57. package/payload/premium-plugins/writer-craft/mcp/scripts/smoke.mjs +289 -28
  58. package/payload/premium-plugins/writer-craft/mcp/src/cli/ingest-session-text.ts +65 -0
  59. package/payload/premium-plugins/writer-craft/mcp/src/index.ts +108 -18
  60. package/payload/premium-plugins/writer-craft/mcp/src/lib/voice-corpus.ts +78 -6
  61. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-distil-profile.ts +115 -56
  62. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-ingest-session-text.ts +229 -0
  63. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-record-feedback.ts +20 -6
  64. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-retrieve-conditioning.ts +38 -13
  65. package/payload/premium-plugins/writer-craft/mcp/src/tools/voice-tag-content.ts +42 -5
  66. 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 human-only corpus
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 human-only nodes.
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 { VOICE_CORPUS_WHERE } from "../lib/voice-corpus.js";
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 k = brief.format === "long" ? 15 : 5;
37
+ const length = brief.length ?? "short";
38
+ const k = length === "long" ? 15 : 5;
31
39
  const tokenBudget = params.tokenBudget ??
32
- (brief.format === "long" ? DEFAULT_TOKEN_BUDGET_LONG : DEFAULT_TOKEN_BUDGET_SHORT);
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 filter when brief.topic is set.
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 ${VOICE_CORPUS_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 ${VOICE_CORPUS_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 ?? brief.format} styleCardBytes=${styleCard?.length ?? 0} exemplarCount=${exemplars.length} tokenBudget=${tokenBudget}\n`);
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"voice-retrieve-conditioning.js","sourceRoot":"","sources":["../../src/tools/voice-retrieve-conditioning.ts"],"names":[],"mappings":"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"}
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":"AAWA,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;CACnB;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;AAED,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CA4EhC"}
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` on one or many content nodes.
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":"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"}
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. Every compiled tool module loads without throwing.
7
- * 2. Every exported handler is a function with the expected name.
8
- * 3. AUTHORSHIP_MODES enum covers the five spec-required values.
9
- * 4. TAGGABLE_PRIMARY_LABELS covers the four content labels.
10
- * 5. `voice-retrieve-conditioning` returns the graceful-degradation
11
- * shape `{styleCard: null, exemplars: []}` when accountId is empty
12
- * (the contract the calling drafting skills depend on).
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
- * Runs with no external dependencies and no neo4j connection — the
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
- // 1. Modules load.
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
- // 2. Graceful-degradation contract — the drafting skills depend on this.
91
- // Empty accountId/adminUserId path returns the empty shape without
92
- // opening a neo4j session. This is the load-bearing contract.
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
- adminUserId: "u",
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 adminUserId",
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
- adminUserId: "",
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
- // 3. voice-tag-content validates the mode enum before any DB work.
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);