Files
taskgraph_ts/tasks/implementation/review/schemas-and-errors.md

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
schema/enums
schema/input-schemas
schema/graph-schemas
schema/result-types
schema/numeric-methods-and-defaults
error/error-hierarchy
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
  • resolveDefaults correctly separates "nullable categorical→default" from "label-only nullable→stays nullable"
  • Error class hierarchy is correct: all extend TaskgraphError, all have proper name and typed fields
  • InvalidInputError can be constructed from Value.Errors() output
  • CircularDependencyError.cycles type is string[][]
  • No Zod, no gray-matter, no js-yaml in any dependency
  • package.json lists 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.ts
    • src/analysis/defaults.ts
    • src/error/index.ts
    • test/schema.test.ts, test/error.test.ts, test/defaults.test.ts
    • src/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:

  • TaskScopeEnumtype TaskScope = Static<typeof TaskScopeEnum>
  • TaskRiskEnumtype TaskRisk = Static<typeof TaskRiskEnum>
  • TaskImpactEnumtype TaskImpact = Static<typeof TaskImpactEnum>
  • TaskLevelEnumtype TaskLevel = Static<typeof TaskLevelEnum>
  • TaskPriorityEnumtype TaskPriority = Static<typeof TaskPriorityEnum>
  • TaskStatusEnumtype TaskStatus = Static<typeof TaskStatusEnum>
  • All object schemas (TaskInput, DependencyEdge, TaskGraphNodeAttributes, etc.) follow same pattern

Nullable Helper

  • Nullable is defined in enums.ts and re-exported from task.ts for convenience
  • TaskInput uses Type.Optional(Nullable(...)) for categorical and metadata fields (matches architecture: "field itself optional AND nullable when present")
  • TaskGraphNodeAttributes uses Type.Optional(EnumSchema) without Nullable (matches architecture: "absent and null both map to undefined on graph")
  • ResolvedTaskAttributes uses Nullable(...) 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 as 1 - 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
  • resolveDefaults default values: risk→"medium", scope→"narrow", impact→"isolated" (matches graph-model.md defaults 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 proper name and Object.setPrototypeOf
  • TaskNotFoundError extends TaskgraphError — has readonly taskId: string
  • CircularDependencyError extends TaskgraphError — has readonly cycles: string[][]
  • InvalidInputError extends TaskgraphError — has readonly field: string and override readonly message: string
  • DuplicateNodeError extends TaskgraphError — has readonly taskId: string
  • DuplicateEdgeError extends TaskgraphError — has readonly prerequisite: string and readonly dependent: string

All subclasses:

  • Set this.name to their class name
  • Call Object.setPrototypeOf(this, new.target.prototype) for correct instanceof
  • Use readonly for typed fields
  • Tests verify instanceof works 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. statusscope 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 qualityRetention naming (not qualityDegradation)

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, due are 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
  • options object with type: "directed", multi: false, allowSelfLoops: false
  • No version field
  • Tests verify no version or schemaVersion in 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.json dependencies: @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., qualityRetention explanation)
  • 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. resolveDefaults tested 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)
  • fromTypeBoxError handles paths with and without leading /
  • SerializedGraph has no version field
  • SerializedGraph rejects wrong options values (undirected, multi, allowSelfLoops)
  • TaskGraphNodeAttributesUpdate accepts 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
  • resolveDefaults is a pure function with no loop — optimal

Suggestions

  1. (Low priority) Consider importing Nullable from ./enums.js in results.ts instead of duplicating the one-liner. Since results.ts already imports TaskScopeEnum etc. from ./enums.js, adding Nullable to that import would reduce duplication without introducing new coupling. This is a minor style preference and not a correctness issue.

Recommendations

  1. All acceptance criteria are met. Implementation is approved.
  2. Consider the Nullable import consolidation as a future cleanup (non-blocking).
  3. 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 both defaults.ts and graph-model.md.