Repository navigation
fix(pddl): correct durative, PDDL 3 and metric semantics; refuse what cannot be honoured - #172
Conversation
Write down where and why we mean to publish jupyddl as citable research software (.docs/PUBLISHING.md): a Zenodo DOI per release, a JOSS paper, ICAPS workshops for the research results, with a readiness checklist, the paper outline, the AI-usage disclosure JOSS requires, and how contributors can be considered for authorship. The README states the project's aim and its Cite section now says which version the 2021 preprint describes. Add the community files an outside contributor expects: a security policy, a pull-request template, a "Propose an algorithm" issue template, and an "Adding an algorithm" section in CONTRIBUTING.md. Run tests and build on pull requests. They were `on: [push]`, which never fires for a fork, so a contributor's PR showed no checks at all. Fix AGENTS.md drift: the Python floor is 3.10 and Mergify is retired. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
A package can now add planners, heuristics and generators by declaring `jupyddl.planners`, `jupyddl.heuristics` or `jupyddl.generators` entry points. Once installed, they work from the CLI, the benchmark harness and `jupyddl.solve` by name, without anyone importing the package. Discovery is lazy and defensive (jupyddl/plugins.py): the CLI runs it before building its parser, the library only on a registry miss, so importing jupyddl never scans installed packages. A built-in name always wins, a plugin that fails to import is skipped with a warning, and JUPYDDL_NO_PLUGINS=1 turns it off. `jupyddl plugins` lists what was found. The CLI now reads `requires_heuristic` from the registry at call time, since INFORMED_PLANNERS is fixed at import and cannot know about plugins. .docs/EXTENDING.md walks through a heuristic, a planner from a priority function, a planner from scratch with the budget/observer contract, a generator, testing the guarantees you claim, and shipping as a plugin. tests/test_extending.py runs every Python block in it, so it cannot drift. best_first now accepts a tuple priority for tie-breaking; observers get its first element as `f`, so traces stay numeric. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
# Conflicts: # .docs/PUBLISHING.md # AGENTS.md # CHANGELOG.md
Each of these returned a plan, a cost or a value without complaint, and validate_plan accepted the plans because it replays them through the same Task.apply. tests/test_semantics.py now pins them on tiny hand-written tasks; 16 of its cases fail on the previous code. - Derived facts were never retracted, and (not <derived>) held from the start. close() now recomputes axioms from scratch and maintains negated derived facts; negation inside a rule body (needs stratification) is refused. The relaxation gets the complement for free, so it never calls a solvable state a dead end. - An atom both added and deleted left its complement true too; a `when` re-adding a deleted atom did the same. Complements: the delete wins. - Under a (total-cost) metric an action without a cost effect is free, as in IPC planners (pegsol/sokoban optimised the wrong objective). Cost inside `when` was charged unconditionally: refused, as are negative costs. - A never-defined numeric fluent read as 0; conditions reading one fail. - blind skipped zero-cost actions and overestimated. - h1/h2/hm crashed on numeric states; LM-cut crashed on an empty goal. - lmcut and h2 clear `admissible` on tasks with conditional effects. - Optimal planners default to lmcut, not hff; `-H none` means blind. - make_heuristic builds a heuristic class or partial instead of passing it through unbuilt. - Fact ids and id sets are built in sorted order, so LM-cut tie-breaks, expansions and plans no longer change with PYTHONHASHSEED. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
- iddfs, idastar and ehc never read their budget, so --time-limit was ignored. They poll per expansion now; the recursive ones unwind with BudgetExhausted. beam polls per node, not per layer, and bnb stops evaluating siblings once the budget fires. - Hitting a depth cap (iddfs max_depth, bnb max_depth) or Python's recursion limit sets stats.truncated: it is not a proof of unsolvability, nor of optimality. - bfs and iddfs leave OPTIMAL_PLANNERS: they minimise plan length, and on errands return cost 16 where 10 is optimal. - Live dashboard: a budget stop no longer reads "no plan found"; lines are clipped to the stream's terminal width so repaints do not smear; the non-tty log writes at most every two seconds. - idastar/ehc/hc/beam without a heuristic raise a clear ValueError; MultiObserver forwards positional arguments; the SearchObserver and best_first docstrings say what actually happens. tests/test_search_contract.py runs every planner where the budget is the only thing that stops it, checks OPTIMAL_PLANNERS against dijkstra under action costs, and pins the dashboard behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
- Exit codes say what happened: 0 solved, 1 no plan, 2 bad input, 3 stopped by a budget, 4 a plan that failed validation. Unsolvable, out of budget and a crash all used to exit 1. - Bad input (missing file, parse error, unknown planner or heuristic, a malformed --sizes) is one line on stderr, not a traceback; JUPYDDL_DEBUG=1 restores the traceback. --version exists. - `demo` and `benchmark demos` never finished. Both stop each search after 10 s by default (--time-limit 0 for none); animate and demo take the same budget flags as solve. - benchmark validates --planners/--heuristic/--metric up front, and chart options check for the viz extra before searching rather than after. - Dead ends (h = inf) crashed the progress chart and coloured the wavefront NaN; in the workbench they made the progress message invalid JSON and aborted the run. - A trace's plan is the visible plan, without compiler bookkeeping. - Packaging: the sdist's own suite failed because .github/ was dropped wholesale; only workflows are excluded now. build runs twine --strict. black is pinned to one major in dev and format.yml; Renovate pins third-party actions to digests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
PR titles become Conventional Commits (pr-title check, plain shell, no third-party action). On every push to main release-please keeps one release PR whose version follows from what was merged: fix -> patch, feat -> minor, feat!/BREAKING CHANGE -> major. tools/release_pr_fixup.py then files the hand-written [Unreleased] notes under the new section and rebuilds web/dist, which records the version. Merging the release PR tags the commit; release-please.yml then dispatches release.yml on the tag, because a tag made with the workflow token triggers no workflows. release.yml now also refuses a dispatch that would re-release an existing version. Renovate opens semantic titles so its PRs pass the check. tests/test_release.py pins the manifest, pyproject and __version__ together and checks the changelog fix-up, including the section release.yml extracts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
…ct benchmark configs - Ranking ties counted as wins, so a constant model scored top-1 = 1.0 and checkpoint selection preferred collapsed epochs. Identical siblings are dropped and a tie with k siblings earns 1/(k+1). - "Held out" top-1 used training groups; split now keeps the held-out groups and selection/reporting use them. - gripper and workshop ignored the seed, so tuning, validation and held-out sets were duplicates. The seed now varies the instance. - blocksworld and random-strips could generate instances already solved at the start; those seeds are re-rolled, every other seed is unchanged. - Malformed heuristic bundles are refused on load; set_flat is atomic; negated facts key on not-<predicate>; DAgger/bootstrap samples split with their base instance; pure ranking without groups raises; learn_heuristic accepts generators for its size arguments. - Benchmark rows carry a config label including planner arguments and record the heuristic that actually ran (api.resolve_heuristic). - summarise_transfer charges unsolved runs instead of averaging over solved ones only. Published figures are left as measured; .docs/learned-heuristics.md and rl-for-search.md carry dated corrections saying what each one measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
… cannot be honoured - Durative actions: at-start effects apply before at-end effects (at-end wins), and over-all/at-end conditions are checked in the state after the at-start effects. Forms the sequential compilation cannot express are refused by name. - sometime-after: an obligation set by the last action can be discharged. - forall around a trajectory constraint or a goal preference is expanded per object instead of being read as one conjunction (or crashing). - Timed initial literals: forced monitors ride on their firing actions, and closing a plan blocks them, so a preference cannot be credited and undone. - Object fluents are rewritten inside constraints; fluent-valued assigns are refused. - Metric: an unmentioned preference costs 0; a metric without total-cost charges nothing for domain actions; total-time charges durations; maximize and other shapes are refused. - ground() no longer mutates the parsed Domain/Problem. - Invalid PDDL (unknown keys and sections, missing :goal) and malformed input are PDDLErrors with line numbers, never a bare IndexError; objects named nan/inf parse as objects. - (increase (total-cost) (road-length ?a ?b)) over static functions is supported, so the IPC transport domains parse; :fluents implies :object-fluents; :ucpop, :domain-axioms and :safety-constraints are refused by name. tests/test_frontend_semantics.py: 56 cases, 55 of which fail on the previous code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014RbGtRHF8XhkiPQCDVHtV2
|
CodeFactor: "3 issues found" The CodeFactor page lists issues client-side, so I couldn't read the exact three. I measured cyclomatic complexity of the changed files instead (
That is almost certainly CodeFactor's "Complex Method" findings. The growth comes from the new validation and refusal branches: unknown keys and sections, metric shapes, timed effects. Decision: not refactoring in this PR. These are maintainability findings, not correctness ones, and the repository's own lint does not gate on complexity. Splitting the parser functions here would mix a structural refactor into a semantic change that 56 new tests pin. It is worth doing as its own Generated by Claude Code |
What and why
Fixes for the parser and PDDL 3 review, the last of the full code review. The rule throughout: fix it when a correct fix exists, otherwise refuse the construct by name. Never return a silently wrong plan.
(at start (busy)) (at end (not (busy)))leftbusytrue. At-end conditions were folded into the preconditionalways) are checked in the state after the at-start effects. Forms the sequential compilation can't express are refused by namesometime-after[t, r]was reported unsolvableψ ∨ (¬pending ∧ ¬φ)forallaround a constraintsometime (forall …), i.e. all objects at once. Wrapped around a preference, it crashednotsilently made the goal falsenot/or/imply/existsis aPDDLError(assign (loc ?p) (loc ?q))treated a fluent name as an objectmaximizewas treated as minimise. Withouttotal-costin the metric, actions still cost somethingtotal-cost, domain actions are free.(total-time)charges durations.maximizeand other shapes are refusedground():preconditon,(:goals …)and a missing:goalwere silently accepted. Malformed input raised bareIndexErrorPDDLErrornaming the key. The tokenizer reports line numbers. A 25k-mutation fuzz raised nothing butPDDLErrornan/inf(increase (total-cost) (road-length ?a ?b))was refused, although:action-costsis marked nativetransportdomains now parse (212 of 213 example files;gridstays intentionally unsupported):ucpop,:domain-axiomsand:safety-constraintswere reported as unknown:fluentsimplies:object-fluents. The three flags are refused by name, so the README reads 20 of 24 flags supportedrequirements.py, the README PDDL table (regenerated from the registry),.docs/PUBLISHING.mdand AGENTS.md (new notes on durative semantics and the cost model) are updated.tools/make_promo.pynow computes the flag count. The promo video has not been re-rendered.How it was checked
tests/test_frontend_semantics.py: 56 cases. 55 fail on the previous code, each for its semantic reason; the 56th is a guard that the fix didn't loosen anything.vehicleis still correctly unsolvable, andgridstill raisesUnsupportedFeatureError.main(including fix(learn): honest ranking metrics, seed-sensitive generators, distinct benchmark configs #171): 625 passed, 1 skipped.flake8andblackare clean, andweb/distis rebuilt.Behaviour changes to note
maximizemetric, or a form the compilations can't honour. Each refusal names the construct.Checklist
flake8 jupyddl tests toolsandpytestpass locallypython tools/build_web.pyre-run;web/distcommittedrequirements.pychanged first; README table follows itCHANGELOG.mdupdatedThe history has a
merge -s oursof the already-merged #171 tip so the push fast-forwards. Squash-merge with the PR title.Generated by Claude Code