@gitgov/core 1.0.2 → 1.2.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,13 +1763,17 @@ 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>;
1769
+ deleteTask(taskId: string, actorId: string): Promise<void>;
1768
1770
  createCycle(payload: Partial<CycleRecord>, actorId: string): Promise<CycleRecord>;
1769
1771
  getCycle(cycleId: string): Promise<CycleRecord | null>;
1770
1772
  getAllCycles(): Promise<CycleRecord[]>;
1771
1773
  updateCycle(cycleId: string, payload: Partial<CycleRecord>): Promise<CycleRecord>;
1772
1774
  addTaskToCycle(cycleId: string, taskId: string): Promise<void>;
1775
+ removeTasksFromCycle(cycleId: string, taskIds: string[]): Promise<void>;
1776
+ moveTasksBetweenCycles(targetCycleId: string, taskIds: string[], sourceCycleId: string): Promise<void>;
1773
1777
  getTasksAssignedToActor(actorId: string): Promise<TaskRecord[]>;
1774
1778
  handleFeedbackCreated(event: FeedbackCreatedEvent): Promise<void>;
1775
1779
  handleFeedbackResolved(event: FeedbackStatusChangedEvent): Promise<void>;
@@ -1845,6 +1849,10 @@ declare class BacklogAdapter implements IBacklogAdapter {
1845
1849
  * Activates a task transitioning from ready to active with permission validation
1846
1850
  */
1847
1851
  activateTask(taskId: string, actorId: string): Promise<TaskRecord>;
1852
+ /**
1853
+ * Pauses a task manually transitioning from active to paused with optional reason
1854
+ */
1855
+ pauseTask(taskId: string, actorId: string, reason?: string): Promise<TaskRecord>;
1848
1856
  /**
1849
1857
  * Resumes a paused task transitioning back to active with optional force override
1850
1858
  */
@@ -1858,6 +1866,11 @@ declare class BacklogAdapter implements IBacklogAdapter {
1858
1866
  * Supports both cancellation (ready/active) and rejection (review) operations
1859
1867
  */
1860
1868
  discardTask(taskId: string, actorId: string, reason?: string): Promise<TaskRecord>;
1869
+ /**
1870
+ * Deletes a draft task completely (no discarded state)
1871
+ * Only works for tasks in 'draft' status that never entered formal workflow
1872
+ */
1873
+ deleteTask(taskId: string, actorId: string): Promise<void>;
1861
1874
  /**
1862
1875
  * Updates a task with new payload
1863
1876
  */
@@ -1926,6 +1939,17 @@ declare class BacklogAdapter implements IBacklogAdapter {
1926
1939
  * Creates bidirectional link between task and cycle
1927
1940
  */
1928
1941
  addTaskToCycle(cycleId: string, taskId: string): Promise<void>;
1942
+ /**
1943
+ * Removes multiple tasks from a cycle with bidirectional unlinking
1944
+ * All business logic and validation happens here in the adapter
1945
+ */
1946
+ removeTasksFromCycle(cycleId: string, taskIds: string[]): Promise<void>;
1947
+ /**
1948
+ * Moves multiple tasks from one cycle to another atomically
1949
+ * Provides transactional semantics - all tasks move or none do
1950
+ * All business logic and validation happens here in the adapter
1951
+ */
1952
+ moveTasksBetweenCycles(targetCycleId: string, taskIds: string[], sourceCycleId: string): Promise<void>;
1929
1953
  lint(): Promise<LintReport>;
1930
1954
  audit(): Promise<AuditReport>;
1931
1955
  processChanges(_changes: unknown[]): Promise<ExecutionRecord[]>;
@@ -5405,7 +5429,7 @@ declare class DiagramGenerator {
5405
5429
  cycleId?: string;
5406
5430
  taskId?: string;
5407
5431
  packageName?: string;
5408
- }): Promise<string>;
5432
+ }, showArchived?: boolean): Promise<string>;
5409
5433
  /**
5410
5434
  * Convenience method to generate from .gitgov/ directory
5411
5435
  */
