@elinpf/dsh-ops-tool-trace 0.1.0

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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Invariant companion for @elinpf/dsh-ops-tool-trace.
3
+ *
4
+ * @module @elinpf/dsh-ops-tool-trace/invariant
5
+ */
6
+ /** Cordis companion plugin name. */
7
+ declare const name = "ops-trace-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - Context carrying the invariant service.
13
+ * @returns a promise resolving after registration.
14
+ */
15
+ declare const apply: (ctx: any) => Promise<void>;
16
+ export { apply, inject, name };
17
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Invariant companion for @elinpf/dsh-ops-tool-trace.
3
+ *
4
+ * @module @elinpf/dsh-ops-tool-trace/invariant
5
+ */
6
+ const PACKAGE_NAME = '@elinpf/dsh-ops-tool-trace';
7
+ /** Cordis companion plugin name. */
8
+ const name = 'ops-trace-invariant';
9
+ /** Service required before the companion can reserve package ownership. */
10
+ const inject = ['invariants'];
11
+ /**
12
+ * No runtime invariant: this tool owns no independent durable shape beyond
13
+ * the session events it appends. The projection fold is pure and stateless;
14
+ * accepted mutations are checked by the tool's state-machine validation
15
+ * before they reach the log, and the fold is idempotent.
16
+ */
17
+ const install = () => { };
18
+ /**
19
+ * Register this package's invariant companion.
20
+ * @param ctx - Context carrying the invariant service.
21
+ * @returns a promise resolving after registration.
22
+ */
23
+ const apply = async (ctx) => {
24
+ ctx.invariants.register(PACKAGE_NAME, install);
25
+ };
26
+ export { apply, inject, name };
27
+ //# sourceMappingURL=invariant.js.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Runtime home of the node-status vocabulary (moved out of types.ts, which is
3
+ * types-only). The zod projection schema, the tool's output JSON schema, and
4
+ * the status_filter enum all derive from NODE_STATUSES — one source of truth.
5
+ *
6
+ * @module @elinpf/dsh-ops-tool-trace
7
+ */
8
+ import type { ForestState, TreeState } from './types.js';
9
+ /**
10
+ * The six node statuses from the state machine — the single source of truth
11
+ * for the status set.
12
+ *
13
+ * - `goal` — a stable target (milestone or final goal); uses dashed connectors
14
+ * - `pending` — a step not yet started; uses thin dashed connectors
15
+ * - `in_progress` — actively being worked on
16
+ * - `done` — completed
17
+ * - `dead_end` — proven unviable; NOT a terminal state (can re-explore)
18
+ * - `resolved` — final goal achieved; terminal state
19
+ */
20
+ export declare const NODE_STATUSES: readonly ["goal", "pending", "in_progress", "done", "dead_end", "resolved"];
21
+ export type NodeStatus = typeof NODE_STATUSES[number];
22
+ /**
23
+ * The active tree is the last unresolved one, or the last tree if all are resolved.
24
+ */
25
+ export declare function activeTree(forest: ForestState): TreeState | null;
26
+ //# sourceMappingURL=node-status.d.ts.map
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Reminder rules for the trace tool.
3
+ *
4
+ * Before this module existed, each reminder check re-parsed the full session
5
+ * event history on every pre-step — including JSON-parsing every historical
6
+ * tool call's arguments — and hand-rolled its own latch map. The latch-reset
7
+ * infinite loop came from exactly that arrangement: replaying history and
8
+ * mutating latch state in the same function.
9
+ *
10
+ * Now: {@link buildReminderContext} derives a small context once (step
11
+ * positions from the event log — no argument parsing — and the live tree
12
+ * from the SessionForestStore, which already owns it), and rules are pure
13
+ * functions of that context. {@link ReminderLatch} is the single latch
14
+ * abstraction: a monotonic per-session version with a minimum gap and a
15
+ * fire cap.
16
+ *
17
+ * @module @elinpf/dsh-ops-tool-trace/reminders
18
+ */
19
+ import type { ForestState, TreeState } from './types.js';
20
+ import type { SessionForestStore } from './session-forests.js';
21
+ /** What a reminder rule sees. Derived once per pre-step, shared by all rules. */
22
+ export interface ReminderContext {
23
+ sessionId: string;
24
+ /** Latest step position, encoded as turn * 1000 + step. */
25
+ currentStep: number;
26
+ /** Step position of the last trace call; 0 = never called. */
27
+ lastTraceStep: number;
28
+ /** The live forest (from the store, not a re-fold). */
29
+ forest: ForestState;
30
+ /** The active tree — null when no investigation exists. */
31
+ tree: TreeState | null;
32
+ }
33
+ /**
34
+ * Derive the reminder context for one pre-step. Returns null when there is no
35
+ * session event stream to judge from.
36
+ */
37
+ export declare function buildReminderContext(agent: unknown, store: SessionForestStore): ReminderContext | null;
38
+ /**
39
+ * One latch abstraction for all reminder rules: fire when `version` has
40
+ * advanced at least the required gap since the last fire, at most
41
+ * `maxFires` times per session. Because the version is an input (computed
42
+ * from the context, never reset by replaying history), re-evaluating the
43
+ * same state is always idempotent.
44
+ *
45
+ * `minGap` may be a function of the number of previous fires, so a rule can
46
+ * back off instead of going permanently silent (the 2026-08-27 live trial: a
47
+ * fixed cap of 5 went quiet after step ~2070, then 36 trace-less steps
48
+ * without a nudge). The idle rule doubles its gap after each fire with a
49
+ * 40-step ceiling, and resets the backoff when the agent answers a reminder
50
+ * (see createIdleRule). Anti-spam is about frequency, not a total budget.
51
+ */
52
+ export declare class ReminderLatch {
53
+ private readonly minGap;
54
+ private readonly maxFires;
55
+ private readonly last;
56
+ constructor(minGap: number | ((fires: number) => number), maxFires: number);
57
+ /** The version of one session's last fire, for compliance checks. */
58
+ firedAt(sessionId: string): number | undefined;
59
+ /** Forget one session's fire history. */
60
+ reset(sessionId: string): void;
61
+ shouldFire(sessionId: string, version: number): boolean;
62
+ }
63
+ /**
64
+ * The idle rule: nudge when an active investigation hasn't updated trace in
65
+ * 5+ steps. Silent when there is no tree or the tree is resolved.
66
+ */
67
+ export declare function createIdleRule(latch: ReminderLatch, gapSteps?: number): (ctx: ReminderContext) => string | null;
68
+ /**
69
+ * The nesting rule: fires when steps pile up flat under milestones — no step
70
+ * nested under another step — while completed nodes already carry findings.
71
+ *
72
+ * Kinds are not stored on nodes, so shape is judged by depth: milestone at
73
+ * depth 1, step at depth 2; a step nested under a step lives at depth ≥ 3.
74
+ * "Flat" = at least 3 nodes at depth 2 and nothing deeper.
75
+ */
76
+ export declare function createNestingRule(latch: ReminderLatch, flatSteps?: number): (ctx: ReminderContext) => string | null;
77
+ //# sourceMappingURL=reminders.d.ts.map
@@ -0,0 +1,67 @@
1
+ /**
2
+ * SessionForestStore — the single owner of a session's in-process forest.
3
+ *
4
+ * Two state holders exist: this store's map (live, synchronously mutated) and
5
+ * the session projection (durable, rebuilt from the session log). Before this
6
+ * module existed, the reconciliation protocol between them — "map wins during
7
+ * a turn, the projection seeds on first access, the log append precedes
8
+ * execute" — lived in comments inside the tool's execute switch, and every
9
+ * historical state bug came from a caller misapplying that protocol. Now the
10
+ * protocol is code, behind two methods:
11
+ *
12
+ * - {@link current} — read the session's forest, seeding from the projection
13
+ * on first access. Projection failures are reported loudly once per session
14
+ * instead of silently starting an empty forest.
15
+ * - {@link apply} — mutate the forest with one trace call. Fully synchronous:
16
+ * the read → seed → fold → write critical section contains no `await`, so
17
+ * no concurrent call can interleave. The create_tree double-fold guard
18
+ * (phantom tree) lives here, not in the caller.
19
+ *
20
+ * @module @elinpf/dsh-ops-tool-trace/session-forests
21
+ */
22
+ import type { ForestState, TraceArgs } from './types.js';
23
+ /** Minimal event shape the fold consumes. */
24
+ export interface FoldableEvent {
25
+ type: string;
26
+ data: {
27
+ name?: string;
28
+ turn?: number;
29
+ step?: number;
30
+ arguments?: string;
31
+ };
32
+ }
33
+ /** Reads the durable projection state for seeding. Null when absent. */
34
+ export type ForestSnapshotter = (session: {
35
+ id: string;
36
+ }) => ForestState | null;
37
+ /** Folds one tool/call-shaped event into forest state (the projection fold). */
38
+ export type ForestFold = (state: ForestState | null, event: FoldableEvent) => ForestState | null;
39
+ export declare class SessionForestStore {
40
+ private readonly snapshot;
41
+ private readonly fold;
42
+ private readonly warn;
43
+ private readonly forests;
44
+ private readonly seedFailureReported;
45
+ constructor(snapshot: ForestSnapshotter, fold: ForestFold, warn: (message: string) => void);
46
+ /**
47
+ * The current forest for a session. Seeds from the projection on first
48
+ * access (session replay / process restart); afterwards the in-process map
49
+ * is authoritative so parallel calls in one turn see each other immediately.
50
+ */
51
+ current(session: {
52
+ id: string;
53
+ }): {
54
+ forest: ForestState;
55
+ seeded: boolean;
56
+ };
57
+ /**
58
+ * Apply one trace call to the session's forest and return the result.
59
+ * Synchronous by design — see the module docstring.
60
+ */
61
+ apply(session: {
62
+ id: string;
63
+ }, args: TraceArgs, turn: number): ForestState;
64
+ /** Drop all in-process state (fiber disposal). */
65
+ clear(): void;
66
+ }
67
+ //# sourceMappingURL=session-forests.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Tree layout — the single pure module for turning a flat node list into a
3
+ * displayable tree.
4
+ *
5
+ * Before this module, the traversal machinery was written three times: the
6
+ * host renderers (src/index.ts) had buildTreeIndex/sortChildren, the web
7
+ * client (src/client.ts) had its own depthOf/treeOrder, and the reminder
8
+ * rules (src/reminders.ts) had a third depthOf. Worse, the two displays
9
+ * disagreed: the model saw siblings status-sorted (active work first) while
10
+ * the human saw insertion order — the same tree, two layouts. Now both sides
11
+ * share these functions, so the human sees the tree in the same order the
12
+ * model reasoned about it.
13
+ *
14
+ * Everything here is a pure function of the node list — safe to call from
15
+ * render paths that re-run on session-log replay.
16
+ *
17
+ * @module @elinpf/dsh-ops-tool-trace/tree-layout
18
+ */
19
+ import type { TreeNode } from './types.js';
20
+ /** Status → display rank: active work first, done/dead after, goal last. */
21
+ export declare const STATUS_ORDER: Record<string, number>;
22
+ /** Sort siblings: in_progress first, then pending, done, dead_end; the goal
23
+ * node (convergence terminal) always last. */
24
+ export declare function sortChildren(nodes: TreeNode[]): TreeNode[];
25
+ /** Build child map and find root from a flat node list. */
26
+ export declare function buildTreeIndex(nodes: TreeNode[]): {
27
+ children: Record<string, TreeNode[]>;
28
+ root: TreeNode | null;
29
+ };
30
+ /**
31
+ * Depth of a node via its parent chain (root = 0). Cycle-safe: a parent loop
32
+ * stops instead of recursing forever. Callers computing depths for many
33
+ * nodes in one pass may share a `cache` across calls.
34
+ */
35
+ export declare function depthOf(nodes: TreeNode[], id: string, cache?: Record<string, number>): number;
36
+ /**
37
+ * DFS flattening for display: children follow their parent, siblings in
38
+ * sortChildren order. Orphans (parent id not present in the list) are
39
+ * appended at the end so no node is ever dropped from view.
40
+ */
41
+ export declare function flattenTree(nodes: TreeNode[]): TreeNode[];
42
+ //# sourceMappingURL=tree-layout.d.ts.map
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Tree layout — the single pure module for turning a flat node list into a
3
+ * displayable tree.
4
+ *
5
+ * Before this module, the traversal machinery was written three times: the
6
+ * host renderers (src/index.ts) had buildTreeIndex/sortChildren, the web
7
+ * client (src/client.ts) had its own depthOf/treeOrder, and the reminder
8
+ * rules (src/reminders.ts) had a third depthOf. Worse, the two displays
9
+ * disagreed: the model saw siblings status-sorted (active work first) while
10
+ * the human saw insertion order — the same tree, two layouts. Now both sides
11
+ * share these functions, so the human sees the tree in the same order the
12
+ * model reasoned about it.
13
+ *
14
+ * Everything here is a pure function of the node list — safe to call from
15
+ * render paths that re-run on session-log replay.
16
+ *
17
+ * @module @elinpf/dsh-ops-tool-trace/tree-layout
18
+ */
19
+ /** Status → display rank: active work first, done/dead after, goal last. */
20
+ export const STATUS_ORDER = {
21
+ in_progress: 0, pending: 1, done: 2, dead_end: 3, goal: 4, resolved: 5,
22
+ };
23
+ /** Sort siblings: in_progress first, then pending, done, dead_end; the goal
24
+ * node (convergence terminal) always last. */
25
+ export function sortChildren(nodes) {
26
+ return [...nodes].sort((a, b) => {
27
+ const aIsGoal = a.id === 'goal';
28
+ const bIsGoal = b.id === 'goal';
29
+ if (aIsGoal && !bIsGoal)
30
+ return 1;
31
+ if (!aIsGoal && bIsGoal)
32
+ return -1;
33
+ return (STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9);
34
+ });
35
+ }
36
+ /** Build child map and find root from a flat node list. */
37
+ export function buildTreeIndex(nodes) {
38
+ const children = {};
39
+ let root = null;
40
+ for (const n of nodes) {
41
+ if (n.parent === null) {
42
+ root = n;
43
+ }
44
+ else {
45
+ if (!children[n.parent])
46
+ children[n.parent] = [];
47
+ children[n.parent].push(n);
48
+ }
49
+ }
50
+ return { children, root };
51
+ }
52
+ /**
53
+ * Depth of a node via its parent chain (root = 0). Cycle-safe: a parent loop
54
+ * stops instead of recursing forever. Callers computing depths for many
55
+ * nodes in one pass may share a `cache` across calls.
56
+ */
57
+ export function depthOf(nodes, id, cache = {}) {
58
+ if (id in cache)
59
+ return cache[id];
60
+ let depth = 0;
61
+ let current = nodes.find((n) => n.id === id);
62
+ const seen = new Set();
63
+ while (current && current.parent !== null && !seen.has(current.id)) {
64
+ seen.add(current.id);
65
+ depth++;
66
+ current = nodes.find((n) => n.id === current.parent);
67
+ }
68
+ cache[id] = depth;
69
+ return depth;
70
+ }
71
+ /**
72
+ * DFS flattening for display: children follow their parent, siblings in
73
+ * sortChildren order. Orphans (parent id not present in the list) are
74
+ * appended at the end so no node is ever dropped from view.
75
+ */
76
+ export function flattenTree(nodes) {
77
+ const { children, root } = buildTreeIndex(nodes);
78
+ const result = [];
79
+ const visited = new Set();
80
+ function visit(node) {
81
+ if (visited.has(node))
82
+ return;
83
+ visited.add(node);
84
+ result.push(node);
85
+ const kids = children[node.id];
86
+ if (kids)
87
+ for (const k of sortChildren(kids))
88
+ visit(k);
89
+ }
90
+ if (root)
91
+ visit(root);
92
+ for (const n of nodes) {
93
+ if (!visited.has(n))
94
+ result.push(n);
95
+ }
96
+ return result;
97
+ }
98
+ //# sourceMappingURL=tree-layout.js.map
package/lib/types.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Type definitions for the ops-trace plugin.
3
+ *
4
+ * @module @elinpf/dsh-ops-tool-trace
5
+ */
6
+ export type { NodeStatus } from './node-status.js';
7
+ import type { NodeStatus } from './node-status.js';
8
+ /**
9
+ * A single node in the investigation tree.
10
+ * Lane and depth are NOT stored here — they are derived client-side.
11
+ */
12
+ export interface TreeNode {
13
+ /** Unique id within the tree. The root node has id 'goal'. */
14
+ id: string;
15
+ /** One-line description of this node. */
16
+ title: string;
17
+ /** Current status per the 05 state machine. */
18
+ status: NodeStatus;
19
+ /** Parent node id; `null` for the goal node (tree root). */
20
+ parent: string | null;
21
+ /** Turns that operated on this node. */
22
+ turns: number[];
23
+ /** Resolution summary (written by `complete` or `resolve`). */
24
+ summary: string | null;
25
+ /** Creation-time rationale (add_step/add_milestone): the hypothesis's
26
+ * "because" clause or the step's concrete target. */
27
+ detail: string | null;
28
+ /** Causal edges: other node ids that are the root cause of this node. */
29
+ caused_by: string[];
30
+ }
31
+ /**
32
+ * One investigation tree (goal + its milestones and steps).
33
+ */
34
+ export interface TreeState {
35
+ /** All nodes in this tree. */
36
+ nodes: TreeNode[];
37
+ /** Whether this tree's goal has been resolved. */
38
+ resolved: boolean;
39
+ }
40
+ /**
41
+ * The full session state carried in a projection snapshot.
42
+ * A forest of independent investigation trees — resolved trees are kept
43
+ * for reference; the active tree is the latest unresolved one.
44
+ */
45
+ export interface ForestState {
46
+ /** All trees in chronological order. */
47
+ trees: TreeState[];
48
+ }
49
+ /** The 11 actions the `trace` tool accepts. */
50
+ export type TraceAction = 'create_tree' | 'add_step' | 'add_milestone' | 'start' | 'complete' | 'abandon' | 'reopen' | 'resolve' | 'link' | 'view' | 'help';
51
+ /**
52
+ * Return value of every `trace` call.
53
+ * `tree` is the active tree that was operated on (or the resolved one).
54
+ */
55
+ export interface TraceResult {
56
+ /** The active tree that was operated on. */
57
+ tree: TreeState;
58
+ /** Status summary of the active tree. */
59
+ summary: {
60
+ total: number;
61
+ counts: Record<NodeStatus, number>;
62
+ incomplete: Array<{
63
+ id: string;
64
+ title: string;
65
+ status: NodeStatus;
66
+ }>;
67
+ warning: string | null;
68
+ };
69
+ /** ID of the newly created node (add_step/add_milestone only). */
70
+ new_node?: string;
71
+ /** Non-blocking advisory hint (add_step only): the chosen parent looks
72
+ * like a flat-hang mistake. Never a rejection — see doctrine.ts. */
73
+ hint?: string;
74
+ }
75
+ /**
76
+ * A single causal-edge pair used by the `link` action.
77
+ */
78
+ export interface LinkPair {
79
+ id: string;
80
+ caused_by: string;
81
+ }
82
+ /**
83
+ * Tool arguments for `trace`. All fields are optional except `action`;
84
+ * which fields are required depends on the action (enforced at execute time).
85
+ */
86
+ export interface TraceArgs {
87
+ action: TraceAction;
88
+ goal_title?: string;
89
+ id?: string;
90
+ parent_id?: string;
91
+ title?: string;
92
+ ids?: string[];
93
+ summary?: string;
94
+ detail?: string;
95
+ caused_by?: string;
96
+ links?: LinkPair[];
97
+ status_filter?: NodeStatus;
98
+ /** view output format: 'full' (default, with detail/summary) or 'tree'
99
+ * (indented outline, shape only). */
100
+ format?: 'full' | 'tree';
101
+ /** Escape hatch for resolve on the goal: force tree closure while nodes
102
+ * are still undecided (result carries a WARN). For abandoning an
103
+ * investigation mid-way. Effective only when resolve targets the goal. */
104
+ force?: boolean;
105
+ }
106
+ //# sourceMappingURL=types.d.ts.map
package/lib/types.js ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Type definitions for the ops-trace plugin.
3
+ *
4
+ * @module @elinpf/dsh-ops-tool-trace
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@elinpf/dsh-ops-tool-trace",
3
+ "version": "0.1.0",
4
+ "description": "Investigation tree tool for ops mode — replaces todo_write with a diverge-converge tree of steps, milestones, dead ends, and a resolved terminal.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./invariant": {
14
+ "types": "./lib/invariant.d.ts",
15
+ "default": "./lib/invariant.js"
16
+ },
17
+ "./types": {
18
+ "types": "./lib/types.d.ts",
19
+ "default": "./lib/types.js"
20
+ },
21
+ "./tree-layout": {
22
+ "types": "./lib/tree-layout.d.ts",
23
+ "default": "./lib/tree-layout.js"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "lib/index.js",
29
+ "lib/invariant.js",
30
+ "lib/types.js",
31
+ "lib/tree-layout.js",
32
+ "lib/**/*.d.ts",
33
+ "cordis.patch.yml"
34
+ ],
35
+ "dsh": {
36
+ "bundle": {
37
+ "patch": "./cordis.patch.yml"
38
+ }
39
+ },
40
+ "dependencies": {
41
+ "@deepseek-ai/schemastery": "^3.18.1",
42
+ "zod": "^4.4.3"
43
+ },
44
+ "peerDependencies": {
45
+ "@deepseek-ai/cordis": "^4.0.1",
46
+ "@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
47
+ "@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
48
+ "@elinpf/dsh-ops-prompts": "^0.1.0",
49
+ "@deepseek-ai/dsh-session": "^0.0.1-rc.1",
50
+ "@deepseek-ai/dsh-session-projection": "^0.0.1-rc.1",
51
+ "@deepseek-ai/dsh-system-prompt": "^0.0.1-rc.1",
52
+ "@deepseek-ai/dsh-tools": "^0.0.1-rc.1"
53
+ },
54
+ "devDependencies": {
55
+ "@deepseek-ai/dsh-attachment": "0.0.1-rc.5",
56
+ "@deepseek-ai/dsh-brand": "0.0.1-rc.5",
57
+ "@deepseek-ai/dsh-invariants": "0.0.1-rc.5",
58
+ "@deepseek-ai/dsh-scope": "0.0.1-rc.5",
59
+ "@deepseek-ai/dsh-timeout": "0.0.1-rc.5",
60
+ "@deepseek-ai/cordis": "4.0.1",
61
+ "@deepseek-ai/dsh-llm": "0.0.1-rc.5",
62
+ "@deepseek-ai/dsh-session": "0.0.1-rc.5",
63
+ "@deepseek-ai/dsh-session-projection": "0.0.1-rc.1",
64
+ "@deepseek-ai/dsh-tools": "0.0.1-rc.1",
65
+ "typescript": "^5.4.0",
66
+ "vitest": "^4.1.11",
67
+ "@elinpf/dsh-ops-prompts": "0.1.0"
68
+ },
69
+ "license": "MIT",
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "scripts": {
74
+ "build": "tsc",
75
+ "typecheck": "tsc --noEmit",
76
+ "test": "vitest run"
77
+ }
78
+ }