Fix critical architecture review issues
Critical fixes: - Rename qualityDegradation → qualityRetention across all docs (semantically inverted: 0.9 meant 90% quality RETAINED, not 90% degradation). Updated schemas, graph-model, cost-benefit, ADRs. - Add TaskInput → TaskGraphNodeAttributes transformation section to graph-model.md, documenting how Nullable(Optional) input fields map to Optional graph attributes - Fix DuplicateEdgeError fields: source/target → prerequisite/dependent to match the established edge direction convention - Fix resolveDefaults signature: Partial<TaskGraphNodeAttributes> → Partial<...> & Pick<TaskGraphNodeAttributes, 'name'> to require the name field - Move Nullable helper definition before its first use in schemas.md - Fix 'construction never throws' contradiction: rephrase to 'construction enforces uniqueness, not data quality' - Define all 6 enum value sets in schemas.md (previously only TaskScope and TaskRisk were explicit) - Add EvConfig parameter table with defaults and semantics - Document WorkflowCostOptions.limit parameter - Add construction error handling table to graph-model.md - Add graph.raw mutation safety warning to api-surface.md - Update build-distribution.md error class list to include DuplicateNodeError and DuplicateEdgeError
This commit is contained in:
@@ -63,7 +63,7 @@ Two downstream projects consume this library. Understanding their needs shapes t
|
||||
The hub's database is the source of truth for tasks at runtime. The coordinator loads task rows + dependency edges from the DB, builds a graphology graph in memory, and runs graph algorithms. This consumer:
|
||||
|
||||
- Builds graphs from structured data (DB query results), not files
|
||||
- Needs per-edge `qualityDegradation` attributes for the DAG propagation model
|
||||
- Needs per-edge `qualityRetention` attributes for the DAG propagation model
|
||||
- Requires the same analysis functions the CLI provides, but called as an API, not via shell
|
||||
|
||||
> See alkhub task storage spec: `/workspace/@alkdev/alkhub_ts/docs/architecture/storage/tasks.md`
|
||||
|
||||
@@ -50,9 +50,10 @@ class TaskGraph {
|
||||
|
||||
// Reactivity
|
||||
get raw(): Graph // underlying graphology instance for direct event listener attachment
|
||||
}
|
||||
```
|
||||
|
||||
**Warning on `graph.raw`**: Mutating the underlying graphology instance directly bypasses `TaskGraph`'s validation and invariants. Operations that violate the "no parallel edges" constraint (adding a second edge between the same node pair without using `addEdgeWithKey`), or that create self-loops in a graph configured to disallow them, will not be caught by `TaskGraph` methods. Consumers using `raw` should treat the graph as read-only for structural changes and use `TaskGraph` methods for all mutations.
|
||||
|
||||
**Notes**:
|
||||
- `topologicalOrder()` throws `CircularDependencyError` (with `cycles` populated) when cyclic — see [ADR-003](decisions/003-topo-order-throws-on-cycle.md)
|
||||
- `subgraph()` returns a new `TaskGraph` with matching nodes and only edges where both endpoints are in the filtered set — see [ADR-007](decisions/007-subgraph-internal-only.md)
|
||||
@@ -131,12 +132,12 @@ const DecomposeResult = Type.Object({
|
||||
|
||||
```typescript
|
||||
const WorkflowCostOptions = Type.Object({
|
||||
includeCompleted: Type.Optional(Type.Boolean()),
|
||||
limit: Type.Optional(Type.Number()),
|
||||
includeCompleted: Type.Optional(Type.Boolean()), // default: false. When false, completed tasks are excluded from output but remain in propagation with p=1.0
|
||||
limit: Type.Optional(Type.Number()), // default: no limit. If set, limits the number of tasks in the result. Useful for large graphs when only top-N results are needed.
|
||||
propagationMode: Type.Optional(
|
||||
Type.Union([Type.Literal("independent"), Type.Literal("dag-propagate")])
|
||||
),
|
||||
defaultQualityDegradation: Type.Optional(Type.Number()),
|
||||
defaultQualityRetention: Type.Optional(Type.Number()), // default: 0.9. Edge quality when not explicitly set.
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ taskgraph_ts/
|
||||
│ │ ├── parse.ts # YAML/frontmatter parsing + typebox validation
|
||||
│ │ └── serialize.ts # TaskInput → markdown with frontmatter
|
||||
│ └── error/
|
||||
│ └── index.ts # TaskgraphError, TaskNotFoundError, CircularDependencyError, InvalidInputError
|
||||
│ └── index.ts # TaskgraphError, TaskNotFoundError, CircularDependencyError, InvalidInputError, DuplicateNodeError, DuplicateEdgeError
|
||||
├── test/
|
||||
│ ├── graph.test.ts
|
||||
│ ├── analysis.test.ts
|
||||
|
||||
@@ -26,6 +26,15 @@ Where categorical fields provide the inputs:
|
||||
- **C_success** = `scopeCostEstimate(scope)` — cost when it works
|
||||
- **C_fail** = modeled via `EvConfig` parameters: `scopeCost + fallbackCost + timeLost × expectedRetries`. The `calculateTaskEv` function uses `scopeCost` as `C_success` and derives `C_fail` from the same `scopeCost` plus `fallbackCost` and `timeLost` scaled by expected retry count. `fallbackCost` and `timeLost` default to 0 if not provided, yielding `C_fail = C_success` in the simplest case. The `valueRate` parameter converts the result to dollar terms if needed.
|
||||
|
||||
### EvConfig Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
|-----------|---------|-------------|
|
||||
| `retries` | 0 | Maximum retry attempts. Used in the EV calculation: each retry adds `timeLost` cost. When 0, no retry cost is considered. |
|
||||
| `fallbackCost` | 0 | Cost incurred when a task fails and no retry succeeds. Added to `scopeCost` in the failure term. |
|
||||
| `timeLost` | 0 | Time cost per retry attempt. Total retry cost = `retries × timeLost`. |
|
||||
| `valueRate` | 0 | Dollar conversion rate. When non-zero, multiplies the EV result to produce dollar-denominated output. When 0, EV is in abstract cost units. |
|
||||
|
||||
### Structural Insight: Upstream Failures Multiply
|
||||
|
||||
```
|
||||
@@ -54,7 +63,7 @@ The Rust CLI computes EV per-task independently — no upstream quality degradat
|
||||
|
||||
### Implementation Approach
|
||||
|
||||
DAG propagation is the **default mode**. The independent model is a degenerate case (set `defaultQualityDegradation: 0` or `propagationMode: 'independent'`).
|
||||
DAG propagation is the **default mode**. The independent model is a degenerate case (set `defaultQualityRetention: 1.0` or `propagationMode: 'independent'`).
|
||||
|
||||
The algorithm processes tasks in topological order, maintaining an `upstreamSuccessProbs` map:
|
||||
|
||||
@@ -66,13 +75,13 @@ The algorithm processes tasks in topological order, maintaining an `upstreamSucc
|
||||
|
||||
2. When computing effective probability for a task with prerequisites:
|
||||
- Start with intrinsic probability
|
||||
- For each prerequisite, compute inherited quality: `parentP + (1 - parentP) × (1 - qualityDegradation)`
|
||||
- For each prerequisite, compute inherited quality: `parentP + (1 - parentP) × qualityRetention`
|
||||
- Multiply all inherited quality factors together with intrinsic probability
|
||||
|
||||
3. The `qualityDegradation` per edge determines how much a parent's failure bleeds through:
|
||||
- 0.0 = no propagation (independent model)
|
||||
- 1.0 = full propagation (parent failure guarantees child failure)
|
||||
- default 0.9 = high but not total propagation
|
||||
3. The `qualityRetention` per edge determines how much upstream quality is preserved:
|
||||
- 0.0 = no retention (full propagation — upstream failure guarantees child failure)
|
||||
- 1.0 = full retention (independent model — upstream failure has no effect on child)
|
||||
- default 0.9 = high retention (only 10% of upstream failure bleeds through)
|
||||
|
||||
### Per-task output
|
||||
|
||||
@@ -90,9 +99,9 @@ When `includeCompleted: false`, completed tasks are excluded from the result's t
|
||||
|-----------|----------------------|-------------------------------|
|
||||
| Topology awareness | None | Full — topological order + upstream propagation |
|
||||
| Upstream failure modeling | Ignored | Each parent's failure degrades child's effective p |
|
||||
| Edge semantics | Not used | `qualityDegradation` per edge, default 0.9 |
|
||||
| Edge semantics | Not used | `qualityRetention` per edge, default 0.9 |
|
||||
| Result interpretation | Sum of independent per-task costs | Total workflow cost accounting for cascading failure |
|
||||
| Degenerate case | — | Set `propagationMode: 'independent'` or `defaultQualityDegradation: 0` |
|
||||
| Degenerate case | — | Set `propagationMode: 'independent'` or `defaultQualityRetention: 1.0` |
|
||||
|
||||
## Risk Analysis Functions
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ The Rust CLI computes expected value per-task independently — no upstream qual
|
||||
|
||||
## Decision
|
||||
|
||||
**DAG-propagation is the default mode.** The independent model is a degenerate case accessible via `propagationMode: 'independent'` or `defaultQualityDegradation: 0`.
|
||||
**DAG-propagation is the default mode.** The independent model is a degenerate case accessible via `propagationMode: 'independent'` or `defaultQualityRetention: 1.0`.
|
||||
|
||||
## Consequences
|
||||
|
||||
@@ -16,12 +16,12 @@ The Rust CLI computes expected value per-task independently — no upstream qual
|
||||
- More accurate cost estimates — captures the structural reality that upstream failures multiply downstream damage
|
||||
- Per-task output includes both `pIntrinsic` and `pEffective` so consumers can see the degradation effect
|
||||
- The independent model is still available as an opt-in degenerate case
|
||||
- Per-edge `qualityDegradation` allows fine-grained modeling of how much each dependency bleeds failure
|
||||
- Per-edge `qualityRetention` allows fine-grained modeling of how much quality is preserved through each dependency
|
||||
|
||||
### Negative
|
||||
- More complex implementation than simple sum
|
||||
- Results differ from the Rust CLI — consumers migrating from CLI to library will see different numbers
|
||||
- Requires `qualityDegradation` per edge (default 0.9) which adds a concept the Rust CLI didn't have
|
||||
- Requires `qualityRetention` per edge (default 0.9) which adds a concept the Rust CLI didn't have
|
||||
|
||||
### Mitigation
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ In the DAG-propagation model, each hop compounds another `<1.0` factor. This imp
|
||||
### Positive
|
||||
- No double-counting of depth effects
|
||||
- Simpler model to explain, implement, and debug
|
||||
- Architecture supports future depth-escalation via per-edge `qualityDegradation` adjustments or `risk` categorical escalation without API changes
|
||||
- Architecture supports future depth-escalation via per-edge `qualityRetention` adjustments or `risk` categorical escalation without API changes
|
||||
|
||||
### Negative
|
||||
- May underestimate cost for very deep dependency chains where risk genuinely escalates with depth
|
||||
@@ -23,4 +23,4 @@ In the DAG-propagation model, each hop compounds another `<1.0` factor. This imp
|
||||
|
||||
### Future
|
||||
|
||||
If empirical data from actual task outcomes shows that depth-escalation is needed, it can be added without API changes — either by adjusting `qualityDegradation` per depth, or by escalating the `risk` categorical. This is a calibration question, not an architecture question.
|
||||
If empirical data from actual task outcomes shows that depth-escalation is needed, it can be added without API changes — either by adjusting `qualityRetention` per depth, or by escalating the `risk` categorical. This is a calibration question, not an architecture question.
|
||||
@@ -32,8 +32,8 @@ class DuplicateNodeError extends TaskgraphError {
|
||||
}
|
||||
|
||||
class DuplicateEdgeError extends TaskgraphError {
|
||||
source: string
|
||||
target: string
|
||||
prerequisite: string
|
||||
dependent: string
|
||||
}
|
||||
```
|
||||
|
||||
@@ -45,7 +45,7 @@ class DuplicateEdgeError extends TaskgraphError {
|
||||
| `CircularDependencyError` | `topologicalOrder()` called on a cyclic graph |
|
||||
| `InvalidInputError` | Frontmatter parsing finds invalid field values or missing required fields |
|
||||
| `DuplicateNodeError` | `addTask` called with an ID that already exists in the graph |
|
||||
| `DuplicateEdgeError` | `addDependency` called for a source→target pair that already exists |
|
||||
| `DuplicateEdgeError` | `addDependency` called for a prerequisite→dependent pair that already exists |
|
||||
|
||||
### Mutation Operations on Non-Existent Targets
|
||||
|
||||
@@ -110,15 +110,15 @@ The library takes a strict approach to cycles:
|
||||
- `findCycles()` returns the actual cycle paths — for debugging and error reporting
|
||||
- `topologicalOrder()` **throws** `CircularDependencyError` when the graph is cyclic, rather than returning a partial ordering — see [ADR-003](decisions/003-topo-order-throws-on-cycle.md)
|
||||
|
||||
**Cyclic graphs are a valid graph state** — they can be constructed, queried, and validated. Only operations that require a DAG (topo sort, critical path, parallel groups, workflow cost) throw on cycles. Construction never throws.
|
||||
**Cyclic graphs are a valid graph state** — they can be constructed, queried, and validated. Only operations that require a DAG (topo sort, critical path, parallel groups, workflow cost) throw on cycles. Construction methods enforce uniqueness but do not reject data quality issues.
|
||||
|
||||
## Construction vs. Validation Error Handling
|
||||
|
||||
The fundamental contract:
|
||||
|
||||
1. **Construction never throws** — `fromTasks`, `fromRecords`, `fromJSON`, `addTask`, `addDependency` can be called freely. `DuplicateNodeError` and `DuplicateEdgeError` are the exceptions — they represent programming errors (adding something that already exists), not data validation issues.
|
||||
2. **Validation returns error arrays** — `validateSchema()`, `validateGraph()`, and `validate()` collect issues without throwing.
|
||||
3. **`topologicalOrder()` is the operation-level exception** — it throws because returning a partial result would be silently incorrect.
|
||||
1. **Construction methods enforce uniqueness, not data quality** — `fromTasks`, `fromRecords`, `fromJSON`, `addTask`, `addDependency` throw only for uniqueness constraint violations (`DuplicateNodeError`, `DuplicateEdgeError`) and missing targets (`TaskNotFoundError`). Data quality issues (invalid enum values, missing required fields, cycles) are the domain of `validate()`, not construction.
|
||||
2. **Validation returns error arrays, never throws** — `validateSchema()`, `validateGraph()`, and `validate()` collect issues without throwing.
|
||||
3. **`topologicalOrder()` is the operation-level exception** — it throws on cyclic graphs because returning a partial result would be silently incorrect.
|
||||
|
||||
This distinction exists because validation is a "check before you proceed" operation (collect all issues, show the user), while topo sort is an operation that cannot produce a meaningful result on a cyclic graph.
|
||||
|
||||
|
||||
@@ -30,18 +30,18 @@ The graph must be constructable from multiple sources to serve both consumers:
|
||||
|
||||
| Path | Source | Consumer | Edge Attributes |
|
||||
|------|--------|----------|----------------|
|
||||
| `fromTasks` | `TaskInput[]` (frontmatter/JSON) | OpenCode plugin, tests | Default `qualityDegradation` (0.9) |
|
||||
| `fromRecords` | `TaskInput[]` + `DependencyEdge[]` | alkhub (DB query results) | Per-edge `qualityDegradation` |
|
||||
| `fromTasks` | `TaskInput[]` (frontmatter/JSON) | OpenCode plugin, tests | Default `qualityRetention` (0.9) |
|
||||
| `fromRecords` | `TaskInput[]` + `DependencyEdge[]` | alkhub (DB query results) | Per-edge `qualityRetention` |
|
||||
| `fromJSON` | `TaskGraphSerialized` (graphology export) | Persistence/round-trip | Preserved from source |
|
||||
| Incremental | `addTask` / `addDependency` calls | Programmatic/testing | Default or explicit |
|
||||
|
||||
**Preferred internal approach**: For paths 1 and 2, build a serialized graph JSON blob (nodes array + edges array) and call `graph.import()`. This is faster than N individual `addNode`/`addEdge` calls and avoids the verbose builder API.
|
||||
|
||||
### qualityDegradation on Construction
|
||||
### qualityRetention on Construction
|
||||
|
||||
`fromTasks` constructs edges from `dependsOn` arrays in frontmatter, which cannot express per-edge `qualityDegradation`. Those edges get the default (0.9). `fromRecords` and `fromJSON` support per-edge values. Edges can be augmented after construction via `updateEdgeAttributes`.
|
||||
`fromTasks` constructs edges from `dependsOn` arrays in frontmatter, which cannot express per-edge `qualityRetention`. Those edges get the default (0.9). `fromRecords` and `fromJSON` support per-edge values. Edges can be augmented after construction via `updateEdgeAttributes`.
|
||||
|
||||
This distinction exists because the file-based frontmatter model has no syntax for per-edge attributes, while the DB-backed model (alkhub) stores per-edge `qualityDegradation` in the `task_dependencies` table. The library serves both without forcing either into the other's shape.
|
||||
This distinction exists because the file-based frontmatter model has no syntax for per-edge attributes, while the DB-backed model (alkhub) stores per-edge `qualityRetention` in the `task_dependencies` table. The library serves both without forcing either into the other's shape.
|
||||
|
||||
## Categorical Field Defaults
|
||||
|
||||
@@ -59,17 +59,29 @@ The raw nullable data is preserved on the graph. `resolveDefaults` is called int
|
||||
|
||||
> See [schemas.md](schemas.md) for the full enum definitions and numeric method tables.
|
||||
|
||||
## Node Metadata
|
||||
## TaskInput → Node Attributes Transformation
|
||||
|
||||
Unlike the original napi design where `DependencyGraph` only stored IDs, node attributes carry the categorical metadata directly. This eliminates the need to pass `TaskInput[]` alongside the graph — `weightedCriticalPath` and `riskPath` read attributes from the graph nodes.
|
||||
When constructing from `TaskInput[]` (via `fromTasks`), the input data shape differs from the graph node attribute shape. The transformation is:
|
||||
|
||||
The graph acts as an in-memory index/metadata store for categorical fields. Task body content, file path, and other non-graph data stay external to the library.
|
||||
| TaskInput field | Graph node attribute | Notes |
|
||||
|----------------|---------------------|-------|
|
||||
| `id` | Node key (not an attribute) | Used as `graph.addNode(id, attributes)` key |
|
||||
| `name` | `name: Type.String()` (required) | Directly transferred |
|
||||
| `dependsOn` | Creates edges (not a node attribute) | Each element → `addDependency(id, dep)` with default `qualityRetention: 0.9` |
|
||||
| `status`, `scope`, `risk`, `impact`, `level`, `priority` | Same-name optional attributes | `Type.Optional(Nullable(Enum))` → `Type.Optional(Enum)`: YAML `null` values and absent fields both map to attribute `undefined` (not stored on the node) |
|
||||
| `tags`, `assignee`, `due`, `created`, `modified` | **Not stored on graph** | These fields exist on `TaskInput` but are not part of `TaskGraphNodeAttributes`. They belong to the caller/consumer, not the graph. |
|
||||
|
||||
The key point: `TaskInput` uses `Type.Optional(Nullable(Enum))` (field can be absent *or* set to null), but `TaskGraphNodeAttributes` uses `Type.Optional(Enum)` (field can be absent, but not null). The transformation strips `null` → `undefined` (not stored). This is correct because on the graph, absent and null mean the same thing: "not assessed."
|
||||
|
||||
For `fromRecords`, the same transformation applies to tasks, and edges come from the explicit `DependencyEdge[]` array with per-edge `qualityRetention` values.
|
||||
|
||||
## Edge Attributes
|
||||
|
||||
Edges carry `qualityDegradation` for the DAG-propagation cost model. If absent, the default (0.9) is used by `workflowCost`. Other algorithms ignore edge attributes.
|
||||
Edges carry `qualityRetention` for the DAG-propagation cost model. If absent, the default (0.9) is used by `workflowCost`. Other algorithms ignore edge attributes.
|
||||
|
||||
> See [cost-benefit.md](cost-benefit.md) for how qualityDegradation is used in propagation.
|
||||
> See [cost-benefit.md](cost-benefit.md) for how qualityRetention is used in propagation.
|
||||
|
||||
> **Note**: This field was renamed from `qualityDegradation` to `qualityRetention` because the original name was semantically inverted — a value of 0.9 meant "90% quality retained" (low degradation), not "90% degradation" (high degradation). See [schemas.md](schemas.md) for details.
|
||||
|
||||
## Graph Reactivity
|
||||
|
||||
@@ -77,6 +89,16 @@ graphology's `Graph` class extends Node.js `EventEmitter` and emits fine-grained
|
||||
|
||||
`TaskGraph` does **not** wrap or re-emit these events. Consumers that need reactivity (e.g., file-watch → coordinator notification) access the underlying graphology instance via `graph.raw` and attach listeners directly. This keeps `TaskGraph` as a pure computation library with no opinion about reactivity.
|
||||
|
||||
## Construction Error Handling
|
||||
|
||||
| Method | Dangling references (node not in graph) | Duplicate IDs/edges | Cycles |
|
||||
|--------|----------------------------------------|--------------------|----|
|
||||
| `fromTasks` | Silently creates nodes for each `dependsOn` target that doesn't match a known task ID. These become orphan nodes with default attributes. **Recommendation**: run `validateGraph()` after construction to detect dangling references. | `DuplicateNodeError` for duplicate task IDs. Uses `mergeNode` for nodes with the same ID (idempotent merge of attributes). Duplicate `dependsOn` entries for the same pair create only one edge (idempotent via `addEdgeWithKey`). | Not detected at construction time. Call `hasCycles()` or `validateGraph()` to detect. |
|
||||
| `fromRecords` | `TaskNotFoundError` if an edge references a task ID not in the `tasks` array. Dependencies are edges and must have both endpoints present. | `DuplicateNodeError` for duplicate task IDs. `DuplicateEdgeError` for duplicate prerequisite→dependent pairs. | Not detected at construction time. Call `validateGraph()` to detect. |
|
||||
| `fromJSON` | Validated against `TaskGraphSerialized` schema. Orphan nodes in the JSON are preserved (graphology import doesn't enforce connectivity). | Uses graphology's `import()` which handles duplicates via merge behavior. | Not detected at construction time. |
|
||||
| `addTask` | N/A | `DuplicateNodeError` if ID already exists | N/A |
|
||||
| `addDependency` | `TaskNotFoundError` if prerequisite or dependent doesn't exist | `DuplicateEdgeError` if the edge already exists | Not detected until `hasCycles()` or `topologicalOrder()` |
|
||||
|
||||
## Constraints
|
||||
|
||||
- **DAG structure** — The library models task dependencies as a directed acyclic graph. Cycles are detected and reported as errors, not silently tolerated. See [errors-validation.md](errors-validation.md).
|
||||
|
||||
@@ -40,7 +40,7 @@ const edits = Value.Diff(oldSerialized, newSerialized);
|
||||
// { type: 'update', path: '/nodes/2/attributes/risk', value: 'high' },
|
||||
// { type: 'insert', path: '/nodes/5', value: { key: 'task-f', attributes: {...} } },
|
||||
// { type: 'delete', path: '/nodes/3' },
|
||||
// { type: 'update', path: '/edges/0/attributes/qualityDegradation', value: 0.8 },
|
||||
// { type: 'update', path: '/edges/0/attributes/qualityRetention', value: 0.8 },
|
||||
// ]
|
||||
```
|
||||
|
||||
|
||||
@@ -42,6 +42,16 @@ The TypeBox schemas serve as the single source of truth for both types and valid
|
||||
|
||||
## Input Schemas
|
||||
|
||||
### Schema Utility: Nullable
|
||||
|
||||
A generic helper for making schema types accept `null` in addition to their defined values:
|
||||
|
||||
```typescript
|
||||
const Nullable = <T extends TSchema>(T: T) => Type.Union([T, Type.Null()])
|
||||
```
|
||||
|
||||
Used in `TaskInput` for fields that can be explicitly set to `null` in YAML frontmatter (distinct from the field being absent).
|
||||
|
||||
### TaskInput
|
||||
|
||||
The universal input shape for a task, matching the Rust `TaskFrontmatter` field set. Note the use of `Type.Optional(Nullable(...))` for categorical fields — this makes the field itself optional at the object level AND nullable when present. YAML frontmatter distinguishes between "key absent" and "key set to null" (e.g., `risk:` with no value), so we need both.
|
||||
@@ -73,11 +83,13 @@ Where `Nullable = <T extends TSchema>(T: T) => Type.Union([T, Type.Null()])`.
|
||||
const DependencyEdge = Type.Object({
|
||||
from: Type.String(), // prerequisite task id
|
||||
to: Type.String(), // dependent task id
|
||||
qualityDegradation: Type.Optional(Type.Number()), // 0.0–1.0, default 0.9
|
||||
qualityRetention: Type.Optional(Type.Number({ default: 0.9 })), // 0.0–1.0, default 0.9
|
||||
})
|
||||
```
|
||||
|
||||
The `qualityDegradation` field models how much upstream failure bleeds through to the dependent task. Value of 0.0 means no propagation (independent model), 1.0 means full propagation. Default is 0.9 following the Python research model. Only used by `workflowCost` in DAG-propagation mode; ignored by all other algorithms.
|
||||
> **Note on naming**: The original name `qualityDegradation` was semantically inverted — a value of 0.9 meant "0.9 quality retained" (low degradation), not "0.9 degradation" (high degradation). The field is now named `qualityRetention` to match its actual semantics: 0.0 means zero quality retention (full propagation of upstream failure), 1.0 means perfect quality retention (independent model). See [cost-benefit.md](cost-benefit.md) for the propagation formula.
|
||||
|
||||
The `qualityRetention` field models how much upstream quality is preserved through a dependency edge. Value of 0.0 means no retention (full propagation of upstream failure to the dependent), 1.0 means complete retention (independent model, upstream failure doesn't affect the dependent at all). Default is 0.9 following the Python research model. Only used by `workflowCost` in DAG-propagation mode; ignored by all other algorithms.
|
||||
|
||||
## Graph Attribute Schemas
|
||||
|
||||
@@ -101,7 +113,7 @@ const TaskGraphNodeAttributes = Type.Object({
|
||||
|
||||
```typescript
|
||||
const TaskGraphEdgeAttributes = Type.Object({
|
||||
qualityDegradation: Type.Optional(Type.Number()),
|
||||
qualityRetention: Type.Optional(Type.Number({ default: 0.9 })),
|
||||
})
|
||||
```
|
||||
|
||||
@@ -140,7 +152,29 @@ const TaskRiskEnum = Type.Union([
|
||||
])
|
||||
type TaskRisk = Static<typeof TaskRiskEnum>
|
||||
|
||||
// ... same pattern for TaskImpact, TaskLevel, TaskPriority, TaskStatus
|
||||
const TaskImpactEnum = Type.Union([
|
||||
Type.Literal("isolated"), Type.Literal("component"),
|
||||
Type.Literal("phase"), Type.Literal("project"),
|
||||
])
|
||||
type TaskImpact = Static<typeof TaskImpactEnum>
|
||||
|
||||
const TaskLevelEnum = Type.Union([
|
||||
Type.Literal("planning"), Type.Literal("decomposition"),
|
||||
Type.Literal("implementation"), Type.Literal("review"), Type.Literal("research"),
|
||||
])
|
||||
type TaskLevel = Static<typeof TaskLevelEnum>
|
||||
|
||||
const TaskPriorityEnum = Type.Union([
|
||||
Type.Literal("low"), Type.Literal("medium"),
|
||||
Type.Literal("high"), Type.Literal("critical"),
|
||||
])
|
||||
type TaskPriority = Static<typeof TaskPriorityEnum>
|
||||
|
||||
const TaskStatusEnum = Type.Union([
|
||||
Type.Literal("pending"), Type.Literal("in-progress"),
|
||||
Type.Literal("completed"), Type.Literal("failed"), Type.Literal("blocked"),
|
||||
])
|
||||
type TaskStatus = Static<typeof TaskStatusEnum>
|
||||
```
|
||||
|
||||
See the naming convention table in "Design Decision: TypeBox as Single Source of Truth" above for the `Enum` suffix rule.
|
||||
@@ -190,9 +224,11 @@ function scopeTokenEstimate(scope: TaskScope): number // 500–10000
|
||||
function riskSuccessProbability(risk: TaskRisk): number // 0.50–0.98
|
||||
function riskWeight(risk: TaskRisk): number // 0.02–0.50
|
||||
function impactWeight(impact: TaskImpact): number // 1.0–3.0
|
||||
function resolveDefaults(attrs: Partial<TaskGraphNodeAttributes>): ResolvedTaskAttributes
|
||||
function resolveDefaults(attrs: Partial<TaskGraphNodeAttributes> & Pick<TaskGraphNodeAttributes, 'name'>): ResolvedTaskAttributes
|
||||
```
|
||||
|
||||
> **Why `Pick<TaskGraphNodeAttributes, 'name'>`**: `resolveDefaults` needs at minimum a `name` to produce a valid `ResolvedTaskAttributes`. The `Partial<>` wrapper would allow `name` to be `undefined`, but the graph always has a `name` on every node (it's required in `TaskGraphNodeAttributes`). The `Pick` ensures callers provide it.
|
||||
|
||||
## ResolvedTaskAttributes
|
||||
|
||||
The output of `resolveDefaults` — all categorical fields resolved to their numeric equivalents for use in analysis. Defined as a TypeBox schema (not a raw `interface`) so that `Static<typeof ResolvedTaskAttributes>` derives the TypeScript type:
|
||||
|
||||
@@ -751,7 +751,7 @@ const TaskGraphNodeAttributesUpdate = Type.Partial(TaskGraphNodeAttributes);
|
||||
type TaskGraphNodeAttributesUpdate = Static<typeof TaskGraphNodeAttributesUpdate>;
|
||||
|
||||
const TaskGraphEdgeAttributes = Type.Object({
|
||||
qualityDegradation: Type.Optional(Type.Number()),
|
||||
qualityRetention: Type.Optional(Type.Number()),
|
||||
});
|
||||
type TaskGraphEdgeAttributes = Static<typeof TaskGraphEdgeAttributes>;
|
||||
```
|
||||
@@ -838,7 +838,7 @@ const WorkflowCostOptions = Type.Object({
|
||||
propagationMode: Type.Optional(
|
||||
Type.Union([Type.Literal("independent"), Type.Literal("dag-propagate")])
|
||||
),
|
||||
defaultQualityDegradation: Type.Optional(Type.Number({ default: 0.9 })),
|
||||
defaultQualityRetention: Type.Optional(Type.Number({ default: 0.9 })),
|
||||
});
|
||||
type WorkflowCostOptions = Static<typeof WorkflowCostOptions>;
|
||||
|
||||
@@ -892,7 +892,7 @@ type RiskDistributionResult = Static<typeof RiskDistributionResult>;
|
||||
const DependencyEdge = Type.Object({
|
||||
from: Type.String(),
|
||||
to: Type.String(),
|
||||
qualityDegradation: Type.Optional(Type.Number({ default: 0.9 })),
|
||||
qualityRetention: Type.Optional(Type.Number({ default: 0.9 })),
|
||||
});
|
||||
type DependencyEdge = Static<typeof DependencyEdge>;
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user