@danypops/papyrus 0.58.0 → 0.58.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.58.0",
3
+ "version": "0.58.2",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -2,15 +2,11 @@ import type { ScopeAssignmentSource } from "../project-registry/scope-source.ts"
2
2
  import type { ScopeMemberRef } from "../scope-group/scope-group.ts";
3
3
 
4
4
  /**
5
- * Project scoping for Docs/Rules/Playbooks: an artifact is explicitly "none" (hidden -- never
6
- * applicable, never context-injected, regardless of project), explicitly "all" (applies
7
- * everywhere), or bound to a bounded, non-empty set of explicit members -- never inferred from
8
- * an accidentally empty join table, which is why `mode` is its own explicit field rather than
9
- * "members.length === 0 means none/all". A member is either a registered project (from the
10
- * shared ProjectRegistryStore) or a scope group (from ScopeGroupStore, itself a possibly-nested
11
- * collection of projects/groups) -- "explicit scope can include nested scopes". Membership is by
12
- * id, never by root/name, so a registered project's root (or a group's name) can move without a
13
- * best-effort string rewrite across every artifact that references it.
5
+ * Project scoping for Docs/Rules/Playbooks: "none" hides an artifact from context injection
6
+ * entirely, "all" applies it everywhere, "explicit" binds it to a non-empty member set (a
7
+ * project or a possibly-nested scope group). `mode` is its own field rather than inferred from
8
+ * an empty member list, and membership is by id rather than root/name, so a project or group can
9
+ * be renamed/moved without rewriting every artifact that references it.
14
10
  */
15
11
  export type ArtifactScopeMode = "none" | "all" | "explicit";
16
12
 
