Foundations to FrontierPart IX: The graduate coreBezhig

G10: Project reasoning, dependency graphs, and risk

6 exercises, each with a complete solution. 1 figure. 1 code listing.

On this page
  1. Prerequisites and observable goals
  2. The central question: what must finish before something else can start?
  3. Critical-path analysis from a DAG
  4. Worked plan
  5. Uncertainty and scope are not fixed durations
  6. A risk register connected to action
  7. Planning for independent learning
  8. Independent practice
  9. Smallest useful hints
  10. Complete solutions: G10
  11. Readiness and continuation

Prerequisites and observable goals#

Before

foundation C05, C08, and C14; G09’s requirements and architecture record.

After

Compute a critical path under explicit assumptions, distinguish uncertainty from a precise schedule, and plan reviewable milestones with acceptance evidence.

The central question: what must finish before something else can start?#

A project plan is a model of dependencies, resources, uncertainty, and evidence. A list of dates without those relationships hides the reasons it may fail. For a solo developer, context switching and learning effort are real resource constraints. Do not interpret a parallel dependency diagram as a promise that one person can do all parallel tasks at once.

A milestone should produce an inspectable artifact: a passing contract test, a verified AST, a reproducible experiment report, or a fault-recovery trace. “Worked on compiler” records effort but does not identify what is now demonstrated. “Parser rejects every unsupported construct in the specified suite and produces typed ASTs for the acceptance examples” is a bounded completion claim.

Critical-path analysis from a DAG#

Let each task vv have fixed nonnegative duration d(v)d(v) and predecessor set pred⁡(v)\operatorname{pred}(v). Assume unlimited resources, no interruptions, and a DAG of finish-to-start dependencies. Earliest start and finish satisfy

ES(v)=max⁡u∈pred⁡(v)EF(u),EF(v)=ES(v)+d(v),ES(v)=\max_{u\in\operatorname{pred}(v)}EF(u),\qquad EF(v)=ES(v)+d(v),

with maximum over an empty set defined as zero. Process tasks in topological order. The earliest whole-project finish is T=max⁡vEF(v)T=\max_vEF(v). In reverse order, compute

LF(v)=min⁡w∈succ⁡(v)LS(w),LS(v)=LF(v)−d(v),LF(v)=\min_{w\in\operatorname{succ}(v)}LS(w),\quad LS(v)=LF(v)-d(v),

using LF(v)=TLF(v)=T for terminal tasks. Slack is LS(v)−ES(v)LS(v)-ES(v). Zero-slack tasks lie on at least one critical path under this model.

The derivation follows directly from constraints: a task cannot begin until every predecessor finishes, so the maximum is a lower bound and is attainable with unlimited resources. Backward limits prevent any successor from starting late. Negative cycles are not the issue; any dependency cycle makes this finish-to-start model infeasible without redefining tasks or dependencies.

Worked plan#

Tasks A, B, C, D have durations 3, 5, 4, 2. A has no predecessor; B and C follow A; D follows both B and C. Then

ES(A)=0,ES(B)=ES(C)=3,ES(D)=max⁡(8,7)=8,ES(A)=0,\quad ES(B)=ES(C)=3,\quad ES(D)=\max(8,7)=8,

so T=10T=10. Reverse calculation gives LS(D)=8LS(D)=8, LS(B)=3LS(B)=3, LS(C)=4LS(C)=4, and LS(A)=0LS(A)=0. Only C has slack one. The critical path A–B–D has length ten.

A solo developer with one task processed at a time needs at least 3+5+4+2=143+5+4+2=14 units for this work, absent other efficiencies. The ten-unit DAG result is an unlimited-resource lower bound, not a one-person commitment. Introducing resource capacities turns scheduling into a different optimization problem.

Uncertainty and scope are not fixed durations#

A guessed duration of five days is not a measured constant. Record a range and its basis: prior similar work, unknown API behavior, dependency availability, or a learning gap. Simple three-point estimates can prompt discussion but do not become validated probability distributions merely because a formula combines them. A Monte Carlo schedule is only as credible as its duration and dependency assumptions.

