@crossworks/content-core 0.232.55 → 0.232.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossworks/content-core",
3
- "version": "0.232.55",
3
+ "version": "0.232.56",
4
4
  "description": "Browser-safe content logic — markdown, blocks, pages, tables, formulas — shared by the server and any client. Zero server deps by design: nothing here may reach @mantle/db, node-only APIs, or the network (the jackdaw-repo-split P0 boundary).",
5
5
  "exports": {
6
6
  "./markdown": "./src/markdown-to-doc.ts",
@@ -1,5 +1,13 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { categoryLabel, moodDisplay, normalizeEntryDate } from './journal-options';
2
+ import {
3
+ AGENT_KIND_KEYS,
4
+ KIND_KEYS,
5
+ USER_KIND_KEYS,
6
+ kindLabel,
7
+ kindLane,
8
+ legacyCategoryToKind,
9
+ normalizeEntryDate,
10
+ } from './journal-options';
3
11
 
4
12
  describe('normalizeEntryDate', () => {
5
13
  it('passes through a full ISO timestamp (canonicalised)', () => {
@@ -32,26 +40,49 @@ describe('normalizeEntryDate', () => {
32
40
  });
33
41
  });
34
42
 
35
- describe('moodDisplay', () => {
36
- it('maps a known mood key to emoji + label', () => {
37
- expect(moodDisplay('grateful')).toEqual({ emoji: '🙏', label: 'Grateful' });
43
+ describe('kind vocabulary', () => {
44
+ it('splits cleanly into the two lanes', () => {
45
+ expect(USER_KIND_KEYS).toEqual(['identity', 'context', 'preference', 'goal']);
46
+ expect(AGENT_KIND_KEYS).toEqual(['lesson', 'expectation', 'gap']);
47
+ expect(KIND_KEYS).toEqual([...USER_KIND_KEYS, ...AGENT_KIND_KEYS]);
38
48
  });
39
- it('tolerates an unknown/free-text mood (no emoji, raw label)', () => {
40
- expect(moodDisplay('zonked')).toEqual({ emoji: '', label: 'zonked' });
49
+ });
50
+
51
+ describe('kindLabel', () => {
52
+ it('maps a known kind key to its label', () => {
53
+ expect(kindLabel('expectation')).toBe('Expectation');
54
+ expect(kindLabel('gap')).toBe('Open question');
55
+ });
56
+ it('title-cases an unknown/free-text kind', () => {
57
+ expect(kindLabel('hobbies')).toBe('Hobbies');
41
58
  });
42
- it('returns null for no mood', () => {
43
- expect(moodDisplay(null)).toBeNull();
59
+ it('returns null for no kind', () => {
60
+ expect(kindLabel(null)).toBeNull();
44
61
  });
45
62
  });
46
63
 
47
- describe('categoryLabel', () => {
48
- it('maps a known category key to its label', () => {
49
- expect(categoryLabel('faith')).toBe('Faith');
64
+ describe('kindLane', () => {
65
+ it('routes agent kinds to the agent lane', () => {
66
+ expect(kindLane('lesson')).toBe('agent');
67
+ expect(kindLane('expectation')).toBe('agent');
68
+ expect(kindLane('gap')).toBe('agent');
50
69
  });
51
- it('title-cases an unknown/free-text category', () => {
52
- expect(categoryLabel('hobbies')).toBe('Hobbies');
70
+ it('routes user kinds — and anything unknown — to the user lane', () => {
71
+ expect(kindLane('identity')).toBe('user');
72
+ expect(kindLane('made-up')).toBe('user');
73
+ expect(kindLane(null)).toBe('user');
74
+ });
75
+ });
76
+
77
+ describe('legacyCategoryToKind', () => {
78
+ it('carries identity and goal over', () => {
79
+ expect(legacyCategoryToKind('identity')).toBe('identity');
80
+ expect(legacyCategoryToKind('goal')).toBe('goal');
53
81
  });
54
- it('returns null for no category', () => {
55
- expect(categoryLabel(null)).toBeNull();
82
+ it('maps every other legacy life area (and none) to context', () => {
83
+ expect(legacyCategoryToKind('work')).toBe('context');
84
+ expect(legacyCategoryToKind('faith')).toBe('context');
85
+ expect(legacyCategoryToKind('emotion')).toBe('context');
86
+ expect(legacyCategoryToKind(null)).toBe('context');
56
87
  });
57
88
  });
@@ -1,65 +1,79 @@
1
1
  /**
2
- * Browser-safe leaf for Journal option lists (moods + categories).
2
+ * Browser-safe leaf for Journal option lists (kinds + gap statuses).
3
3
  *
4
4
  * These constants are needed both server-side (CRUD, extractor framing, the
5
- * identity-context distiller) and client-side (the /journal editor + filters).
6
- * They live in their own module — with NO `@mantle/db` import — so a client
7
- * component can pull them in without dragging `postgres` into the browser
8
- * bundle. Same pattern as `contacts-format.ts`. `journal.ts` re-exports these.
5
+ * identity/working-notes distillers) and client-side (the /journal editor +
6
+ * filters). They live in their own module — with NO `@mantle/db` import — so a
7
+ * client component can pull them in without dragging `postgres` into the
8
+ * browser bundle. Same pattern as `contacts-format.ts`. `journal.ts`
9
+ * re-exports these.
10
+ *
11
+ * Journal v2 (2026-08): the mood palette and life-area categories are GONE.
12
+ * A journal entry is now one of a small set of KINDS across two lanes:
13
+ * - user lane: durable self-knowledge that grounds every agent turn
14
+ * - agent lane: what an agent has learned about doing its job well
15
+ * Legacy rows keep their old `mood`/`category` jsonb values; nothing reads
16
+ * `mood` anymore, and `category` maps to a kind at read time (see
17
+ * `legacyCategoryToKind`). No migration, no backfill.
9
18
  */
10
19
 
11
- /** Curated mood palette (emoji + label). Stored as the bare key string in
12
- * `data.mood`; the UI maps key emoji + label. Free text is tolerated on
13
- * read, but the picker offers these. */
14
- export const MOODS = [
15
- { key: 'happy', label: 'Happy', emoji: '😀' },
16
- { key: 'grateful', label: 'Grateful', emoji: '🙏' },
17
- { key: 'calm', label: 'Calm', emoji: '😌' },
18
- { key: 'excited', label: 'Excited', emoji: '🤩' },
19
- { key: 'hopeful', label: 'Hopeful', emoji: '🌱' },
20
- { key: 'reflective', label: 'Reflective', emoji: '🤔' },
21
- { key: 'tired', label: 'Tired', emoji: '😮‍💨' },
22
- { key: 'anxious', label: 'Anxious', emoji: '😟' },
23
- { key: 'sad', label: 'Sad', emoji: '😔' },
24
- { key: 'angry', label: 'Angry', emoji: '😠' },
25
- ] as const;
26
-
27
- export type MoodKey = (typeof MOODS)[number]['key'];
28
- export const MOOD_KEYS: readonly string[] = MOODS.map((m) => m.key);
20
+ /** The lane a kind belongs to. User-lane entries feed the "# About the user"
21
+ * block; agent-lane entries feed the per-turn "# Working notes" block. */
22
+ export type JournalLane = 'user' | 'agent';
29
23
 
30
- /** Life areas the entry speaks to. Drives the identity block's grouping
31
- * ("## Work", "## Faith", …) and the list filter. */
32
- export const CATEGORIES = [
33
- { key: 'identity', label: 'Identity' },
34
- { key: 'work', label: 'Work' },
35
- { key: 'family', label: 'Family' },
36
- { key: 'relationships', label: 'Relationships' },
37
- { key: 'faith', label: 'Faith' },
38
- { key: 'health', label: 'Health' },
39
- { key: 'emotion', label: 'Emotion' },
40
- { key: 'goal', label: 'Goal' },
41
- { key: 'reflection', label: 'Reflection' },
24
+ /** The full kind vocabulary. Stored as the bare key string in `data.kind`;
25
+ * free text is tolerated on read, but pickers/tools offer these. */
26
+ export const KINDS = [
27
+ // ── user lane: who the user/org is and what they want ──────────────────
28
+ { key: 'identity', lane: 'user', label: 'Identity' },
29
+ { key: 'context', lane: 'user', label: 'Context' },
30
+ { key: 'preference', lane: 'user', label: 'Preference' },
31
+ { key: 'goal', lane: 'user', label: 'Goal' },
32
+ // ── agent lane: what an agent has learned about doing its job ──────────
33
+ { key: 'lesson', lane: 'agent', label: 'Lesson' },
34
+ { key: 'expectation', lane: 'agent', label: 'Expectation' },
35
+ { key: 'gap', lane: 'agent', label: 'Open question' },
42
36
  ] as const;
43
37
 
44
- export type CategoryKey = (typeof CATEGORIES)[number]['key'];
45
- export const CATEGORY_KEYS: readonly string[] = CATEGORIES.map((c) => c.key);
38
+ export type KindKey = (typeof KINDS)[number]['key'];
39
+ export const KIND_KEYS: readonly string[] = KINDS.map((k) => k.key);
40
+ export const USER_KIND_KEYS: readonly string[] = KINDS.filter((k) => k.lane === 'user').map(
41
+ (k) => k.key,
42
+ );
43
+ export const AGENT_KIND_KEYS: readonly string[] = KINDS.filter((k) => k.lane === 'agent').map(
44
+ (k) => k.key,
45
+ );
46
46
 
47
- /** Mood key display (emoji + label), tolerant of free-text/unknown values. */
48
- export function moodDisplay(key: string | null): { emoji: string; label: string } | null {
49
- if (!key) return null;
50
- const found = MOODS.find((m) => m.key === key);
51
- if (found) return { emoji: found.emoji, label: found.label };
52
- return { emoji: '', label: key };
53
- }
47
+ /** Gap-entry lifecycle. Only entries with kind='gap' carry a status. */
48
+ export const GAP_STATUSES = ['open', 'resolved'] as const;
49
+ export type GapStatus = (typeof GAP_STATUSES)[number];
54
50
 
55
- /** Category key → human label, tolerant of free-text/unknown values. */
56
- export function categoryLabel(key: string | null): string | null {
51
+ /** Kind key → human label, tolerant of free-text/unknown values. */
52
+ export function kindLabel(key: string | null): string | null {
57
53
  if (!key) return null;
58
- const found = CATEGORIES.find((c) => c.key === key);
54
+ const found = KINDS.find((k) => k.key === key);
59
55
  if (found) return found.label;
60
56
  return key.charAt(0).toUpperCase() + key.slice(1);
61
57
  }
62
58
 
59
+ /** Which lane a kind belongs to. Unknown/free-text kinds read as user lane —
60
+ * the safe default (they render as user self-knowledge, never as agent
61
+ * working notes). */
62
+ export function kindLane(key: string | null): JournalLane {
63
+ const found = KINDS.find((k) => k.key === key);
64
+ return found?.lane ?? 'user';
65
+ }
66
+
67
+ /** Map a legacy pre-v2 `category` value to a kind, for rows written before
68
+ * the kind vocabulary existed. `identity`/`goal` carry over; every other
69
+ * life area (work, family, faith, health, emotion, …) reads as `context`.
70
+ * Only consulted when a row has no `kind`. */
71
+ export function legacyCategoryToKind(category: string | null): KindKey {
72
+ if (category === 'identity') return 'identity';
73
+ if (category === 'goal') return 'goal';
74
+ return 'context';
75
+ }
76
+
63
77
  /**
64
78
  * Normalise a user/agent-supplied entry date to a canonical ISO-8601 string,
65
79
  * or return null if it isn't a real date. Stored `entry_date` is later cast to