@@ -5413,7 +5437,7 @@ declare class DiagramGenerator {
5413
5437
  cycleId?: string;
5414
5438
  taskId?: string;
5415
5439
  packageName?: string;
5416
- }): Promise<string>;
5440
+ }, showArchived?: boolean): Promise<string>;
5417
5441
  /**
5418
5442
  * Loads all cycle records from the filesystem
5419
5443
  */
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
  */
@@ -4334,6 +4383,9 @@ var BacklogAdapter = class {
4334
4383
  const task = taskRecord.payload;
4335
4384
  const actor = await this.getActor(actorId);
4336
4385
  if (!["ready", "active", "review"].includes(task.status)) {
4386
+ if (task.status === "draft") {
4387
+ throw new Error(`ProtocolViolationError: Cannot cancel task in 'draft' state. Use 'gitgov task delete ${taskId}' to remove draft tasks.`);
4388
+ }
4337
4389
  throw new Error(`ProtocolViolationError: Task is in '${task.status}' state. Cannot cancel from this state. Only 'ready', 'active', and 'review' tasks can be cancelled.`);
4338
4390
  }
4339
4391
  const context = {
@@ -4372,6 +4424,39 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
4372
4424
  });
4373
4425
  return updatedPayload;
4374
4426
  }
4427
+ /**
4428
+ * Deletes a draft task completely (no discarded state)
4429
+ * Only works for tasks in 'draft' status that never entered formal workflow
4430
+ */
4431
+ async deleteTask(taskId, actorId) {
4432
+ const taskRecord = await this.taskStore.read(taskId);
4433
+ if (!taskRecord) {
4434
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4435
+ }
4436
+ const task = taskRecord.payload;
4437
+ if (task.status !== "draft") {
4438
+ if (task.status === "review") {
4439
+ throw new Error(`ProtocolViolationError: Cannot delete task in 'review' state. Use 'gitgov task reject ${taskId}' to discard tasks under review.`);
4440
+ } else if (task.status === "ready" || task.status === "active") {
4441
+ throw new Error(`ProtocolViolationError: Cannot delete task in '${task.status}' state. Use 'gitgov task cancel ${taskId}' to discard tasks from ready/active states.`);
4442
+ }
4443
+ throw new Error(`ProtocolViolationError: Cannot delete task in '${task.status}' state. Only draft tasks can be deleted.`);
4444
+ }
4445
+ await this.getActor(actorId);
4446
+ await this.taskStore.delete(taskId);
4447
+ this.eventBus.publish({
4448
+ type: "task.status.changed",
4449
+ timestamp: Date.now(),
4450
+ source: "backlog_adapter",
4451
+ payload: {
4452
+ taskId,
4453
+ oldStatus: "draft",
4454
+ newStatus: "deleted",
4455
+ actorId,
4456
+ reason: "Draft task deleted"
4457
+ }
4458
+ });
4459
+ }
4375
4460
  /**
4376
4461
  * Updates a task with new payload
4377
4462
  */
