@gitgov/core 1.0.2 → 1.1.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.
@@ -1763,6 +1763,7 @@ interface IBacklogAdapter {
1763
1763
  updateTask(taskId: string, payload: Partial<TaskRecord>): Promise<TaskRecord>;
1764
1764
  activateTask(taskId: string, actorId: string): Promise<TaskRecord>;
1765
1765
  completeTask(taskId: string, actorId: string): Promise<TaskRecord>;
1766
+ pauseTask(taskId: string, actorId: string, reason?: string): Promise<TaskRecord>;
1766
1767
  resumeTask(taskId: string, actorId: string, force?: boolean): Promise<TaskRecord>;
1767
1768
  discardTask(taskId: string, actorId: string, reason?: string): Promise<TaskRecord>;
1768
1769
  createCycle(payload: Partial<CycleRecord>, actorId: string): Promise<CycleRecord>;
@@ -1770,6 +1771,8 @@ interface IBacklogAdapter {
1770
1771
  getAllCycles(): Promise<CycleRecord[]>;
1771
1772
  updateCycle(cycleId: string, payload: Partial<CycleRecord>): Promise<CycleRecord>;
1772
1773
  addTaskToCycle(cycleId: string, taskId: string): Promise<void>;
1774
+ removeTasksFromCycle(cycleId: string, taskIds: string[]): Promise<void>;
1775
+ moveTasksBetweenCycles(targetCycleId: string, taskIds: string[], sourceCycleId: string): Promise<void>;
1773
1776
  getTasksAssignedToActor(actorId: string): Promise<TaskRecord[]>;
1774
1777
  handleFeedbackCreated(event: FeedbackCreatedEvent): Promise<void>;
1775
1778
  handleFeedbackResolved(event: FeedbackStatusChangedEvent): Promise<void>;
@@ -1845,6 +1848,10 @@ declare class BacklogAdapter implements IBacklogAdapter {
1845
1848
  * Activates a task transitioning from ready to active with permission validation
1846
1849
  */
1847
1850
  activateTask(taskId: string, actorId: string): Promise<TaskRecord>;
1851
+ /**
1852
+ * Pauses a task manually transitioning from active to paused with optional reason
1853
+ */
1854
+ pauseTask(taskId: string, actorId: string, reason?: string): Promise<TaskRecord>;
1848
1855
  /**
1849
1856
  * Resumes a paused task transitioning back to active with optional force override
1850
1857
  */
@@ -1926,6 +1933,17 @@ declare class BacklogAdapter implements IBacklogAdapter {
1926
1933
  * Creates bidirectional link between task and cycle
1927
1934
  */
1928
1935
  addTaskToCycle(cycleId: string, taskId: string): Promise<void>;
1936
+ /**
1937
+ * Removes multiple tasks from a cycle with bidirectional unlinking
1938
+ * All business logic and validation happens here in the adapter
1939
+ */
1940
+ removeTasksFromCycle(cycleId: string, taskIds: string[]): Promise<void>;
1941
+ /**
1942
+ * Moves multiple tasks from one cycle to another atomically
1943
+ * Provides transactional semantics - all tasks move or none do
1944
+ * All business logic and validation happens here in the adapter
1945
+ */
1946
+ moveTasksBetweenCycles(targetCycleId: string, taskIds: string[], sourceCycleId: string): Promise<void>;
1929
1947
  lint(): Promise<LintReport>;
1930
1948
  audit(): Promise<AuditReport>;
1931
1949
  processChanges(_changes: unknown[]): Promise<ExecutionRecord[]>;
@@ -5405,7 +5423,7 @@ declare class DiagramGenerator {
5405
5423
  cycleId?: string;
5406
5424
  taskId?: string;
5407
5425
  packageName?: string;
5408
- }): Promise<string>;
5426
+ }, showArchived?: boolean): Promise<string>;
5409
5427
  /**
5410
5428
  * Convenience method to generate from .gitgov/ directory
5411
5429
  */
@@ -5413,7 +5431,7 @@ declare class DiagramGenerator {
5413
5431
  cycleId?: string;
5414
5432
  taskId?: string;
5415
5433
  packageName?: string;
5416
- }): Promise<string>;
5434
+ }, showArchived?: boolean): Promise<string>;
5417
5435
  /**
5418
5436
  * Loads all cycle records from the filesystem
5419
5437
  */
