@crossworks/content-core 0.232.350 → 0.232.354

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.350",
3
+ "version": "0.232.354",
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
  "./app-nav": "./src/app-nav.ts",
@@ -27,10 +27,11 @@
27
27
  "./table-formula": "./src/table-formula.ts",
28
28
  "./table-formula-mathjs": "./src/table-formula-mathjs.ts",
29
29
  "./table-model": "./src/table-model.ts",
30
- "./thinking-tiers": "./src/thinking-tiers.ts"
30
+ "./thinking-tiers": "./src/thinking-tiers.ts",
31
+ "./tree": "./src/tree.ts"
31
32
  },
32
33
  "dependencies": {
33
- "@mantle/client-types": "npm:@crossworks/client-types@0.232.350",
34
+ "@mantle/client-types": "npm:@crossworks/client-types@0.232.354",
34
35
  "marked": "^18.0.11",
35
36
  "mathjs": "15.2.0"
36
37
  },
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { TREE_KIND_SPECS, TREE_KINDS, TREE_MAX_DEPTH } from '@mantle/client-types/tree';
3
+ import {
4
+ clampTreePath,
5
+ effectiveLevel,
6
+ isTreeFolderPathAllowed,
7
+ treeDepth,
8
+ treeFolderChain,
9
+ treeKindOfPath,
10
+ treeMoveDepth,
11
+ treeParentPath,
12
+ } from './tree';
13
+
14
+ describe('tree path math', () => {
15
+ it('finds the kind from the root label', () => {
16
+ expect(treeKindOfPath('files.clients.acme')).toBe('files');
17
+ expect(treeKindOfPath('draw')).toBe('draw');
18
+ expect(treeKindOfPath('documentation.x')).toBeNull();
19
+ expect(treeKindOfPath('')).toBeNull();
20
+ });
21
+
22
+ it('measures depth below the root', () => {
23
+ expect(treeDepth('files')).toBe(0);
24
+ expect(treeDepth('files.a')).toBe(1);
25
+ expect(treeDepth('files.a.b.c')).toBe(3);
26
+ });
27
+
28
+ it('walks parents and the folder chain', () => {
29
+ expect(treeParentPath('files.a.b')).toBe('files.a');
30
+ expect(treeParentPath('files')).toBe('files');
31
+ expect(treeFolderChain('files.a.b')).toEqual(['files.a', 'files.a.b']);
32
+ expect(treeFolderChain('files')).toEqual([]);
33
+ });
34
+
35
+ it('allows folders at depth 1..3 only', () => {
36
+ expect(isTreeFolderPathAllowed('files')).toBe(false);
37
+ expect(isTreeFolderPathAllowed('files.a')).toBe(true);
38
+ expect(isTreeFolderPathAllowed('files.a.b.c')).toBe(true);
39
+ expect(isTreeFolderPathAllowed('files.a.b.c.d')).toBe(false);
40
+ });
41
+
42
+ it('clamps a deep path into the third level', () => {
43
+ expect(clampTreePath('files.a.b.c.d.e')).toBe('files.a.b.c');
44
+ expect(clampTreePath('files.a')).toBe('files.a');
45
+ });
46
+
47
+ it('measures a moved subtree', () => {
48
+ expect(treeMoveDepth('files.a', 2)).toBe(3);
49
+ expect(treeMoveDepth('files.a.b', 2)).toBeGreaterThan(TREE_MAX_DEPTH);
50
+ });
51
+
52
+ it('has one spec per kind with distinct roots', () => {
53
+ const roots = TREE_KINDS.map((k) => TREE_KIND_SPECS[k].root);
54
+ expect(new Set(roots).size).toBe(TREE_KINDS.length);
55
+ for (const k of TREE_KINDS) {
56
+ expect(TREE_KIND_SPECS[k].kind).toBe(k);
57
+ expect(TREE_KIND_SPECS[k].sorts.length).toBeGreaterThan(0);
58
+ }
59
+ });
60
+ });
61
+
62
+ describe('the effective level', () => {
63
+ it('is the more open of an item’s own level and its folder’s share', () => {
64
+ expect(effectiveLevel('admin', null)).toBe('admin');
65
+ expect(effectiveLevel('admin', 'team')).toBe('team');
66
+ expect(effectiveLevel('admin', 'client')).toBe('client');
67
+ expect(effectiveLevel('team', 'client')).toBe('client');
68
+ expect(effectiveLevel('client', 'team')).toBe('client');
69
+ // A public item stays public in a client folder: folding its text for the
70
+ // public reader is safe for the client reader too.
71
+ expect(effectiveLevel('public', 'client')).toBe('public');
72
+ });
73
+ });
package/src/tree.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Path math for the item tree (@mantle/client-types/tree). A folder's or an
3
+ * item's place is an ltree path under its kind's root: `files` is the root,
4
+ * `files.clients` a top-level folder, `files.clients.acme.contracts` the
5
+ * deepest a folder may sit. Pure, so the brain and every client agree on
6
+ * depth, parents and crumbs without a round trip.
7
+ */
8
+ import {
9
+ TREE_KIND_SPECS,
10
+ TREE_KINDS,
11
+ TREE_MAX_DEPTH,
12
+ type TreeKind,
13
+ type TreeShareLevel,
14
+ } from '@mantle/client-types/tree';
15
+ import type { AccessLevel } from '@mantle/client-types';
16
+
17
+ const LEVEL_RANK: Record<AccessLevel, number> = { public: 0, client: 1, team: 2, admin: 3 };
18
+
19
+ /**
20
+ * The level an item is read at: the more open of its own level and the share
21
+ * it inherits from a folder (public < client < team < admin). This is the
22
+ * level its pill shows, its embeds follow and its indexed page text is
23
+ * folded for. Folding for the more open reader is safe for every reader of
24
+ * the row: what a public or client reader may see, every reader above may
25
+ * too. (Which ROLES read the row is the database's union of the two:
26
+ * nodes_viewer_read, migration 0204.)
27
+ */
28
+ export function effectiveLevel(
29
+ own: AccessLevel,
30
+ inherited: TreeShareLevel | null | undefined,
31
+ ): AccessLevel {
32
+ if (!inherited) return own;
33
+ return LEVEL_RANK[inherited] < LEVEL_RANK[own] ? inherited : own;
34
+ }
35
+
36
+ function labels(path: string): string[] {
37
+ return path ? path.split('.') : [];
38
+ }
39
+
40
+ /** The kind whose root the path sits under, or null outside every root. */
41
+ export function treeKindOfPath(path: string): TreeKind | null {
42
+ const root = labels(path)[0];
43
+ return TREE_KINDS.find((k) => TREE_KIND_SPECS[k].root === root) ?? null;
44
+ }
45
+
46
+ /** How deep below its root a path is: 0 for the root itself, 1 for a
47
+ * top-level folder. */
48
+ export function treeDepth(path: string): number {
49
+ return Math.max(0, labels(path).length - 1);
50
+ }
51
+
52
+ /** The parent path; the root is its own parent. */
53
+ export function treeParentPath(path: string): string {
54
+ const parts = labels(path);
55
+ return parts.length <= 1 ? path : parts.slice(0, -1).join('.');
56
+ }
57
+
58
+ /** The folder paths from the top level down to `path` itself, root excluded:
59
+ * `files.a.b` gives `['files.a', 'files.a.b']`. */
60
+ export function treeFolderChain(path: string): string[] {
61
+ const parts = labels(path);
62
+ const out: string[] = [];
63
+ for (let i = 2; i <= parts.length; i++) out.push(parts.slice(0, i).join('.'));
64
+ return out;
65
+ }
66
+
67
+ /** Whether a folder may live at `path` (its depth is within the limit). */
68
+ export function isTreeFolderPathAllowed(path: string): boolean {
69
+ const depth = treeDepth(path);
70
+ return depth >= 1 && depth <= TREE_MAX_DEPTH;
71
+ }
72
+
73
+ /**
74
+ * Cut a folder path that would sit too deep back to the deepest allowed
75
+ * level: the extra levels are lifted into the third. Inbound paths use it (an
76
+ * agent's `mkdir -p`, the crawler, Accept), so a deep chain lands in the
77
+ * deepest folder instead of failing.
78
+ */
79
+ export function clampTreePath(path: string): string {
80
+ const parts = labels(path);
81
+ return parts.slice(0, TREE_MAX_DEPTH + 1).join('.');
82
+ }
83
+
84
+ /** A moved folder subtree's deepest depth: `subtreeDepth` levels (1 for a
85
+ * folder with no subfolders) placed under `targetPath`. */
86
+ export function treeMoveDepth(targetPath: string, subtreeDepth: number): number {
87
+ return treeDepth(targetPath) + subtreeDepth;
88
+ }