@danypops/papyrus 0.56.0 → 0.57.1

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.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/src/artifact/artifact-relationship-view.ts +1 -1
  3. package/src/artifact/artifact-scope-store.ts +32 -26
  4. package/src/artifact/in-memory-artifact-scope-store.ts +82 -43
  5. package/src/artifact/sqlite-artifact-scope-store.ts +105 -56
  6. package/src/cli/docs-command.ts +71 -0
  7. package/src/cli/gates-command.ts +1 -1
  8. package/src/cli/note-command.ts +1 -1
  9. package/src/cli/playbooks-command.ts +71 -0
  10. package/src/cli/rules-command.ts +71 -0
  11. package/src/cli/scope-groups-command.ts +197 -0
  12. package/src/cli/task-command.ts +1 -1
  13. package/src/cli.ts +7 -0
  14. package/src/constants.ts +10 -2
  15. package/src/db.ts +117 -7
  16. package/src/{stores → discussion}/discussion-round-store.ts +1 -1
  17. package/src/discussion/discussion-service.ts +2 -2
  18. package/src/{stores → discussion}/sqlite-discussion-round-store.ts +2 -2
  19. package/src/display-graph/graph-renderer.ts +5 -0
  20. package/src/docs/docs-service.ts +55 -20
  21. package/src/domain-service-shared.ts +90 -2
  22. package/src/{domain → gate}/gate-execution.ts +1 -1
  23. package/src/{stores → gate}/gate-runner.ts +1 -1
  24. package/src/{stores → gate}/sqlite-gate-runner.ts +3 -3
  25. package/src/graph-projection/graph-projection-service.ts +2 -2
  26. package/src/{stores → graph-projection}/graph-projection-store.ts +1 -1
  27. package/src/{stores → graph-projection}/sqlite-graph-projection-store.ts +2 -2
  28. package/src/handlers/discuss.ts +1 -1
  29. package/src/handlers/docs.ts +57 -2
  30. package/src/handlers/playbooks.ts +49 -8
  31. package/src/handlers/projects.ts +1 -1
  32. package/src/handlers/registry.ts +10 -5
  33. package/src/handlers/rules.ts +66 -2
  34. package/src/handlers/scope-groups.ts +100 -0
  35. package/src/handlers/task-classifiers.ts +1 -1
  36. package/src/handlers/tasks.ts +4 -4
  37. package/src/index.ts +9 -9
  38. package/src/log/log-service.ts +2 -2
  39. package/src/{stores → log}/log-store.ts +1 -1
  40. package/src/{stores → log}/sqlite-log-store.ts +2 -2
  41. package/src/modules/docs.ts +21 -1
  42. package/src/modules/graph-projection.ts +2 -2
  43. package/src/modules/logs.ts +1 -1
  44. package/src/modules/notes.ts +1 -1
  45. package/src/modules/playbooks.ts +24 -3
  46. package/src/modules/projects.ts +3 -3
  47. package/src/modules/rules.ts +21 -1
  48. package/src/modules/scope-groups.ts +92 -0
  49. package/src/modules/tasks.ts +3 -3
  50. package/src/{stores → note}/note-event-store.ts +1 -1
  51. package/src/note/note-service.ts +2 -2
  52. package/src/{stores → note}/sqlite-note-event-store.ts +2 -2
  53. package/src/ops.ts +1 -1
  54. package/src/playbook/playbook-definition.ts +2 -2
  55. package/src/playbook/playbook-execution.ts +1 -1
  56. package/src/playbook/playbook-service.ts +61 -26
  57. package/src/playbook/workflow-execution.ts +8 -8
  58. package/src/{stores → project-registry}/in-memory-project-registry-store.ts +2 -2
  59. package/src/{ports → project-registry}/project-registry-store.ts +1 -1
  60. package/src/{stores → project-registry}/sqlite-project-registry-store.ts +3 -3
  61. package/src/rules/rules-service.ts +56 -24
  62. package/src/scope-group/delete-scope-group.ts +17 -0
  63. package/src/scope-group/in-memory-scope-group-store.ts +173 -0
  64. package/src/scope-group/scope-group-store.ts +28 -0
  65. package/src/scope-group/scope-group.ts +79 -0
  66. package/src/scope-group/sqlite-scope-group-store.ts +223 -0
  67. package/src/service.ts +46 -21
  68. package/src/session-identity/session-identity-service.ts +2 -2
  69. package/src/{stores → session-identity}/sqlite-session-identity-store.ts +1 -1
  70. package/src/{stores → task/create-request}/sqlite-task-create-request-store.ts +1 -1
  71. package/src/{stores → task/focus}/sqlite-task-focus-store.ts +3 -3
  72. package/src/{stores → task/focus}/task-focus-store.ts +1 -1
  73. package/src/{stores → task/lease}/sqlite-task-lease-store.ts +4 -4
  74. package/src/{stores → task/lease}/task-lease-store.ts +2 -2
  75. package/src/{domain → task/lease}/task-lease.ts +1 -1
  76. package/src/{stores → task/mutation-request}/sqlite-task-mutation-request-store.ts +1 -1
  77. package/src/task/task-context.ts +1 -1
  78. package/src/task/task-edges.ts +2 -2
  79. package/src/task/task-focus-coordinator.ts +3 -3
  80. package/src/task/task-graph-view.ts +1 -1
  81. package/src/task/task-lease-coordinator.ts +2 -2
  82. package/src/task/task-mutation-coordinator.ts +1 -1
  83. package/src/task/task-project-scope.ts +5 -5
  84. package/src/task/task-relationship-view.ts +1 -1
  85. package/src/task/task-service.ts +15 -15
  86. package/src/{stores → task-event}/sqlite-task-event-store.ts +2 -2
  87. package/src/{stores → task-event}/task-event-store.ts +1 -1
  88. package/src/{stores → task-scope}/sqlite-task-scope-store.ts +3 -3
  89. package/src/{stores → task-scope}/task-scope-store.ts +3 -3
  90. package/src/{domain → task-scope}/task-scope.ts +1 -1
  91. package/src/domain/index.ts +0 -8
  92. package/src/stores/graph-renderer.ts +0 -5
  93. package/src/stores/index.ts +0 -1
  94. /package/src/{domain → discussion}/discussion.ts +0 -0
  95. /package/src/{domain → display-graph}/display-graph.ts +0 -0
  96. /package/src/{domain → gate}/gate.ts +0 -0
  97. /package/src/{domain → graph-projection}/graph-projection.ts +0 -0
  98. /package/src/{domain → log}/log-entry.ts +0 -0
  99. /package/src/{domain → note}/note-event.ts +0 -0
  100. /package/src/{domain → playbook}/blueprint-definition.ts +0 -0
  101. /package/src/{domain → project-registry}/project-registry.ts +0 -0
  102. /package/src/{stores → session-identity}/session-identity-store.ts +0 -0
  103. /package/src/{domain → session-identity}/session-identity.ts +0 -0
  104. /package/src/{domain → task}/checklist.ts +0 -0
  105. /package/src/{stores → task/create-request}/task-create-request-store.ts +0 -0
  106. /package/src/{stores → task/mutation-request}/task-mutation-request-store.ts +0 -0
  107. /package/src/{domain → task-event}/task-event.ts +0 -0
