diff --git a/readiness/2907-blocked-by-set-mutations/analysis.json b/readiness/2907-blocked-by-set-mutations/analysis.json
new file mode 100644
index 000000000..3a599cdeb
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/analysis.json
@@ -0,0 +1,592 @@
+{
+ "schemaVersion": 1,
+ "viewVersion": "1.0",
+ "workId": "2907-blocked-by-set-mutations",
+ "stage": "analyze",
+ "status": "implementationReady",
+ "generator": "FS.GG.SDD.Artifacts/1.2.5",
+ "sources": [
+ {
+ "path": ".fsgg/agents.yml",
+ "kind": "agentsConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "c67035b1cf31b6fa802c10f761eeb1e5bf5d198c38d159b54fac299957b5aac3"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/project.yml",
+ "kind": "projectConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ef78f002b33ce22f738bac2c0dbf716db729a7d3ae3e4a68a4ce4b93810ced66"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/sdd.yml",
+ "kind": "sddConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "fe5a7c270a975153e80ab0d08a610c9b302585dcb0b5f7c654841ceced9272cb"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "db56994d8c948ccf24af6a447846e8059252e8a02782bd3d3fa4b669d676d176"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/checklist.md",
+ "kind": "checklist",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "kind": "clarification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/plan.md",
+ "kind": "plan",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/spec.md",
+ "kind": "specification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "kind": "tasks",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ }
+ ],
+ "sourceRelationships": [
+ {
+ "id": "AR001",
+ "sourcePath": "work/2907-blocked-by-set-mutations/checklist.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/plan.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "planSourceChecklist",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR002",
+ "sourcePath": "work/2907-blocked-by-set-mutations/checklist.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "taskSourceChecklist",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR003",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/checklist.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "checklistSourceClarifications",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR004",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "DEC-001",
+ "targetId": null,
+ "relationship": "clarificationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR005",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "DEC-002",
+ "targetId": null,
+ "relationship": "clarificationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR006",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "DEC-003",
+ "targetId": null,
+ "relationship": "clarificationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR007",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "DEC-004",
+ "targetId": null,
+ "relationship": "clarificationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR008",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/plan.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "planSourceClarifications",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR009",
+ "sourcePath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "taskSourceClarifications",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR010",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PC-001",
+ "targetId": null,
+ "relationship": "contractDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR011",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "GV-001",
+ "targetId": null,
+ "relationship": "generatedViewDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR012",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PM-001",
+ "targetId": null,
+ "relationship": "migrationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR013",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-001",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR014",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-002",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR015",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-003",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR016",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-004",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR017",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-005",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR018",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "PD-006",
+ "targetId": null,
+ "relationship": "planDecisionDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR019",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "taskSourcePlan",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR020",
+ "sourcePath": "work/2907-blocked-by-set-mutations/plan.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "VO-001",
+ "targetId": null,
+ "relationship": "verificationDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR021",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-001",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR022",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-002",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR023",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-003",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR024",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-004",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR025",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-005",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR026",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "AC-006",
+ "targetId": null,
+ "relationship": "acceptanceDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR027",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/checklist.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "checklistSourceSpec",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR028",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/plan.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "planSourceSpec",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR029",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-001",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR030",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-002",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR031",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-003",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR032",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-004",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR033",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-005",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR034",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "FR-006",
+ "targetId": null,
+ "relationship": "requirementDisposition",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR035",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "sourceSpec",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR036",
+ "sourcePath": "work/2907-blocked-by-set-mutations/spec.md",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": null,
+ "targetId": null,
+ "relationship": "taskSourceSpec",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR037",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T007",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR038",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T008",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR039",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T009",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR040",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T010",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR041",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T011",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR042",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T012",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR043",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T013",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ },
+ {
+ "id": "AR044",
+ "sourcePath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "targetPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceId": "T014",
+ "targetId": "T001",
+ "relationship": "taskDependency",
+ "state": "current",
+ "diagnosticIds": []
+ }
+ ],
+ "readiness": {
+ "status": "implementationReady",
+ "readyCount": 44,
+ "advisoryCount": 0,
+ "warningCount": 0,
+ "blockingCount": 0,
+ "staleSourceCount": 0,
+ "missingDispositionCount": 0,
+ "malformedSourceCount": 0,
+ "generatedViewFindingCount": 0,
+ "acceptedDeferralCount": 0
+ },
+ "findings": [],
+ "generatedViews": [
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "currency": "current",
+ "diagnosticIds": []
+ }
+ ],
+ "optionalBoundaryFacts": [
+ {
+ "path": ".fsgg/policy.yml",
+ "relationship": "optionalGovernancePolicy",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/capabilities.yml",
+ "relationship": "optionalGovernanceCapabilities",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/tooling.yml",
+ "relationship": "optionalGovernanceTooling",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ }
+ ],
+ "diagnostics": [],
+ "nextAction": {
+ "actionId": "analysis.next.implement",
+ "command": null,
+ "reason": "Lifecycle sources are current and ready for implementation."
+ }
+}
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/governance-handoff.json b/readiness/2907-blocked-by-set-mutations/governance-handoff.json
new file mode 100644
index 000000000..8082f117e
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/governance-handoff.json
@@ -0,0 +1,347 @@
+{
+ "schemaVersion": 1,
+ "contractVersion": "2.0.0",
+ "generatorVersion": "FS.GG.SDD.Artifacts/1.2.5",
+ "workId": "2907-blocked-by-set-mutations",
+ "sources": [
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/ship.json",
+ "digest": "sha256:15ccda01a5e111e480f6f043942874794f5c2d481f8e8b197862f2c0c23fac98",
+ "schemaVersion": 1
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/verify.json",
+ "digest": "sha256:3c01ddc13ad6bff9bbecf40db062b2122ca967a695b8496fffe3f706fc4ac886",
+ "schemaVersion": 1
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "digest": "sha256:db56994d8c948ccf24af6a447846e8059252e8a02782bd3d3fa4b669d676d176",
+ "schemaVersion": 1
+ }
+ ],
+ "evidence": {
+ "nodes": [
+ {
+ "id": "evidence:EV001",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV002",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV003",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV004",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV005",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV006",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV007",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV008",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV009",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV010",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV011",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV012",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV013",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "evidence:EV014",
+ "state": "real",
+ "rationale": null
+ },
+ {
+ "id": "task:T001",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T002",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T003",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T004",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T005",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T006",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T007",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T008",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T009",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T010",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T011",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T012",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T013",
+ "state": "pending",
+ "rationale": null
+ },
+ {
+ "id": "task:T014",
+ "state": "pending",
+ "rationale": null
+ }
+ ],
+ "dependencies": [
+ {
+ "dependent": "evidence:EV001",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "evidence:EV002",
+ "dependency": "task:T002"
+ },
+ {
+ "dependent": "evidence:EV003",
+ "dependency": "task:T003"
+ },
+ {
+ "dependent": "evidence:EV004",
+ "dependency": "task:T004"
+ },
+ {
+ "dependent": "evidence:EV005",
+ "dependency": "task:T005"
+ },
+ {
+ "dependent": "evidence:EV006",
+ "dependency": "task:T006"
+ },
+ {
+ "dependent": "evidence:EV007",
+ "dependency": "task:T007"
+ },
+ {
+ "dependent": "evidence:EV008",
+ "dependency": "task:T008"
+ },
+ {
+ "dependent": "evidence:EV009",
+ "dependency": "task:T009"
+ },
+ {
+ "dependent": "evidence:EV010",
+ "dependency": "task:T010"
+ },
+ {
+ "dependent": "evidence:EV011",
+ "dependency": "task:T011"
+ },
+ {
+ "dependent": "evidence:EV012",
+ "dependency": "task:T012"
+ },
+ {
+ "dependent": "evidence:EV013",
+ "dependency": "task:T013"
+ },
+ {
+ "dependent": "evidence:EV014",
+ "dependency": "task:T014"
+ },
+ {
+ "dependent": "task:T001",
+ "dependency": "evidence:EV001"
+ },
+ {
+ "dependent": "task:T002",
+ "dependency": "evidence:EV002"
+ },
+ {
+ "dependent": "task:T003",
+ "dependency": "evidence:EV003"
+ },
+ {
+ "dependent": "task:T004",
+ "dependency": "evidence:EV004"
+ },
+ {
+ "dependent": "task:T005",
+ "dependency": "evidence:EV005"
+ },
+ {
+ "dependent": "task:T006",
+ "dependency": "evidence:EV006"
+ },
+ {
+ "dependent": "task:T007",
+ "dependency": "evidence:EV007"
+ },
+ {
+ "dependent": "task:T007",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T008",
+ "dependency": "evidence:EV008"
+ },
+ {
+ "dependent": "task:T008",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T009",
+ "dependency": "evidence:EV009"
+ },
+ {
+ "dependent": "task:T009",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T010",
+ "dependency": "evidence:EV010"
+ },
+ {
+ "dependent": "task:T010",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T011",
+ "dependency": "evidence:EV011"
+ },
+ {
+ "dependent": "task:T011",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T012",
+ "dependency": "evidence:EV012"
+ },
+ {
+ "dependent": "task:T012",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T013",
+ "dependency": "evidence:EV013"
+ },
+ {
+ "dependent": "task:T013",
+ "dependency": "task:T001"
+ },
+ {
+ "dependent": "task:T014",
+ "dependency": "evidence:EV014"
+ },
+ {
+ "dependent": "task:T014",
+ "dependency": "task:T001"
+ }
+ ]
+ },
+ "performanceEvidence": [],
+ "governedReferences": [],
+ "governanceConfig": {
+ "policyPresent": false,
+ "capabilitiesPresent": false,
+ "toolingPresent": false
+ },
+ "readiness": {
+ "shipDisposition": "shipReady",
+ "verificationReadiness": "verificationReady",
+ "counts": {
+ "advisory": 0,
+ "warning": 0,
+ "blocking": 0,
+ "classifiedObligationsUnmet": 0,
+ "journeyObligationsUnmet": 0
+ },
+ "blockingDiagnosticIds": [],
+ "perViewState": [
+ {
+ "view": "ship.json",
+ "state": "current"
+ },
+ {
+ "view": "verify.json",
+ "state": "current"
+ },
+ {
+ "view": "work-model.json",
+ "state": "current"
+ }
+ ]
+ },
+ "diagnostics": []
+}
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/ship-verdict.json b/readiness/2907-blocked-by-set-mutations/ship-verdict.json
new file mode 100644
index 000000000..c81449636
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/ship-verdict.json
@@ -0,0 +1,23 @@
+{
+ "schemaVersion": 1,
+ "viewVersion": "1.0",
+ "workId": "2907-blocked-by-set-mutations",
+ "stage": "ship",
+ "status": "shipReady",
+ "generator": "FS.GG.SDD.Artifacts/1.2.5",
+ "sourcesDigest": {
+ "algorithm": "sha256",
+ "value": "00f1473b9a7b16b75f2b89e5acb81a50061e65d74344e37497e3416a8adabe1c"
+ },
+ "verificationReadiness": {
+ "status": "verificationReady",
+ "evidenceSupportedCount": 14,
+ "evidenceSelfAttestedCount": 0,
+ "evidenceObservedCount": 14
+ },
+ "disposition": {
+ "state": "shipReady",
+ "blockingFindingIds": []
+ },
+ "readiness": "shipReady"
+}
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/ship.json b/readiness/2907-blocked-by-set-mutations/ship.json
new file mode 100644
index 000000000..583ed827c
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/ship.json
@@ -0,0 +1,354 @@
+{
+ "schemaVersion": 1,
+ "viewVersion": "1.0",
+ "workId": "2907-blocked-by-set-mutations",
+ "stage": "ship",
+ "status": "shipReady",
+ "generator": "FS.GG.SDD.Artifacts/1.2.5",
+ "sources": [
+ {
+ "path": ".fsgg/agents.yml",
+ "kind": "agentsConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "c67035b1cf31b6fa802c10f761eeb1e5bf5d198c38d159b54fac299957b5aac3"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/project.yml",
+ "kind": "projectConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ef78f002b33ce22f738bac2c0dbf716db729a7d3ae3e4a68a4ce4b93810ced66"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/sdd.yml",
+ "kind": "sddConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "fe5a7c270a975153e80ab0d08a610c9b302585dcb0b5f7c654841ceced9272cb"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/analysis.json",
+ "kind": "analysis",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "38a2c866a5b712987a139f889e06969751a270028812b7fa4e61bf9b6a9c805e"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/verify.json",
+ "kind": "source",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "3c01ddc13ad6bff9bbecf40db062b2122ca967a695b8496fffe3f706fc4ac886"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "db56994d8c948ccf24af6a447846e8059252e8a02782bd3d3fa4b669d676d176"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/checklist.md",
+ "kind": "checklist",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "kind": "clarification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "kind": "evidence",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "dd6696d33868cea46883b8391ce40853995f3c47aaa0db190ae172d716383f7f"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/plan.md",
+ "kind": "plan",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/spec.md",
+ "kind": "specification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "kind": "tasks",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ }
+ ],
+ "lifecycleReadiness": {
+ "status": "shipReady",
+ "stages": [
+ {
+ "stage": "analyze",
+ "status": "ready"
+ },
+ {
+ "stage": "checklist",
+ "status": "ready"
+ },
+ {
+ "stage": "clarify",
+ "status": "ready"
+ },
+ {
+ "stage": "evidence",
+ "status": "ready"
+ },
+ {
+ "stage": "plan",
+ "status": "ready"
+ },
+ {
+ "stage": "specify",
+ "status": "ready"
+ },
+ {
+ "stage": "tasks",
+ "status": "ready"
+ },
+ {
+ "stage": "verify",
+ "status": "ready"
+ }
+ ]
+ },
+ "verificationReadiness": {
+ "status": "verificationReady",
+ "blockingFindingIds": [],
+ "evidenceSupportedCount": 14,
+ "evidenceSelfAttestedCount": 0,
+ "evidenceObservedCount": 14,
+ "evidenceDeferredCount": 0,
+ "evidenceMissingCount": 0,
+ "evidenceStaleCount": 0,
+ "evidenceSyntheticCount": 0,
+ "evidenceInvalidCount": 0
+ },
+ "evidenceDispositions": [
+ {
+ "id": "ED-EV001",
+ "obligationId": "EV001",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV002",
+ "obligationId": "EV002",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV003",
+ "obligationId": "EV003",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV004",
+ "obligationId": "EV004",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV005",
+ "obligationId": "EV005",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV006",
+ "obligationId": "EV006",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV007",
+ "obligationId": "EV007",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV008",
+ "obligationId": "EV008",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV009",
+ "obligationId": "EV009",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV010",
+ "obligationId": "EV010",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV011",
+ "obligationId": "EV011",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV012",
+ "obligationId": "EV012",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV013",
+ "obligationId": "EV013",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ },
+ {
+ "id": "ED-EV014",
+ "obligationId": "EV014",
+ "state": "supported",
+ "observed": true,
+ "severity": "ready",
+ "diagnosticIds": []
+ }
+ ],
+ "generatedViews": [
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/analysis.json",
+ "kind": "analysis",
+ "currency": "current",
+ "diagnosticIds": []
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/verify.json",
+ "kind": "verification",
+ "currency": "current",
+ "diagnosticIds": []
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "currency": "current",
+ "diagnosticIds": []
+ }
+ ],
+ "disposition": {
+ "state": "shipReady",
+ "blockingFindingIds": [],
+ "warningFindingIds": [],
+ "advisoryFindingIds": [],
+ "contributingStages": [],
+ "classifiedObligationsUnmetCount": 0,
+ "journeyObligationsUnmetCount": 0,
+ "correction": ""
+ },
+ "findings": [],
+ "governanceCompatibility": [
+ {
+ "path": ".fsgg/policy.yml",
+ "relationship": "optionalGovernancePolicy",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/capabilities.yml",
+ "relationship": "optionalGovernanceCapabilities",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/tooling.yml",
+ "relationship": "optionalGovernanceTooling",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ }
+ ],
+ "diagnostics": [],
+ "readiness": "shipReady",
+ "nextAction": {
+ "actionId": "ship.next.protectedBoundary",
+ "command": null,
+ "reason": "Ship readiness is current and ready for the protected-boundary handoff."
+ }
+}
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/test-results/boardops.trx b/readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
new file mode 100644
index 000000000..35f196a3d
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
@@ -0,0 +1,1859 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/verify.json b/readiness/2907-blocked-by-set-mutations/verify.json
new file mode 100644
index 000000000..4fccc6604
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/verify.json
@@ -0,0 +1,898 @@
+{
+ "schemaVersion": 1,
+ "viewVersion": "1.0",
+ "workId": "2907-blocked-by-set-mutations",
+ "stage": "verify",
+ "status": "verificationReady",
+ "generator": "FS.GG.SDD.Artifacts/1.2.5",
+ "sources": [
+ {
+ "path": ".fsgg/agents.yml",
+ "kind": "agentsConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "c67035b1cf31b6fa802c10f761eeb1e5bf5d198c38d159b54fac299957b5aac3"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/project.yml",
+ "kind": "projectConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ef78f002b33ce22f738bac2c0dbf716db729a7d3ae3e4a68a4ce4b93810ced66"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": ".fsgg/sdd.yml",
+ "kind": "sddConfig",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "fe5a7c270a975153e80ab0d08a610c9b302585dcb0b5f7c654841ceced9272cb"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/analysis.json",
+ "kind": "analysis",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "38a2c866a5b712987a139f889e06969751a270028812b7fa4e61bf9b6a9c805e"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "db56994d8c948ccf24af6a447846e8059252e8a02782bd3d3fa4b669d676d176"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/checklist.md",
+ "kind": "checklist",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "kind": "clarification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "kind": "evidence",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "dd6696d33868cea46883b8391ce40853995f3c47aaa0db190ae172d716383f7f"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/plan.md",
+ "kind": "plan",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/spec.md",
+ "kind": "specification",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "kind": "tasks",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540"
+ },
+ "schemaVersion": 1,
+ "schemaStatus": "current"
+ }
+ ],
+ "lifecycleReadiness": {
+ "status": "implementationReady",
+ "stages": [
+ {
+ "stage": "analyze",
+ "status": "implementationReady"
+ },
+ {
+ "stage": "checklist",
+ "status": "current"
+ },
+ {
+ "stage": "clarify",
+ "status": "current"
+ },
+ {
+ "stage": "evidence",
+ "status": "evidenceReady"
+ },
+ {
+ "stage": "plan",
+ "status": "current"
+ },
+ {
+ "stage": "specify",
+ "status": "current"
+ },
+ {
+ "stage": "tasks",
+ "status": "current"
+ }
+ ]
+ },
+ "taskGraph": {
+ "taskCount": 14,
+ "dependencyCount": 8,
+ "dependenciesValid": true,
+ "statusesValid": true,
+ "findingIds": []
+ },
+ "evidenceDispositions": [
+ {
+ "id": "ED-EV001",
+ "obligationId": "EV001",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV001"
+ ],
+ "affectedTaskIds": [
+ "T001"
+ ],
+ "affectedSourceIds": [
+ "AC-001",
+ "FR-001",
+ "PD-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV002",
+ "obligationId": "EV002",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV002"
+ ],
+ "affectedTaskIds": [
+ "T002"
+ ],
+ "affectedSourceIds": [
+ "AC-002",
+ "FR-002",
+ "PD-002"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV003",
+ "obligationId": "EV003",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV003"
+ ],
+ "affectedTaskIds": [
+ "T003"
+ ],
+ "affectedSourceIds": [
+ "AC-001",
+ "AC-002",
+ "AC-003",
+ "FR-003",
+ "PD-003"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV004",
+ "obligationId": "EV004",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV004"
+ ],
+ "affectedTaskIds": [
+ "T004"
+ ],
+ "affectedSourceIds": [
+ "AC-004",
+ "FR-004",
+ "PD-004"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV005",
+ "obligationId": "EV005",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV005"
+ ],
+ "affectedTaskIds": [
+ "T005"
+ ],
+ "affectedSourceIds": [
+ "AC-005",
+ "FR-005",
+ "PD-005"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV006",
+ "obligationId": "EV006",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV006"
+ ],
+ "affectedTaskIds": [
+ "T006"
+ ],
+ "affectedSourceIds": [
+ "AC-006",
+ "FR-006",
+ "PD-006"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV007",
+ "obligationId": "EV007",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV007"
+ ],
+ "affectedTaskIds": [
+ "T007"
+ ],
+ "affectedSourceIds": [
+ "DEC-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV008",
+ "obligationId": "EV008",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV008"
+ ],
+ "affectedTaskIds": [
+ "T008"
+ ],
+ "affectedSourceIds": [
+ "DEC-002"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV009",
+ "obligationId": "EV009",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV009"
+ ],
+ "affectedTaskIds": [
+ "T009"
+ ],
+ "affectedSourceIds": [
+ "DEC-003"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV010",
+ "obligationId": "EV010",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV010"
+ ],
+ "affectedTaskIds": [
+ "T010"
+ ],
+ "affectedSourceIds": [
+ "DEC-004"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV011",
+ "obligationId": "EV011",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV011"
+ ],
+ "affectedTaskIds": [
+ "T011"
+ ],
+ "affectedSourceIds": [
+ "PC-001",
+ "PD-001",
+ "PD-004"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV012",
+ "obligationId": "EV012",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV012"
+ ],
+ "affectedTaskIds": [
+ "T012"
+ ],
+ "affectedSourceIds": [
+ "PC-001",
+ "PD-001",
+ "PD-002",
+ "PD-003",
+ "PD-004",
+ "PD-005",
+ "PD-006",
+ "VO-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV013",
+ "obligationId": "EV013",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV013"
+ ],
+ "affectedTaskIds": [
+ "T013"
+ ],
+ "affectedSourceIds": [
+ "PC-001",
+ "PM-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "ED-EV014",
+ "obligationId": "EV014",
+ "state": "supported",
+ "observed": true,
+ "classifiedRequirement": false,
+ "journeyRequirement": false,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV014"
+ ],
+ "affectedTaskIds": [
+ "T014"
+ ],
+ "affectedSourceIds": [
+ "GV-001",
+ "PD-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ }
+ ],
+ "testDispositions": [
+ {
+ "id": "TD-EV001",
+ "obligationId": "EV001",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV001"
+ ],
+ "affectedTaskIds": [
+ "T001"
+ ],
+ "affectedRequirementIds": [
+ "FR-001"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV002",
+ "obligationId": "EV002",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV002"
+ ],
+ "affectedTaskIds": [
+ "T002"
+ ],
+ "affectedRequirementIds": [
+ "FR-002"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV003",
+ "obligationId": "EV003",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV003"
+ ],
+ "affectedTaskIds": [
+ "T003"
+ ],
+ "affectedRequirementIds": [
+ "FR-003"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV004",
+ "obligationId": "EV004",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV004"
+ ],
+ "affectedTaskIds": [
+ "T004"
+ ],
+ "affectedRequirementIds": [
+ "FR-004"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV005",
+ "obligationId": "EV005",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV005"
+ ],
+ "affectedTaskIds": [
+ "T005"
+ ],
+ "affectedRequirementIds": [
+ "FR-005"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV006",
+ "obligationId": "EV006",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV006"
+ ],
+ "affectedTaskIds": [
+ "T006"
+ ],
+ "affectedRequirementIds": [
+ "FR-006"
+ ],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV007",
+ "obligationId": "EV007",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV007"
+ ],
+ "affectedTaskIds": [
+ "T007"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV008",
+ "obligationId": "EV008",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV008"
+ ],
+ "affectedTaskIds": [
+ "T008"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV009",
+ "obligationId": "EV009",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV009"
+ ],
+ "affectedTaskIds": [
+ "T009"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV010",
+ "obligationId": "EV010",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV010"
+ ],
+ "affectedTaskIds": [
+ "T010"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV011",
+ "obligationId": "EV011",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV011"
+ ],
+ "affectedTaskIds": [
+ "T011"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV012",
+ "obligationId": "EV012",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV012"
+ ],
+ "affectedTaskIds": [
+ "T012"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV013",
+ "obligationId": "EV013",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV013"
+ ],
+ "affectedTaskIds": [
+ "T013"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "id": "TD-EV014",
+ "obligationId": "EV014",
+ "state": "satisfied",
+ "observed": true,
+ "recordRequirement": false,
+ "evidenceIds": [
+ "EV014"
+ ],
+ "affectedTaskIds": [
+ "T014"
+ ],
+ "affectedRequirementIds": [],
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ }
+ ],
+ "skillVisibility": [
+ {
+ "skill": "automated-tests",
+ "requiringTaskIds": [
+ "T012"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "skill": "deterministic-json",
+ "requiringTaskIds": [
+ "T014"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "skill": "fsharp",
+ "requiringTaskIds": [
+ "T001",
+ "T002",
+ "T003",
+ "T004",
+ "T005",
+ "T006",
+ "T007",
+ "T008",
+ "T009",
+ "T010",
+ "T011"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "skill": "implementation",
+ "requiringTaskIds": [
+ "T001",
+ "T002",
+ "T003",
+ "T004",
+ "T005",
+ "T006",
+ "T007",
+ "T008",
+ "T009",
+ "T010"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "skill": "readiness-evidence",
+ "requiringTaskIds": [
+ "T012"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ },
+ {
+ "skill": "schema-versioning",
+ "requiringTaskIds": [
+ "T013"
+ ],
+ "visibility": "visible",
+ "sourceArtifactPath": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "severity": "ready",
+ "diagnosticIds": [],
+ "correction": ""
+ }
+ ],
+ "generatedViews": [
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/analysis.json",
+ "kind": "verification",
+ "currency": "current",
+ "diagnosticIds": []
+ },
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "currency": "current",
+ "diagnosticIds": []
+ }
+ ],
+ "findings": [
+ {
+ "id": "VF001",
+ "severity": "warning",
+ "category": "warning",
+ "path": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "relatedIds": [
+ "analysis",
+ "checklist",
+ "clarifications",
+ "plan",
+ "spec",
+ "tasks"
+ ],
+ "message": "One or more evidence source snapshots no longer match current source digests.",
+ "correction": "Rerun the evidence command after reviewing the changed source artifacts."
+ }
+ ],
+ "governanceCompatibility": [
+ {
+ "path": ".fsgg/policy.yml",
+ "relationship": "optionalGovernancePolicy",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/capabilities.yml",
+ "relationship": "optionalGovernanceCapabilities",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ },
+ {
+ "path": ".fsgg/tooling.yml",
+ "relationship": "optionalGovernanceTooling",
+ "requiredBySdd": false,
+ "state": "notEvaluated",
+ "diagnosticIds": []
+ }
+ ],
+ "diagnostics": [
+ {
+ "id": "evidence.staleEvidenceSource",
+ "severity": "warning",
+ "artifact": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "message": "One or more evidence source snapshots no longer match current source digests.",
+ "correction": "Rerun the evidence command after reviewing the changed source artifacts.",
+ "relatedIds": [
+ "analysis",
+ "checklist",
+ "clarifications",
+ "plan",
+ "spec",
+ "tasks"
+ ]
+ }
+ ],
+ "readiness": "verificationReady",
+ "nextAction": {
+ "actionId": "verify.next.ship",
+ "command": null,
+ "reason": "Verification readiness is current and ready for ship."
+ }
+}
\ No newline at end of file
diff --git a/readiness/2907-blocked-by-set-mutations/work-model.json b/readiness/2907-blocked-by-set-mutations/work-model.json
new file mode 100644
index 000000000..77ad75073
--- /dev/null
+++ b/readiness/2907-blocked-by-set-mutations/work-model.json
@@ -0,0 +1,1252 @@
+{
+ "schemaVersion": 1,
+ "modelVersion": "1.2.0",
+ "workId": "2907-blocked-by-set-mutations",
+ "project": {
+ "id": ".github-2127",
+ "defaultWorkRoot": "work",
+ "profile": null
+ },
+ "sources": [
+ {
+ "path": ".fsgg/agents.yml",
+ "kind": "agentsConfig",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "c67035b1cf31b6fa802c10f761eeb1e5bf5d198c38d159b54fac299957b5aac3"
+ }
+ },
+ {
+ "path": ".fsgg/project.yml",
+ "kind": "projectConfig",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "ef78f002b33ce22f738bac2c0dbf716db729a7d3ae3e4a68a4ce4b93810ced66"
+ }
+ },
+ {
+ "path": ".fsgg/sdd.yml",
+ "kind": "sddConfig",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "fe5a7c270a975153e80ab0d08a610c9b302585dcb0b5f7c654841ceced9272cb"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/checklist.md",
+ "kind": "checklist",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "kind": "clarifications",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "kind": "evidence",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "02b417f6fac4ec096e11ab6622d556948782cfd43b0d2bbfc5da284e3b29e7b0"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/plan.md",
+ "kind": "plan",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/spec.md",
+ "kind": "spec",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f"
+ }
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "kind": "tasks",
+ "owner": "sdd",
+ "schemaVersion": 1,
+ "rawSchemaVersion": "1",
+ "schemaStatus": "current",
+ "sourceDigest": {
+ "algorithm": "sha256",
+ "value": "ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540"
+ }
+ }
+ ],
+ "workItem": {
+ "id": "2907-blocked-by-set-mutations",
+ "title": "Blocked-by Set Mutations",
+ "stage": "specify",
+ "changeTier": "tier1",
+ "status": "specified"
+ },
+ "performanceIntent": null,
+ "requirements": [
+ {
+ "id": "FR-001",
+ "title": "\u0060Blocked by\u0060 add canonicalizes and unions requested refs with the live canonical set, removes duplicates, and preserves every existing edge. (Stories: US-001; Acceptance: AC-001)",
+ "text": "\u0060Blocked by\u0060 add canonicalizes and unions requested refs with the live canonical set, removes duplicates, and preserves every existing edge. (Stories: US-001; Acceptance: AC-001)",
+ "acceptanceCriteria": [
+ "AC-001"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 37,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T001"
+ ],
+ "linkedEvidenceIds": [
+ "EV001"
+ ]
+ },
+ {
+ "id": "FR-002",
+ "title": "\u0060Blocked by\u0060 remove canonicalizes and subtracts requested refs from the live canonical set, preserves every non-requested edge, and uses the distinct clear mutation when the result is empty. (Stories: US-001; Acceptance: AC-002)",
+ "text": "\u0060Blocked by\u0060 remove canonicalizes and subtracts requested refs from the live canonical set, preserves every non-requested edge, and uses the distinct clear mutation when the result is empty. (Stories: US-001; Acceptance: AC-002)",
+ "acceptanceCriteria": [
+ "AC-002"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 38,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T002"
+ ],
+ "linkedEvidenceIds": [
+ "EV002"
+ ]
+ },
+ {
+ "id": "FR-003",
+ "title": "Add and remove bind both the live field value and Projects-v2 item revision; a changed value or revision at the guarded write boundary fails stale and emits no mutation. (Stories: US-001; Acceptance: AC-001, AC-002, AC-003)",
+ "text": "Add and remove bind both the live field value and Projects-v2 item revision; a changed value or revision at the guarded write boundary fails stale and emits no mutation. (Stories: US-001; Acceptance: AC-001, AC-002, AC-003)",
+ "acceptanceCriteria": [
+ "AC-001",
+ "AC-002",
+ "AC-003"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 39,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T003"
+ ],
+ "linkedEvidenceIds": [
+ "EV003"
+ ]
+ },
+ {
+ "id": "FR-004",
+ "title": "The command names add, remove, replace, and clear as distinct caller intents for \u0060Blocked by\u0060; an ambiguous bare replacement is refused while unrelated scalar-field syntax remains compatible. (Stories: US-001; Acceptance: AC-004)",
+ "text": "The command names add, remove, replace, and clear as distinct caller intents for \u0060Blocked by\u0060; an ambiguous bare replacement is refused while unrelated scalar-field syntax remains compatible. (Stories: US-001; Acceptance: AC-004)",
+ "acceptanceCriteria": [
+ "AC-004"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 40,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T004"
+ ],
+ "linkedEvidenceIds": [
+ "EV004"
+ ]
+ },
+ {
+ "id": "FR-005",
+ "title": "Lint reports a body-only \u0060Blocked by:\u0060 declaration when the authoritative field is empty and a divergent projection when their canonical sets differ, while an agreeing generated projection stays green. Invalid body syntax is reported rather than promoted to authority. (Stories: US-002; Acceptance: AC-005)",
+ "text": "Lint reports a body-only \u0060Blocked by:\u0060 declaration when the authoritative field is empty and a divergent projection when their canonical sets differ, while an agreeing generated projection stays green. Invalid body syntax is reported rather than promoted to authority. (Stories: US-002; Acceptance: AC-005)",
+ "acceptanceCriteria": [
+ "AC-005"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 41,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T005"
+ ],
+ "linkedEvidenceIds": [
+ "EV005"
+ ]
+ },
+ {
+ "id": "FR-006",
+ "title": "Focused tests and gate inversion prove add preserves \u0060#290\u0060 while adding \u0060#299\u0060, remove preserves \u0060#299\u0060 while removing \u0060#290\u0060, a changed observation is never overwritten, and lint distinguishes inert, divergent, invalid, absent, and agreeing body projections. (Stories: US-001, US-002; Acceptance: AC-006)",
+ "text": "Focused tests and gate inversion prove add preserves \u0060#290\u0060 while adding \u0060#299\u0060, remove preserves \u0060#299\u0060 while removing \u0060#290\u0060, a changed observation is never overwritten, and lint distinguishes inert, divergent, invalid, absent, and agreeing body projections. (Stories: US-001, US-002; Acceptance: AC-006)",
+ "acceptanceCriteria": [
+ "AC-006"
+ ],
+ "priority": null,
+ "classification": [],
+ "source": "work/2907-blocked-by-set-mutations/spec.md",
+ "sourceLocation": {
+ "line": 42,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T006"
+ ],
+ "linkedEvidenceIds": [
+ "EV006"
+ ]
+ }
+ ],
+ "decisions": [
+ {
+ "id": "DEC-001",
+ "title": "Add four closed, mutually exclusive option fields and parser flags scoped only to \u0060set-field\u0060. \u0060--add\u0060 and \u0060--remove\u0060 derive from the live set, \u0060--replace\u0060 writes only the supplied canonical set, and \u0060--clear\u0060 uses \u0060Board.Clear\u0060. A bare \u0060Blocked by\u0060 positional value refuses and names the four valid forms. Scalar fields retain their existing positional contract; batch equality remains an explicitly named replacement/clear operation.",
+ "decision": "Add four closed, mutually exclusive option fields and parser flags scoped only to \u0060set-field\u0060. \u0060--add\u0060 and \u0060--remove\u0060 derive from the live set, \u0060--replace\u0060 writes only the supplied canonical set, and \u0060--clear\u0060 uses \u0060Board.Clear\u0060. A bare \u0060Blocked by\u0060 positional value refuses and names the four valid forms. Scalar fields retain their existing positional contract; batch equality remains an explicitly named replacement/clear operation.",
+ "requirementRefs": [
+ "FR-001",
+ "FR-002",
+ "FR-004"
+ ],
+ "storyRefs": [],
+ "acceptanceRefs": [
+ "AC-001",
+ "AC-002",
+ "AC-004"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "sourceLocation": {
+ "line": 26,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T007"
+ ]
+ },
+ {
+ "id": "DEC-002",
+ "title": "Introduce a guarded single-field board write accepting \u0060BlockedByObservation\u0060. It resolves the item, re-reads the observation immediately before mutation, and emits no mutation on mismatch. The fixture controls the two reads independently and asserts a stale diagnostic plus zero update/clear documents when the second revision or value differs.",
+ "decision": "Introduce a guarded single-field board write accepting \u0060BlockedByObservation\u0060. It resolves the item, re-reads the observation immediately before mutation, and emits no mutation on mismatch. The fixture controls the two reads independently and asserts a stale diagnostic plus zero update/clear documents when the second revision or value differs.",
+ "requirementRefs": [
+ "FR-003"
+ ],
+ "storyRefs": [],
+ "acceptanceRefs": [
+ "AC-003"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "sourceLocation": {
+ "line": 27,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T008"
+ ]
+ },
+ {
+ "id": "DEC-003",
+ "title": "Parse only an unfenced, line-leading \u0060Blocked by:\u0060 body projection using the same dependency canonicalizer as the field. No body line means no finding. A present line with empty authoritative field yields \u0060BLOCKED-BY-BODY-INERT\u0060; unequal canonical sets or invalid projection syntax yield the same stable rule family with a precise divergent/invalid detail; equal canonical sets stay green.",
+ "decision": "Parse only an unfenced, line-leading \u0060Blocked by:\u0060 body projection using the same dependency canonicalizer as the field. No body line means no finding. A present line with empty authoritative field yields \u0060BLOCKED-BY-BODY-INERT\u0060; unequal canonical sets or invalid projection syntax yield the same stable rule family with a precise divergent/invalid detail; equal canonical sets stay green.",
+ "requirementRefs": [
+ "FR-005"
+ ],
+ "storyRefs": [],
+ "acceptanceRefs": [
+ "AC-005"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "sourceLocation": {
+ "line": 28,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T009"
+ ]
+ },
+ {
+ "id": "DEC-004",
+ "title": "Mutation controls independently replace union with requested-only, subtraction with empty, and stale comparison with unconditional success; lint controls suppress the body comparison. Each control must red a named focused test, then be restored before the full gates.",
+ "decision": "Mutation controls independently replace union with requested-only, subtraction with empty, and stale comparison with unconditional success; lint controls suppress the body comparison. Each control must red a named focused test, then be restored before the full gates.",
+ "requirementRefs": [
+ "FR-006"
+ ],
+ "storyRefs": [],
+ "acceptanceRefs": [
+ "AC-006"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "sourceLocation": {
+ "line": 29,
+ "column": 1
+ },
+ "linkedTaskIds": [
+ "T010"
+ ]
+ }
+ ],
+ "tasks": [
+ {
+ "id": "T001",
+ "title": "Implement requirement FR-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-001"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-001",
+ "FR-001",
+ "PD-001"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV001"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 1,
+ "column": 1
+ }
+ },
+ {
+ "id": "T002",
+ "title": "Implement requirement FR-002",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-002"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-002",
+ "FR-002",
+ "PD-002"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV002"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 2,
+ "column": 1
+ }
+ },
+ {
+ "id": "T003",
+ "title": "Implement requirement FR-003",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-003"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-001",
+ "AC-002",
+ "AC-003",
+ "FR-003",
+ "PD-003"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV003"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "id": "T004",
+ "title": "Implement requirement FR-004",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-004"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-004",
+ "FR-004",
+ "PD-004"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV004"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 4,
+ "column": 1
+ }
+ },
+ {
+ "id": "T005",
+ "title": "Implement requirement FR-005",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-005"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-005",
+ "FR-005",
+ "PD-005"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV005"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 5,
+ "column": 1
+ }
+ },
+ {
+ "id": "T006",
+ "title": "Implement requirement FR-006",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [],
+ "requirements": [
+ "FR-006"
+ ],
+ "decisions": [],
+ "sourceIds": [
+ "AC-006",
+ "FR-006",
+ "PD-006"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV006"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 6,
+ "column": 1
+ }
+ },
+ {
+ "id": "T007",
+ "title": "Implement clarification decision DEC-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [
+ "DEC-001"
+ ],
+ "sourceIds": [
+ "DEC-001"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV007"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 7,
+ "column": 1
+ }
+ },
+ {
+ "id": "T008",
+ "title": "Implement clarification decision DEC-002",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [
+ "DEC-002"
+ ],
+ "sourceIds": [
+ "DEC-002"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV008"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 8,
+ "column": 1
+ }
+ },
+ {
+ "id": "T009",
+ "title": "Implement clarification decision DEC-003",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [
+ "DEC-003"
+ ],
+ "sourceIds": [
+ "DEC-003"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV009"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 9,
+ "column": 1
+ }
+ },
+ {
+ "id": "T010",
+ "title": "Implement clarification decision DEC-004",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [
+ "DEC-004"
+ ],
+ "sourceIds": [
+ "DEC-004"
+ ],
+ "requiredSkills": [
+ "fsharp",
+ "implementation"
+ ],
+ "requiredEvidence": [
+ "EV010"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 10,
+ "column": 1
+ }
+ },
+ {
+ "id": "T011",
+ "title": "Update contract surface PC-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [],
+ "sourceIds": [
+ "PC-001",
+ "PD-001",
+ "PD-004"
+ ],
+ "requiredSkills": [
+ "fsharp"
+ ],
+ "requiredEvidence": [
+ "EV011"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 11,
+ "column": 1
+ }
+ },
+ {
+ "id": "T012",
+ "title": "Record verification evidence VO-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [],
+ "sourceIds": [
+ "PC-001",
+ "PD-001",
+ "PD-002",
+ "PD-003",
+ "PD-004",
+ "PD-005",
+ "PD-006",
+ "VO-001"
+ ],
+ "requiredSkills": [
+ "automated-tests",
+ "readiness-evidence"
+ ],
+ "requiredEvidence": [
+ "EV012"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 12,
+ "column": 1
+ }
+ },
+ {
+ "id": "T013",
+ "title": "Handle migration posture PM-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [],
+ "sourceIds": [
+ "PC-001",
+ "PM-001"
+ ],
+ "requiredSkills": [
+ "schema-versioning"
+ ],
+ "requiredEvidence": [
+ "EV013"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 13,
+ "column": 1
+ }
+ },
+ {
+ "id": "T014",
+ "title": "Refresh generated view impact GV-001",
+ "status": "pending",
+ "owner": "sdd",
+ "dependencies": [
+ "T001"
+ ],
+ "requirements": [],
+ "decisions": [],
+ "sourceIds": [
+ "GV-001",
+ "PD-001"
+ ],
+ "requiredSkills": [
+ "deterministic-json"
+ ],
+ "requiredEvidence": [
+ "EV014"
+ ],
+ "source": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "sourceLocation": {
+ "line": 14,
+ "column": 1
+ }
+ }
+ ],
+ "evidence": [
+ {
+ "id": "EV001",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T001",
+ "taskRefs": [
+ "T001"
+ ],
+ "requirementRefs": [
+ "FR-001"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 1,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV002",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T002",
+ "taskRefs": [
+ "T002"
+ ],
+ "requirementRefs": [
+ "FR-002"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 2,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV003",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T003",
+ "taskRefs": [
+ "T003"
+ ],
+ "requirementRefs": [
+ "FR-003"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 3,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV004",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T004",
+ "taskRefs": [
+ "T004"
+ ],
+ "requirementRefs": [
+ "FR-004"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 4,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV005",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T005",
+ "taskRefs": [
+ "T005"
+ ],
+ "requirementRefs": [
+ "FR-005"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 5,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV006",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T006",
+ "taskRefs": [
+ "T006"
+ ],
+ "requirementRefs": [
+ "FR-006"
+ ],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 6,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV007",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T007",
+ "taskRefs": [
+ "T007"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 7,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV008",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T008",
+ "taskRefs": [
+ "T008"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 8,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV009",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T009",
+ "taskRefs": [
+ "T009"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 9,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV010",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T010",
+ "taskRefs": [
+ "T010"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 10,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV011",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T011",
+ "taskRefs": [
+ "T011"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 11,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV012",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T012",
+ "taskRefs": [
+ "T012"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 12,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV013",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T013",
+ "taskRefs": [
+ "T013"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 13,
+ "column": 1
+ }
+ },
+ {
+ "id": "EV014",
+ "kind": "verification",
+ "subjectType": "task",
+ "subjectId": "T014",
+ "taskRefs": [
+ "T014"
+ ],
+ "requirementRefs": [],
+ "artifactRefs": [
+ "src/FS.GG.Coord.Cli.BoardOps/Handlers.fs",
+ "src/FS.GG.Coord.Cli.Kernel/Options.fs",
+ "src/FS.GG.Coord.Cli/LintApplication.fs",
+ "src/FS.GG.Coord.GitHub/Board.fs",
+ "tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs",
+ "work/2907-blocked-by-set-mutations/verification/verification-evidence.md"
+ ],
+ "result": "pass",
+ "synthetic": false,
+ "performanceBudget": null,
+ "performanceEvidenceArtifact": null,
+ "performanceMeasurements": [],
+ "rationale": null,
+ "source": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "sourceLocation": {
+ "line": 14,
+ "column": 1
+ }
+ }
+ ],
+ "generatedViews": [
+ {
+ "path": "readiness/2907-blocked-by-set-mutations/work-model.json",
+ "kind": "workModel",
+ "schemaVersion": 1,
+ "generator": {
+ "id": "FS.GG.SDD.Artifacts",
+ "version": "1.2.5"
+ },
+ "sources": [
+ {
+ "path": ".fsgg/agents.yml",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "c67035b1cf31b6fa802c10f761eeb1e5bf5d198c38d159b54fac299957b5aac3"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": ".fsgg/project.yml",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ef78f002b33ce22f738bac2c0dbf716db729a7d3ae3e4a68a4ce4b93810ced66"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": ".fsgg/sdd.yml",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "fe5a7c270a975153e80ab0d08a610c9b302585dcb0b5f7c654841ceced9272cb"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/checklist.md",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/clarifications.md",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/evidence.yml",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "02b417f6fac4ec096e11ab6622d556948782cfd43b0d2bbfc5da284e3b29e7b0"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/plan.md",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/spec.md",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f"
+ },
+ "schemaVersion": 1
+ },
+ {
+ "path": "work/2907-blocked-by-set-mutations/tasks.yml",
+ "digest": {
+ "algorithm": "sha256",
+ "value": "ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540"
+ },
+ "schemaVersion": 1
+ }
+ ],
+ "outputDigest": {
+ "algorithm": "sha256",
+ "value": "041086d508d87646cfdf5f884ca356216b1262d60ceb06999d65b2fbc6c06586"
+ },
+ "currency": "current"
+ }
+ ],
+ "diagnostics": [],
+ "governanceBoundaries": []
+}
\ No newline at end of file
diff --git a/src/FS.GG.Coord.Cli.BoardOps/Handlers.fs b/src/FS.GG.Coord.Cli.BoardOps/Handlers.fs
index 20809f77f..240c5a1c5 100644
--- a/src/FS.GG.Coord.Cli.BoardOps/Handlers.fs
+++ b/src/FS.GG.Coord.Cli.BoardOps/Handlers.fs
@@ -452,6 +452,136 @@ module Handlers =
Error ExitError
+ let private canonicalBlockedByRefs (ref: Ref) (raw: string) : Result =
+ match Blockers.canonicalizeBlockedBy ref.Owner ref.Repo raw with
+ | Ok None -> Ok []
+ | Ok(Some canonical) ->
+ canonical.Split(',')
+ |> Array.map (fun value -> value.Trim())
+ |> Array.filter (fun value -> value <> "")
+ |> List.ofArray
+ |> Ok
+ | Error Blockers.Placeholder ->
+ eprint
+ $"fsgg-coord-engine: 'Blocked by' does not take a placeholder ('%s{raw.Trim()}'). Use --clear to clear the set."
+
+ Error ExitError
+ | Error Blockers.NotIssueRefs ->
+ eprint
+ $"fsgg-coord-engine: 'Blocked by' takes issue refs (owner/repo#n), not prose: '%s{raw}'. If the item ITSELF is blocked, that is a Status: set-field Status Blocked"
+
+ Error ExitError
+
+ let private blockedByWrite (refs: string list) : Board.FieldWrite =
+ match refs with
+ | [] -> Board.Clear
+ | values -> Board.Set(String.concat ", " values)
+
+ let private setBlockedByMutation (ctx: Context) (opts: Options) (mutation: Options.BlockedByMutation) : int =
+ match opts.Args with
+ | [ refArg; field ] when field = "Blocked by" ->
+ match parseRef ctx refArg, worker opts with
+ | Error msg, _ ->
+ eprint $"fsgg-coord-engine: %s{msg}"
+ ExitError
+ | _, Error c -> c
+ | Ok ref, Ok w ->
+ // Validate caller-supplied refs before any board read. Add/remove still need a live
+ // observation to derive their result, but malformed intent must spend zero GraphQL.
+ let boardResult =
+ match mutation with
+ | Options.ClearBlockedBy -> Board.bootstrapCached ctx.Transport ctx.Owner ctx.Title |> Result.mapError Choice2Of2
+ | Options.AddBlockedBy raw
+ | Options.RemoveBlockedBy raw
+ | Options.ReplaceBlockedBy raw ->
+ match canonicalBlockedByRefs ref raw with
+ | Error rc -> Error(Choice1Of2 rc)
+ | Ok _ -> Board.bootstrapCached ctx.Transport ctx.Owner ctx.Title |> Result.mapError Choice2Of2
+
+ match boardResult with
+ | Error(Choice1Of2 rc) -> rc
+ | Error(Choice2Of2 e) -> fail e
+ | Ok board ->
+ let renderOutcome operation write outcome =
+ match outcome with
+ | Board.Written ->
+ printfn
+ "set %s Blocked by %s = %s"
+ ref.Canonical
+ operation
+ (match write with | Board.Set value -> value | Board.Clear -> "")
+ ExitGreen
+ | Board.Deferred ->
+ printfn "set-field %s Blocked by %s — QUEUED (budget exhausted; flush replays it)" ref.Canonical operation
+ Errors.ExRate
+ | Board.NotOnBoard ->
+ eprint $"fsgg-coord-engine: %s{ref.Canonical} is not an item on this board — nothing written."
+ ExitError
+
+ let writeExplicit operation write =
+ match Board.boardWrite ctx.Transport board ref.Owner ref.Repo ref.Number "Blocked by" write w.Id with
+ | Error e -> Error e
+ | Ok outcome -> Ok(renderOutcome operation write outcome)
+
+ // The lease begins BEFORE the first live edge observation and remains held through
+ // the GraphQL mutation. Projects v2 has no expected-revision input, so moving only the
+ // second read closer to the write would preserve the overwrite race this boundary owns.
+ let transaction () : Errors.IoResult =
+ match mutation with
+ | Options.ReplaceBlockedBy raw ->
+ match canonicalBlockedByRefs ref raw with
+ | Error rc -> Ok rc
+ | Ok [] ->
+ eprint "fsgg-coord-engine: --replace needs at least one issue ref; use --clear to clear the set."
+ Ok ExitError
+ | Ok refs ->
+ let write = blockedByWrite refs
+ writeExplicit "replace" write
+ | Options.ClearBlockedBy ->
+ writeExplicit "clear" Board.Clear
+ | Options.AddBlockedBy raw
+ | Options.RemoveBlockedBy raw as edgeMutation ->
+ match Board.itemBlockedByObservation ctx.Transport board ref.Owner ref.Repo ref.Number with
+ | Error e -> Error e
+ | Ok observed ->
+ let liveRaw = observed.Value |> Option.defaultValue ""
+ match canonicalBlockedByRefs ref liveRaw, canonicalBlockedByRefs ref raw with
+ | Error rc, _
+ | _, Error rc -> Ok rc
+ | _, Ok [] ->
+ let operation =
+ match edgeMutation with
+ | Options.AddBlockedBy _ -> "add"
+ | _ -> "remove"
+
+ eprint $"fsgg-coord-engine: --%s{operation} needs at least one issue ref."
+ Ok ExitError
+ | Ok live, Ok requested ->
+ let result, operation =
+ match edgeMutation with
+ | Options.AddBlockedBy _ ->
+ let seen = Set.ofList live
+ live @ (requested |> List.filter (fun value -> not (Set.contains value seen))), "add"
+ | Options.RemoveBlockedBy _ ->
+ let removed = Set.ofList requested
+ live |> List.filter (fun value -> not (Set.contains value removed)), "remove"
+ | _ -> failwith "unreachable explicit edge mutation"
+
+ let write = blockedByWrite result
+ match Board.boardWriteGuarded ctx.Transport board ref.Owner ref.Repo ref.Number observed write with
+ | Error e -> Error e
+ | Ok outcome -> Ok(renderOutcome operation write outcome)
+
+ match Writes.withBlockedByMutationLease ctx.Transport ref transaction with
+ | Error e -> fail e
+ | Ok code -> code
+ | [ _; field ] ->
+ eprint $"fsgg-coord-engine: --add/--remove/--replace/--clear apply only to the set-valued 'Blocked by' field (got '%s{field}')."
+ ExitError
+ | _ ->
+ eprint "fsgg-coord-engine: an explicit Blocked by mutation takes [ 'Blocked by' and exactly one of --add REFS, --remove REFS, --replace REFS, or --clear."
+ ExitError
+
// `set-field --batch ][ Field=Value ...` — N fields in ONE aliased mutation (#448).
//
// The whole point is the call count: three separate writes are three GraphQL points; the same three
@@ -620,12 +750,17 @@ module Handlers =
eprint "fsgg-coord-engine: set-field --batch takes ][ followed by one or more Field=Value pairs."
ExitError
- let setField (ctx: Context) (opts: Options) : int =
+ let private setFieldPositional (ctx: Context) (opts: Options) : int =
if opts.Batch then
setFieldBatchCmd ctx opts
else
match opts.Args with
+ | [ _; "Blocked by"; _ ] ->
+ eprint
+ "fsgg-coord-engine: bare replacement of the set-valued 'Blocked by' field is refused. Name the intent: --add REFS, --remove REFS, --replace REFS, or --clear."
+
+ ExitError
| [ refArg; field; value ] ->
match parseRef ctx refArg, worker opts with
| Error msg, _ ->
@@ -718,8 +853,16 @@ module Handlers =
eprint $"fsgg-coord-engine: %s{ref.Canonical} is not an item on this board — nothing written."
ExitError
| _ ->
- eprint "fsgg-coord-engine: set-field takes ][ (an empty value clears)."
+ eprint "fsgg-coord-engine: set-field takes ][ , or ][ 'Blocked by' with one explicit set mutation."
+ ExitError
+
+ let setField (ctx: Context) (opts: Options) : int =
+ match opts.Batch, opts.BlockedByMutation with
+ | true, Some _ ->
+ eprint "fsgg-coord-engine: set-field --batch cannot be combined with --add, --remove, --replace, or --clear."
ExitError
+ | _, Some mutation -> setBlockedByMutation ctx opts mutation
+ | _ -> setFieldPositional ctx opts
let child (ctx: Context) (opts: Options) : int =
match opts.Args with
diff --git a/src/FS.GG.Coord.Cli.Kernel/Options.fs b/src/FS.GG.Coord.Cli.Kernel/Options.fs
index 6ee69d1f0..fa1010e56 100644
--- a/src/FS.GG.Coord.Cli.Kernel/Options.fs
+++ b/src/FS.GG.Coord.Cli.Kernel/Options.fs
@@ -71,6 +71,15 @@ module Options =
| Json
| Text
+ // Explicit set-valued mutation intent for `set-field ... 'Blocked by'` (.github#2907).
+ // A union makes add/remove/replace/clear mutually exclusive by construction; the parser refuses
+ // a second spelling instead of silently choosing the last one.
+ type BlockedByMutation =
+ | AddBlockedBy of string
+ | RemoveBlockedBy of string
+ | ReplaceBlockedBy of string
+ | ClearBlockedBy
+
/// WHICH STDOUT PROJECTIONS A COMMAND ACTUALLY HAS (#1523).
///
/// This is the ONE hand-written fact about the renderers, and everything else about `--json`/`--text`
@@ -178,6 +187,8 @@ module Options =
/// `repo#n`, or an issue URL, and prose or a placeholder is refused before anything is written.
/// Read ONLY by `release`; every other command refuses it.
BlockedBy: string option
+ // The explicit `Blocked by` set operation supplied to `set-field`.
+ BlockedByMutation: BlockedByMutation option
/// `ready --all` — widen past the "not Done" default without naming a column (#520: `ready` is a
/// TRUTH read, so `--all` shows the whole board, Done and closed items and all).
All: bool
@@ -473,7 +484,10 @@ IO (read and write the board — $FSGG_COORD_OWNER / $FSGG_COORD_PROJECT, $GITHU
default only ever fills an EMPTY column: a row that already
carries a Status keeps it, so re-running `add` cannot overwrite
one somebody set. `--status S` names the column instead
- set-field ][ write one board field (empty value clears)
+ set-field ][ write one scalar board field (empty value clears)
+ set-field ][ 'Blocked by' mutate the dependency set explicitly:
+ --add REFS | --remove REFS preserve every other live edge and fail stale on change
+ --replace REFS | --clear explicitly replace the set or clear it
set-field --batch ][ Field=Value ... write N fields in ONE aliased mutation (#448)
flush [--dry-run] REPLAY the board writes an exhausted budget queued — the verb
every "QUEUED; flush replays it" message names (#862). Replays
@@ -740,6 +754,10 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
| FIssue
| FStatus
| FBlockedBy
+ | FBlockedByAdd
+ | FBlockedByRemove
+ | FBlockedByReplace
+ | FBlockedByClear
| FAll
| FBatch
| FStrict
@@ -821,6 +839,10 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
// `release --blocked-by` (.github#2079) — the ONE call that pairs the `Blocked by` field write
// with `--status Blocked`, so `release` is its only reader; every other command refuses it.
| FBlockedBy -> Only [ Release ]
+ | FBlockedByAdd
+ | FBlockedByRemove
+ | FBlockedByReplace
+ | FBlockedByClear -> Only [ SetField ]
| FMint -> Only [ WhoAmI ]
| FLocal -> Only [ Who ]
@@ -929,6 +951,10 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
| FIssue -> "--issue", []
| FStatus -> "--status", []
| FBlockedBy -> "--blocked-by", []
+ | FBlockedByAdd -> "--add", []
+ | FBlockedByRemove -> "--remove", []
+ | FBlockedByReplace -> "--replace", []
+ | FBlockedByClear -> "--clear", []
| FAll -> "--all", []
| FBatch -> "--batch", []
| FExplain -> "--explain", []
@@ -1105,6 +1131,12 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
if o.Issue.IsSome then FIssue
if o.Status.IsSome then FStatus
if o.BlockedBy.IsSome then FBlockedBy
+ match o.BlockedByMutation with
+ | Some(AddBlockedBy _) -> FBlockedByAdd
+ | Some(RemoveBlockedBy _) -> FBlockedByRemove
+ | Some(ReplaceBlockedBy _) -> FBlockedByReplace
+ | Some ClearBlockedBy -> FBlockedByClear
+ | None -> ()
if o.All then FAll
if o.Batch then FBatch
if o.Strict then FStrict
@@ -1690,6 +1722,24 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
| "--blocked-by" :: value :: t -> flags { acc with BlockedBy = Some value } t
| [ "--blocked-by" ] -> Error "--blocked-by needs a value"
+ | ("--add" | "--remove" | "--replace") as flag :: value :: _ when value.StartsWith "-" ->
+ Error $"%s{flag} needs a value (got flag '%s{value}')"
+ | ("--add" | "--remove" | "--replace") as flag :: value :: t ->
+ match acc.BlockedByMutation with
+ | Some _ -> Error "set-field accepts exactly one of --add, --remove, --replace, or --clear"
+ | None ->
+ let mutation =
+ match flag with
+ | "--add" -> AddBlockedBy value
+ | "--remove" -> RemoveBlockedBy value
+ | _ -> ReplaceBlockedBy value
+ flags { acc with BlockedByMutation = Some mutation } t
+ | [ ("--add" | "--remove" | "--replace") as flag ] -> Error $"%s{flag} needs a value"
+ | "--clear" :: t ->
+ match acc.BlockedByMutation with
+ | Some _ -> Error "set-field accepts exactly one of --add, --remove, --replace, or --clear"
+ | None -> flags { acc with BlockedByMutation = Some ClearBlockedBy } t
+
// `issues --label` / `--state` — a label may legitimately begin with a hyphen, but a bare
// trailing `--label` with nothing after it is still an error, so the empty-tail guard stays.
| "--label" :: value :: t -> flags { acc with Label = Some value } t
@@ -1848,6 +1898,7 @@ EXIT CODES — the engine's own (the shim translates them for a caller that stil
Issue = None
Status = None
BlockedBy = None
+ BlockedByMutation = None
All = false
Batch = false
Explain = false
diff --git a/src/FS.GG.Coord.Cli.Kernel/Options.fsi b/src/FS.GG.Coord.Cli.Kernel/Options.fsi
index 6deac15ce..b1d75dbae 100644
--- a/src/FS.GG.Coord.Cli.Kernel/Options.fsi
+++ b/src/FS.GG.Coord.Cli.Kernel/Options.fsi
@@ -191,6 +191,13 @@ module Options =
/// Every nullary command case, derived from the union rather than maintained as a second list.
val allCommands: Command list
+ /// Explicit set-valued mutation intent for `set-field ... 'Blocked by'`.
+ type BlockedByMutation =
+ | AddBlockedBy of string
+ | RemoveBlockedBy of string
+ | ReplaceBlockedBy of string
+ | ClearBlockedBy
+
type Options =
{ Command: Command
Render: Render
@@ -269,6 +276,8 @@ module Options =
/// FIELD in the SAME call (.github#2079), so a coherent park is one call. Canonicalized exactly
/// as `set-field ][ 'Blocked by' ` already canonicalizes it.
BlockedBy: string option
+ /// Exactly one explicit set mutation for `set-field ... 'Blocked by'`, or none.
+ BlockedByMutation: BlockedByMutation option
/// `--all` (`ready`) — widen past the "not Done" default without naming a column. `ready` is a
/// TRUTH read (#520), so `--all` shows the whole board — Done, and closed-but-still-columned rows.
All: bool
diff --git a/src/FS.GG.Coord.Cli/Client.fs b/src/FS.GG.Coord.Cli/Client.fs
index 896d9eabe..579e3b39e 100644
--- a/src/FS.GG.Coord.Cli/Client.fs
+++ b/src/FS.GG.Coord.Cli/Client.fs
@@ -51,6 +51,8 @@ module Client =
let blockedNoReasonVerdict = LintApplication.blockedNoReasonVerdict
+ let blockedByBodyProjectionVerdict = LintApplication.blockedByBodyProjectionVerdict
+
let humanParkResolvedVerdict = LintApplication.humanParkResolvedVerdict
let blockerCycleVerdicts = LintApplication.blockerCycleVerdicts
@@ -8736,6 +8738,11 @@ scoped credential) and is tracked at .github#2332, not fixable from this repo's
|> Option.map (mk "BLOCKED-NO-REASON" "error" r)
|> Option.toList
+ let blockedByProjectionFindings (r: Scan.Row) (body: string) : LintFinding list =
+ blockedByBodyProjectionVerdict r.Ref.Owner r.Ref.Repo r.BlockedByRaw body
+ |> Option.map (mk "BLOCKED-BY-BODY-INERT" "error" r)
+ |> Option.toList
+
let humanParkFindings (r: Scan.Row) (body: string) : LintFinding list =
Map.tryFind r.Ref blockersByRef
|> Option.bind (fun blockers -> humanParkResolvedVerdict r.State r.Status blockers body)
@@ -8860,7 +8867,15 @@ scoped credential) and is tracked at .github#2332, not fixable from this repo's
// One body read serves the touch-set rules, the human-block rule, and the epic
// body-child-refs.
- let bodyNeeded = isTouchSetCandidate || isEpic || isHumanBlockCandidate
+ // Every open non-Done body may carry an inert `Blocked by:` projection. There is
+ // no authoritative pre-index for body text, so diagnosing it requires reading the
+ // same body operators would otherwise trust. Closed/Done rows are historical and
+ // omitted from this live dependency-safety census.
+ let isBlockedByProjectionCandidate =
+ r.State = IssueState.Open && r.Status <> BoardStatus.Done
+
+ let bodyNeeded =
+ isTouchSetCandidate || isEpic || isHumanBlockCandidate || isBlockedByProjectionCandidate
let bodyResult =
if bodyNeeded then
@@ -8874,6 +8889,7 @@ scoped credential) and is tracked at .github#2332, not fixable from this repo's
let tsFindings = touchSetFindings r body
let hbFindings = humanBlockFindings r body
+ let blockedByProjection = blockedByProjectionFindings r body
let humanPark = humanParkFindings r body
let clsFindings = classFindings r body
@@ -8918,6 +8934,7 @@ scoped credential) and is tracked at .github#2332, not fixable from this repo's
(acc
@ tsFindings
@ hbFindings
+ @ blockedByProjection
@ humanPark
@ clsFindings
@ statusUnsetFindings
diff --git a/src/FS.GG.Coord.Cli/Client.fsi b/src/FS.GG.Coord.Cli/Client.fsi
index 008c9b59f..a51a029c3 100644
--- a/src/FS.GG.Coord.Cli/Client.fsi
+++ b/src/FS.GG.Coord.Cli/Client.fsi
@@ -75,6 +75,10 @@ module Client =
(FS.GG.Coord.Types.IssueState ->
FS.GG.Coord.Types.BoardStatus -> string -> string -> string option)
+ /// Compatibility seam for the body/field dependency-projection lint verdict.
+ val blockedByBodyProjectionVerdict:
+ (string -> string -> string -> string -> string option)
+
/// `lint`'s HUMAN-PARK-RESOLVED sentence for one row, or `None`. An ALIAS of
/// `LintApplication.humanParkResolvedVerdict`.
///
diff --git a/src/FS.GG.Coord.Cli/LintApplication.fs b/src/FS.GG.Coord.Cli/LintApplication.fs
index be6363a03..a0d85bfd7 100644
--- a/src/FS.GG.Coord.Cli/LintApplication.fs
+++ b/src/FS.GG.Coord.Cli/LintApplication.fs
@@ -157,6 +157,64 @@ module LintApplication =
else
None
+ // Diagnose a `Blocked by:` BODY projection without ever treating it as dependency authority
+ // (.github#2907). Fenced examples are quotations and ignored. An agreeing canonical projection is
+ // useful human-readable text and stays green; empty, divergent, duplicated, or invalid projections
+ // are actionable because operators otherwise read an inert line as the live edge set.
+ let blockedByBodyProjectionVerdict
+ (owner: string)
+ (repo: string)
+ (blockedBy: string)
+ (body: string)
+ : string option =
+
+ let mutable fence: string option = None
+ let projections = ResizeArray()
+
+ for line in body.Replace("\r\n", "\n").Split('\n') do
+ let trimmed = line.TrimStart()
+ let marker =
+ if trimmed.StartsWith("```") then Some "```"
+ elif trimmed.StartsWith("~~~") then Some "~~~"
+ else None
+
+ match fence, marker with
+ | None, Some opened -> fence <- Some opened
+ | Some opened, Some closed when opened = closed -> fence <- None
+ | None, None ->
+ let prefix = "Blocked by:"
+ if trimmed.StartsWith(prefix, System.StringComparison.OrdinalIgnoreCase) then
+ projections.Add(trimmed.Substring(prefix.Length).Trim())
+ | _ -> ()
+
+ let render value = if System.String.IsNullOrWhiteSpace value then "" else value
+ let canonicalSet (value: string) =
+ value.Split(',') |> Array.map (fun part -> part.Trim()) |> Set.ofArray
+
+ match List.ofSeq projections with
+ | [] -> None
+ | _ :: _ :: _ ->
+ Some
+ $"body declares %d{projections.Count} unfenced `Blocked by:` lines, but the Projects-v2 `Blocked by` field is the sole authority; keep exactly one generated human-readable projection."
+ | [ raw ] ->
+ match Blockers.canonicalizeBlockedBy owner repo raw with
+ | Error _
+ | Ok None ->
+ Some
+ $"body declares an invalid or empty `Blocked by:` projection (`%s{render raw}`); it is inert because only the Projects-v2 field creates dependency edges."
+ | Ok(Some bodyCanonical) ->
+ match Blockers.canonicalizeBlockedBy owner repo blockedBy with
+ | Ok(Some fieldCanonical) when canonicalSet fieldCanonical = canonicalSet bodyCanonical -> None
+ | Ok None ->
+ Some
+ $"body says `Blocked by: %s{bodyCanonical}` while the authoritative Projects-v2 field is empty; the body line is inert and creates no dependency edge."
+ | Ok(Some fieldCanonical) ->
+ Some
+ $"body projects `Blocked by: %s{bodyCanonical}` but the authoritative Projects-v2 field is `%s{fieldCanonical}`; regenerate or remove the divergent body projection."
+ | Error _ ->
+ Some
+ $"body projects `Blocked by: %s{bodyCanonical}` but the authoritative Projects-v2 field is not a canonical issue-ref set (`%s{render blockedBy}`); repair the field authority before trusting the projection."
+
let humanParkResolvedVerdict state status (blockers: Blocker list) (body: string) : string option =
match HumanBlock.parse body with
| Some AwaitingHumanDecision when state = IssueState.Open && status = BoardStatus.Blocked && not (List.isEmpty blockers) && (blockers |> List.forall Blockers.isResolved) ->
diff --git a/src/FS.GG.Coord.Cli/LintApplication.fsi b/src/FS.GG.Coord.Cli/LintApplication.fsi
index 16ca3fcae..79493f81c 100644
--- a/src/FS.GG.Coord.Cli/LintApplication.fsi
+++ b/src/FS.GG.Coord.Cli/LintApplication.fsi
@@ -71,6 +71,11 @@ module LintApplication =
val blockedNoReasonVerdict:
state: IssueState -> status: BoardStatus -> blockedBy: string -> body: string -> string option
+ /// `Some detail` when an unfenced body `Blocked by:` projection is inert, divergent, duplicated, or
+ /// invalid relative to the authoritative Projects-v2 field; `None` when absent or canonically equal.
+ val blockedByBodyProjectionVerdict:
+ owner: string -> repo: string -> blockedBy: string -> body: string -> string option
+
val humanParkResolvedVerdict:
state: IssueState -> status: BoardStatus -> blockers: Blocker list -> body: string -> string option
diff --git a/src/FS.GG.Coord.GitHub/Board.fs b/src/FS.GG.Coord.GitHub/Board.fs
index 59b1e52e0..d741fe057 100644
--- a/src/FS.GG.Coord.GitHub/Board.fs
+++ b/src/FS.GG.Coord.GitHub/Board.fs
@@ -1435,6 +1435,57 @@ module Board =
// REPORTED, NEVER SWALLOWED. A refusal nobody can read is a refusal that did not happen.
Error e
+ // A revision-guarded single-field write for a value derived from `Blocked by`'s live set
+ // (.github#2907). Unlike `boardWrite`, this path is never deferred: replaying a derived value after
+ // its observation has aged would turn an edge-local intent back into destructive replacement.
+ let boardWriteGuarded
+ (transport: IGitHubTransport)
+ (board: BoardMap)
+ (owner: string)
+ (repo: string)
+ (number: int)
+ (expected: BlockedByObservation)
+ (write: FieldWrite)
+ : IoResult =
+
+ let subject = $"%s{owner}/%s{repo}#%d{number} guarded Blocked by write"
+
+ let resolved =
+ if String.Equals(owner, board.Owner, StringComparison.OrdinalIgnoreCase) then
+ itemId transport board owner repo number
+ else
+ itemIdCached transport board owner repo number
+
+ match resolved with
+ | Error e -> Error e
+ | Ok None -> Ok NotOnBoard
+ | Ok(Some item) ->
+ match itemBlockedByObservation transport board owner repo number with
+ | Error e -> Error e
+ | Ok observed when observed <> expected ->
+ Error(
+ Malformed(
+ subject,
+ "Stale dependency-edge observation: the Projects item revision or Blocked by edge changed at the board-write boundary; no mutation was sent. Re-derive and retry."
+ )
+ )
+ | Ok _ ->
+ match setField transport board item "Blocked by" write with
+ | Error e -> Error e
+ | Ok() ->
+ Cache.patchScan
+ board.Owner
+ board.Title
+ owner
+ repo
+ number
+ "Blocked by"
+ (match write with
+ | Set value -> value
+ | Clear -> "")
+
+ Ok Written
+
// The batch sibling of `boardWrite` (#448): resolve the item, emit N fields in ONE aliased document,
// and carry the SAME deferral policy the single write does.
//
diff --git a/src/FS.GG.Coord.GitHub/Board.fsi b/src/FS.GG.Coord.GitHub/Board.fsi
index 0b71d2d94..68e617af6 100644
--- a/src/FS.GG.Coord.GitHub/Board.fsi
+++ b/src/FS.GG.Coord.GitHub/Board.fsi
@@ -327,6 +327,19 @@ module Board =
worker: string ->
IoResult
+ /// Write a `Blocked by` value derived from a live set only while the field value and Projects-v2
+ /// item revision still equal `expected`. A mismatch fails stale before mutation. Guarded writes are
+ /// never placed on the unconditional deferred queue because replay would lose the observation bound.
+ val boardWriteGuarded:
+ transport: IGitHubTransport ->
+ board: BoardMap ->
+ owner: string ->
+ repo: string ->
+ number: int ->
+ expected: BlockedByObservation ->
+ write: FieldWrite ->
+ IoResult
+
/// **THE ONE BATCH BOARD WRITE (#448).** `boardWrite` for N fields in one aliased document.
///
/// Carries the SAME deferral policy — an exhausted budget QUEUES every pair, so the whole batch replays
diff --git a/src/FS.GG.Coord.GitHub/Writes.fs b/src/FS.GG.Coord.GitHub/Writes.fs
index 451418889..afa8cfb2c 100644
--- a/src/FS.GG.Coord.GitHub/Writes.fs
+++ b/src/FS.GG.Coord.GitHub/Writes.fs
@@ -368,6 +368,132 @@ module Writes =
| Error(NotFound _) -> Ok()
| Error e -> Error e
+ // GitHub Projects v2 field mutations expose no expected-revision input. A read immediately before
+ // `updateProjectV2ItemFieldValue` therefore cannot be a compare-and-set: another process can write
+ // after that read and before our mutation, and our derived set would erase its edge. Serialize the
+ // complete read/derive/write transaction on REST instead, where comment ids provide the same total
+ // order as the claim CAS. The marker is deliberately ephemeral; a bounded stale marker from a dead
+ // process is collected before a new contender enters.
+ []
+ let private BlockedByLeaseMarker = ""
+ let now () = DateTimeOffset.UtcNow
+ let isStale candidate = now () - candidate.UpdatedAt > TimeSpan.FromMinutes(float BlockedByLeaseMinutes)
+
+ let rec deleteAll candidates =
+ match candidates with
+ | [] -> Ok()
+ | candidate :: rest ->
+ deleteComment transport ref candidate.Id
+ |> Result.bind (fun () -> deleteAll rest)
+
+ let finish myId actionResult =
+ match deleteComment transport ref myId, actionResult with
+ | Ok(), result -> result
+ | Error cleanup, Ok _ ->
+ Error(Malformed(subject, $"the guarded mutation succeeded but lease comment %d{myId} could not be removed (%s{Errors.explain cleanup}); do not retry until the lease expires"))
+ | Error cleanup, Error actionError ->
+ Error(Malformed(subject, $"the guarded mutation failed (%s{Errors.explain actionError}) and lease comment %d{myId} could not be removed (%s{Errors.explain cleanup})"))
+
+ let runIfWinner myId =
+ blockedByLeaseCandidates transport ref
+ |> Result.bind (fun candidates ->
+ let live = candidates |> List.filter (isStale >> not)
+ match live |> List.tryHead with
+ | Some winner when winner.Id = myId -> finish myId (action ())
+ | Some winner ->
+ deleteComment transport ref myId
+ |> Result.bind (fun () -> Error(Malformed(subject, $"Blocked by mutation lease is held by comment %d{winner.Id}; no board mutation was sent")))
+ | None ->
+ deleteComment transport ref myId
+ |> Result.bind (fun () -> Error(Malformed(subject, "our mutation lease candidate disappeared before the authoritative election"))))
+
+ blockedByLeaseCandidates transport ref
+ |> Result.bind (fun initial ->
+ initial
+ |> List.filter isStale
+ |> deleteAll
+ |> Result.bind (fun () -> blockedByLeaseCandidates transport ref)
+ |> Result.bind (fun current ->
+ match current |> List.filter (isStale >> not) |> List.tryHead with
+ | Some winner ->
+ Error(Malformed(subject, $"Blocked by mutation lease is held by comment %d{winner.Id}; no board mutation was sent"))
+ | None ->
+ match postComment transport ref body with
+ | Ok myId -> runIfWinner myId
+ | Error writeError ->
+ blockedByLeaseCandidates transport ref
+ |> Result.bind (fun recovered ->
+ match recovered |> List.tryFind (fun candidate -> candidate.Body = body) with
+ | Some ours -> runIfWinner ours.Id
+ | None -> Error writeError)))
+
let private patchComment
(transport: IGitHubTransport)
(ref: Ref)
diff --git a/src/FS.GG.Coord.GitHub/Writes.fsi b/src/FS.GG.Coord.GitHub/Writes.fsi
index 69b00cfef..602d5655a 100644
--- a/src/FS.GG.Coord.GitHub/Writes.fsi
+++ b/src/FS.GG.Coord.GitHub/Writes.fsi
@@ -100,6 +100,16 @@ module Writes =
body: string ->
Errors.IoResult
+ /// Serialize one complete explicit `Blocked by` read/derive/write transaction using an ephemeral,
+ /// REST-backed comment CAS. Projects v2 field mutations have no expected-revision input, so another
+ /// pre-read is not mutation authority. The lowest live GitHub comment id wins; stale candidates from
+ /// a dead process expire after ten minutes, and every completed contender removes only its own marker.
+ val withBlockedByMutationLease:
+ transport: Transport.IGitHubTransport ->
+ ref: FS.GG.Coord.Types.Ref ->
+ action: (unit -> Errors.IoResult<'value>) ->
+ Errors.IoResult<'value>
+
/// Validate an intake draft before issuing its REST issue-create request. Invalid drafts spend no IO.
val createIntake: transport: Transport.IGitHubTransport -> draft: FS.GG.Coord.Intake.Draft -> Errors.IoResult
diff --git a/tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs b/tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs
index 0f7ed0f58..78bb6bdab 100644
--- a/tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs
+++ b/tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs
@@ -1,7 +1,9 @@
namespace FS.GG.Coord.Cli.Tests
+open System
open System.IO
open System.Text.RegularExpressions
+open System.Text.Json
open Xunit
open FS.GG.Coord
open FS.GG.Coord.Types
@@ -14,6 +16,358 @@ module BlockerLintTests =
let private ref' n : Ref = { Owner = "FS-GG"; Repo = ".github"; Number = n }
+ []
+ let ``#2907 explicit Blocked by flags are mutually exclusive and scoped to set-field`` () =
+ let parsed =
+ Options.parse [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299" ]
+ |> Result.defaultWith failwith
+
+ Assert.Equal(Some(Options.AddBlockedBy "#299"), parsed.BlockedByMutation)
+
+ Assert.True(
+ (Options.parse [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299"; "--remove"; "#290" ]).IsError
+ )
+
+ Assert.True((Options.parse [ "heartbeat"; "FS.GG.SDD#42"; "--clear" ]).IsError)
+
+ []
+ []
+ []
+ []
+ []
+ let ``#2907 lint treats body dependency text as projection only`` (fieldValue: string) (body: string) (expected: bool) =
+ let actual =
+ Client.blockedByBodyProjectionVerdict "FS-GG" ".github" fieldValue body
+ |> Option.isSome
+
+ Assert.Equal(expected, actual)
+
+ module private BlockedBySetMutationFixture =
+
+ let private ok body : Errors.IoResult =
+ Ok { Status = 200; Body = body; ETag = None; NextLink = None; Headers = Map.empty }
+
+ type Outcome =
+ { Code: int
+ Out: string
+ Error: string
+ Mutations: string list
+ IdentityLadderCleared: bool }
+
+ let private identitySessionVars =
+ [ "CLAUDE_CODE_SESSION_ID"
+ "OPENCODE_SESSION_ID"
+ "FSGG_AGENT_SESSION_ID"
+ "FSGG_AGENT_HARNESS" ]
+
+ let private runCore
+ (leaseRaceWinner: bool)
+ (initialValue: string)
+ (secondValue: string)
+ (initialRevision: string)
+ (secondRevision: string)
+ (args: string list) =
+ let mutable observations = 0
+ let mutable leaseReads = 0
+ let mutable leaseBody = ""
+ let mutations = ResizeArray()
+
+ let observation value revision =
+ $"""{{"data":{{"repository":{{"issue":{{"projectItems":{{"totalCount":1,"nodes":[{{"updatedAt":"%s{revision}","project":{{"number":12}},"fieldValueByName":{{"text":"%s{value}"}}}}]}}}}}}}},"rateLimit":{{"cost":1,"remaining":4977}}}}"""
+
+ let transport =
+ Fake.Recorder(fun request ->
+ match request.Method, request.Path.Trim('/'), request.Body with
+ | "GET", "repos/FS-GG/FS.GG.SDD/issues/42/comments", _ ->
+ leaseReads <- leaseReads + 1
+ if leaseReads < 3 then ok "[]"
+ else
+ let now = DateTimeOffset.UtcNow.ToString("o")
+ let ours = {| id = 200L; body = leaseBody; updated_at = now; html_url = "https://fixture/200" |}
+ if leaseRaceWinner then
+ let winner =
+ {| id = 100L
+ body = ""
+ updated_at = now
+ html_url = "https://fixture/100" |}
+ ok (JsonSerializer.Serialize [| winner; ours |])
+ else
+ ok (JsonSerializer.Serialize [| ours |])
+ | "POST", "repos/FS-GG/FS.GG.SDD/issues/42/comments", Json payload ->
+ use document = JsonDocument.Parse payload
+ leaseBody <- document.RootElement.GetProperty("body").GetString()
+ ok """{"id":200}"""
+ | "DELETE", "repos/FS-GG/FS.GG.SDD/issues/comments/200", _ -> ok "{}"
+ | "GET", "rate_limit", _ -> ok """{"resources":{"graphql":{"remaining":4980,"limit":5000}}}"""
+ | "POST", "graphql", Query(document, _) when document.Contains "projectsV2" ->
+ ok """{"data":{"organization":{"projectsV2":{"pageInfo":{"hasNextPage":false,"endCursor":null},"nodes":[{"number":12,"title":"Coordination","id":"PVT_coord"}]}}}}"""
+ | "POST", "graphql", Query(document, _) when document.Contains "fields(first" ->
+ ok """{"data":{"organization":{"projectV2":{"fields":{"totalCount":1,"nodes":[{"id":"PVTF_blocked","name":"Blocked by","dataType":"TEXT"}]}}}},"rateLimit":{"cost":1,"remaining":4977}}"""
+ | "POST", "graphql", Query(document, _) when document.Contains "updatedAt" && document.Contains "fieldValueByName" ->
+ observations <- observations + 1
+ if observations = 1 then ok (observation initialValue initialRevision)
+ else ok (observation secondValue secondRevision)
+ | "POST", "graphql", Query(document, _) when document.Contains "projectItems" && document.Contains "id project" ->
+ ok """{"data":{"repository":{"issue":{"projectItems":{"nodes":[{"id":"PVTI_coord123","project":{"number":12}}]}}}}}"""
+ | "POST", "graphql", Query(document, variables) when document.Contains "updateProjectV2ItemFieldValue" ->
+ let text =
+ variables
+ |> List.tryPick (function | "text", VString value -> Some value | _ -> None)
+ |> Option.defaultValue ""
+ mutations.Add text
+ ok """{"data":{"updateProjectV2ItemFieldValue":{"clientMutationId":null}}}"""
+ | "POST", "graphql", Query(document, _) when document.Contains "clearProjectV2ItemFieldValue" ->
+ mutations.Add ""
+ ok """{"data":{"clearProjectV2ItemFieldValue":{"clientMutationId":null}}}"""
+ | method, path, _ -> Error(Errors.NotFound $"fixture serves no %s{method} %s{path}"))
+
+ let dir = Path.Combine(Path.GetTempPath(), "fsgg-2907-" + Guid.NewGuid().ToString("N"))
+ let previousCache = Environment.GetEnvironmentVariable "FSGG_COORD_CACHE"
+ let previousWorker = Environment.GetEnvironmentVariable "FSGG_WORKER"
+ let previousSessions =
+ identitySessionVars
+ |> List.map (fun variable -> variable, Environment.GetEnvironmentVariable variable)
+ let stdout, stderr = Console.Out, Console.Error
+ use capturedOut = new StringWriter()
+ use capturedErr = new StringWriter()
+
+ try
+ Directory.CreateDirectory dir |> ignore
+ Environment.SetEnvironmentVariable("FSGG_COORD_CACHE", dir)
+ Environment.SetEnvironmentVariable("CLAUDE_CODE_SESSION_ID", null)
+ Environment.SetEnvironmentVariable("OPENCODE_SESSION_ID", null)
+ Environment.SetEnvironmentVariable("FSGG_AGENT_SESSION_ID", null)
+ Environment.SetEnvironmentVariable("FSGG_AGENT_HARNESS", null)
+ Environment.SetEnvironmentVariable("FSGG_WORKER", null)
+ let identityLadderCleared =
+ identitySessionVars
+ |> List.forall (fun variable -> String.IsNullOrEmpty(Environment.GetEnvironmentVariable variable))
+ Console.SetOut capturedOut
+ Console.SetError capturedErr
+ let opts = Options.parse (args @ [ "--worker"; "plover-2907" ]) |> Result.defaultWith failwith
+ let context: Kernel.Context =
+ { Transport = transport; Owner = "FS-GG"; Title = "Coordination"; DefaultRepo = Some "FS.GG.SDD"; ChoreLocks = [] }
+ let code = Handlers.setField context opts
+ Console.Out.Flush()
+ Console.Error.Flush()
+ { Code = code
+ Out = capturedOut.ToString()
+ Error = capturedErr.ToString()
+ Mutations = List.ofSeq mutations
+ IdentityLadderCleared = identityLadderCleared }
+ finally
+ Console.SetOut stdout
+ Console.SetError stderr
+ Environment.SetEnvironmentVariable("FSGG_COORD_CACHE", previousCache)
+ Environment.SetEnvironmentVariable("FSGG_WORKER", previousWorker)
+ for variable, value in previousSessions do
+ Environment.SetEnvironmentVariable(variable, value)
+ try Directory.Delete(dir, true) with _ -> ()
+
+ let run initialValue secondValue initialRevision secondRevision args =
+ runCore false initialValue secondValue initialRevision secondRevision args
+
+ let runContended initialValue secondValue initialRevision secondRevision args =
+ runCore true initialValue secondValue initialRevision secondRevision args
+
+ []
+ let ``#2907 add preserves the existing edge while adding the requested edge`` () =
+ let result =
+ BlockedBySetMutationFixture.run
+ "FS-GG/FS.GG.SDD#290"
+ "FS-GG/FS.GG.SDD#290"
+ "r1"
+ "r1"
+ [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299" ]
+ Assert.Equal(0, result.Code)
+ Assert.Equal([ "FS-GG/FS.GG.SDD#290, FS-GG/FS.GG.SDD#299" ], result.Mutations)
+
+ []
+ let ``#2907 remove preserves every edge not requested`` () =
+ let result =
+ BlockedBySetMutationFixture.run
+ "FS-GG/FS.GG.SDD#290, FS-GG/FS.GG.SDD#299"
+ "FS-GG/FS.GG.SDD#290, FS-GG/FS.GG.SDD#299"
+ "r1"
+ "r1"
+ [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--remove"; "#290" ]
+ Assert.Equal(0, result.Code)
+ Assert.Equal([ "FS-GG/FS.GG.SDD#299" ], result.Mutations)
+
+ []
+ let ``#2907 concurrent revision change fails stale and sends no mutation`` () =
+ let result =
+ BlockedBySetMutationFixture.run
+ "FS-GG/FS.GG.SDD#290"
+ "FS-GG/FS.GG.SDD#290, FS-GG/FS.GG.SDD#777"
+ "r1"
+ "r2"
+ [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299" ]
+ Assert.NotEqual(0, result.Code)
+ Assert.Contains("Stale dependency-edge observation", result.Error)
+ Assert.Empty(result.Mutations)
+
+ []
+ let ``#2907 lower comment-id contender fences the mutation before any edge read or write`` () =
+ let result =
+ BlockedBySetMutationFixture.runContended
+ "FS-GG/FS.GG.SDD#290"
+ "FS-GG/FS.GG.SDD#290"
+ "r1"
+ "r1"
+ [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299" ]
+
+ Assert.NotEqual(0, result.Code)
+ Assert.Contains("lease is held by comment 100", result.Error)
+ Assert.Empty(result.Mutations)
+
+ []
+ let ``#2907 two concurrent writers serialize without losing either edge`` () =
+ let sync = obj ()
+ let bothReadyToPost = new Threading.ManualResetEventSlim(false)
+ let bothPosted = new Threading.ManualResetEventSlim(false)
+ let bothElected = new Threading.ManualResetEventSlim(false)
+ let comments = Collections.Generic.Dictionary()
+ let emptyReadsByThread = Collections.Generic.Dictionary()
+ let mutable nextComment = 100L
+ let mutable readyToPost = 0
+ let mutable postCount = 0
+ let mutable electionReads = 0
+ let mutable edges = Set.empty
+ let mutable actionsInProgress = 0
+ let mutable maximumConcurrentActions = 0
+
+ let response body : Errors.IoResult =
+ Ok { Status = 200; Body = body; ETag = None; NextLink = None; Headers = Map.empty }
+
+ let commentJson () =
+ lock sync (fun () ->
+ let now = DateTimeOffset.UtcNow.ToString("o")
+ comments
+ |> Seq.sortBy (fun entry -> entry.Key)
+ |> Seq.map (fun entry ->
+ {| id = entry.Key
+ body = entry.Value
+ updated_at = now
+ html_url = $"https://fixture/%d{entry.Key}" |})
+ |> Seq.toArray
+ |> JsonSerializer.Serialize)
+
+ let transport =
+ { new IGitHubTransport with
+ member _.Send(request: Request) =
+ match request.Method, request.Path.Trim('/'), request.Body with
+ | "GET", "repos/FS-GG/.github/issues/42/comments", _ ->
+ let mustWaitToPost, json =
+ lock sync (fun () ->
+ let threadId = Threading.Thread.CurrentThread.ManagedThreadId
+ let priorReads =
+ match emptyReadsByThread.TryGetValue threadId with
+ | true, count -> count
+ | _ -> 0
+ let currentReads = priorReads + 1
+ emptyReadsByThread.[threadId] <- currentReads
+ let mustWaitToPost = postCount = 0 && currentReads = 2
+ if mustWaitToPost then
+ readyToPost <- readyToPost + 1
+ if readyToPost = 2 then bothReadyToPost.Set()
+ if postCount >= 2 then
+ electionReads <- electionReads + 1
+ if electionReads >= 2 then bothElected.Set()
+ mustWaitToPost, commentJson ())
+ if mustWaitToPost then Assert.True(bothReadyToPost.Wait(TimeSpan.FromSeconds 10.0), "both contenders did not reach the post boundary")
+ response json
+ | "POST", "repos/FS-GG/.github/issues/42/comments", Json payload ->
+ let id, body, mustWait =
+ use document = JsonDocument.Parse payload
+ let body = document.RootElement.GetProperty("body").GetString()
+ lock sync (fun () ->
+ let id = nextComment
+ nextComment <- nextComment + 1L
+ comments.[id] <- body
+ postCount <- postCount + 1
+ if postCount = 2 then bothPosted.Set()
+ id, body, postCount <= 2)
+ if mustWait then Assert.True(bothPosted.Wait(TimeSpan.FromSeconds 10.0), "both contenders did not post")
+ response (JsonSerializer.Serialize {| id = id; body = body |})
+ | "DELETE", path, _ when path.StartsWith "repos/FS-GG/.github/issues/comments/" ->
+ let id = int64 (path.Substring(path.LastIndexOf('/') + 1))
+ lock sync (fun () -> comments.Remove id |> ignore)
+ response "{}"
+ | method, path, _ -> Error(Errors.NotFound $"race fixture serves no %s{method} %s{path}") }
+
+ let run edge : Errors.IoResult =
+ Writes.withBlockedByMutationLease transport (ref' 42) (fun () ->
+ lock sync (fun () ->
+ actionsInProgress <- actionsInProgress + 1
+ maximumConcurrentActions <- max maximumConcurrentActions actionsInProgress)
+ Assert.True(bothElected.Wait(TimeSpan.FromSeconds 10.0), "both contenders did not complete election reads")
+ lock sync (fun () ->
+ let observed = edges
+ edges <- Set.add edge observed
+ actionsInProgress <- actionsInProgress - 1)
+ Ok())
+
+ let first = Threading.Tasks.Task.Run(fun () -> run "FS-GG/.github#290")
+ let second = Threading.Tasks.Task.Run(fun () -> run "FS-GG/.github#299")
+ Threading.Tasks.Task.WaitAll [| first :> Threading.Tasks.Task; second :> Threading.Tasks.Task |]
+
+ let firstResults = [ first.Result; second.Result ]
+ let firstErrors =
+ firstResults
+ |> List.choose (function | Error error -> Some(Errors.explain error) | Ok() -> None)
+ |> String.concat " | "
+ Assert.True(firstResults |> List.filter Result.isOk |> List.length = 1, firstErrors)
+ Assert.Equal(1, firstResults |> List.filter Result.isError |> List.length)
+ Assert.Equal(1, lock sync (fun () -> edges.Count))
+ Assert.Equal(1, maximumConcurrentActions)
+
+ let losingEdge =
+ if first.Result.IsError then "FS-GG/.github#290" else "FS-GG/.github#299"
+
+ Assert.True((run losingEdge).IsOk)
+ Assert.Equal(Set.ofList [ "FS-GG/.github#290"; "FS-GG/.github#299" ], lock sync (fun () -> edges))
+ Assert.Equal(1, maximumConcurrentActions)
+
+ []
+ let ``#2907 set mutation fixture clears every identity session source before using explicit worker`` () =
+ let sessionVariables =
+ [ "CLAUDE_CODE_SESSION_ID"
+ "OPENCODE_SESSION_ID"
+ "FSGG_AGENT_SESSION_ID"
+ "FSGG_AGENT_HARNESS" ]
+ let previous =
+ sessionVariables
+ |> List.map (fun variable -> variable, Environment.GetEnvironmentVariable variable)
+
+ let cases =
+ [ [ "CLAUDE_CODE_SESSION_ID", "shared-claude-session" ]
+ [ "OPENCODE_SESSION_ID", "per-worker-opencode-session" ]
+ [ "FSGG_AGENT_SESSION_ID", "custom-session"; "FSGG_AGENT_HARNESS", "custom-harness" ]
+ [ "FSGG_AGENT_HARNESS", "orphaned-harness-name" ] ]
+
+ try
+ for poisoned in cases do
+ for variable in sessionVariables do
+ Environment.SetEnvironmentVariable(variable, null)
+ for variable, value in poisoned do
+ Environment.SetEnvironmentVariable(variable, value)
+
+ let result =
+ BlockedBySetMutationFixture.run
+ "FS-GG/FS.GG.SDD#290"
+ "FS-GG/FS.GG.SDD#290"
+ "r1"
+ "r1"
+ [ "set-field"; "FS.GG.SDD#42"; "Blocked by"; "--add"; "#299" ]
+
+ Assert.True(result.IdentityLadderCleared, $"identity ladder was not cleared for %A{poisoned}")
+ Assert.Equal(0, result.Code)
+ Assert.Equal([ "FS-GG/FS.GG.SDD#290, FS-GG/FS.GG.SDD#299" ], result.Mutations)
+ finally
+ for variable, value in previous do
+ Environment.SetEnvironmentVariable(variable, value)
+
/// .github#2698 — a comment ledger holding one current delivery-route receipt for `subject`, as
/// `Reads.commentBodies` reads it. Any fixture whose command RESOLVES a `Status=Ready` write needs
/// one now; a `[]` ledger is the unschedulable-from-birth row the refusal exists to stop.
@@ -73,7 +427,7 @@ module BlockerLintTests =
let directStatusWrites =
Regex.Matches(source, "Board\\.boardWrite[\\s\\S]{0,300}?\\\"Status\\\"").Count
Assert.Equal(4, directStatusWrites)
- Assert.Equal(12, Regex.Matches(source, "Board\\.boardWrite\\b").Count)
+ Assert.Equal(13, Regex.Matches(source, "Board\\.boardWrite\\b").Count)
Assert.Equal(3, Regex.Matches(source, "Board\\.boardWriteBatch\\b").Count)
Assert.Equal(3, Regex.Matches(source, "requireCoherentBlockedWrite ctx").Count)
Assert.Equal(3, Regex.Matches(chore, "Some\\(\\\"Status\\\"").Count)
diff --git a/tests/coord-engine-parity/blockedby_server.py b/tests/coord-engine-parity/blockedby_server.py
index 8e52cc406..ff74fd1f0 100644
--- a/tests/coord-engine-parity/blockedby_server.py
+++ b/tests/coord-engine-parity/blockedby_server.py
@@ -29,6 +29,7 @@
import re
import sys
import threading
+from datetime import datetime, timezone
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
RATE = {"cost": 1, "remaining": 4960}
@@ -47,6 +48,8 @@
LOCK = threading.Lock()
GQL_COUNT = 0 # every POST to /graphql — the `gcount` the corpus reads, one transport over.
WRITES = [] # every field mutation, in order: {field, op: set|clear, text}
+COMMENTS = [] # ephemeral REST-backed Blocked-by mutation leases
+NEXT_COMMENT_ID = 1000
def _is_field_mutation(doc):
@@ -108,6 +111,24 @@ def do_POST(self):
q = body.get("query", "")
a = graphql(q, body.get("variables", {}) or {})
return self._send(200, a if a is not None else {"errors": [{"message": f"unhandled {q[:60]}"}]})
+ if re.match(r"^/repos/[^/]+/[^/]+/issues/(\d+)/comments$", p):
+ global NEXT_COMMENT_ID
+ try:
+ body = json.loads(raw).get("body")
+ except json.JSONDecodeError:
+ body = None
+ if not isinstance(body, str):
+ return self._send(422, {"message": "body required"})
+ with LOCK:
+ comment = {
+ "id": NEXT_COMMENT_ID,
+ "body": body,
+ "updated_at": datetime.now(timezone.utc).isoformat(),
+ "html_url": f"https://fixture/comments/{NEXT_COMMENT_ID}",
+ }
+ NEXT_COMMENT_ID += 1
+ COMMENTS.append(comment)
+ return self._send(201, comment)
self._send(500, {"message": f"unhandled POST {p}"})
def do_GET(self):
@@ -120,6 +141,9 @@ def do_GET(self):
if p.rstrip("/") == "/_writes":
with LOCK:
return self._send(200, {"writes": WRITES, "last": WRITES[-1] if WRITES else None})
+ if re.match(r"^/repos/[^/]+/[^/]+/issues/(\d+)/comments$", p):
+ with LOCK:
+ return self._send(200, list(COMMENTS))
m = re.match(r"^/repos/[^/]+/[^/]+/issues/(\d+)$", p)
if m:
n = int(m.group(1))
@@ -128,6 +152,17 @@ def do_GET(self):
return self._send(200, {"resources": {"graphql": {"remaining": 4960, "limit": 5000}}})
self._send(500, {"message": f"unhandled GET {p}"})
+ def do_DELETE(self):
+ p = self.path.split("?", 1)[0]
+ match = re.match(r"^/repos/[^/]+/[^/]+/issues/comments/(\d+)$", p)
+ if match:
+ comment_id = int(match.group(1))
+ with LOCK:
+ before = len(COMMENTS)
+ COMMENTS[:] = [comment for comment in COMMENTS if comment["id"] != comment_id]
+ return self._send(204, {}) if len(COMMENTS) < before else self._send(404, {"message": "Not Found"})
+ self._send(500, {"message": f"unhandled DELETE {p}"})
+
def main():
s = ThreadingHTTPServer(("127.0.0.1", 0), H)
diff --git a/tests/coord-engine-parity/lint_server.py b/tests/coord-engine-parity/lint_server.py
index 4d9880306..81f570c0d 100644
--- a/tests/coord-engine-parity/lint_server.py
+++ b/tests/coord-engine-parity/lint_server.py
@@ -43,11 +43,14 @@
"""
import json
+import os
import re
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
RATE = {"cost": 1, "remaining": 4988}
+MISSING_BODY = int(os.environ.get("FSGG_LINT_MISSING_BODY", "-1"))
+BODY_READS = []
SDD = "FS-GG/FS.GG.SDD"
GAME = "FS-GG/FS.GG.Game"
@@ -103,6 +106,10 @@ def node(n, status, repo, state="OPEN", title="", severity="Low"):
400: "An umbrella epic.\n\nPaths: none",
422: "Should we do X or Y? A decision.\n\nPaths: none",
407: "Paths: src/Real/**",
+ # The body-projection lint added by .github#2907 reads every open, non-Done row, including work in
+ # progress. Keep #423 ordinary and classed so it isolates that census expansion without adding a
+ # schedulability/class finding of its own.
+ 423: "Paths: src/Real/**\n\nClass: hardening",
# CLASSED, and it has to be: this is the ONLY row in the `--repo game` scope, and case14 asserts that
# scope is clean AND empty. An unclassed row here would make CLASS-UNSET fire and turn the corpus's one
# "a real item, really scanned, and nothing wrong with it" leg into a finding — the assertion would then
@@ -126,7 +133,7 @@ def node(n, status, repo, state="OPEN", title="", severity="Low"):
# reads the board column back with, and refusing it would report a correctly-triaged row as untriaged.
436: "Paths: src/Real/**\n\nClass: Defect ",
437: "Paths: none\n\nClass: hardening",
- # 423 (In progress) and 424 (closed) are NOT candidates, so the engine must never read their bodies.
+ # 424 is closed, so the engine must never read its body.
}
@@ -180,12 +187,15 @@ def do_POST(self):
def do_GET(self):
p = self.path.split("?", 1)[0]
+ if p.rstrip("/") == "/_body_reads":
+ return self._send(200, BODY_READS)
if re.match(r"^/repos/[^/]+/[^/]+/issues/(\d+)/comments$", p):
return self._send(200, [])
m = re.match(r"^/repos/[^/]+/[^/]+/issues/(\d+)$", p)
if m:
n = int(m.group(1))
- if n in BODIES:
+ BODY_READS.append(n)
+ if n in BODIES and n != MISSING_BODY:
return self._send(200, {"number": n, "body": BODIES[n]})
return self._send(404, {"message": "Not Found"})
if p.rstrip("/") == "/rate_limit":
diff --git a/tests/coord-engine-parity/run.sh b/tests/coord-engine-parity/run.sh
index 39c0be41a..d1f3055e5 100755
--- a/tests/coord-engine-parity/run.sh
+++ b/tests/coord-engine-parity/run.sh
@@ -1401,7 +1401,7 @@ sfsrv -- set-field
if [ -z "$SF_PORT" ]; then bad "set-field ref-first fixture bound a port"; else
# a. WRONG IN BOTH WAYS: an unrecognised ref, AND prose in `Blocked by` (which gateField refuses on its
# own — b proves it). Ref-first means the REF refusal is the one the caller is handed.
- r1="$(sfr set-field --worker sf-1032 not-an-issue 'Blocked by' 'the audio team is busy')"; r1rc=$?
+ r1="$(sfr set-field --worker sf-1032 not-an-issue 'Blocked by' --replace 'the audio team is busy')"; r1rc=$?
{ [ "$r1rc" -eq 1 ] && printf '%s' "$r1" | grep -q "unrecognised issue ref 'not-an-issue'"; } \
&& ok "#1032: a bad ref is refused AS A REF — 'unrecognised issue ref', exit 1" \
|| bad "#1032: a bad ref must be refused as a ref" "rc=$r1rc out=$r1"
@@ -1411,7 +1411,7 @@ if [ -z "$SF_PORT" ]; then bad "set-field ref-first fixture bound a port"; else
# b. THE GATE IS ALIVE. The same prose value on a GOOD ref DOES reach the value refusal. This is what
# makes (a) a statement about ORDERING rather than about a value check that never runs.
- r2="$(sfr set-field --worker sf-1032 FS.GG.SDD#42 'Blocked by' 'the audio team is busy')"; r2rc=$?
+ r2="$(sfr set-field --worker sf-1032 FS.GG.SDD#42 'Blocked by' --replace 'the audio team is busy')"; r2rc=$?
{ [ "$r2rc" -eq 1 ] && printf '%s' "$r2" | grep -q "'Blocked by' takes issue refs"; } \
&& ok "#1032: a GOOD ref with the same prose value DOES reach the value refusal — (a) is not vacuous" \
|| bad "#1032: the 'Blocked by' gate must still refuse prose on a good ref" "rc=$r2rc out=$r2"
@@ -1449,7 +1449,7 @@ fi
# #611's fix. Each fixture below runs exactly the ONE command its assertion is about.
sfsrv -- set-field
if [ -z "$SF_PORT" ]; then bad "set-field ref-budget fixture bound a port"; else
- sfr set-field --worker sf-1032 not-an-issue 'Blocked by' 'the audio team is busy' >/dev/null 2>&1
+ sfr set-field --worker sf-1032 not-an-issue 'Blocked by' --replace 'the audio team is busy' >/dev/null 2>&1
[ "$(muts_on "$SF_PORT" | jq -r '.graphql')" = "0" ] \
&& ok "#1032: a refused ref spends ZERO GraphQL — no read is paid for a ref that can never be used" \
|| bad "#1032: a refused ref must not spend the budget the lock lives on" "graphql=$(muts_on "$SF_PORT" | jq -r '.graphql')"
@@ -1460,7 +1460,7 @@ fi
# claims as much. A claim with no leg behind it is the thing this whole section is about, so: pin it.
sfsrv -- set-field
if [ -z "$SF_PORT" ]; then bad "set-field value-gate budget fixture bound a port"; else
- sfr set-field --worker sf-1032 FS.GG.SDD#42 'Blocked by' 'the audio team is busy' >/dev/null 2>&1
+ sfr set-field --worker sf-1032 FS.GG.SDD#42 'Blocked by' --replace 'the audio team is busy' >/dev/null 2>&1
[ "$(muts_on "$SF_PORT" | jq -r '.graphql')" = "0" ] \
&& ok "#1032: the 'Blocked by' value gate spends ZERO GraphQL too — it also precedes the board read" \
|| bad "#1032: a refused value must not spend the budget either" "graphql=$(muts_on "$SF_PORT" | jq -r '.graphql')"
@@ -1942,7 +1942,7 @@ rm -f "$SC_OUT"; rm -rf "$SC_CACHE" "$CO"
# Projects v2 has no dependency field, so `Blocked by` is TEXT. In bash it drifted back into a free-form
# LOG ("RESOLVED: #8 closed, shipped @d80a8ae"), and `.blocked` — which reads the field back as refs —
# could not parse it, so an item the board displayed as blocked reached the scheduler UNBLOCKED. The gate
-# is on the WRITE: `set-field 'Blocked by' ` canonicalizes every accepted form
+# is on the WRITE: `set-field 'Blocked by' --replace ` canonicalizes every accepted form
# (owner/repo#n, repo#n, a bare #n adopting the item's own repo, an issue URL) to one `owner/repo#n`,
# de-dupes refs that canonicalize alike, and REFUSES prose — before any board read, so a refused value
# spends no GraphQL (the budget that dies first). The corpus (case 13 lines 153-243) counts `gh`; this
@@ -1959,35 +1959,35 @@ if [ -z "$BB_PORT" ]; then bad "#480: Blocked by fixture bound a port"; else
BBCACHE="$(mktemp -d)"
# The WRITE path shares one cache — the first write warms bootstrap, each records its mutation.
bbw() { FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$BBCACHE" \
- "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' "$1" 2>&1; }
+ "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' "$@" 2>&1; }
bblast() { bbget "$BB_PORT" /_writes | jq -r "$1"; }
# 1. A full ref passes through, canonical.
- bbw 'FS-GG/FS.GG.SDD#8' >/dev/null
+ bbw --replace 'FS-GG/FS.GG.SDD#8' >/dev/null
[ "$(bblast '.last | "\(.op) \(.field) \(.text)"')" = "set Blocked by FS-GG/FS.GG.SDD#8" ] \
&& ok "Blocked by: a full owner/repo#n ref writes as-is (canonical --text, one transport over)" \
|| bad "Blocked by: full ref passthrough" "$(bblast '.last')"
# 2. A bare #n adopts the BLOCKED item's own repo (SDD#42 -> FS-GG/FS.GG.SDD#33).
- bbw '#33' >/dev/null
+ bbw --replace '#33' >/dev/null
[ "$(bblast '.last.text')" = "FS-GG/FS.GG.SDD#33" ] \
&& ok "Blocked by: a bare #n adopts the blocked item's repo (#33 -> FS-GG/FS.GG.SDD#33)" \
|| bad "Blocked by: bare #n adoption" "$(bblast '.last.text')"
# 3. A LIST canonicalizes every form — a repo#n and an issue URL, in order.
- bbw 'FS.GG.Rendering#33 , https://github.com/FS-GG/FS.GG.Templates/issues/8' >/dev/null
+ bbw --replace 'FS.GG.Rendering#33 , https://github.com/FS-GG/FS.GG.Templates/issues/8' >/dev/null
[ "$(bblast '.last.text')" = "FS-GG/FS.GG.Rendering#33, FS-GG/FS.GG.Templates#8" ] \
&& ok "Blocked by: a list canonicalizes EVERY form (repo#n + URL), in order" \
|| bad "Blocked by: list canonicalization" "$(bblast '.last.text')"
# 4. Refs that canonicalize alike are DE-DUPED — one edge, not two.
- bbw '#8, FS-GG/FS.GG.SDD#8' >/dev/null
+ bbw --replace '#8, FS-GG/FS.GG.SDD#8' >/dev/null
[ "$(bblast '.last.text')" = "FS-GG/FS.GG.SDD#8" ] \
&& ok "Blocked by: refs that canonicalize alike are de-duped (#8 == FS-GG/FS.GG.SDD#8)" \
|| bad "Blocked by: de-dupe" "$(bblast '.last.text')"
# 5. An EMPTY value CLEARS — via the distinct clear mutation, never an empty --text (a no-op on the API).
- bbw '' >/dev/null
+ bbw --clear >/dev/null
[ "$(bblast '.last.op')" = "clear" ] \
&& ok "Blocked by: an empty value CLEARS the field (clearProjectV2ItemFieldValue, not an empty update)" \
|| bad "Blocked by: empty clears" "$(bblast '.last')"
@@ -1996,7 +1996,7 @@ if [ -z "$BB_PORT" ]; then bad "#480: Blocked by fixture bound a port"; else
# cache, so if the gate did NOT fire, bootstrap WOULD hit /graphql: the delta of 0 proves precedence.
before="$(bbget "$BB_PORT" /_gqlcount | jq -r '.count')"
pr_refuse="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" \
- "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' 'RESOLVED: #8 closed, shipped @d80a8ae' 2>&1)"; refrc=$?
+ "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' --replace 'RESOLVED: #8 closed, shipped @d80a8ae' 2>&1)"; refrc=$?
after="$(bbget "$BB_PORT" /_gqlcount | jq -r '.count')"
{ [ "$refrc" -ne 0 ] && [ "$before" = "$after" ]; } \
&& ok "Blocked by: a refused write (a delivery log) is rejected AND spends ZERO GraphQL (validation precedes resolution)" \
@@ -2004,26 +2004,26 @@ if [ -z "$BB_PORT" ]; then bad "#480: Blocked by fixture bound a port"; else
# 7. The delivery log, the inverted edge, and a ref TRAILED by prose are all prose — all refused.
( set +e
- FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' 'blocks FS.GG.Governance#14' >/dev/null 2>&1
+ FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' --replace 'blocks FS.GG.Governance#14' >/dev/null 2>&1
[ $? -ne 0 ] ) \
&& ok "Blocked by: the inverted 'blocks X' edge is refused (wrong direction)" \
|| bad "Blocked by: inverted edge must refuse"
( set +e
- FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' 'FS-GG/FS.GG.SDD#8 (republish vehicle)' >/dev/null 2>&1
+ FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' --replace 'FS-GG/FS.GG.SDD#8 (republish vehicle)' >/dev/null 2>&1
[ $? -ne 0 ] ) \
&& ok "Blocked by: prose TRAILING a valid ref is refused — the anchored match will not swallow it" \
|| bad "Blocked by: trailing prose must refuse"
# 8. The prose refusal REDIRECTS: names Status as the home for 'the item IS blocked'.
- prose_out="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' 'not a ref' 2>&1)"
+ prose_out="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' --replace 'not a ref' 2>&1)"
printf '%s' "$prose_out" | grep -q 'set-field Status Blocked' \
&& ok "Blocked by: the prose refusal names Status as the right home for 'is blocked'" \
|| bad "Blocked by: prose refusal must name Status" "$prose_out"
# 9. A '-'/'none' PLACEHOLDER is a distinct refusal — it points at CLEARING, not at Status.
- ph_out="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' 'none' 2>&1)"; phrc=$?
- { [ "$phrc" -ne 0 ] && printf '%s' "$ph_out" | grep -q "'Blocked by' ''"; } \
- && ok "Blocked by: a '-'/'none' placeholder is refused TOWARD clearing (points at 'Blocked by' '')" \
+ ph_out="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$BB_PORT" FSGG_COORD_CACHE="$(mktemp -d)" "$ENGINE" set-field --worker bb-13 FS.GG.SDD#42 'Blocked by' --replace 'none' 2>&1)"; phrc=$?
+ { [ "$phrc" -ne 0 ] && printf '%s' "$ph_out" | grep -q -- "--clear"; } \
+ && ok "Blocked by: a '-'/'none' placeholder is refused TOWARD the explicit --clear intent" \
|| bad "Blocked by: placeholder must point at clearing" "rc=$phrc out=$ph_out"
# 10. THE GATE IS SCOPED to `Blocked by`. Every other TEXT field stays free-form — Contract takes prose.
@@ -2809,6 +2809,35 @@ if [ -z "$LINT_PORT" ]; then bad "lint fixture bound a port"; else
&& ok "case14: ...and that clean scope produced zero findings (a real item, really scanned)" \
|| bad "case14: --repo game must be clean+empty" "$(lt --repo game --json 2>/dev/null)"
+ # .github#2907 expands the body census to every open, non-Done row because an inert `Blocked by:`
+ # projection can mislead operators even while work is in progress. The normal fixture above proves
+ # #423 is readable. Now remove exactly that body: the server records that production requested it,
+ # and lint must abort rather than emit a confident findings array over a partial census.
+ LINT_MISSING_OUT="$(mktemp)"
+ FSGG_LINT_MISSING_BODY=423 python3 "$HERE/lint_server.py" >"$LINT_MISSING_OUT" 2>/dev/null &
+ LINT_MISSING_SRV=$!; LINT_MISSING_PORT=""
+ for _ in $(seq 1 50); do
+ LINT_MISSING_PORT="$(head -n1 "$LINT_MISSING_OUT" 2>/dev/null)"
+ [ -n "$LINT_MISSING_PORT" ] && break
+ sleep 0.1
+ done
+ rm -f "$LINT_MISSING_OUT"
+ if [ -z "$LINT_MISSING_PORT" ]; then
+ unmeasured "#2907: unreadable open/In-progress body fails closed — fixture did not bind"
+ else
+ lint_missing="$(FSGG_GITHUB_API_BASE="http://127.0.0.1:$LINT_MISSING_PORT" GITHUB_TOKEN=t \
+ FSGG_COORD_OWNER=FS-GG FSGG_COORD_PROJECT=Coordination FSGG_COORD_CACHE="$(mktemp -d)" \
+ "$ENGINE" lint --json 2>&1)"; lint_missing_rc=$?
+ lint_missing_reads="$(curl -s "http://127.0.0.1:$LINT_MISSING_PORT/_body_reads" | jq -c '.')"
+ printf '%s' "$lint_missing_reads" | jq -e 'index(423) != null' >/dev/null \
+ && ok "#2907: production lint requests the open/In-progress #423 body" \
+ || bad "#2907: missing-body control must prove #423 was requested" "$lint_missing_reads"
+ { [ "$lint_missing_rc" -ne 0 ] && ! printf '%s' "$lint_missing" | jq -e 'type == "array"' >/dev/null 2>&1; } \
+ && ok "#2907: an unreadable #423 body aborts lint instead of returning a partial findings array" \
+ || bad "#2907: unreadable body must fail closed with no findings verdict" "rc=$lint_missing_rc out=$lint_missing"
+ kill "$LINT_MISSING_SRV" 2>/dev/null
+ fi
+
# ---- the CLASS axis: CLASS-UNSET (.github#1588) and CLASS-INVALID (.github#1651) ----------------
#
# The SIXTH lint rule, certified HERE on the day it lands. .github#1609 was filed because two rules —
diff --git a/work/2907-blocked-by-set-mutations/charter.md b/work/2907-blocked-by-set-mutations/charter.md
new file mode 100644
index 000000000..26ff3d213
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/charter.md
@@ -0,0 +1,39 @@
+---
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+title: Blocked-by Set Mutations
+stage: charter
+changeTier: tier1
+status: chartered
+policyPointers:
+ - .fsgg/sdd.yml
+ - .fsgg/agents.yml
+ - .fsgg/policy.yml
+ - .fsgg/capabilities.yml
+ - .fsgg/tooling.yml
+---
+
+# Blocked-by Set Mutations Charter
+
+## Identity
+- Work id: `2907-blocked-by-set-mutations`
+- Lifecycle stage: charter
+- Status: chartered
+
+## Principles
+- Treat `Blocked by` as a set whose individual edges are preserved unless the caller explicitly removes them.
+- Bind every derived set mutation to the live Projects-v2 item revision and observed field value; stale observations fail closed.
+- Keep the Projects-v2 `Blocked by` field authoritative. Body text is a human-readable projection and never mutation input.
+- Ship every new write or lint gate with a discriminating control that demonstrates the gate can fail.
+
+## Scope Boundaries
+- Change only the `set-field` command contract, its Projects-v2 write boundary, the inert-body lint rule, and focused tests.
+- Preserve unrelated scalar field writes, explicit clearing, board authority, and existing dependency parsing/canonicalization.
+- Do not infer dependency edges from issue bodies or silently repair a divergent body projection.
+
+## Policy Pointers
+- SDD policy comes from `.fsgg/sdd.yml` and `.fsgg/agents.yml`.
+- Governance files are optional compatibility pointers and are not evaluated by this command.
+
+## Lifecycle Notes
+- Next lifecycle action: `fsgg-sdd specify --work 2907-blocked-by-set-mutations`.
diff --git a/work/2907-blocked-by-set-mutations/checklist.md b/work/2907-blocked-by-set-mutations/checklist.md
new file mode 100644
index 000000000..63ea9211d
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/checklist.md
@@ -0,0 +1,55 @@
+---
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+title: Blocked By Set Mutations
+stage: checklist
+changeTier: tier1
+status: checklistReady
+sourceSpec: work/2907-blocked-by-set-mutations/spec.md
+sourceClarifications: work/2907-blocked-by-set-mutations/clarifications.md
+publicOrToolFacingImpact: true
+---
+
+# Blocked By Set Mutations Checklist
+
+Prose status: checklistReady
+
+## Source Specification
+- work/2907-blocked-by-set-mutations/spec.md
+
+## Source Clarifications
+- work/2907-blocked-by-set-mutations/clarifications.md
+
+## Source Snapshot
+- spec: work/2907-blocked-by-set-mutations/spec.md sha256:bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f schemaVersion:1
+- clarifications: work/2907-blocked-by-set-mutations/clarifications.md sha256:29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5 schemaVersion:1
+
+## Checklist Items
+- CHK-001 [FR-001] [AC-001] blocking: Requirement FR-001 is testable and linked to acceptance coverage.
+- CHK-002 [FR-002] [AC-002] blocking: Requirement FR-002 is testable and linked to acceptance coverage.
+- CHK-003 [FR-003] [AC-001] [AC-002] [AC-003] blocking: Requirement FR-003 is testable and linked to acceptance coverage.
+- CHK-004 [FR-004] [AC-004] blocking: Requirement FR-004 is testable and linked to acceptance coverage.
+- CHK-005 [FR-005] [AC-005] blocking: Requirement FR-005 is testable and linked to acceptance coverage.
+- CHK-006 [FR-006] [AC-006] blocking: Requirement FR-006 is testable and linked to acceptance coverage.
+
+## Review Results
+- CR-001 [CHK:CHK-001] [FR-001] [AC-001] pass: Requirement FR-001 is testable and linked to acceptance coverage.
+- CR-002 [CHK:CHK-002] [FR-002] [AC-002] pass: Requirement FR-002 is testable and linked to acceptance coverage.
+- CR-003 [CHK:CHK-003] [FR-003] [AC-001] [AC-002] [AC-003] pass: Requirement FR-003 is testable and linked to acceptance coverage.
+- CR-004 [CHK:CHK-004] [FR-004] [AC-004] pass: Requirement FR-004 is testable and linked to acceptance coverage.
+- CR-005 [CHK:CHK-005] [FR-005] [AC-005] pass: Requirement FR-005 is testable and linked to acceptance coverage.
+- CR-006 [CHK:CHK-006] [FR-006] [AC-006] pass: Requirement FR-006 is testable and linked to acceptance coverage.
+
+## Accepted Deferrals
+No accepted checklist deferrals recorded.
+
+## Blocking Findings
+No blocking findings recorded.
+
+## Advisory Notes
+No advisory notes recorded.
+
+## Lifecycle Notes
+- Specification requirements reviewed: 6.
+- Clarification decisions reviewed: 4.
+- Next lifecycle action: `fsgg-sdd plan --work 2907-blocked-by-set-mutations`.
diff --git a/work/2907-blocked-by-set-mutations/clarifications.md b/work/2907-blocked-by-set-mutations/clarifications.md
new file mode 100644
index 000000000..0e066104d
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/clarifications.md
@@ -0,0 +1,38 @@
+---
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+title: Blocked-by Set Mutations
+stage: clarify
+changeTier: tier1
+status: clarified
+sourceSpec: work/2907-blocked-by-set-mutations/spec.md
+publicOrToolFacingImpact: true
+---
+
+# Blocked-by Set Mutations Clarifications
+
+## Source Specification
+- work/2907-blocked-by-set-mutations/spec.md
+
+## Clarification Questions
+- CQ-001 [AMB:AMB-001] blocking answered: What public spelling makes add, remove, replace, and clear explicit without weakening the parser's residue rule or changing scalar field writes?
+- CQ-002 [AMB:AMB-002] blocking answered: Where is revision freshness checked when Projects-v2 exposes item `updatedAt` but no native compare-and-set input?
+
+## Answers
+- CQ-001 [AMB:AMB-001] answer: Keep `set-field ][ ` for scalar fields. For `Blocked by`, accept exactly one of `--add `, `--remove `, `--replace `, or `--clear`; refuse the legacy bare positional value with a corrective diagnostic. The existing `--batch` `Field=Value` grammar remains explicitly replacement-shaped by `=`, and `Field=` remains its explicit clear spelling.
+- CQ-002 [AMB:AMB-002] answer: Add/remove first read `{ Value; Revision }`, derive the canonical result, then pass the same observation to the one board-write boundary. That boundary immediately re-reads both facts and refuses stale before emitting the mutation when either differs. GitHub supplies no server-side `If-Match`, so documentation must describe this as the strongest revision guard Projects-v2 exposes, not claim a nonexistent transport-level compare-and-set. Tests must inject a changed second observation and prove mutation count zero.
+
+## Decisions
+- DEC-001 [CQ-001] [AMB:AMB-001] [FR-001] [FR-002] [FR-004] [AC-001] [AC-002] [AC-004]: Add four closed, mutually exclusive option fields and parser flags scoped only to `set-field`. `--add` and `--remove` derive from the live set, `--replace` writes only the supplied canonical set, and `--clear` uses `Board.Clear`. A bare `Blocked by` positional value refuses and names the four valid forms. Scalar fields retain their existing positional contract; batch equality remains an explicitly named replacement/clear operation.
+- DEC-002 [CQ-002] [AMB:AMB-002] [FR-003] [AC-003]: Introduce a guarded single-field board write accepting `BlockedByObservation`. It resolves the item, re-reads the observation immediately before mutation, and emits no mutation on mismatch. The fixture controls the two reads independently and asserts a stale diagnostic plus zero update/clear documents when the second revision or value differs.
+- DEC-003 [FR-005] [AC-005]: Parse only an unfenced, line-leading `Blocked by:` body projection using the same dependency canonicalizer as the field. No body line means no finding. A present line with empty authoritative field yields `BLOCKED-BY-BODY-INERT`; unequal canonical sets or invalid projection syntax yield the same stable rule family with a precise divergent/invalid detail; equal canonical sets stay green.
+- DEC-004 [FR-006] [AC-006]: Mutation controls independently replace union with requested-only, subtraction with empty, and stale comparison with unconditional success; lint controls suppress the body comparison. Each control must red a named focused test, then be restored before the full gates.
+
+## Accepted Deferrals
+No accepted deferrals recorded.
+
+## Remaining Ambiguity
+No blocking ambiguity remains.
+
+## Lifecycle Notes
+- Next lifecycle action: `fsgg-sdd checklist --work 2907-blocked-by-set-mutations`.
diff --git a/work/2907-blocked-by-set-mutations/evidence.yml b/work/2907-blocked-by-set-mutations/evidence.yml
new file mode 100644
index 000000000..c5204394d
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/evidence.yml
@@ -0,0 +1,416 @@
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+stage: evidence
+status: evidenceReady
+sourceSpec: work/2907-blocked-by-set-mutations/spec.md
+sourceClarifications: work/2907-blocked-by-set-mutations/clarifications.md
+sourceChecklist: work/2907-blocked-by-set-mutations/checklist.md
+sourcePlan: work/2907-blocked-by-set-mutations/plan.md
+sourceTasks: work/2907-blocked-by-set-mutations/tasks.yml
+sourceAnalysis: readiness/2907-blocked-by-set-mutations/analysis.json
+sourceSnapshots:
+ - label: analysis
+ path: readiness/2907-blocked-by-set-mutations/analysis.json
+ digest: 3ae6df877929bd095ea483ed27cced27a3a43bcfe0acd2c4ada8c1b5411888ad
+ schemaVersion: 1
+ - label: checklist
+ path: work/2907-blocked-by-set-mutations/checklist.md
+ digest: 2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95
+ schemaVersion: 1
+ - label: clarifications
+ path: work/2907-blocked-by-set-mutations/clarifications.md
+ digest: 29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5
+ schemaVersion: 1
+ - label: plan
+ path: work/2907-blocked-by-set-mutations/plan.md
+ digest: 243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d
+ schemaVersion: 1
+ - label: spec
+ path: work/2907-blocked-by-set-mutations/spec.md
+ digest: bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f
+ schemaVersion: 1
+ - label: tasks
+ path: work/2907-blocked-by-set-mutations/tasks.yml
+ digest: ae85b8835c2a835841d0d3ad0afbb741d36a51b524397d083b88b34203a8c540
+ schemaVersion: 1
+evidence:
+ - id: EV001
+ kind: verification
+ subject:
+ type: task
+ id: T001
+ taskRefs: [T001]
+ requirementRefs: [FR-001]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-001]
+ obligationRefs: [EV001]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV002
+ kind: verification
+ subject:
+ type: task
+ id: T002
+ taskRefs: [T002]
+ requirementRefs: [FR-002]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-002]
+ obligationRefs: [EV002]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV003
+ kind: verification
+ subject:
+ type: task
+ id: T003
+ taskRefs: [T003]
+ requirementRefs: [FR-003]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-003]
+ obligationRefs: [EV003]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV004
+ kind: verification
+ subject:
+ type: task
+ id: T004
+ taskRefs: [T004]
+ requirementRefs: [FR-004]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-004]
+ obligationRefs: [EV004]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV005
+ kind: verification
+ subject:
+ type: task
+ id: T005
+ taskRefs: [T005]
+ requirementRefs: [FR-005]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-005]
+ obligationRefs: [EV005]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV006
+ kind: verification
+ subject:
+ type: task
+ id: T006
+ taskRefs: [T006]
+ requirementRefs: [FR-006]
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-006]
+ obligationRefs: [EV006]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV007
+ kind: verification
+ subject:
+ type: task
+ id: T007
+ taskRefs: [T007]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: []
+ obligationRefs: [EV007]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV008
+ kind: verification
+ subject:
+ type: task
+ id: T008
+ taskRefs: [T008]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: []
+ obligationRefs: [EV008]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV009
+ kind: verification
+ subject:
+ type: task
+ id: T009
+ taskRefs: [T009]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: []
+ obligationRefs: [EV009]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV010
+ kind: verification
+ subject:
+ type: task
+ id: T010
+ taskRefs: [T010]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: []
+ obligationRefs: [EV010]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV011
+ kind: verification
+ subject:
+ type: task
+ id: T011
+ taskRefs: [T011]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-001, PD-004]
+ obligationRefs: [EV011]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV012
+ kind: verification
+ subject:
+ type: task
+ id: T012
+ taskRefs: [T012]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-001, PD-002, PD-003, PD-004, PD-005, PD-006]
+ obligationRefs: [EV012]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV013
+ kind: verification
+ subject:
+ type: task
+ id: T013
+ taskRefs: [T013]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: []
+ obligationRefs: [EV013]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+ - id: EV014
+ kind: verification
+ subject:
+ type: task
+ id: T014
+ taskRefs: [T014]
+ requirementRefs: []
+ acceptanceScenarioRefs: []
+ clarificationDecisionRefs: []
+ checklistResultRefs: []
+ planDecisionRefs: [PD-001]
+ obligationRefs: [EV014]
+ artifacts: [src/FS.GG.Coord.Cli.BoardOps/Handlers.fs, src/FS.GG.Coord.Cli.Kernel/Options.fs, src/FS.GG.Coord.Cli/LintApplication.fs, src/FS.GG.Coord.GitHub/Board.fs, tests/FS.GG.Coord.Cli.BoardOps.Tests/BlockerLintTests.fs, work/2907-blocked-by-set-mutations/verification/verification-evidence.md]
+ sourceRefs:
+ - kind: verification
+ path: tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj
+ result: pass
+ synthetic: false
+ observedRun:
+ source: readiness/2907-blocked-by-set-mutations/test-results/boardops.trx
+ digest: "sha256:52d675d5191ed3ecc2eca8f68c16ac5cdecf53b48fe4be444878366dc7a49c1c"
+ digestContract: exact-bytes-v1
+ outcome: passed
+ passed: 257
+ failed: 0
+ skipped: 0
+ notes: [The observed BoardOps receipt covers parser, "and inert-body lint controls; verification-evidence.md records four independently observed red inversions and the complete affected-project gate set.", set-mutation, stale-write]
+lifecycleNotes:
+ - "Next lifecycle action: verify."
diff --git a/work/2907-blocked-by-set-mutations/plan.md b/work/2907-blocked-by-set-mutations/plan.md
new file mode 100644
index 000000000..c2a890663
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/plan.md
@@ -0,0 +1,62 @@
+---
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+title: Blocked By Set Mutations
+stage: plan
+changeTier: tier1
+status: planned
+sourceSpec: work/2907-blocked-by-set-mutations/spec.md
+sourceClarifications: work/2907-blocked-by-set-mutations/clarifications.md
+sourceChecklist: work/2907-blocked-by-set-mutations/checklist.md
+publicOrToolFacingImpact: true
+---
+
+# Blocked By Set Mutations Plan
+
+Prose status: planned
+
+## Source Snapshot
+- spec: work/2907-blocked-by-set-mutations/spec.md sha256:bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f schemaVersion:1
+- clarifications: work/2907-blocked-by-set-mutations/clarifications.md sha256:29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5 schemaVersion:1
+- checklist: work/2907-blocked-by-set-mutations/checklist.md sha256:2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95 schemaVersion:1
+
+## Plan Scope
+- Work item 2907-blocked-by-set-mutations is planned from the current specification, clarification, and checklist facts.
+- Requirement count: 6.
+- Clarification decision count: 4.
+- Checklist result count: 6.
+
+## Plan Decisions
+- PD-001 [AC-001] [FR-001] complete: Extend `Options` with mutually exclusive `--add`, `--remove`, `--replace`, and `--clear` intents scoped to `set-field`; in `Handlers.setField`, read and canonicalize the live set, union requested additions in stable canonical order, and issue the derived write through the guarded boundary.
+- PD-002 [AC-002] [FR-002] complete: Subtract canonical requested refs from the observed set while retaining every other edge; select `Board.Clear` only when subtraction produces an empty set. Removing an absent edge remains an idempotent guarded write outcome, not replacement of the whole set.
+- PD-003 [AC-001] [AC-002] [AC-003] [FR-003] complete: Add `Board.boardWriteGuarded` beside the existing chokepoint. It resolves the item, re-reads `{Value; Revision}`, refuses any mismatch as stale, emits the derived single-field mutation only after a match, and never places guarded writes on the unconditional deferred queue.
+- PD-004 [AC-004] [FR-004] complete: For `Blocked by`, refuse the legacy positional-value form and require one explicit intent. `--replace` uses canonical replacement and `--clear` maps directly to `Board.Clear`; scalar fields retain their positional value/empty-clear behavior and batch `Field=Value` remains explicitly replacement-shaped.
+- PD-005 [AC-005] [FR-005] complete: Put the deterministic body/field comparison in `LintApplication`, expose it through the existing compatibility seam, and invoke it during the fresh lint census. The body is projection-only: the rule reports inert, divergent, or invalid text and never creates an edge or a reconcile chore.
+- PD-006 [AC-006] [FR-006] complete: Add parser/residue and command-handler tests, Board transport tests with independently controlled observations, and lint verdict plus application-route tests. Capture four bounded inversions and then run BoardOps, GitHub, CLI, full solution, formatting, projections, and SDD analyze gates.
+
+## Contract Impact
+- PC-001 [PD-001] [PD-004] command report: `set-field` advertises four explicit `Blocked by` mutation intents. Existing bare `Blocked by` replacement is intentionally refused with migration guidance; unrelated scalar writes and batch equality syntax remain compatible. `lint` adds stable `BLOCKED-BY-BODY-INERT` diagnostics without changing JSON schema.
+
+## Verification Obligations
+- VO-001 [PD-001] [PD-002] [PD-003] [PD-004] [PD-005] [PD-006] [PC-001] semanticTest: Run focused parser, BoardOps, GitHub Board, and lint application tests; demonstrate red/green inversions for union preservation, subtraction preservation, stale refusal, and lint comparison; then run all affected projects, the solution, formatting, signature/projection generation checks, and `fsgg-sdd analyze`.
+
+## Performance Intent
+No performance intent is declared for this work item.
+
+## Migration Posture
+- PM-001 [PC-001] compatible: Scalar and batch callers need no migration. Single-field `Blocked by` callers replace the bare positional value with `--replace`, use `--add`/`--remove` for edge-local intent, and use `--clear` for clearing; the refusal prints these exact remedies.
+
+## Generated View Impact
+- GV-001 [PD-001] workModel: Regenerate `readiness/2907-blocked-by-set-mutations/work-model.json` and `analysis.json` only through `fsgg-sdd tasks`/`analyze`; regenerate checked-in command/protocol projections through the repository generator if its check reports drift.
+
+## Accepted Deferrals
+No accepted plan deferrals recorded.
+
+## Planning Findings
+No blocking planning findings recorded.
+
+## Advisory Notes
+- The lint application boundary lives in `src/FS.GG.Coord.Cli/LintApplication.{fs,fsi}` and its invocation/compatibility surface in `Client.{fs,fsi}`, which were absent from the filed touch-set. Widen those exact paths before editing and stop on overlap.
+
+## Lifecycle Notes
+- Next lifecycle action: `fsgg-sdd tasks --work 2907-blocked-by-set-mutations`.
diff --git a/work/2907-blocked-by-set-mutations/spec.md b/work/2907-blocked-by-set-mutations/spec.md
new file mode 100644
index 000000000..3bc7515a3
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/spec.md
@@ -0,0 +1,54 @@
+---
+schemaVersion: 1
+workId: 2907-blocked-by-set-mutations
+title: Blocked-by Set Mutations
+stage: specify
+changeTier: tier1
+status: specified
+publicOrToolFacingImpact: true
+---
+
+# Blocked-by Set Mutations Specification
+
+Prose status: specified
+
+## User Value
+Operators can add or remove one dependency edge without overwriting concurrent edges, and lint diagnoses inert body-only declarations.
+
+## Scope
+- SB-001: Explicit set-valued Blocked by add/remove/replace/clear CLI semantics, revision-bound writes, lint projection diagnostics, and focused mutation-proven tests.
+
+## Non-Goals
+- SB-002: Changing the Projects v2 field as dependency authority or changing unrelated board fields.
+
+## User Stories
+- US-001 (P1): An operator safely mutates one blocker while preserving every other live blocker.
+- US-002 (P1): A maintainer sees body-only or divergent Blocked by projections in lint.
+
+## Acceptance Scenarios
+- AC-001 [US-001] [FR-001] [FR-003]: Given live `Blocked by` contains `FS-GG/.github#290`, when an operator explicitly adds `FS-GG/.github#299` against the observed item revision, then the resulting value contains both canonical edges.
+- AC-002 [US-001] [FR-002] [FR-003]: Given live `Blocked by` contains `#290, #299`, when an operator explicitly removes `#290` against the observed item revision, then `#299` remains and only `#290` is removed.
+- AC-003 [US-001] [FR-003]: Given the field value or Projects item revision changes after the mutation derives its set, when the write boundary validates the observation, then it reports stale and emits no field mutation.
+- AC-004 [US-001] [FR-004]: Given a caller targets `Blocked by`, when it requests replacement or clearing, then replacement is explicitly named and clearing uses the distinct clear operation; an ambiguous bare replacement is refused before mutation.
+- AC-005 [US-002] [FR-005]: Given a body contains a live `Blocked by:` line, when the authoritative board field is empty or differs, then lint reports the inert or divergent projection; when canonical sets agree, lint remains green for that rule.
+- AC-006 [US-001] [US-002] [FR-006]: Given each new gate is inverted independently, when focused suites run against the bounded fixture, then the relevant add-preservation, remove-preservation, stale-write, or lint-projection test turns red and returns green after restoration.
+
+## Functional Requirements
+- FR-001: `Blocked by` add canonicalizes and unions requested refs with the live canonical set, removes duplicates, and preserves every existing edge. (Stories: US-001; Acceptance: AC-001)
+- FR-002: `Blocked by` remove canonicalizes and subtracts requested refs from the live canonical set, preserves every non-requested edge, and uses the distinct clear mutation when the result is empty. (Stories: US-001; Acceptance: AC-002)
+- FR-003: Add and remove bind both the live field value and Projects-v2 item revision; a changed value or revision at the guarded write boundary fails stale and emits no mutation. (Stories: US-001; Acceptance: AC-001, AC-002, AC-003)
+- FR-004: The command names add, remove, replace, and clear as distinct caller intents for `Blocked by`; an ambiguous bare replacement is refused while unrelated scalar-field syntax remains compatible. (Stories: US-001; Acceptance: AC-004)
+- FR-005: Lint reports a body-only `Blocked by:` declaration when the authoritative field is empty and a divergent projection when their canonical sets differ, while an agreeing generated projection stays green. Invalid body syntax is reported rather than promoted to authority. (Stories: US-002; Acceptance: AC-005)
+- FR-006: Focused tests and gate inversion prove add preserves `#290` while adding `#299`, remove preserves `#299` while removing `#290`, a changed observation is never overwritten, and lint distinguishes inert, divergent, invalid, absent, and agreeing body projections. (Stories: US-001, US-002; Acceptance: AC-006)
+
+## Ambiguities
+- AMB-001: The public spelling for the four explicit `Blocked by` intents must preserve the existing parser's total flag-residue checks and avoid changing unrelated fields.
+- AMB-002: GitHub Projects-v2 does not expose an `If-Match` argument on field mutation; the plan must state exactly where the revision is revalidated and must not describe a read/write window as stronger authority than it provides.
+
+## Public Or Tool-Facing Impact
+- `set-field` gains explicit set-mutation syntax and updated help/command-contract documentation.
+- `lint` gains a stable finding code for inert or divergent body projections.
+- No serialized board field or SDD schema changes.
+
+## Lifecycle Notes
+- Next lifecycle action: `fsgg-sdd clarify --work 2907-blocked-by-set-mutations`.
diff --git a/work/2907-blocked-by-set-mutations/tasks.yml b/work/2907-blocked-by-set-mutations/tasks.yml
new file mode 100644
index 000000000..bf9b61b72
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/tasks.yml
@@ -0,0 +1,176 @@
+schemaVersion: 1
+work:
+ id: 2907-blocked-by-set-mutations
+ title: "Blocked By Set Mutations"
+ stage: tasks
+ status: tasksReady
+ sourceSpec: work/2907-blocked-by-set-mutations/spec.md
+ sourceClarifications: work/2907-blocked-by-set-mutations/clarifications.md
+ sourceChecklist: work/2907-blocked-by-set-mutations/checklist.md
+ sourcePlan: work/2907-blocked-by-set-mutations/plan.md
+ publicOrToolFacingImpact: true
+
+sources:
+ - label: spec
+ path: work/2907-blocked-by-set-mutations/spec.md
+ digest: bbfc8ac46a0af4e5487e6e08af4603eb9e97d26877c78774d4f96d892930802f
+ schemaVersion: 1
+ - label: clarifications
+ path: work/2907-blocked-by-set-mutations/clarifications.md
+ digest: 29371de9aa66598f033214d6fd42665dac6d53189f32f572cdd1327bb6537bc5
+ schemaVersion: 1
+ - label: checklist
+ path: work/2907-blocked-by-set-mutations/checklist.md
+ digest: 2799d51aeaf8b672cd72d857c78bd907426e73a20a8b7c8cda8ea1ad2c71fe95
+ schemaVersion: 1
+ - label: plan
+ path: work/2907-blocked-by-set-mutations/plan.md
+ digest: 243f8da1ca1d80b697dcbd4c9359371b98f08f39961ec7bb1c363424b198288d
+ schemaVersion: 1
+tasks:
+ - id: T001
+ title: Implement requirement FR-001
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-001]
+ decisions: []
+ sourceIds: [AC-001, FR-001, PD-001]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV001]
+ - id: T002
+ title: Implement requirement FR-002
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-002]
+ decisions: []
+ sourceIds: [AC-002, FR-002, PD-002]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV002]
+ - id: T003
+ title: Implement requirement FR-003
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-003]
+ decisions: []
+ sourceIds: [AC-001, AC-002, AC-003, FR-003, PD-003]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV003]
+ - id: T004
+ title: Implement requirement FR-004
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-004]
+ decisions: []
+ sourceIds: [AC-004, FR-004, PD-004]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV004]
+ - id: T005
+ title: Implement requirement FR-005
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-005]
+ decisions: []
+ sourceIds: [AC-005, FR-005, PD-005]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV005]
+ - id: T006
+ title: Implement requirement FR-006
+ status: pending
+ owner: sdd
+ dependencies: []
+ requirements: [FR-006]
+ decisions: []
+ sourceIds: [AC-006, FR-006, PD-006]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV006]
+ - id: T007
+ title: Implement clarification decision DEC-001
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: [DEC-001]
+ sourceIds: [DEC-001]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV007]
+ - id: T008
+ title: Implement clarification decision DEC-002
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: [DEC-002]
+ sourceIds: [DEC-002]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV008]
+ - id: T009
+ title: Implement clarification decision DEC-003
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: [DEC-003]
+ sourceIds: [DEC-003]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV009]
+ - id: T010
+ title: Implement clarification decision DEC-004
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: [DEC-004]
+ sourceIds: [DEC-004]
+ requiredSkills: [fsharp, implementation]
+ requiredEvidence: [EV010]
+ - id: T011
+ title: Update contract surface PC-001
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: []
+ sourceIds: [PC-001, PD-001, PD-004]
+ requiredSkills: [fsharp]
+ requiredEvidence: [EV011]
+ - id: T012
+ title: Record verification evidence VO-001
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: []
+ sourceIds: [PC-001, PD-001, PD-002, PD-003, PD-004, PD-005, PD-006, VO-001]
+ requiredSkills: [automated-tests, readiness-evidence]
+ requiredEvidence: [EV012]
+ - id: T013
+ title: Handle migration posture PM-001
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: []
+ sourceIds: [PC-001, PM-001]
+ requiredSkills: [schema-versioning]
+ requiredEvidence: [EV013]
+ - id: T014
+ title: Refresh generated view impact GV-001
+ status: pending
+ owner: sdd
+ dependencies: [T001]
+ requirements: []
+ decisions: []
+ sourceIds: [GV-001, PD-001]
+ requiredSkills: [deterministic-json]
+ requiredEvidence: [EV014]
+acceptedDeferrals: []
+findings: []
+advisoryNotes:
+ - "Optional Governance pointers remain compatibility facts only."
+lifecycleNotes:
+ - "Next lifecycle action: fsgg-sdd analyze --work 2907-blocked-by-set-mutations."
diff --git a/work/2907-blocked-by-set-mutations/verification/verification-evidence.md b/work/2907-blocked-by-set-mutations/verification/verification-evidence.md
new file mode 100644
index 000000000..e3935adb1
--- /dev/null
+++ b/work/2907-blocked-by-set-mutations/verification/verification-evidence.md
@@ -0,0 +1,57 @@
+# Verification evidence
+
+## Positive controls
+
+- `dotnet test tests/FS.GG.Coord.Cli.BoardOps.Tests/FS.GG.Coord.Cli.BoardOps.Tests.fsproj --no-restore`: 257 passed, 0 failed, 0 skipped.
+- `dotnet test tests/FS.GG.Coord.GitHub.Tests/FS.GG.Coord.GitHub.Tests.fsproj --no-restore`: 671 passed, 0 failed, 0 skipped.
+- `dotnet test tests/FS.GG.Coord.Cli.Kernel.Tests/FS.GG.Coord.Cli.Kernel.Tests.fsproj --no-restore`: 182 passed, 0 failed, 0 skipped.
+- The Lifecycle, CLI, and Core test project commands each exited zero.
+- The durable TRX at `readiness/2907-blocked-by-set-mutations/test-results/boardops.trx` records all 257 BoardOps tests passing.
+- `tests/coord-engine-parity/run.sh` passed the explicit replace/clear, ref-first, zero-GraphQL refusal, canonicalization, de-duplication, and scoped-field controls after its legacy positional calls were migrated.
+- After the initial critic identified a missing hosted body for open/In-progress item #423, the repaired
+ serialized parity run passed 616/616 assertions with zero failures and zero not-measured results. Its
+ negative control removes only #423's body, proves production requested that body, and proves lint
+ aborts rather than returning a partial JSON findings array.
+- After the successor pass, hosted `harness-identity / ladder-decided` exposed an ambient-session leak in
+ `BlockedBySetMutationFixture.run`. The round-2 repair clears all four variables currently read by
+ `Identity.resolve`, uses an explicit `--worker`, and exercises each poisoned source independently.
+ `python3 scripts/check-harness-identity.py --root .` passed its 111-shell/104-F# census, the focused
+ BlockerLint selection passed 36/36, and `tests/harness-identity/run.sh` passed 14/14 mutation controls.
+- The round-3 repair serializes the complete Blocked-by observation/derive/guarded-write transaction
+ behind GitHub's issue-comment ticket order. A two-task control forces both contenders through their
+ empty pre-censuses, posts both tickets, and holds the winner until both election reads complete. One
+ first attempt executes, the loser executes no action, and retry leaves both requested edges present;
+ maximum concurrent actions remains one. The focused `#2907` selection passed 11/11.
+- A clean serialized parity run passed 616/616 with zero not-measured, and the production write harness
+ passed 180/186. Its six failures are the pre-existing #2133 validator-version mismatch: installed
+ `fsgg-sdd` reports 1.1.0 while those controls accept only 1.0.0; all other write assertions passed.
+- `scripts/generate-projections --check` reported every projection current; the signature-doc mutation sweep killed 435/435 mutants.
+- The Release engine build and the 12-entry deterministic package check exited zero.
+- Recovery validation after rebasing onto `origin/main` at `712f0257c15b8027432bbf7d4c1ea3df9b643105`
+ rebuilt the Release CLI with zero warnings/errors, passed BoardOps 257/257 and GitHub 671/671,
+ and confirmed `scripts/generate-projections --check` current. Regenerating the lifecycle views with
+ `fsgg-sdd` 1.2.5 required a second `analyze` pass after the first refreshed `work-model.json`; the
+ second pass returned `implementationReady`, `verify` returned `verificationReady` with all 14
+ obligations backed by observed evidence, and `ship` returned `shipReady`.
+
+## Gate inversions
+
+Each bounded mutation was applied alone, the focused test was observed red, and the production implementation was restored before the positive controls:
+
+1. Add-set derivation was changed from union with the observed set to requested-only. The add-preservation control failed: expected `#290, #299`, actual `#299`.
+2. Remove-set derivation was forced to clear. The remove-preservation control failed: expected `#299`, actual ``.
+3. The guarded-write revision/value match was forced false. The stale-observation control failed because the command returned zero and the transport recorded a mutation.
+4. The inert-body verdict was suppressed with `Ok None`. The lint theory failed because the divergent body case expected a finding and observed none.
+5. The production mutation-lease election was reversed from lowest to highest comment id. The lower-id
+ contender control failed because the command returned zero instead of fencing our higher-id writer.
+
+These inversions discriminate union, subtraction, stale-write refusal, body-projection linting, and
+server-ordered mutation fencing independently.
+
+## Runtime controls
+
+- Parser controls cover all four explicit intents, mutual exclusion, and rejection outside `set-field`.
+- Legacy positional `Blocked by` replacement is refused before transport with the four explicit remedies; the parity fixture uses `--replace` and `--clear` and proves malformed explicit values spend zero GraphQL.
+- Handler transport controls independently vary the first observation and guarded re-observation, assert the derived field mutation, and assert zero mutation on stale data.
+- Lint controls distinguish absent/equal projection from empty, divergent, duplicate, and invalid body text; fenced examples remain ignored.
+- The body lint is diagnostic-only and never feeds a board mutation route.
]