package/dist/src/index.js CHANGED
@@ -4236,6 +4236,55 @@ var BacklogAdapter = class {
4236
4236
  });
4237
4237
  return updatedPayload;
4238
4238
  }
4239
+ /**
4240
+ * Pauses a task manually transitioning from active to paused with optional reason
4241
+ */
4242
+ async pauseTask(taskId, actorId, reason) {
4243
+ const taskRecord = await this.taskStore.read(taskId);
4244
+ if (!taskRecord) {
4245
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4246
+ }
4247
+ const task = taskRecord.payload;
4248
+ if (task.status !== "active") {
4249
+ throw new Error(`ProtocolViolationError: Task is in '${task.status}' state. Cannot pause (requires active).`);
4250
+ }
4251
+ const actor = await this.getActor(actorId);
4252
+ const context = {
4253
+ task,
4254
+ actor,
4255
+ signatures: taskRecord.header.signatures,
4256
+ transitionTo: "paused"
4257
+ };
4258
+ const transitionRule = await this.workflowMethodologyAdapter.getTransitionRule("active", "paused", context);
4259
+ if (!transitionRule) {
4260
+ throw new Error("ProtocolViolationError: Workflow methodology rejected active\u2192paused transition");
4261
+ }
4262
+ const updatedPayload = {
4263
+ ...task,
4264
+ status: "paused",
4265
+ // Add reason to notes with [PAUSED] prefix if provided
4266
+ ...reason && {
4267
+ notes: `${task.notes || ""}
4268
+ [PAUSED] ${reason} (${(/* @__PURE__ */ new Date()).toISOString()})`.trim()
4269
+ }
4270
+ };
4271
+ const updatedRecord = { ...taskRecord, payload: updatedPayload };
4272
+ const signedRecord = await this.identity.signRecord(updatedRecord, actorId, "pauser");
4273
+ await this.taskStore.write(signedRecord);
4274
+ this.eventBus.publish({
4275
+ type: "task.status.changed",
4276
+ timestamp: Date.now(),
4277
+ source: "backlog_adapter",
4278
+ payload: {
4279
+ taskId,
4280
+ oldStatus: "active",
4281
+ newStatus: "paused",
4282
+ actorId,
4283
+ reason: reason || "Task manually paused"
4284
+ }
4285
+ });
4286
+ return updatedPayload;
4287
+ }
4239
4288
  /**
4240
4289
  * Resumes a paused task transitioning back to active with optional force override
4241
4290
  */
@@ -4801,6 +4850,156 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
4801
4850
  this.taskStore.write(signedTaskRecord)
4802
4851
  ]);
4803
4852
  }
