13 KiB
id, name, status, depends_on, scope, risk, impact, level
| id | name | status | depends_on | scope | risk | impact | level | ||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| review/schemas-and-errors | Review schema, enum, and error implementations for consistency | done |
|
narrow | low | phase | review |
Description
Review the schema and error layer implementations before building the graph and analysis layers on top. This is a critical checkpoint because everything downstream depends on these types being correct and consistent with the architecture docs.
Acceptance Criteria
- All TypeBox schemas match schemas.md exactly
- All
Static<typeof>type aliases correctly derived — no manual type definitions - Nullable helper used consistently in TaskInput (not in TaskGraphNodeAttributes)
- Enum values match DB/frontmatter conventions exactly
- Numeric method tables match spec tables exactly
resolveDefaultscorrectly separates "nullable categorical→default" from "label-only nullable→stays nullable"- Error class hierarchy is correct: all extend TaskgraphError, all have proper
nameand typed fields InvalidInputErrorcan be constructed fromValue.Errors()outputCircularDependencyError.cyclestype isstring[][]- No Zod, no gray-matter, no js-yaml in any dependency
package.jsonlists only approved dependencies- All tests pass
References
- docs/architecture/schemas.md
- docs/architecture/errors-validation.md
- docs/architecture/frontmatter.md — supply chain constraints
Notes
See review report below.
Summary
Review outcome: Approved with 1 suggestion (no blocking or warning issues).
All acceptance criteria pass. Implementation is consistent with architecture docs. See detailed review below.
Code Review: review/schemas-and-errors
Summary
- Files reviewed: 12 (6 implementation + 6 test files, plus barrel exports)
src/schema/enums.ts,src/schema/task.ts,src/schema/graph.ts,src/schema/results.tssrc/analysis/defaults.tssrc/error/index.tstest/schema.test.ts,test/error.test.ts,test/defaults.test.tssrc/schema/index.ts(barrel)
- Critical issues: 0
- Warnings: 0
- Suggestions: 1
- Tests: ✅ 257 passed (7 test files)
- Lint (tsc --noEmit): ✅ Clean, no errors
- Overall: Approved
Architecture Compliance
✅ TypeBox as Single Source of Truth
All types are derived via Static<typeof Schema> — no manual interface or type definitions for schema shapes. Enum constants use Enum suffix, type aliases do not. This matches the naming convention table in schemas.md exactly.
Verified:
TaskScopeEnum→type TaskScope = Static<typeof TaskScopeEnum>✅TaskRiskEnum→type TaskRisk = Static<typeof TaskRiskEnum>✅TaskImpactEnum→type TaskImpact = Static<typeof TaskImpactEnum>✅TaskLevelEnum→type TaskLevel = Static<typeof TaskLevelEnum>✅TaskPriorityEnum→type TaskPriority = Static<typeof TaskPriorityEnum>✅TaskStatusEnum→type TaskStatus = Static<typeof TaskStatusEnum>✅- All object schemas (
TaskInput,DependencyEdge,TaskGraphNodeAttributes, etc.) follow same pattern ✅
✅ Nullable Helper
Nullableis defined inenums.tsand re-exported fromtask.tsfor convenience ✅TaskInputusesType.Optional(Nullable(...))for categorical and metadata fields ✅ (matches architecture: "field itself optional AND nullable when present")TaskGraphNodeAttributesusesType.Optional(EnumSchema)without Nullable ✅ (matches architecture: "absent and null both map to undefined on graph")ResolvedTaskAttributesusesNullable(...)for label-only fields (level, priority, status) and plain enum schemas for categorical fields with defaults (scope, risk, impact) ✅
✅ Enum Values Match Spec
All enum values match schemas.md exactly:
TaskScopeEnum: "single", "narrow", "moderate", "broad", "system" ✅TaskRiskEnum: "trivial", "low", "medium", "high", "critical" ✅TaskImpactEnum: "isolated", "component", "phase", "project" ✅TaskLevelEnum: "planning", "decomposition", "implementation", "review", "research" ✅TaskPriorityEnum: "low", "medium", "high", "critical" ✅TaskStatusEnum: "pending", "in-progress", "completed", "failed", "blocked" ✅
✅ Numeric Methods Match Spec Tables
defaults.ts mapping tables are exact matches:
scopeCostEstimate: single→1.0, narrow→2.0, moderate→3.0, broad→4.0, system→5.0 ✅scopeTokenEstimate: single→500, narrow→1500, moderate→3000, broad→6000, system→10000 ✅riskSuccessProbability: trivial→0.98, low→0.90, medium→0.80, high→0.65, critical→0.50 ✅riskWeight: computed as1 - riskSuccessProbability(risk)✅ (tested and matches 0.02, 0.10, 0.20, 0.35, 0.50)impactWeight: isolated→1.0, component→1.5, phase→2.0, project→3.0 ✅resolveDefaultsdefault values: risk→"medium", scope→"narrow", impact→"isolated" ✅ (matchesgraph-model.mddefaults table)
✅ resolveDefaults Correctly Separates Categorical vs Label-only
- Categorical fields with defaults (scope, risk, impact) are resolved via
??with default values ✅ - Label-only fields (level, priority, status) remain nullable via
?? null✅ - The
Partial<TaskGraphNodeAttributes> & Pick<TaskGraphNodeAttributes, 'name'>signature is correct ✅ - Derived numeric fields are computed from the resolved categorical values ✅
✅ Error Class Hierarchy
All error classes match errors-validation.md:
TaskgraphError extends Error✅ — base class with propernameandObject.setPrototypeOfTaskNotFoundError extends TaskgraphError✅ — hasreadonly taskId: stringCircularDependencyError extends TaskgraphError✅ — hasreadonly cycles: string[][]InvalidInputError extends TaskgraphError✅ — hasreadonly field: stringandoverride readonly message: stringDuplicateNodeError extends TaskgraphError✅ — hasreadonly taskId: stringDuplicateEdgeError extends TaskgraphError✅ — hasreadonly prerequisite: stringandreadonly dependent: string
All subclasses:
- Set
this.nameto their class name ✅ - Call
Object.setPrototypeOf(this, new.target.prototype)for correctinstanceof✅ - Use
readonlyfor typed fields ✅ - Tests verify
instanceofworks across the full prototype chain ✅
✅ InvalidInputError.fromTypeBoxError
Static factory method for creating from TypeBox Value.Errors() output ✅
- Strips leading
/from path ✅ - Handles paths without leading slash ✅
- Handles nested paths (e.g.,
/attributes/risk) ✅ - Tests verify this behavior ✅
✅ Schema Field-by-Field Verification
TaskInput (src/schema/task.ts):
All 13 fields present and match arch spec. status ↔ scope order in implementation matches spec order (status first in code vs scope first in spec doc — implementation uses status first, which is a cosmetic difference in object literal key ordering, not a semantic issue).
DependencyEdge (src/schema/task.ts):
from: Type.String(),to: Type.String(),qualityRetention: Type.Optional(Type.Number({ default: 0.9 }))✅- Matches spec including the
qualityRetentionnaming (notqualityDegradation) ✅
TaskGraphNodeAttributes (src/schema/graph.ts):
name: Type.String()(required) ✅- All categorical fields:
Type.Optional(Enum)✅ - Does NOT include tags/assignee/due/created/modified ✅ (these belong to TaskInput only)
- Test verifies
tags,assignee,dueare undefined in properties ✅
TaskGraphEdgeAttributes (src/schema/graph.ts):
qualityRetention: Type.Optional(Type.Number())✅ (note: no default here unlike DependencyEdge — this matches the architecture distinction between input schema and graph attribute schema)
TaskGraphNodeAttributesUpdate (src/schema/graph.ts):
Type.Partial(TaskGraphNodeAttributes)— matches arch spec ✅
SerializedGraph generic factory and TaskGraphSerialized (src/schema/graph.ts):
- Generic factory parameterized with NodeAttrs, EdgeAttrs, GraphAttrs ✅
optionsobject withtype: "directed",multi: false,allowSelfLoops: false✅- No version field ✅
- Tests verify no
versionorschemaVersionin properties ✅
ResolvedTaskAttributes (src/schema/results.ts):
- All 9 fields present and matching spec ✅
scope,risk,impact: plain enum (not nullable) ✅level,priority,status:Nullable(Enum)✅- All 5 numeric fields:
Type.Number()✅
✅ Dependency Audit
- No Zod in dependency tree ✅
- No gray-matter in dependency tree ✅
- No js-yaml in dependency tree ✅
package.jsondependencies: @alkdev/typebox, graphology (+ plugins), yaml — all approved per architecture ✅
Code Quality
Clean Code
- All files are well-organized with clear section comments ✅
- JSDoc comments explain purpose and semantics (e.g.,
qualityRetentionexplanation) ✅ - No magic numbers — numeric tables use
Record<>constants with explicit mapping ✅ - No commented-out code ✅
- No TODOs without issue references ✅
- Functions are short and focused (all under 30 lines) ✅
One Minor Note on Nullable Duplication
Nullable is defined in both enums.ts (exported) and results.ts (local, not exported). The results.ts version includes a comment: "duplicated here for schema locality". This is a deliberate choice — results.ts has no import from enums.ts for Nullable, opting for local definition. This avoids a circular or cross-module dependency for a trivial utility, which is reasonable. The test suite verifies that the re-export from task.ts is the same function object as from enums.ts.
Suggestion (non-blocking): Consider importing Nullable from enums.js in results.ts instead of duplicating it. The enums.ts file already exports it, and results.ts already imports TaskScopeEnum etc. from ./enums.js. This would ensure a single definition. However, the current approach is acceptable since the function is trivially one line and the duplication is explicitly documented.
Testing
Coverage Assessment
- Enum schemas: All 6 enums tested with valid values, invalid values, null, and undefined. 30 tests ✅
- Nullable helper: Tested with valid values, null, invalid values, undefined. 3 tests ✅
- TaskInput: Comprehensive — minimal, full, null categorical, absent, invalid, wrong types, structured errors. 14 tests ✅
- DependencyEdge: Minimal, with qualityRetention, boundary values, missing fields, wrong types, structured errors. 9 tests ✅
- Graph schemas: NodeAttributes, NodeAttributesUpdate, EdgeAttributes, SerializedGraph factory — including verification that tags/assignee/due are not in the schema. 13+ tests ✅
- Result schemas: RiskPathResult, DecomposeResult, WorkflowCostOptions, WorkflowCostResult, EvConfig, EvResult, RiskDistributionResult. All have valid, invalid, missing field, and wrong type tests. ~20 tests ✅
- Type alias correctness: Compile-time type assertions alongside runtime checks for all enums and schemas ✅
- Error classes: All 6 classes tested for instanceof chain, name, field access, and message formatting. 31 tests ✅
- defaults.ts: All numeric functions tested with explicit value mapping.
resolveDefaultstested with all-defaults, explicit values, label-only preservation, and mixed scenarios. 30 tests ✅ - riskWeight invariant: Explicitly tested that
riskWeight(r) === 1 - riskSuccessProbability(r)for all risk values ✅
Edge Cases Tested
- Nullable re-export identity (
NullableFromTask === Nullable) ✅ fromTypeBoxErrorhandles paths with and without leading/✅- SerializedGraph has no version field ✅
- SerializedGraph rejects wrong
optionsvalues (undirected, multi, allowSelfLoops) ✅ TaskGraphNodeAttributesUpdateaccepts empty object ✅- Prototype chain correctness for all error subclasses ✅
Security
- No secrets in code ✅
- No input validation concerns at this layer (schemas are definitions, not runtime input paths) ✅
- Supply chain check: no Zod, no gray-matter, no js-yaml ✅
Performance
- Numeric functions are simple
Record<>lookups — O(1) ✅ - No unnecessary allocations ✅
resolveDefaultsis a pure function with no loop — optimal ✅
Suggestions
- (Low priority) Consider importing
Nullablefrom./enums.jsinresults.tsinstead of duplicating the one-liner. Sinceresults.tsalready importsTaskScopeEnumetc. from./enums.js, addingNullableto that import would reduce duplication without introducing new coupling. This is a minor style preference and not a correctness issue.
Recommendations
- ✅ All acceptance criteria are met. Implementation is approved.
- Consider the
Nullableimport consolidation as a future cleanup (non-blocking). - The resolvedDefaults defaults match the architecture spec (
graph-model.md): risk→"medium", scope→"narrow", impact→"isolated". Any future changes to these defaults should be reflected in bothdefaults.tsandgraph-model.md.