@danypops/papyrus 0.55.0 → 0.57.0
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 +14 -1
- package/src/artifact/artifact-relationship-view.ts +1 -1
- package/src/artifact/artifact-scope-store.ts +32 -26
- package/src/artifact/in-memory-artifact-scope-store.ts +82 -43
- package/src/artifact/index.ts +3 -0
- package/src/artifact/sqlite-artifact-scope-store.ts +105 -56
- package/src/cli/docs-command.ts +71 -0
- package/src/cli/gates-command.ts +1 -1
- package/src/cli/note-command.ts +1 -1
- package/src/cli/playbooks-command.ts +71 -0
- package/src/cli/rules-command.ts +71 -0
- package/src/cli/scope-groups-command.ts +197 -0
- package/src/cli/task-command.ts +1 -1
- package/src/cli.ts +7 -0
- package/src/constants.ts +10 -2
- package/src/db.ts +117 -7
- package/src/{stores → discussion}/discussion-round-store.ts +1 -1
- package/src/discussion/discussion-service.ts +2 -2
- package/src/discussion/index.ts +1 -0
- package/src/{stores → discussion}/sqlite-discussion-round-store.ts +2 -2
- package/src/display-graph/graph-renderer.ts +5 -0
- package/src/docs/docs-service.ts +55 -20
- package/src/domain-service-shared.ts +90 -2
- package/src/{domain → gate}/gate-execution.ts +1 -1
- package/src/{stores → gate}/gate-runner.ts +1 -1
- package/src/{stores → gate}/sqlite-gate-runner.ts +3 -3
- package/src/graph-projection/graph-projection-service.ts +2 -2
- package/src/{stores → graph-projection}/graph-projection-store.ts +1 -1
- package/src/{stores → graph-projection}/sqlite-graph-projection-store.ts +2 -2
- package/src/handlers/discuss.ts +1 -1
- package/src/handlers/docs.ts +57 -2
- package/src/handlers/playbooks.ts +49 -8
- package/src/handlers/projects.ts +1 -1
- package/src/handlers/registry.ts +10 -5
- package/src/handlers/rules.ts +66 -2
- package/src/handlers/scope-groups.ts +100 -0
- package/src/handlers/task-classifiers.ts +1 -1
- package/src/handlers/tasks.ts +4 -4
- package/src/index.ts +9 -9
- package/src/log/log-service.ts +2 -2
- package/src/{stores → log}/log-store.ts +1 -1
- package/src/{stores → log}/sqlite-log-store.ts +2 -2
- package/src/modules/docs.ts +21 -1
- package/src/modules/graph-projection.ts +2 -2
- package/src/modules/logs.ts +1 -1
- package/src/modules/notes.ts +1 -1
- package/src/modules/playbooks.ts +24 -3
- package/src/modules/projects.ts +3 -3
- package/src/modules/rules.ts +21 -1
- package/src/modules/scope-groups.ts +92 -0
- package/src/modules/tasks.ts +3 -3
- package/src/note/index.ts +1 -0
- package/src/{stores → note}/note-event-store.ts +1 -1
- package/src/note/note-service.ts +2 -2
- package/src/{stores → note}/sqlite-note-event-store.ts +2 -2
- package/src/ops.ts +1 -1
- package/src/playbook/index.ts +2 -0
- package/src/playbook/playbook-definition.ts +2 -2
- package/src/playbook/playbook-execution.ts +1 -1
- package/src/playbook/playbook-service.ts +61 -26
- package/src/playbook/workflow-execution.ts +8 -8
- package/src/{stores → project-registry}/in-memory-project-registry-store.ts +2 -2
- package/src/{ports → project-registry}/project-registry-store.ts +1 -1
- package/src/{stores → project-registry}/sqlite-project-registry-store.ts +3 -3
- package/src/rules/rules-service.ts +56 -24
- package/src/scope-group/delete-scope-group.ts +17 -0
- package/src/scope-group/in-memory-scope-group-store.ts +173 -0
- package/src/scope-group/scope-group-store.ts +28 -0
- package/src/scope-group/scope-group.ts +79 -0
- package/src/scope-group/sqlite-scope-group-store.ts +223 -0
- package/src/service.ts +46 -21
- package/src/session-identity/session-identity-service.ts +2 -2
- package/src/{stores → session-identity}/sqlite-session-identity-store.ts +1 -1
- package/src/task/index.ts +13 -0
- package/src/task/task-context.ts +1 -1
- package/src/task/task-edges.ts +2 -2
- package/src/task/task-focus-coordinator.ts +3 -3
- package/src/task/task-graph-view.ts +1 -1
- package/src/task/task-lease-coordinator.ts +2 -2
- package/src/task/task-mutation-coordinator.ts +1 -1
- package/src/task/task-project-scope.ts +5 -5
- package/src/task/task-relationship-view.ts +1 -1
- package/src/task/task-service.ts +24 -24
- package/src/{stores → task-create-request}/sqlite-task-create-request-store.ts +1 -1
- package/src/{stores → task-event}/sqlite-task-event-store.ts +2 -2
- package/src/{stores → task-event}/task-event-store.ts +1 -1
- package/src/{stores → task-focus}/sqlite-task-focus-store.ts +1 -1
- package/src/{stores → task-lease}/sqlite-task-lease-store.ts +2 -2
- package/src/{stores → task-lease}/task-lease-store.ts +1 -1
- package/src/{stores → task-mutation-request}/sqlite-task-mutation-request-store.ts +5 -1
- package/src/{stores → task-scope}/sqlite-task-scope-store.ts +3 -3
- package/src/{stores → task-scope}/task-scope-store.ts +3 -3
- package/src/{domain → task-scope}/task-scope.ts +1 -1
- package/src/stores/graph-renderer.ts +0 -5
- /package/src/{domain → discussion}/discussion.ts +0 -0
- /package/src/{domain → display-graph}/display-graph.ts +0 -0
- /package/src/{domain → gate}/gate.ts +0 -0
- /package/src/{domain → graph-projection}/graph-projection.ts +0 -0
- /package/src/{domain → log}/log-entry.ts +0 -0
- /package/src/{domain → note}/note-event.ts +0 -0
- /package/src/{domain → playbook}/blueprint-definition.ts +0 -0
- /package/src/{domain → project-registry}/project-registry.ts +0 -0
- /package/src/{stores → session-identity}/session-identity-store.ts +0 -0
- /package/src/{domain → session-identity}/session-identity.ts +0 -0
- /package/src/{domain → task}/checklist.ts +0 -0
- /package/src/{stores → task-create-request}/task-create-request-store.ts +0 -0
- /package/src/{domain → task-event}/task-event.ts +0 -0
- /package/src/{stores → task-focus}/task-focus-store.ts +0 -0
- /package/src/{domain → task-lease}/task-lease.ts +0 -0
- /package/src/{stores → task-mutation-request}/task-mutation-request-store.ts +0 -0
package/package.json
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
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",
|
|
7
7
|
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./artifact": "./src/artifact/index.ts",
|
|
11
|
+
"./client": "./src/client.ts",
|
|
12
|
+
"./discussion": "./src/discussion/index.ts",
|
|
13
|
+
"./domain": "./src/domain/index.ts",
|
|
14
|
+
"./note": "./src/note/index.ts",
|
|
15
|
+
"./playbook": "./src/playbook/index.ts",
|
|
16
|
+
"./stores": "./src/stores/index.ts",
|
|
17
|
+
"./task": "./src/task/index.ts",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"sideEffects": false,
|
|
8
21
|
"bin": {
|
|
9
22
|
"papyrus": "src/cli.ts"
|
|
10
23
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode } from "../
|
|
1
|
+
import type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode } from "../display-graph/display-graph.ts";
|
|
2
2
|
import { fallbackLabel } from "../task/task-relationship-view.ts";
|
|
3
3
|
import type { Artifact } from "./artifact.ts";
|
|
4
4
|
|
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ScopeMemberRef } from "../scope-group/scope-group.ts";
|
|
2
|
+
import type { TaskScopeSource } from "../task-scope/task-scope.ts";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* Project scoping for Docs/Rules/Playbooks: an artifact is
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* project
|
|
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.
|
|
12
14
|
*/
|
|
13
|
-
export type ArtifactScopeMode = "
|
|
15
|
+
export type ArtifactScopeMode = "none" | "all" | "explicit";
|
|
14
16
|
|
|
15
17
|
export interface ArtifactScope {
|
|
16
18
|
artifactId: string;
|
|
17
19
|
mode: ArtifactScopeMode;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
+
/** Direct membership only (not expanded through nested groups) -- always empty when mode is "none"/"all", always non-empty when mode is "explicit". */
|
|
21
|
+
members: ScopeMemberRef[];
|
|
20
22
|
source: TaskScopeSource;
|
|
21
23
|
}
|
|
22
24
|
|
|
@@ -27,24 +29,28 @@ export interface LegacyArtifactScope {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface ArtifactScopeStore {
|
|
30
|
-
/** The real, non-lossy multi-membership view. Defaults to
|
|
32
|
+
/** The real, non-lossy multi-membership view. Defaults to "all"/unscoped for an artifact with no scope row yet. */
|
|
31
33
|
scope(artifactId: string): ArtifactScope;
|
|
32
|
-
/** Single-root compatibility view over scope():
|
|
34
|
+
/** Single-root compatibility view over scope(): an "all"/"none" or unscoped artifact omits projectRoot; an "explicit" mode artifact with exactly one project-type membership (and no group members) resolves it back to that project's current root; anything else (zero, more than one, or any group membership) omits projectRoot, since this shape cannot represent it. */
|
|
33
35
|
get(artifactId: string): LegacyArtifactScope | undefined;
|
|
34
|
-
/** Single-root compatibility shim: registers/resolves projectRoot and replaces the artifact's scope with exactly that one membership, or
|
|
36
|
+
/** Single-root compatibility shim: registers/resolves projectRoot and replaces the artifact's scope with exactly that one project membership, or setAll() when projectRoot is undefined. Every existing caller (rules/docs/playbooks assign_project) keeps working unchanged. */
|
|
35
37
|
assign(artifactId: string, projectRoot: string | undefined, source: TaskScopeSource): LegacyArtifactScope;
|
|
36
|
-
/** Sets an artifact to explicitly
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
/**
|
|
38
|
+
/** Sets an artifact to explicitly "all" (applies everywhere), clearing any membership. */
|
|
39
|
+
setAll(artifactId: string, source: TaskScopeSource): ArtifactScope;
|
|
40
|
+
/** Sets an artifact to explicitly "none" (fully hidden -- never applicable, never context-injected, regardless of project), clearing any membership. */
|
|
41
|
+
setNone(artifactId: string, source: TaskScopeSource): ArtifactScope;
|
|
42
|
+
/** Replaces an artifact's entire explicit membership set with exactly these (project and/or group) members -- must be non-empty; use setAll/setNone to clear scoping entirely. */
|
|
43
|
+
replaceMembers(artifactId: string, members: readonly ScopeMemberRef[], source: TaskScopeSource): ArtifactScope;
|
|
44
|
+
/** Adds one member to an artifact's explicit membership (idempotent), switching mode to "explicit" if it was "all"/"none". Enforces the bounded maximum membership count. */
|
|
45
|
+
addMember(artifactId: string, member: ScopeMemberRef, source: TaskScopeSource): ArtifactScope;
|
|
46
|
+
/** Removes one member from an artifact's explicit membership (idempotent). Rejects removing the last membership while mode is "explicit": a caller must explicitly call setAll/setNone instead of accidentally broadening/narrowing scope by emptying the set. */
|
|
47
|
+
removeMember(artifactId: string, member: ScopeMemberRef): ArtifactScope;
|
|
48
|
+
/** Bounded id listing for one project root's EXACT direct membership (or the "all" bucket when projectRoot is undefined) -- an unregistered root always yields an empty list. Deliberately does not expand nested groups (audit semantics, matching this method's own pre-existing "exact membership" contract) -- use appliesToProjectRoot for the applicable/injection query instead. */
|
|
45
49
|
ids(projectRoot: string | undefined, limit: number): string[];
|
|
46
|
-
/** True when artifactId's scope
|
|
50
|
+
/** True when artifactId's scope is "all", or "explicit" with projectId reachable from its member set (a direct project member, or transitively through a nested group member) -- false for "none". False for an unscoped artifact with no row is never returned (defaults to "all" behavior); injection call sites decide their own applicability policy beyond this raw membership fact. */
|
|
47
51
|
appliesToProject(artifactId: string, projectId: string): boolean;
|
|
48
|
-
/** Root-based convenience over appliesToProject, for a caller (e.g. rules.injectable) that only has a project root, not a resolved id
|
|
52
|
+
/** Root-based convenience over appliesToProject, for a caller (e.g. rules.injectable) that only has a project root, not a resolved id. An unregistered root means only an "all"-mode artifact applies; projectRoot === undefined means "no project context at all", so only "all"-mode artifacts apply either way. */
|
|
49
53
|
appliesToProjectRoot(artifactId: string, projectRoot: string | undefined): boolean;
|
|
54
|
+
/** True when any artifact's own explicit scope references this scope group as a direct member -- checked by deleteScopeGroup before allowing a group to be deleted. */
|
|
55
|
+
referencesGroup(groupId: string): boolean;
|
|
50
56
|
}
|
|
@@ -1,30 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
import type { ProjectRegistryStore } from "../
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import { ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT } from "../constants.ts";
|
|
2
|
+
import { InMemoryProjectRegistryStore } from "../project-registry/in-memory-project-registry-store.ts";
|
|
3
|
+
import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
|
|
4
|
+
import { InMemoryScopeGroupStore } from "../scope-group/in-memory-scope-group-store.ts";
|
|
5
|
+
import { type ScopeMemberRef, sameScopeMember } from "../scope-group/scope-group.ts";
|
|
6
|
+
import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
|
|
7
|
+
import type { TaskScopeSource } from "../task-scope/task-scope.ts";
|
|
8
|
+
import type { ArtifactScope, ArtifactScopeMode, ArtifactScopeStore, LegacyArtifactScope } from "./artifact-scope-store.ts";
|
|
6
9
|
|
|
7
10
|
interface Row {
|
|
8
|
-
mode:
|
|
9
|
-
|
|
11
|
+
mode: ArtifactScopeMode;
|
|
12
|
+
members: ScopeMemberRef[];
|
|
10
13
|
source: TaskScopeSource;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export class InMemoryArtifactScopeStore implements ArtifactScopeStore {
|
|
14
17
|
private readonly rows = new Map<string, Row>();
|
|
15
18
|
private readonly registry: InMemoryProjectRegistryStore;
|
|
19
|
+
private readonly scopeGroups: ScopeGroupStore;
|
|
16
20
|
|
|
17
|
-
// Membership is stored by project id, never by root, so a registry root move (see
|
|
18
|
-
// ProjectRegistryStore.registerProject) needs no rewrite here at all --
|
|
19
|
-
// InMemoryTaskScopeStore, this store never subscribes to root-move notifications.
|
|
20
|
-
constructor(registry?: ProjectRegistryStore) {
|
|
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.
|
|
24
|
+
constructor(registry?: ProjectRegistryStore, scopeGroups?: ScopeGroupStore) {
|
|
21
25
|
this.registry = registry instanceof InMemoryProjectRegistryStore ? registry : new InMemoryProjectRegistryStore();
|
|
26
|
+
this.scopeGroups = scopeGroups ?? new InMemoryScopeGroupStore();
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
private toScope(artifactId: string, row: Row | undefined): ArtifactScope {
|
|
25
30
|
return row
|
|
26
|
-
? { artifactId, mode: row.mode,
|
|
27
|
-
: { artifactId, mode: "
|
|
31
|
+
? { artifactId, mode: row.mode, members: [...row.members], source: row.source }
|
|
32
|
+
: { artifactId, mode: "all", members: [], source: "unscoped" };
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
scope(artifactId: string): ArtifactScope {
|
|
@@ -34,58 +39,67 @@ export class InMemoryArtifactScopeStore implements ArtifactScopeStore {
|
|
|
34
39
|
get(artifactId: string): LegacyArtifactScope | undefined {
|
|
35
40
|
const row = this.rows.get(artifactId);
|
|
36
41
|
if (!row) return undefined;
|
|
37
|
-
const
|
|
38
|
-
|
|
42
|
+
const onlyProjectMember =
|
|
43
|
+
row.mode === "explicit" && row.members.length === 1 && row.members[0]!.type === "project" ? row.members[0] : undefined;
|
|
44
|
+
const projectRoot = onlyProjectMember === undefined ? undefined : this.registry.byId(onlyProjectMember.id)?.projectRoot;
|
|
39
45
|
return { artifactId, ...(projectRoot === undefined ? {} : { projectRoot }), source: row.source };
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
assign(artifactId: string, projectRoot: string | undefined, source: TaskScopeSource): LegacyArtifactScope {
|
|
43
49
|
if (projectRoot === undefined) {
|
|
44
|
-
this.
|
|
50
|
+
this.setAll(artifactId, source);
|
|
45
51
|
return { artifactId, source };
|
|
46
52
|
}
|
|
47
53
|
const project = this.registry.registerProject({ projectRoot });
|
|
48
|
-
this.
|
|
54
|
+
this.replaceMembers(artifactId, [{ type: "project", id: project.id }], source);
|
|
49
55
|
return { artifactId, projectRoot: project.projectRoot, source };
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
|
|
53
|
-
const row: Row = { mode: "
|
|
58
|
+
setAll(artifactId: string, source: TaskScopeSource): ArtifactScope {
|
|
59
|
+
const row: Row = { mode: "all", members: [], source };
|
|
54
60
|
this.rows.set(artifactId, row);
|
|
55
61
|
return this.toScope(artifactId, row);
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
setNone(artifactId: string, source: TaskScopeSource): ArtifactScope {
|
|
65
|
+
const row: Row = { mode: "none", members: [], source };
|
|
66
|
+
this.rows.set(artifactId, row);
|
|
67
|
+
return this.toScope(artifactId, row);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
replaceMembers(artifactId: string, members: readonly ScopeMemberRef[], source: TaskScopeSource): ArtifactScope {
|
|
71
|
+
if (members.length === 0) throw new Error("replaceMembers requires at least one member; use setAll/setNone to clear scoping");
|
|
72
|
+
if (members.length > ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
|
|
73
|
+
throw new Error(`an artifact cannot have more than ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} scope members`);
|
|
62
74
|
}
|
|
63
|
-
const
|
|
75
|
+
const unique = dedupeMembers(members);
|
|
76
|
+
const row: Row = { mode: "explicit", members: unique, source };
|
|
64
77
|
this.rows.set(artifactId, row);
|
|
65
78
|
return this.toScope(artifactId, row);
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
|
|
81
|
+
addMember(artifactId: string, member: ScopeMemberRef, source: TaskScopeSource): ArtifactScope {
|
|
69
82
|
const existing = this.rows.get(artifactId);
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
throw new Error(`an artifact cannot
|
|
83
|
+
const members = dedupeMembers(existing?.mode === "explicit" ? existing.members : []);
|
|
84
|
+
if (!members.some((candidate) => sameScopeMember(candidate, member)) && members.length >= ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
|
|
85
|
+
throw new Error(`an artifact cannot have more than ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} scope members`);
|
|
73
86
|
}
|
|
74
|
-
|
|
75
|
-
const row: Row = { mode: "
|
|
87
|
+
if (!members.some((candidate) => sameScopeMember(candidate, member))) members.push(member);
|
|
88
|
+
const row: Row = { mode: "explicit", members, source };
|
|
76
89
|
this.rows.set(artifactId, row);
|
|
77
90
|
return this.toScope(artifactId, row);
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
removeMember(artifactId: string, member: ScopeMemberRef): ArtifactScope {
|
|
81
94
|
const existing = this.rows.get(artifactId);
|
|
82
|
-
if (existing?.mode !== "
|
|
83
|
-
|
|
84
|
-
throw new Error("cannot remove the last project membership; call setGlobal to make this artifact apply everywhere instead");
|
|
95
|
+
if (existing?.mode !== "explicit" || !existing.members.some((candidate) => sameScopeMember(candidate, member))) {
|
|
96
|
+
return this.toScope(artifactId, existing);
|
|
85
97
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
98
|
+
if (existing.members.length === 1) {
|
|
99
|
+
throw new Error("cannot remove the last scope member; call setAll or setNone instead of accidentally widening/hiding scope");
|
|
100
|
+
}
|
|
101
|
+
const members = existing.members.filter((candidate) => !sameScopeMember(candidate, member));
|
|
102
|
+
const row: Row = { mode: "explicit", members, source: existing.source };
|
|
89
103
|
this.rows.set(artifactId, row);
|
|
90
104
|
return this.toScope(artifactId, row);
|
|
91
105
|
}
|
|
@@ -93,7 +107,7 @@ export class InMemoryArtifactScopeStore implements ArtifactScopeStore {
|
|
|
93
107
|
ids(projectRoot: string | undefined, limit: number): string[] {
|
|
94
108
|
if (projectRoot === undefined) {
|
|
95
109
|
return [...this.rows.entries()]
|
|
96
|
-
.filter(([, row]) => row.mode === "
|
|
110
|
+
.filter(([, row]) => row.mode === "all")
|
|
97
111
|
.map(([artifactId]) => artifactId)
|
|
98
112
|
.sort()
|
|
99
113
|
.slice(0, limit);
|
|
@@ -101,23 +115,48 @@ export class InMemoryArtifactScopeStore implements ArtifactScopeStore {
|
|
|
101
115
|
const project = this.registry.byRoot(projectRoot);
|
|
102
116
|
if (!project) return [];
|
|
103
117
|
return [...this.rows.entries()]
|
|
104
|
-
.filter(([, row]) => row.mode === "
|
|
118
|
+
.filter(([, row]) => row.mode === "explicit" && row.members.some((member) => member.type === "project" && member.id === project.id))
|
|
105
119
|
.map(([artifactId]) => artifactId)
|
|
106
120
|
.sort()
|
|
107
121
|
.slice(0, limit);
|
|
108
122
|
}
|
|
109
123
|
|
|
124
|
+
private expandedProjectIds(row: Row): Set<string> {
|
|
125
|
+
const result = new Set<string>();
|
|
126
|
+
for (const member of row.members) {
|
|
127
|
+
if (member.type === "project") result.add(member.id);
|
|
128
|
+
else for (const projectId of this.scopeGroups.expandToProjectIds(member.id)) result.add(projectId);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
110
133
|
appliesToProject(artifactId: string, projectId: string): boolean {
|
|
111
134
|
const row = this.rows.get(artifactId);
|
|
112
|
-
if (!row || row.mode === "
|
|
113
|
-
|
|
135
|
+
if (!row || row.mode === "all") return true;
|
|
136
|
+
if (row.mode === "none") return false;
|
|
137
|
+
return this.expandedProjectIds(row).has(projectId);
|
|
114
138
|
}
|
|
115
139
|
|
|
116
140
|
appliesToProjectRoot(artifactId: string, projectRoot: string | undefined): boolean {
|
|
117
141
|
const row = this.rows.get(artifactId);
|
|
118
|
-
if (!row || row.mode === "
|
|
142
|
+
if (!row || row.mode === "all") return true;
|
|
143
|
+
if (row.mode === "none") return false;
|
|
119
144
|
if (projectRoot === undefined) return false;
|
|
120
145
|
const project = this.registry.byRoot(projectRoot);
|
|
121
|
-
return project !== undefined && row.
|
|
146
|
+
return project !== undefined && this.expandedProjectIds(row).has(project.id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
referencesGroup(groupId: string): boolean {
|
|
150
|
+
return [...this.rows.values()].some(
|
|
151
|
+
(row) => row.mode === "explicit" && row.members.some((member) => member.type === "group" && member.id === groupId),
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function dedupeMembers(members: readonly ScopeMemberRef[]): ScopeMemberRef[] {
|
|
157
|
+
const result: ScopeMemberRef[] = [];
|
|
158
|
+
for (const member of members) {
|
|
159
|
+
if (!result.some((candidate) => sameScopeMember(candidate, member))) result.push(member);
|
|
122
160
|
}
|
|
161
|
+
return result;
|
|
123
162
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT } from "../constants.ts";
|
|
2
2
|
import type { Db } from "../db.ts";
|
|
3
3
|
import { inTransaction } from "../db.ts";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import { SQLiteProjectRegistryStore } from "../project-registry/sqlite-project-registry-store.ts";
|
|
5
|
+
import type { ScopeMemberRef } from "../scope-group/scope-group.ts";
|
|
6
|
+
import { SQLiteScopeGroupStore } from "../scope-group/sqlite-scope-group-store.ts";
|
|
7
|
+
import type { TaskScopeSource } from "../task-scope/task-scope.ts";
|
|
6
8
|
import type { ArtifactScope, ArtifactScopeMode, ArtifactScopeStore, LegacyArtifactScope } from "./artifact-scope-store.ts";
|
|
7
9
|
|
|
8
10
|
interface ScopeRow {
|
|
@@ -11,22 +13,33 @@ interface ScopeRow {
|
|
|
11
13
|
source: TaskScopeSource;
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
interface MemberRow {
|
|
17
|
+
member_type: "project" | "group";
|
|
18
|
+
member_id: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function memberFromRow(row: MemberRow): ScopeMemberRef {
|
|
22
|
+
return row.member_type === "project" ? { type: "project", id: row.member_id } : { type: "group", id: row.member_id };
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
|
|
15
26
|
private readonly registry: SQLiteProjectRegistryStore;
|
|
27
|
+
private readonly scopeGroups: SQLiteScopeGroupStore;
|
|
16
28
|
|
|
17
|
-
// Membership is by project id, not root -- a registry root move
|
|
18
|
-
//
|
|
19
|
-
//
|
|
29
|
+
// Membership is by project/group id, not root/name -- a registry root move or scope group
|
|
30
|
+
// rename needs no rewrite of artifact_scope_members at all, unlike SQLiteTaskScopeStore's own
|
|
31
|
+
// task_scopes/task_views rewrite.
|
|
20
32
|
constructor(private readonly db: Db) {
|
|
21
33
|
this.registry = new SQLiteProjectRegistryStore(db);
|
|
34
|
+
this.scopeGroups = new SQLiteScopeGroupStore(db);
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
private
|
|
37
|
+
private members(artifactId: string): ScopeMemberRef[] {
|
|
25
38
|
return (
|
|
26
|
-
this.db
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
).map(
|
|
39
|
+
this.db
|
|
40
|
+
.prepare("SELECT member_type, member_id FROM artifact_scope_members WHERE artifact_id = ? ORDER BY member_type, member_id")
|
|
41
|
+
.all(artifactId) as MemberRow[]
|
|
42
|
+
).map(memberFromRow);
|
|
30
43
|
}
|
|
31
44
|
|
|
32
45
|
private readRow(artifactId: string): ScopeRow | undefined {
|
|
@@ -38,28 +51,28 @@ export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
|
|
|
38
51
|
|
|
39
52
|
scope(artifactId: string): ArtifactScope {
|
|
40
53
|
const row = this.readRow(artifactId);
|
|
41
|
-
if (!row) return { artifactId, mode: "
|
|
42
|
-
return { artifactId, mode: row.mode,
|
|
54
|
+
if (!row) return { artifactId, mode: "all", members: [], source: "unscoped" };
|
|
55
|
+
return { artifactId, mode: row.mode, members: row.mode === "explicit" ? this.members(artifactId) : [], source: row.source };
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
get(artifactId: string): LegacyArtifactScope | undefined {
|
|
46
59
|
const row = this.readRow(artifactId);
|
|
47
60
|
if (!row) return undefined;
|
|
48
|
-
if (row.mode !== "
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
51
|
-
const project = this.registry.matchingProjects(
|
|
61
|
+
if (row.mode !== "explicit") return { artifactId, source: row.source };
|
|
62
|
+
const members = this.members(artifactId);
|
|
63
|
+
if (members.length !== 1 || members[0]!.type !== "project") return { artifactId, source: row.source };
|
|
64
|
+
const project = this.registry.matchingProjects(members[0]!.id).find((candidate) => candidate.id === members[0]!.id);
|
|
52
65
|
return { artifactId, ...(project ? { projectRoot: project.projectRoot } : {}), source: row.source };
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
assign(artifactId: string, projectRoot: string | undefined, source: TaskScopeSource): LegacyArtifactScope {
|
|
56
69
|
if (projectRoot === undefined) {
|
|
57
|
-
this.
|
|
70
|
+
this.setAll(artifactId, source);
|
|
58
71
|
return { artifactId, source };
|
|
59
72
|
}
|
|
60
73
|
return inTransaction(this.db, () => {
|
|
61
74
|
const project = this.registry.registerProject({ projectRoot });
|
|
62
|
-
this.
|
|
75
|
+
this.replaceMembers(artifactId, [{ type: "project", id: project.id }], source);
|
|
63
76
|
return { artifactId, projectRoot: project.projectRoot, source };
|
|
64
77
|
});
|
|
65
78
|
}
|
|
@@ -78,58 +91,77 @@ export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
|
|
|
78
91
|
.run(artifactId, mode, source, new Date().toISOString());
|
|
79
92
|
}
|
|
80
93
|
|
|
81
|
-
|
|
94
|
+
setAll(artifactId: string, source: TaskScopeSource): ArtifactScope {
|
|
95
|
+
return inTransaction(this.db, () => {
|
|
96
|
+
this.upsertScopeRow(artifactId, "all", source);
|
|
97
|
+
this.db.prepare("DELETE FROM artifact_scope_members WHERE artifact_id = ?").run(artifactId);
|
|
98
|
+
return { artifactId, mode: "all", members: [], source };
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setNone(artifactId: string, source: TaskScopeSource): ArtifactScope {
|
|
82
103
|
return inTransaction(this.db, () => {
|
|
83
|
-
this.upsertScopeRow(artifactId, "
|
|
84
|
-
this.db.prepare("DELETE FROM
|
|
85
|
-
return { artifactId, mode: "
|
|
104
|
+
this.upsertScopeRow(artifactId, "none", source);
|
|
105
|
+
this.db.prepare("DELETE FROM artifact_scope_members WHERE artifact_id = ?").run(artifactId);
|
|
106
|
+
return { artifactId, mode: "none", members: [], source };
|
|
86
107
|
});
|
|
87
108
|
}
|
|
88
109
|
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
throw new Error(`an artifact cannot
|
|
110
|
+
replaceMembers(artifactId: string, members: readonly ScopeMemberRef[], source: TaskScopeSource): ArtifactScope {
|
|
111
|
+
if (members.length === 0) throw new Error("replaceMembers requires at least one member; use setAll/setNone to clear scoping");
|
|
112
|
+
if (members.length > ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
|
|
113
|
+
throw new Error(`an artifact cannot have more than ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} scope members`);
|
|
93
114
|
}
|
|
94
115
|
return inTransaction(this.db, () => {
|
|
95
|
-
this.upsertScopeRow(artifactId, "
|
|
96
|
-
this.db.prepare("DELETE FROM
|
|
97
|
-
const insert = this.db.prepare("INSERT OR IGNORE INTO
|
|
98
|
-
const
|
|
99
|
-
for (const
|
|
100
|
-
|
|
116
|
+
this.upsertScopeRow(artifactId, "explicit", source);
|
|
117
|
+
this.db.prepare("DELETE FROM artifact_scope_members WHERE artifact_id = ?").run(artifactId);
|
|
118
|
+
const insert = this.db.prepare("INSERT OR IGNORE INTO artifact_scope_members (artifact_id, member_type, member_id) VALUES (?, ?, ?)");
|
|
119
|
+
const seen = new Set<string>();
|
|
120
|
+
for (const member of members) {
|
|
121
|
+
const key = `${member.type}:${member.id}`;
|
|
122
|
+
if (seen.has(key)) continue;
|
|
123
|
+
seen.add(key);
|
|
124
|
+
insert.run(artifactId, member.type, member.id);
|
|
125
|
+
}
|
|
126
|
+
return { artifactId, mode: "explicit", members: this.members(artifactId), source };
|
|
101
127
|
});
|
|
102
128
|
}
|
|
103
129
|
|
|
104
|
-
|
|
130
|
+
addMember(artifactId: string, member: ScopeMemberRef, source: TaskScopeSource): ArtifactScope {
|
|
105
131
|
return inTransaction(this.db, () => {
|
|
106
|
-
const current = this.
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
const current = this.members(artifactId);
|
|
133
|
+
const already = current.some((candidate) => candidate.type === member.type && candidate.id === member.id);
|
|
134
|
+
if (!already && current.length >= ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
|
|
135
|
+
throw new Error(`an artifact cannot have more than ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} scope members`);
|
|
109
136
|
}
|
|
110
|
-
this.upsertScopeRow(artifactId, "
|
|
111
|
-
this.db
|
|
112
|
-
|
|
137
|
+
this.upsertScopeRow(artifactId, "explicit", source);
|
|
138
|
+
this.db
|
|
139
|
+
.prepare("INSERT OR IGNORE INTO artifact_scope_members (artifact_id, member_type, member_id) VALUES (?, ?, ?)")
|
|
140
|
+
.run(artifactId, member.type, member.id);
|
|
141
|
+
return { artifactId, mode: "explicit", members: this.members(artifactId), source };
|
|
113
142
|
});
|
|
114
143
|
}
|
|
115
144
|
|
|
116
|
-
|
|
145
|
+
removeMember(artifactId: string, member: ScopeMemberRef): ArtifactScope {
|
|
117
146
|
return inTransaction(this.db, () => {
|
|
118
147
|
const row = this.readRow(artifactId);
|
|
119
|
-
const current = row?.mode === "
|
|
120
|
-
|
|
148
|
+
const current = row?.mode === "explicit" ? this.members(artifactId) : [];
|
|
149
|
+
const already = current.some((candidate) => candidate.type === member.type && candidate.id === member.id);
|
|
150
|
+
if (row?.mode !== "explicit" || !already) return this.scope(artifactId);
|
|
121
151
|
if (current.length === 1) {
|
|
122
|
-
throw new Error("cannot remove the last
|
|
152
|
+
throw new Error("cannot remove the last scope member; call setAll or setNone instead of accidentally widening/hiding scope");
|
|
123
153
|
}
|
|
124
|
-
this.db
|
|
125
|
-
|
|
154
|
+
this.db
|
|
155
|
+
.prepare("DELETE FROM artifact_scope_members WHERE artifact_id = ? AND member_type = ? AND member_id = ?")
|
|
156
|
+
.run(artifactId, member.type, member.id);
|
|
157
|
+
return { artifactId, mode: "explicit", members: this.members(artifactId), source: row.source };
|
|
126
158
|
});
|
|
127
159
|
}
|
|
128
160
|
|
|
129
161
|
ids(projectRoot: string | undefined, limit: number): string[] {
|
|
130
162
|
if (projectRoot === undefined) {
|
|
131
163
|
return (
|
|
132
|
-
this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE mode = '
|
|
164
|
+
this.db.prepare("SELECT artifact_id FROM artifact_scopes WHERE mode = 'all' ORDER BY artifact_id LIMIT ?").all(limit) as Array<{
|
|
133
165
|
artifact_id: string;
|
|
134
166
|
}>
|
|
135
167
|
).map((row) => row.artifact_id);
|
|
@@ -139,28 +171,45 @@ export class SQLiteArtifactScopeStore implements ArtifactScopeStore {
|
|
|
139
171
|
return (
|
|
140
172
|
this.db
|
|
141
173
|
.prepare(`
|
|
142
|
-
SELECT
|
|
143
|
-
FROM
|
|
144
|
-
JOIN artifact_scopes s ON s.artifact_id =
|
|
145
|
-
WHERE
|
|
146
|
-
ORDER BY
|
|
174
|
+
SELECT asm.artifact_id AS artifact_id
|
|
175
|
+
FROM artifact_scope_members asm
|
|
176
|
+
JOIN artifact_scopes s ON s.artifact_id = asm.artifact_id AND s.mode = 'explicit'
|
|
177
|
+
WHERE asm.member_type = 'project' AND asm.member_id = ?
|
|
178
|
+
ORDER BY asm.artifact_id
|
|
147
179
|
LIMIT ?
|
|
148
180
|
`)
|
|
149
181
|
.all(project.id, limit) as Array<{ artifact_id: string }>
|
|
150
182
|
).map((row) => row.artifact_id);
|
|
151
183
|
}
|
|
152
184
|
|
|
185
|
+
private expandedProjectIds(artifactId: string): Set<string> {
|
|
186
|
+
const result = new Set<string>();
|
|
187
|
+
for (const member of this.members(artifactId)) {
|
|
188
|
+
if (member.type === "project") result.add(member.id);
|
|
189
|
+
else for (const projectId of this.scopeGroups.expandToProjectIds(member.id)) result.add(projectId);
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
153
194
|
appliesToProject(artifactId: string, projectId: string): boolean {
|
|
154
195
|
const row = this.readRow(artifactId);
|
|
155
|
-
if (!row || row.mode === "
|
|
156
|
-
|
|
196
|
+
if (!row || row.mode === "all") return true;
|
|
197
|
+
if (row.mode === "none") return false;
|
|
198
|
+
return this.expandedProjectIds(artifactId).has(projectId);
|
|
157
199
|
}
|
|
158
200
|
|
|
159
201
|
appliesToProjectRoot(artifactId: string, projectRoot: string | undefined): boolean {
|
|
160
202
|
const row = this.readRow(artifactId);
|
|
161
|
-
if (!row || row.mode === "
|
|
203
|
+
if (!row || row.mode === "all") return true;
|
|
204
|
+
if (row.mode === "none") return false;
|
|
162
205
|
if (projectRoot === undefined) return false;
|
|
163
206
|
const project = this.db.prepare("SELECT id FROM task_projects WHERE project_root = ?").get(projectRoot) as { id: string } | null;
|
|
164
|
-
return project !== null && this.
|
|
207
|
+
return project !== null && this.expandedProjectIds(artifactId).has(project.id);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
referencesGroup(groupId: string): boolean {
|
|
211
|
+
return (
|
|
212
|
+
this.db.prepare("SELECT 1 FROM artifact_scope_members WHERE member_type = 'group' AND member_id = ? LIMIT 1").get(groupId) !== null
|
|
213
|
+
);
|
|
165
214
|
}
|
|
166
215
|
}
|