Scaffold CommiTea: yarn workspaces, Electron shell, core label schema, design system
- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens (preflight off; tokens/base.css owns the reset); boots to a Reginald placeholder proving fonts/tokens/core wiring - packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard) with pessimistic conflict resolution + 15 unit tests - docs/design: full design handoff (tokens, 16 component contracts, interactive 14-screen prototype, Reginald voice rules) - docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic scheduler + Monte Carlo, directive log) - Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local store is a rebuildable cache over gitea REST/webhooks (better-sqlite3 in main process, arriving in P1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
16
packages/core/package.json
Normal file
16
packages/core/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@commitea/core",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.3",
|
||||
"vitest": "^3.0.5"
|
||||
}
|
||||
}
|
||||
10
packages/core/src/index.ts
Normal file
10
packages/core/src/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
ESTIMATE_LABELS,
|
||||
PRIORITY_LABELS,
|
||||
HARD_DEADLINE_LABEL,
|
||||
extractLabelFacts,
|
||||
isCommiteaLabel,
|
||||
parseEstimateLabel,
|
||||
parsePriorityLabel,
|
||||
} from './labels/label-schema.js'
|
||||
export type { EstimateLabel, LabelFacts, PriorityLabel } from './labels/label-schema.js'
|
||||
97
packages/core/src/labels/label-schema.test.ts
Normal file
97
packages/core/src/labels/label-schema.test.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
ESTIMATE_LABELS,
|
||||
extractLabelFacts,
|
||||
parseEstimateLabel,
|
||||
parsePriorityLabel,
|
||||
} from './label-schema.js'
|
||||
|
||||
describe('parseEstimateLabel', () => {
|
||||
it.each([
|
||||
['est/1d', 1],
|
||||
['est/2d', 2],
|
||||
['est/3d', 3],
|
||||
['est/5d', 5],
|
||||
['est/8d', 8],
|
||||
])('parses %s → %i days', (label, days) => {
|
||||
expect(parseEstimateLabel(label)).toBe(days)
|
||||
})
|
||||
|
||||
it('rejects estimates outside the fixed set', () => {
|
||||
expect(parseEstimateLabel('est/4d')).toBeNull()
|
||||
expect(parseEstimateLabel('est/13d')).toBeNull()
|
||||
expect(parseEstimateLabel('est/3h')).toBeNull()
|
||||
expect(parseEstimateLabel('EST/3d')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parsePriorityLabel', () => {
|
||||
it('parses p/1 through p/4', () => {
|
||||
expect(parsePriorityLabel('p/1')).toBe(1)
|
||||
expect(parsePriorityLabel('p/4')).toBe(4)
|
||||
})
|
||||
|
||||
it('rejects out-of-range and malformed priorities', () => {
|
||||
expect(parsePriorityLabel('p/0')).toBeNull()
|
||||
expect(parsePriorityLabel('p/5')).toBeNull()
|
||||
expect(parsePriorityLabel('P/1')).toBeNull()
|
||||
expect(parsePriorityLabel('priority/1')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('extractLabelFacts', () => {
|
||||
it('extracts all three axes from a full label set', () => {
|
||||
const facts = extractLabelFacts(['est/3d', 'p/2', 'deadline/hard', 'bug'])
|
||||
expect(facts).toEqual({
|
||||
estimateDays: 3,
|
||||
priority: 2,
|
||||
hardDeadline: true,
|
||||
malformed: [],
|
||||
conflicts: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores labels outside CommiTea namespaces entirely', () => {
|
||||
const facts = extractLabelFacts(['bug', 'enhancement', 'wontfix'])
|
||||
expect(facts.estimateDays).toBeNull()
|
||||
expect(facts.priority).toBeNull()
|
||||
expect(facts.malformed).toEqual([])
|
||||
})
|
||||
|
||||
it('reports in-namespace labels that fail to parse as malformed', () => {
|
||||
const facts = extractLabelFacts(['est/4d', 'p/9', 'deadline/soft'])
|
||||
expect(facts.malformed).toEqual(['est/4d', 'p/9', 'deadline/soft'])
|
||||
expect(facts.estimateDays).toBeNull()
|
||||
expect(facts.priority).toBeNull()
|
||||
expect(facts.hardDeadline).toBe(false)
|
||||
})
|
||||
|
||||
it('resolves estimate conflicts pessimistically and reports them', () => {
|
||||
const facts = extractLabelFacts(['est/2d', 'est/8d'])
|
||||
expect(facts.estimateDays).toBe(8)
|
||||
expect(facts.conflicts).toEqual(['est/8d'])
|
||||
})
|
||||
|
||||
it('resolves priority conflicts toward urgency and reports them', () => {
|
||||
const facts = extractLabelFacts(['p/3', 'p/1'])
|
||||
expect(facts.priority).toBe(1)
|
||||
expect(facts.conflicts).toEqual(['p/1'])
|
||||
})
|
||||
|
||||
it('handles the empty label list', () => {
|
||||
expect(extractLabelFacts([])).toEqual({
|
||||
estimateDays: null,
|
||||
priority: null,
|
||||
hardDeadline: false,
|
||||
malformed: [],
|
||||
conflicts: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the fixed estimate set in sync with its parser', () => {
|
||||
for (const label of ESTIMATE_LABELS) {
|
||||
expect(parseEstimateLabel(label)).not.toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
91
packages/core/src/labels/label-schema.ts
Normal file
91
packages/core/src/labels/label-schema.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* The gitea label schema — CommiTea's only footprint in managed work repos.
|
||||
* Fixed sets by design (docs/PLAN.md): estimates are coarse on purpose, the
|
||||
* scheduler's calibration layer refines them; anything outside these sets is
|
||||
* someone else's label and none of our business.
|
||||
*/
|
||||
|
||||
export const ESTIMATE_LABELS = ['est/1d', 'est/2d', 'est/3d', 'est/5d', 'est/8d'] as const
|
||||
export const PRIORITY_LABELS = ['p/1', 'p/2', 'p/3', 'p/4'] as const
|
||||
export const HARD_DEADLINE_LABEL = 'deadline/hard'
|
||||
|
||||
export type EstimateLabel = (typeof ESTIMATE_LABELS)[number]
|
||||
export type PriorityLabel = (typeof PRIORITY_LABELS)[number]
|
||||
|
||||
const COMMITEA_NAMESPACES = ['est/', 'p/', 'deadline/'] as const
|
||||
|
||||
export interface LabelFacts {
|
||||
/** Days from an `est/*` label; null when absent. Largest wins on conflict. */
|
||||
estimateDays: number | null
|
||||
/** 1 (most urgent) … 4 from a `p/*` label; null when absent. Most urgent wins on conflict. */
|
||||
priority: number | null
|
||||
hardDeadline: boolean
|
||||
/** Labels inside CommiTea namespaces that don't match the fixed sets. */
|
||||
malformed: string[]
|
||||
/** Valid CommiTea labels that duplicated an axis (e.g. two `est/*` labels). */
|
||||
conflicts: string[]
|
||||
}
|
||||
|
||||
export function parseEstimateLabel(label: string): number | null {
|
||||
if (!(ESTIMATE_LABELS as readonly string[]).includes(label)) return null
|
||||
return Number(/^est\/(\d+)d$/.exec(label)![1])
|
||||
}
|
||||
|
||||
export function parsePriorityLabel(label: string): number | null {
|
||||
if (!(PRIORITY_LABELS as readonly string[]).includes(label)) return null
|
||||
return Number(label.slice(2))
|
||||
}
|
||||
|
||||
export function isCommiteaLabel(label: string): boolean {
|
||||
return COMMITEA_NAMESPACES.some((ns) => label.startsWith(ns))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce an issue's label list to scheduler inputs. Conflicting labels on one
|
||||
* axis resolve pessimistically (largest estimate) / urgently (lowest priority
|
||||
* number) and are reported in `conflicts` so the agent can nag about them.
|
||||
*/
|
||||
export function extractLabelFacts(labels: readonly string[]): LabelFacts {
|
||||
const facts: LabelFacts = {
|
||||
estimateDays: null,
|
||||
priority: null,
|
||||
hardDeadline: false,
|
||||
malformed: [],
|
||||
conflicts: [],
|
||||
}
|
||||
|
||||
for (const label of labels) {
|
||||
if (!isCommiteaLabel(label)) continue
|
||||
|
||||
if (label === HARD_DEADLINE_LABEL) {
|
||||
facts.hardDeadline = true
|
||||
continue
|
||||
}
|
||||
|
||||
const estimate = parseEstimateLabel(label)
|
||||
if (estimate !== null) {
|
||||
if (facts.estimateDays !== null) {
|
||||
facts.conflicts.push(label)
|
||||
facts.estimateDays = Math.max(facts.estimateDays, estimate)
|
||||
} else {
|
||||
facts.estimateDays = estimate
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const priority = parsePriorityLabel(label)
|
||||
if (priority !== null) {
|
||||
if (facts.priority !== null) {
|
||||
facts.conflicts.push(label)
|
||||
facts.priority = Math.min(facts.priority, priority)
|
||||
} else {
|
||||
facts.priority = priority
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
facts.malformed.push(label)
|
||||
}
|
||||
|
||||
return facts
|
||||
}
|
||||
7
packages/core/tsconfig.json
Normal file
7
packages/core/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2022"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user