Separate delivery risk from technical correctness. A feature may be mathematically correct but impossible to validate before a deadline; an easy feature may be unnecessary to the stated goal. Reduce risk by building a thin end-to-end vertical slice early: parse one expression through execution, or train one baseline through artifact loading and result storage. That reveals interface misunderstandings before every subsystem grows independently.

A risk register connected to action#

A useful entry includes condition, consequence, likelihood/severity judgment, mitigation, trigger, owner, and review date. For GP1: an unsupported syntax construct sneaks through host parsing; consequence is a language-scope or safety breach; mitigation is an explicit AST whitelist and rejection tests; trigger is a new grammar feature; owner is the person changing the parser. For GP2: model feature order changes; consequence is silent wrong scoring; mitigation is a versioned schema and artifact validation; trigger is any feature-pipeline change.

Do not hide uncertainty in decorative red/amber/green labels. Record why you assigned a priority and what observation would change it. A low-probability irreversible failure can require stronger mitigation than a common recoverable inconvenience. The right response can be removing a feature, reducing scope, adding a check, or deferring deployment, not always writing more code.

Planning for independent learning#

Distinguish implementation time from prerequisite repair. Before a compiler milestone, reserve a diagnostic for scope and stack tracing. Before an ML milestone, reserve a gradient check and a leakage review. A learning plan that assumes these skills without evidence can fail for reasons unrelated to effort.

Use two effort levels. In a lower-load 30–45 minute session, complete one proof obligation or one failing test investigation. In a 60–90 minute session, implement a bounded function, test it, and explain one changed-input case. Do not measure progress only in pages or lines. Preserve unaided attempts so that later code review can distinguish your understanding from a copied artifact.

Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

G10.01CSolution

Compute earliest starts, latest starts, slack, and finish for A=3, B=5, C=4, D=2 with dependencies A before B/C and B/C before D.

G10.02TSolution

Increase C to duration 7. Recompute the finish and critical path. Explain what happens to B’s slack.

G10.03CSolution

Explain why the unlimited-resource result of ten units is not a feasible duration for one nonpreemptive worker doing all four original tasks sequentially. Give the lower bound in that resource model.

G10.04TSolution

Write a risk entry for a training/test split accidentally sharing user groups. Include a prevention check, a trigger for review, and an action when it is discovered after evaluation.

G10.05TSolution

Turn “finish the compiler” into three progressive milestones with acceptance criteria and one deliberate nonrequirement. Explain why a thin end-to-end slice should come before many optimizations.

G10.06LSolution

Implement critical_path in starters/planning.py for at most 1,000 named tasks with finite nonnegative durations. Reject unknown dependencies and cycles. Return finish, earliest/latest starts, and slack. Test disconnected tasks, zero durations, a cycle, and the worked example; report the model’s resource assumptions.

Smallest useful hints#

For .02, recompute the maximum predecessor finish at D, then traverse backward. For .04, a discovered leak affects the credibility of the result, not only the next code commit. For .06, Kahn’s algorithm detects a cycle when not all tasks are processed.

Complete solutions: G10#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

G10.01Show solution

A critical path with one unit of slack

Earliest starts: A 0, B 3, C 3, D 8. Finish: 10. Latest starts: A 0, B 3, C 4, D 8. Slack: A 0, B 0, C 1, D 0. Path A–B–D determines ten units. These results assume fixed durations and enough resources to overlap B and C.

Back to G10.01
G10.02Show solution

The controlling branch changes

A finishes at 3, B at 8, C at 10, so D starts at 10 and finishes at 12. Latest starts are D 10, C 3, B 5, A 0. B now has slack 5−3=25-3=2; C has zero. Path A–C–D is critical. Slack is relative to the whole current dependency model, not an inherent property of a task.

Back to G10.02
G10.03Show solution

Resource capacity adds another lower bound

