Compare commits
3 Commits
v0.2026040
...
v0.2026040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89a605262c | ||
|
|
e097c5b717 | ||
|
|
c0df2124ff |
35
CLAUDE.md
35
CLAUDE.md
@@ -10,6 +10,8 @@
|
||||
|
||||
Run `/laputa-next-task` — fetches next task (To Rework first, then Open), moves to In Progress, returns full description.
|
||||
|
||||
**Before writing a single line of code:** run `mcp__codescene__code_health_score` to check the current codebase health against `.codescene-thresholds`. If the score is already below the threshold, **stop and refactor first** — find the worst files with the MCP, improve them, commit, then start the task. Never start feature work on a codebase that is already below the gate.
|
||||
|
||||
- Read task description and all comments fully
|
||||
- For To Rework: the ❌ QA failed comment tells you exactly what to fix
|
||||
- Check `docs/adr/` for relevant architecture decisions before structural choices
|
||||
@@ -45,7 +47,15 @@ bash ~/.openclaw/skills/laputa-qa/scripts/screenshot.sh /tmp/qa-native.png
|
||||
|
||||
Use `osascript` for keyboard interactions. Write result as Todoist comment (✅ or ❌). **⚠️ WKWebView:** `osascript keystroke` blocked inside editor — rely on Playwright for text input features.
|
||||
|
||||
After both phases pass, run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
|
||||
After both phases pass, add a **completion comment** to the Todoist task before running `/laputa-done`. The comment must include:
|
||||
- What was implemented (1–2 lines)
|
||||
- QA: what was tested and how (Playwright / native screenshot / osascript)
|
||||
- Refactoring: any files refactored to meet the CodeScene gate (or "none needed")
|
||||
- ADRs: any new/updated ADRs (or "none")
|
||||
- Docs: any updated docs (ARCHITECTURE.md, ABSTRACTIONS.md, etc.) (or "none")
|
||||
- Code health: final Hotspot and Average scores after push
|
||||
|
||||
Then run `/laputa-done <task_id>` → moves to In Review, notifies Brian, self-dispatches next task.
|
||||
|
||||
---
|
||||
|
||||
@@ -61,7 +71,7 @@ After both phases pass, run `/laputa-done <task_id>` → moves to In Review, not
|
||||
|
||||
Red → Green → Refactor → Commit. One cycle per commit. For bugs: write failing regression test first, then fix. Exception: pure CSS/layout changes.
|
||||
|
||||
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests before adding new ones. Prefer E2E over unit tests for user flows.
|
||||
**Test quality (Kent Beck's Desiderata):** Isolated · Deterministic · Fast · Behavioral · Structure-insensitive · Specific · Predictive. Fix flaky tests first. Prefer E2E over unit tests for user flows.
|
||||
|
||||
### Code health (mandatory)
|
||||
|
||||
@@ -75,22 +85,15 @@ Pre-commit and pre-push hooks enforce **Hotspot Code Health** and **Average Code
|
||||
|
||||
### Check suite (runs on every push)
|
||||
```bash
|
||||
pnpm lint && npx tsc --noEmit
|
||||
pnpm test
|
||||
pnpm test:coverage # frontend ≥70%
|
||||
cargo test
|
||||
cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
|
||||
pnpm lint && npx tsc --noEmit && pnpm test && pnpm test:coverage # frontend ≥70%
|
||||
cargo test && cargo llvm-cov --manifest-path src-tauri/Cargo.toml --no-clean --fail-under-lines 85
|
||||
```
|
||||
|
||||
### Architecture Decision Records (ADRs)
|
||||
### ADRs & docs
|
||||
|
||||
ADRs live in `docs/adr/`. Check before structural choices. Create the ADR **in the same commit as the code**. Never edit existing ADRs — create a new one that supersedes. Use `/create-adr` for template.
|
||||
ADRs live in `docs/adr/`. Create in the same commit as the code. Never edit existing — create a new one that supersedes. Use `/create-adr`. **When:** new dependency, storage strategy, platform target, core abstraction, cross-cutting pattern. **Not for:** bug fixes, styling, refactors.
|
||||
|
||||
**When to create one:** new dependency, storage strategy, platform target, core abstraction change, cross-cutting pattern. **Not for:** bug fixes, UI styling, refactors, test additions.
|
||||
|
||||
### Keep docs/ in sync
|
||||
|
||||
After Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit.
|
||||
After any Tauri command, new component/hook, data model change, or new integration: update `docs/ARCHITECTURE.md`, `docs/ABSTRACTIONS.md`, and/or `docs/GETTING-STARTED.md` in the same commit.
|
||||
|
||||
---
|
||||
|
||||
@@ -102,9 +105,7 @@ Default to `demo-vault-v2/`. If you must use `~/Laputa/` for testing: **never co
|
||||
|
||||
### UI design
|
||||
|
||||
1. Open `ui-design.pen` first — study existing frames for visual language
|
||||
2. Design in light mode. Create `design/<slug>.pen` for the task
|
||||
3. On completion: merge frames into `ui-design.pen`, delete `design/<slug>.pen`
|
||||
Open `ui-design.pen` first (light mode). Create `design/<slug>.pen` for the task; on completion merge into `ui-design.pen` and delete it.
|
||||
|
||||
### UI components — mandatory rules
|
||||
|
||||
|
||||
@@ -37,9 +37,14 @@ function extractWikilinks(value: FrontmatterValue): string[] {
|
||||
*/
|
||||
export type RelationshipPatch = Record<string, string[] | null>
|
||||
|
||||
/** Properties patch: a partial update to merge into `entry.properties`.
|
||||
* Keys map to their new scalar values. A `null` value means "remove this key". */
|
||||
export type PropertiesPatch = Record<string, string | number | boolean | null>
|
||||
|
||||
export interface EntryPatchResult {
|
||||
patch: Partial<VaultEntry>
|
||||
relationshipPatch: RelationshipPatch | null
|
||||
propertiesPatch: PropertiesPatch | null
|
||||
}
|
||||
|
||||
/** Map a frontmatter key+value to the corresponding VaultEntry field(s). */
|
||||
@@ -49,7 +54,8 @@ export function frontmatterToEntryPatch(
|
||||
const k = key.toLowerCase().replace(/\s+/g, '_')
|
||||
if (op === 'delete') {
|
||||
const relPatch: RelationshipPatch = { [key]: null }
|
||||
return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch }
|
||||
const propPatch: PropertiesPatch | null = !(k in ENTRY_DELETE_MAP) ? { [key]: null } : null
|
||||
return { patch: ENTRY_DELETE_MAP[k] ?? {}, relationshipPatch: relPatch, propertiesPatch: propPatch }
|
||||
}
|
||||
const str = value != null ? String(value) : null
|
||||
const arr = Array.isArray(value) ? value.map(String) : []
|
||||
@@ -73,17 +79,24 @@ export function frontmatterToEntryPatch(
|
||||
const wikilinks = value != null ? extractWikilinks(value) : []
|
||||
const relationshipPatch: RelationshipPatch | null =
|
||||
wikilinks.length > 0 ? { [key]: wikilinks } : null
|
||||
return { patch: updates[k] ?? {}, relationshipPatch }
|
||||
// For unknown keys (custom properties), produce a propertiesPatch
|
||||
const isKnownKey = k in updates
|
||||
const propertiesPatch: PropertiesPatch | null =
|
||||
!isKnownKey && value != null ? { [key]: typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : String(value) } : null
|
||||
return { patch: updates[k] ?? {}, relationshipPatch, propertiesPatch }
|
||||
}
|
||||
|
||||
/** Parse frontmatter from full content and return a merged VaultEntry patch for all known fields. */
|
||||
export function contentToEntryPatch(content: string): Partial<VaultEntry> {
|
||||
const fm = parseFrontmatter(content)
|
||||
const merged: Partial<VaultEntry> = {}
|
||||
const customProps: Record<string, string | number | boolean | null> = {}
|
||||
for (const [key, value] of Object.entries(fm)) {
|
||||
const { patch } = frontmatterToEntryPatch('update', key, value)
|
||||
const { patch, propertiesPatch } = frontmatterToEntryPatch('update', key, value)
|
||||
Object.assign(merged, patch)
|
||||
if (propertiesPatch) Object.assign(customProps, propertiesPatch)
|
||||
}
|
||||
if (Object.keys(customProps).length > 0) merged.properties = customProps
|
||||
return merged
|
||||
}
|
||||
|
||||
@@ -117,6 +130,18 @@ export interface FrontmatterOpOptions {
|
||||
silent?: boolean
|
||||
}
|
||||
|
||||
/** Apply a properties patch by merging into the existing properties map. */
|
||||
export function applyPropertiesPatch(
|
||||
existing: Record<string, string | number | boolean | null>, propPatch: PropertiesPatch,
|
||||
): Record<string, string | number | boolean | null> {
|
||||
const merged = { ...existing }
|
||||
for (const [k, v] of Object.entries(propPatch)) {
|
||||
if (v === null) delete merged[k]
|
||||
else merged[k] = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
/** Apply a relationship patch by merging into the existing relationships map. */
|
||||
export function applyRelationshipPatch(
|
||||
existing: Record<string, string[]>, relPatch: RelationshipPatch,
|
||||
@@ -144,12 +169,17 @@ export async function runFrontmatterAndApply(
|
||||
try {
|
||||
const newContent = await executeFrontmatterOp(op, path, key, value)
|
||||
callbacks.updateTab(path, newContent)
|
||||
const { patch, relationshipPatch } = frontmatterToEntryPatch(op, key, value)
|
||||
const { patch, relationshipPatch, propertiesPatch } = frontmatterToEntryPatch(op, key, value)
|
||||
const fullPatch = { ...patch }
|
||||
if (relationshipPatch && callbacks.getEntry) {
|
||||
if ((relationshipPatch || propertiesPatch) && callbacks.getEntry) {
|
||||
const current = callbacks.getEntry(path)
|
||||
if (current) {
|
||||
fullPatch.relationships = applyRelationshipPatch(current.relationships, relationshipPatch)
|
||||
if (relationshipPatch) {
|
||||
fullPatch.relationships = applyRelationshipPatch(current.relationships, relationshipPatch)
|
||||
}
|
||||
if (propertiesPatch) {
|
||||
fullPatch.properties = applyPropertiesPatch(current.properties, propertiesPatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(fullPatch).length > 0) callbacks.updateEntry(path, fullPatch)
|
||||
|
||||
@@ -373,9 +373,10 @@ describe('frontmatterToEntryPatch', () => {
|
||||
expect(result.relationshipPatch).toEqual({ 'Belongs to': ['[[x]]'] })
|
||||
})
|
||||
|
||||
it('returns empty patch for unknown non-wikilink keys', () => {
|
||||
it('returns propertiesPatch for unknown non-wikilink keys', () => {
|
||||
const result = frontmatterToEntryPatch('update', 'custom_field', 'value')
|
||||
expect(result.patch).toEqual({})
|
||||
expect(result.propertiesPatch).toEqual({ custom_field: 'value' })
|
||||
// Non-wikilink value → no relationship change
|
||||
expect(result.relationshipPatch).toBeNull()
|
||||
})
|
||||
@@ -417,10 +418,11 @@ describe('frontmatterToEntryPatch', () => {
|
||||
expect(result.patch).toEqual({ listPropertiesDisplay: [] })
|
||||
})
|
||||
|
||||
it('returns empty patch for unknown key on delete, with relationship removal', () => {
|
||||
it('returns empty patch for unknown key on delete, with relationship and properties removal', () => {
|
||||
const result = frontmatterToEntryPatch('delete', 'unknown_key')
|
||||
expect(result.patch).toEqual({})
|
||||
expect(result.relationshipPatch).toEqual({ unknown_key: null })
|
||||
expect(result.propertiesPatch).toEqual({ unknown_key: null })
|
||||
})
|
||||
|
||||
it('delete of known key also produces relationship removal', () => {
|
||||
@@ -490,9 +492,9 @@ describe('contentToEntryPatch', () => {
|
||||
expect(contentToEntryPatch(content)).toEqual({ isA: 'Type', sidebarLabel: 'Projects' })
|
||||
})
|
||||
|
||||
it('ignores unknown frontmatter keys', () => {
|
||||
it('includes custom frontmatter keys in properties patch', () => {
|
||||
const content = '---\ntype: Note\ncustom: value\n---\n'
|
||||
expect(contentToEntryPatch(content)).toEqual({ isA: 'Note' })
|
||||
expect(contentToEntryPatch(content)).toEqual({ isA: 'Note', properties: { custom: 'value' } })
|
||||
})
|
||||
|
||||
it('preserves _favorite_index as a number (not null)', () => {
|
||||
|
||||
Reference in New Issue
Block a user