4853
+ /**
4854
+ * Removes multiple tasks from a cycle with bidirectional unlinking
4855
+ * All business logic and validation happens here in the adapter
4856
+ */
4857
+ async removeTasksFromCycle(cycleId, taskIds) {
4858
+ if (!cycleId || typeof cycleId !== "string") {
4859
+ throw new Error("ValidationError: cycleId must be a non-empty string");
4860
+ }
4861
+ if (!Array.isArray(taskIds) || taskIds.length === 0) {
4862
+ throw new Error("ValidationError: taskIds must be a non-empty array");
4863
+ }
4864
+ const cycleRecord = await this.cycleStore.read(cycleId);
4865
+ if (!cycleRecord) {
4866
+ throw new Error(`RecordNotFoundError: Cycle not found: ${cycleId}`);
4867
+ }
4868
+ const taskRecords = await Promise.all(
4869
+ taskIds.map(async (taskId) => {
4870
+ const taskRecord = await this.taskStore.read(taskId);
4871
+ if (!taskRecord) {
4872
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4873
+ }
4874
+ return { taskId, record: taskRecord };
4875
+ })
4876
+ );
4877
+ const cycleTaskIds = cycleRecord.payload.taskIds || [];
4878
+ const notLinkedTasks = taskIds.filter((taskId) => !cycleTaskIds.includes(taskId));
4879
+ if (notLinkedTasks.length > 0) {
4880
+ throw new Error(`ValidationError: Tasks not linked to cycle ${cycleId}: ${notLinkedTasks.join(", ")}`);
4881
+ }
4882
+ const updatedCycle = {
4883
+ ...cycleRecord.payload,
4884
+ taskIds: cycleTaskIds.filter((id) => !taskIds.includes(id))
4885
+ };
4886
+ const currentActor = await this.identity.getCurrentActor();
4887
+ const signedCycleRecord = await this.identity.signRecord(
4888
+ { ...cycleRecord, payload: updatedCycle },
4889
+ currentActor.id,
4890
+ "author"
4891
+ );
4892
+ const signedTaskRecords = await Promise.all(
4893
+ taskRecords.map(async ({ record }) => {
4894
+ const taskCycleIds = record.payload.cycleIds || [];
4895
+ const updatedTask = {
4896
+ ...record.payload,
4897
+ cycleIds: taskCycleIds.filter((id) => id !== cycleId)
4898
+ };
4899
+ return await this.identity.signRecord(
4900
+ { ...record, payload: updatedTask },
4901
+ currentActor.id,
4902
+ "author"
4903
+ );
4904
+ })
4905
+ );
4906
+ await Promise.all([
4907
+ this.cycleStore.write(signedCycleRecord),
4908
+ ...signedTaskRecords.map(
4909
+ (signedTask) => this.taskStore.write(signedTask)
4910
+ )
4911
+ ]);
4912
+ }
4913
+ /**
4914
+ * Moves multiple tasks from one cycle to another atomically
4915
+ * Provides transactional semantics - all tasks move or none do
4916
+ * All business logic and validation happens here in the adapter
4917
+ */
4918
+ async moveTasksBetweenCycles(targetCycleId, taskIds, sourceCycleId) {
4919
+ if (!sourceCycleId || typeof sourceCycleId !== "string") {
4920
+ throw new Error("ValidationError: sourceCycleId must be a non-empty string");
4921
+ }
4922
+ if (!targetCycleId || typeof targetCycleId !== "string") {
4923
+ throw new Error("ValidationError: targetCycleId must be a non-empty string");
4924
+ }
4925
+ if (!Array.isArray(taskIds) || taskIds.length === 0) {
4926
+ throw new Error("ValidationError: taskIds must be a non-empty array");
4927
+ }
4928
+ if (sourceCycleId === targetCycleId) {
4929
+ throw new Error("ValidationError: Source and target cycles must be different");
4930
+ }
4931
+ const [sourceCycleRecord, targetCycleRecord] = await Promise.all([
4932
+ this.cycleStore.read(sourceCycleId),
4933
+ this.cycleStore.read(targetCycleId)
4934
+ ]);
4935
+ if (!sourceCycleRecord) {
4936
+ throw new Error(`RecordNotFoundError: Source cycle not found: ${sourceCycleId}`);
4937
+ }
4938
+ if (!targetCycleRecord) {
4939
+ throw new Error(`RecordNotFoundError: Target cycle not found: ${targetCycleId}`);
4940
+ }
4941
+ const taskRecords = await Promise.all(
4942
+ taskIds.map(async (taskId) => {
4943
+ const taskRecord = await this.taskStore.read(taskId);
4944
+ if (!taskRecord) {
4945
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4946
+ }
4947
+ return { taskId, record: taskRecord };
4948
+ })
4949
+ );
4950
+ const sourceTaskIds = sourceCycleRecord.payload.taskIds || [];
4951
+ const notLinkedTasks = taskIds.filter((taskId) => !sourceTaskIds.includes(taskId));
4952
+ if (notLinkedTasks.length > 0) {
4953
+ throw new Error(`ValidationError: Tasks not linked to source cycle ${sourceCycleId}: ${notLinkedTasks.join(", ")}`);
4954
+ }
4955
+ const updatedSourceCycle = {
4956
+ ...sourceCycleRecord.payload,
4957
+ taskIds: sourceTaskIds.filter((id) => !taskIds.includes(id))
4958
+ };
4959
+ const updatedTargetCycle = {
4960
+ ...targetCycleRecord.payload,
4961
+ taskIds: [...targetCycleRecord.payload.taskIds || [], ...taskIds]
4962
+ };
4963
+ const currentActor = await this.identity.getCurrentActor();
4964
+ const [signedSourceCycle, signedTargetCycle] = await Promise.all([
4965
+ this.identity.signRecord(
4966
+ { ...sourceCycleRecord, payload: updatedSourceCycle },
4967
+ currentActor.id,
4968
+ "author"
4969
+ ),
4970
+ this.identity.signRecord(
4971
+ { ...targetCycleRecord, payload: updatedTargetCycle },
4972
+ currentActor.id,
4973
+ "author"
4974
+ )
4975
+ ]);
4976
+ const signedTaskRecords = await Promise.all(
4977
+ taskRecords.map(async ({ record }) => {
4978
+ const taskCycleIds = record.payload.cycleIds || [];
4979
+ const updatedTask = {
4980
+ ...record.payload,
4981
+ cycleIds: taskCycleIds.filter((id) => id !== sourceCycleId).concat(targetCycleId)
4982
+ // Add target
4983
+ };
4984
+ return await this.identity.signRecord(
4985
+ { ...record, payload: updatedTask },
4986
+ currentActor.id,
4987
+ "author"
4988
+ );
4989
+ })
4990
+ );
4991
+ try {
4992
+ await Promise.all([
4993
+ this.cycleStore.write(signedSourceCycle),
4994
+ this.cycleStore.write(signedTargetCycle),
4995
+ ...signedTaskRecords.map(
4996
+ (signedTask) => this.taskStore.write(signedTask)
4997
+ )
4998
+ ]);
4999
+ } catch (error) {
5000
+ throw new Error(`AtomicOperationError: Failed to move tasks between cycles: ${error instanceof Error ? error.message : "Unknown error"}`);
5001
+ }
5002
+ }
4804
5003
  // TODO: Implement when lint_command.md is implemented
