@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 +1 -1
- package/src/artifact/artifact-scope-store.ts +5 -9
- package/src/artifact/in-memory-artifact-scope-store.ts +1 -3
- package/src/gate/gate-execution.ts +4 -17
- package/src/handlers/artifact-helpers.ts +1 -6
- package/src/handlers/operation-schema.ts +3 -6
- package/src/handlers/paired-mutation.ts +1 -7
- package/src/handlers/shared.ts +6 -15
- package/src/handlers/task-classifiers.ts +1 -9
- package/src/project-registry/scope-source.ts +1 -16
- package/src/scope-group/scope-group.ts +4 -11
- package/src/task/event/task-event-store.ts +1 -10
- package/src/task/scope/task-scope-store.ts +2 -18
- package/src/task/scope/task-scope.ts +1 -1
- package/src/task/task-edges.ts +4 -11
- package/src/task/task-focus-coordinator.ts +2 -8
- package/src/task/task-lease-coordinator.ts +3 -6
- package/src/task/task-mutation-coordinator.ts +9 -19
- package/src/task/task-project-scope.ts +2 -7
package/package.json
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* an
|
|
9
|
-
*
|
|
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
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
3
|
-
* a
|
|
4
|
-
*
|
|
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";
|
package/src/handlers/shared.ts
CHANGED
|
@@ -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 (
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
/**
|
|
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 {
|
package/src/task/task-edges.ts
CHANGED
|
@@ -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)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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)
|
|
25
|
-
*
|
|
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)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
|
45
|
-
*
|
|
46
|
-
*
|
|
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
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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)
|
|
22
|
-
*
|
|
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(
|