@@ -18,9 +18,7 @@ export class InMemoryArtifactScopeStore implements ArtifactScopeStore {
18
18
  private readonly registry: InMemoryProjectRegistryStore;
19
19
  private readonly scopeGroups: ScopeGroupStore;
20
20
 
21
- // Membership is stored by project/group id, never by root/name, so a registry root move (see
22
- // ProjectRegistryStore.registerProject) or a scope group rename needs no rewrite here at all --
23
- // unlike InMemoryTaskScopeStore, this store never subscribes to root-move notifications.
21
+ // Membership is by project/group id, so a registry root move or group rename needs no rewrite here.
24
22
  constructor(registry?: ProjectRegistryStore, scopeGroups?: ScopeGroupStore) {
25
23
  this.registry = registry instanceof InMemoryProjectRegistryStore ? registry : new InMemoryProjectRegistryStore();
26
24
  this.scopeGroups = scopeGroups ?? new InMemoryScopeGroupStore();
@@ -1,21 +1,8 @@
1
1
  /**
2
- * Gate-execution engine, split out of ops.ts (the artifact-CRUD file) as part of a SOLID-audit-
3
- * driven decomposition (see Doc "Modularity playbook: building-block-shaped TypeScript modules
4
- * for papyrus/pi-papyrus" and the "gate-execution engine" child of "Epic: Modularize papyrus/
5
- * pi-papyrus god-files into building-block modules"). This logic was already unified in a prior
6
- * refactor (sync/async outcome evaluation shared via evaluateProcessGateResult/spawnErrorGateResult)
7
- * but still lived inside the artifact-CRUD file until now.
8
- *
9
- * Only `runGates`/`runGatesAsync` are real public API -- verified via find_references before this
10
- * move, not assumed from a grep hit count: the only two real importers were
11
- * stores/sqlite-gate-runner.ts and test/ops.test.ts (every other `runGates`-named hit in the
12
- * codebase is Tasks.runGates, a same-named but distinct method that calls into this module only
13
- * indirectly, through the GateRunner port). Both were updated to import from this file directly;
14
- * no barrel re-export needed.
15
- *
16
- * Depends on ops.ts's own `getArtifact` (still the right owner of that read -- it's real
17
- * artifact-CRUD, not gate-execution's own concern) -- a one-directional dependency, since ops.ts
18
- * no longer needs to import anything back from here.
2
+ * Gate-execution engine. `Tasks.runGates` is a separate, same-named method that calls into this
3
+ * module only indirectly, through the GateRunner port -- not the same thing as this file's
4
+ * `runGates`/`runGatesAsync`. Depends on ops.ts's `getArtifact` (real artifact-CRUD, not this
5
+ * module's own concern); ops.ts does not depend back on this file.
19
6
  */
20
7
  import { createRequire } from "node:module";
21
8
  import {
@@ -1,9 +1,4 @@
1
- /**
2
- * Cross-domain artifact name/id resolution and workflow-run narrative building, split out of
3
- * handlers/shared.ts as part of a SOLID-audit-driven decomposition (see Doc "Modularity playbook:
4
- * building-block-shaped TypeScript modules for papyrus/pi-papyrus" and the "handlers/shared.ts
5
- * split" child of "Epic: Modularize papyrus/pi-papyrus god-files into building-block modules").
6
- */
1
+ /** Cross-domain artifact name/id resolution and workflow-run narrative building. */
7
2
  import { type VehicleContentBlock, VehicleError } from "@danypops/vehicle-core";
8
3
  import type { Artifact } from "../artifact/artifact.ts";
9
4
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
@@ -1,10 +1,7 @@
1
1
  /**
2
- * Generic, domain-agnostic operation-input schema DSL, split out of handlers/shared.ts as part of
3
- * a SOLID-audit-driven decomposition (see Doc "Modularity playbook: building-block-shaped
4
- * TypeScript modules for papyrus/pi-papyrus" and the "handlers/shared.ts split" child of "Epic:
5
- * Modularize papyrus/pi-papyrus god-files into building-block modules"). Nothing here references
6
- * a specific Papyrus domain (tasks/docs/rules/...) -- a real candidate to eventually become its
7
- * own building block other Vehicle-backed daemons could reuse directly.
2
+ * Generic, domain-agnostic operation-input schema DSL. Nothing here references a specific
3
+ * Papyrus domain (tasks/docs/rules/...) -- a real candidate to eventually become its own building
4
+ * block other Vehicle-backed daemons could reuse directly.
8
5
  */
9
6
  import {
10
7
  defineVehicleSchema,
@@ -1,10 +1,4 @@
1
- /**
2
- * Vehicle-operation-definer DSL (createOperationDefiner) and the paired add/remove mutation shape
3
- * built on top of it (definePairedMutation), split out of handlers/shared.ts as part of a
4
- * SOLID-audit-driven decomposition (see Doc "Modularity playbook: building-block-shaped
5
- * TypeScript modules for papyrus/pi-papyrus" and the "handlers/shared.ts split" child of "Epic:
6
- * Modularize papyrus/pi-papyrus god-files into building-block modules").
7
- */
1
+ /** Vehicle-operation-definer DSL (createOperationDefiner) and the paired add/remove mutation shape built on top of it (definePairedMutation). */
8
2
  import { bindVehicleOperation, defineVehicleOperation, type VehicleLimits, type VehicleOperationContext } from "@danypops/vehicle-core";
9
3
  import type { VehicleRegistry } from "@danypops/vehicle-server";
10
4
  import { looseObjectSchema, passthroughOutput } from "./operation-schema.ts";
@@ -1,20 +1,11 @@
1
1
  /**
2
2
  * Barrel re-exporting the shared schema helpers and name->id resolution used by every per-domain
3
- * VehicleRegistry projection (notes-vehicle.ts, rules-vehicle.ts, docs-vehicle.ts,
4
- * artifact-trash-vehicle.ts, ...). Kept as a real file (not a `shared/index.ts` subdirectory) so
5
- * every existing `from "./shared.ts"` import across the 8 handler files that depend on it keeps
6
- * resolving unchanged -- this codebase's imports always carry an explicit `.ts` extension, which
7
- * does not implicitly resolve a bare specifier to a directory's own index file the way Node's
8
- * CJS `require()` does.
9
- *
10
- * The real implementation now lives in four focused sibling modules instead of one 435-line file
11
- * mixing four unrelated concerns -- see Doc "Modularity playbook: building-block-shaped
12
- * TypeScript modules for papyrus/pi-papyrus" and the "handlers/shared.ts split" child of "Epic:
13
- * Modularize papyrus/pi-papyrus god-files into building-block modules":
14
- * - operation-schema.ts: generic, domain-agnostic operation-input schema DSL
15
- * - paired-mutation.ts: the Vehicle-operation-definer DSL and the paired add/remove shape built on it
16
- * - task-classifiers.ts: business-rule error classifiers specific to this package's own domain errors
17
- * - artifact-helpers.ts: cross-domain artifact name/id resolution and workflow-run narrative building
3
+ * VehicleRegistry projection. Kept as a real file (not a `shared/index.ts` subdirectory) so every
4
+ * existing `from "./shared.ts"` import keeps resolving unchanged -- this codebase's imports
5
+ * always carry an explicit `.ts` extension, which does not implicitly resolve a bare specifier to
6
+ * a directory's own index file the way Node's CJS `require()` does. Real implementation lives in
7
+ * operation-schema.ts (schema DSL), paired-mutation.ts (operation-definer DSL), task-classifiers.ts
8
+ * (domain error classifiers), artifact-helpers.ts (name/id resolution, workflow-run narrative).
18
9
  */
19
10
  export {
20
11
  buildWorkflowRunContent,
@@ -1,12 +1,4 @@
1
- /**
2
- * Business-rule error classifiers -- turn an ordinary, expected domain rejection into its own
3
- * classified VehicleError instead of vehicle-registry's generic opaque "handler-failed", split out
4
- * of handlers/shared.ts as part of a SOLID-audit-driven decomposition (see Doc "Modularity
5
- * playbook: building-block-shaped TypeScript modules for papyrus/pi-papyrus" and the
6
- * "handlers/shared.ts split" child of "Epic: Modularize papyrus/pi-papyrus god-files into
7
- * building-block modules"). Unlike operation-schema.ts, every function here is specific to this
8
- * package's own domain error types.
9
- */
1
+ /** Business-rule error classifiers -- turn an ordinary, expected domain rejection into its own classified VehicleError instead of vehicle-registry's generic opaque "handler-failed". */
10
2
  import { VehicleError } from "@danypops/vehicle-core";
11
3
  import { PlaybookCompositionError } from "../playbook/playbook-definition.ts";
12
4
  import { InvalidSessionSecretError } from "../session-identity/session-identity-service.ts";
@@ -1,24 +1,9 @@
1
1
  import { basename, isAbsolute, normalize } from "node:path";
2
2
  import { TASK_PROJECT_ROOT_MAX_LENGTH } from "../constants.ts";
3
3
 
4
- /**
5
- * How a project root ended up attached to a scoped artifact -- shared across every artifact kind
6
- * (Tasks, Docs, Rules, Playbooks) rather than owned by Tasks alone, same category as Project
7
- * itself in project-registry.ts. This lived inside task-scope.ts under the name
8
- * `TaskScopeSource` until a SOLID audit (see Doc "SOLID pattern analysis: task-event/task-scope
9
- * shared-kernel extraction") found it consumed identically by artifact/, docs-service.ts,
10
- * rules-service.ts, playbook-service.ts, and modules/projects.ts -- a Role Interface split
11
- * couldn't fix this half of the problem because there's no single supplier to narrow a contract
12
- * for; every one of those five call sites is a peer owner of the same vocabulary. task-scope.ts
13
- * keeps `TaskScopeSource` as a backward-compatible alias of this type for Task's own call sites.
14
- */
4
+ /** How a project root got attached to a scoped artifact -- shared across Tasks/Docs/Rules/Playbooks, same category as Project in project-registry.ts. */
15
5
  export type ScopeAssignmentSource = "cwd" | "explicit" | "unscoped";
16
6
 
17
- /**
18
- * Same relocation as ScopeAssignmentSource above, for the same reason: a pure, artifact-kind-
19
- * agnostic validation/normalization function that every non-Task scope-assigning service already
20
- * called directly rather than through any Task-owned abstraction.
21
- */
22
7
  export function normalizeProjectRoot(value: string): string {
23
8
  if (!isAbsolute(value)) throw new Error("project_root must be an absolute path");
24
9
  const normalized = normalize(value);
@@ -1,17 +1,10 @@
1
1
  import { SCOPE_GROUP_ALIAS_MAX_COUNT, SCOPE_GROUP_NAME_MAX_LENGTH } from "../constants.ts";
2
2
 
3
3
  /**
4
- * A named, reusable collection of scope members (registered projects and/or other scope groups)
5
- * -- the "explicit scope can include nested scopes" primitive. Lives alongside the project
6
- * registry (same shape: id/name/aliases/createdAt/updatedAt) rather than as an Artifact, since
7
- * it is bookkeeping infrastructure for scoping, not itself a piece of knowledge or work.
8
- *
9
- * Deliberately explicit/opt-in membership, not filesystem-path-derived nesting: a registered
10
- * project whose root happens to be a subdirectory of another registered project's root is NOT
11
- * automatically its child (a real false-positive confirmed live: alignment-lector is registered
12
- * under lector/packages/alignment-lector, a subdirectory of lector's own root, but is Lector's
13
- * adapter for a *different* project, not part of Lector's own scope). A group's membership is
14
- * only ever what was deliberately added to it.
4
+ * A named, reusable collection of scope members (registered projects and/or other scope groups).
5
+ * Membership is explicit/opt-in, never path-derived: a project registered under another
6
+ * project's root directory (e.g. a vendored adapter package) is not automatically that parent's
7
+ * child -- only what's deliberately added counts.
15
8
  */
16
9
  export interface ScopeGroup {
17
10
  id: string;
@@ -10,16 +10,7 @@ import {
10
10
  validateTaskEvent,
11
11
  } from "./task-event.ts";
12
12
 
13
- /**
14
- * Role Interface (Fowler: https://martinfowler.com/bliki/RoleInterface.html) for the one
15
- * collaboration playbook workflow execution actually has with Task's event log: append one event
16
- * inside a single atomic transaction. Extracted after a SOLID audit found TaskEventStore's full
17
- * Header Interface (all 4 methods, including history/feed -- pagination concerns that exist
18
- * purely for Task's own history/feed reader endpoints) threaded unchanged through
19
- * handlers/playbooks.ts, modules/playbooks.ts, and playbook/workflow-execution.ts, which never
20
- * calls history()/feed() at all. TaskEventStore still implements this structurally with zero
21
- * changes to either concrete store; only the playbook-facing field type narrows to this.
22
- */
13
+ /** What a caller outside Task needs to append events -- history/feed stay on TaskEventStore since only Task's own readers use them. */
23
14
  export interface TaskEventSink {
24
15
  atomic<T>(operation: () => T): T;
25
16
  append(event: AppendTaskEvent): TaskEvent;
@@ -9,17 +9,7 @@ import type {
9
9
  TaskViewPreference,
10
10
  } from "./task-scope.ts";
11
11
 
12
- /**
13
- * Role Interface (Fowler: https://martinfowler.com/bliki/RoleInterface.html) for the one
14
- * collaboration playbook workflow execution actually has with Task's own scope bookkeeping:
15
- * assigning a task's project scope when a playbook step creates/advances one. Extracted after a
16
- * SOLID audit found TaskScopeStore's full Header Interface (all 8 methods -- project-catalog
17
- * registration, view-preference bookkeeping, task-id listing, none of which playbook ever
18
- * touches) threaded unchanged through handlers/playbooks.ts, modules/playbooks.ts, and
19
- * playbook/workflow-execution.ts, which only ever calls .assign(). TaskScopeStore still
20
- * implements this structurally with zero changes to either concrete store; only the
21
- * playbook-facing field type narrows to this.
22
- */
12
+ /** What a caller outside Task needs to assign a task's project scope -- the rest of TaskScopeStore (catalog, views, listing) stays Task-only. */
23
13
  export interface TaskScopeAssigner {
24
14
  assign(taskId: string, projectRoot: string | undefined, source: TaskScopeSource): TaskProjectScope;
25
15
  }
@@ -34,13 +24,7 @@ export interface TaskScopeStore extends TaskScopeAssigner {
34
24
  registerProject(input: RegisterTaskProjectInput): TaskProject;
35
25
  }
36
26
 
37
- /**
38
- * Task's own scope/view bookkeeping, composing a ProjectRegistryStore for the project-catalog
39
- * methods (projects/matchingProjects/registerProject) rather than implementing that bookkeeping
40
- * itself -- see project-registry-store.ts. Pass a shared registry instance to keep Task and a
41
- * non-Task ArtifactScopeStore resolving against the exact same project identities; omitted, this
42
- * store gets its own private one (matching this class's own behavior before the extraction).
43
- */
27
+ /** Pass a shared registry to keep Task and ArtifactScopeStore resolving the same project identities; omitted, this gets its own private one. */
44
28
  export class InMemoryTaskScopeStore implements TaskScopeStore {
45
29
  private readonly scopes = new Map<string, TaskProjectScope>();
46
30
  private readonly views = new Map<string, TaskViewPreference>();
@@ -6,7 +6,7 @@ export { normalizeProjectRoot } from "../../project-registry/scope-source.ts";
6
6
 
7
7
  export type TaskViewMode = "project" | "graph" | "all";
8
8
 
9
- /** Task's own name for the shared, kind-neutral scope-assignment provenance -- see scope-source.ts. Kept as a type alias so every existing Task-scope call site keeps working unchanged. */
9
+ /** Alias of ScopeAssignmentSource for Task's own call sites -- see project-registry/scope-source.ts. */
10
10
  export type TaskScopeSource = ScopeAssignmentSource;
11
11
 
12
12
  export interface TaskProjectScope {
@@ -10,17 +10,10 @@ import { assertDependencyEdgeAllowed, TaskExecutionBoundExceededError } from "./
10
10
  import type { TaskGraph } from "./task-service.ts";
11
11
 
12
12
  /**
13
- * Task dependency/containment edge mutations (depend/undepend/contain/uncontain), split out of
14
- * the Tasks god class as part of a SOLID-audit-driven decomposition (see task b51419a0 and the
15
- * "TaskEdges" child of "Epic: Modularize papyrus/pi-papyrus god-files into building-block
16
- * modules"), mirroring the TaskLeaseCoordinator/TaskMutationCoordinator/TaskFocusCoordinator/
17
- * TaskProjectScope precedent in this same directory.
18
- *
19
- * Unlike those simpler extractions, this one only owns the mutation side of edges -- it reads the
20
- * graph/relationships it needs through injected callbacks (dependencyCheckGraph/dependencyIds/
21
- * relationships) rather than duplicating that graph-construction machinery, since those reads are
22
- * shared with concerns that stay on Tasks (list/graph/buildGraph, progress propagation, blockage
23
- * checks in transition/complete).
13
+ * Task dependency/containment edge mutations (depend/undepend/contain/uncontain). Reads the
14
+ * graph/relationships it needs through injected callbacks rather than owning graph construction,
15
+ * since those reads are shared with concerns that stay on Tasks (list/graph, progress propagation,
16
+ * blockage checks).
24
17
  */
25
18
  export class TaskEdges {
26
19
  constructor(
@@ -21,14 +21,8 @@ export interface TaskFocus {
21
21
  export type TaskFocusMutationResult = TaskFocus & TaskMutationMetadata;
22
22
 
23
23
  /**
24
- * Task Focus (the single active/paused task per session scope), split out of the Tasks god class
25
- * as part of a SOLID-audit-driven decomposition (see task b51419a0 and the "TaskFocusCoordinator"
26
- * child of "Epic: Modularize papyrus/pi-papyrus god-files into building-block modules"), mirroring
27
- * the existing TaskLeaseCoordinator/TaskMutationCoordinator precedent in this same directory.
28
- *
29
- * Focus is orthogonal to lifecycle and lease: focusing a task does not start it, and does not
30
- * claim its lease -- so this concern has nothing to do with status transitions or worker
31
- * exclusivity, the other concerns that were previously interleaved with it in one class.
24
+ * Task Focus (the single active/paused task per session scope). Orthogonal to lifecycle and
25
+ * lease: focusing a task does not start it and does not claim its lease.
32
26
  */
33
27
  export class TaskFocusCoordinator {
34
28
  constructor(
@@ -3,12 +3,9 @@ import type { TaskLease, TaskLeaseView } from "./lease/task-lease.ts";
3
3
  import type { TaskLeaseStore } from "./lease/task-lease-store.ts";
4
4
 
5
5
  /**
6
- * Task lease management (claim/heartbeat/release/get/reap), split out of the Tasks god class as
7
- * part of a SOLID-audit-driven decomposition (see task b51419a0). A lease is orthogonal to
8
- * lifecycle and Focus -- claiming a task does not start it, and does not require it to be
9
- * Focused -- so its own concern (a single active worker per task, TTL-based) has nothing to do
10
- * with status transitions, idempotency receipts, or checklist review, the other concerns that
11
- * were previously interleaved with it in one class.
6
+ * Task lease management (claim/heartbeat/release/get/reap) -- a single active worker per task,
7
+ * TTL-based. Orthogonal to lifecycle and Focus: claiming a task does not start it or require it
8
+ * to be Focused.
12
9
  */
13
10
  export class TaskLeaseCoordinator {
14
11
  constructor(
@@ -41,26 +41,16 @@ function canonicalJson(value: unknown): string {
41
41
  }
42
42
 
43
43
  /**
44
- * Idempotency-key-backed mutation receipt plumbing, split out of the Tasks god class as part of
45
- * a SOLID-audit-driven decomposition (see task b51419a0). Owns reserving a "pending" receipt
46
- * before a real mutation runs, replaying an already-completed one, rejecting a genuinely
47
- * different payload reused under the same key, and rejecting a NEW attempt against a
48
- * task+operation that already has one in flight.
44
+ * Idempotency-key-backed mutation receipt plumbing: reserves a "pending" receipt before a real
45
+ * mutation runs, replays an already-completed one, rejects a different payload reused under the
46
+ * same key, and rejects a new attempt against a task+operation that already has one in flight.
49
47
  *
50
- * `validate`, when supplied to prepare(), runs before anything else -- including before the
51
- * existing/replay lookup -- specifically so a caller-supplied validation failure (e.g. an
52
- * over-length `reason`) can never leave a receipt reserved with no way to ever mark it complete.
53
- * This is the direct fix for a real incident (task a54f0649, discovered live completing task
54
- * d0eb81b7): validation previously ran deep inside the CALLER's own atomic block (appendEvent's
55
- * own validateTaskEvent), strictly AFTER prepare()'s reserving call had already durably written
56
- * the receipt as pending. Once that validation threw, nothing downstream ever reached the code
57
- * path that marks a receipt complete, permanently stranding it -- and since the pending-mutation
58
- * lock is keyed on (taskId, operation) rather than the idempotency key, that stuck receipt then
59
- * blocked every subsequent attempt on the same task+operation, under ANY key, until the record's
60
- * 7-day retention window expired. No self-service recovery existed; the live incident required a
61
- * direct database row deletion. Every caller that can determine its own event-context validity up
62
- * front (reason/sessionId length, at minimum) should now pass a `validate` callback here instead
63
- * of only validating once real mutation work is already underway.
48
+ * `validate`, when passed to prepare(), runs before the existing/replay lookup, so a
49
+ * caller-supplied validation failure can never leave a receipt reserved with no way to complete
50
+ * it -- a stuck pending receipt blocks every later attempt on that task+operation, under any key,
51
+ * until its retention window expires, with no self-service recovery. Any caller that can
52
+ * determine its own event-context validity up front should pass `validate` here rather than
53
+ * validating only once real mutation work is underway.
64
54
  */
65
55
  export class TaskMutationCoordinator {
66
56
  constructor(
@@ -18,13 +18,8 @@ export class TaskProjectAmbiguousError extends Error {}
18
18
 
19
19
  /**
20
20
  * Task project-scope management (scopeSelection/setView/assignProject/projects/resolveProject/
21
- * registerProject), split out of the Tasks god class as part of a SOLID-audit-driven
22
- * decomposition (see task b51419a0 and the "TaskProjectScope" child of "Epic: Modularize
23
- * papyrus/pi-papyrus god-files into building-block modules"), mirroring the existing
24
- * TaskLeaseCoordinator/TaskMutationCoordinator/TaskFocusCoordinator precedent in this directory.
25
- *
26
- * list()/graph() (which stay on Tasks -- they're core query/graph-construction, not project-scope
27
- * itself) still call scopeSelection() on this collaborator via Tasks' own thin delegation.
21
+ * registerProject). list()/graph() stay on Tasks but still call scopeSelection() here via a thin
22
+ * delegation, since those are core query/graph-construction, not project-scope itself.
28
23
  */
29
24
  export class TaskProjectScope {
30
25
  constructor(