@@ -4801,6 +4886,156 @@ ${task.status === "review" ? "[REJECTED]" : "[CANCELLED]"} ${reason} (${(/* @__P
4801
4886
  this.taskStore.write(signedTaskRecord)
4802
4887
  ]);
4803
4888
  }
4889
+ /**
4890
+ * Removes multiple tasks from a cycle with bidirectional unlinking
4891
+ * All business logic and validation happens here in the adapter
4892
+ */
4893
+ async removeTasksFromCycle(cycleId, taskIds) {
4894
+ if (!cycleId || typeof cycleId !== "string") {
4895
+ throw new Error("ValidationError: cycleId must be a non-empty string");
4896
+ }
4897
+ if (!Array.isArray(taskIds) || taskIds.length === 0) {
4898
+ throw new Error("ValidationError: taskIds must be a non-empty array");
4899
+ }
4900
+ const cycleRecord = await this.cycleStore.read(cycleId);
4901
+ if (!cycleRecord) {
4902
+ throw new Error(`RecordNotFoundError: Cycle not found: ${cycleId}`);
4903
+ }
4904
+ const taskRecords = await Promise.all(
4905
+ taskIds.map(async (taskId) => {
4906
+ const taskRecord = await this.taskStore.read(taskId);
4907
+ if (!taskRecord) {
4908
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4909
+ }
4910
+ return { taskId, record: taskRecord };
4911
+ })
4912
+ );
4913
+ const cycleTaskIds = cycleRecord.payload.taskIds || [];
4914
+ const notLinkedTasks = taskIds.filter((taskId) => !cycleTaskIds.includes(taskId));
4915
+ if (notLinkedTasks.length > 0) {
4916
+ throw new Error(`ValidationError: Tasks not linked to cycle ${cycleId}: ${notLinkedTasks.join(", ")}`);
4917
+ }
4918
+ const updatedCycle = {
4919
+ ...cycleRecord.payload,
4920
+ taskIds: cycleTaskIds.filter((id) => !taskIds.includes(id))
4921
+ };
4922
+ const currentActor = await this.identity.getCurrentActor();
4923
+ const signedCycleRecord = await this.identity.signRecord(
4924
+ { ...cycleRecord, payload: updatedCycle },
4925
+ currentActor.id,
4926
+ "author"
4927
+ );
4928
+ const signedTaskRecords = await Promise.all(
4929
+ taskRecords.map(async ({ record }) => {
4930
+ const taskCycleIds = record.payload.cycleIds || [];
4931
+ const updatedTask = {
4932
+ ...record.payload,
4933
+ cycleIds: taskCycleIds.filter((id) => id !== cycleId)
4934
+ };
4935
+ return await this.identity.signRecord(
4936
+ { ...record, payload: updatedTask },
4937
+ currentActor.id,
4938
+ "author"
4939
+ );
4940
+ })
4941
+ );
4942
+ await Promise.all([
4943
+ this.cycleStore.write(signedCycleRecord),
4944
+ ...signedTaskRecords.map(
4945
+ (signedTask) => this.taskStore.write(signedTask)
4946
+ )
4947
+ ]);
4948
+ }
4949
+ /**
4950
+ * Moves multiple tasks from one cycle to another atomically
4951
+ * Provides transactional semantics - all tasks move or none do
4952
+ * All business logic and validation happens here in the adapter
4953
+ */
4954
+ async moveTasksBetweenCycles(targetCycleId, taskIds, sourceCycleId) {
4955
+ if (!sourceCycleId || typeof sourceCycleId !== "string") {
4956
+ throw new Error("ValidationError: sourceCycleId must be a non-empty string");
4957
+ }
4958
+ if (!targetCycleId || typeof targetCycleId !== "string") {
4959
+ throw new Error("ValidationError: targetCycleId must be a non-empty string");
4960
+ }
4961
+ if (!Array.isArray(taskIds) || taskIds.length === 0) {
4962
+ throw new Error("ValidationError: taskIds must be a non-empty array");
4963
+ }
4964
+ if (sourceCycleId === targetCycleId) {
4965
+ throw new Error("ValidationError: Source and target cycles must be different");
4966
+ }
4967
+ const [sourceCycleRecord, targetCycleRecord] = await Promise.all([
4968
+ this.cycleStore.read(sourceCycleId),
4969
+ this.cycleStore.read(targetCycleId)
4970
+ ]);
4971
+ if (!sourceCycleRecord) {
4972
+ throw new Error(`RecordNotFoundError: Source cycle not found: ${sourceCycleId}`);
4973
+ }
4974
+ if (!targetCycleRecord) {
4975
+ throw new Error(`RecordNotFoundError: Target cycle not found: ${targetCycleId}`);
4976
+ }
4977
+ const taskRecords = await Promise.all(
4978
+ taskIds.map(async (taskId) => {
4979
+ const taskRecord = await this.taskStore.read(taskId);
4980
+ if (!taskRecord) {
4981
+ throw new Error(`RecordNotFoundError: Task not found: ${taskId}`);
4982
+ }
4983
+ return { taskId, record: taskRecord };
4984
+ })
4985
+ );
4986
+ const sourceTaskIds = sourceCycleRecord.payload.taskIds || [];
4987
+ const notLinkedTasks = taskIds.filter((taskId) => !sourceTaskIds.includes(taskId));
4988
+ if (notLinkedTasks.length > 0) {
4989
+ throw new Error(`ValidationError: Tasks not linked to source cycle ${sourceCycleId}: ${notLinkedTasks.join(", ")}`);
4990
+ }
4991
+ const updatedSourceCycle = {
4992
+ ...sourceCycleRecord.payload,
4993
+ taskIds: sourceTaskIds.filter((id) => !taskIds.includes(id))
4994
+ };
4995
+ const updatedTargetCycle = {
4996
+ ...targetCycleRecord.payload,
4997
+ taskIds: [...targetCycleRecord.payload.taskIds || [], ...taskIds]
4998
+ };
4999
+ const currentActor = await this.identity.getCurrentActor();
5000
+ const [signedSourceCycle, signedTargetCycle] = await Promise.all([
5001
+ this.identity.signRecord(
5002
+ { ...sourceCycleRecord, payload: updatedSourceCycle },
5003
+ currentActor.id,
5004
+ "author"
5005
+ ),
5006
+ this.identity.signRecord(
5007
+ { ...targetCycleRecord, payload: updatedTargetCycle },
5008
+ currentActor.id,
5009
+ "author"
5010
+ )
5011
+ ]);
5012
+ const signedTaskRecords = await Promise.all(
5013
+ taskRecords.map(async ({ record }) => {
5014
+ const taskCycleIds = record.payload.cycleIds || [];
5015
+ const updatedTask = {
5016
+ ...record.payload,
5017
+ cycleIds: taskCycleIds.filter((id) => id !== sourceCycleId).concat(targetCycleId)
5018
+ // Add target
5019
+ };
5020
+ return await this.identity.signRecord(
5021
+ { ...record, payload: updatedTask },
5022
+ currentActor.id,
5023
+ "author"
5024
+ );
5025
+ })
5026
+ );
5027
+ try {
5028
+ await Promise.all([
5029
+ this.cycleStore.write(signedSourceCycle),
5030
+ this.cycleStore.write(signedTargetCycle),
5031
+ ...signedTaskRecords.map(
5032
+ (signedTask) => this.taskStore.write(signedTask)
5033
+ )
5034
+ ]);
5035
+ } catch (error) {
5036
+ throw new Error(`AtomicOperationError: Failed to move tasks between cycles: ${error instanceof Error ? error.message : "Unknown error"}`);
5037
+ }
5038
+ }
4804
5039
  // TODO: Implement when lint_command.md is implemented
4805
5040
  async lint() {
4806
5041
  throw new Error("NotImplementedError: lint() will be implemented when lint_command.md is ready");
@@ -5442,7 +5677,18 @@ var ProjectAdapter = class {
5442
5677
  {
5443
5678
  type: "human",
5444
5679
  displayName: options.actorName || "Project Owner",
5445
- roles: ["admin", "author"]
5680
+ roles: [
5681
+ "admin",
5682
+ // Platform admin (future use)
5683
+ "author",
5684
+ // Create & submit tasks
5685
+ "approver:product",
5686
+ // Approve tasks (product decisions)
5687
+ "approver:quality",
5688
+ // Complete tasks (quality validation)
5689
+ "developer"
5690
+ // General development work
5691
+ ]
5446
5692
  },
5447
5693
  "bootstrap"
5448
5694
  );
@@ -5848,7 +6094,8 @@ var workflow_methodology_default_default = {
5848
6094
  },
5849
6095
  active: {
5850
6096
  from: [
5851
- "ready"
6097
+ "ready",
6098
+ "paused"
5852
6099
  ],
5853
6100
  requires: {
5854
6101
  event: "first_execution_record_created",
@@ -6018,7 +6265,8 @@ var workflow_methodology_scrum_default = {
6018
6265
  },
6019
6266
  active: {
6020
6267
  from: [
6021
- "ready"
6268
+ "ready",
6269
+ "paused"
6022
6270
  ],
6023
6271
  requires: {
6024
6272
  event: "sprint_started",
@@ -7714,8 +7962,8 @@ var DiagramGenerator = class {
7714
7962
  /**
7715
7963
  * Primary API - Performance optimized with caching
7716
7964
  */
7717
- async generateFromRecords(cycles, tasks, filters) {
7718
- const cacheKey = this.generateCacheKey(cycles, tasks);
7965
+ async generateFromRecords(cycles, tasks, filters, showArchived = false) {
7966
+ const cacheKey = this.generateCacheKey(cycles, tasks, showArchived);
7719
7967
  if (this.cache.has(cacheKey)) {
7720
7968
  this.metrics.incrementCacheHits();
7721
7969
  return this.renderFromCache(cacheKey);
@@ -7724,8 +7972,12 @@ var DiagramGenerator = class {
7724
7972
  try {
7725
7973
  let finalCycles = cycles;
7726
7974
  let finalTasks = tasks;
7975
+ if (!showArchived) {
7976
+ finalCycles = cycles.filter((cycle) => cycle.status !== "archived");
7977
+ finalTasks = tasks.filter((task) => task.status !== "archived");
7978
+ }
7727
7979
  if (filters && (filters.cycleId || filters.taskId || filters.packageName)) {
7728
- const filtered = this.analyzer.filterEntities(cycles, tasks, filters);
7980
+ const filtered = this.analyzer.filterEntities(finalCycles, finalTasks, filters);
7729
7981
  finalCycles = filtered.filteredCycles;
7730
7982
  finalTasks = filtered.filteredTasks;
7731
7983
  }
@@ -7743,10 +7995,10 @@ var DiagramGenerator = class {
7743
7995
  /**
7744
7996
  * Convenience method to generate from .gitgov/ directory
7745
7997
  */
7746
- async generateFromFiles(gitgovPath = ".gitgov", filters) {
7998
+ async generateFromFiles(gitgovPath = ".gitgov", filters, showArchived = false) {
7747
7999
  const cycles = await this.loadCycleRecords(gitgovPath);
7748
8000
  const tasks = await this.loadTaskRecords(gitgovPath);
7749
- return this.generateFromRecords(cycles, tasks, filters);
8001
+ return this.generateFromRecords(cycles, tasks, filters, showArchived);
7750
8002
  }
7751
8003
  /**
7752
8004
  * Loads all cycle records from the filesystem
@@ -7830,13 +8082,14 @@ var DiagramGenerator = class {
7830
8082
  /**
7831
8083
  * Generates cache key for efficient lookups
7832
8084
  */
7833
- generateCacheKey(cycles, tasks) {
8085
+ generateCacheKey(cycles, tasks, showArchived = false) {
7834
8086
  const cycleIds = [...new Set(cycles.map((c) => c.id))].sort();
7835
8087
  const taskIds = [...new Set(tasks.map((t) => t.id))].sort();
7836
8088
  const cycleHash = this.hashArray(cycleIds);
7837
8089
  const taskHash = this.hashArray(taskIds);
7838
8090
  const optionsHash = this.hashString(JSON.stringify(this.options));
7839
- return `diagram:${cycleHash}-${taskHash}-${optionsHash}`;
8091
+ const archivedFlag = showArchived ? "with-archived" : "no-archived";
8092
+ return `diagram:${cycleHash}-${taskHash}-${optionsHash}-${archivedFlag}`;
7840
8093
  }
7841
8094
  /**
7842
8095
  * Efficient hash function for arrays