0.6 rust重构01
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"hooks": {}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env node
|
||||
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
function rootDir() {
|
||||
try {
|
||||
return execSync("git rev-parse --show-toplevel", {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const payload = readStdin();
|
||||
const root = rootDir();
|
||||
mkdirSync(join(root, ".omx", "logs"), { recursive: true });
|
||||
const exitCode =
|
||||
typeof payload?.tool_output?.exit_code === "number"
|
||||
? payload.tool_output.exit_code
|
||||
: typeof payload?.exit_code === "number"
|
||||
? payload.exit_code
|
||||
: null;
|
||||
const status = exitCode ?? "unknown";
|
||||
const isExplicitBashFailure = payload?.tool_name === "Bash" && exitCode !== null && exitCode !== 0;
|
||||
appendFileSync(
|
||||
join(root, ".omx", "logs", "hooks.log"),
|
||||
`${new Date().toISOString()} PostToolUse status=${status} ${JSON.stringify(payload)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PostToolUse",
|
||||
additionalContext: isExplicitBashFailure ? "OMX noticed a failing Bash command. Check the team inbox or diagnostics." : "",
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const payload = readStdin();
|
||||
const command = JSON.stringify(payload).toLowerCase();
|
||||
const blocked = [
|
||||
"rm -rf /",
|
||||
"mkfs",
|
||||
"dd if=",
|
||||
"shutdown",
|
||||
"reboot",
|
||||
"poweroff",
|
||||
"git reset --hard",
|
||||
];
|
||||
|
||||
if (blocked.some((token) => command.includes(token))) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
decision: "block",
|
||||
reason: "OMX safety hook blocked an obviously destructive command.",
|
||||
}));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
function rootDir() {
|
||||
try {
|
||||
return execSync("git rev-parse --show-toplevel", {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(path, fallback) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(path, value) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
||||
}
|
||||
|
||||
const root = rootDir();
|
||||
const payload = readStdin();
|
||||
const omx = join(root, ".omx");
|
||||
mkdirSync(join(omx, "logs"), { recursive: true });
|
||||
appendFileSync(join(omx, "logs", "hooks.log"), `${new Date().toISOString()} SessionStart ${JSON.stringify(payload)}\n`, "utf8");
|
||||
|
||||
const runtimePath = join(omx, "state", "hook-runtime.json");
|
||||
const runtime = readJson(runtimePath, {});
|
||||
writeJson(runtimePath, {
|
||||
...runtime,
|
||||
lastSessionStartAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "SessionStart",
|
||||
additionalContext: "",
|
||||
},
|
||||
}));
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env node
|
||||
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
function rootDir() {
|
||||
try {
|
||||
return execSync("git rev-parse --show-toplevel", {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(path, fallback) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(path, value) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
||||
}
|
||||
|
||||
const root = rootDir();
|
||||
const payload = readStdin();
|
||||
const omx = join(root, ".omx");
|
||||
mkdirSync(join(omx, "logs"), { recursive: true });
|
||||
appendFileSync(join(omx, "logs", "hooks.log"), `${new Date().toISOString()} Stop ${JSON.stringify(payload)}\n`, "utf8");
|
||||
|
||||
const runtimePath = join(omx, "state", "hook-runtime.json");
|
||||
const runtime = readJson(runtimePath, {});
|
||||
writeJson(runtimePath, {
|
||||
...runtime,
|
||||
lastStopAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
}));
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
|
||||
function rootDir() {
|
||||
try {
|
||||
return execSync("git rev-parse --show-toplevel", {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
}).trim();
|
||||
} catch {
|
||||
return process.cwd();
|
||||
}
|
||||
}
|
||||
|
||||
function readStdin() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(0, "utf8") || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function readJson(path, fallback) {
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(path, value) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, JSON.stringify(value, null, 2), "utf8");
|
||||
}
|
||||
|
||||
const root = rootDir();
|
||||
const payload = readStdin();
|
||||
const omx = join(root, ".omx");
|
||||
mkdirSync(join(omx, "logs"), { recursive: true });
|
||||
appendFileSync(join(omx, "logs", "hooks.log"), `${new Date().toISOString()} UserPromptSubmit ${JSON.stringify(payload)}\n`, "utf8");
|
||||
|
||||
const runtimePath = join(omx, "state", "hook-runtime.json");
|
||||
const runtime = readJson(runtimePath, {});
|
||||
writeJson(runtimePath, {
|
||||
...runtime,
|
||||
lastPromptAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
continue: true,
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "UserPromptSubmit",
|
||||
additionalContext: "",
|
||||
},
|
||||
}));
|
||||
@@ -49,3 +49,4 @@ wolai-frontend/public/documents/**
|
||||
artifacts/
|
||||
artifacts/**
|
||||
tmp
|
||||
design
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
# Design System Inspired by Notion
|
||||
|
||||
## 1. Visual Theme & Atmosphere
|
||||
|
||||
Notion's website embodies the philosophy of the tool itself: a blank canvas that gets out of your way. The design system is built on warm neutrals rather than cold grays, creating a distinctly approachable minimalism that feels like quality paper rather than sterile glass. The page canvas is pure white (`#ffffff`) but the text isn't pure black -- it's a warm near-black (`rgba(0,0,0,0.95)`) that softens the reading experience imperceptibly. The warm gray scale (`#f6f5f4`, `#31302e`, `#615d59`, `#a39e98`) carries subtle yellow-brown undertones, giving the interface a tactile, almost analog warmth.
|
||||
|
||||
The custom NotionInter font (a modified Inter) is the backbone of the system. At display sizes (64px), it uses aggressive negative letter-spacing (-2.125px), creating headlines that feel compressed and precise. The weight range is broader than typical systems: 400 for body, 500 for UI elements, 600 for semi-bold labels, and 700 for display headings. OpenType features `"lnum"` (lining numerals) and `"locl"` (localized forms) are enabled on larger text, adding typographic sophistication that rewards close reading.
|
||||
|
||||
What makes Notion's visual language distinctive is its border philosophy. Rather than heavy borders or shadows, Notion uses ultra-thin `1px solid rgba(0,0,0,0.1)` borders -- borders that exist as whispers, barely perceptible division lines that create structure without weight. The shadow system is equally restrained: multi-layer stacks with cumulative opacity never exceeding 0.05, creating depth that's felt rather than seen.
|
||||
|
||||
**Key Characteristics:**
|
||||
- NotionInter (modified Inter) with negative letter-spacing at display sizes (-2.125px at 64px)
|
||||
- Warm neutral palette: grays carry yellow-brown undertones (`#f6f5f4` warm white, `#31302e` warm dark)
|
||||
- Near-black text via `rgba(0,0,0,0.95)` -- not pure black, creating micro-warmth
|
||||
- Ultra-thin borders: `1px solid rgba(0,0,0,0.1)` throughout -- whisper-weight division
|
||||
- Multi-layer shadow stacks with sub-0.05 opacity for barely-there depth
|
||||
- Notion Blue (`#0075de`) as the singular accent color for CTAs and interactive elements
|
||||
- Pill badges (9999px radius) with tinted blue backgrounds for status indicators
|
||||
- 8px base spacing unit with an organic, non-rigid scale
|
||||
|
||||
## 2. Color Palette & Roles
|
||||
|
||||
### Primary
|
||||
- **Notion Black** (`rgba(0,0,0,0.95)` / `#000000f2`): Primary text, headings, body copy. The 95% opacity softens pure black without sacrificing readability.
|
||||
- **Pure White** (`#ffffff`): Page background, card surfaces, button text on blue.
|
||||
- **Notion Blue** (`#0075de`): Primary CTA, link color, interactive accent -- the only saturated color in the core UI chrome.
|
||||
|
||||
### Brand Secondary
|
||||
- **Deep Navy** (`#213183`): Secondary brand color, used sparingly for emphasis and dark feature sections.
|
||||
- **Active Blue** (`#005bab`): Button active/pressed state -- darker variant of Notion Blue.
|
||||
|
||||
### Warm Neutral Scale
|
||||
- **Warm White** (`#f6f5f4`): Background surface tint, section alternation, subtle card fill. The yellow undertone is key.
|
||||
- **Warm Dark** (`#31302e`): Dark surface background, dark section text. Warmer than standard grays.
|
||||
- **Warm Gray 500** (`#615d59`): Secondary text, descriptions, muted labels.
|
||||
- **Warm Gray 300** (`#a39e98`): Placeholder text, disabled states, caption text.
|
||||
|
||||
### Semantic Accent Colors
|
||||
- **Teal** (`#2a9d99`): Success states, positive indicators.
|
||||
- **Green** (`#1aae39`): Confirmation, completion badges.
|
||||
- **Orange** (`#dd5b00`): Warning states, attention indicators.
|
||||
- **Pink** (`#ff64c8`): Decorative accent, feature highlights.
|
||||
- **Purple** (`#391c57`): Premium features, deep accents.
|
||||
- **Brown** (`#523410`): Earthy accent, warm feature sections.
|
||||
|
||||
### Interactive
|
||||
- **Link Blue** (`#0075de`): Primary link color with underline-on-hover.
|
||||
- **Link Light Blue** (`#62aef0`): Lighter link variant for dark backgrounds.
|
||||
- **Focus Blue** (`#097fe8`): Focus ring on interactive elements.
|
||||
- **Badge Blue Bg** (`#f2f9ff`): Pill badge background, tinted blue surface.
|
||||
- **Badge Blue Text** (`#097fe8`): Pill badge text, darker blue for readability.
|
||||
|
||||
### Shadows & Depth
|
||||
- **Card Shadow** (`rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.84688px, rgba(0,0,0,0.02) 0px 0.8px 2.925px, rgba(0,0,0,0.01) 0px 0.175px 1.04062px`): Multi-layer card elevation.
|
||||
- **Deep Shadow** (`rgba(0,0,0,0.01) 0px 1px 3px, rgba(0,0,0,0.02) 0px 3px 7px, rgba(0,0,0,0.02) 0px 7px 15px, rgba(0,0,0,0.04) 0px 14px 28px, rgba(0,0,0,0.05) 0px 23px 52px`): Five-layer deep elevation for modals and featured content.
|
||||
- **Whisper Border** (`1px solid rgba(0,0,0,0.1)`): Standard division border -- cards, dividers, sections.
|
||||
|
||||
## 3. Typography Rules
|
||||
|
||||
### Font Family
|
||||
- **Primary**: `NotionInter`, with fallbacks: `Inter, -apple-system, system-ui, Segoe UI, Helvetica, Apple Color Emoji, Arial, Segoe UI Emoji, Segoe UI Symbol`
|
||||
- **OpenType Features**: `"lnum"` (lining numerals) and `"locl"` (localized forms) enabled on display and heading text.
|
||||
|
||||
### Hierarchy
|
||||
|
||||
| Role | Font | Size | Weight | Line Height | Letter Spacing | Notes |
|
||||
|------|------|------|--------|-------------|----------------|-------|
|
||||
| Display Hero | NotionInter | 64px (4.00rem) | 700 | 1.00 (tight) | -2.125px | Maximum compression, billboard headlines |
|
||||
| Display Secondary | NotionInter | 54px (3.38rem) | 700 | 1.04 (tight) | -1.875px | Secondary hero, feature headlines |
|
||||
| Section Heading | NotionInter | 48px (3.00rem) | 700 | 1.00 (tight) | -1.5px | Feature section titles, with `"lnum"` |
|
||||
| Sub-heading Large | NotionInter | 40px (2.50rem) | 700 | 1.50 | normal | Card headings, feature sub-sections |
|
||||
| Sub-heading | NotionInter | 26px (1.63rem) | 700 | 1.23 (tight) | -0.625px | Section sub-titles, content headers |
|
||||
| Card Title | NotionInter | 22px (1.38rem) | 700 | 1.27 (tight) | -0.25px | Feature cards, list titles |
|
||||
| Body Large | NotionInter | 20px (1.25rem) | 600 | 1.40 | -0.125px | Introductions, feature descriptions |
|
||||
| Body | NotionInter | 16px (1.00rem) | 400 | 1.50 | normal | Standard reading text |
|
||||
| Body Medium | NotionInter | 16px (1.00rem) | 500 | 1.50 | normal | Navigation, emphasized UI text |
|
||||
| Body Semibold | NotionInter | 16px (1.00rem) | 600 | 1.50 | normal | Strong labels, active states |
|
||||
| Body Bold | NotionInter | 16px (1.00rem) | 700 | 1.50 | normal | Headlines at body size |
|
||||
| Nav / Button | NotionInter | 15px (0.94rem) | 600 | 1.33 | normal | Navigation links, button text |
|
||||
| Caption | NotionInter | 14px (0.88rem) | 500 | 1.43 | normal | Metadata, secondary labels |
|
||||
| Caption Light | NotionInter | 14px (0.88rem) | 400 | 1.43 | normal | Body captions, descriptions |
|
||||
| Badge | NotionInter | 12px (0.75rem) | 600 | 1.33 | 0.125px | Pill badges, tags, status labels |
|
||||
| Micro Label | NotionInter | 12px (0.75rem) | 400 | 1.33 | 0.125px | Small metadata, timestamps |
|
||||
|
||||
### Principles
|
||||
- **Compression at scale**: NotionInter at display sizes uses -2.125px letter-spacing at 64px, progressively relaxing to -0.625px at 26px and normal at 16px. The compression creates density at headlines while maintaining readability at body sizes.
|
||||
- **Four-weight system**: 400 (body/reading), 500 (UI/interactive), 600 (emphasis/navigation), 700 (headings/display). The broader weight range compared to most systems allows nuanced hierarchy.
|
||||
- **Warm scaling**: Line height tightens as size increases -- 1.50 at body (16px), 1.23-1.27 at sub-headings, 1.00-1.04 at display. This creates denser, more impactful headlines.
|
||||
- **Badge micro-tracking**: The 12px badge text uses positive letter-spacing (0.125px) -- the only positive tracking in the system, creating wider, more legible small text.
|
||||
|
||||
## 4. Component Stylings
|
||||
|
||||
### Buttons
|
||||
|
||||
**Primary Blue**
|
||||
- Background: `#0075de` (Notion Blue)
|
||||
- Text: `#ffffff`
|
||||
- Padding: 8px 16px
|
||||
- Radius: 4px (subtle)
|
||||
- Border: `1px solid transparent`
|
||||
- Hover: background darkens to `#005bab`
|
||||
- Active: scale(0.9) transform
|
||||
- Focus: `2px solid` focus outline, `var(--shadow-level-200)` shadow
|
||||
- Use: Primary CTA ("Get Notion free", "Try it")
|
||||
|
||||
**Secondary / Tertiary**
|
||||
- Background: `rgba(0,0,0,0.05)` (translucent warm gray)
|
||||
- Text: `#000000` (near-black)
|
||||
- Padding: 8px 16px
|
||||
- Radius: 4px
|
||||
- Hover: text color shifts, scale(1.05)
|
||||
- Active: scale(0.9) transform
|
||||
- Use: Secondary actions, form submissions
|
||||
|
||||
**Ghost / Link Button**
|
||||
- Background: transparent
|
||||
- Text: `rgba(0,0,0,0.95)`
|
||||
- Decoration: underline on hover
|
||||
- Use: Tertiary actions, inline links
|
||||
|
||||
**Pill Badge Button**
|
||||
- Background: `#f2f9ff` (tinted blue)
|
||||
- Text: `#097fe8`
|
||||
- Padding: 4px 8px
|
||||
- Radius: 9999px (full pill)
|
||||
- Font: 12px weight 600
|
||||
- Use: Status badges, feature labels, "New" tags
|
||||
|
||||
### Cards & Containers
|
||||
- Background: `#ffffff`
|
||||
- Border: `1px solid rgba(0,0,0,0.1)` (whisper border)
|
||||
- Radius: 12px (standard cards), 16px (featured/hero cards)
|
||||
- Shadow: `rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.84688px, rgba(0,0,0,0.02) 0px 0.8px 2.925px, rgba(0,0,0,0.01) 0px 0.175px 1.04062px`
|
||||
- Hover: subtle shadow intensification
|
||||
- Image cards: 12px top radius, image fills top half
|
||||
|
||||
### Inputs & Forms
|
||||
- Background: `#ffffff`
|
||||
- Text: `rgba(0,0,0,0.9)`
|
||||
- Border: `1px solid #dddddd`
|
||||
- Padding: 6px
|
||||
- Radius: 4px
|
||||
- Focus: blue outline ring
|
||||
- Placeholder: warm gray `#a39e98`
|
||||
|
||||
### Navigation
|
||||
- Clean horizontal nav on white, not sticky
|
||||
- Brand logo left-aligned (33x34px icon + wordmark)
|
||||
- Links: NotionInter 15px weight 500-600, near-black text
|
||||
- Hover: color shift to `var(--color-link-primary-text-hover)`
|
||||
- CTA: blue pill button ("Get Notion free") right-aligned
|
||||
- Mobile: hamburger menu collapse
|
||||
- Product dropdowns with multi-level categorized menus
|
||||
|
||||
### Image Treatment
|
||||
- Product screenshots with `1px solid rgba(0,0,0,0.1)` border
|
||||
- Top-rounded images: `12px 12px 0px 0px` radius
|
||||
- Dashboard/workspace preview screenshots dominate feature sections
|
||||
- Warm gradient backgrounds behind hero illustrations (decorative character illustrations)
|
||||
|
||||
### Distinctive Components
|
||||
|
||||
**Feature Cards with Illustrations**
|
||||
- Large illustrative headers (The Great Wave, product UI screenshots)
|
||||
- 12px radius card with whisper border
|
||||
- Title at 22px weight 700, description at 16px weight 400
|
||||
- Warm white (`#f6f5f4`) background variant for alternating sections
|
||||
|
||||
**Trust Bar / Logo Grid**
|
||||
- Company logos (trusted teams section) in their brand colors
|
||||
- Horizontal scroll or grid layout with team counts
|
||||
- Metric display: large number + description pattern
|
||||
|
||||
**Metric Cards**
|
||||
- Large number display (e.g., "$4,200 ROI")
|
||||
- NotionInter 40px+ weight 700 for the metric
|
||||
- Description below in warm gray body text
|
||||
- Whisper-bordered card container
|
||||
|
||||
## 5. Layout Principles
|
||||
|
||||
### Spacing System
|
||||
- Base unit: 8px
|
||||
- Scale: 2px, 3px, 4px, 5px, 6px, 7px, 8px, 11px, 12px, 14px, 16px, 24px, 32px
|
||||
- Non-rigid organic scale with fractional values (5.6px, 6.4px) for micro-adjustments
|
||||
|
||||
### Grid & Container
|
||||
- Max content width: approximately 1200px
|
||||
- Hero: centered single-column with generous top padding (80-120px)
|
||||
- Feature sections: 2-3 column grids for cards
|
||||
- Full-width warm white (`#f6f5f4`) section backgrounds for alternation
|
||||
- Code/dashboard screenshots as contained with whisper border
|
||||
|
||||
### Whitespace Philosophy
|
||||
- **Generous vertical rhythm**: 64-120px between major sections. Notion lets content breathe with vast vertical padding.
|
||||
- **Warm alternation**: White sections alternate with warm white (`#f6f5f4`) sections, creating gentle visual rhythm without harsh color breaks.
|
||||
- **Content-first density**: Body text blocks are compact (line-height 1.50) but surrounded by ample margin, creating islands of readable content in a sea of white space.
|
||||
|
||||
### Border Radius Scale
|
||||
- Micro (4px): Buttons, inputs, functional interactive elements
|
||||
- Subtle (5px): Links, list items, menu items
|
||||
- Standard (8px): Small cards, containers, inline elements
|
||||
- Comfortable (12px): Standard cards, feature containers, image tops
|
||||
- Large (16px): Hero cards, featured content, promotional blocks
|
||||
- Full Pill (9999px): Badges, pills, status indicators
|
||||
- Circle (100%): Tab indicators, avatars
|
||||
|
||||
## 6. Depth & Elevation
|
||||
|
||||
| Level | Treatment | Use |
|
||||
|-------|-----------|-----|
|
||||
| Flat (Level 0) | No shadow, no border | Page background, text blocks |
|
||||
| Whisper (Level 1) | `1px solid rgba(0,0,0,0.1)` | Standard borders, card outlines, dividers |
|
||||
| Soft Card (Level 2) | 4-layer shadow stack (max opacity 0.04) | Content cards, feature blocks |
|
||||
| Deep Card (Level 3) | 5-layer shadow stack (max opacity 0.05, 52px blur) | Modals, featured panels, hero elements |
|
||||
| Focus (Accessibility) | `2px solid var(--focus-color)` outline | Keyboard focus on all interactive elements |
|
||||
|
||||
**Shadow Philosophy**: Notion's shadow system uses multiple layers with extremely low individual opacity (0.01 to 0.05) that accumulate into soft, natural-looking elevation. The 4-layer card shadow spans from 1.04px to 18px blur, creating a gradient of depth rather than a single hard shadow. The 5-layer deep shadow extends to 52px blur at 0.05 opacity, producing ambient occlusion that feels like natural light rather than computer-generated depth. This layered approach makes elements feel embedded in the page rather than floating above it.
|
||||
|
||||
### Decorative Depth
|
||||
- Hero section: decorative character illustrations (playful, hand-drawn style)
|
||||
- Section alternation: white to warm white (`#f6f5f4`) background shifts
|
||||
- No hard section borders -- separation comes from background color changes and spacing
|
||||
|
||||
## 7. Responsive Behavior
|
||||
|
||||
### Breakpoints
|
||||
| Name | Width | Key Changes |
|
||||
|------|-------|-------------|
|
||||
| Mobile Small | <400px | Tight single column, minimal padding |
|
||||
| Mobile | 400-600px | Standard mobile, stacked layout |
|
||||
| Tablet Small | 600-768px | 2-column grids begin |
|
||||
| Tablet | 768-1080px | Full card grids, expanded padding |
|
||||
| Desktop Small | 1080-1200px | Standard desktop layout |
|
||||
| Desktop | 1200-1440px | Full layout, maximum content width |
|
||||
| Large Desktop | >1440px | Centered, generous margins |
|
||||
|
||||
### Touch Targets
|
||||
- Buttons use comfortable padding (8px-16px vertical)
|
||||
- Navigation links at 15px with adequate spacing
|
||||
- Pill badges have 8px horizontal padding for tap targets
|
||||
- Mobile menu toggle uses standard hamburger button
|
||||
|
||||
### Collapsing Strategy
|
||||
- Hero: 64px display -> scales to 40px -> 26px on mobile, maintains proportional letter-spacing
|
||||
- Navigation: horizontal links + blue CTA -> hamburger menu
|
||||
- Feature cards: 3-column -> 2-column -> single column stacked
|
||||
- Product screenshots: maintain aspect ratio with responsive images
|
||||
- Trust bar logos: grid -> horizontal scroll on mobile
|
||||
- Footer: multi-column -> stacked single column
|
||||
- Section spacing: 80px+ -> 48px on mobile
|
||||
|
||||
### Image Behavior
|
||||
- Workspace screenshots maintain whisper border at all sizes
|
||||
- Hero illustrations scale proportionally
|
||||
- Product screenshots use responsive images with consistent border radius
|
||||
- Full-width warm white sections maintain edge-to-edge treatment
|
||||
|
||||
## 8. Accessibility & States
|
||||
|
||||
### Focus System
|
||||
- All interactive elements receive visible focus indicators
|
||||
- Focus outline: `2px solid` with focus color + shadow level 200
|
||||
- Tab navigation supported throughout all interactive components
|
||||
- High contrast text: near-black on white exceeds WCAG AAA (>14:1 ratio)
|
||||
|
||||
### Interactive States
|
||||
- **Default**: Standard appearance with whisper borders
|
||||
- **Hover**: Color shift on text, scale(1.05) on buttons, underline on links
|
||||
- **Active/Pressed**: scale(0.9) transform, darker background variant
|
||||
- **Focus**: Blue outline ring with shadow reinforcement
|
||||
- **Disabled**: Warm gray (`#a39e98`) text, reduced opacity
|
||||
|
||||
### Color Contrast
|
||||
- Primary text (rgba(0,0,0,0.95)) on white: ~18:1 ratio
|
||||
- Secondary text (#615d59) on white: ~5.5:1 ratio (WCAG AA)
|
||||
- Blue CTA (#0075de) on white: ~4.6:1 ratio (WCAG AA for large text)
|
||||
- Badge text (#097fe8) on badge bg (#f2f9ff): ~4.5:1 ratio (WCAG AA for large text)
|
||||
|
||||
## 9. Agent Prompt Guide
|
||||
|
||||
### Quick Color Reference
|
||||
- Primary CTA: Notion Blue (`#0075de`)
|
||||
- Background: Pure White (`#ffffff`)
|
||||
- Alt Background: Warm White (`#f6f5f4`)
|
||||
- Heading text: Near-Black (`rgba(0,0,0,0.95)`)
|
||||
- Body text: Near-Black (`rgba(0,0,0,0.95)`)
|
||||
- Secondary text: Warm Gray 500 (`#615d59`)
|
||||
- Muted text: Warm Gray 300 (`#a39e98`)
|
||||
- Border: `1px solid rgba(0,0,0,0.1)`
|
||||
- Link: Notion Blue (`#0075de`)
|
||||
- Focus ring: Focus Blue (`#097fe8`)
|
||||
|
||||
### Example Component Prompts
|
||||
- "Create a hero section on white background. Headline at 64px NotionInter weight 700, line-height 1.00, letter-spacing -2.125px, color rgba(0,0,0,0.95). Subtitle at 20px weight 600, line-height 1.40, color #615d59. Blue CTA button (#0075de, 4px radius, 8px 16px padding, white text) and ghost button (transparent bg, near-black text, underline on hover)."
|
||||
- "Design a card: white background, 1px solid rgba(0,0,0,0.1) border, 12px radius. Use shadow stack: rgba(0,0,0,0.04) 0px 4px 18px, rgba(0,0,0,0.027) 0px 2.025px 7.85px, rgba(0,0,0,0.02) 0px 0.8px 2.93px, rgba(0,0,0,0.01) 0px 0.175px 1.04px. Title at 22px NotionInter weight 700, letter-spacing -0.25px. Body at 16px weight 400, color #615d59."
|
||||
- "Build a pill badge: #f2f9ff background, #097fe8 text, 9999px radius, 4px 8px padding, 12px NotionInter weight 600, letter-spacing 0.125px."
|
||||
- "Create navigation: white header. NotionInter 15px weight 600 for links, near-black text. Blue pill CTA 'Get Notion free' right-aligned (#0075de bg, white text, 4px radius)."
|
||||
- "Design an alternating section layout: white sections alternate with warm white (#f6f5f4) sections. Each section has 64-80px vertical padding, max-width 1200px centered. Section heading at 48px weight 700, line-height 1.00, letter-spacing -1.5px."
|
||||
|
||||
### Iteration Guide
|
||||
1. Always use warm neutrals -- Notion's grays have yellow-brown undertones (#f6f5f4, #31302e, #615d59, #a39e98), never blue-gray
|
||||
2. Letter-spacing scales with font size: -2.125px at 64px, -1.875px at 54px, -0.625px at 26px, normal at 16px
|
||||
3. Four weights: 400 (read), 500 (interact), 600 (emphasize), 700 (announce)
|
||||
4. Borders are whispers: 1px solid rgba(0,0,0,0.1) -- never heavier
|
||||
5. Shadows use 4-5 layers with individual opacity never exceeding 0.05
|
||||
6. The warm white (#f6f5f4) section background is essential for visual rhythm
|
||||
7. Pill badges (9999px) for status/tags, 4px radius for buttons and inputs
|
||||
8. Notion Blue (#0075de) is the only saturated color in core UI -- use it sparingly for CTAs and links
|
||||
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Harness 会在每次会话启动时执行此脚本,这里只做轻量且可重复执行的环境检查。
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
command -v git >/dev/null || {
|
||||
echo "ERROR: 缺少 git"
|
||||
exit 1
|
||||
}
|
||||
|
||||
command -v python3 >/dev/null || {
|
||||
echo "ERROR: 缺少 python3"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [ -d "$ROOT_DIR/wolai-frontend" ]; then
|
||||
if ! command -v node >/dev/null; then
|
||||
echo "WARN: 缺少 node,前端相关任务可能无法执行"
|
||||
fi
|
||||
if ! command -v pnpm >/dev/null; then
|
||||
echo "WARN: 缺少 pnpm,前端相关任务可能无法执行"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Harness 环境检查完成"
|
||||
@@ -0,0 +1 @@
|
||||
[2026-04-14T05:21:15Z] [SESSION-0] INIT Harness initialized for project /mnt/Data1T/mnote
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 2,
|
||||
"created": "2026-04-14T05:21:15Z",
|
||||
"session_config": {
|
||||
"concurrency_mode": "exclusive",
|
||||
"max_tasks_per_session": 20,
|
||||
"max_sessions": 50
|
||||
},
|
||||
"tasks": [],
|
||||
"session_count": 0,
|
||||
"last_session": null
|
||||
}
|
||||
Generated
+38
@@ -0,0 +1,38 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "core-domain"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "core-protocol"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-domain",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-log"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-domain",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "index-fts"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-domain",
|
||||
"event-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "storage-convex-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-domain",
|
||||
"core-protocol",
|
||||
"event-log",
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"crates/core-domain",
|
||||
"crates/core-protocol",
|
||||
"crates/event-log",
|
||||
"crates/storage-convex-bridge",
|
||||
"crates/index-fts",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
version = "0.1.0"
|
||||
authors = ["mnote"]
|
||||
|
||||
[workspace.metadata]
|
||||
description = "mnote 单仓收口阶段的 Rust 内核 workspace"
|
||||
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "core-domain"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1,6 @@
|
||||
# core-domain
|
||||
|
||||
阶段 1 骨架 crate。
|
||||
|
||||
职责说明请先看:
|
||||
- ../../design/phase1-b-crate-responsibilities-v0.md
|
||||
@@ -0,0 +1,43 @@
|
||||
use crate::ids::{AssetId, AssetVersionId, BlockId, PageId, TaskId};
|
||||
use crate::time::Timestamp;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ActorRef {
|
||||
pub actor_type: String,
|
||||
pub actor_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SourceKind {
|
||||
Web,
|
||||
Cli,
|
||||
Agent,
|
||||
Job,
|
||||
Import,
|
||||
System,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ChangeReason {
|
||||
pub code: String,
|
||||
pub detail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RefLink {
|
||||
Page(PageId),
|
||||
Block(BlockId),
|
||||
Asset(AssetId),
|
||||
AssetVersion(AssetVersionId),
|
||||
Task(TaskId),
|
||||
External(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuditInfo {
|
||||
pub created_at: Timestamp,
|
||||
pub updated_at: Timestamp,
|
||||
pub created_by: Option<ActorRef>,
|
||||
pub updated_by: Option<ActorRef>,
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
use crate::audit::{ActorRef, AuditInfo, ChangeReason, RefLink, SourceKind};
|
||||
use crate::ids::{
|
||||
AgentSessionId, AssetId, AssetVersionId, BlockId, CommandLogId, EventId, PageId, ReferenceId,
|
||||
TaskId, WorkspaceId,
|
||||
};
|
||||
use crate::time::Timestamp;
|
||||
use crate::types::{Locale, Revision};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Workspace {
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub slug: String,
|
||||
pub title: String,
|
||||
pub owner_actor_id: String,
|
||||
pub default_locale: Locale,
|
||||
pub storage_policy: String,
|
||||
pub sync_policy: String,
|
||||
pub feature_flags: Vec<String>,
|
||||
pub archived_at: Option<Timestamp>,
|
||||
pub audit: AuditInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PageType {
|
||||
Note,
|
||||
Doc,
|
||||
DatabaseRecord,
|
||||
Inbox,
|
||||
Template,
|
||||
System,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum PageStatus {
|
||||
Active,
|
||||
Archived,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Page {
|
||||
pub page_id: PageId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub parent_page_id: Option<PageId>,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub icon: Option<String>,
|
||||
pub cover_asset_id: Option<AssetId>,
|
||||
pub body_root_block_id: Option<BlockId>,
|
||||
pub page_type: PageType,
|
||||
pub status: PageStatus,
|
||||
pub last_edited_at: Option<Timestamp>,
|
||||
pub last_edited_by: Option<ActorRef>,
|
||||
pub current_revision: Revision,
|
||||
pub audit: AuditInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BlockType {
|
||||
Paragraph,
|
||||
Heading,
|
||||
BulletedListItem,
|
||||
NumberedListItem,
|
||||
Todo,
|
||||
Quote,
|
||||
CodeBlock,
|
||||
Divider,
|
||||
Callout,
|
||||
PageReference,
|
||||
BlockReference,
|
||||
EmbedAsset,
|
||||
EmbedView,
|
||||
EmbedOnlyoffice,
|
||||
EmbedMindmap,
|
||||
EmbedTable,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Block {
|
||||
pub block_id: BlockId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub page_id: PageId,
|
||||
pub parent_block_id: Option<BlockId>,
|
||||
pub prev_block_id: Option<BlockId>,
|
||||
pub next_block_id: Option<BlockId>,
|
||||
pub sort_key: String,
|
||||
pub block_type: BlockType,
|
||||
pub content: String,
|
||||
pub props: Vec<(String, String)>,
|
||||
pub annotations: Vec<String>,
|
||||
pub status: String,
|
||||
pub revision: Revision,
|
||||
pub deleted_at: Option<Timestamp>,
|
||||
pub audit: AuditInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AssetKind {
|
||||
Image,
|
||||
Office,
|
||||
Pdf,
|
||||
Audio,
|
||||
Video,
|
||||
Export,
|
||||
OcrDerived,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AssetStatus {
|
||||
Active,
|
||||
Archived,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Asset {
|
||||
pub asset_id: AssetId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub storage_key: String,
|
||||
pub original_name: String,
|
||||
pub mime_type: String,
|
||||
pub size_bytes: u64,
|
||||
pub checksum: Option<String>,
|
||||
pub origin: String,
|
||||
pub asset_kind: AssetKind,
|
||||
pub status: AssetStatus,
|
||||
pub latest_version_id: Option<AssetVersionId>,
|
||||
pub audit: AuditInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AssetVersion {
|
||||
pub asset_version_id: AssetVersionId,
|
||||
pub asset_id: AssetId,
|
||||
pub version_no: u32,
|
||||
pub storage_key: String,
|
||||
pub checksum: Option<String>,
|
||||
pub derived_from_version_id: Option<AssetVersionId>,
|
||||
pub change_reason: Option<ChangeReason>,
|
||||
pub created_at: Timestamp,
|
||||
pub created_by: Option<ActorRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReferenceKind {
|
||||
PageToPage,
|
||||
BlockToBlock,
|
||||
BlockToPage,
|
||||
BlockToAssetFragment,
|
||||
TaskToObject,
|
||||
External,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Reference {
|
||||
pub reference_id: ReferenceId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub source_object_type: String,
|
||||
pub source_object_id: String,
|
||||
pub target_object_type: String,
|
||||
pub target_object_id: String,
|
||||
pub anchor: String,
|
||||
pub label: Option<String>,
|
||||
pub snippet: Option<String>,
|
||||
pub confidence: Option<String>,
|
||||
pub ref_kind: ReferenceKind,
|
||||
pub created_at: Timestamp,
|
||||
pub created_by: Option<ActorRef>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TaskType {
|
||||
Import,
|
||||
Ocr,
|
||||
Summary,
|
||||
Reindex,
|
||||
Sync,
|
||||
Transform,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TaskStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TaskPriority {
|
||||
Low,
|
||||
Normal,
|
||||
High,
|
||||
Critical,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Task {
|
||||
pub task_id: TaskId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub task_type: TaskType,
|
||||
pub status: TaskStatus,
|
||||
pub priority: TaskPriority,
|
||||
pub assignee_actor_id: Option<String>,
|
||||
pub source_page_id: Option<PageId>,
|
||||
pub source_block_id: Option<BlockId>,
|
||||
pub input_payload: Option<String>,
|
||||
pub output_payload: Option<String>,
|
||||
pub due_at: Option<Timestamp>,
|
||||
pub completed_at: Option<Timestamp>,
|
||||
pub audit: AuditInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AgentSession {
|
||||
pub agent_session_id: AgentSessionId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub initiator_actor_id: Option<String>,
|
||||
pub status: String,
|
||||
pub started_at: Timestamp,
|
||||
pub updated_at: Timestamp,
|
||||
pub ended_at: Option<Timestamp>,
|
||||
pub tool_policy: Option<String>,
|
||||
pub confirmation_policy: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandStatus {
|
||||
Pending,
|
||||
Succeeded,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandLog {
|
||||
pub command_log_id: CommandLogId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub command_name: String,
|
||||
pub actor: ActorRef,
|
||||
pub source: SourceKind,
|
||||
pub target_objects: Vec<RefLink>,
|
||||
pub payload_summary: String,
|
||||
pub refs: Vec<RefLink>,
|
||||
pub idempotency_key: Option<String>,
|
||||
pub dry_run: bool,
|
||||
pub status: CommandStatus,
|
||||
pub created_at: Timestamp,
|
||||
pub finished_at: Option<Timestamp>,
|
||||
pub error_code: Option<String>,
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EventType {
|
||||
PageCreated,
|
||||
PageUpdated,
|
||||
BlockInserted,
|
||||
BlockMoved,
|
||||
BlockUpdated,
|
||||
BlockDeleted,
|
||||
AssetVersionAdded,
|
||||
ReferenceCreated,
|
||||
TaskUpdated,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DomainEvent {
|
||||
pub event_id: EventId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub command_log_id: CommandLogId,
|
||||
pub aggregate_type: String,
|
||||
pub aggregate_id: String,
|
||||
pub event_type: EventType,
|
||||
pub before_revision: Option<Revision>,
|
||||
pub after_revision: Option<Revision>,
|
||||
pub payload_json: String,
|
||||
pub created_at: Timestamp,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
macro_rules! define_id {
|
||||
($name:ident) => {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct $name(pub String);
|
||||
|
||||
impl $name {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
define_id!(WorkspaceId);
|
||||
define_id!(PageId);
|
||||
define_id!(BlockId);
|
||||
define_id!(AssetId);
|
||||
define_id!(AssetVersionId);
|
||||
define_id!(ReferenceId);
|
||||
define_id!(TaskId);
|
||||
define_id!(AgentSessionId);
|
||||
define_id!(CommandLogId);
|
||||
define_id!(EventId);
|
||||
@@ -0,0 +1,34 @@
|
||||
pub mod audit;
|
||||
pub mod entities;
|
||||
pub mod ids;
|
||||
pub mod time;
|
||||
pub mod types;
|
||||
|
||||
pub use audit::{ActorRef, AuditInfo, ChangeReason, RefLink, SourceKind};
|
||||
pub use entities::{
|
||||
AgentSession, Asset, AssetKind, AssetStatus, AssetVersion, Block, BlockType, CommandLog,
|
||||
CommandStatus, DomainEvent, EventType, Page, PageStatus, PageType, Reference, ReferenceKind,
|
||||
Task, TaskPriority, TaskStatus, TaskType, Workspace,
|
||||
};
|
||||
pub use ids::{
|
||||
AgentSessionId, AssetId, AssetVersionId, BlockId, CommandLogId, EventId, PageId, ReferenceId,
|
||||
TaskId, WorkspaceId,
|
||||
};
|
||||
pub use time::Timestamp;
|
||||
pub use types::{Locale, Revision};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn revision_initial_is_one() {
|
||||
assert_eq!(Revision::initial().0, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_id_keeps_original_value() {
|
||||
let workspace_id = WorkspaceId::new("ws_demo");
|
||||
assert_eq!(workspace_id.as_str(), "ws_demo");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Timestamp(pub String);
|
||||
|
||||
impl Timestamp {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Revision(pub u64);
|
||||
|
||||
impl Revision {
|
||||
pub fn initial() -> Self {
|
||||
Self(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Locale(pub String);
|
||||
|
||||
impl Locale {
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "core-protocol"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
core-domain = { path = "../core-domain" }
|
||||
@@ -0,0 +1,8 @@
|
||||
# core-protocol
|
||||
|
||||
阶段 1 协议模型 crate。
|
||||
|
||||
职责说明请先看:
|
||||
- ../../design/phase1-d-command-api-v0.md
|
||||
- ../../design/phase1-d-query-api-v0.md
|
||||
- ../../design/phase1-d-tool-api-constraints-v0.md
|
||||
@@ -0,0 +1,101 @@
|
||||
use crate::common::{ActorPayload, AffectedObject, SourcePayload, TargetRef};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandEnvelope<T> {
|
||||
pub name: String,
|
||||
pub command_id: String,
|
||||
pub idempotency_key: Option<String>,
|
||||
pub actor: ActorPayload,
|
||||
pub source: SourcePayload,
|
||||
pub target: Option<TargetRef>,
|
||||
pub payload: T,
|
||||
pub reason: Option<String>,
|
||||
pub refs: Vec<String>,
|
||||
pub dry_run: bool,
|
||||
pub validate_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandResult {
|
||||
pub ok: bool,
|
||||
pub command_id: String,
|
||||
pub event_ids: Vec<String>,
|
||||
pub affected_objects: Vec<AffectedObject>,
|
||||
pub revision: Option<u64>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreateWorkspace {
|
||||
pub name: String,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CreatePage {
|
||||
pub title: String,
|
||||
pub parent_page_id: Option<String>,
|
||||
pub position: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdatePageTitle {
|
||||
pub page_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdatePageStats {
|
||||
pub page_id: String,
|
||||
pub word_count: i64,
|
||||
pub character_count: i64,
|
||||
pub block_count: i64,
|
||||
pub todo_total: i64,
|
||||
pub todo_done: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdatePageOptions {
|
||||
pub page_id: String,
|
||||
pub wide_layout: Option<bool>,
|
||||
pub small_text: Option<bool>,
|
||||
pub show_heading_numbers: Option<bool>,
|
||||
pub show_toc: Option<bool>,
|
||||
pub show_structure: Option<bool>,
|
||||
pub protect_editing: Option<bool>,
|
||||
pub show_word_count: Option<bool>,
|
||||
pub collapse_backlinks: Option<bool>,
|
||||
pub page_font: Option<String>,
|
||||
pub layout_density: Option<String>,
|
||||
pub hide_child_pages: Option<bool>,
|
||||
pub show_block_ref_count: Option<bool>,
|
||||
pub embed_default_block_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct InsertBlock {
|
||||
pub page_id: String,
|
||||
pub block_type: String,
|
||||
pub content: String,
|
||||
pub parent_block_id: Option<String>,
|
||||
pub prev_block_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct UpdateBlock {
|
||||
pub block_id: String,
|
||||
pub patch_content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MoveBlock {
|
||||
pub block_id: String,
|
||||
pub new_parent_block_id: Option<String>,
|
||||
pub new_page_id: Option<String>,
|
||||
pub prev_block_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DeleteBlock {
|
||||
pub block_id: String,
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ActorPayload {
|
||||
pub actor_type: String,
|
||||
pub actor_id: String,
|
||||
pub session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SourcePayload {
|
||||
pub channel: String,
|
||||
pub client: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TargetRef {
|
||||
pub workspace_id: Option<String>,
|
||||
pub page_id: Option<String>,
|
||||
pub block_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RequestMeta {
|
||||
pub idempotency_key: Option<String>,
|
||||
pub validate_only: bool,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AffectedObject {
|
||||
pub object_type: String,
|
||||
pub object_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ResponseMeta {
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct OkPayload<T> {
|
||||
pub data: T,
|
||||
pub meta: ResponseMeta,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorDetail {
|
||||
pub field: Option<String>,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ErrorPayload {
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
pub details: Vec<ErrorDetail>,
|
||||
pub retryable: bool,
|
||||
pub meta: ResponseMeta,
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AccessContext {
|
||||
pub tenant_id: Option<String>,
|
||||
pub workspace_id: String,
|
||||
pub actor_id: String,
|
||||
pub actor_type: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AccessDecision {
|
||||
Allow,
|
||||
Deny(AccessDenyReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AccessDenyReason {
|
||||
MissingTenant,
|
||||
MissingWorkspace,
|
||||
PermissionDenied,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum JobStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Succeeded,
|
||||
Failed,
|
||||
RetryScheduled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct JobTicket {
|
||||
pub job_id: String,
|
||||
pub job_type: String,
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
pub workspace_id: String,
|
||||
pub status: JobStatus,
|
||||
}
|
||||
|
||||
impl JobTicket {
|
||||
pub fn new(
|
||||
job_id: impl Into<String>,
|
||||
job_type: impl Into<String>,
|
||||
request_id: impl Into<String>,
|
||||
trace_id: impl Into<String>,
|
||||
workspace_id: impl Into<String>,
|
||||
status: JobStatus,
|
||||
) -> Self {
|
||||
Self {
|
||||
job_id: job_id.into(),
|
||||
job_type: job_type.into(),
|
||||
request_id: request_id.into(),
|
||||
trace_id: trace_id.into(),
|
||||
workspace_id: workspace_id.into(),
|
||||
status,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decide_access(context: &AccessContext) -> AccessDecision {
|
||||
if context.tenant_id.is_none() {
|
||||
return AccessDecision::Deny(AccessDenyReason::MissingTenant);
|
||||
}
|
||||
if context.workspace_id.trim().is_empty() {
|
||||
return AccessDecision::Deny(AccessDenyReason::MissingWorkspace);
|
||||
}
|
||||
AccessDecision::Allow
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn access_requires_tenant() {
|
||||
let context = AccessContext {
|
||||
tenant_id: None,
|
||||
workspace_id: "ws-1".to_string(),
|
||||
actor_id: "user-1".to_string(),
|
||||
actor_type: "human".to_string(),
|
||||
source: "react-next".to_string(),
|
||||
};
|
||||
assert_eq!(
|
||||
decide_access(&context),
|
||||
AccessDecision::Deny(AccessDenyReason::MissingTenant)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn access_allows_valid_context() {
|
||||
let context = AccessContext {
|
||||
tenant_id: Some("tenant-1".to_string()),
|
||||
workspace_id: "ws-1".to_string(),
|
||||
actor_id: "user-1".to_string(),
|
||||
actor_type: "human".to_string(),
|
||||
source: "react-next".to_string(),
|
||||
};
|
||||
assert_eq!(decide_access(&context), AccessDecision::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn job_ticket_keeps_trace_fields() {
|
||||
let ticket = JobTicket::new(
|
||||
"job-1",
|
||||
"rebuild-index",
|
||||
"req-1",
|
||||
"trace-1",
|
||||
"ws-1",
|
||||
JobStatus::Pending,
|
||||
);
|
||||
assert_eq!(ticket.trace_id, "trace-1");
|
||||
assert_eq!(ticket.status, JobStatus::Pending);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
pub mod command;
|
||||
pub mod common;
|
||||
pub mod governance;
|
||||
pub mod query;
|
||||
pub mod tool;
|
||||
|
||||
pub use command::{
|
||||
CommandEnvelope, CreatePage, CreateWorkspace, DeleteBlock, InsertBlock, MoveBlock,
|
||||
UpdateBlock, UpdatePageStats, UpdatePageTitle,
|
||||
};
|
||||
pub use common::{
|
||||
ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta,
|
||||
SourcePayload, TargetRef,
|
||||
};
|
||||
pub use query::{GetPage, ListPageBlocks, QueryEnvelope, SearchBlocks, SearchPages};
|
||||
pub use tool::{InvocationKind, ToolInvocation};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn command_envelope_keeps_validate_only_flag() {
|
||||
let envelope = CommandEnvelope::<CreateWorkspace> {
|
||||
name: "create_workspace".into(),
|
||||
command_id: "cmd_1".into(),
|
||||
idempotency_key: Some("idem_1".into()),
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "cli".into(),
|
||||
client: "mnote-cli".into(),
|
||||
},
|
||||
target: None,
|
||||
payload: CreateWorkspace {
|
||||
name: "demo".into(),
|
||||
slug: Some("demo".into()),
|
||||
},
|
||||
reason: Some("初始化工作区".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: true,
|
||||
};
|
||||
|
||||
assert!(envelope.validate_only);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QueryEnvelope<T> {
|
||||
pub name: String,
|
||||
pub payload: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pagination {
|
||||
pub limit: u32,
|
||||
pub cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GetPage {
|
||||
pub page_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListPageBlocks {
|
||||
pub page_id: String,
|
||||
pub pagination: Pagination,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SearchPages {
|
||||
pub query: String,
|
||||
pub workspace_id: Option<String>,
|
||||
pub pagination: Pagination,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SearchBlocks {
|
||||
pub query: String,
|
||||
pub page_id: Option<String>,
|
||||
pub pagination: Pagination,
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InvocationKind {
|
||||
Command,
|
||||
Query,
|
||||
Job,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ToolInvocation {
|
||||
pub tool: String,
|
||||
pub kind: InvocationKind,
|
||||
pub args_json: String,
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "event-log"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
core-domain = { path = "../core-domain" }
|
||||
@@ -0,0 +1,8 @@
|
||||
# event-log
|
||||
|
||||
阶段 1 事件日志 crate。
|
||||
|
||||
职责说明请先看:
|
||||
- ../../design/phase1-e-command-log-v0.md
|
||||
- ../../design/phase1-e-domain-event-v0.md
|
||||
- ../../design/phase1-e-event-generation-rules-v0.md
|
||||
@@ -0,0 +1,29 @@
|
||||
use core_domain::Timestamp;
|
||||
use core_domain::{RefLink, WorkspaceId};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommandLogStatus {
|
||||
Pending,
|
||||
Succeeded,
|
||||
Failed,
|
||||
RolledBack,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CommandLogRecord {
|
||||
pub command_log_id: String,
|
||||
pub command_name: String,
|
||||
pub actor_type: String,
|
||||
pub actor_id: String,
|
||||
pub source: String,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub target_objects: Vec<RefLink>,
|
||||
pub payload_summary: String,
|
||||
pub refs_json: String,
|
||||
pub idempotency_key: Option<String>,
|
||||
pub status: CommandLogStatus,
|
||||
pub created_at: Timestamp,
|
||||
pub finished_at: Option<Timestamp>,
|
||||
pub trace_id: String,
|
||||
pub request_id: String,
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use core_domain::Timestamp;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EventStatus {
|
||||
Pending,
|
||||
Committed,
|
||||
Rejected,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DomainEventRecord {
|
||||
pub event_id: String,
|
||||
pub workspace_id: String,
|
||||
pub aggregate_type: String,
|
||||
pub aggregate_id: String,
|
||||
pub event_type: String,
|
||||
pub event_version: u32,
|
||||
pub payload_json: String,
|
||||
pub command_log_id: String,
|
||||
pub actor_type: String,
|
||||
pub created_at: Timestamp,
|
||||
pub status: EventStatus,
|
||||
pub trace_id: String,
|
||||
pub request_id: String,
|
||||
pub command_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EventGenerationOutcome {
|
||||
pub command_log_id: String,
|
||||
pub emitted_event_ids: Vec<String>,
|
||||
pub generated_for_replay: bool,
|
||||
pub generated_for_audit: bool,
|
||||
pub generated_for_index_catchup: bool,
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
pub mod command_log;
|
||||
pub mod domain_event;
|
||||
pub mod rules;
|
||||
|
||||
pub use command_log::{CommandLogRecord, CommandLogStatus};
|
||||
pub use domain_event::{DomainEventRecord, EventGenerationOutcome, EventStatus};
|
||||
pub use rules::{should_emit_domain_event, EventGenerationRule};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn failed_command_does_not_emit_success_event() {
|
||||
let should_emit = should_emit_domain_event(&EventGenerationRule {
|
||||
command_succeeded: false,
|
||||
changed_domain_state: false,
|
||||
rolled_back: false,
|
||||
});
|
||||
|
||||
assert!(!should_emit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EventGenerationRule {
|
||||
pub command_succeeded: bool,
|
||||
pub changed_domain_state: bool,
|
||||
pub rolled_back: bool,
|
||||
}
|
||||
|
||||
pub fn should_emit_domain_event(rule: &EventGenerationRule) -> bool {
|
||||
rule.command_succeeded && rule.changed_domain_state && !rule.rolled_back
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "index-fts"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
core-domain = { path = "../core-domain" }
|
||||
event-log = { path = "../event-log" }
|
||||
@@ -0,0 +1,6 @@
|
||||
# index-fts
|
||||
|
||||
阶段 1 骨架 crate。
|
||||
|
||||
职责说明请先看:
|
||||
- ../../design/phase1-b-crate-responsibilities-v0.md
|
||||
@@ -0,0 +1,355 @@
|
||||
use event_log::DomainEventRecord;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum IndexedEntityKind {
|
||||
PageTitle,
|
||||
PageSummary,
|
||||
BlockContent,
|
||||
BlockPath,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexedDocument {
|
||||
pub workspace_id: String,
|
||||
pub entity_kind: IndexedEntityKind,
|
||||
pub entity_id: String,
|
||||
pub parent_id: Option<String>,
|
||||
pub title: Option<String>,
|
||||
pub content: String,
|
||||
pub updated_at: String,
|
||||
pub source_event_id: Option<String>,
|
||||
pub revision: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct IndexCursor {
|
||||
pub workspace_id: String,
|
||||
pub last_processed_event_id: String,
|
||||
pub last_processed_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SearchHit {
|
||||
pub workspace_id: String,
|
||||
pub entity_kind: IndexedEntityKind,
|
||||
pub entity_id: String,
|
||||
pub snippet: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SearchResultSet {
|
||||
pub query: String,
|
||||
pub workspace_id: Option<String>,
|
||||
pub page_id: Option<String>,
|
||||
pub hits: Vec<SearchHit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProjectedDocumentBatch {
|
||||
pub workspace_id: String,
|
||||
pub source_event_id: String,
|
||||
pub documents: Vec<IndexedDocument>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ProjectionResult {
|
||||
pub cursor: IndexCursor,
|
||||
pub batches: Vec<ProjectedDocumentBatch>,
|
||||
}
|
||||
|
||||
pub trait DomainEventProjector {
|
||||
fn project(&self, event: &DomainEventRecord) -> Vec<IndexedDocument>;
|
||||
}
|
||||
|
||||
pub fn supported_index_objects() -> Vec<IndexedEntityKind> {
|
||||
vec![
|
||||
IndexedEntityKind::PageTitle,
|
||||
IndexedEntityKind::PageSummary,
|
||||
IndexedEntityKind::BlockContent,
|
||||
IndexedEntityKind::BlockPath,
|
||||
]
|
||||
}
|
||||
|
||||
pub fn can_rebuild_from_events(cursor: &IndexCursor) -> bool {
|
||||
!cursor.workspace_id.trim().is_empty() && !cursor.last_processed_event_id.trim().is_empty()
|
||||
}
|
||||
|
||||
pub fn advance_workspace_cursor(
|
||||
cursor: &IndexCursor,
|
||||
event_id: impl Into<String>,
|
||||
processed_at: impl Into<String>,
|
||||
) -> IndexCursor {
|
||||
IndexCursor {
|
||||
workspace_id: cursor.workspace_id.clone(),
|
||||
last_processed_event_id: event_id.into(),
|
||||
last_processed_at: processed_at.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_domain_event<P: DomainEventProjector>(
|
||||
projector: &P,
|
||||
event: &DomainEventRecord,
|
||||
cursor: &IndexCursor,
|
||||
) -> ProjectionResult {
|
||||
let documents = projector.project(event);
|
||||
ProjectionResult {
|
||||
cursor: advance_workspace_cursor(
|
||||
cursor,
|
||||
event.event_id.clone(),
|
||||
event.created_at.as_str().to_string(),
|
||||
),
|
||||
batches: if documents.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![ProjectedDocumentBatch {
|
||||
workspace_id: event.workspace_id.clone(),
|
||||
source_event_id: event.event_id.clone(),
|
||||
documents,
|
||||
}]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rebuild_from_events<P: DomainEventProjector>(
|
||||
projector: &P,
|
||||
events: &[DomainEventRecord],
|
||||
cursor: &IndexCursor,
|
||||
) -> ProjectionResult {
|
||||
let mut next_cursor = cursor.clone();
|
||||
let mut batches = Vec::new();
|
||||
for event in events {
|
||||
if event.workspace_id != cursor.workspace_id {
|
||||
continue;
|
||||
}
|
||||
let projected = projector.project(event);
|
||||
next_cursor = advance_workspace_cursor(
|
||||
&next_cursor,
|
||||
event.event_id.clone(),
|
||||
event.created_at.as_str().to_string(),
|
||||
);
|
||||
if !projected.is_empty() {
|
||||
batches.push(ProjectedDocumentBatch {
|
||||
workspace_id: event.workspace_id.clone(),
|
||||
source_event_id: event.event_id.clone(),
|
||||
documents: projected,
|
||||
});
|
||||
}
|
||||
}
|
||||
ProjectionResult {
|
||||
cursor: next_cursor,
|
||||
batches,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_pages(
|
||||
query: &str,
|
||||
workspace_id: Option<&str>,
|
||||
documents: &[IndexedDocument],
|
||||
limit: usize,
|
||||
) -> SearchResultSet {
|
||||
let query = query.trim().to_lowercase();
|
||||
let hits = documents
|
||||
.iter()
|
||||
.filter(|document| matches!(document.entity_kind, IndexedEntityKind::PageTitle | IndexedEntityKind::PageSummary))
|
||||
.filter(|document| workspace_id.map_or(true, |workspace| document.workspace_id == workspace))
|
||||
.filter(|document| document.title.as_deref().unwrap_or("").to_lowercase().contains(&query)
|
||||
|| document.content.to_lowercase().contains(&query))
|
||||
.take(limit)
|
||||
.map(|document| SearchHit {
|
||||
workspace_id: document.workspace_id.clone(),
|
||||
entity_kind: document.entity_kind.clone(),
|
||||
entity_id: document.entity_id.clone(),
|
||||
snippet: Some(document.content.chars().take(120).collect()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
SearchResultSet {
|
||||
query: query.into(),
|
||||
workspace_id: workspace_id.map(|value| value.into()),
|
||||
page_id: None,
|
||||
hits,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_blocks(
|
||||
query: &str,
|
||||
page_id: Option<&str>,
|
||||
documents: &[IndexedDocument],
|
||||
limit: usize,
|
||||
) -> SearchResultSet {
|
||||
let query = query.trim().to_lowercase();
|
||||
let hits = documents
|
||||
.iter()
|
||||
.filter(|document| matches!(document.entity_kind, IndexedEntityKind::BlockContent | IndexedEntityKind::BlockPath))
|
||||
.filter(|document| page_id.map_or(true, |page| document.parent_id.as_deref() == Some(page) || document.entity_id == page))
|
||||
.filter(|document| document.content.to_lowercase().contains(&query))
|
||||
.take(limit)
|
||||
.map(|document| SearchHit {
|
||||
workspace_id: document.workspace_id.clone(),
|
||||
entity_kind: document.entity_kind.clone(),
|
||||
entity_id: document.entity_id.clone(),
|
||||
snippet: Some(document.content.chars().take(120).collect()),
|
||||
})
|
||||
.collect();
|
||||
|
||||
SearchResultSet {
|
||||
query: query.into(),
|
||||
workspace_id: None,
|
||||
page_id: page_id.map(|value| value.into()),
|
||||
hits,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MinimalWorkspaceProjector;
|
||||
|
||||
impl DomainEventProjector for MinimalWorkspaceProjector {
|
||||
fn project(&self, event: &DomainEventRecord) -> Vec<IndexedDocument> {
|
||||
let content = event.payload_json.trim();
|
||||
let (entity_kind, title, parent_id) = match event.aggregate_type.as_str() {
|
||||
"page" => (
|
||||
IndexedEntityKind::PageTitle,
|
||||
Some(event.event_type.clone()),
|
||||
None,
|
||||
),
|
||||
"block" => (
|
||||
IndexedEntityKind::BlockContent,
|
||||
None,
|
||||
Some(event.aggregate_id.clone()),
|
||||
),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
vec![IndexedDocument {
|
||||
workspace_id: event.workspace_id.clone(),
|
||||
entity_kind,
|
||||
entity_id: event.aggregate_id.clone(),
|
||||
parent_id,
|
||||
title,
|
||||
content: content.to_string(),
|
||||
updated_at: event.created_at.as_str().to_string(),
|
||||
source_event_id: Some(event.event_id.clone()),
|
||||
revision: Some(event.event_version as u64),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_domain::Timestamp;
|
||||
use event_log::EventStatus;
|
||||
|
||||
fn cursor() -> IndexCursor {
|
||||
IndexCursor {
|
||||
workspace_id: "ws_1".into(),
|
||||
last_processed_event_id: "evt_0".into(),
|
||||
last_processed_at: "2026-04-11T00:00:00Z".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn event(
|
||||
workspace_id: &str,
|
||||
aggregate_type: &str,
|
||||
aggregate_id: &str,
|
||||
event_id: &str,
|
||||
event_type: &str,
|
||||
) -> DomainEventRecord {
|
||||
DomainEventRecord {
|
||||
event_id: event_id.into(),
|
||||
workspace_id: workspace_id.into(),
|
||||
aggregate_type: aggregate_type.into(),
|
||||
aggregate_id: aggregate_id.into(),
|
||||
event_type: event_type.into(),
|
||||
event_version: 1,
|
||||
payload_json: "{\"text\":\"hello\"}".into(),
|
||||
command_log_id: "cmd_1".into(),
|
||||
actor_type: "human".into(),
|
||||
created_at: Timestamp::new("2026-04-11T00:00:01Z"),
|
||||
status: EventStatus::Committed,
|
||||
trace_id: "trace_1".into(),
|
||||
request_id: "req_1".into(),
|
||||
command_id: "cmd_1".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_page_and_block_projection_objects() {
|
||||
assert_eq!(
|
||||
supported_index_objects(),
|
||||
vec![
|
||||
IndexedEntityKind::PageTitle,
|
||||
IndexedEntityKind::PageSummary,
|
||||
IndexedEntityKind::BlockContent,
|
||||
IndexedEntityKind::BlockPath
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_advances_incrementally() {
|
||||
let next = advance_workspace_cursor(&cursor(), "evt_2", "2026-04-11T00:00:02Z");
|
||||
assert_eq!(next.last_processed_event_id, "evt_2");
|
||||
assert_eq!(next.workspace_id, "ws_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projector_builds_real_documents_for_page_and_block() {
|
||||
let projector = MinimalWorkspaceProjector;
|
||||
let page_docs =
|
||||
projector.project(&event("ws_1", "page", "page_1", "evt_1", "page.created"));
|
||||
let block_docs =
|
||||
projector.project(&event("ws_1", "block", "block_1", "evt_2", "block.created"));
|
||||
assert_eq!(page_docs.len(), 1);
|
||||
assert_eq!(block_docs.len(), 1);
|
||||
assert_eq!(page_docs[0].entity_kind, IndexedEntityKind::PageTitle);
|
||||
assert_eq!(block_docs[0].entity_kind, IndexedEntityKind::BlockContent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rebuild_skips_other_workspaces_and_keeps_cursor() {
|
||||
let projector = MinimalWorkspaceProjector;
|
||||
let events = vec![
|
||||
event("ws_2", "page", "page_x", "evt_x", "page.created"),
|
||||
event("ws_1", "block", "block_1", "evt_1", "block.created"),
|
||||
];
|
||||
let result = rebuild_from_events(&projector, &events, &cursor());
|
||||
assert_eq!(result.batches.len(), 1);
|
||||
assert_eq!(result.cursor.last_processed_event_id, "evt_1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn search_helpers_return_hits() {
|
||||
let docs = vec![
|
||||
IndexedDocument {
|
||||
workspace_id: "ws_1".into(),
|
||||
entity_kind: IndexedEntityKind::PageTitle,
|
||||
entity_id: "page_1".into(),
|
||||
parent_id: None,
|
||||
title: Some("Rust Notes".into()),
|
||||
content: "Rust notes for phase 2".into(),
|
||||
updated_at: "2026-04-11T00:00:01Z".into(),
|
||||
source_event_id: Some("evt_1".into()),
|
||||
revision: Some(1),
|
||||
},
|
||||
IndexedDocument {
|
||||
workspace_id: "ws_1".into(),
|
||||
entity_kind: IndexedEntityKind::BlockContent,
|
||||
entity_id: "block_1".into(),
|
||||
parent_id: Some("page_1".into()),
|
||||
title: None,
|
||||
content: "A block about rust search indexing".into(),
|
||||
updated_at: "2026-04-11T00:00:02Z".into(),
|
||||
source_event_id: Some("evt_2".into()),
|
||||
revision: Some(1),
|
||||
},
|
||||
];
|
||||
|
||||
let pages = search_pages("rust", Some("ws_1"), &docs, 10);
|
||||
assert_eq!(pages.hits.len(), 1);
|
||||
assert_eq!(pages.hits[0].entity_id, "page_1");
|
||||
|
||||
let blocks = search_blocks("search", Some("page_1"), &docs, 10);
|
||||
assert_eq!(blocks.hits.len(), 1);
|
||||
assert_eq!(blocks.hits[0].entity_id, "block_1");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "storage-convex-bridge"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
core-domain = { path = "../core-domain" }
|
||||
core-protocol = { path = "../core-protocol" }
|
||||
event-log = { path = "../event-log" }
|
||||
@@ -0,0 +1,18 @@
|
||||
# storage-convex-bridge
|
||||
|
||||
阶段 1 的 Convex 桥接 crate。
|
||||
|
||||
当前职责:
|
||||
- 把 Rust 协议对象映射为 Convex 读写请求
|
||||
- 透传 request / actor / workspace / auth 上下文
|
||||
- 生成最小 CommandLog / DomainEvent 骨架
|
||||
|
||||
当前明确不做:
|
||||
- 不复制主事实层
|
||||
- 不维护第二数据库
|
||||
- 不隐藏 tenant / auth / request context
|
||||
- 不承载复杂业务裁决
|
||||
|
||||
相关设计文档:
|
||||
- `../../design/phase1-f-bridge-responsibilities-v0.md`
|
||||
- `../../design/phase1-f-write-read-flow-v0.md`
|
||||
@@ -0,0 +1,18 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BridgeContext {
|
||||
pub deployment_id: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
pub actor_type: String,
|
||||
pub actor_id: String,
|
||||
pub session_id: Option<String>,
|
||||
pub tenant_id: Option<String>,
|
||||
pub auth_token: Option<String>,
|
||||
pub source_channel: String,
|
||||
pub source_client: String,
|
||||
pub idempotency_key: Option<String>,
|
||||
pub validate_only: bool,
|
||||
pub dry_run: bool,
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
pub mod context;
|
||||
pub mod mapping;
|
||||
pub mod read_path;
|
||||
pub mod types;
|
||||
pub mod validation;
|
||||
pub mod write_path;
|
||||
|
||||
pub use context::BridgeContext;
|
||||
pub use mapping::{
|
||||
map_command_name_to_convex, map_query_name_to_convex, ConvexMutationRequest, ConvexQueryRequest,
|
||||
};
|
||||
pub use read_path::{build_query_request, QueryReadResult};
|
||||
pub use types::{BridgeError, BridgeErrorKind, BridgeResult};
|
||||
pub use validation::{validate_command_envelope, validate_query_envelope};
|
||||
pub use write_path::{
|
||||
build_command_log_record, build_domain_event_record, build_write_pipeline, build_write_request,
|
||||
WritePipelineResult,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use core_protocol::{
|
||||
command::{CreatePage, CreateWorkspace, UpdatePageOptions, UpdatePageStats, UpdatePageTitle},
|
||||
query::GetPage,
|
||||
ActorPayload, CommandEnvelope, QueryEnvelope, SourcePayload, TargetRef,
|
||||
};
|
||||
|
||||
fn demo_context() -> BridgeContext {
|
||||
BridgeContext {
|
||||
deployment_id: Some("dep_1".into()),
|
||||
project_id: Some("proj_1".into()),
|
||||
request_id: "req_1".into(),
|
||||
trace_id: "trace_1".into(),
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
workspace_id: Some("ws_1".into()),
|
||||
tenant_id: Some("tenant_1".into()),
|
||||
auth_token: Some("token_1".into()),
|
||||
source_channel: "cli".into(),
|
||||
source_client: "mnote-cli".into(),
|
||||
idempotency_key: Some("idem_ctx".into()),
|
||||
validate_only: false,
|
||||
dry_run: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_write_request_keeps_workspace_scope() {
|
||||
let command = CommandEnvelope {
|
||||
name: "create_page".into(),
|
||||
command_id: "cmd_1".into(),
|
||||
idempotency_key: Some("idem_cmd".into()),
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "cli".into(),
|
||||
client: "mnote-cli".into(),
|
||||
},
|
||||
target: Some(TargetRef {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: None,
|
||||
block_id: None,
|
||||
}),
|
||||
payload: CreatePage {
|
||||
title: "第一页".into(),
|
||||
parent_page_id: None,
|
||||
position: None,
|
||||
},
|
||||
reason: Some("初始化页面".into()),
|
||||
refs: vec!["spec:phase1-f".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
|
||||
let request = build_write_request(&demo_context(), &command).expect("write request should build");
|
||||
assert_eq!(request.function_name, "pages:create");
|
||||
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
|
||||
assert_eq!(request.deployment_id.as_deref(), Some("dep_1"));
|
||||
assert_eq!(request.project_id.as_deref(), Some("proj_1"));
|
||||
assert_eq!(request.idempotency_key.as_deref(), Some("idem_cmd"));
|
||||
assert!(request.payload_json.contains("\"kind\":\"command\""));
|
||||
assert!(request.payload_json.contains("\"name\":\"create_page\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_query_request_includes_trace_and_workspace() {
|
||||
let query = QueryEnvelope {
|
||||
name: "get_page".into(),
|
||||
payload: GetPage {
|
||||
page_id: "page_1".into(),
|
||||
},
|
||||
};
|
||||
|
||||
let request = build_query_request(&demo_context(), &query).expect("query request should build");
|
||||
assert_eq!(request.function_name, "pages:get");
|
||||
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
|
||||
assert_eq!(request.request_id, "req_1");
|
||||
assert_eq!(request.trace_id, "trace_1");
|
||||
assert!(request.payload_json.contains("\"kind\":\"query\""));
|
||||
assert!(request.payload_json.contains("\"name\":\"get_page\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_write_pipeline_sets_trace_fields() {
|
||||
let command = CommandEnvelope {
|
||||
name: "create_workspace".into(),
|
||||
command_id: "cmd_2".into(),
|
||||
idempotency_key: None,
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "cli".into(),
|
||||
client: "mnote-cli".into(),
|
||||
},
|
||||
target: None,
|
||||
payload: CreateWorkspace {
|
||||
name: "demo".into(),
|
||||
slug: Some("demo".into()),
|
||||
},
|
||||
reason: Some("初始化工作区".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
|
||||
let pipeline = build_write_pipeline(&demo_context(), &command).expect("pipeline should build");
|
||||
assert_eq!(pipeline.command_log.command_name, "create_workspace");
|
||||
assert!(pipeline.command_log.payload_summary.contains("request_id=req_1"));
|
||||
assert_eq!(pipeline.command_log.trace_id, "trace_1");
|
||||
assert_eq!(pipeline.command_log.request_id, "req_1");
|
||||
assert!(pipeline.domain_event.payload_json.contains("\"trace_id\":\"trace_1\""));
|
||||
assert_eq!(pipeline.result.event_ids, vec!["evt_cmd_2".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_title_command_maps_to_documents_update_title() {
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.title.update".into(),
|
||||
command_id: "cmd_title_1".into(),
|
||||
idempotency_key: Some("idem_title".into()),
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(TargetRef {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("page_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: UpdatePageTitle {
|
||||
page_id: "page_1".into(),
|
||||
title: "新标题".into(),
|
||||
},
|
||||
reason: None,
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
|
||||
let request = build_write_request(&demo_context(), &command).expect("write request should build");
|
||||
assert_eq!(request.function_name, "documents:updateTitle");
|
||||
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
|
||||
assert!(request.payload_json.contains("\"name\":\"documents.title.update\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_stats_command_maps_to_documents_update_stats() {
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.stats.update".into(),
|
||||
command_id: "cmd_stats_1".into(),
|
||||
idempotency_key: Some("idem_stats".into()),
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(TargetRef {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("page_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: UpdatePageStats {
|
||||
page_id: "page_1".into(),
|
||||
word_count: 10,
|
||||
character_count: 20,
|
||||
block_count: 3,
|
||||
todo_total: 4,
|
||||
todo_done: 1,
|
||||
},
|
||||
reason: None,
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
|
||||
let request = build_write_request(&demo_context(), &command).expect("write request should build");
|
||||
assert_eq!(request.function_name, "documents:updateStats");
|
||||
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
|
||||
assert!(request.payload_json.contains("\"name\":\"documents.stats.update\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_options_command_maps_to_documents_update_options() {
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.options.update".into(),
|
||||
command_id: "cmd_options_1".into(),
|
||||
idempotency_key: Some("idem_options".into()),
|
||||
actor: ActorPayload {
|
||||
actor_type: "human".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("session_1".into()),
|
||||
},
|
||||
source: SourcePayload {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(TargetRef {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("page_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: UpdatePageOptions {
|
||||
page_id: "page_1".into(),
|
||||
wide_layout: None,
|
||||
small_text: None,
|
||||
show_heading_numbers: None,
|
||||
show_toc: Some(true),
|
||||
show_structure: None,
|
||||
protect_editing: None,
|
||||
show_word_count: None,
|
||||
collapse_backlinks: None,
|
||||
page_font: None,
|
||||
layout_density: Some("compact".into()),
|
||||
hide_child_pages: None,
|
||||
show_block_ref_count: None,
|
||||
embed_default_block_id: None,
|
||||
},
|
||||
reason: None,
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
};
|
||||
|
||||
let request = build_write_request(&demo_context(), &command).expect("write request should build");
|
||||
assert_eq!(request.function_name, "documents:updateOptions");
|
||||
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
|
||||
assert!(request.payload_json.contains("\"name\":\"documents.options.update\""));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::context::BridgeContext;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConvexMutationRequest {
|
||||
pub function_name: String,
|
||||
pub deployment_id: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
pub idempotency_key: Option<String>,
|
||||
pub actor_id: String,
|
||||
pub payload_json: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ConvexQueryRequest {
|
||||
pub function_name: String,
|
||||
pub deployment_id: Option<String>,
|
||||
pub project_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
pub actor_id: String,
|
||||
pub payload_json: String,
|
||||
}
|
||||
|
||||
pub fn map_command_name_to_convex(command_name: &str) -> &'static str {
|
||||
match command_name {
|
||||
"create_workspace" => "workspaces:create",
|
||||
"create_page" => "pages:create",
|
||||
"documents.title.update" => "documents:updateTitle",
|
||||
"documents.stats.update" => "documents:updateStats",
|
||||
"documents.options.update" => "documents:updateOptions",
|
||||
"insert_block" => "blocks:insert",
|
||||
"update_block" => "blocks:update",
|
||||
"move_block" => "blocks:move",
|
||||
"delete_block" => "blocks:delete",
|
||||
_ => "commands:unknown",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_query_name_to_convex(query_name: &str) -> &'static str {
|
||||
match query_name {
|
||||
"get_page" => "pages:get",
|
||||
"list_page_blocks" => "blocks:list_by_page",
|
||||
"search_pages" => "search:pages",
|
||||
"search_blocks" => "search:blocks",
|
||||
_ => "queries:unknown",
|
||||
}
|
||||
}
|
||||
|
||||
fn json_opt(value: &Option<String>) -> String {
|
||||
match value {
|
||||
Some(v) => format!("\"{}\"", v),
|
||||
None => "null".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn payload_json_for_command(context: &BridgeContext, command_name: &str) -> String {
|
||||
format!(
|
||||
"{{\"kind\":\"command\",\"name\":\"{}\",\"request_id\":\"{}\",\"trace_id\":\"{}\",\"deployment_id\":{},\"project_id\":{},\"workspace_id\":{},\"tenant_id\":{},\"idempotency_key\":{},\"actor\":{{\"type\":\"{}\",\"id\":\"{}\",\"session_id\":{}}},\"source\":{{\"channel\":\"{}\",\"client\":\"{}\"}}}}",
|
||||
command_name,
|
||||
context.request_id,
|
||||
context.trace_id,
|
||||
json_opt(&context.deployment_id),
|
||||
json_opt(&context.project_id),
|
||||
json_opt(&context.workspace_id),
|
||||
json_opt(&context.tenant_id),
|
||||
json_opt(&context.idempotency_key),
|
||||
context.actor_type,
|
||||
context.actor_id,
|
||||
json_opt(&context.session_id),
|
||||
context.source_channel,
|
||||
context.source_client,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn payload_json_for_query(context: &BridgeContext, query_name: &str) -> String {
|
||||
format!(
|
||||
"{{\"kind\":\"query\",\"name\":\"{}\",\"request_id\":\"{}\",\"trace_id\":\"{}\",\"deployment_id\":{},\"project_id\":{},\"workspace_id\":{},\"tenant_id\":{},\"actor_id\":\"{}\",\"source\":{{\"channel\":\"{}\",\"client\":\"{}\"}}}}",
|
||||
query_name,
|
||||
context.request_id,
|
||||
context.trace_id,
|
||||
json_opt(&context.deployment_id),
|
||||
json_opt(&context.project_id),
|
||||
json_opt(&context.workspace_id),
|
||||
json_opt(&context.tenant_id),
|
||||
context.actor_id,
|
||||
context.source_channel,
|
||||
context.source_client,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
use crate::context::BridgeContext;
|
||||
use crate::mapping::{map_query_name_to_convex, payload_json_for_query, ConvexQueryRequest};
|
||||
use crate::types::BridgeResult;
|
||||
use crate::validation::validate_query_envelope;
|
||||
use core_protocol::QueryEnvelope;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QueryReadResult {
|
||||
pub workspace_id: Option<String>,
|
||||
pub request_id: String,
|
||||
pub trace_id: String,
|
||||
}
|
||||
|
||||
pub fn build_query_request<T>(
|
||||
context: &BridgeContext,
|
||||
query: &QueryEnvelope<T>,
|
||||
) -> BridgeResult<ConvexQueryRequest> {
|
||||
validate_query_envelope(context, query)?;
|
||||
Ok(ConvexQueryRequest {
|
||||
function_name: map_query_name_to_convex(&query.name).to_string(),
|
||||
deployment_id: context.deployment_id.clone(),
|
||||
project_id: context.project_id.clone(),
|
||||
workspace_id: context.workspace_id.clone(),
|
||||
request_id: context.request_id.clone(),
|
||||
trace_id: context.trace_id.clone(),
|
||||
actor_id: context.actor_id.clone(),
|
||||
payload_json: payload_json_for_query(context, &query.name),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum BridgeErrorKind {
|
||||
Validation,
|
||||
Unauthorized,
|
||||
Conflict,
|
||||
NotFound,
|
||||
Transport,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BridgeError {
|
||||
pub kind: BridgeErrorKind,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl BridgeError {
|
||||
pub fn validation(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: BridgeErrorKind::Validation,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type BridgeResult<T> = Result<T, BridgeError>;
|
||||
@@ -0,0 +1,50 @@
|
||||
use crate::context::BridgeContext;
|
||||
use crate::types::{BridgeError, BridgeResult};
|
||||
use core_protocol::{CommandEnvelope, QueryEnvelope};
|
||||
|
||||
pub fn validate_command_envelope<T>(
|
||||
context: &BridgeContext,
|
||||
command: &CommandEnvelope<T>,
|
||||
) -> BridgeResult<()> {
|
||||
if command.name.trim().is_empty() {
|
||||
return Err(BridgeError::validation("command name 不能为空"));
|
||||
}
|
||||
if command.command_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("command_id 不能为空"));
|
||||
}
|
||||
if context.request_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("request_id 不能为空"));
|
||||
}
|
||||
if context.trace_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("trace_id 不能为空"));
|
||||
}
|
||||
if context.actor_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("actor_id 不能为空"));
|
||||
}
|
||||
if context
|
||||
.workspace_id
|
||||
.as_deref()
|
||||
.unwrap_or("")
|
||||
.trim()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(BridgeError::validation("workspace_id 不能为空"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_query_envelope<T>(
|
||||
context: &BridgeContext,
|
||||
query: &QueryEnvelope<T>,
|
||||
) -> BridgeResult<()> {
|
||||
if query.name.trim().is_empty() {
|
||||
return Err(BridgeError::validation("query name 不能为空"));
|
||||
}
|
||||
if context.request_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("request_id 不能为空"));
|
||||
}
|
||||
if context.trace_id.trim().is_empty() {
|
||||
return Err(BridgeError::validation("trace_id 不能为空"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
use crate::context::BridgeContext;
|
||||
use crate::mapping::{map_command_name_to_convex, payload_json_for_command, ConvexMutationRequest};
|
||||
use crate::types::BridgeResult;
|
||||
use crate::validation::validate_command_envelope;
|
||||
use core_domain::{RefLink, Timestamp, WorkspaceId};
|
||||
use core_protocol::{command::CommandResult, AffectedObject, CommandEnvelope};
|
||||
use event_log::{CommandLogRecord, CommandLogStatus, DomainEventRecord, EventStatus};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WritePipelineResult {
|
||||
pub mutation: ConvexMutationRequest,
|
||||
pub command_log: CommandLogRecord,
|
||||
pub domain_event: DomainEventRecord,
|
||||
pub result: CommandResult,
|
||||
}
|
||||
|
||||
pub fn build_write_request<T>(
|
||||
context: &BridgeContext,
|
||||
command: &CommandEnvelope<T>,
|
||||
) -> BridgeResult<ConvexMutationRequest> {
|
||||
validate_command_envelope(context, command)?;
|
||||
let workspace_id = command
|
||||
.target
|
||||
.as_ref()
|
||||
.and_then(|target| target.workspace_id.clone())
|
||||
.or_else(|| context.workspace_id.clone());
|
||||
Ok(ConvexMutationRequest {
|
||||
function_name: map_command_name_to_convex(&command.name).to_string(),
|
||||
deployment_id: context.deployment_id.clone(),
|
||||
project_id: context.project_id.clone(),
|
||||
workspace_id,
|
||||
request_id: context.request_id.clone(),
|
||||
trace_id: context.trace_id.clone(),
|
||||
idempotency_key: command
|
||||
.idempotency_key
|
||||
.clone()
|
||||
.or_else(|| context.idempotency_key.clone()),
|
||||
actor_id: context.actor_id.clone(),
|
||||
payload_json: payload_json_for_command(context, &command.name),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_command_log_record(
|
||||
command_id: &str,
|
||||
command_name: &str,
|
||||
context: &BridgeContext,
|
||||
workspace_id: &str,
|
||||
) -> CommandLogRecord {
|
||||
CommandLogRecord {
|
||||
command_log_id: format!("clog_{command_id}"),
|
||||
command_name: command_name.into(),
|
||||
actor_type: context.actor_type.clone(),
|
||||
actor_id: context.actor_id.clone(),
|
||||
source: context.source_channel.clone(),
|
||||
workspace_id: WorkspaceId::new(workspace_id),
|
||||
target_objects: vec![RefLink::External(format!("workspace:{workspace_id}"))],
|
||||
payload_summary: format!(
|
||||
"command={command_name};request_id={};trace_id={}",
|
||||
context.request_id, context.trace_id
|
||||
),
|
||||
refs_json: format!(
|
||||
"[\"request:{}\",\"trace:{}\"]",
|
||||
context.request_id, context.trace_id
|
||||
),
|
||||
idempotency_key: context.idempotency_key.clone(),
|
||||
status: CommandLogStatus::Pending,
|
||||
created_at: Timestamp::new("2026-04-11T00:00:00Z"),
|
||||
finished_at: None,
|
||||
trace_id: context.trace_id.clone(),
|
||||
request_id: context.request_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_domain_event_record(
|
||||
command_id: &str,
|
||||
command_name: &str,
|
||||
context: &BridgeContext,
|
||||
workspace_id: &str,
|
||||
) -> DomainEventRecord {
|
||||
DomainEventRecord {
|
||||
event_id: format!("evt_{command_id}"),
|
||||
workspace_id: workspace_id.into(),
|
||||
aggregate_type: "workspace".into(),
|
||||
aggregate_id: workspace_id.into(),
|
||||
event_type: format!("{command_name}_requested"),
|
||||
event_version: 1,
|
||||
payload_json: format!(
|
||||
"{{\"request_id\":\"{}\",\"trace_id\":\"{}\",\"command_id\":\"{}\",\"command_name\":\"{}\"}}",
|
||||
context.request_id, context.trace_id, command_id, command_name
|
||||
),
|
||||
command_log_id: format!("clog_{command_id}"),
|
||||
actor_type: context.actor_type.clone(),
|
||||
created_at: Timestamp::new("2026-04-11T00:00:00Z"),
|
||||
status: EventStatus::Pending,
|
||||
trace_id: context.trace_id.clone(),
|
||||
request_id: context.request_id.clone(),
|
||||
command_id: command_id.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_write_pipeline<T>(
|
||||
context: &BridgeContext,
|
||||
command: &CommandEnvelope<T>,
|
||||
) -> BridgeResult<WritePipelineResult> {
|
||||
let mutation = build_write_request(context, command)?;
|
||||
let workspace_id = mutation
|
||||
.workspace_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "workspace_unknown".into());
|
||||
let command_log =
|
||||
build_command_log_record(&command.command_id, &command.name, context, &workspace_id);
|
||||
let domain_event =
|
||||
build_domain_event_record(&command.command_id, &command.name, context, &workspace_id);
|
||||
let result = CommandResult {
|
||||
ok: true,
|
||||
command_id: command.command_id.clone(),
|
||||
event_ids: vec![domain_event.event_id.clone()],
|
||||
affected_objects: vec![AffectedObject {
|
||||
object_type: "workspace".into(),
|
||||
object_id: workspace_id.clone(),
|
||||
}],
|
||||
revision: Some(1),
|
||||
warnings: vec![],
|
||||
};
|
||||
Ok(WritePipelineResult {
|
||||
mutation,
|
||||
command_log,
|
||||
domain_event,
|
||||
result,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":4704921907237318175,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/lix/.rustup/toolchains/1.88.0-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.88.0 (6b00bc388 2025-06-23)\nbinary: rustc\ncommit-hash: 6b00bc3880198600130e1cf62b8f8a93494488cc\ncommit-date: 2025-06-23\nhost: x86_64-unknown-linux-gnu\nrelease: 1.88.0\nLLVM version: 20.1.5\n","stderr":""}},"successes":{}}
|
||||
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
e053498f67f46e4d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":6167971972065703354,"profile":17672942494452627365,"path":7278239953193670039,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-domain-40e6f74f7ef1a9aa/dep-lib-core_domain","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
31ec3f0d1a2e5472
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":6167971972065703354,"profile":1722584277633009122,"path":7278239953193670039,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-domain-bd799d0a25dcc494/dep-test-lib-core_domain","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
4e803ee3d8e7c709
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":6167971972065703354,"profile":8731458305071235362,"path":7278239953193670039,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-domain-cdff95eec220868e/dep-lib-core_domain","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
44b9888b46dbed5c
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":12929318708107190822,"profile":1722584277633009122,"path":17093070036165809465,"deps":[[12060949818064432287,"core_domain",false,704786785418248270]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-protocol-0fdc11ecf89ad5aa/dep-test-lib-core_protocol","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
69002a3ff9918d3d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":12929318708107190822,"profile":17672942494452627365,"path":17093070036165809465,"deps":[[12060949818064432287,"core_domain",false,5579665713981379552]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-protocol-4592bf3d65d89282/dep-lib-core_protocol","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
49c72387560f483c
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":12929318708107190822,"profile":8731458305071235362,"path":17093070036165809465,"deps":[[12060949818064432287,"core_domain",false,704786785418248270]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/core-protocol-f5d7351438cd4f82/dep-lib-core_protocol","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
a1e984aa5f972227
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":2075902485319387119,"profile":17672942494452627365,"path":3721623360069338606,"deps":[[12060949818064432287,"core_domain",false,5579665713981379552]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/event-log-563618231cacaf7e/dep-lib-event_log","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
8f57032f1a7da02a
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":2075902485319387119,"profile":8731458305071235362,"path":3721623360069338606,"deps":[[12060949818064432287,"core_domain",false,704786785418248270]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/event-log-d9b5c77d292421e8/dep-lib-event_log","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
bf54d8ba9752e161
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":1159665536100219803,"profile":17672942494452627365,"path":12914430766979976756,"deps":[[12060949818064432287,"core_domain",false,5579665713981379552],[16479709356797637466,"event_log",false,2819982753825876385]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/index-fts-e444a40571811be1/dep-lib-index_fts","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
d0e3930355d176f1
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":16973354705979238807,"profile":8731458305071235362,"path":2510431785475190115,"deps":[[7016416594668144918,"core_protocol",false,4343738704907716425],[12060949818064432287,"core_domain",false,704786785418248270],[16479709356797637466,"event_log",false,3071592497278048143]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/storage-convex-bridge-45166d07753c36d7/dep-lib-storage_convex_bridge","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
b564adb2c82f1811
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":16973354705979238807,"profile":1722584277633009122,"path":2510431785475190115,"deps":[[7016416594668144918,"core_protocol",false,4343738704907716425],[12060949818064432287,"core_domain",false,704786785418248270],[16479709356797637466,"event_log",false,3071592497278048143]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/storage-convex-bridge-5bf1d06211b95150/dep-test-lib-storage_convex_bridge","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
+1
@@ -0,0 +1 @@
|
||||
8dd9c30bdd3eb769
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"rustc":11410426090777951712,"features":"[]","declared_features":"[]","target":16973354705979238807,"profile":17672942494452627365,"path":2510431785475190115,"deps":[[7016416594668144918,"core_protocol",false,4435361707722408041],[12060949818064432287,"core_domain",false,5579665713981379552],[16479709356797637466,"event_log",false,2819982753825876385]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/storage-convex-bridge-cd4527dde6c0c1c6/dep-lib-storage_convex_bridge","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
@@ -0,0 +1,10 @@
|
||||
/mnt/Data1T/mnote/rust/target/debug/deps/core_domain-40e6f74f7ef1a9aa.d: crates/core-domain/src/lib.rs crates/core-domain/src/audit.rs crates/core-domain/src/entities.rs crates/core-domain/src/ids.rs crates/core-domain/src/time.rs crates/core-domain/src/types.rs
|
||||
|
||||
/mnt/Data1T/mnote/rust/target/debug/deps/libcore_domain-40e6f74f7ef1a9aa.rmeta: crates/core-domain/src/lib.rs crates/core-domain/src/audit.rs crates/core-domain/src/entities.rs crates/core-domain/src/ids.rs crates/core-domain/src/time.rs crates/core-domain/src/types.rs
|
||||
|
||||
crates/core-domain/src/lib.rs:
|
||||
crates/core-domain/src/audit.rs:
|
||||
crates/core-domain/src/entities.rs:
|
||||
crates/core-domain/src/ids.rs:
|
||||
crates/core-domain/src/time.rs:
|
||||
crates/core-domain/src/types.rs:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user