package/src/db.ts CHANGED
@@ -233,17 +233,34 @@ CREATE INDEX IF NOT EXISTS graph_projection_identities_artifact_idx ON graph_pro
233
233
  CREATE TABLE IF NOT EXISTS artifact_scopes (
234
234
  artifact_id TEXT PRIMARY KEY REFERENCES artifacts(id),
235
235
  project_root TEXT,
236
- mode TEXT NOT NULL DEFAULT 'global' CHECK (mode IN ('global', 'projects')),
236
+ mode TEXT NOT NULL DEFAULT 'all' CHECK (mode IN ('none', 'all', 'explicit')),
237
237
  source TEXT NOT NULL CHECK (source IN ('cwd', 'explicit', 'unscoped')),
238
238
  assigned_at TEXT NOT NULL
239
239
  );
240
240
  CREATE INDEX IF NOT EXISTS artifact_scopes_project_idx ON artifact_scopes(project_root, artifact_id);
241
- CREATE TABLE IF NOT EXISTS artifact_scope_projects (
241
+ CREATE TABLE IF NOT EXISTS artifact_scope_members (
242
242
  artifact_id TEXT NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
243
- project_id TEXT NOT NULL REFERENCES task_projects(id),
244
- PRIMARY KEY (artifact_id, project_id)
243
+ member_type TEXT NOT NULL CHECK (member_type IN ('project', 'group')),
244
+ member_id TEXT NOT NULL,
245
+ PRIMARY KEY (artifact_id, member_type, member_id)
245
246
  );
246
- CREATE INDEX IF NOT EXISTS artifact_scope_projects_project_idx ON artifact_scope_projects(project_id, artifact_id);
247
+ CREATE INDEX IF NOT EXISTS artifact_scope_members_member_idx ON artifact_scope_members(member_type, member_id, artifact_id);
248
+ CREATE TABLE IF NOT EXISTS scope_groups (
249
+ id TEXT PRIMARY KEY,
250
+ name TEXT NOT NULL,
251
+ aliases_json TEXT NOT NULL DEFAULT '[]',
252
+ created_at TEXT NOT NULL,
253
+ updated_at TEXT NOT NULL
254
+ );
255
+ CREATE INDEX IF NOT EXISTS scope_groups_name_idx ON scope_groups(name);
256
+ CREATE TABLE IF NOT EXISTS scope_group_members (
257
+ group_id TEXT NOT NULL REFERENCES scope_groups(id) ON DELETE CASCADE,
258
+ member_type TEXT NOT NULL CHECK (member_type IN ('project', 'group')),
259
+ member_id TEXT NOT NULL,
260
+ PRIMARY KEY (group_id, member_type, member_id)
261
+ );
262
+ CREATE INDEX IF NOT EXISTS scope_group_members_group_idx ON scope_group_members(group_id);
263
+ CREATE INDEX IF NOT EXISTS scope_group_members_member_idx ON scope_group_members(member_type, member_id);
247
264
  CREATE TABLE IF NOT EXISTS log_sources (
248
265
  id TEXT PRIMARY KEY,
249
266
  label TEXT NOT NULL,
@@ -864,7 +881,18 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
864
881
  // first if no Task ever used it either.
865
882
  up: (db) => {
866
883
  const existing = new Set((db.prepare("PRAGMA table_info(artifact_scopes)").all() as Array<{ name: string }>).map((row) => row.name));
867
- if (!existing.has("mode")) {
884
+ // True only when migrating a genuinely pre-v28 database. A fixture that bootstraps from
885
+ // the CURRENT SCHEMA text (which already declares this column, in whatever its own current
886
+ // CHECK vocabulary is -- version 29 later widens/renames it from 'global'/'projects' to
887
+ // 'none'/'all'/'explicit') and then only fakes an older user_version to exercise an earlier
888
+ // migration's own path already has the column; forcing this migration's literal 'projects'
889
+ // value into it would violate version 29's tighter CHECK once that has shipped. The
890
+ // membership-table backfill below still always runs regardless (idempotent, and never
891
+ // touches the CHECK-restricted mode column itself) -- version 29's own remap pass detects
892
+ // "has a membership row" directly rather than trusting this flag's own mode literal, so a
893
+ // fixture that skips this exact write still ends up in the correct final 'explicit' state.
894
+ const columnPreexisted = existing.has("mode");
895
+ if (!columnPreexisted) {
868
896
  db.exec("ALTER TABLE artifact_scopes ADD COLUMN mode TEXT NOT NULL DEFAULT 'global' CHECK (mode IN ('global', 'projects'))");
869
897
  }
870
898
  db.exec(`
@@ -893,11 +921,93 @@ const FUTURE_MIGRATIONS: ReadonlyArray<PapyrusMigration> = [
893
921
  const now = new Date().toISOString();
894
922
  insertProject.run(projectId, basename(row.project_root) || row.project_root, row.project_root, now, now);
895
923
  }
896
- setProjectsMode.run(row.artifact_id);
924
+ if (!columnPreexisted) setProjectsMode.run(row.artifact_id);
897
925
  insertMembership.run(row.artifact_id, projectId);
898
926
  }
899
927
  },
900
928
  },
929
+ {
930
+ version: 29,
931
+ name: "artifact-scope-tri-state-and-scope-groups",
932
+ // See scope-group/. Adds the nested-scope-group registry (scope_groups/scope_group_members),
933
+ // and replaces artifact_scopes' two-state mode ('global'|'projects') with a real three-state
934
+ // one ('none'|'all'|'explicit') -- 'none' (fully hidden, never applicable) did not exist
935
+ // before this. SQLite has no ALTER TABLE ... CHECK, so artifact_scopes is rebuilt (copy with
936
+ // remapped mode values, drop, rename) rather than altered in place; every row's own
937
+ // artifact_id/project_root/source/assigned_at is preserved byte-for-byte, only `mode`'s
938
+ // values change (global->all, projects->explicit) and its CHECK constraint widens.
939
+ // artifact_scope_projects is folded into the new, kind-neutral artifact_scope_members
940
+ // (adding member_type so a membership row can name a project OR a scope group) --
941
+ // existing project memberships become member_type='project' rows, unchanged in meaning.
942
+ up: (db) => {
943
+ db.exec(`
944
+ CREATE TABLE IF NOT EXISTS scope_groups (
945
+ id TEXT PRIMARY KEY,
946
+ name TEXT NOT NULL,
947
+ aliases_json TEXT NOT NULL DEFAULT '[]',
948
+ created_at TEXT NOT NULL,
949
+ updated_at TEXT NOT NULL
950
+ );
951
+ CREATE INDEX IF NOT EXISTS scope_groups_name_idx ON scope_groups(name);
952
+ CREATE TABLE IF NOT EXISTS scope_group_members (
953
+ group_id TEXT NOT NULL REFERENCES scope_groups(id) ON DELETE CASCADE,
954
+ member_type TEXT NOT NULL CHECK (member_type IN ('project', 'group')),
955
+ member_id TEXT NOT NULL,
956
+ PRIMARY KEY (group_id, member_type, member_id)
957
+ );
958
+ CREATE INDEX IF NOT EXISTS scope_group_members_group_idx ON scope_group_members(group_id);
959
+ CREATE INDEX IF NOT EXISTS scope_group_members_member_idx ON scope_group_members(member_type, member_id);
960
+ `);
961
+ // Guarded by whether the OLD artifact_scope_projects table still exists, the same class of
962
+ // concern version 16's/note-events' own comments cover: a fixture that bootstraps from the
963
+ // CURRENT SCHEMA text (which already declares artifact_scopes/artifact_scope_members in
964
+ // their POST-this-migration shape) and then only fakes an older user_version to exercise an
965
+ // earlier migration's own path must not attempt to rebuild a table that's already correctly
966
+ // shaped -- rebuilding it again with a CASE mode remap over already-new mode values ('all'/
967
+ // 'explicit') would violate the new CHECK, since those aren't 'global'/'projects' either.
968
+ const oldTableExists =
969
+ db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'artifact_scope_projects'").get() != null;
970
+ if (!oldTableExists) return;
971
+ db.exec(`
972
+ CREATE TABLE artifact_scopes_v29 (
973
+ artifact_id TEXT PRIMARY KEY REFERENCES artifacts(id),
974
+ project_root TEXT,
975
+ mode TEXT NOT NULL DEFAULT 'all' CHECK (mode IN ('none', 'all', 'explicit')),
976
+ source TEXT NOT NULL CHECK (source IN ('cwd', 'explicit', 'unscoped')),
977
+ assigned_at TEXT NOT NULL
978
+ );
979
+ -- Keyed off real membership-row existence, not the old mode literal: v28's own write of
980
+ -- that literal is itself conditionally skipped (see its own comment) when replaying
981
+ -- against a fixture that already has this column in a later, tightened CHECK shape --
982
+ -- membership rows are still always backfilled unconditionally either way, so checking
983
+ -- for one directly is correct in every case, historical or fixture-simulated alike.
984
+ INSERT INTO artifact_scopes_v29 (artifact_id, project_root, mode, source, assigned_at)
985
+ SELECT s.artifact_id, s.project_root,
986
+ CASE
987
+ WHEN EXISTS (SELECT 1 FROM artifact_scope_projects p WHERE p.artifact_id = s.artifact_id) THEN 'explicit'
988
+ WHEN s.mode = 'global' THEN 'all'
989
+ WHEN s.mode = 'projects' THEN 'explicit'
990
+ ELSE s.mode
991
+ END,
992
+ s.source, s.assigned_at
993
+ FROM artifact_scopes s;
994
+ DROP TABLE artifact_scopes;
995
+ ALTER TABLE artifact_scopes_v29 RENAME TO artifact_scopes;
996
+ CREATE INDEX IF NOT EXISTS artifact_scopes_project_idx ON artifact_scopes(project_root, artifact_id);
997
+
998
+ CREATE TABLE IF NOT EXISTS artifact_scope_members (
999
+ artifact_id TEXT NOT NULL REFERENCES artifacts(id) ON DELETE CASCADE,
1000
+ member_type TEXT NOT NULL CHECK (member_type IN ('project', 'group')),
1001
+ member_id TEXT NOT NULL,
1002
+ PRIMARY KEY (artifact_id, member_type, member_id)
1003
+ );
1004
+ INSERT INTO artifact_scope_members (artifact_id, member_type, member_id)
1005
+ SELECT artifact_id, 'project', project_id FROM artifact_scope_projects;
1006
+ DROP TABLE artifact_scope_projects;
1007
+ CREATE INDEX IF NOT EXISTS artifact_scope_members_member_idx ON artifact_scope_members(member_type, member_id, artifact_id);
1008
+ `);
1009
+ },
1010
+ },
901
1011
  ];
902
1012
 
903
1013
  /**
@@ -1,4 +1,4 @@
1
- import type { AppendDiscussionRound, DiscussionRound, DiscussionRoundQuery } from "../domain/discussion.ts";
1
+ import type { AppendDiscussionRound, DiscussionRound, DiscussionRoundQuery } from "../discussion/discussion.ts";
2
2
 
3
3
  /** Persistence port for a Discussion's append-only rounds (see domain/discussion.ts). */
4
4
  export interface DiscussionRoundStore {
@@ -21,8 +21,8 @@ import {
21
21
  validateDiscussionOptions,
22
22
  validateSelectedOptions,
23
23
  validateSettlement,
24
- } from "../domain/discussion.ts";
25
- import type { DiscussionRoundStore } from "../stores/discussion-round-store.ts";
24
+ } from "./discussion.ts";
25
+ import type { DiscussionRoundStore } from "./discussion-round-store.ts";
26
26
 
27
27
  export class DiscussionError extends Error {}
28
28
 
@@ -8,8 +8,8 @@ import {
8
8
  validateDiscussionActor,
9
9
  validateDiscussionContent,
10
10
  validateDiscussionOptions,
11
- } from "../domain/discussion.ts";
12
- import type { DiscussionRoundStore } from "./discussion-round-store.ts";
11
+ } from "../discussion/discussion.ts";
12
+ import type { DiscussionRoundStore } from "../discussion/discussion-round-store.ts";
13
13
 
14
14
  interface DiscussionRoundRow {
15
15
  id: number;
@@ -0,0 +1,5 @@
1
+ import type { DisplayGraph, RenderedGraph } from "../display-graph/display-graph.ts";
2
+
3
+ export interface GraphRenderer {
4
+ render(graph: DisplayGraph): RenderedGraph;
5
+ }
@@ -9,23 +9,29 @@ import type { ArtifactEventContext } from "../artifact/artifact-event.ts";
9
9
  import type { ArtifactScope, ArtifactScopeStore } from "../artifact/artifact-scope-store.ts";
10
10
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
11
11
  import type { ArtifactAction, AuthorityRegistry } from "../authority-registry.ts";
12
- import { ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT } from "../constants.ts";
13
- import { resolveProjectReference } from "../domain/project-registry.ts";
14
- import { normalizeProjectRoot } from "../domain/task-scope.ts";
15
12
  import {
13
+ addArtifactScopeGroup,
14
+ addArtifactScopeProject,
16
15
  assertBodyBounds,
17
16
  assertLabelsBounds,
18
17
  assertTitleBounds,
19
18
  type ListFilter,
20
19
  listScoped,
20
+ removeArtifactScopeGroup,
21
+ removeArtifactScopeProject,
22
+ replaceArtifactScopeGroups,
23
+ replaceArtifactScopeProjects,
21
24
  requireContentUpdateFields,
22
25
  requireKind,
23
26
  runTransition,
27
+ setArtifactScopeNone,
24
28
  type TransitionTable,
25
29
  type UpdateContentInput,
26
30
  } from "../domain-service-shared.ts";
27
31
  import { NOTE_SUBTYPE } from "../note/note-service.ts";
28
- import type { ProjectRegistryStore } from "../ports/project-registry-store.ts";
32
+ import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
33
+ import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
34
+ import { normalizeProjectRoot } from "../task-scope/task-scope.ts";
29
35
 
30
36
  function rejectsNoteTemplate(artifacts: ArtifactStore, templateId: string | undefined, subtype: string | undefined): boolean {
31
37
  if (subtype === NOTE_SUBTYPE) return true;
@@ -117,11 +123,7 @@ export function createDocument(
117
123
  context,
118
124
  );
119
125
  if (input.projectReferences !== undefined && input.projectReferences.length > 0) {
120
- if (input.projectReferences.length > ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT) {
121
- throw new Error(`projectReferences may include at most ${ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT} entries`);
122
- }
123
- const ids = input.projectReferences.map((reference) => resolveProjectReference(registry!, reference).id);
124
- scopes.replaceProjects(document.id, ids, "explicit");
126
+ replaceArtifactScopeProjects(scopes, registry!, document.id, input.projectReferences);
125
127
  } else {
126
128
  scopes.assign(document.id, projectRoot, projectRoot === undefined ? "unscoped" : "explicit");
127
129
  }
@@ -163,7 +165,13 @@ export function docScope(artifacts: ArtifactStore, scopes: ArtifactScopeStore, i
163
165
 
164
166
  export function setDocGlobal(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string): ArtifactScope {
165
167
  requireDocument(artifacts, id);
166
- return scopes.setGlobal(id, "explicit");
168
+ return scopes.setAll(id, "explicit");
169
+ }
170
+
171
+ /** Fully hides this Doc -- never applicable, never context-injected, regardless of project. The only new mode beyond the pre-existing global/projects pair. */
172
+ export function setDocNone(artifacts: ArtifactStore, scopes: ArtifactScopeStore, id: string): ArtifactScope {
173
+ requireDocument(artifacts, id);
174
+ return setArtifactScopeNone(scopes, id);
167
175
  }
168
176
 
169
177
  export function replaceDocProjects(
@@ -174,12 +182,7 @@ export function replaceDocProjects(
174
182
  projectReferences: readonly string[],
175
183
  ): ArtifactScope {
176
184
  requireDocument(artifacts, id);
177
- if (projectReferences.length === 0) throw new Error("projectReferences must be non-empty; use docs.set_global to clear scoping");
178
- if (projectReferences.length > ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT) {
179
- throw new Error(`projectReferences may include at most ${ARTIFACT_SCOPE_MAX_PROJECTS_PER_ARTIFACT} entries`);
180
- }
181
- const ids = projectReferences.map((reference) => resolveProjectReference(registry, reference).id);
182
- return scopes.replaceProjects(id, ids, "explicit");
185
+ return replaceArtifactScopeProjects(scopes, registry, id, projectReferences);
183
186
  }
184
187
 
185
188
  export function addDocProject(
@@ -190,8 +193,7 @@ export function addDocProject(
190
193
  projectReference: string,
191
194
  ): ArtifactScope {
192
195
  requireDocument(artifacts, id);
193
- const project = resolveProjectReference(registry, projectReference);
194
- return scopes.addProject(id, project.id, "explicit");
196
+ return addArtifactScopeProject(scopes, registry, id, projectReference);
195
197
  }
196
198
 
197
199
  export function removeDocProject(
@@ -202,8 +204,41 @@ export function removeDocProject(
202
204
  projectReference: string,
203
205
  ): ArtifactScope {
204
206
  requireDocument(artifacts, id);
205
- const project = resolveProjectReference(registry, projectReference);
206
- return scopes.removeProject(id, project.id);
207
+ return removeArtifactScopeProject(scopes, registry, id, projectReference);
208
+ }
209
+
210
+ /** Scope-group ('nested scope') siblings of replaceDocProjects/addDocProject/removeDocProject -- same Note-rejecting Doc guard, delegating the actual reference-resolution and store call to the shared, kind-agnostic helpers. */
211
+ export function replaceDocGroups(
212
+ artifacts: ArtifactStore,
213
+ scopes: ArtifactScopeStore,
214
+ scopeGroups: ScopeGroupStore,
215
+ id: string,
216
+ groupReferences: readonly string[],
217
+ ): ArtifactScope {
218
+ requireDocument(artifacts, id);
219
+ return replaceArtifactScopeGroups(scopes, scopeGroups, id, groupReferences);
220
+ }
221
+
222
+ export function addDocGroup(
223
+ artifacts: ArtifactStore,
224
+ scopes: ArtifactScopeStore,
225
+ scopeGroups: ScopeGroupStore,
226
+ id: string,
227
+ groupReference: string,
228
+ ): ArtifactScope {
229
+ requireDocument(artifacts, id);
230
+ return addArtifactScopeGroup(scopes, scopeGroups, id, groupReference);
231
+ }
232
+
233
+ export function removeDocGroup(
234
+ artifacts: ArtifactStore,
235
+ scopes: ArtifactScopeStore,
236
+ scopeGroups: ScopeGroupStore,
237
+ id: string,
238
+ groupReference: string,
239
+ ): ArtifactScope {
240
+ requireDocument(artifacts, id);
241
+ return removeArtifactScopeGroup(scopes, scopeGroups, id, groupReference);
207
242
  }
208
243
 
209
244
  function requireDocument(artifacts: ArtifactStore, id: string): Artifact {
@@ -7,16 +7,21 @@
7
7
 
8
8
  import type { Artifact } from "./artifact/artifact.ts";
9
9
  import type { ArtifactEventContext } from "./artifact/artifact-event.ts";
10
- import type { ArtifactScopeStore } from "./artifact/artifact-scope-store.ts";
10
+ import type { ArtifactScope, ArtifactScopeStore } from "./artifact/artifact-scope-store.ts";
11
11
  import type { ArtifactStore } from "./artifact/artifact-store.ts";
12
12
  import {
13
13
  ARTIFACT_BODY_MAX_LENGTH,
14
14
  ARTIFACT_LABEL_MAX_COUNT,
15
15
  ARTIFACT_LABEL_MAX_LENGTH,
16
16
  ARTIFACT_SCOPE_MAX_ARTIFACTS,
17
+ ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT,
17
18
  ARTIFACT_TITLE_MAX_LENGTH,
18
19
  } from "./constants.ts";
19
- import { normalizeProjectRoot } from "./domain/task-scope.ts";
20
+ import { resolveProjectReference } from "./project-registry/project-registry.ts";
21
+ import type { ProjectRegistryStore } from "./project-registry/project-registry-store.ts";
22
+ import { resolveScopeGroupReference } from "./scope-group/scope-group.ts";
23
+ import type { ScopeGroupStore } from "./scope-group/scope-group-store.ts";
24
+ import { normalizeProjectRoot } from "./task-scope/task-scope.ts";
20
25
 
21
26
  export interface UpdateContentInput {
22
27
  title?: string;
@@ -181,3 +186,86 @@ export function assignArtifactProject(
181
186
  );
182
187
  return artifacts.get(id)!;
183
188
  }
189
+
190
+ /**
191
+ * Kind-agnostic scope-mutation primitives shared by docs/rules/playbook-service.ts's own thin,
192
+ * per-kind wrappers (each of which validates the artifact's own kind/subtype first -- e.g.
193
+ * requireDocument's Note exclusion -- then delegates here for the actual reference-resolution +
194
+ * bounds-check + store-call, which is genuinely identical across every kind). Mirrors
195
+ * assignArtifactProject's own already-established split, extended to the tri-state
196
+ * none/all/explicit model and to scope groups ('nested scopes').
197
+ */
198
+ export function setArtifactScopeNone(scopes: ArtifactScopeStore, artifactId: string): ArtifactScope {
199
+ return scopes.setNone(artifactId, "explicit");
200
+ }
201
+
202
+ export function replaceArtifactScopeProjects(
203
+ scopes: ArtifactScopeStore,
204
+ registry: ProjectRegistryStore,
205
+ artifactId: string,
206
+ projectReferences: readonly string[],
207
+ ): ArtifactScope {
208
+ if (projectReferences.length === 0) throw new Error("projectReferences must be non-empty; use set_none/set_global to clear scoping");
209
+ if (projectReferences.length > ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
210
+ throw new Error(`projectReferences may include at most ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} entries`);
211
+ }
212
+ const members = projectReferences.map((reference) => ({ type: "project" as const, id: resolveProjectReference(registry, reference).id }));
213
+ return scopes.replaceMembers(artifactId, members, "explicit");
214
+ }
215
+
216
+ export function addArtifactScopeProject(
217
+ scopes: ArtifactScopeStore,
218
+ registry: ProjectRegistryStore,
219
+ artifactId: string,
220
+ projectReference: string,
221
+ ): ArtifactScope {
222
+ const project = resolveProjectReference(registry, projectReference);
223
+ return scopes.addMember(artifactId, { type: "project", id: project.id }, "explicit");
224
+ }
225
+
226
+ export function removeArtifactScopeProject(
227
+ scopes: ArtifactScopeStore,
228
+ registry: ProjectRegistryStore,
229
+ artifactId: string,
230
+ projectReference: string,
231
+ ): ArtifactScope {
232
+ const project = resolveProjectReference(registry, projectReference);
233
+ return scopes.removeMember(artifactId, { type: "project", id: project.id });
234
+ }
235
+
236
+ export function replaceArtifactScopeGroups(
237
+ scopes: ArtifactScopeStore,
238
+ scopeGroups: ScopeGroupStore,
239
+ artifactId: string,
240
+ groupReferences: readonly string[],
241
+ ): ArtifactScope {
242
+ if (groupReferences.length === 0) throw new Error("groupReferences must be non-empty; use set_none/set_global to clear scoping");
243
+ if (groupReferences.length > ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT) {
244
+ throw new Error(`groupReferences may include at most ${ARTIFACT_SCOPE_MAX_MEMBERS_PER_ARTIFACT} entries`);
245
+ }
246
+ const members = groupReferences.map((reference) => ({
247
+ type: "group" as const,
248
+ id: resolveScopeGroupReference(scopeGroups, reference).id,
249
+ }));
250
+ return scopes.replaceMembers(artifactId, members, "explicit");
251
+ }
252
+
253
+ export function addArtifactScopeGroup(
254
+ scopes: ArtifactScopeStore,
255
+ scopeGroups: ScopeGroupStore,
256
+ artifactId: string,
257
+ groupReference: string,
258
+ ): ArtifactScope {
259
+ const group = resolveScopeGroupReference(scopeGroups, groupReference);
260
+ return scopes.addMember(artifactId, { type: "group", id: group.id }, "explicit");
261
+ }
262
+
263
+ export function removeArtifactScopeGroup(
264
+ scopes: ArtifactScopeStore,
265
+ scopeGroups: ScopeGroupStore,
266
+ artifactId: string,
267
+ groupReference: string,
268
+ ): ArtifactScope {
269
+ const group = resolveScopeGroupReference(scopeGroups, groupReference);
270
+ return scopes.removeMember(artifactId, { type: "group", id: group.id });
271
+ }
@@ -26,8 +26,8 @@ import {
26
26
  GATE_TEST_TIMEOUT_MS,
27
27
  } from "../constants.ts";
28
28
  import type { Db } from "../db.ts";
29
+ import type { Gate, GateResult, GateRunOptions } from "../gate/gate.ts";
29
30
  import { getArtifact } from "../ops.ts";
30
- import type { Gate, GateResult, GateRunOptions } from "./gate.ts";
31
31
 
32
32
  const require_ = createRequire(import.meta.url);
33
33
 
@@ -1,4 +1,4 @@
1
- import type { GateResult, GateRunOptions } from "../domain/gate.ts";
1
+ import type { GateResult, GateRunOptions } from "../gate/gate.ts";
2
2
 
3
3
  export interface GateRunner {
4
4
  run(artifactId: string, options?: GateRunOptions): GateResult[];
@@ -1,7 +1,7 @@
1
1
  import type { Db } from "../db.ts";
2
- import type { GateResult, GateRunOptions } from "../domain/gate.ts";
3
- import { runGates, runGatesAsync } from "../domain/gate-execution.ts";
4
- import type { GateRunner } from "./gate-runner.ts";
2
+ import type { GateResult, GateRunOptions } from "../gate/gate.ts";
3
+ import { runGates, runGatesAsync } from "../gate/gate-execution.ts";
4
+ import type { GateRunner } from "../gate/gate-runner.ts";
5
5
 
6
6
  export class SQLiteGateRunner implements GateRunner {
7
7
  constructor(private readonly db: Db) {}
@@ -12,8 +12,8 @@ import {
12
12
  GRAPH_PROJECTION_MAX_ARTIFACTS_PER_BATCH,
13
13
  GRAPH_PROJECTION_MAX_EDGES_PER_BATCH,
14
14
  } from "../constants.ts";
15
- import { GRAPH_PROJECTION_SCHEMA_VERSION, type GraphProjectionBatch, type GraphProjectionResult } from "../domain/graph-projection.ts";
16
- import type { GraphProjectionStore } from "../stores/graph-projection-store.ts";
15
+ import { GRAPH_PROJECTION_SCHEMA_VERSION, type GraphProjectionBatch, type GraphProjectionResult } from "./graph-projection.ts";
16
+ import type { GraphProjectionStore } from "./graph-projection-store.ts";
17
17
 
18
18
  function boundedId(value: string, label: string): string {
19
19
  if (!value || value.length === 0) throw new Error(`${label} is required`);
@@ -1,4 +1,4 @@
1
- import type { ProjectionCheckpoint } from "../domain/graph-projection.ts";
1
+ import type { ProjectionCheckpoint } from "../graph-projection/graph-projection.ts";
2
2
 
3
3
  /**
4
4
  * Producer-scoped state a graph projection consumer needs beyond the generic ArtifactStore:
@@ -1,7 +1,7 @@
1
1
  import type { Db } from "../db.ts";
2
2
  import { inTransaction } from "../db.ts";
3
- import type { ProjectionCheckpoint } from "../domain/graph-projection.ts";
4
- import type { GraphProjectionStore } from "./graph-projection-store.ts";
3
+ import type { ProjectionCheckpoint } from "../graph-projection/graph-projection.ts";
4
+ import type { GraphProjectionStore } from "../graph-projection/graph-projection-store.ts";
5
5
 
6
6
  export class SQLiteGraphProjectionStore implements GraphProjectionStore {
7
7
  constructor(private readonly db: Db) {}
@@ -18,8 +18,8 @@ import type { VehicleRegistry } from "@danypops/vehicle-server";
18
18
  import type { Artifact } from "../artifact/artifact.ts";
19
19
  import type { ArtifactStore } from "../artifact/artifact-store.ts";
20
20
  import { DISCUSSION_OPTION_DESCRIPTION_MAX_LENGTH, DISCUSSION_OPTION_MAX_LENGTH, DISCUSSION_OPTIONS_MAX_COUNT } from "../constants.ts";
21
+ import { DISCUSSION_SUBTYPE, type DiscussionRound } from "../discussion/discussion.ts";
21
22
  import type { DiscussionAndRounds, Discussions } from "../discussion/discussion-service.ts";
22
- import { DISCUSSION_SUBTYPE, type DiscussionRound } from "../domain/discussion.ts";
23
23
  import { discussOperations } from "../modules/discuss.ts";
24
24
  import {
25
25
  createOperationDefiner,
@@ -9,7 +9,8 @@ import type { ArtifactStore } from "../artifact/artifact-store.ts";
9
9
  import type { AuthorityRegistry } from "../authority-registry.ts";
10
10
  import { listDocuments } from "../docs/docs-service.ts";
11
11
  import { docsOperations } from "../modules/docs.ts";
12
- import type { ProjectRegistryStore } from "../ports/project-registry-store.ts";
12
+ import type { ProjectRegistryStore } from "../project-registry/project-registry-store.ts";
13
+ import type { ScopeGroupStore } from "../scope-group/scope-group-store.ts";
13
14
  import { booleanProp, createOperationDefiner, numberProp, resolveArtifactIdWidened, stringProp, validationError } from "./shared.ts";
14
15
 
15
16
  const OWNER = "docs";
@@ -54,8 +55,9 @@ export function registerDocsVehicleOperations(
54
55
  scopes: ArtifactScopeStore,
55
56
  authority: AuthorityRegistry,
56
57
  projectRegistry: ProjectRegistryStore,
58
+ scopeGroups: ScopeGroupStore,
57
59
  ): void {
58
- const moduleOperations = new Map(docsOperations(artifacts, scopes, authority, projectRegistry).map((op) => [op.name, op]));
60
+ const moduleOperations = new Map(docsOperations(artifacts, scopes, authority, projectRegistry, scopeGroups).map((op) => [op.name, op]));
59
61
  const call = (name: string, input: Record<string, unknown>): unknown => moduleOperations.get(name)!.execute(input);
60
62
  const define = createOperationDefiner(registry, OWNER, "docs", ["docs:read", "docs:write"], call);
61
63
 
@@ -209,6 +211,59 @@ export function registerDocsVehicleOperations(
209
211
  (input) => ({ ...input, id: resolveDocId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name) }),
210
212
  );
211
213
 
214
+ define(
215
+ "set_none",
216
+ "Fully hides a Doc -- never applicable, never context-injected, regardless of project. The only way back is set_global, add_project, add_group, or replace_projects/replace_groups.",
217
+ "local-write",
218
+ { id: stringProp, name: stringProp, project_root: stringProp },
219
+ [],
220
+ (input) => ({ ...input, id: resolveDocId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name) }),
221
+ );
222
+
223
+ define(
224
+ "add_group",
225
+ "Adds one scope group (a named, reusable, possibly-nested collection of projects and/or other groups) to a Doc's explicit scope, switching it from global/none to project-bound if it wasn't already. Idempotent if the group is already a member.",
226
+ "local-write",
227
+ {
228
+ id: stringProp,
229
+ name: stringProp,
230
+ group: { ...stringProp, description: "Exact scope group id, name, or alias to add." },
231
+ project_root: stringProp,
232
+ },
233
+ ["group"],
234
+ (input) => ({ ...input, id: resolveDocId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name) }),
235
+ );
236
+
237
+ define(
238
+ "remove_group",
239
+ "Removes one scope group from a Doc's explicit scope. Rejected while it is the Doc's only remaining scope member -- call set_global or set_none first.",
240
+ "local-write",
241
+ {
242
+ id: stringProp,
243
+ name: stringProp,
244
+ group: { ...stringProp, description: "Exact scope group id, name, or alias to remove." },
245
+ project_root: stringProp,
246
+ },
247
+ ["group"],
248
+ (input) => ({ ...input, id: resolveDocId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name) }),
249
+ );
250
+
251
+ define(
252
+ "replace_groups",
253
+ "Replaces a Doc's entire scope-group membership with exactly this bounded, non-empty list of scope group references (id/name/alias). Use set_global/set_none instead to clear scoping entirely.",
254
+ "local-write",
255
+ {
256
+ id: stringProp,
257
+ name: stringProp,
258
+ groups: { type: "array", description: "Non-empty list of exact scope group id/name/alias references." } as unknown as {
259
+ type: string;
260
+ },
261
+ project_root: stringProp,
262
+ },
263
+ ["groups"],
264
+ (input) => ({ ...input, id: resolveDocId(artifacts, scopes, input.project_root as string | undefined, input.id, input.name) }),
265
+ );
266
+
212
267
  define(
213
268
  "update",
214
269
  "Changes a Doc's title/body/labels (at least one required). Refused for a read-only external projection (e.g. web-spider-ingested Docs) -- capture a correction as a new linked Doc instead.",