4805
5004
  async lint() {
4806
5005
  throw new Error("NotImplementedError: lint() will be implemented when lint_command.md is ready");
@@ -5442,7 +5641,18 @@ var ProjectAdapter = class {
5442
5641
  {
5443
5642
  type: "human",
5444
5643
  displayName: options.actorName || "Project Owner",
5445
- roles: ["admin", "author"]
5644
+ roles: [
5645
+ "admin",
5646
+ // Platform admin (future use)
5647
+ "author",
5648
+ // Create & submit tasks
5649
+ "approver:product",
5650
+ // Approve tasks (product decisions)
5651
+ "approver:quality",
5652
+ // Complete tasks (quality validation)
5653
+ "developer"
5654
+ // General development work
5655
+ ]
5446
5656
  },
5447
5657
  "bootstrap"
5448
5658
  );
@@ -5848,7 +6058,8 @@ var workflow_methodology_default_default = {
5848
6058
  },
5849
6059
  active: {
5850
6060
  from: [
5851
- "ready"
6061
+ "ready",
6062
+ "paused"
5852
6063
  ],
5853
6064
  requires: {
5854
6065
  event: "first_execution_record_created",
@@ -6018,7 +6229,8 @@ var workflow_methodology_scrum_default = {
6018
6229
  },
6019
6230
  active: {
6020
6231
  from: [
6021
- "ready"
6232
+ "ready",
6233
+ "paused"
6022
6234
  ],
6023
6235
  requires: {
6024
6236
  event: "sprint_started",
@@ -7714,8 +7926,8 @@ var DiagramGenerator = class {
7714
7926
  /**
7715
7927
  * Primary API - Performance optimized with caching
7716
7928
  */
7717
- async generateFromRecords(cycles, tasks, filters) {
7718
- const cacheKey = this.generateCacheKey(cycles, tasks);
7929
+ async generateFromRecords(cycles, tasks, filters, showArchived = false) {
7930
+ const cacheKey = this.generateCacheKey(cycles, tasks, showArchived);
7719
7931
  if (this.cache.has(cacheKey)) {
7720
7932
  this.metrics.incrementCacheHits();
7721
7933
  return this.renderFromCache(cacheKey);
@@ -7724,8 +7936,12 @@ var DiagramGenerator = class {
7724
7936
  try {
7725
7937
  let finalCycles = cycles;
7726
7938
  let finalTasks = tasks;
7939
+ if (!showArchived) {
7940
+ finalCycles = cycles.filter((cycle) => cycle.status !== "archived");
7941
+ finalTasks = tasks.filter((task) => task.status !== "archived");
7942
+ }
7727
7943
  if (filters && (filters.cycleId || filters.taskId || filters.packageName)) {
7728
- const filtered = this.analyzer.filterEntities(cycles, tasks, filters);
7944
+ const filtered = this.analyzer.filterEntities(finalCycles, finalTasks, filters);
7729
7945
  finalCycles = filtered.filteredCycles;
7730
7946
  finalTasks = filtered.filteredTasks;
7731
7947
  }
@@ -7743,10 +7959,10 @@ var DiagramGenerator = class {
7743
7959
  /**
7744
7960
  * Convenience method to generate from .gitgov/ directory
7745
7961
  */
7746
- async generateFromFiles(gitgovPath = ".gitgov", filters) {
7962
+ async generateFromFiles(gitgovPath = ".gitgov", filters, showArchived = false) {
7747
7963
  const cycles = await this.loadCycleRecords(gitgovPath);
7748
7964
  const tasks = await this.loadTaskRecords(gitgovPath);
7749
- return this.generateFromRecords(cycles, tasks, filters);
7965
+ return this.generateFromRecords(cycles, tasks, filters, showArchived);
7750
7966
  }
7751
7967
  /**
7752
7968
  * Loads all cycle records from the filesystem
@@ -7830,13 +8046,14 @@ var DiagramGenerator = class {
7830
8046
  /**
7831
8047
  * Generates cache key for efficient lookups
7832
8048
  */
7833
- generateCacheKey(cycles, tasks) {
8049
+ generateCacheKey(cycles, tasks, showArchived = false) {
7834
8050
  const cycleIds = [...new Set(cycles.map((c) => c.id))].sort();
7835
8051
  const taskIds = [...new Set(tasks.map((t) => t.id))].sort();
7836
8052
  const cycleHash = this.hashArray(cycleIds);
7837
8053
  const taskHash = this.hashArray(taskIds);
7838
8054
  const optionsHash = this.hashString(JSON.stringify(this.options));
7839
- return `diagram:${cycleHash}-${taskHash}-${optionsHash}`;
8055
+ const archivedFlag = showArchived ? "with-archived" : "no-archived";
8056
+ return `diagram:${cycleHash}-${taskHash}-${optionsHash}-${archivedFlag}`;
7840
8057
  }
7841
8058
  /**
7842
8059
  * Efficient hash function for arrays