The four durations sum to 14. One worker cannot perform B and C simultaneously, so even though neither depends on the other, their nine combined units must be scheduled sequentially. Any topological order A,B,C,D or A,C,B,D takes 14 under these fixed-duration assumptions. The project finish must satisfy both dependency and resource lower bounds.

Back to G10.03
G10.04Show solution

Treat leakage as an evidence failure

Risk: records from one user appear in training and evaluation, allowing correlated behavior to inflate performance. Prevention: split group IDs first, assert disjointness, and fit preprocessing on training only. Trigger: any join, split, deduplication, or feature-generation change. On discovery: mark the prior evaluation invalid for its claimed generalization target, repair the pipeline, document the issue, and evaluate under a corrected plan with appropriately reserved data. Do not keep the favorable number and quietly change the code afterward.

Back to G10.04
G10.05Show solution

Milestones with observable outputs

First: literals, addition, and multiplication parse to typed ASTs, interpret, compile, and execute with equal results on acceptance examples. Second: lexical let and conditionals pass shadowing and branch-type tests, with a verifier rejecting malformed bytecode. Third: semantics-preserving simplification passes differential tests and a written rule-by-rule argument, with a reproducible build/report. Nonrequirement: general Python compatibility or native machine-code generation. The thin slice detects representation/interface mistakes before a large optimizer depends on them.

Back to G10.05
G10.06Show solution

Complete dependency-model implementation

The heap gives deterministic ordering among currently ready tasks. The forward pass computes earliest times; the reverse pass computes latest allowable starts. Set-valued dependencies avoid double-counting duplicate prerequisites. This is a critical-path calculator, not a resource-constrained scheduler or a probabilistic completion forecast.

graduate_code/solutions/planning.pyPython
"""G10: deterministic critical path under unlimited-resource assumptions."""
import heapq
import math


def critical_path(durations, dependencies):
    if not 1 <= len(durations) <= 1000:
        raise ValueError("1..1000 tasks required")
    if any(type(name) is not str or not name for name in durations):
        raise ValueError("nonempty string task names required")
    if any(not math.isfinite(d) or d < 0 for d in durations.values()):
        raise ValueError("finite nonnegative durations required")
    if set(dependencies)-set(durations):
        raise ValueError("dependency map names an unknown task")
    before = {name:set(dependencies.get(name,())) for name in durations}
    if any(pred-set(durations) for pred in before.values()):
        raise ValueError("unknown predecessor")
    after = {name:[] for name in durations}
    for task,pred in before.items():
        for p in pred:
            after[p].append(task)
    degree = {name:len(pred) for name,pred in before.items()}
    ready = [name for name,d in degree.items() if d == 0]
    heapq.heapify(ready)
    order,earliest,finish = [],{},{}
    while ready:
        task = heapq.heappop(ready)
        order.append(task)
        earliest[task] = max((finish[p] for p in before[task]),default=0.)
        finish[task] = earliest[task]+durations[task]
        if not math.isfinite(finish[task]):
            raise ValueError("time overflow")
        for child in after[task]:
            degree[child] -= 1
            if degree[child] == 0:
                heapq.heappush(ready,child)
    if len(order) != len(durations):
        raise ValueError("dependency cycle")
    end = max(finish.values())
    latest = {}
    for task in reversed(order):
        end_task = min((latest[c] for c in after[task]),default=end)
        latest[task] = end_task-durations[task]
    return {"finish":end,"earliest":earliest,"latest":latest,
            "slack":{t:latest[t]-earliest[t] for t in durations},"order":order}


if __name__ == "__main__":
    print(critical_path({"A":3,"B":5,"C":4,"D":2},
                        {"B":["A"],"C":["A"],"D":["B","C"]}))
Back to G10.06

Readiness and continuation#

You should be able to explain why a changed duration moves the critical path and why spare dependency slack is not automatically spare human capacity. Continue with resource-constrained scheduling, queueing of work in progress, calibrated estimation, and real project retrospectives. The reference is a bounded analysis tool for reasoning, not proof of project-management expertise.